Write me a tool to pull my bookmarked posts from X. It should call a secrets store to get the keys rather than env vars
I'll create a tool that pulls your X (Twitter) bookmarks using a secrets store for authentication. This keeps keys out of environment variables and agent reach.
Python
复制
"""
X Bookmarks Tool - Secure bookmark retrieval for agents.
Uses a secrets store (e.g., AWS Secrets Manager, HashiCorp Vault, 1Password)
instead of environment variables for credential management.
"""
import json
import requests
from typing import List, Dict, Optional, Protocol
from dataclasses import dataclass
from datetime import datetime
class SecretsStore(Protocol):
"""Protocol for secrets store implementations."""
def get_secret(self, secret_name: str) -> Dict[str, str]: ...
@dataclass
class XPost:
"""Sanitized post data safe to pass to agents."""
id: str
text: str
author_username: str
created_at: datetime
like_count: int
bookmarked_at: Optional[datetime] = None
def to_dict(self) -> Dict:
return {
"id": self.id,
"text": self.text,
"author": self.author_username,
"created": self.created_at.isoformat(),
"likes": self.like_count,
"saved": self.bookmarked_at.isoformat() if self.bookmarked_at else None
}
class AWSSecretsManager:
"""AWS Secrets Manager implementation."""
def __init__(self, region: str = "us-east-1"):
import boto3
self.client = boto3.client("secretsmanager", region_name=region)
def get_secret(self, secret_name: str) -> Dict[str, str]:
response = self.client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
class HashiCorpVault:
"""HashiCorp Vault implementation."""
def __init__(self, vault_addr: str, role_id: str, secret_id: str):
self.vault_addr = vault_addr.rstrip("/")
# Authenticate with AppRole
auth_resp = requests.post(
f"{self.vault_addr}/v1/auth/approle/login",
json={"role_id": role_id, "secret_id": secret_id}
)
self.token = auth_resp.json()["auth"]["client_token"]
def get_secret(self, path: str) -> Dict[str, str]:
headers = {"X-Vault-Token": self.token}
response = requests.get(
f"{self.vault_addr}/v1/secret/data/{path}",
headers=headers
)
return response.json()["data"]["data"]
class OnePasswordConnect:
"""1Password Connect Server implementation."""
def __init__(self, connect_host: str, connect_token: str):
self.host = connect_host.rstrip("/")
self.headers = {"Authorization": f"Bearer {connect_token}"}
def get_secret(self, item_uuid: str) -> Dict[str, str]:
response = requests.get(
f"{self.host}/v1/items/{item_uuid}",
headers=self.headers
)
item = response.json()
# Extract fields from 1Password item structure
fields = {f["label"]: f["value"] for f in item.get("fields", [])}
return fields
class XBookmarksTool:
"""
Tool for fetching X/Twitter bookmarks.
Agents interact with this, never with X API directly.
"""
X_API_BASE = "https://api.twitter.com/2"
def __init__(
self,
secrets_store: SecretsStore,
secret_path: str = "x-api-credentials",
max_results: int = 100
):
self.secrets = secrets_store
self.secret_path = secret_path
self.max_results = max_results
self._credentials: Optional[Dict] = None
def _load_credentials(self) -> Dict[str, str]:
"""Lazy load credentials from secrets store."""
if self._credentials is None:
self._credentials = self.secrets.get_secret(self.secret_path)
required = {"bearer_token", "api_key", "api_secret"}
if not required.issubset(self._credentials.keys()):
raise ValueError(f"Missing required secrets: {required - set(self._credentials.keys())}")
return self._credentials
def _get_auth_header(self) -> Dict[str, str]:
creds = self._load_credentials()
return {"Authorization": f"Bearer {creds['bearer_token']}"}
def _fetch_bookmarks_page(
self,
user_id: str,
pagination_token: Optional[str] = None
) -> Dict:
"""Fetch single page of bookmarks from X API."""
url = f"{self.X_API_BASE}/users/{user_id}/bookmarks"
params = {
"max_results": min(self.max_results, 100),
"tweet.fields": "created_at,public_metrics,author_id",
"expansions": "author_id",
"user.fields": "username"
}
if pagination_token:
params["pagination_token"] = pagination_token
response = requests.get(
url,
headers=self._get_auth_header(),
params=params,
timeout=30
)
response.raise_for_status()
return response.json()
def _parse_posts(self, data: Dict) -> List[XPost]:
"""Parse API response into sanitized XPost objects."""
tweets = data.get("data", [])
users = {
u["id"]: u["username"]
for u in data.get("includes", {}).get("users", [])
}
posts = []
for tweet in tweets:
metrics = tweet.get("public_metrics", {})
posts.append(XPost(
id=tweet["id"],
text=tweet["text"],
author_username=users.get(tweet["author_id"], "unknown"),
created_at=datetime.fromisoformat(
tweet["created_at"].replace("Z", "+00:00")
),
like_count=metrics.get("like_count", 0),
bookmarked_at=None # X API doesn't expose this
))
return posts
def get_bookmarks(
self,
user_id: Optional[str] = None,
limit: Optional[int] = None
) -> List[Dict]:
"""
Fetch bookmarks for a user.
Args:
user_id: X user ID (if None, fetches authenticated user)
limit: Max posts to return (None for all)
Returns:
List of sanitized post dictionaries safe for agents
"""
# If no user_id provided, look up authenticated user
if user_id is None:
user_id = self._get_authenticated_user_id()
all_posts = []
next_token = None
while True:
data = self._fetch_bookmarks_page(user_id, next_token)
posts = self._parse_posts(data)
all_posts.extend(posts)
# Respect limit if provided
if limit and len(all_posts) >= limit:
all_posts = all_posts[:limit]
break
next_token = data.get("meta", {}).get("next_token")
if not next_token:
break
return [post.to_dict() for post in all_posts]
def _get_authenticated_user_id(self) -> str:
"""Get user ID from bearer token."""
response = requests.get(
f"{self.X_API_BASE}/users/me",
headers=self._get_auth_header(),
params={"user.fields": "id"}
)
response.raise_for_status()
return response.json()["data"]["id"]
def search_bookmarks(
self,
query: str,
case_sensitive: bool = False
) -> List[Dict]:
"""
Search bookmarked posts by text content.
Safe for agents - no external API call, filters local data.
"""
bookmarks = self.get_bookmarks()
query = query if case_sensitive else query.lower()
results = []
for post in bookmarks:
text = post["text"] if case_sensitive else post["text"].lower()
if query in text:
results.append(post)
return results
# Example usage for different secrets stores
def create_tool_with_aws() -> XBookmarksTool:
"""Create tool using AWS Secrets Manager."""
store = AWSSecretsManager(region="us-west-2")
return XBookmarksTool(
secrets_store=store,
secret_path="prod/x-api/agent-readonly"
)
def create_tool_with_vault() -> XBookmarksTool:
"""Create tool using HashiCorp Vault."""
store = HashiCorpVault(
vault_addr="https://vault.company.com",
role_id="agent-role",
secret_id="..." # From secure distribution
)
return XBookmarksTool(
secrets_store=store,
secret_path="social-media/x-api"
)
def create_tool_with_1password() -> XBookmarksTool:
"""Create tool using 1Password Connect."""
store = OnePasswordConnect(
connect_host="http://localhost:8080",
connect_token="..." # From OP_CONNECT_TOKEN
)
return XBookmarksTool(
secrets_store=store,
secret_path="uuid-of-x-api-item"
)
# Agent-facing interface
class AgentBookmarkInterface:
"""
Simplified interface exposed to agents.
Only exposes safe, read-only operations.
"""
def __init__(self, tool: XBookmarksTool):
self._tool = tool
def list_recent(self, count: int = 20) -> str:
"""Get recent bookmarks as JSON string."""
posts = self._tool.get_bookmarks(limit=count)
return json.dumps(posts, indent=2)
def find_posts_about(self, topic: str) -> str:
"""Search bookmarks for topic."""
results = self._tool.search_bookmarks(topic)
return json.dumps(results, indent=2)
def get_stats(self) -> str:
"""Get bookmark statistics."""
posts = self._tool.get_bookmarks(limit=1000)
return json.dumps({
"total_fetched": len(posts),
"top_authors": self._count_authors(posts),
"date_range": self._date_range(posts)
}, indent=2)
def _count_authors(self, posts: List[Dict]) -> Dict[str, int]:
from collections import Counter
return dict(Counter(p["author"] for p in posts).most_common(5))
def _date_range(self, posts: List[Dict]) -> Dict[str, str]:
if not posts:
return {}
dates = [datetime.fromisoformat(p["created"]) for p in posts]
return {
"oldest": min(dates).isoformat(),
"newest": max(dates).isoformat()
}Key Security Features
| Feature | Implementation |
|---|---|
| No env vars | Credentials fetched from secrets store at runtime |
| Protocol-based | Easy to swap AWS, Vault, 1Password, etc. |
| Lazy loading | Credentials only fetched when needed |
| Sanitized output | XPost dataclass filters raw API response |
| No write ops | Read-only interface for agents |
Usage Example
Python
复制
# Setup (trusted code)
tool = create_tool_with_aws()
agent_interface = AgentBookmarkInterface(tool)
# Agent interaction (untrusted)
recent = agent_interface.list_recent(10)
python_posts = agent_interface.find_posts_about("python")The agent sees only the
AgentBookmarkInterface methods—no API keys, no raw HTTP, no credential access.