a digital person for bluesky

Fix feed tool to be fully self-contained for sandboxing

- Move FEED_PRESETS dictionary inside function body
- Tools must be completely self-contained with no external dependencies
- Update Pydantic schema to use hardcoded list instead of dynamic reference

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+12 -13
tools
+12 -13
tools/feed.py
··· 3 from typing import Optional, Union 4 5 6 - # Predefined feed mappings 7 - FEED_PRESETS = { 8 - "home": None, # Home timeline (default) 9 - "discover": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 10 - "ai-for-grownups": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.generator/ai-for-grownups", 11 - "atmosphere": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.generator/the-atmosphere" 12 - } 13 - 14 - 15 class FeedArgs(BaseModel): 16 - feed_name: Optional[str] = Field(None, description=f"Named feed preset: {list(FEED_PRESETS.keys())}. If not provided, returns home timeline") 17 feed_uri: Optional[str] = Field(None, description="Custom feed URI (e.g., 'at://did:plc:abc/app.bsky.feed.generator/feed-name'). Overrides feed_name if provided") 18 max_posts: int = Field(default=25, description="Maximum number of posts to retrieve (max 100)") 19 ··· 35 import requests 36 37 try: 38 # Validate inputs 39 max_posts = min(max_posts, 100) 40 ··· 45 feed_display_name = feed_uri.split('/')[-1] if '/' in feed_uri else feed_uri 46 elif feed_name: 47 # Look up named preset 48 - if feed_name not in FEED_PRESETS: 49 - available_feeds = list(FEED_PRESETS.keys()) 50 raise Exception(f"Unknown feed name '{feed_name}'. Available feeds: {available_feeds}") 51 - resolved_feed_uri = FEED_PRESETS[feed_name] 52 feed_display_name = feed_name 53 else: 54 # Default to home timeline
··· 3 from typing import Optional, Union 4 5 6 class FeedArgs(BaseModel): 7 + feed_name: Optional[str] = Field(None, description="Named feed preset: ['home', 'discover', 'ai-for-grownups', 'atmosphere']. If not provided, returns home timeline") 8 feed_uri: Optional[str] = Field(None, description="Custom feed URI (e.g., 'at://did:plc:abc/app.bsky.feed.generator/feed-name'). Overrides feed_name if provided") 9 max_posts: int = Field(default=25, description="Maximum number of posts to retrieve (max 100)") 10 ··· 26 import requests 27 28 try: 29 + # Predefined feed mappings (must be inside function for sandboxing) 30 + feed_presets = { 31 + "home": None, # Home timeline (default) 32 + "discover": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 33 + "ai-for-grownups": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.generator/ai-for-grownups", 34 + "atmosphere": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.generator/the-atmosphere" 35 + } 36 + 37 # Validate inputs 38 max_posts = min(max_posts, 100) 39 ··· 44 feed_display_name = feed_uri.split('/')[-1] if '/' in feed_uri else feed_uri 45 elif feed_name: 46 # Look up named preset 47 + if feed_name not in feed_presets: 48 + available_feeds = list(feed_presets.keys()) 49 raise Exception(f"Unknown feed name '{feed_name}'. Available feeds: {available_feeds}") 50 + resolved_feed_uri = feed_presets[feed_name] 51 feed_display_name = feed_name 52 else: 53 # Default to home timeline