Skip to main content

Official Python SDK for the AnyFrame control plane.

Project description

anyframe

PyPI version

The official Python SDK for the AnyFrame control plane - point an agent at a repo, get a sandbox running Claude Code inside.

                                ┌──────────────────────────────┐
                                │  Template  (repo · prompt)   │
                                │   ├── Skills                 │
                                │   ├── MCPs                   │
                                │   └── Connector toggles      │
                                └─────────────┬────────────────┘
                                              │ bind
                                ┌─────────────▼────────────────┐
                                │  Agent  (runtime · overrides)│
   ┌──────────┐   anyframe SDK  └─────────────┬────────────────┘
   │   you    │ ───────────────────▶          │  build
   │ (python) │                               ▼
   └──────────┘   ┌──────────────────────────────────────────┐
                  │ Session (sandbox · chat · previews)      │
                  └──────────────────────────────────────────┘

Templates are the reusable blueprint: repo, system prompt, skills, MCPs, connector toggles, baseline permissions and env vars. Agents bind a template to a runtime plus per-agent overrides. Connectors plug MCP servers (Linear, Sentry, …) in once at the user (or org) level and toggle per-template.

Demo

https://github.com/user-attachments/assets/93e8edb6-2a19-4182-9492-fd397a33921c

Install

uv add anyframe

Get an API key

The SDK authenticates with a personal API token (prefix afm_).

  1. Sign in at anyfrm.com.

  2. Open Dashboard → Settings → API keys and click Create key.

  3. Copy the afm_... token (shown once - store it now).

  4. Drop it into a .env next 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? Install a GitHub App from the dashboard's Integrations page, then pass its install_id to templates.create(). See Integrations.

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()

template = af.templates.create(
    name="box",
    repo_url="tinyhq/box",
    install_cmd="bun install",
    system_prompt="You are a careful, terse engineer.",
)
agent = af.agents.create(name="demo", template_id=template.id)

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

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()
        templates = await af.templates.list()

asyncio.run(main())

Templates

Templates own the what: the repo, install/serve commands, system prompt, skills, MCPs, connector toggles, baseline permissions and env vars. One template can back many agents.

template = af.templates.create(
    name="box",
    repo_url="tinyhq/box",
    install_cmd="bun install",
    system_prompt="You are a careful, terse engineer.",
    install_id=42,                       # GitHub App install (required for private repos)
)
af.templates.list()
af.templates.get(template.id)            # TemplateDetail: includes skills, mcps, connectors
af.templates.update(template.id, system_prompt="Be brief.")
af.templates.delete(template.id)         # 409 if any agent still bound

# Nested sub-resources
af.templates.skills.create(template.id, name="deploy", source="inline", content={...})
af.templates.mcps.create(template.id, name="git", transport="http", config={"url": "..."})
af.templates.connectors.set(template.id, connector_id, enabled=True)

Agents

Agents are the binding: a template plus a runtime plus optional per-agent overrides.

agent = af.agents.create(
    name="prod-bot",
    template_id=template.id,
    runtime="claude",                          # or "codex"
    permissions_override={"preset": "full_trust"},
    env_vars_override={"DEBUG": "1"},
)
af.agents.list()
af.agents.get(agent.id)                        # AgentDetail: embeds template + cached image
af.agents.update(agent.id, runtime="codex")
af.agents.update(agent.id, permissions_override=None)   # clear → fall back to template
af.agents.delete(agent.id)

Builds

Builds bake the bound template's repo + dependencies into a cached sandbox image. Cached by (template recipe + agent runtime).

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 log URL
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)

Setup sessions + save-as-base

Setup sessions seed an agent's filesystem (clone, install, warm caches), then promote to that agent's warmup image. Future sessions hydrate from the promoted snapshot.

session = af.sessions.create(agent_id=agent.id, is_setup_session=True)
af.sessions.wait_until_running(session.id)
# ... interactive setup ...
result = af.sessions.save_as_base(session.id)  # SaveAsBaseResult

Chat

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

af.sessions.previews_start(session.id, cmd="bun dev", port=3000, name="web")
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"),
])

Collaboration (org sessions)

for p in af.sessions.presence(session.id):
    print(p.login, "driver" if p.is_driver else "watcher")

req = af.sessions.request_control(session.id, message="taking over deploy")
af.sessions.handoff(session.id, to_user_id=5, request_id=req.id)
af.sessions.take_over(session.id)                       # admin / owner
af.sessions.set_privacy(session.id, private=True)

Connectors

User- or org-level MCP connectors. Configure once, then toggle per-template - every agent bound to the template inherits the set.

af.connectors.list()
af.connectors.discover("https://mcp.linear.app/sse")
af.connectors.create_oauth(mcp_url="...", display_name="Linear")
af.connectors.create_bearer(mcp_url="...", display_name="...", token="...")
af.connectors.create_custom_header(mcp_url="...", display_name="...", header_name="X-API-Key", token="...")
af.connectors.create_stdio(display_name="local-fs", command="npx", args=["..."])
af.connectors.reauthorize(connector_id)
af.connectors.delete(connector_id)

# Catalog (curated: Linear, Sentry, …)
af.connectors.list_catalog()
af.connectors.install_catalog_oauth("linear")
af.connectors.install_catalog_bearer("sentry", token="...")

Credentials

The control plane needs a runtime credential - Claude OAuth (default Claude runtime) or an OpenAI Codex token (Codex runtime). Only redacted views ever come back.

af.credentials.get()                        # set flag + last4 for claude / codex
af.credentials.set_claude("sk-...")
af.credentials.set_codex("sk-...")
af.credentials.clear_claude()
af.credentials.clear_codex()

Credits

bal = af.credits.get()
# CreditBalance(limit=1000, used=250, remaining=750,
#               scope='personal', org_token_active=False, …)

scope reflects whether the balance is the personal pool or the active org's pool. org_token_active=True means the org has a BYO runtime token set; sessions don't draw from the pool.

Integrations

af.integrations.list()                              # every install in scope
af.integrations.list_github_installs()              # picker shape
af.integrations.list_github_repos(install_id)
af.integrations.set_binding(install_id, agent_id=7) # route webhooks
af.integrations.delete_binding(install_id)
af.integrations.delete(install_id)

Integration installs are how the control plane mints short-lived tokens at sandbox boot time (GitHub App tokens for cloning, Slack workspace tokens for posting, …) and routes incoming webhook events to a bound agent.

Orgs

af.orgs.list()                                       # OrgMembership[]
org = af.orgs.create(slug="acme", name="Acme", auto_join_domain="acme.com")
af.orgs.get("acme")
af.orgs.update("acme", name="Acme Corp")
af.orgs.transfer_ownership("acme", new_owner_user_id=42)
af.orgs.delete("acme")                               # archive

af.set_active_org(org.id)                            # subsequent calls scope to the org
af.set_active_org(None)                              # back to personal

# Nested
af.orgs.members.list("acme")
af.orgs.members.change_role("acme", user_id, role="admin")
af.orgs.invitations.create("acme", github_login="alice", message="join us")
af.orgs.invitations.accept_by_token("tok_xyz")
af.orgs.join_requests.create("acme")
af.orgs.credentials.set_claude("acme", "sk-...")
af.orgs.audit.list("acme", kind="agent.created", limit=50)

Optional shared workspaces — every member sees the same templates, agents, sessions, and connectors, and shares one credit pool. Whole surface is gated on a server ORGS_ENABLED flag.

Identity

me = af.me()                              # User: id, login, email, name, …
                                          # …plus memberships, active_org_id,
                                          # suggested_orgs, pending_invitations
                                          # when ORGS_ENABLED is on
af.public_config()                        # PublicConfig (unauthenticated flags)

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.

Tokens

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

Migrating from 1.x

2.0 is the first breaking release. Three structural changes:

  1. Agent ↔ Template split. Every field that described what an agent does (repo, install/serve, system prompt, skills, MCPs, connector toggles, baseline permissions and env vars) moved to a new Template resource. Agents now bind to a template + optional runtime, permissions_override, env_vars_override.
  2. Repo access via integrations, not credentials. credentials.set_github / clear_github and the Credentials.github field are gone. Install a GitHub App from the dashboard and pass its install_id to templates.create().
  3. Optional org workspace. af.orgs, af.set_active_org(...), and the new org-aware fields on af.me() (memberships, active_org_id, …). Personal-only callers don't need to change anything else.
1.x 2.0
af.agents.create(name=…, repo_url=…, install_cmd=…, system_prompt=…) tpl = af.templates.create(...); af.agents.create(name=…, template_id=tpl.id)
af.agents.skills.* / mcps.* / connectors.* af.templates.skills.* / mcps.* / connectors.*
af.credentials.set_github(token) install a GitHub App via Integrations
creds.github (removed)

New surfaces: af.templates, af.credits, af.integrations, af.orgs, af.set_active_org(...), af.public_config(), connectors.create_custom_header(...), connectors.create_stdio(...), and session collab (presence, request_control, handoff, take_over, set_privacy).

License

MIT.


Docs: docs.anyfrm.com · Found a bug or have a question? Join us on Discord.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anyframe-2.0.0.tar.gz (41.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anyframe-2.0.0-py3-none-any.whl (49.4 kB view details)

Uploaded Python 3

File details

Details for the file anyframe-2.0.0.tar.gz.

File metadata

  • Download URL: anyframe-2.0.0.tar.gz
  • Upload date:
  • Size: 41.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for anyframe-2.0.0.tar.gz
Algorithm Hash digest
SHA256 0b2d3d80ae691daeab4dbe45bd0693746b24d95168a020c18875c0af39abf39a
MD5 b71d6490ff510198c9b7a213becfa207
BLAKE2b-256 e115dd8f48cd566c7b85a9f8ef6fd1aa5c895657e7e249b305707185f4fc3849

See more details on using hashes here.

File details

Details for the file anyframe-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: anyframe-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 49.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for anyframe-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6654bf7b38b0411e40dcdce20d948af0910e6fe70e2d801613bf3bd11f5bcdeb
MD5 cb51bf3ff6f0158140148bdf64bdd1f4
BLAKE2b-256 17a8c699823914a71dce5f27837dd19a00136509b45a459fec39961873b31db9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page