The official Python SDK for the AnyFrame control plane - point an agent at a repo, get a sandbox running Claude Code inside.
┌──────────────────────────────┐
│ Agent (repo, system prompt)│
│ ├── Skills │
│ ├── MCPs │
│ └── Connector toggles │
┌──────────┐ anyframe SDK └─────────────┬────────────────┘
│ you │ ───────────────────▶ │ build
│ (python) │ ▼
└──────────┘ ┌──────────────────────────────────────────┐
│ Session (sandbox · chat · serve) │
└──────────────────────────────────────────┘
User-level Connectors plug MCP servers (Linear, Sentry, …) in once and toggle them per-agent. Skills + MCPs ride with the agent into every session it boots.
Demo
AnyFrame.mp4
Install
uv add anyframe
Get an API key
The SDK authenticates with a personal API token (prefix afm_).
-
Sign in at anyfrm.com.
-
Open Dashboard → Settings → API keys and click Create key.
-
Copy the
afm_...token (shown once - store it now). -
Drop it into a
.envnext to your script, or export it:ANYFRAME_API_KEY=afm_...
Already authed in another script? af.tokens.create(name="ci-bot") mints a new one programmatically.
Working with private repos? Also set a GitHub PAT once - in the dashboard's Credentials page, or via af.credentials.set_github("ghp_..."). See Credentials.
Quickstart
Take over a web session
Already have a session running in the web UI? Attach to it and take a turn:
import anyframe af = anyframe.AnyFrame() # reads ANYFRAME_API_KEY + ANYFRAME_BASE_URL # Grab the session id from the web URL, or list and pick one: session = next(s for s in af.sessions.list() if s.status == "running") af.sessions.message(session.id, {"text": "summarize what you've done so far"}) for event in af.sessions.events(session.id): # live SSE; Ctrl-C when done print(event.event, event.json())
message and events proxy verbatim to the in-sandbox chat - the web UI and the SDK are two clients on the same channel.
Build a fresh agent from scratch
import anyframe af = anyframe.AnyFrame() agent = af.agents.create(name="demo", repo_url="tinyhq/box", install_cmd="bun install") af.agents.build(agent.id) af.agents.wait_for_build(agent.id) session = af.sessions.create(agent_id=agent.id) session = af.sessions.wait_until_running(session.id) print(session.sandbox_url)
Authentication
.env in your project root, or shell environment:
ANYFRAME_API_KEY=afm_... ANYFRAME_BASE_URL=https://api.anyfrm.com # optional ANYFRAME_LOG_LEVEL=INFO # set DEBUG for request tracing
See Get an API key for where the afm_... token comes from.
Async
Every method exists on AsyncAnyFrame with the same signature, just await-ed.
import asyncio, anyframe async def main(): async with anyframe.AsyncAnyFrame() as af: me = await af.me() agents = await af.agents.list() asyncio.run(main())
Agents
Agents are the unit of "what runs in the sandbox" - a repo, a system prompt, a permissions config.
af.agents.list() af.agents.create( name="demo", repo_url="owner/name", install_cmd="bun install", runtime="claude", # or "codex" env_vars={"DATABASE_URL": "..."}, # injected into every session ) af.agents.get(agent_id) # AgentDetail: includes skills, mcps, connectors, image af.agents.update(agent_id, name="renamed") af.agents.delete(agent_id)
Skills
Skills are bundles of instructions the agent loads at boot (think: "deploy this app", "review this PR").
af.agents.skills.list(agent_id) af.agents.skills.create(agent_id, name="deploy", source="inline", content={...}) af.agents.skills.update(agent_id, skill_id, enabled=False) af.agents.skills.delete(agent_id, skill_id)
MCPs
MCPs configured inline on the agent - for one-off MCP servers that aren't worth setting up as a reusable connector.
af.agents.mcps.list(agent_id) af.agents.mcps.create(agent_id, name="git", transport="http", config={"url": "..."}) af.agents.mcps.update(agent_id, mcp_id, enabled=False) af.agents.mcps.delete(agent_id, mcp_id)
Connectors
User-level MCP connectors - configure once, then opt in per-agent via the connector-toggle API below.
af.connectors.list() discovery = af.connectors.discover("https://mcp.linear.app/sse") authorize = af.connectors.create_oauth(mcp_url=discovery.mcp_url, display_name="Linear") # open authorize.authorize_url in a browser; callback completes server-side af.connectors.create_bearer(mcp_url="...", display_name="...", token="...") af.connectors.reauthorize(connector_id) af.connectors.delete(connector_id)
Catalog
The control plane ships with a curated catalog (Linear, Sentry, Google, …). Install by slug instead of pasting URLs.
catalog = af.connectors.list_catalog() # ConnectorCatalogItem[] af.connectors.install_catalog_oauth("linear") # → authorize URL (DCR or pre-registered) af.connectors.install_catalog_bearer("sentry", token="...")
Per-agent toggle (controls which connectors apply to one agent):
af.agents.connectors.list(agent_id) af.agents.connectors.set(agent_id, connector_id, enabled=True)
Builds
Builds bake an agent's repo + dependencies into a cached sandbox image - required before a session can boot it.
af.agents.build(agent_id, force=False) # queue a build af.agents.build_status(agent_id) # current state + cached image id af.agents.builds(agent_id, limit=20) # history af.agents.build_log_url(agent_id, build_id) # signed R2 URL for the archived log for event in af.agents.stream_build(agent_id, build_id): print(event.event, event.json()) # live SSE log frames af.agents.wait_for_build(agent_id) # blocks until succeeded / fails
Sessions
A session is one live sandbox. Lifecycle is booting → running → snapshotting → terminated; resume brings a terminated session back from its snapshot.
session = af.sessions.create(agent_id=agent.id, idle_timeout_s=300) af.sessions.wait_until_running(session.id) af.sessions.list() af.sessions.get(session.id) af.sessions.snapshots(session.id) af.sessions.terminate(session.id) af.sessions.resume(session.id) af.sessions.delete(session.id) # hard delete; requires terminated
Setup sessions + save-as-base
Setup sessions are user-driven sandboxes you use to seed an agent's filesystem (clone, install, warm caches), then promote to that agent's warmup image. Future normal sessions then hydrate from the promoted snapshot.
session = af.sessions.create(agent_id=agent.id, is_setup_session=True) af.sessions.wait_until_running(session.id) # ... do interactive setup ... result = af.sessions.save_as_base(session.id) # SaveAsBaseResult print(result.warmup_image_id)
Chat
Talk to the running agent. message and respond proxy verbatim to the in-sandbox chat server; events is the live SSE stream; transcript reads persisted history.
af.sessions.message(session.id, {"text": "deploy main to staging"}) for event in af.sessions.events(session.id, last_event_id=None): print(event.id, event.event, event.json()) af.sessions.transcript(session.id, since=0, limit=1000) af.sessions.respond(session.id, {"decision": "approve", "tool_use_id": "..."})
Previews (in-sandbox dev servers)
Launch dev servers inside the sandbox and tunnel their ports out. Multiple previews can run per session - name them or address them by port.
af.sessions.previews_start(session.id, cmd="bun dev", port=3000, name="web") af.sessions.previews_status(session.id, name="web") # PreviewActionResult af.sessions.previews_list(session.id) # Preview[] af.sessions.previews_logs(session.id, name="web", tail=200) af.sessions.previews_stop(session.id, name="web") # Atomic batch - restarts at most once when allocating new ports af.sessions.previews_batch_start(session.id, [ anyframe.PreviewSpec(cmd="bun dev", port=3000, name="web"), anyframe.PreviewSpec(cmd="bun api", port=4000, name="api"), ])
Migrated from the old
serve_*methods. The control plane replaced/sessions/{id}/serve/*with a singlePOST /sessions/{id}/previewsaction body. The SDK methods above target that endpoint;Session.previews(a list ofPreview) replacesserve_status/serve_port/serve_url.
Attention rail
A curated, newest-first list of things the operator should act on - pending permission prompts, idle running sessions, and recently-paused sessions.
for item in af.attention.list(limit=20): print(item.kind, item.agent_name, item.preview)
Each row is one of AttentionPendingItem, AttentionIdleItem, or AttentionPausedItem. Discriminate on item.kind.
Credentials
The control plane needs a runtime credential - Claude OAuth (default Claude runtime) or an OpenAI Codex token (Codex runtime) - plus a GitHub PAT for private repos. It only ever shows you redacted views.
af.credentials.get() # set flag + last4 for claude / codex / github af.credentials.set_claude("sk-...") af.credentials.set_codex("sk-...") af.credentials.set_github("ghp_...") af.credentials.clear_claude() af.credentials.clear_codex() af.credentials.clear_github()
Tokens
Manage the API keys this SDK uses. create returns the raw token exactly once - store it now.
af.tokens.list() created = af.tokens.create(name="ci-bot") print(created.token) # afm_... one-time af.tokens.revoke(created.id)
Errors
All errors derive from anyframe.AnyFrameError, so one except catches everything.
anyframe.AnyFrameError # base ├── anyframe.APIError # any non-2xx (status_code, message) ├── anyframe.AuthError # 401 - bad / missing API key ├── anyframe.NotFoundError # 404 ├── anyframe.ConflictError # 409 - e.g. delete on a running session ├── anyframe.ValidationError # 400/422 (carries field-level details) ├── anyframe.RateLimitError # 429 (exposes retry_after) └── anyframe.ServerError # 5xx
License
MIT.
Docs: docs.anyfrm.com · Found a bug or have a question? Join us on Discord.























