# Anona Compute # Anona Compute > Secure microVM sandboxes for AI agents. Works with the E2B SDK. Anona Compute gives every AI agent its own computer: a KVM microVM with its own kernel, filesystem and network policy. Agents run code, move files, start long-lived processes, drive a desktop, and pause and resume with memory intact. ## Set up with your agent Paste this into Claude Code, Codex or Cursor. It installs the CLI, asks you to approve one sign-in, registers the MCP server and proves it works with a sandbox. ```text paste-into-your-agent Set up Anona Compute for me. Fetch https://docs.compute.anonalabs.com/agents/onboarding-prompt.md and follow it. ``` - [Agent setup](/agents/overview): How the sign-in works and what the agent gets. - [Manual setup](/quickstart): Your first sandbox from Python, TypeScript or the CLI. ## Or point the E2B SDK at Anona The API is compatible with the E2B SDK. Point the stock `e2b` package at Anona with two environment variables, or use the Anona SDK extras for live view and SSH. ```bash setup.sh export E2B_DOMAIN=compute.anonalabs.com export E2B_API_KEY=e2b_YOUR_API_KEY ``` ## Where to go next - [Quickstart](/quickstart): Create a sandbox, run a command, clean up. - [Migrate from E2B](/migrate-from-e2b): What carries over unchanged, and what differs. - [Recipes](/recipes/index): Start from what you are building. - [Concepts](/concepts/sandboxes): Sandboxes, templates, lifecycle, security. - [CLI](/cli/overview): Lifecycle, exec, ssh, live view and MCP. - [API reference](/api-reference/introduction): Every route the gateway serves. ## What you get | | | |---|---| | **Isolation** | One KVM microVM per sandbox, behind a gateway that checks every request. See [Security](/concepts/security). | | **E2B compatibility** | The `e2b` Python and JavaScript SDKs (checked with 2.20.3, 2.51.0 and 2.61.0): create, connect, commands, files, PTY, pause and resume. See [Migrate from E2B](/migrate-from-e2b). | | **Long-running sandboxes** | No lifetime limit on paid plans, and pause on timeout instead of kill. See [Lifecycle](/concepts/lifecycle). | | **Live view** | Watch or take over a sandbox's terminal or desktop in your own page. See [Live view](/guides/live-view). | | **SSH** | `anona-compute ssh`, `scp`, port forwarding and VS Code Remote, with 10-minute certificates. See [SSH](/guides/ssh). | | **Egress control** | Per-sandbox allow and deny lists on top of a platform floor that always blocks private ranges and cloud metadata. See [Networking](/concepts/networking). | | **Usage pricing** | You pay for active vCPU time and resident memory, not wall-clock time. See [Pricing](/pricing). | ## Docs for agents | | | |---|---| | Index of every page | [`/llms.txt`](https://docs.compute.anonalabs.com/llms.txt) | | Everything in one file | [`/llms-full.txt`](https://docs.compute.anonalabs.com/llms-full.txt) | | Any page as Markdown | Add `.md` to its URL | | Tools over MCP | [Agents via MCP](/guides/mcp) | See [Docs for agents](/agents/docs-for-agents). ## Pieces | Piece | What it is | |---|---| | API | `https://api.compute.anonalabs.com`, the E2B-compatible gateway | | Dashboard | [app.compute.anonalabs.com](https://app.compute.anonalabs.com): sign-in, API keys, sandboxes, usage | | CLI | `anona-compute`: sign in, sandbox lifecycle, exec, ssh, live view, and an MCP server for agents | | SDK extras | `anona` (Python) and `@anona/sdk` (TypeScript): subclasses of the E2B `Sandbox` with `live` and `ssh` | Anona Compute is in preview. Pages mark anything not available yet as **Coming soon**, and anything built but not yet proven on the production fleet says so where it matters. Preview pricing is not charged. # Quickstart > Create a sandbox, run a command and clean up, in about two minutes. ### Get an API key Sign in to the [dashboard](https://app.compute.anonalabs.com), open **API keys** and create a key. Keys look like `e2b_` followed by hex; the secret is shown once. ```bash env.sh export ANONA_API_KEY=e2b_YOUR_API_KEY ``` The Anona SDK extras and the CLI read `ANONA_API_KEY` (or `E2B_API_KEY`) and default to `compute.anonalabs.com`. ### Install ```text Python pip install anona ``` ```text TypeScript npm install @anona/sdk e2b ``` ```text CLI # anona-compute: a single binary for Linux, macOS and Windows (amd64, arm64) ``` **Coming soon:** the `anona` and `@anona/sdk` packages on PyPI and npm, and downloadable `anona-compute` binaries. Until they are published, the stock E2B SDK works on its own (`pip install e2b` or `npm install e2b`, then `from e2b import Sandbox` with `E2B_DOMAIN=compute.anonalabs.com`); see [Migrate from E2B](/migrate-from-e2b). ### Run code in a sandbox ```python quickstart.py from anona import Sandbox sbx = Sandbox.create("base") # a KVM microVM, ready in about a second print("sandbox", sbx.sandbox_id) result = sbx.commands.run("uname -a && whoami") print(result.stdout) sbx.files.write("/home/user/hello.txt", "hello from Anona\n") print(sbx.files.read("/home/user/hello.txt")) sbx.kill() ``` ```typescript quickstart.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base"); // a KVM microVM, ready in about a second console.log("sandbox", sbx.sandboxId); const result = await sbx.commands.run("uname -a && whoami"); console.log(result.stdout); await sbx.files.write("/home/user/hello.txt", "hello from Anona\n"); console.log(await sbx.files.read("/home/user/hello.txt")); await sbx.kill(); ``` ```bash quickstart.sh printenv ANONA_API_KEY | anona-compute login --with-api-key # or: anona-compute login (browser) SANDBOX_ID=$(anona-compute create -t base --json | jq -r .sandboxID) anona-compute exec "$SANDBOX_ID" -- 'uname -a && whoami' anona-compute list anona-compute kill "$SANDBOX_ID" ``` ## Next - [Run code](/guides/run-code): Commands, exit codes, environment and working directories. - [Long-running sandboxes](/concepts/lifecycle): Timeouts, no-limit sandboxes, pause and resume. - [Templates](/concepts/templates): `base`, `python`, `node`, `code-interpreter`, `desktop`, `coding-agent`. - [Agents over MCP](/guides/mcp): Let Claude Code, Codex or Cursor drive sandboxes. # Migrate from E2B > Point the stock E2B SDK at Anona Compute, and what differs. Anona Compute serves the E2B sandbox API, so existing code that uses the `e2b` SDK keeps working. Change two environment variables: ```bash migrate-env.sh export E2B_DOMAIN=compute.anonalabs.com # the SDK derives https://api.compute.anonalabs.com export E2B_API_KEY=e2b_YOUR_API_KEY # an Anona Compute key from the dashboard ``` Set `E2B_DOMAIN`, not `E2B_API_URL`: recent SDKs ignore or deprecate `E2B_API_URL`. Sandbox traffic then goes to `https://-.sbx.compute.anonalabs.com`, which the SDK builds from the domain the API returns. ```python e2b_unchanged.py from e2b import Sandbox # the stock SDK, unchanged with Sandbox.create() as sbx: # default template: base print(sbx.commands.run("echo hello from e2b code").stdout) sbx.files.write("/home/user/data.csv", "a,b\n1,2\n") print([e.name for e in sbx.files.list("/home/user")]) ``` ```typescript e2b_unchanged.ts import { Sandbox } from "e2b"; // the stock SDK, unchanged const sbx = await Sandbox.create(); // default template: base console.log((await sbx.commands.run("echo hello from e2b code")).stdout); await sbx.files.write("/home/user/table.csv", "a,b\n1,2\n"); console.log((await sbx.files.list("/home/user")).map((e) => e.name)); await sbx.kill(); ``` The Anona SDK extras (`anona`, `@anona/sdk`) are subclasses of the E2B `Sandbox`: swap the import to get `sandbox.live` and `sandbox.ssh` and the Anona domain as default. See [SDK extras](/sdk/overview). ## Compatible Checked with the stock SDK (e2b 2.20.3, 2.51.0 and 2.61.0) against the gateway: | Area | Works | |---|---| | Sandboxes | `create` (v1 and v2 API), `connect`, `list`, `get_info`, `kill`, `set_timeout`, `is_running`, metadata, environment variables | | Lifecycle | `pause`, `connect` / resume with memory kept, `lifecycle={"on_timeout": "pause"}` | | Commands | `commands.run`, streaming callbacks, background commands, `send_stdin`, `kill`, `list`, `connect` | | Files | `read`, `write`, `list`, `make_dir`, `remove`, `rename`, `exists`, `get_info`, `watch_dir`, signed `upload_url` / `download_url` | | Terminal | `pty.create`, `send_stdin`, `resize` | | Ports | `get_host(port)`; see [Preview links](/guides/preview-links) | | Network | `allow_internet_access`, `network` (`allow_out`, `deny_out`, `rules` with vault placeholders) on create, and `update_network` on a running sandbox; see [Networking](/concepts/networking) | | Desktop | The `e2b-desktop` SDK on the `desktop` template, including the noVNC stream on port 6080 | | Templates | `Template.build` and the `e2b` CLI's template commands: build from steps or a Dockerfile, list, get, logs, delete, aliases; see [Templates](/concepts/templates#build-your-own) | | Errors | E2B-shaped error bodies, so the SDK raises its usual exceptions | ## Differences | Topic | Anona Compute | |---|---| | API keys | Created in the Anona dashboard. They use the E2B format (`e2b_` + hex), so the SDK accepts them. E2B keys do not work here, and Anona keys do not work on E2B. | | Templates | The built-in templates are `base`, `python`, `node`, `code-interpreter`, `desktop` and `coding-agent`. E2B template names you built yourself do not exist here. See [Templates](/concepts/templates). | | Lifetime | `timeout=-1` means no deadline on plans without a maximum lifetime. Paused sandboxes are kept until you kill them. See [Lifecycle](/concepts/lifecycle). | | Default user | Commands run as `user` (uid 1000) with passwordless `sudo`, as on E2B. | | Metrics and logs | `get_metrics` and the sandbox log routes are served from the sandbox's own envd, so they need no log collector. Logs are envd's structured log: every process's stdout and stderr, process starts and exits, and file operations. The default minimum level is `info`; pass `level=debug` for envd's own internals. A paused sandbox returns an empty page, since its envd is frozen. | | Default egress | Depends on your plan: paid plans allow public internet by default, the free plan has none. Private ranges and cloud metadata are always blocked. See [Networking](/concepts/networking). | ## Not available yet Calls to these return an error from the gateway today. They are **Coming soon**: | E2B feature | Status | |---|---| | `fork`, `create_snapshot`, `list_snapshots` | Coming soon | | Filesystem-only pause (`mode="filesystem"`, `autoPauseMemory: false`) | Not supported: a pause always keeps memory | | Volumes, IAM, E2B secrets, team and admin APIs | Not supported; `volume_mounts` and `iam` are refused with 400. For secrets, use the Anona [credential vault](/concepts/networking#credentials-without-exposing-them) | | Resume on incoming traffic (`auto_resume`) | Accepted on create; waking a paused sandbox on a request to its URL is Coming soon. Call `Sandbox.connect(id)` to resume. | | SOCKS egress proxy (`network.egress_proxy`) | Not supported | # Agent self-onboarding > A coding agent sets up Anona Compute from the terminal. You approve one browser sign-in. > For agents: fetch [`/llms.txt`](/agents/docs-for-agents) first. It indexes every page as Markdown. Give Claude Code, Codex, Cursor or any MCP client its own computers. The agent installs the CLI, asks you to approve a sign-in, registers the MCP server and creates a sandbox to prove it works. You do one thing: approve the sign-in in your browser. - [Paste one prompt](/agents/onboarding-prompt): Hand this to your agent and it does the rest. - [Docs for agents](/agents/docs-for-agents): `llms.txt` and a Markdown copy of every page. ## The flow ```text agent you Anona Compute | anona-compute login --no-browser | |------------------------------------------------------------->| device code request | prints URL + code | |-------------------------------> opens URL, checks code, | | picks organization, approves | | |--------------------------->| | CLI now holds a session for this machine | | claude mcp add anona-compute -- anona-compute mcp | | tools/call create_sandbox, run_command, kill_sandbox | |------------------------------------------------------------->| ``` The sign-in is a standard device flow (RFC 8628). The agent never sees your password or an API key. The CLI trades its session for gateway tokens that last 15 minutes, and `anona-compute logout` revokes the session. ## Ways to connect | Path | Needs | Good for | Credential | |---|---|---|---| | **A. CLI and MCP** | The `anona-compute` binary | Claude Code, Codex, Cursor, any stdio MCP client | Browser sign-in held by the CLI | | **B. CLI with an API key** | The binary and an `e2b_` key | Headless machines and CI | A scoped, expiring [API key](/guides/ci) | | **C. SDK in the agent's code** | `pip install anona` or `npm install @anona/sdk` | An agent you wrote | An API key in `ANONA_API_KEY` | | **D. Stock E2B SDK** | `pip install e2b` and two environment variables | Code that already uses E2B | An API key as `E2B_API_KEY` | **Coming soon:** downloadable `anona-compute` binaries and the `anona` and `@anona/sdk` packages. Until they ship, path D works today; see [Migrate from E2B](/migrate-from-e2b). ## Setup per path **A. CLI and MCP** ```bash agent_login.sh anona-compute login --no-browser # prints a URL and a code; the human approves anona-compute whoami # user, organization, role ``` Then register the server with the agent's client: ```bash claude_code.sh claude mcp add anona-compute -- anona-compute mcp ``` ```bash codex.sh codex mcp add anona-compute -- anona-compute mcp ``` Cursor and other clients: see [Agents via MCP](/guides/mcp). MCP tools usually load when the client starts, so restart the agent once. **B. API key** Create a key in the [dashboard](https://app.compute.anonalabs.com) under **API keys**, with the least access the job needs. Then: ```bash agent_key.sh printenv ANONA_API_KEY | anona-compute login --with-api-key anona-compute whoami ``` **C and D. SDK** ```bash agent_env.sh export E2B_DOMAIN=compute.anonalabs.com export E2B_API_KEY=e2b_YOUR_API_KEY ``` The [quickstart](/quickstart) has the first sandbox in Python and TypeScript. ## What the agent gets Over MCP the agent can create, pause, resume, extend and kill sandboxes; run commands and background processes; read and write files; get a live-view link for you to watch, a preview link for a dev server it started, an SSH config and a desktop screenshot; and read its own usage. The full list is in [Agents via MCP](/guides/mcp#tools). ## Limits - The agent can do what your role and credential allow, no more. Start it with `--read-only` after `mcp` to offer only the tools that do not change sandboxes. - A browser sign-in is a session for one machine. It cannot be used with the SDK or the REST API; those need an API key. - Sandbox limits (count, size, lifetime) come from your plan. See [Limits](/concepts/limits). - Billing is per active CPU and resident memory; an agent that leaves sandboxes running is billed. Tell it to `kill` or let it [pause when idle](/concepts/lifecycle#pause-when-idle). ## Troubleshooting **The agent says the tools are missing.** The client loads MCP tools at start. Restart it, then ask it to list its tools. **`login` waits forever.** The code expires. Run it again, open the URL it prints and check that the code matches. **`unauthorized` after a while.** The session was revoked, or the key expired. Run `anona-compute whoami`, then sign in again. **A create fails with a limit error.** Your plan's sandbox, memory or lifetime limit is reached. The message names it; see [Troubleshooting](/troubleshooting). # Self-onboarding prompt > One prompt that connects a coding agent to Anona Compute and has it prove it works. Paste this into Claude Code, Codex or Cursor. The agent reads the prompt below, asks you to approve one sign-in and creates a sandbox to check the setup. ```text paste-into-your-agent Set up Anona Compute for me. Fetch https://docs.compute.anonalabs.com/agents/onboarding-prompt.md and follow it. ``` ## The prompt This is the page the agent reads. It is written for the agent. ```text onboarding-prompt You are connecting yourself to Anona Compute, which gives you disposable Linux microVM sandboxes. Work through these steps in order and stop to ask the human only where marked. 1. Check what you already have. - If you can already call tools named create_sandbox and run_command, skip to step 5. - Run `anona-compute whoami`. If it prints a user and organization, ask the human whether to use that account. Do not assume. 2. Install the CLI. - Run `anona-compute version`. If the command is missing, tell the human that release binaries are not published yet and ask for an API key (dashboard: https://app.compute.anonalabs.com, "API keys"). Then use the stock E2B SDK: set E2B_DOMAIN=compute.anonalabs.com and E2B_API_KEY, and stop here. 3. Sign in. ASK THE HUMAN to approve. - Run `anona-compute login --no-browser`. Show the human the URL and code it prints and wait. They sign in, check the code, pick the organization and approve. - Run `anona-compute whoami` and confirm it succeeds. 4. Register the MCP server, then ask the human to restart you. - Claude Code: `claude mcp add anona-compute -- anona-compute mcp` - Codex: `codex mcp add anona-compute -- anona-compute mcp` - Other clients: command `anona-compute`, argument `mcp`. - Tools load when the client starts, so a restart is normal. 5. Prove it works. - Call create_sandbox with template "base" and timeout_seconds 300. - Call run_command with `uname -a && whoami`. - Call kill_sandbox. Report the output to the human. Rules while you work: - Kill every sandbox you create when you finish with it. Running sandboxes are billed. - Never print or store an API key in a file you commit. - Docs index: https://docs.compute.anonalabs.com/llms.txt ``` ## What the human sees One browser page asking them to approve a code. Nothing else needs their input unless the CLI is not installed or the agent needs a restart. - [How it works](/agents/overview): The sign-in flow, the ways to connect and the limits. # Docs for agents > Machine-readable docs: llms.txt and a Markdown copy of every page. These docs are built to be read by agents as well as people. | What | Where | |---|---| | Index of every page, with descriptions | [`/llms.txt`](https://docs.compute.anonalabs.com/llms.txt) | | Every page in one file | [`/llms-full.txt`](https://docs.compute.anonalabs.com/llms-full.txt) | | Any page as Markdown | Add `.md` to its URL, for example `/quickstart.md` | | One prompt that sets an agent up | [Self-onboarding prompt](/agents/onboarding-prompt) | | The tool list an agent gets | [Agents via MCP](/guides/mcp#tools) | | The route list | [`/api-reference/anona-openapi.yaml`](/api-reference/introduction) and `e2b-openapi.yaml` | Start from `llms.txt`: it is small, and each line links to a Markdown page. Fetch `llms-full.txt` only when you need to search across everything. ## Tips for agents using the docs - Prefer the `.md` URL to the HTML page; it has no navigation or scripts. - Code blocks are runnable. Each one is run against a local stack before the docs ship, so copy them as written. - A **Coming soon** note means the feature is not available; do not call it. - Check [Limits](/concepts/limits) before planning work that creates many sandboxes. # Agents via MCP > Give Claude Code, Codex, Cursor or any MCP client its own sandboxes. `anona-compute mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. It acts with the CLI's credential, so sign in once and register it with your agent: ```bash mcp_login.sh anona-compute login # browser sign-in; or: printenv ANONA_API_KEY | anona-compute login --with-api-key ``` **Claude Code** ```bash claude_code.sh claude mcp add anona-compute -- anona-compute mcp ``` **Codex** ```bash codex.sh codex mcp add anona-compute -- anona-compute mcp ``` Or in `~/.codex/config.toml`: ```toml [mcp_servers.anona-compute] command = "anona-compute" args = ["mcp"] ``` **Cursor** `~/.cursor/mcp.json`, or `.cursor/mcp.json` in a project: ```json { "mcpServers": { "anona-compute": { "command": "anona-compute", "args": ["mcp"] } } } ``` **Other clients** Any client that starts stdio servers: command `anona-compute`, arguments `mcp`. Put `--profile ` before `mcp` to use another [profile](/cli/configuration), and `--read-only` after it to offer only the read-only tools. ## Tools | Tool | What it does | |---|---| | `whoami` | User, organization and role (or the masked API key) | | `list_sandboxes`, `get_sandbox` | State, template, end time (empty means no limit), metadata | | `list_templates` | The organization's templates, with their build status | | `create_sandbox` | Template, `timeout_seconds` (`-1` for no limit), `internet` on or off, metadata, environment variables, auto-pause | | `kill_sandbox`, `pause_sandbox`, `resume_sandbox`, `extend_sandbox` | Lifecycle; `extend_sandbox` takes `-1` for no limit | | `run_command` | Runs `bash -l -c`, returns stdout, stderr and exit code; `timeout_seconds` (default 60) | | `start_process`, `read_process_output`, `kill_process` | Background processes; output is read by offset, optionally waiting for more | | `read_file`, `write_file`, `list_files` | Text or base64 content, directory listings | | `get_live_view_url` | A short-lived view (or control) link for the desktop, DOM or terminal | | `get_preview_url` | A shareable link to one port of a sandbox (a dev server the agent started), 1 minute to 24 hours | | `get_usage` | Usage and cost for a billing period: meters, totals, a projection for the current month, the most expensive sandboxes | | `get_ssh_config` | Signs a 10-minute SSH certificate and returns an `ssh_config` block | | `desktop_screenshot` | PNG of the `desktop` template's screen, as image content | Every call goes to the API with your credential, so your role and your API key's scopes decide what works. A dashboard viewer, or `--read-only`, gets `whoami`, `list_sandboxes`, `get_sandbox`, `get_usage` and view-only `get_live_view_url`. `get_usage` needs the `usage:read` scope on an API key. Background-process output is kept by the MCP server process (the last 4 MiB per stream), so it is lost if the agent restarts the server. ## Check it without an agent Talk to the server over stdio yourself, the way an agent does: ```python mcp_check.py import json, subprocess srv = subprocess.Popen(["anona-compute", "mcp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True) def call(id, method, params=None): srv.stdin.write(json.dumps({"jsonrpc": "2.0", "id": id, "method": method, "params": params or {}}) + "\n") srv.stdin.flush() return json.loads(srv.stdout.readline()) call(1, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "check", "version": "0"}}) srv.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n") print([t["name"] for t in call(2, "tools/list")["result"]["tools"]]) created = call(3, "tools/call", {"name": "create_sandbox", "arguments": {"template": "base", "timeout_seconds": 600}}) sandbox_id = created["result"]["structuredContent"]["sandbox_id"] ran = call(4, "tools/call", {"name": "run_command", "arguments": {"sandbox_id": sandbox_id, "command": "echo hi from mcp"}}) print(ran["result"]["structuredContent"]) call(5, "tools/call", {"name": "kill_sandbox", "arguments": {"sandbox_id": sandbox_id}}) srv.stdin.close() srv.wait() ``` ## Over HTTP For clients that connect to a URL instead of starting a process, serve streamable HTTP on a loopback address. Every request needs the bearer token: ```text ANONA_COMPUTE_MCP_TOKEN=$(openssl rand -hex 24) anona-compute mcp --http 127.0.0.1:8765 # endpoint http://127.0.0.1:8765/mcp, header Authorization: Bearer $ANONA_COMPUTE_MCP_TOKEN ``` Only loopback addresses are accepted, because the server acts with your credentials. Without `ANONA_COMPUTE_MCP_TOKEN` a random token is printed on start. ## Inside a sandbox The `coding-agent` template ships Claude Code, Codex, OpenCode, pi, Kimi Code, Mistral Vibe and Cursor Agent, each wired to a computer-use MCP server and an `anona` skill. Give the agent its provider key as an environment variable when you create the sandbox. See [Templates](/concepts/templates#coding-agent). # Sandboxes > What a sandbox is, how to find it again, and what runs inside. A sandbox is a KVM microVM started from a [template](/concepts/templates). It has its own Linux kernel, a writable root filesystem, and its own network policy. Inside it, E2B's `envd` daemon serves commands, files and terminals on port 49983; the SDKs and the CLI talk to it through the Anona gateway. | Property | Value | |---|---| | ID | An opaque string such as `i3k2…`; use it to connect, kill or view the sandbox | | OS | Ubuntu 24.04 | | User | `user` (uid 1000), home `/home/user`, passwordless `sudo` | | Size | CPU and memory come from the template (for example 2 vCPU and 1 GiB for `base`); the create request has no size fields | | State | `running` or `paused`; a killed sandbox is gone | | Lifetime | A timeout from now, or no deadline; see [Lifecycle](/concepts/lifecycle) | ## Metadata and listing Attach string metadata at create time, then filter on it when you list: ```python metadata.py from anona import Sandbox from e2b import SandboxQuery sbx = Sandbox.create("base", metadata={"job": "nightly-eval", "owner": "ci"}) for info in Sandbox.list(query=SandboxQuery(metadata={"job": "nightly-eval"})).next_items(): print(info.sandbox_id, info.template_id, info.metadata) info = sbx.get_info() print(info.state, info.started_at, info.end_at) sbx.kill() ``` ```typescript metadata.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base", { metadata: { job: "nightly-eval", owner: "ci" } }); const page = await Sandbox.list({ query: { metadata: { job: "nightly-eval" } } }).nextItems(); for (const info of page) console.log(info.sandboxId, info.templateId, info.metadata); const info = await sbx.getInfo(); console.log(info.state, info.startedAt, info.endAt); await sbx.kill(); ``` ```bash metadata.sh SANDBOX_ID=$(anona-compute create -t base --metadata job=nightly-eval --json | jq -r .sandboxID) anona-compute list --metadata job=nightly-eval anona-compute info "$SANDBOX_ID" anona-compute kill "$SANDBOX_ID" ``` Metadata keys starting with `anona.` are reserved and refused. ## Connect from anywhere A sandbox outlives the process that created it. Any process holding an API key of the same organization can reconnect by ID: ```python connect.py import os from anona import Sandbox sbx = Sandbox.connect(os.environ["SANDBOX_ID"]) print(sbx.commands.run("hostname").stdout) ``` ```typescript connect.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.connect(process.env.SANDBOX_ID!); console.log((await sbx.commands.run("hostname")).stdout); ``` Another organization's sandbox answers "not found", exactly like a sandbox that does not exist. # Templates > The images sandboxes start from. A template is the image a sandbox boots from, plus its default size. Every template runs Ubuntu 24.04 with E2B's `envd` 0.5.13 on port 49983, the user `user` (uid 1000) and `tini`, and starts nothing else at boot unless it says so. That keeps idle sandboxes small, which is what keeps them cheap. | Template | Adds | vCPU | Memory | Disk | |---|---|---|---|---| | `base` (default) | git, less, unzip, xz-utils, OpenSSH client and an on-demand SSH server | 2 | 1 GiB | 1 GiB | | `python` | `base` + Python 3.12, build-essential, uv, a venv at `/opt/venv` with numpy, pandas, requests, httpx | 2 | 2 GiB | 2 GiB | | `node` | `base` + Node.js 24 LTS, npm, pnpm (`npm i -g` goes to `~/.npm-global`) | 2 | 2 GiB | 2 GiB | | `code-interpreter` | `python` + Jupyter Server and E2B's code-interpreter server on port 49999 | 2 | 2 GiB | 2 GiB | | `desktop` | Xvfb, XFCE, xdotool, scrot, x11vnc, noVNC on 6080, Google Chrome. Nothing runs until you start the desktop | 2 | 2 GiB | 4 GiB | | `coding-agent` | `python` + Node 24, Bun, Rust, Java 21, gh, Docker CLI (no daemon), Chrome, ripgrep, jq and seven coding-agent CLIs: Claude Code, Codex, OpenCode, pi, Kimi Code, Mistral Vibe, Cursor Agent | 2 | 4 GiB | 8 GiB | Pick one by name: ```python template.py from anona import Sandbox sbx = Sandbox.create("python") print(sbx.get_info().template_id) sbx.kill() ``` ```typescript template.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("python"); console.log((await sbx.getInfo()).templateId); await sbx.kill(); ``` ```bash template.sh anona-compute run -t python -- 'echo running in a python sandbox' anona-compute config set default_template python # the CLI's default for create and run ``` ## code-interpreter The `code-interpreter` template runs E2B's own code-interpreter server (a Python kernel behind Jupyter) on port 49999, the server the `e2b-code-interpreter` SDK talks to. Create it with `Sandbox.create(template="code-interpreter")` from that SDK. Running the `e2b-code-interpreter` SDK end to end against Anona is not part of the published compatibility checks yet. ## coding-agent Each agent CLI in `coding-agent` has the `anona-computer-use` MCP server registered in its own config, and an `anona` skill installed. The computer-use server is a placeholder today: its `screenshot`, `mouse_move`, `click`, `type_text` and `key` tools call `scrot` and `xdotool` and need a running X display. The agents need their own provider credentials; pass them as environment variables at create time. ## Build your own Build a template from a Dockerfile with the CLI, or from the E2B SDK's `Template.build`. The gateway builds the image with BuildKit in an isolated builder, adds the Anona runtime layer (`envd`, the entrypoint and the guest agent), pushes it and registers it; sandboxes then start from it by name. ```bash template_build.sh cat > Dockerfile <<'EOF' FROM python:3.12-slim RUN pip install --no-cache-dir polars EOF anona-compute template build --name my-env # builds the current directory, streams the log anona-compute template list anona-compute template info my-env anona-compute template logs my-env anona-compute create -t my-env # a sandbox from it anona-compute template delete my-env ``` ```python template_build.py from e2b import Template # the stock SDK's build flow works unchanged template = ( Template() .from_image("python:3.12-slim") .run_cmd("pip install --no-cache-dir polars") ) Template.build(template, alias="my-env", cpu_count=2, memory_mb=2048) ``` What the builder enforces: - Every `FROM` is pinned by digest at build time, so a rebuild gets the same base. - `EXPOSE 49983` (envd) is refused; the Anona runtime owns that port. - The build runs with no credentials, no privileges and its own CPU, memory, PID and time limits, on a network whose only way out is a proxy that applies the same [platform floor](/concepts/networking) as a sandbox. - The image's `CMD`/`ENTRYPOINT` runs at sandbox start only when the last stage declares one, or with `--start-cmd`. - `.dockerignore` in the build context is honoured. Defaults and limits: 2 vCPU and 2 GiB per sandbox unless `--cpu`/`--memory` say otherwise (at most 8 vCPU and 16 GiB), 20 templates per organization, 2 builds at a time, and a build context up to 1 GiB. Deleting a template leaves sandboxes that already run from it alone; new creates stop resolving the name. The dashboard's **Templates** page lists templates, build status and build logs, and deletes them. Template builds have been exercised end to end against a real gateway, BuildKit and a local registry (`tests/templates/run_e2e.sh`), with sandbox creation through the fake engine. Registration against CubeSandbox on AWS has not been run yet. Template sizes are the preview defaults and may change. The create request has no CPU or memory fields, in E2B as here; a sandbox gets its template's size. # Lifecycle and long-running sandboxes > Timeouts, no-limit sandboxes, pause and resume. ```mermaid stateDiagram-v2 [*] --> running: create running --> paused: pause, idle, or timeout with on_timeout=pause paused --> running: connect / resume running --> [*]: kill, or timeout (default) paused --> [*]: kill ``` ## Timeouts Every sandbox has a timeout: the number of seconds from now until its timeout action runs. The action is **kill** by default, or **pause** if you ask for it. Each call to `set_timeout` replaces the previous value, counted from that moment. | Where | Default | |---|---| | SDK `Sandbox.create()` | 300 s | | CLI `create`, `run` | `--timeout 300s` | | API `POST /v2/sandboxes` | 300 s | | API `POST /sandboxes` (v1) | 15 s | ```python timeouts.py from anona import Sandbox sbx = Sandbox.create("base", timeout=600) # 10 minutes sbx.set_timeout(3600) # now one hour from this call print(sbx.get_info().end_at) sbx.kill() ``` ```typescript timeouts.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base", { timeoutMs: 600_000 }); // 10 minutes await sbx.setTimeout(3_600_000); // one hour from now console.log((await sbx.getInfo()).endAt); await sbx.kill(); ``` ```bash timeouts.sh SANDBOX_ID=$(anona-compute create -t base --timeout 10m --json | jq -r .sandboxID) anona-compute timeout "$SANDBOX_ID" 1h anona-compute kill "$SANDBOX_ID" ``` ## No time limit On plans without a maximum lifetime, a timeout of `-1` means **no deadline**: the sandbox runs until you pause or kill it. It reports `endAt` as `9999-12-31T23:59:59Z`, because the E2B schema requires a date. A sandbox without a deadline that you pause and resume keeps having no deadline. ```python no_limit.py from anona import Sandbox sbx = Sandbox.create("base", timeout=-1) print(sbx.get_info().end_at) # 9999-12-31 23:59:59+00:00 sbx.kill() ``` ```bash no_limit_api.sh # The REST API takes -1 directly; so do the dashboard and the MCP create_sandbox tool. curl -sS https://api.compute.anonalabs.com/v2/sandboxes \ -H "X-API-Key: $E2B_API_KEY" -H 'Content-Type: application/json' \ -d '{"templateID": "base", "timeout": -1}' | jq -r .sandboxID > sandbox.id curl -sS -X DELETE "https://api.compute.anonalabs.com/sandboxes/$(cat sandbox.id)" -H "X-API-Key: $E2B_API_KEY" ``` A plan with a maximum lifetime (the free plan's is one hour) refuses `-1` and any longer timeout with `400`, naming the limit. The `anona-compute` CLI takes positive durations only. A sandbox without a deadline is billed for as long as it runs. Let it [pause when idle](#pause-when-idle): paused sandboxes cost only snapshot storage. ## Pause and resume Pausing snapshots the sandbox's memory and disk and stops the VM. Resuming restores it: running processes, open files and `/dev/shm` carry on where they were. Paused sandboxes are kept until you kill them; they count against your plan's paused-sandbox limit, and their snapshot storage is billed per GB-month. ```python pause_resume.py from anona import Sandbox sbx = Sandbox.create("base") sbx.commands.run("echo kept > /dev/shm/marker") sbx.pause() sbx = Sandbox.connect(sbx.sandbox_id) # resumes a paused sandbox print(sbx.commands.run("cat /dev/shm/marker").stdout) sbx.kill() ``` ```typescript pause_resume.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base"); await sbx.commands.run("echo kept > /dev/shm/marker"); await sbx.pause(); const again = await Sandbox.connect(sbx.sandboxId); // resumes a paused sandbox console.log((await again.commands.run("cat /dev/shm/marker")).stdout); await again.kill(); ``` ```bash pause_resume.sh SANDBOX_ID=$(anona-compute create -t base --json | jq -r .sandboxID) anona-compute pause "$SANDBOX_ID" anona-compute list --state paused anona-compute resume "$SANDBOX_ID" anona-compute kill "$SANDBOX_ID" ``` On the engine's test hosts, a pause took 0.1 to 0.3 s and a resume 0.8 to 0.9 s, with memory, uptime and a detached process kept. Production numbers are not published yet. ## Pause on timeout Ask for pause instead of kill when the timeout runs out. The sandbox then waits, paused, until you resume it: ```python pause_on_timeout.py from anona import Sandbox sbx = Sandbox.create("base", timeout=3600, lifecycle={"on_timeout": "pause"}) print(sbx.get_info().state) sbx.kill() ``` ```typescript pause_on_timeout.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base", { timeoutMs: 3_600_000, lifecycle: { onTimeout: "pause" } }); console.log((await sbx.getInfo()).state); await sbx.kill(); ``` ```bash pause_on_timeout.sh SANDBOX_ID=$(anona-compute create -t base --timeout 1h --auto-pause --json | jq -r .sandboxID) anona-compute info "$SANDBOX_ID" anona-compute kill "$SANDBOX_ID" ``` `auto_resume` (resume when a request reaches the sandbox's URL) is accepted with pause-on-timeout, but waking on traffic is **Coming soon**: a request to a paused sandbox's URL gets `409` today. Resume with `connect`. ## Pause when idle A sandbox can pause itself once nothing has used it for a while: no request to any of its ports, no open terminal, live view, SSH session or byte pipe, and no running process started through `envd`. Starting to run again (resume) restarts the clock. This is what the dashboard's lifetime **No limit (pauses when idle)** sets up, with 15 minutes. ```python idle_pause.py import os import httpx from anona import Sandbox sbx = Sandbox.create("base", timeout=-1) # no deadline ... api = httpx.Client(base_url="https://api.compute.anonalabs.com", headers={"X-API-Key": os.environ["E2B_API_KEY"]}) r = api.put(f"/v1/sandboxes/{sbx.sandbox_id}/auto-pause", json={"idleMinutes": 15}) # ... but pause after 15 idle minutes r.raise_for_status() print(r.json()) # {"sandboxID": ..., "allowPublicTraffic": ..., "publicPorts": [], "idlePauseMinutes": 15} sbx.kill() ``` ```bash idle_pause_api.sh # Or at create time, over the API: autoPause takes {"idleMinutes": n} (0 = off; {} = the team default, else 15). curl -sS https://api.compute.anonalabs.com/v2/sandboxes -H "X-API-Key: $E2B_API_KEY" -H 'Content-Type: application/json' \ -d '{"templateID": "base", "timeout": -1, "autoPause": {"idleMinutes": 30}}' | jq -r .sandboxID > sandbox.id curl -sS "https://api.compute.anonalabs.com/v1/sandboxes/$(cat sandbox.id)/access" -H "X-API-Key: $E2B_API_KEY" | jq .idlePauseMinutes curl -sS -X DELETE "https://api.compute.anonalabs.com/sandboxes/$(cat sandbox.id)" -H "X-API-Key: $E2B_API_KEY" ``` `idleMinutes` ranges from 1 to 10080 (one week); `0` turns it off. The SDK's `lifecycle={"on_timeout": "pause"}` (`autoPause: true` on the wire) pauses at the timeout, and idles only if your organization has a default idle time. Resume a paused sandbox with `Sandbox.connect(id)`. ## Kill `kill` returns as soon as the gateway has the request (it waits up to 2 s for the engine) and finishes in the background with retries. The sandbox disappears from `list` at once. ## Fork and snapshots **Coming soon:** forking a running sandbox into copies (`anona-compute fork`, E2B `fork`) and named snapshots. # Networking and egress > What a sandbox can reach, and who can reach it. ## Egress Every create compiles a network policy from three layers, strictest first: 1. **The platform floor**, which no setting can open: private and internal ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, loopback, link-local including the cloud metadata address `169.254.169.254`, multicast and reserved ranges, and their IPv6 equivalents), names such as `localhost`, `*.local` and `*.internal`, and the mail and DNS-over-TLS ports 25, 465, 587, 2525 and 853. 2. **Your plan's limits.** Paid plans allow the public internet by default; the free plan allows no public egress. 3. **The sandbox's own policy**: the E2B fields below on create, else your team's egress settings (see [Which policy applies](#which-policy-applies)). | Field | Meaning | |---|---| | `allow_internet_access` | `false` cuts all public egress for this sandbox | | `network.deny_out` | Block these IPv4 addresses or CIDRs. `0.0.0.0/0` blocks all public egress, the same as `allow_internet_access=false` | | `network.allow_out` | Allow these destinations (IPs, CIDRs or host names) even when the rest is blocked. Allow entries win over deny entries | | `network.allow_public_traffic` | Whether the sandbox's ports are reachable without a token (see [Ingress](#ingress)) | ```python egress.py from anona import Sandbox offline = Sandbox.create("base", allow_internet_access=False) offline.kill() only_pypi = Sandbox.create( "base", network={"deny_out": ["0.0.0.0/0"], "allow_out": ["pypi.org", "files.pythonhosted.org"]}, ) only_pypi.kill() no_example = Sandbox.create("base", network={"deny_out": ["93.184.215.14/32"]}) no_example.kill() ``` ```typescript egress.ts import { Sandbox } from "@anona/sdk"; const offline = await Sandbox.create("base", { allowInternetAccess: false }); await offline.kill(); const onlyPypi = await Sandbox.create("base", { network: { denyOut: ["0.0.0.0/0"], allowOut: ["pypi.org", "files.pythonhosted.org"] }, }); await onlyPypi.kill(); ``` ```bash egress.sh anona-compute run --no-internet -t base -- 'echo this sandbox has no internet' ``` Rules the gateway enforces when it compiles your policy: - A destination inside the platform floor is refused with `400`, and so is a host name that resolves into it (this blocks DNS-rebinding tricks). - Host names in `allow_out` need public egress off (`deny_out: ["0.0.0.0/0"]` or `allow_internet_access=false`), as on E2B. Wildcard names and names that do not resolve are refused for now. - `deny_out` takes IPv4 addresses and CIDRs only. - Unknown keys inside `network` are refused, so a typo cannot silently drop part of a policy. - Per plan: up to 512 allow entries and 1,024 deny entries on paid plans (64 and 256 on the free plan), and at most 10 DNS servers. ## Which policy applies A new sandbox gets the first of these that exists, then the platform floor and your plan's limits on top: 1. The `network` and `allow_internet_access` fields of its create request. 2. Its template's policy (built-in templates have none). 3. Your organization's **team egress settings**: set them on the dashboard's **Network** page (owners and admins), or with `PUT /v1/network`. 4. Your plan's default. Ask the API which policy a sandbox runs with, and where it came from: ```bash effective_policy.sh curl -sS "https://api.compute.anonalabs.com/v1/sandboxes/$SANDBOX_ID/network/effective" \ -H "X-API-Key: $E2B_API_KEY" | jq '{source, tier, internet: .policy.allow_internet_access}' curl -sS https://api.compute.anonalabs.com/v1/network -H "X-API-Key: $E2B_API_KEY" | jq '{tier, revision, limits}' ``` ## Change a running sandbox `update_network` replaces a running sandbox's policy. It goes through the same floor and limits as a create. An update that does not mention `allow_internet_access` keeps the sandbox's current setting. ```python update_network.py from anona import Sandbox with Sandbox.create("base") as sbx: # lock it down to one API after setup sbx.update_network({"deny_out": ["0.0.0.0/0"], "allow_out": ["1.1.1.1"]}) ``` ```typescript update_network.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base"); await sbx.updateNetwork({ denyOut: ["0.0.0.0/0"], allowOut: ["1.1.1.1"] }); await sbx.kill(); ``` The caller needs to be able to control the sandbox: an API key with `sandboxes:use` (and the sandbox on its list, if it has one), or a dashboard member or above. ## Credentials without exposing them The **credential vault** keeps API tokens out of sandboxes. Store a secret on the dashboard's **Credential vault** page (owners and admins), then name it in a header rule for one host. The egress proxy adds the header to the sandbox's HTTPS requests to that host; the sandbox, its environment and its disk never contain the value. ```python vault_rule.py from anona import Sandbox # "github_token" was stored in the dashboard's Credential vault first. sbx = Sandbox.create( "base", network={ "deny_out": ["0.0.0.0/0"], "allow_out": ["api.github.com"], "rules": {"api.github.com": [{"transform": {"headers": {"Authorization": "Bearer ${vault.github_token}"}}}]}, }, ) print(sbx.commands.run("env | grep -ci token || true").stdout.strip(), "token variables in the sandbox") sbx.kill() ``` ```typescript vault_rule.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base", { network: { denyOut: ["0.0.0.0/0"], allowOut: ["api.github.com"], rules: { "api.github.com": [{ transform: { headers: { Authorization: "Bearer ${vault.github_token}" } } }] }, }, }); await sbx.kill(); ``` - A header value must be exactly `${vault.}`, optionally after `Bearer ` or `Basic `. Literal values are refused, so no credential ends up in a policy. - The first use binds the secret to that host and header. The vault page lists every binding and revokes one. Proxies cache a value for up to 60 seconds, so a rotation or revocation reaches running sandboxes within a minute. - The same rules work in the team egress settings, so every new sandbox gets them. - Secret names are lowercase letters, digits and `_`, up to 63 characters; values up to 8 KiB. The vault is included in every plan at no charge. ## Ingress All traffic to a sandbox goes through the Anona gateway's sandbox edge at `https://-.sbx.compute.anonalabs.com`: | Port | Who gets in | |---|---| | 49983 (`envd`: commands, files) | Only requests with the sandbox's access token, which the SDK holds; signed file URLs for `GET`/`POST /files` | | Any other port | Anyone, when the sandbox allows public traffic or the port is public; otherwise a valid preview link (or its cookie), or the `e2b-traffic-access-token` header | Single ports can also be made public, or shared with signed, expiring links; see [Preview links](/guides/preview-links). Sandbox URLs live on their own domain, so content served from a sandbox never shares cookies with the API or the dashboard. ## DNS Sandboxes resolve names through public resolvers (1.1.1.1 and 1.0.0.1), so they cannot resolve names inside Anona's network. # Security and isolation > How sandboxes are kept apart from each other and from Anona. | Layer | What it does | |---|---| | **A microVM per sandbox** | Each sandbox is a KVM virtual machine with its own guest kernel. Escaping it takes a hypervisor escape, not a container escape. | | **Network policy per sandbox** | An eBPF filter (layer 3) and an egress proxy (layer 7) enforce the [egress policy](/concepts/networking). Private ranges and cloud metadata are always blocked. | | **A gateway in front of everything** | The engine's own API is never exposed. Every byte to or from a sandbox passes the gateway's sandbox edge, which checks a per-sandbox token. | | **Per-sandbox tokens** | The `envd` access token and the traffic token are derived per sandbox; a token from another organization, or from a deleted sandbox that had the same ID, never matches. | | **Tenant isolation** | A sandbox of another organization answers "not found", the same as one that does not exist, so IDs cannot be probed. | | **Scoped API keys** | Keys carry scopes (read, use, create, delete, usage, billing, integrations) and can be limited to a list of sandbox IDs. See [API keys in CI](/guides/ci). | | **Revocable sessions** | Dashboard and CLI sessions can be revoked on the dashboard; the API stops accepting them within seconds. | | **Short-lived access** | Live-view tokens last at most 10 minutes and can all be revoked at once; SSH certificates last 10 minutes and name one sandbox. | ## Inside the sandbox - Commands run as `user` (uid 1000), with passwordless `sudo` inside the VM. Root inside the VM is not root on the host. - Templates start no daemons at boot beyond `envd` and the Anona guest agent (which carries live view and SSH). The SSH server starts on first use and listens on loopback only. - Sandboxes have public DNS resolvers and no route to Anona's internal network. ## Credentials Secrets passed as environment variables (`envs=`, or per command) live in the sandbox's memory and disk like any other data, and a pause snapshots them too. For API tokens, use the **credential vault** instead: the egress proxy adds the token to requests to one host and the sandbox never sees it. See [Networking](/concepts/networking#credentials-without-exposing-them). # Regions and limits > Where sandboxes run and how many you can have. ## Region Sandboxes run in AWS `us-east-1` (N. Virginia). The API, the dashboard and live view are served from the same region. **Coming soon:** more regions. ## Plan limits Preview values. They will change before general availability. | Limit | Free | Paid | |---|---|---| | Running sandboxes at once | 2 | 100 | | Paused sandboxes kept | 5 | 200 | | Longest timeout | 1 hour | No maximum (`timeout=-1` allowed) | | Public internet egress | Off | On by default | | API keys | 2 | 20 | | Templates you build | 20 | 20 | | Template builds at once | 2 | 2 | | Included credit | $5 | none | What the gateway answers when you hit one: | Situation | Response | |---|---| | Creating more running sandboxes than your plan allows | `429` with `error_code` `sandbox_quota_exceeded` | | Pausing beyond the paused limit | `429` | | A timeout above your plan's maximum | `400`, naming the limit | | Creating an API key beyond the limit | `429` | Per-sandbox size limits, per-minute create rates and request rates exist in the plans but are not enforced by the gateway yet. Sandbox size comes from the [template](/concepts/templates). ## Cold starts When no sandbox has run for a while, the sandbox servers are stopped to save cost. The next create waits while one starts (up to 60 s); if that takes longer, the API answers `503` with `Retry-After: 30` and the SDK retries. Paused sandboxes do not need a running server until you resume them. How long a cold start takes on the production fleet has not been measured yet, so the preview has no published create or resume latency. On the engine's test hosts a resume took 0.8 to 0.9 s. # Run code > Run commands, read exit codes, set the environment and the working directory. `commands.run` runs a shell command (`/bin/bash -l -c`) in the sandbox and waits for it. It returns stdout, stderr and the exit code; a non-zero exit raises an exception you can catch. ```python run_code.py from anona import Sandbox from e2b import CommandExitException with Sandbox.create("base") as sbx: r = sbx.commands.run("echo $GREETING from $(pwd)", envs={"GREETING": "hi"}, cwd="/tmp") print(r.stdout.strip(), r.exit_code) sbx.files.write("/tmp/fib.sh", "a=0; b=1; for i in $(seq 10); do echo $a; t=$((a+b)); a=$b; b=$t; done\n") print(sbx.commands.run("bash /tmp/fib.sh | tail -1").stdout.strip()) print(sbx.commands.run("whoami", user="root").stdout.strip()) try: sbx.commands.run("exit 3") except CommandExitException as e: print("exit code", e.exit_code) ``` ```typescript run_code.ts import { Sandbox } from "@anona/sdk"; import { CommandExitError } from "e2b"; const sbx = await Sandbox.create("base"); const r = await sbx.commands.run("echo $GREETING from $(pwd)", { envs: { GREETING: "hi" }, cwd: "/tmp" }); console.log(r.stdout.trim(), r.exitCode); console.log((await sbx.commands.run("whoami", { user: "root" })).stdout.trim()); try { await sbx.commands.run("exit 3"); } catch (e) { if (e instanceof CommandExitError) console.log("exit code", e.exitCode); else throw e; } await sbx.kill(); ``` ```bash run_code.sh # anona-compute exec exits with the command's exit code anona-compute exec "$SANDBOX_ID" -e GREETING=hi -w /tmp -- 'echo $GREETING from $(pwd)' anona-compute exec "$SANDBOX_ID" -u root -- whoami anona-compute exec "$SANDBOX_ID" -- 'exit 3' || echo "exit code $?" # one-shot: create, run, kill (also on error or Ctrl-C) anona-compute run -t base -- uname -a ``` | Option | Python | TypeScript | CLI | |---|---|---|---| | Environment | `envs={...}` | `envs: {...}` | `-e KEY=VALUE` | | Working directory | `cwd=` | `cwd:` | `-w` | | User | `user="root"` | `user: "root"` | `-u root` | | Stop waiting after | `timeout=` (seconds, default 60, `0` = none) | `timeoutMs:` | `--cmd-timeout 5m` | Sandbox-wide environment variables go on create: `Sandbox.create("base", envs={"API_TOKEN": "…"})` or `anona-compute create -e API_TOKEN=…`. ## Install packages Every template has `sudo` for `user`. Install what you need at runtime: ```python install.py from anona import Sandbox with Sandbox.create("base") as sbx: r = sbx.commands.run("sudo apt-get update -qq && sudo apt-get install -y -qq cowsay >/dev/null && /usr/games/cowsay moo", timeout=300) print(r.stdout) ``` For Python and Node work, start from the `python` or `node` template so the toolchain is already there. Pause the sandbox between runs to keep what you installed. ## JSON output from the CLI `--json` prints one JSON object per line, which suits scripts: ```bash exec_json.sh anona-compute exec "$SANDBOX_ID" --json -- 'echo out; echo err >&2' # {"type":"stdout","data":"out\n"} # {"type":"stderr","data":"err\n"} # {"type":"exit","exitCode":0} ``` # Files > Read, write, list and watch files; upload and download with signed URLs. The `files` API reads and writes the sandbox filesystem as the sandbox user. Paths are absolute, or relative to `/home/user`. ```python files.py from anona import Sandbox with Sandbox.create("base") as sbx: sbx.files.make_dir("/home/user/project") sbx.files.write("/home/user/project/notes.txt", "first line\n") sbx.files.write("/home/user/project/blob.bin", bytes(range(256))) print(sbx.files.read("/home/user/project/notes.txt")) print(len(sbx.files.read("/home/user/project/blob.bin", format="bytes"))) for entry in sbx.files.list("/home/user/project"): print(entry.type, entry.name, entry.size) sbx.files.rename("/home/user/project/notes.txt", "/home/user/project/README.txt") print(sbx.files.exists("/home/user/project/notes.txt"), sbx.files.get_info("/home/user/project/README.txt").size) sbx.files.remove("/home/user/project/blob.bin") ``` ```typescript files.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base"); await sbx.files.makeDir("/home/user/project"); await sbx.files.write("/home/user/project/notes.txt", "first line\n"); console.log(await sbx.files.read("/home/user/project/notes.txt")); for (const entry of await sbx.files.list("/home/user/project")) console.log(entry.type, entry.name); await sbx.files.rename("/home/user/project/notes.txt", "/home/user/project/README.txt"); console.log(await sbx.files.exists("/home/user/project/notes.txt")); await sbx.kill(); ``` ## Watch a directory ```python watch.py import time from anona import Sandbox with Sandbox.create("base") as sbx: sbx.files.make_dir("/tmp/inbox") handle = sbx.files.watch_dir("/tmp/inbox") sbx.files.write("/tmp/inbox/job.json", "{}") time.sleep(1) print([(e.type.name, e.name) for e in handle.get_new_events()]) handle.stop() ``` ## Signed upload and download URLs Hand a URL to a browser or another service so it can move a file without your API key. The URL is signed for one path and expires. ```python signed_urls.py import httpx from anona import Sandbox with Sandbox.create("base", secure=True) as sbx: up = sbx.upload_url("/home/user/report.csv", use_signature_expiration=300) httpx.post(up, files={"file": ("report.csv", b"a,b\n1,2\n")}).raise_for_status() down = sbx.download_url("/home/user/report.csv", use_signature_expiration=300) print(httpx.get(down).text) ``` ## Copy files with the CLI Over SSH (see [SSH](/guides/ssh)): ```bash scp.sh echo "local data" > data.txt anona-compute scp "$SANDBOX_ID" ./data.txt :/home/user/ anona-compute scp "$SANDBOX_ID" :/home/user/data.txt ./copy.txt cat copy.txt ``` **Coming soon:** `anona-compute push`, which copies a git working tree into a sandbox. # Processes and streaming > Stream output as it happens, run servers in the background, talk to stdin, open a terminal. ## Stream output Pass callbacks to receive output while the command runs: ```python stream.py from anona import Sandbox with Sandbox.create("base") as sbx: sbx.commands.run( "for i in 1 2 3; do echo tick $i; sleep 0.5; done; echo done >&2", on_stdout=lambda line: print("out:", line.strip()), on_stderr=lambda line: print("err:", line.strip()), ) ``` ```typescript stream.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base"); await sbx.commands.run("for i in 1 2 3; do echo tick $i; sleep 0.5; done; echo done >&2", { onStdout: (line) => console.log("out:", line.trim()), onStderr: (line) => console.log("err:", line.trim()), }); await sbx.kill(); ``` ## Background processes `background=True` returns a handle at once. The process keeps running after your script exits, for as long as the sandbox lives; reconnect to it by PID from anywhere. ```python background.py from anona import Sandbox with Sandbox.create("base") as sbx: server = sbx.commands.run("sleep 300", background=True) print("pid", server.pid) for p in sbx.commands.list(): print(p.pid, p.cmd, p.args) sbx.commands.kill(server.pid) ``` ```typescript background.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.create("base"); const proc = await sbx.commands.run("sleep 300", { background: true }); console.log("pid", proc.pid); console.log((await sbx.commands.list()).map((p) => p.pid)); await sbx.commands.kill(proc.pid); await sbx.kill(); ``` ```bash background.sh PID=$(anona-compute exec "$SANDBOX_ID" --background -- 'sleep 300') echo "started $PID" anona-compute exec "$SANDBOX_ID" -- "kill $PID" ``` ## Write to stdin ```python stdin.py from anona import Sandbox with Sandbox.create("base") as sbx: proc = sbx.commands.run("read name; echo hello $name", background=True, stdin=True) sbx.commands.send_stdin(proc.pid, "world\n") print(proc.wait().stdout) ``` ## Terminals (PTY) A PTY gives you a real terminal: colours, line editing and full-screen programs. ```python pty.py from anona import Sandbox from e2b import PtySize with Sandbox.create("base") as sbx: out = [] term = sbx.pty.create(PtySize(rows=24, cols=80)) sbx.pty.send_stdin(term.pid, b"echo $TERM; exit\n") term.wait(on_pty=out.append) print(b"".join(out).decode(errors="replace")) ``` From the terminal, `anona-compute connect` opens an interactive login shell in the sandbox over the same channel, with no SSH server involved: ```text anona-compute connect $SANDBOX_ID # Ctrl-] then d detaches; the shell keeps running ``` To watch a terminal in a browser instead, see [Live view](/guides/live-view). # Live view > Watch or take over a sandbox's terminal or desktop from your own page. Live view streams a sandbox to a browser: its terminal, or its desktop. Your backend mints a short-lived **viewer token** with your API key, and your page shows the returned embed URL in an iframe. Many people can watch the same sandbox; the sandbox serves one stream per view, whatever the number of viewers. | Mode (`channels`) | Shows | Status | |---|---|---| | `pty` (embed mode `terminal`) | A shell in the sandbox (xterm.js). Needs `control` scope: it starts a shell | Available | | `desktop` | The `desktop` template's screen (noVNC) | Available | | `dom` | The page open in the sandbox's Chrome, recorded with rrweb and replayed in your browser: light, sharp at any size, passwords masked. Templates with Chrome (`desktop`, `coding-agent`); Chrome is started with DevTools on first use if it is not running | Available | | `thumb` | A 320 px WebP of the screen (or of the active Chrome page) every 3 seconds | Available | Scopes: `view` watches; `control` also sends keystrokes, mouse and resize. Tokens last 30 to 600 seconds (default 600). ## Mint a token ```python live_view.py import os from anona import Sandbox sbx = Sandbox.connect(os.environ["SANDBOX_ID"]) url = sbx.live.url(mode="pty", scope="control", ttl=300) # terminal you can type into print(url) tok = sbx.live.token(scope="view", channels=["desktop"], ttl=120) print(tok.expires_at, tok.claims()["ch"]) sbx.live.revoke_all() # closes every open viewer, invalidates issued tokens ``` ```typescript live_view.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.connect(process.env.SANDBOX_ID!); const url = await sbx.live.url({ mode: "pty", scope: "control", ttl: 300 }); console.log(url); const tok = await sbx.live.token({ scope: "view", channels: ["desktop"], ttl: 120 }); console.log(tok.expiresAt, tok.claims().ch); await sbx.live.revokeAll(); ``` ```bash live_view.sh anona-compute view "$SANDBOX_ID" --mode terminal --control --ttl 5m anona-compute view "$SANDBOX_ID" --mode desktop --json ``` The embed URL looks like `https://stream.compute.anonalabs.com/embed/v1/?mode=terminal#t=`. The token is in the URL fragment, which browsers never send to a server, so it does not end up in logs. ## Embed it Mint the token on your server (never ship your API key to a browser) and put the URL in an iframe: ```html ``` ### Keep a long session alive A token expires after at most 10 minutes. The embed asks its host page for a new one shortly before that, with `postMessage`. Answer it with a fresh token from your backend: ```js const frame = document.querySelector("iframe"); const streamOrigin = "https://stream.compute.anonalabs.com"; window.addEventListener("message", async (event) => { if (event.origin !== streamOrigin || event.data?.source !== "anona-viewer") return; if (event.data.type === "token-request") { const { token } = await fetch("/api/viewer-token").then((r) => r.json()); // your backend mints it frame.contentWindow.postMessage( { source: "anona-viewer-host", type: "token", id: event.data.id, token }, streamOrigin, ); } if (event.data.type === "state") console.log("viewer", event.data.state); }); ``` The viewer reconnects on its own after network drops. It stops for good when the token is revoked, the sandbox is gone, or the token does not allow the channel. ## Thumbnails For lists and dashboards, fetch the latest thumbnail directly. It is a WebP while the sandbox has a screen or a Chrome page to show, and `204 No Content` otherwise: ```bash thumbnail.sh code=$(curl -sS -o thumb.webp -w '%{http_code}' \ "https://api.compute.anonalabs.com/v1/sandboxes/$SANDBOX_ID/thumbnail" -H "X-API-Key: $E2B_API_KEY") echo "thumbnail: HTTP $code" # 200 with image/webp, or 204 when there is nothing to show ``` The sandbox grabs frames only while someone asks for them, and every viewer shares the same frames. Send `If-None-Match` with the last `ETag` to get `304` when nothing changed. ## Who may mint | Caller | `view` | `control` | |---|---|---| | API key with `sandboxes:use` | Yes | Yes | | Dashboard owner, admin, member | Yes | Yes | | Dashboard viewer | Yes | No | A key limited to certain sandboxes can mint only for those. A sandbox of another organization answers `404`. The dashboard's sandbox page has the same live view built in. # SSH and VS Code Remote > Plain OpenSSH into a sandbox: shells, scp, rsync, port forwarding and editors. Every template that includes `base` has an OpenSSH server that starts on the first connection. You log in with a **certificate** that the Anona gateway signs for one sandbox and 10 minutes; there are no passwords and no long-lived keys to manage. The connection is carried over Anona's stream relay, so the sandbox needs no public port. ## With the CLI `anona-compute ssh` makes a throwaway key, has it signed, and runs your system `ssh` through the relay: ```bash ssh.sh anona-compute ssh "$SANDBOX_ID" -- 'uname -a; id' ``` Everything after `--` goes to `ssh`: a remote command, or flags such as `-L 8080:localhost:8080` to forward a port. Interactive sessions work too: `anona-compute ssh $SANDBOX_ID`. Copy files with `scp` semantics; sandbox paths start with a colon: ```bash scp_tree.sh mkdir -p out && echo hi > out/a.txt anona-compute scp "$SANDBOX_ID" -- -r ./out :/home/user/ anona-compute ssh "$SANDBOX_ID" -- 'ls -R /home/user/out' ``` ## ssh_config for other tools `--print-config` prints an `ssh_config` block with a fresh certificate, so `ssh`, `rsync` and editors can use `Host ` directly: ```bash ssh_config.sh anona-compute ssh "$SANDBOX_ID" --print-config > anona-sandbox.conf ssh -F anona-sandbox.conf "$SANDBOX_ID" 'echo connected with plain ssh' ``` To use it from every tool, append it to `~/.ssh/config` instead: `anona-compute ssh --print-config >> ~/.ssh/config`. The certificate in the block expires after 10 minutes. Run `anona-compute ssh --print-config` again (or any `anona-compute ssh`) to renew it; connections already open stay open. ### VS Code Remote-SSH 1. Install the Remote-SSH extension. 2. Run `anona-compute ssh --print-config >> ~/.ssh/config`. 3. In VS Code, run **Remote-SSH: Connect to Host…** and pick the sandbox ID. VS Code installs its server in `/home/user/.vscode-server` inside the sandbox, so the sandbox needs internet egress to download it. If you reconnect later than 10 minutes after step 2, repeat step 2 first. Editors that read `~/.ssh/config` (Cursor, JetBrains Gateway, Zed) take the same block. The plain `ssh` connection above is part of the docs' automated checks; the editor flow itself has not been run in them yet. ## With the SDK Sign your own key and write a config block, for tools that manage their own SSH: ```python ssh_sdk.py import os, pathlib, subprocess from anona import Sandbox key = pathlib.Path.home() / ".ssh" / "anona_ed25519" key.parent.mkdir(exist_ok=True) if not key.exists(): subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)], check=True) sbx = Sandbox.connect(os.environ["SANDBOX_ID"]) cert = sbx.ssh.certificate(f"{key}.pub") cert.write(f"{key}-cert.pub") print(cert.user, cert.host, cert.expires_at) print(sbx.ssh.config(identity_file=str(key))) ``` ```typescript ssh_sdk.ts import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { Sandbox } from "@anona/sdk"; const key = `${homedir()}/.ssh/anona_ed25519`; mkdirSync(`${homedir()}/.ssh`, { recursive: true }); if (!existsSync(key)) execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-f", key]); const sbx = await Sandbox.connect(process.env.SANDBOX_ID!); const cert = await sbx.ssh.certificate(readFileSync(`${key}.pub`, "utf8")); writeFileSync(`${key}-cert.pub`, cert.cert + "\n", { mode: 0o600 }); console.log(cert.user, cert.host, cert.expiresAt); console.log(sbx.ssh.config({ identityFile: key })); ``` The block uses `ProxyCommand anona-compute ssh --proxy %h`, so the CLI must be on your `PATH` and signed in. ## Details | | | |---|---| | Login user | `user` | | Certificate | Ed25519 CA, valid 10 minutes, for one sandbox (principal `sbx:`), allows a PTY and port forwarding | | Who can get one | API keys with `sandboxes:use`, dashboard members and above | | Host key | Generated in the sandbox; clients use `StrictHostKeyChecking accept-new`. **Coming soon:** host certificates | | CA public key | `https://api.compute.anonalabs.com/.well-known/anona-ssh-user-ca.pub` | Without SSH, `anona-compute connect ` opens a shell over the sandbox's own terminal API; see [Processes](/guides/processes#terminals-pty). # Desktop and computer use > A Linux desktop your agent can see and drive, and you can watch. The `desktop` template has an XFCE desktop on Xvfb, Google Chrome, `xdotool` and `scrot`, and VNC servers. Nothing runs until you start it, so an idle desktop sandbox costs the same as any other. It is compatible with E2B's `e2b-desktop` SDK, which starts the desktop on create and gives you screenshots, mouse and keyboard. ## Drive it from code ```python desktop.py from anona.desktop import Sandbox # e2b_desktop.Sandbox plus live view and SSH desk = Sandbox.create() # template "desktop", starts Xvfb and XFCE try: print(desk.get_screen_size()) desk.move_mouse(200, 150) desk.left_click() desk.write("hello") png = desk.screenshot() open("screen.png", "wb").write(png) print("screenshot", len(png), "bytes", png[:4]) print("watch it:", desk.live_url()) # Anona live view, control scope finally: desk.kill() ``` ```typescript desktop.ts import { Sandbox as Desktop } from "@e2b/desktop"; import { Sandbox } from "@anona/sdk"; const desk = await Desktop.create(); // template "desktop", starts Xvfb and XFCE try { console.log(await desk.getScreenSize()); await desk.moveMouse(200, 150); await desk.leftClick(); const png = await desk.screenshot(); console.log("screenshot", png.length, "bytes"); const sbx = await Sandbox.connect(desk.sandboxId); // the Anona extras on the same sandbox console.log("watch it:", await sbx.live.desktopUrl()); } finally { await desk.kill(); } ``` Install the Python extra with `pip install "anona[desktop]"`; for TypeScript add `@e2b/desktop` next to `@anona/sdk`. ## Watch it There are two ways to see the screen: | | Anona live view | E2B noVNC stream | |---|---|---| | How | `desk.live_url()` (Python), `sbx.live.desktopUrl()` (TypeScript), `anona-compute desktop ` | `desk.stream.start()` then `desk.stream.get_url()` | | Transport | Anona's stream relay; the sandbox opens no port | noVNC on port 6080 of the sandbox URL | | Access | Short-lived viewer token; view-only viewers get a separate view-only VNC server | Port URL; see [Preview links](/guides/preview-links) | | Embedding | See [Live view](/guides/live-view) | E2B's own page | ## From the terminal ```text anona-compute desktop $SANDBOX_ID # print the live-view URL (control) anona-compute desktop $SANDBOX_ID --vnc 127.0.0.1:5901 # serve the desktop as plain VNC for any VNC client anona-compute desktop $SANDBOX_ID --vnc 127.0.0.1:5901 --view-only anona-compute desktop $SANDBOX_ID --mode novnc # the E2B noVNC URL on port 6080 ``` ## For agents - Over MCP, `desktop_screenshot` returns the screen as an image; see [Agents via MCP](/guides/mcp). - The `coding-agent` template has a computer-use MCP server registered with each agent CLI. It is a placeholder today (`screenshot`, `mouse_move`, `click`, `type_text`, `key` through `scrot` and `xdotool`). **Coming soon:** a computer-use API in the Anona guest agent. # Ports and preview links > Share a server running inside a sandbox: signed links, public ports, or the traffic token. Every port of a sandbox has a URL: `https://-.sbx.compute.anonalabs.com`. The SDK builds it for you with `get_host`. Who can open it depends on how you share it: | Way | Who gets in | Good for | |---|---|---| | **Preview link** | Anyone with the link, for one port, until it expires (1 minute to 24 hours) | Showing a dev server or app to a person | | **Public port** | Anyone, on that port, until you make it private again | Webhook receivers, demos | | **Traffic token** | Requests carrying the `e2b-traffic-access-token` header | Your own backend or proxy | | **Public sandbox** (`allow_public_traffic`) | Anyone, on every port | Throwaway previews | Port 49983 (`envd`, which runs commands and files) is never shared: it always needs the sandbox's own access token, which the SDK holds. ## Preview links ```bash preview.sh anona-compute preview "$SANDBOX_ID" 3000 # prints a link, valid 1 hour anona-compute preview "$SANDBOX_ID" 3000 --ttl 15m anona-compute preview "$SANDBOX_ID" --revoke # ends every link (and live-view token) of the sandbox ``` ```python preview_link.py import os import httpx api = httpx.Client(base_url="https://api.compute.anonalabs.com", headers={"X-API-Key": os.environ["E2B_API_KEY"]}) sid = os.environ["SANDBOX_ID"] link = api.post(f"/v1/sandboxes/{sid}/preview-links", json={"port": 3000, "ttlSeconds": 900}).json() print(link["url"].split("?")[0], "expires", link["expiresAt"]) api.post(f"/v1/sandboxes/{sid}/preview-links:revoke-all").raise_for_status() ``` The link looks like `https://3000-.sbx.compute.anonalabs.com/?anona_sig=…&anona_exp=…`. On the first visit the sandbox edge exchanges it for a cookie that is scoped to that one port's host (`Secure`, `HttpOnly`, `SameSite=Lax`) and redirects to the same URL without the signature, so the app inside never sees it. Other ports and other sandboxes stay private. Revoking ends every link and every live-view token of the sandbox within a second. The dashboard's sandbox page has **Share preview**, and agents get links from the MCP tool `get_preview_url`. ## Public ports ```bash public_port.sh anona-compute preview "$SANDBOX_ID" 8080 --public # anyone can open port 8080, no link needed curl -sS "https://api.compute.anonalabs.com/v1/sandboxes/$SANDBOX_ID/access" -H "X-API-Key: $E2B_API_KEY" | jq -c . anona-compute preview "$SANDBOX_ID" 8080 --private ``` Up to 64 public ports per sandbox. `PUT /v1/sandboxes/{id}/ports/{port}` with `{"public": true}` does the same over the API. ## Traffic token For your own services, keep the port private and send the sandbox's traffic token: ```python private_port.py import httpx from anona import Sandbox with Sandbox.create("python", network={"allow_public_traffic": False}) as sbx: sbx.commands.run("cd /tmp && python3 -m http.server 8000", background=True) sbx.commands.run("for i in $(seq 50); do curl -s localhost:8000 >/dev/null && break; sleep 0.1; done") url = "https://" + sbx.get_host(8000) print(httpx.get(url).status_code) # refused without the token print(httpx.get(url, headers={"e2b-traffic-access-token": sbx.traffic_access_token}).status_code) ``` A request to a paused sandbox's URL gets `409`; resume it with `Sandbox.connect` first. # Webhooks and telemetry export > Signed callbacks for sandbox lifecycle events, and sandbox metrics in your own OpenTelemetry backend. ## Webhooks Anona Compute sends an HTTPS `POST` to your endpoint when a sandbox is created, paused, resumed, killed or runs into trouble. Set endpoints up on the dashboard's **Webhooks** page, or with the API. ### Events | Type | When | |---|---| | `sandbox.lifecycle.created` | Create finished; the sandbox runs | | `sandbox.lifecycle.paused` | Paused, memory and disk kept | | `sandbox.lifecycle.resumed` | Resumed from pause | | `sandbox.lifecycle.killed` | Killed, by request or at its timeout | | `sandbox.lifecycle.updated` | Its timeout changed | | `sandbox.error` | Create failed, or the sandbox or its host was lost | | `sandbox.degraded` | Its host is being evacuated | | `sandbox.recovered` | Running again after an evacuation | | `webhook.test` | Only from **Send test event** | The body follows E2B's `SandboxEvent` shape: ```json { "id": "6a0d5b0e-3f0c-4f6e-9a55-1b2f0c7d9e11", "version": "v1", "type": "sandbox.lifecycle.paused", "timestamp": "2026-10-28T12:00:04Z", "sandboxId": "i3f9a...", "sandboxExecutionId": "...", "sandboxTemplateId": "base", "sandboxBuildId": "", "sandboxTeamId": "...", "eventData": { "cpuCount": 2, "memoryMB": 2048, "timeoutSeconds": 300 } } ``` `eventData` can also carry `endReason` (`killed`, `timeout`, `failed`, `lost`) and `reason`. ### Manage endpoints with the API Owners and admins manage webhooks in the dashboard. For automation, create a **custom** API key with the `integrations:manage` scope; no preset includes it. ```python webhooks_api.py import os import httpx api = httpx.Client(base_url="https://api.compute.anonalabs.com", headers={"X-API-Key": os.environ["E2B_API_KEY"]}) r = api.post("/v1/webhooks", json={ "name": "lifecycle to my app", "url": "https://example.com/anona/webhooks", "events": ["sandbox.lifecycle.created", "sandbox.lifecycle.killed"], }) r.raise_for_status() hook, secret = r.json()["webhook"], r.json()["secret"] # secret: whsec_..., shown once print(hook["id"], hook["events"], secret[:6] + "...") api.post(f"/v1/webhooks/{hook['id']}/test").raise_for_status() # queue a webhook.test event print([d["eventType"] for d in api.get(f"/v1/webhooks/{hook['id']}/deliveries").json()["deliveries"]]) api.patch(f"/v1/webhooks/{hook['id']}", json={"enabled": False}).raise_for_status() api.delete(f"/v1/webhooks/{hook['id']}").raise_for_status() ``` URLs must be `https://`, with a public host name: private, loopback and cloud-metadata addresses are refused when you save the URL, and again at every send (a name that later resolves inside is blocked). Redirects are never followed. Up to 10 endpoints per organization. ### Delivery - **At least once.** Deduplicate on the `webhook-id` header (the event ID; the same on every retry and every endpoint). - **No ordering.** Use `timestamp`. - **Success** is any `2xx` within 10 seconds; anything else, including a redirect, is a failed attempt. - **Retries** back off exponentially from 30 seconds up to 2 hours between attempts. After 10 attempts (about 4 hours) a delivery is dead. Resend it from the dashboard or with `POST /v1/webhooks/{id}/deliveries/{deliveryId}/resend`. - Events and the delivery log are kept for 30 days. ### Verify the signature Deliveries are signed with [Standard Webhooks](https://www.standardwebhooks.com), so its libraries (`standardwebhooks` on PyPI and npm, and others) verify them. By hand: | Header | Value | |---|---| | `webhook-id` | The event ID | | `webhook-timestamp` | Unix seconds when this attempt was signed | | `webhook-signature` | `v1,`, where `key` is the base64 part of your `whsec_` secret | Verify the **raw** body before parsing it, reject timestamps more than 5 minutes off, compare in constant time, and deduplicate on `webhook-id`. For 24 hours after you rotate a secret, the header carries two signatures separated by a space; accept the request if either matches. ```python receiver.py import base64, hashlib, hmac, json, os, time from flask import Flask, request, abort SECRET = os.environ["ANONA_WEBHOOK_SECRET"] # whsec_... KEY = base64.b64decode(SECRET.removeprefix("whsec_")) app = Flask(__name__) @app.post("/anona/webhooks") def hook(): msg_id = request.headers.get("webhook-id", "") ts = request.headers.get("webhook-timestamp", "0") body = request.get_data() # raw bytes if not msg_id or abs(time.time() - int(ts)) > 300: abort(400) mac = hmac.new(KEY, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest() want = "v1," + base64.b64encode(mac).decode() if not any(hmac.compare_digest(s, want) for s in request.headers.get("webhook-signature", "").split(" ")): abort(401) event = json.loads(body) # deduplicate on event["id"] print(event["type"], event["sandboxId"]) return "", 200 ``` ```js receiver.mjs import { createHmac, timingSafeEqual } from "node:crypto"; import express from "express"; const secret = process.env.ANONA_WEBHOOK_SECRET; // whsec_... const app = express(); app.post("/anona/webhooks", express.raw({ type: "application/json" }), (req, res) => { const id = req.header("webhook-id"); const ts = req.header("webhook-timestamp"); const sigs = (req.header("webhook-signature") ?? "").split(" "); if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const want = "v1," + createHmac("sha256", key).update(`${id}.${ts}.`).update(req.body).digest("base64"); const ok = sigs.some((s) => s.length === want.length && timingSafeEqual(Buffer.from(s), Buffer.from(want))); if (!ok) return res.sendStatus(401); const event = JSON.parse(req.body); // deduplicate on event.id console.log(event.type, event.sandboxId); res.sendStatus(200); }); app.listen(8080); ``` Rotate a secret with **Rotate secret** in the dashboard or `POST /v1/webhooks/{id}/rotate-secret`; the old secret keeps signing for 24 hours. ## OpenTelemetry export Send every sandbox's metrics and lifecycle spans to your own OTLP/HTTP backend (Grafana, Honeycomb, Datadog, an OpenTelemetry Collector, ...). Set it on the dashboard's **Telemetry** page, or: ```bash otel_export.sh curl -sS -X PUT https://api.compute.anonalabs.com/v1/otel-export \ -H "X-API-Key: $E2B_API_KEY" -H 'Content-Type: application/json' \ -d '{"endpoint": "https://example.com/otlp", "headers": {"Authorization": "Bearer my-backend-token"}, "enabled": true}' \ | jq '{endpoint: .export.endpoint, headers: .export.headerNames, every: .intervalSeconds}' curl -sS -X DELETE https://api.compute.anonalabs.com/v1/otel-export -H "X-API-Key: $E2B_API_KEY" -o /dev/null -w '%{http_code}\n' ``` Payloads (protobuf) go to `/v1/metrics` and `/v1/traces` every 60 seconds. Header values are encrypted at rest and never returned; leave `headers` out of an update to keep them. | Metric | Type | Unit | |---|---|---| | `anona.sandbox.cpu.utilization` | Gauge, per minute | fraction of the sandbox's vCPUs | | `anona.sandbox.cpu.time` | Sum, delta | s | | `anona.sandbox.memory.usage` | Gauge (mean resident over the minute) | By | | `anona.sandbox.memory.limit` | Gauge | By | | `anona.sandbox.vcpus` | Gauge | `{vcpu}` | | `anona.sandbox.network.io` | Sum, delta, `network.io.direction` = `transmit` / `receive` | By | Resource attributes: `service.name=anona-sandbox`, `anona.tenant.id`, `anona.sandbox.id`, `anona.template.id`. Each lifecycle event is a span from the sandbox's previous event to this one; all spans of a sandbox share one trace. `sandbox.error` spans have status ERROR. If your endpoint is down, metrics older than an hour and events older than a day are dropped rather than replayed; the dashboard shows the last error. The same access rules apply as for webhooks: owners and admins, or a custom key with `integrations:manage`. # CI and API keys > Use sandboxes from CI with a scoped, expiring API key. ## Create a key for CI Create keys in the [dashboard](https://app.compute.anonalabs.com) under **API keys** (owners and admins). Give CI its own key, with the least access it needs and an expiry: | Preset | Scopes | For | |---|---|---| | Read only | `sandboxes:read`, `templates:read`, `usage:read` | Dashboards, monitoring, cost reports | | Use existing | `sandboxes:read`, `sandboxes:use` | Driving sandboxes someone else created | | Create and use | `sandboxes:read`, `sandboxes:use`, `sandboxes:create`, `sandboxes:delete`, `templates:read` | CI jobs that create and clean up their own sandboxes | | Full access | `sandboxes:*`, `templates:*`, `usage:read`, `billing:read` | Trusted automation | | Custom | Any of `sandboxes:read`, `sandboxes:use`, `sandboxes:create`, `sandboxes:delete`, `templates:read`, `templates:write`, `usage:read`, `billing:read`, `integrations:manage` | `integrations:manage` (webhooks, telemetry export) is only in custom keys | Options on every key: - **Expiry**: 7, 30, 90 or 365 days, or never. - **Sandbox list**: limit the key to specific sandbox IDs. Other sandboxes answer "not found" to it. The secret is shown once. Revoking a key stops it at once. Keys created before the `usage:read` and `billing:read` scopes existed do not have them. Key management is a dashboard action: API keys cannot create, list or revoke keys (the `/v1/api-keys` routes accept dashboard sessions only, see the [API reference](/api-reference/introduction)). ## Use it in a job Store the key as a CI secret and expose it as `E2B_API_KEY` (or `ANONA_API_KEY`) together with `E2B_DOMAIN`: ```yaml # .github/workflows/sandbox.yml name: sandbox on: [push] jobs: test-in-sandbox: runs-on: ubuntu-latest env: E2B_DOMAIN: compute.anonalabs.com E2B_API_KEY: ${{ secrets.ANONA_COMPUTE_API_KEY }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install e2b - run: python ci/run_in_sandbox.py ``` A job script that always cleans up, even when the test fails: ```python run_in_sandbox.py import sys from anona import Sandbox from e2b import CommandExitException with Sandbox.create("base", timeout=900, metadata={"ci": "true"}) as sbx: # killed on exit sbx.files.write("/home/user/test.sh", "set -e\necho running tests\ntest 2 -gt 1\n") try: r = sbx.commands.run("bash /home/user/test.sh", timeout=600) print(r.stdout) except CommandExitException as e: print(e.stdout, e.stderr, file=sys.stderr) sys.exit(e.exit_code) ``` ```bash run_in_sandbox.sh # anona-compute run creates, runs, and kills the sandbox (also on failure or Ctrl-C), # and exits with the command's exit code. export ANONA_COMPUTE_API_KEY="$E2B_API_KEY" anona-compute run -t base --timeout 15m --metadata ci=true -- 'echo running tests && test 2 -gt 1' ``` Set a timeout on every CI sandbox, so a cancelled job cannot leave one running. Tag CI sandboxes with metadata, then sweep leftovers: ```bash sweep.sh anona-compute list --metadata ci=true --json | jq -r .sandboxID | while read -r id; do anona-compute kill "$id" done ``` ## The CLI with a key The CLI takes `--api-key`, else `ANONA_COMPUTE_API_KEY` or `E2B_API_KEY`, else the profile; a key wins over a browser sign-in. To keep one in a profile: ```bash key_profile.sh printenv E2B_API_KEY | anona-compute --profile ci login --with-api-key anona-compute --profile ci whoami ``` # Recipes > Start from what you are building: each recipe is a short path through the docs. Each recipe says what you want, which pieces to use and in what order. The details live in the guides it links to. - [Run code an agent wrote](/recipes/run-untrusted-code): One throwaway microVM per task, killed when it finishes. - [A dev box for a coding agent](/recipes/coding-agent-box): Claude Code or Codex inside a sandbox, with previews you can open. - [An agent that runs for days](/recipes/long-running-agent): No deadline, pause when idle, resume with memory intact. - [Let an agent use a desktop](/recipes/desktop-agent): Screenshots, mouse and keyboard, and you watching. - [Test every push in a sandbox](/recipes/ci-per-push): A scoped, expiring key and a sandbox per job. - [Your own environment](/recipes/custom-environment): Build a template from a Dockerfile and start sandboxes from it. # Run code an agent wrote > One throwaway microVM per task, killed when it finishes. **You want** to execute code you did not write, such as an LLM's output, without risking your machine or other users. **Why a sandbox fits.** Each sandbox is a KVM microVM with its own kernel, so a bad script can fill its own disk and nothing else. See [Security](/concepts/security). ### Create one sandbox per task Pick the smallest [template](/concepts/templates) that has what the code needs: `python` for scripts, `node` for JavaScript. Set a short timeout so a forgotten sandbox is killed for you. ### Run, read, and clean up ```python run_untrusted.py from anona import Sandbox with Sandbox.create("python", timeout=120) as sbx: sbx.files.write("/home/user/task.py", "print(sum(range(10)))\n") result = sbx.commands.run("python /home/user/task.py") print(result.stdout, result.exit_code) ``` The `with` block kills the sandbox on exit. ### Decide what it may reach Sandboxes can reach the internet by default, minus private ranges and cloud metadata, which are always blocked. Turn egress off or allow only the hosts you name: [Networking](/concepts/networking). Give API tokens through the credential vault so the code never sees them. **Next:** [Run code](/guides/run-code) for exit codes and environment, [Processes](/guides/processes) for streaming output. # A dev box for a coding agent > Claude Code or Codex inside a sandbox, with previews you can open. **You want** a coding agent to work in its own machine: install packages, run servers, break things. **Pieces.** The `coding-agent` [template](/concepts/templates#coding-agent) ships seven agent CLIs (Claude Code, Codex, OpenCode, pi, Kimi Code, Mistral Vibe, Cursor Agent), Node, Python, Bun, Rust, Java, `gh`, Chrome, `ripgrep` and `jq`, with 2 vCPU, 4 GiB and 8 GiB of disk. ### Create the sandbox with the agent's key Pass the provider key as an environment variable at create time. The agents need their own credentials. ```bash coding_box.sh SANDBOX_ID=$(anona-compute create -t coding-agent --timeout 1h --json | jq -r .sandboxID) echo "$SANDBOX_ID" anona-compute kill "$SANDBOX_ID" ``` ### Work in it Open a terminal with [`anona-compute ssh`](/guides/ssh) or watch and take over from the browser with [live view](/guides/live-view). ### Show what it built When the agent starts a dev server on a port, share it with a [preview link](/guides/preview-links): `anona-compute preview "$SANDBOX_ID" 3000`. The link works for anyone who has it, until it expires (1 minute to 24 hours). **Driving it from your own agent.** Register the [MCP server](/guides/mcp) so an outer agent can create these boxes and hand work to the one inside. # An agent that runs for days > No deadline, pause when idle, resume with memory intact. **You want** an agent sandbox that survives overnight and over weekends without paying for the hours nobody uses it. **How it works.** Pricing counts active CPU and resident memory, and a paused sandbox costs only snapshot storage ([Pricing](/pricing)). So give the sandbox no deadline and let it pause itself. ### Create with no deadline A timeout of `-1` means no deadline, on plans without a maximum lifetime. The free plan allows one hour at most. See [Lifecycle](/concepts/lifecycle#no-time-limit). ### Pause when idle Set idle minutes (1 to 10080) on create or later. The sandbox pauses once nothing has used it: no request, no open terminal, live view or SSH, no running process started through `envd`. The dashboard's **No limit (pauses when idle)** does the same with 15 minutes. ### Come back ```python come_back.py from anona import Sandbox sbx = Sandbox.create("base", timeout=-1) sbx.commands.run("echo kept > /dev/shm/note") sbx.pause() sbx = Sandbox.connect(sbx.sandbox_id) # resumes it print(sbx.commands.run("cat /dev/shm/note").stdout) sbx.kill() ``` Running processes, open files and `/dev/shm` carry on where they were. A sandbox without a deadline is billed for as long as it runs. Always set idle pause, or kill it. # Let an agent use a desktop > Screenshots, mouse and keyboard, and you watching. **You want** a computer-use agent: it looks at a screen, clicks, types, and you can see what it does. **Pieces.** The `desktop` [template](/concepts/templates) has XFCE on Xvfb, Chrome, `xdotool` and `scrot`. Nothing runs until you start the desktop, so an idle one costs the same as any other sandbox. ### Start a desktop and take a screenshot ```python desktop_agent.py from anona.desktop import Sandbox desk = Sandbox.create() try: print(desk.get_screen_size()) open("screen.png", "wb").write(desk.screenshot()) finally: desk.kill() ``` ### Loop: look, decide, act Send the screenshot to your model, then call `move_mouse`, `left_click`, `write` or the other actions it returns. [Desktop](/guides/desktop) lists them. ### Watch it `desk.live_url()` returns a live-view link. With `control` scope you can take over. For web pages only, the `dom` channel replays the page with passwords masked: [Live view](/guides/live-view). # Test every push in a sandbox > A scoped, expiring key and a sandbox per job. **You want** CI jobs that run tests in a clean machine and leave nothing behind. ### Make a key for CI In the [dashboard](https://app.compute.anonalabs.com), create a key with the **Create and use** preset and a 30 or 90 day expiry. It can create, use and delete sandboxes, and nothing else. See [CI and API keys](/guides/ci). ### Store it as a secret Expose it as `E2B_API_KEY` with `E2B_DOMAIN=compute.anonalabs.com`. The stock `e2b` SDK works as is. ### Create, run, kill Create a sandbox per job, copy the repository in, run the tests, and kill the sandbox in a step that always runs. The CLI's `exec` and `run` exit with the remote command's exit code, so a failing test fails the job. **Next:** [CI and API keys](/guides/ci) has a complete GitHub Actions workflow. # Your own environment > Build a template from a Dockerfile and start sandboxes from it. **You want** sandboxes that start with your dependencies already installed. ### Write a Dockerfile Any base image works. `EXPOSE 49983` is refused because the Anona runtime owns that port, and every `FROM` is pinned by digest at build time. ### Build it ```text build_template anona-compute template build --name my-env anona-compute template logs my-env ``` ### Start from it ```text start_from_template anona-compute create -t my-env ``` Limits: 20 templates per organization, 2 builds at a time, a build context up to 1 GiB. Details and the SDK route: [Templates](/concepts/templates#build-your-own). Template builds are verified against a real gateway and BuildKit, but registration against CubeSandbox on AWS has not been run yet. # anona-compute CLI > Sign in, manage sandboxes, run commands, SSH, live view and an MCP server for agents. `anona-compute` is one static binary for Linux, macOS and Windows (amd64 and arm64). It is called `anona-compute` because `anona` is the Anona Memory CLI; the two have separate sign-ins and settings. **Coming soon:** downloadable release binaries. ## Sign in ```text anona-compute login # browser sign-in (device code); --no-browser prints the URL only anona-compute whoami # user, organization, role anona-compute logout # revokes this machine's session on the dashboard ``` Or use an API key, which wins over a browser sign-in: ```bash cli_key.sh printenv ANONA_API_KEY | anona-compute login --with-api-key # saves it in the profile anona-compute whoami ANONA_COMPUTE_API_KEY=$ANONA_API_KEY anona-compute list # or E2B_API_KEY, or --api-key ``` ## A tour ```bash cli_tour.sh SANDBOX_ID=$(anona-compute create -t base --timeout 30m --json | jq -r .sandboxID) anona-compute list anona-compute info "$SANDBOX_ID" anona-compute exec "$SANDBOX_ID" -- 'echo hello > /tmp/x && cat /tmp/x' anona-compute timeout "$SANDBOX_ID" 1h anona-compute pause "$SANDBOX_ID" anona-compute resume "$SANDBOX_ID" anona-compute view "$SANDBOX_ID" --mode terminal --control anona-compute kill "$SANDBOX_ID" ``` | Task | Commands | |---|---| | Lifecycle | [`create`](/cli/reference/create), [`list`](/cli/reference/list), [`info`](/cli/reference/info), [`pause`](/cli/reference/pause), [`resume`](/cli/reference/resume), [`timeout`](/cli/reference/timeout), [`kill`](/cli/reference/kill) | | Run things | [`exec`](/cli/reference/exec), [`run`](/cli/reference/run), [`connect`](/cli/reference/connect), [`logs`](/cli/reference/logs) | | Access | [`ssh`](/cli/reference/ssh), [`scp`](/cli/reference/scp), [`view`](/cli/reference/view), [`desktop`](/cli/reference/desktop), [`preview`](/cli/reference/preview) | | Agents | [`mcp`](/cli/reference/mcp) | | Account | [`login`](/cli/reference/login), [`logout`](/cli/reference/logout), [`whoami`](/cli/reference/whoami), [`usage`](/cli/reference/usage), [`config`](/cli/reference/config) | | Templates | [`template build`](/cli/reference/template-build), [`list`](/cli/reference/template-list), [`info`](/cli/reference/template-info), [`logs`](/cli/reference/template-logs), [`delete`](/cli/reference/template-delete) | | Coming soon | [`fork`](/cli/reference/fork), [`keys`](/cli/reference/keys), [`limits`](/cli/reference/limits), [`push`](/cli/reference/push), [`recover`](/cli/reference/recover) | `sandbox` (alias `sbx`) groups the lifecycle commands: `anona-compute sbx ls` is `anona-compute list`. ## Output and exit codes `--json` (or `-o json`) prints one JSON object per line on stdout; messages go to stderr. `exec`, `run` and `connect` exit with the remote command's exit code, so the CLI composes in shell scripts and CI. # Configuration and profiles > Where the CLI keeps settings and credentials, and the variables it reads. Every setting resolves in this order: the flag, then the environment, then the selected profile in the config file. | Setting | Flag | Environment | Profile key | Default | |---|---|---|---|---| | API key | `--api-key` | `ANONA_COMPUTE_API_KEY`, `E2B_API_KEY` (and `ANONA_API_KEY` when it holds an `e2b_` key) | `api_key` | browser sign-in | | Domain | | `ANONA_COMPUTE_DOMAIN`, `E2B_DOMAIN` | `domain` | `compute.anonalabs.com` | | API URL | `--api-url` | `ANONA_COMPUTE_API_URL`, `E2B_API_URL` | `api_url` | `https://api.` | | Dashboard URL | `--web-url` (login) | `ANONA_COMPUTE_WEB_URL` | `web_url` | `https://app.` | | Default template | `-t` | | `default_template` | `base` | | Profile | `--profile` | `ANONA_COMPUTE_PROFILE` | | `default` | ```bash cli_config.sh anona-compute config set default_template python anona-compute config get default_template anona-compute --profile staging config set domain compute.anonalabs.com anona-compute config list # every profile; API keys are masked ``` ## Files | What | Where | |---|---| | Settings | `$XDG_CONFIG_HOME/anona-compute/config.toml` (usually `~/.config/anona-compute/config.toml`; `ANONA_COMPUTE_CONFIG` overrides), mode 0600 | | Browser sign-in secret | The OS keychain (macOS Keychain, Windows Credential Manager, Secret Service on Linux), else `credentials.json` (mode 0600) next to `config.toml`. `ANONA_COMPUTE_CREDENTIAL_STORE=file` or `keyring` forces one | | SSH keys for `anona-compute ssh` | `$XDG_RUNTIME_DIR/anona-compute/ssh/`, ephemeral | The browser sign-in keeps a dashboard session for the machine (shown on the dashboard's **CLI & agent sessions** page as `CLI on `) and trades it for an API token that lasts 15 minutes. Signing out, revoking the session on that page, removal from the organization and role changes reach the API within seconds, also for tokens already issued; after a role change the CLI (and a running MCP server) fetches a new token on its own. The CLI never reads Anona Memory's settings or keys, and never sends an Anona Memory key to the compute API. # anona-compute > Manage Anona Compute sandboxes from the terminal ```text anona-compute manages Anona Compute sandboxes through the E2B-compatible API. Configuration comes from flags, then the environment (ANONA_COMPUTE_API_KEY or E2B_API_KEY, ANONA_COMPUTE_DOMAIN or E2B_DOMAIN, ANONA_COMPUTE_API_URL or E2B_API_URL), then the profile in $XDG_CONFIG_HOME/anona-compute/config.toml. ``` ## Usage ```text anona-compute [flags] anona-compute [command] ``` ## Commands | Command | Description | |---|---| | [`config`](/cli/reference/config) | Read and write profile settings | | [`connect`](/cli/reference/connect) | Open an interactive shell in a sandbox | | [`create`](/cli/reference/create) | Create sandboxes | | [`desktop`](/cli/reference/desktop) | Open a sandbox desktop | | [`exec`](/cli/reference/exec) | Run a command in a sandbox | | [`fork`](/cli/reference/fork) | Fork a running sandbox into new sandboxes | | [`info`](/cli/reference/info) | Show one sandbox | | [`keys`](/cli/reference/keys) | Manage API keys (not available yet) | | [`kill`](/cli/reference/kill) | Kill sandboxes | | [`limits`](/cli/reference/limits) | Show plan limits and usage (not available yet) | | [`list`](/cli/reference/list) | List sandboxes | | [`login`](/cli/reference/login) | Sign in through the browser (or save an API key with --with-api-key) | | [`logout`](/cli/reference/logout) | Sign out: revoke the CLI session on the dashboard and remove local credentials | | [`logs`](/cli/reference/logs) | Show sandbox logs | | [`mcp`](/cli/reference/mcp) | Run an MCP server so AI agents can use Anona Compute | | [`pause`](/cli/reference/pause) | Pause a sandbox | | [`preview`](/cli/reference/preview) | Share a sandbox port with a signed preview link | | [`push`](/cli/reference/push) | Push a git working tree into a sandbox (not available yet) | | [`recover`](/cli/reference/recover) | Recover a sandbox from its last snapshot (not available yet) | | [`resume`](/cli/reference/resume) | Resume a paused sandbox | | [`run`](/cli/reference/run) | Create a sandbox, run one command, then kill the sandbox | | [`sandbox`](/cli/reference/sandbox) | Sandbox commands (same as the top-level create, list, info, ...) | | [`scp`](/cli/reference/scp) | Copy files to or from a sandbox over SSH | | [`ssh`](/cli/reference/ssh) | SSH into a sandbox | | [`template`](/cli/reference/template) | Build and manage templates | | [`timeout`](/cli/reference/timeout) | Set how long a sandbox lives from now | | [`usage`](/cli/reference/usage) | Show usage and cost for a billing period | | [`version`](/cli/reference/version) | Print the CLI version | | [`view`](/cli/reference/view) | Open a live view of a sandbox | | [`whoami`](/cli/reference/whoami) | Show who the CLI is signed in as | ## Flags | Flag | Type | Description | |---|---|---| | `--api-key` | string | API key (env ANONA_COMPUTE_API_KEY, E2B_API_KEY) | | `--api-url` | string | API URL (env ANONA_COMPUTE_API_URL, E2B_API_URL; default https://api.<E2B_DOMAIN>) | | `--help`, `-h` | | help for anona-compute | | `--json` | | JSON output: one object per line on stdout (same as -o json) | | `--output`, `-o` | string | output format: text or json (default "text") | | `--profile` | string | config profile to use (env ANONA_COMPUTE_PROFILE; default "default") | | `--request-timeout` | duration | timeout for each API request (0 = none) | | `--verbose`, `-v` | | verbose output on stderr | # anona-compute create > Create sandboxes ```text Create one or more sandboxes (POST /v2/sandboxes) and print their IDs. CPU and memory come from the template; the E2B create request has no fields for them. ``` ## Usage ```text anona-compute create [flags] ``` **Aliases:** `create`, `new` ## Flags | Flag | Type | Description | |---|---|---| | `--auto-pause` | | pause instead of kill when the timeout ends | | `--count` | int | number of sandboxes to create (1-100) (default 1) | | `--env`, `-e` | stringArray | environment variable KEY=VALUE for the sandbox (repeatable) | | `--fail-fast` | | with --count, stop at the first failure | | `--help`, `-h` | | help for create | | `--metadata` | stringArray | metadata KEY=VALUE (repeatable) | | `--no-internet` | | create the sandbox without internet access | | `--template`, `-t` | string | template ID or alias (default: profile default_template, else "base") | | `--timeout` | string | sandbox lifetime before it is killed or paused (e.g. 300s, 1h) (default "300s") | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute list > List sandboxes ```text List sandboxes ``` ## Usage ```text anona-compute list [flags] ``` **Aliases:** `list`, `ls` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for list | | `--limit` | int | show at most N sandboxes (0 = all) | | `--metadata` | stringArray | only sandboxes with metadata KEY=VALUE (repeatable) | | `--state`, `-s` | string | running, paused or all (default "running") | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute info > Show one sandbox ```text Show one sandbox ``` ## Usage ```text anona-compute info SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for info | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute pause > Pause a sandbox `--mode filesystem` is refused by the gateway: filesystem-only snapshots are not available, so pause always keeps memory. ```text Pause a sandbox (POST /sandboxes/{id}/pause). --mode full keeps memory, so running processes continue after resume. --mode filesystem keeps only the disk; resume cold-boots the sandbox. ``` ## Usage ```text anona-compute pause SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for pause | | `--mode` | string | full (memory and disk) or filesystem (disk only) (default "full") | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute resume > Resume a paused sandbox ```text Resume a paused sandbox (POST /v2/sandboxes/{id}/connect). A running sandbox is left running; its timeout is only ever extended. ``` ## Usage ```text anona-compute resume SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for resume | | `--timeout` | string | sandbox lifetime from now (default "300s") | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute timeout > Set how long a sandbox lives from now The CLI takes positive durations. To remove the deadline (`-1`), use the API, the dashboard or the MCP `extend_sandbox` tool; see [Lifecycle](/concepts/lifecycle). ```text Set the sandbox timeout (POST /sandboxes/{id}/timeout). The sandbox ends DURATION from now (e.g. 600, 10m, 2h); each call replaces the previous value. ``` ## Usage ```text anona-compute timeout SANDBOX_ID DURATION [flags] ``` **Aliases:** `timeout`, `extend` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for timeout | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute kill > Kill sandboxes ```text Kill sandboxes ``` ## Usage ```text anona-compute kill SANDBOX_ID... [flags] ``` **Aliases:** `kill`, `rm`, `delete` ## Flags | Flag | Type | Description | |---|---|---| | `--all` | | kill every running and paused sandbox | | `--help`, `-h` | | help for kill | | `--yes` | | confirm --all | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute fork > Fork a running sandbox into new sandboxes **Coming soon.** The gateway does not serve `POST /sandboxes/{id}/fork` yet; the command reports that the server does not support fork. ```text Fork a sandbox (POST /sandboxes/{id}/fork): checkpoint it with its memory and start --count new sandboxes from that checkpoint. Prints one line per fork. ``` ## Usage ```text anona-compute fork SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--count` | int | number of forks (1-20) (default 1) | | `--help`, `-h` | | help for fork | | `--timeout` | string | lifetime of each fork (empty = server default, 15s in the E2B spec) (default "300s") | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute sandbox > Sandbox commands (same as the top-level create, list, info, ...) ```text Sandbox commands (same as the top-level create, list, info, ...) ``` ## Usage ```text anona-compute sandbox [flags] anona-compute sandbox [command] ``` **Aliases:** `sandbox`, `sbx` ## Commands | Command | Description | |---|---| | [`sandbox create`](/cli/reference/create) | Create sandboxes | | [`sandbox exec`](/cli/reference/exec) | Run a command in a sandbox | | [`sandbox fork`](/cli/reference/fork) | Fork a running sandbox into new sandboxes | | [`sandbox info`](/cli/reference/info) | Show one sandbox | | [`sandbox kill`](/cli/reference/kill) | Kill sandboxes | | [`sandbox list`](/cli/reference/list) | List sandboxes | | [`sandbox logs`](/cli/reference/logs) | Show sandbox logs | | [`sandbox pause`](/cli/reference/pause) | Pause a sandbox | | [`sandbox resume`](/cli/reference/resume) | Resume a paused sandbox | | [`sandbox timeout`](/cli/reference/timeout) | Set how long a sandbox lives from now | ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for sandbox | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute exec > Run a command in a sandbox ```text Run a command in a running sandbox and stream its output. anona-compute exits with the command's exit code. With one argument the command is a shell string (anona-compute exec ID -- 'ls | wc -l'); several arguments are quoted and joined. With --json, output is one object per line: {"type":"stdout"|"stderr","data":...} and finally {"type":"exit","exitCode":N}. ``` ## Usage ```text anona-compute exec SANDBOX_ID -- COMMAND [ARGS...] [flags] ``` **Aliases:** `exec`, `ex` ## Flags | Flag | Type | Description | |---|---|---| | `--background`, `-b` | | start the command, print its PID and return | | `--cmd-timeout` | duration | stop waiting for the command after this long (0 = no limit) | | `--cwd`, `-w` | string | working directory | | `--env`, `-e` | stringArray | environment variable KEY=VALUE for the command (repeatable) | | `--help`, `-h` | | help for exec | | `--user`, `-u` | string | user to run as (default: the sandbox's default user) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute run > Create a sandbox, run one command, then kill the sandbox ```text Create a sandbox, run one command in it, print a summary on stderr, and kill the sandbox (also on error or Ctrl-C). anona-compute exits with the command's exit code. Cost is printed when the API reports it; the current API does not, so the summary says "unknown" rather than guessing. ``` ## Usage ```text anona-compute run [flags] -- COMMAND [ARGS...] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--auto-pause` | | pause instead of kill when the timeout ends | | `--cmd-timeout` | duration | stop waiting for the command after this long (0 = no limit) | | `--cwd`, `-w` | string | working directory | | `--env`, `-e` | stringArray | environment variable KEY=VALUE for the sandbox (repeatable) | | `--help`, `-h` | | help for run | | `--keep` | | do not kill the sandbox after the command | | `--metadata` | stringArray | metadata KEY=VALUE (repeatable) | | `--no-internet` | | create the sandbox without internet access | | `--template`, `-t` | string | template ID or alias (default: profile default_template, else "base") | | `--timeout` | string | sandbox lifetime before it is killed or paused (e.g. 300s, 1h) (default "300s") | | `--user`, `-u` | string | user to run as (default: the sandbox's default user) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute connect > Open an interactive shell in a sandbox ```text Open an interactive login shell (/bin/bash -i -l) on a PTY through envd, the same channel the E2B SDK's sandbox.pty uses; no sshd is needed. The local terminal is put in raw mode and resizes follow it. Type Ctrl-] then d to detach (the shell keeps running). anona-compute exits with the shell's exit code. ``` ## Usage ```text anona-compute connect SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--cwd`, `-w` | string | working directory | | `--help`, `-h` | | help for connect | | `--user`, `-u` | string | user to run the shell as (default: the sandbox's default user) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute logs > Show sandbox logs ```text Show sandbox logs (GET /v2/sandboxes/{id}/logs), oldest first. --follow keeps polling for new lines until the sandbox ends or you press Ctrl-C (exit 0). ``` ## Usage ```text anona-compute logs SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--follow`, `-f` | | keep printing new lines | | `--help`, `-h` | | help for logs | | `--level` | string | minimum level: debug, info, warn or error | | `--search` | string | only lines containing this text (case-sensitive) | | `--since` | string | only lines newer than this (e.g. 10m, 1h) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute ssh > SSH into a sandbox ```text Open an OpenSSH session to a sandbox. anona-compute makes an ephemeral Ed25519 key under $XDG_RUNTIME_DIR/anona-compute/ssh/, has the gateway sign it (a 10-minute certificate for the sandbox user), and runs the system ssh with ProxyCommand "anona-compute ssh --proxy %n", which carries the connection over the stream relay. Arguments after -- go to ssh (a remote command, -L, -R, ...). --print-config prints an ssh_config block, so scp, rsync and editors can use "Host " directly; the certificate in it expires after 10 minutes, so run it again (or anona-compute ssh) to renew. ``` ## Usage ```text anona-compute ssh SANDBOX_ID [-- SSH_ARGS...] [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for ssh | | `--print-config` | | print an ssh_config block for the sandbox instead of connecting | | `--proxy` | | act as the OpenSSH ProxyCommand: pipe stdin/stdout to the sandbox sshd | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute scp > Copy files to or from a sandbox over SSH ```text Copy files with the system scp through the same certificate and relay as anona-compute ssh. Write sandbox paths with a leading colon: anona-compute scp sbx_123 ./data.csv :/home/user/ anona-compute scp sbx_123 -- -r :/home/user/out ./out ``` ## Usage ```text anona-compute scp SANDBOX_ID [SCP_FLAGS...] SOURCE... TARGET [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for scp | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute view > Open a live view of a sandbox ```text Mint a live-view token and print the embed URL (the token is in the URL fragment, which browsers never send to a server). Modes: dom (browser agents), terminal (a new shell; needs --control) and desktop. ``` ## Usage ```text anona-compute view SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--control` | | allow input (control scope) | | `--help`, `-h` | | help for view | | `--mode` | string | dom, terminal or desktop (default "dom") | | `--ttl` | duration | token lifetime, 30s to 10m (default 10m0s) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute desktop > Open a sandbox desktop ```text Reach the desktop of a sandbox (desktop template). anona-compute desktop ID print the live-view URL (control) anona-compute desktop ID --vnc 127.0.0.1:5901 serve the desktop as plain VNC (RFB) on a local port for any VNC client; each client gets its own session through the relay anona-compute desktop ID --mode novnc print the E2B-compatible noVNC URL on port 6080 (started by the e2b-desktop SDK) ``` ## Usage ```text anona-compute desktop SANDBOX_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for desktop | | `--mode` | string | live (Anona relay) or novnc (E2B noVNC URL) (default "live") | | `--view-only` | | with --vnc: view scope (the sandbox serves a view-only VNC server) | | `--vnc` | string | serve the desktop as VNC on this local address (for example 127.0.0.1:5901) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute preview > Share a sandbox port with a signed preview link ```text Print a shareable link to one port of a sandbox, for example a dev server on 3000. Anyone with the link can open that port in a browser until it expires (--ttl, default 1h, at most 24h): the sandbox edge exchanges it for a cookie scoped to that port's host. Other ports and envd stay private. anona-compute preview 3000 anona-compute preview 3000 --ttl 15m anona-compute preview --revoke end every link (and live-view token) of the sandbox anona-compute preview 3000 --public serve the port to anyone, no link needed anona-compute preview 3000 --private back to links only ``` ## Usage ```text anona-compute preview SANDBOX_ID [PORT] [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for preview | | `--private` | | make the port private again | | `--public` | | make the port public instead of printing a link | | `--revoke` | | revoke every preview link and live-view token of the sandbox | | `--ttl` | duration | link lifetime, 1m to 24h (default 1h) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute mcp > Run an MCP server so AI agents can use Anona Compute ```text Serve the Model Context Protocol so AI agents (Claude Code, Codex, Cursor and other MCP clients) can create sandboxes, run commands, move files and kill sandboxes with this CLI's credentials (anona-compute login, or an API key). By default the server speaks MCP over stdio; the agent starts it: claude mcp add anona-compute -- anona-compute mcp codex mcp add anona-compute -- anona-compute mcp --http 127.0.0.1:8765 serves streamable HTTP at http://127.0.0.1:8765/mcp instead. Only loopback addresses are allowed, and every request must carry Authorization: Bearer , where the token is ANONA_COMPUTE_MCP_TOKEN or a random one printed on stderr at start. Every tool call goes through the gateway with your credential, so it can do exactly what you can. A viewer (or --read-only) gets the read-only tools only. ``` ## Usage ```text anona-compute mcp [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for mcp | | `--http` | string | serve streamable HTTP on this loopback address (for example 127.0.0.1:8765) instead of stdio | | `--read-only` | | offer only the tools that do not change sandboxes | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute login > Sign in through the browser (or save an API key with --with-api-key) ```text Sign in to Anona Compute. Without flags, anona-compute login starts a device sign-in (RFC 8628): it prints a URL and a code, opens the browser, and waits while you sign in to the dashboard, check the code, pick the organization and approve. The CLI then holds a dashboard session for this machine and trades it for gateway tokens that last 15 minutes. The session secret is stored in the OS keychain when one is available, else in credentials.json (mode 0600) next to config.toml; ANONA_COMPUTE_CREDENTIAL_STORE=file|keyring forces one. anona-compute logout revokes it. --with-api-key reads an API key (e2b_ + lowercase hex) from stdin and saves it to the profile instead. The dashboard is --web-url, else ANONA_COMPUTE_WEB_URL, else the profile's web_url, else https://app.. ``` ## Usage ```text anona-compute login [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for login | | `--no-browser` | | print the sign-in URL but do not open a browser | | `--org` | string | organization slug to act in (default: the one picked in the browser) | | `--web-url` | string | dashboard URL (env ANONA_COMPUTE_WEB_URL; default https://app.<domain>) | | `--with-api-key` | | read an API key from stdin instead of signing in through the browser | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute logout > Sign out: revoke the CLI session on the dashboard and remove local credentials ```text Sign out of the selected profile. A browser login's session is deleted on the dashboard first (gateway tokens already issued from it stop being renewed and expire within 15 minutes), then removed from the keychain or credentials file. An API key saved with --with-api-key is removed from the profile (revoke the key itself in the dashboard). ``` ## Usage ```text anona-compute logout [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for logout | | `--local` | | only remove local credentials; do not contact the dashboard | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute whoami > Show who the CLI is signed in as ```text Show the account behind the current credentials. For a browser login this asks the dashboard (user, organization, role); for an API key it checks the key against the gateway. ``` ## Usage ```text anona-compute whoami [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for whoami | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute usage > Show usage and cost for a billing period ```text Show the organization's usage and cost for a billing period (GET /v1/usage on the gateway): meters, totals with the part not yet rated, a projection for the current month, and the most expensive sandboxes. Browser logins of any role may read usage; API keys need the usage:read scope. --billing prints the plan, rates and invoices instead (GET /v1/billing): owners and admins, or keys with billing:read. --json prints the gateway's whole document. ``` ## Usage ```text anona-compute usage [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--billing` | | show plan, rates and invoices instead (owners, admins, keys with billing:read) | | `--help`, `-h` | | help for usage | | `--period` | string | billing period: current or YYYY-MM (default "current") | | `--top` | int | list this many of the most expensive sandboxes (0 = none) (default 5) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute limits > Show plan limits and usage (not available yet) **Coming soon.** This command is a placeholder: it prints what it needs and exits 1. ```text Show plan limits and usage (not available yet) ``` ## Usage ```text anona-compute limits [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for limits | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute keys > Manage API keys (not available yet) **Coming soon.** This command is a placeholder: it prints what it needs and exits 1. ```text Manage API keys (not available yet) ``` ## Usage ```text anona-compute keys [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for keys | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute config > Read and write profile settings ```text Read and write settings of the selected profile (--profile, default "default"). Keys: api_url, api_key, domain, default_template, web_url. The file is written with mode 0600. ``` ## Usage ```text anona-compute config [flags] anona-compute config [command] ``` ## Commands | Command | Description | |---|---| | [`config get`](/cli/reference/config-get) | Print one setting | | [`config list`](/cli/reference/config-list) | List every profile and its settings (API keys are masked) | | [`config set`](/cli/reference/config-set) | Change one setting | ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for config | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute config get > Print one setting ```text Print one setting ``` ## Usage ```text anona-compute config get KEY [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for get | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute config list > List every profile and its settings (API keys are masked) ```text List every profile and its settings (API keys are masked) ``` ## Usage ```text anona-compute config list [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for list | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute config set > Change one setting ```text Change one setting ``` ## Usage ```text anona-compute config set KEY VALUE [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for set | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute template > Build and manage templates ```text Build and manage templates ``` ## Usage ```text anona-compute template [flags] anona-compute template [command] ``` **Aliases:** `template`, `templates`, `tpl` ## Commands | Command | Description | |---|---| | [`template build`](/cli/reference/template-build) | Build a template from a Dockerfile | | [`template delete`](/cli/reference/template-delete) | Delete a template (running sandboxes keep running) | | [`template info`](/cli/reference/template-info) | Show a template and its builds | | [`template list`](/cli/reference/template-list) | List your templates | | [`template logs`](/cli/reference/template-logs) | Print a build's log (the newest build unless --build), following it while it runs | ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for template | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute template build > Build a template from a Dockerfile ```text Builds a template from a Dockerfile and a build context (default: the current directory), streaming the build log. The gateway builds it with BuildKit in an isolated builder, adds the Anona runtime (envd), pushes it and registers it; sandboxes then start from it by name: anona-compute template build -f Dockerfile --name my-env anona-compute create -t my-env Every FROM image is pinned by digest. EXPOSE 49983 (envd) is refused. The image's CMD runs at sandbox start only when the last stage declares CMD or ENTRYPOINT, or with --start-cmd. .dockerignore in the context is honored. ``` ## Usage ```text anona-compute template build [CONTEXT_DIR] [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--cpu` | int | vCPUs of sandboxes from this template (default 2) | | `--file`, `-f` | string | Dockerfile (default CONTEXT_DIR/Dockerfile) | | `--help`, `-h` | | help for build | | `--memory` | int | memory of sandboxes from this template, MiB (default 2048) | | `--name`, `-n` | string | template name (default: the context directory's name) | | `--no-cache` | | build without the build cache | | `--start-cmd` | string | command to run when a sandbox starts (after envd) | | `--tag` | stringArray | tag for this build (repeatable) | | `--wait` | | stream the build log and wait until it is ready (--wait=false returns at once) (default true) | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute template list > List your templates ```text List your templates ``` ## Usage ```text anona-compute template list [flags] ``` **Aliases:** `list`, `ls` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for list | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute template info > Show a template and its builds ```text Show a template and its builds ``` ## Usage ```text anona-compute template info NAME|TEMPLATE_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for info | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute template logs > Print a build's log (the newest build unless --build), following it while it runs ```text Print a build's log (the newest build unless --build), following it while it runs ``` ## Usage ```text anona-compute template logs NAME|TEMPLATE_ID [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--build` | string | build ID (default: the newest) | | `--help`, `-h` | | help for logs | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute template delete > Delete a template (running sandboxes keep running) ```text Delete a template (running sandboxes keep running) ``` ## Usage ```text anona-compute template delete NAME|TEMPLATE_ID [flags] ``` **Aliases:** `delete`, `rm` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for delete | | `--yes`, `-y` | | confirm the deletion | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute push > Push a git working tree into a sandbox (not available yet) **Coming soon.** This command is a placeholder: it prints what it needs and exits 1. ```text Push a git working tree into a sandbox (not available yet) ``` ## Usage ```text anona-compute push [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for push | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute recover > Recover a sandbox from its last snapshot (not available yet) **Coming soon.** This command is a placeholder: it prints what it needs and exits 1. ```text Recover a sandbox from its last snapshot (not available yet) ``` ## Usage ```text anona-compute recover [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for recover | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # anona-compute version > Print the CLI version ```text Print the CLI version ``` ## Usage ```text anona-compute version [flags] ``` ## Flags | Flag | Type | Description | |---|---|---| | `--help`, `-h` | | help for version | Global flags (`--api-key`, `--api-url`, `--json`, `--output`, `--profile`, `--request-timeout`, `--verbose`) work on every command; see [anona-compute](/cli/reference/anona-compute). # SDK extras > The E2B SDK plus Anona's live view, SSH and defaults. Anona Compute works with the stock E2B SDKs. The Anona packages are thin extras on top of them: | | Python | TypeScript | |---|---|---| | Package | `anona` (needs `e2b>=2.61,<3`; extra `anona[desktop]` adds `e2b-desktop`) | `@anona/sdk` (peer dependency `e2b ^2.61.0`; optional `@e2b/desktop`) | | `Sandbox` | Subclass of `e2b.Sandbox` | Subclass of e2b's `Sandbox` | | Adds | `sandbox.live`, `sandbox.ssh`, `anona.configure()`, `anona.desktop.Sandbox` | `sandbox.live`, `sandbox.ssh`, `configure()` | | Defaults | Domain `compute.anonalabs.com`; reads `ANONA_API_KEY` | Same | Everything else (`commands`, `files`, `pty`, `pause`, `connect`, …) is the E2B SDK, unchanged. Its own documentation applies. **Coming soon:** `anona` on PyPI and `@anona/sdk` on npm. ## Environment | Anona variable | Fills | Notes | |---|---|---| | `ANONA_API_KEY` | `E2B_API_KEY` | used when `E2B_API_KEY` is unset | | `ANONA_DOMAIN` | `E2B_DOMAIN` | default `compute.anonalabs.com` | | `ANONA_API_URL` | `E2B_API_URL` | E2B deprecates it; fallback only | `E2B_*` values always win, so an environment already set up for the stock SDK keeps working unchanged. - [Python reference](/sdk/python) - [TypeScript reference](/sdk/typescript) # Python: anona > anona.Sandbox, live view tokens and SSH certificates. ```text pip install anona # or: pip install "anona[desktop]" ``` ## `anona.configure` ```python Signature anona.configure(api_key=None, *, domain=None, api_url=None, debug=None) -> None ``` Sets the `E2B_*` variables of this process so the stock SDK, and the extras, talk to Anona. Missing values fall back to `ANONA_*` variables, then to `E2B_DOMAIN=compute.anonalabs.com`. ```python configure.py import os import anona from e2b import Sandbox # the stock SDK picks up the configuration anona.configure(api_key=os.environ["ANONA_API_KEY"]) with Sandbox.create() as sbx: print(sbx.commands.run("echo configured").stdout) ``` ## `anona.Sandbox` A subclass of `e2b.Sandbox`. Every E2B method works unchanged; `create`, `connect` and `list` default to the Anona domain. Two properties are added: | Property | Type | |---|---| | `sandbox.live` | `anona.Live` | | `sandbox.ssh` | `anona.Ssh` | ## `sandbox.live` | Method | Returns | | |---|---|---| | `token(scope="view", channels=None, ttl=600)` | `ViewerToken` | `scope`: `"view"` or `"control"`. `channels`: any of `"dom"`, `"pty"`, `"desktop"`, `"thumb"` (default `["dom"]`). `ttl`: 30 to 600 seconds | | `url(mode="dom", scope="view", ttl=600)` | `str` | Mints a token for one channel and returns its embed URL | | `revoke_all()` | `None` | Closes every open viewer and invalidates issued tokens | `ViewerToken` has `token`, `expires_at` (`datetime`), `embed_url`, and `claims()` (decodes the payload without verifying it). Its `repr` never shows the token. ```python live_tokens.py import os from anona import Sandbox sbx = Sandbox.connect(os.environ["SANDBOX_ID"]) tok = sbx.live.token(scope="control", channels=["pty"], ttl=120) print(tok) # ViewerToken(expires_at=...) print(tok.claims()["scp"], tok.claims()["sbx"] == sbx.sandbox_id) print(sbx.live.url(mode="pty", scope="control").split("#")[0]) ``` Invalid arguments raise `e2b.InvalidArgumentException` before any request. Gateway errors map to the E2B exceptions: 400 `InvalidArgumentException`, 401/403 `AuthenticationException`, 404 `NotFoundException`, 429 `RateLimitException`. ## `sandbox.ssh` | Method | Returns | | |---|---|---| | `certificate(public_key)` | `SshCertificate` | `public_key`: an OpenSSH public key string, or the path of a `.pub` file (`ssh-ed25519`, `ecdsa-sha2-nistp256`, `ssh-rsa`). Refuses private keys | | `config(identity_file="~/.ssh/anona_ed25519", certificate_file=None, alias=None, user="user", proxy_command="anona-compute ssh --proxy %h")` | `str` | An `ssh_config` block. Makes no request | `SshCertificate` has `cert`, `user`, `host`, `proxy_command`, `expires_at`, and `write(path)` (writes with mode 0600). See [SSH](/guides/ssh) for a full example. ## `anona.desktop.Sandbox` A subclass of `e2b_desktop.Sandbox` (install `anona[desktop]`) with the same `live` and `ssh`, plus: | Method | Returns | | |---|---|---| | `live_url(control=True, ttl=600)` | `str` | Embed URL of the desktop channel. Control scope by default, since the desktop channel carries input | See [Desktop](/guides/desktop). ## Constants `anona.DEFAULT_DOMAIN` (`"compute.anonalabs.com"`), `anona.DEFAULT_API_URL`, `anona.ENV_MAP`, `anona.__version__`. # TypeScript: @anona/sdk > Sandbox, live view tokens and SSH certificates for Node.js. ```text npm install @anona/sdk e2b ``` ESM only; Node.js 22.19 or newer. ## `configure` ```ts Signature configure(opts?: { apiKey?: string; domain?: string; apiUrl?: string; debug?: boolean }): void ``` Sets the `E2B_*` variables of this process so the stock `e2b` package, and the extras, talk to Anona. ```typescript configure.ts import { configure } from "@anona/sdk"; import { Sandbox } from "e2b"; // the stock SDK picks up the configuration configure({ apiKey: process.env.ANONA_API_KEY }); const sbx = await Sandbox.create(); console.log((await sbx.commands.run("echo configured")).stdout); await sbx.kill(); ``` ## `Sandbox` A subclass of e2b's `Sandbox`. Every e2b method works unchanged and defaults to the Anona domain. Added: | Member | Type | |---|---| | `sandbox.live` | `Live` | | `sandbox.ssh` | `Ssh` | | `Sandbox.connectionDefaults(opts?)` | the resolved connection options (diagnostics) | ## `sandbox.live` | Method | Returns | | |---|---|---| | `token({ scope?, channels?, ttl? })` | `Promise` | `scope`: `"view"` (default) or `"control"`. `channels`: `"dom"`, `"pty"`, `"desktop"`, `"thumb"` (default `["dom"]`). `ttl`: 30 to 600 s (default 600) | | `url({ mode?, scope?, ttl? })` | `Promise` | Embed URL for one channel | | `desktopUrl({ control?, ttl? })` | `Promise` | Embed URL for the desktop; control scope unless `control: false` | | `revokeAll()` | `Promise` | Closes every viewer and invalidates issued tokens | `ViewerToken` has `token`, `expiresAt` (`Date`), `embedUrl` and `claims()`. `toString()` and `toJSON()` never include the token. ```typescript live_tokens.ts import { Sandbox } from "@anona/sdk"; const sbx = await Sandbox.connect(process.env.SANDBOX_ID!); const tok = await sbx.live.token({ scope: "control", channels: ["pty"], ttl: 120 }); console.log(String(tok), JSON.stringify(tok)); console.log(tok.claims().scp, tok.claims().sbx === sbx.sandboxId); ``` Errors map to e2b's classes: 400 `InvalidArgumentError`, 401/403 `AuthenticationError`, 404 `NotFoundError`, 429 `RateLimitError`, others `SandboxError`. ## `sandbox.ssh` | Method | Returns | | |---|---|---| | `certificate(publicKey)` | `Promise` | The OpenSSH public key **text** (not a path) | | `config({ identityFile?, certificateFile?, alias?, user?, proxyCommand? })` | `string` | An `ssh_config` block. No request | `SshCertificate` is `{ cert, user, host, proxyCommand, expiresAt? }`. See [SSH](/guides/ssh). ## Desktop There is no desktop subclass in TypeScript: create with `@e2b/desktop`, then `Sandbox.connect(desk.sandboxId)` from `@anona/sdk` for `live.desktopUrl()`. See [Desktop](/guides/desktop). ## Exports `Sandbox`, `configure`, `applyEnv`, `anonaDefaults`, `Live`, `ViewerToken`, `Ssh`, `CHANNELS`, `SCOPES`, `MIN_TTL`, `MAX_TTL`, `DEFAULT_DOMAIN`, `DEFAULT_API_URL`, `DOMAIN_ENV`, `ENV_MAP`, and the types `ConfigureOpts`, `ViewerScope`, `ViewerChannel`, `ViewerTokenOpts`, `ViewerTokenClaims`, `SshCertificate`, `SshConfigOpts`. # API reference > The REST API behind the SDKs and the CLI. Base URL: `https://api.compute.anonalabs.com` Most users never call it directly: the E2B SDK, the Anona extras and the CLI do. Two groups of routes: | Group | What | Source | |---|---|---| | [Sandboxes and templates (E2B)](/api-reference/sandboxes/sandboxes/create-sandbox-v2) | Create, list, get, kill, pause, resume, connect, timeout, network update, template builds | The E2B OpenAPI spec (SDK 2.61.0), cut to the operations Anona serves | | [Anona](/api-reference/anona/live-view/mint-a-viewer-token) | Viewer tokens, SSH certificates, network policy, credential vault, usage and billing, API keys | Written from the gateway's handlers | Both files are checked against a running gateway: every operation is called and each answer's status and body are validated against the spec. ## Authentication Send your API key in the `X-API-Key` header, or as `Authorization: Bearer e2b_…`: ```bash api_auth.sh curl -sS https://api.compute.anonalabs.com/v2/sandboxes -H "X-API-Key: $E2B_API_KEY" curl -sS https://api.compute.anonalabs.com/v2/sandboxes -H "Authorization: Bearer $E2B_API_KEY" ``` The `/v1/api-keys` routes accept only dashboard sessions; manage keys in the dashboard. Changing the team network settings and the credential vault needs an owner or admin session (API keys can read them). ## Errors Errors have the E2B shape, so the SDKs raise their usual exceptions: ```json { "code": 429, "message": "concurrent sandbox limit reached (100)", "error_code": "sandbox_quota_exceeded" } ``` | Status | Meaning | |---|---| | 400 | Invalid request, or a feature not supported (the message says which) | | 401 | Missing, unknown, revoked or expired API key | | 403 | The key's scopes or your role do not allow this; or the organization is suspended | | 404 | Not found, or not yours (the same answer) | | 409 | The sandbox is in the wrong state (for example, a request to a paused sandbox's URL) | | 429 | A plan limit (`error_code` `sandbox_quota_exceeded`) | | 503 | No capacity yet, for example during a cold start; retry after `Retry-After` seconds | ## Sandbox traffic Commands, files and terminals do not go to the API host. They go to the sandbox's own `envd` at `https://49983-.sbx.compute.anonalabs.com` (Connect RPC, E2B's `envd` protocol), with the sandbox's access token from the create response. Live view and SSH use `wss://stream.compute.anonalabs.com`. # Dashboard > app.compute.anonalabs.com: sign-in, organizations, keys and sandboxes. The dashboard at [app.compute.anonalabs.com](https://app.compute.anonalabs.com) is where you sign in, manage your organization and API keys, and look after sandboxes. | Page | What you do there | |---|---| | **Sign in** | Email magic link or GitHub | | **Onboarding** | Create your organization | | **Sandboxes** | List running and paused sandboxes with live thumbnails; create one with a template and a lifetime (up to 30 days, or no limit with pause when idle) | | **Sandbox** | State, template and size; pause, resume, extend and kill; **Share preview** links to a port; the live terminal and desktop; connection snippets for the SDKs and the CLI | | **API keys** | Create keys with a preset or custom scopes, an expiry and an optional sandbox list; revoke them | | **Network** | Team egress settings: the default network policy of new sandboxes (owners and admins change it) | | **Credential vault** | Secrets that sandboxes use through header rules without seeing them (owners and admins add, rotate and delete) | | **CLI & agent sessions** | Every machine signed in with `anona-compute login`; revoke one or all. Owners and admins also see members' sessions | | **Webhooks** | Endpoints for lifecycle events, their signing secrets, the delivery log, test events and resend (owners and admins) | | **Telemetry** | OpenTelemetry export of sandbox metrics and lifecycle spans to your endpoint (owners and admins) | | **Usage** | This month's metered usage by day, priced at your plan's rates | | **Billing** | Invoice drafts (owners and admins) | | **Device** | Approve an `anona-compute login` from the terminal | ## Roles | Role | Can | |---|---| | Owner, admin | Everything, including API keys, billing, network settings and the credential vault | | Member | Create, use, pause and kill sandboxes; mint control live views and SSH certificates | | Viewer | Read only: list sandboxes and watch live views | Sign-outs, revoked sessions, removals from the organization and role changes reach the API within seconds, also for tokens already issued. **Coming soon:** team invitations from the dashboard. ## The CLI sign-in `anona-compute login` uses the browser device flow: the terminal prints a URL and a code such as `ABCD-EFGH`, the dashboard's **Device** page asks you to confirm the code and pick the organization, and the CLI receives a session for that machine. `anona-compute logout`, or **Revoke** on the **CLI & agent sessions** page, ends it. # Usage and billing > What is metered, and where to read it: dashboard, CLI, MCP and API. Usage is metered per sandbox and priced at your plan's [rates](/pricing). During the preview no payment method is needed and nothing is charged; invoices are drafts. ## Where to see it | Where | What | Who | |---|---|---| | Dashboard **Usage** page | The month by day, per meter, per sandbox and per template | Every role | | Dashboard **Billing** page | Plan, rates and invoice drafts | Owners and admins | | `anona-compute usage` | Meters, totals, the part not yet rated, a projection for the current month, the most expensive sandboxes | Browser sign-in of any role, or a key with `usage:read` | | `anona-compute usage --billing` | Plan, rates and invoices | Owners and admins, or a key with `billing:read` | | MCP tool `get_usage` | The same as `anona-compute usage`, for agents | As above | | `GET /v1/usage`, `GET /v1/billing` | The JSON documents behind all of the above | As above | API keys need the scope: the **Read only** preset has `usage:read`, **Full access** has both. Keys created before these scopes existed do not have them. ```bash usage.sh anona-compute usage # this month anona-compute usage --period 2026-10 --top 10 anona-compute usage --json | jq '{period: .period.key, total: .totals.amountUsd, meters: [.meters[] | {meter, quantity}]}' anona-compute usage --billing --json | jq '{provider, rates: [.rates[] | {meter, unitPriceUsd}]}' ``` ```python usage_api.py import os import httpx r = httpx.get( "https://api.compute.anonalabs.com/v1/usage", params={"period": "current"}, headers={"X-API-Key": os.environ["E2B_API_KEY"]}, ) r.raise_for_status() usage = r.json() print(usage["period"]["key"], usage["totals"]["amountUsd"], usage["totals"].get("projectedUsd")) for m in usage["meters"]: print(f'{m["meter"]:<20} {m["quantity"]:>10.3f} {m["unit"]:<10} ${m["amountUsd"]:.4f}') ``` ## What counts | Meter | Counted when | |---|---| | Active vCPU-hours | The sandbox's CPU is busy, while it runs | | Resident GiB-hours | Memory the sandbox actually holds, while it runs | | Snapshot storage | A paused sandbox's snapshot exists | | Egress | Bytes leave the sandbox to the internet | Killing a sandbox stops all four. Pausing stops the first two. Usage is rated hourly; until then the totals show it as `pendingUsd`, estimated at the same rates. ## Coming soon Card payments and issued invoices, and spend caps you set yourself. For per-sandbox metrics in your own monitoring, see [OpenTelemetry export](/guides/webhooks#opentelemetry-export). # Pricing (preview) > Pay for active CPU and resident memory, not wall-clock time. **Preview pricing.** These are the rates the metering pipeline uses today. They may change before general availability, and **no payments are taken during the preview**: usage is metered and shown in the dashboard, and invoices are drafts. | Meter | Price | |---|---| | Active vCPU-hour | **$0.012** | | Resident memory, GiB-hour | **$0.008** | | Snapshot storage (paused sandboxes), GB-month | $0.03 | | Egress, GB | $0.12 | **Active CPU, not wall-clock.** A sandbox that waits for an LLM response uses almost no CPU, so it costs mostly its memory. A sandbox with 2 vCPU busy at 20% is billed 0.4 vCPU-hours per hour. **Paused costs only storage.** A paused sandbox has no CPU or memory charge; you pay for its snapshot until you resume or kill it. ## Examples Per hour, for a sandbox with 2 vCPU and 4 GiB: | Pattern | Usage | Price per hour | |---|---|---| | Busy (CPU-bound work) | 2 active vCPU, 4 GiB | $0.024 + $0.032 = **$0.056** | | Typical agent (mostly waiting) | 0.4 active vCPU, 1.5 GiB | $0.0048 + $0.012 = **$0.017** | | Idle-heavy | almost no CPU, little memory resident | about **$0.002** | The free plan has the same rates with $5 of included credit and the [free plan limits](/concepts/limits). The credential vault is included at no charge. See [Usage and billing](/usage-and-billing) for where to see what you used: the dashboard, `anona-compute usage`, or `GET /v1/usage`. # Troubleshooting > Common errors and what to do about them. - Check that `E2B_API_KEY` (or `ANONA_API_KEY`) holds an **Anona Compute** key from the dashboard. E2B keys and Anona Memory keys do not work here. - The key may be revoked or expired; see **API keys** in the dashboard. - Keys are `e2b_` followed by lowercase hex. The SDK refuses other formats before sending anything. Set `E2B_DOMAIN=compute.anonalabs.com`. Recent SDKs ignore `E2B_API_URL`. Or use `anona.Sandbox` / `@anona/sdk`, which default to the Anona domain. - The sandbox may have reached its timeout and been killed (the default is 300 s from the SDK). Use a longer `timeout`, `set_timeout`, or `lifecycle={"on_timeout": "pause"}`. - The key may belong to another organization, or be limited to a list of sandbox IDs. Both look exactly like "not found". Your key lacks `sandboxes:use` (needed for commands, live view and SSH), or your dashboard role is viewer. Create a key with **Create and use**, or ask an admin for the member role. `anona-compute usage`, the MCP `get_usage` tool and `GET /v1/usage` need a key with `usage:read` (the **Read only** and **Full access** presets have it); billing needs `billing:read` (**Full access**). Create a new key; existing keys keep their scopes. Team egress settings and the credential vault are changed by dashboard owners and admins. Any role and any key can read them. Store the secret on the dashboard's **Credential vault** page under exactly the name the rule uses (`${vault.}`), then create the sandbox again. Kill sandboxes you no longer need (`anona-compute list`, then `anona-compute kill`), or pause them: paused sandboxes do not count as running. See [limits](/concepts/limits). Your plan has a maximum lifetime (the free plan's is one hour). Use a timeout up to that maximum. These E2B features are not available on Anona Compute; remove them from the create call. See [Migrate from E2B](/migrate-from-e2b#not-available-yet). Host names in `allow_out` need public egress off: add `deny_out: ["0.0.0.0/0"]`. Destinations in private or metadata ranges, and names that resolve into them, are always refused. See [Networking](/concepts/networking). A sandbox server is starting after an idle period. The SDK retries; if you call the API yourself, wait `Retry-After` seconds and retry. The sandbox is paused, for example by its idle timer. Resume it with `Sandbox.connect(id)` or `anona-compute resume `. The certificate is older than 10 minutes, or was minted for another sandbox. Run `anona-compute ssh ` again, or refresh the block with `anona-compute ssh --print-config`. Tokens last at most 10 minutes. Answer the embed's `token-request` message with a fresh token ([Live view](/guides/live-view#keep-a-long-session-alive)). After `revoke_all`, mint new tokens. The `dom` and `thumb` channels are not available yet. Port 49983 belongs to the Anona runtime (`envd`), so a template cannot expose it. Every `FROM` is pinned by digest at build time; a tag that cannot be resolved fails the build. See [Templates](/concepts/templates#build-your-own). These commands exist in the CLI ahead of their server side. See the [CLI overview](/cli/overview) for what is coming. ## Get more detail - `anona-compute -v …` prints verbose detail on stderr. - Every API error body has `code` and `message`, and often `error_code`. # FAQ > Short answers to common questions. No. Set `E2B_DOMAIN=compute.anonalabs.com` and an Anona API key. See [Migrate from E2B](/migrate-from-e2b) for the features that are not available yet. No. Each sandbox is a KVM microVM with its own kernel. See [Security](/concepts/security). On paid plans, as long as you want: create it with `timeout=-1`, and let it pause when idle. On the free plan, up to one hour. A paused sandbox costs no CPU or memory. See [Lifecycle](/concepts/lifecycle). They are frozen with the sandbox's memory and carry on after resume. On paid plans yes, by default; on the free plan no. Private networks and cloud metadata are always blocked. You can restrict each sandbox further. See [Networking](/concepts/networking). The `coding-agent` template has the Docker CLI but no daemon. Running a Docker daemon in a sandbox is not supported yet. Yes: build one from a Dockerfile with `anona-compute template build`, or with the E2B SDK's `Template.build`. See [Templates](/concepts/templates#build-your-own). Not per sandbox: a sandbox gets its template's size. See [Templates](/concepts/templates). By active vCPU time, resident memory, snapshot storage and egress. During the preview nothing is charged. See [Pricing](/pricing). AWS `us-east-1`. See [Regions and limits](/concepts/limits). # Changelog > What changed, newest first. ## Build your own templates - **Template builds**: `anona-compute template build` from a Dockerfile, and the stock E2B SDK's `Template.build` and `e2b` CLI commands, served by the gateway (`/v3/templates`, file uploads, build status, logs, list, get, delete, aliases). Each build runs in its own rootless BuildKit container with no credentials and only proxied egress, gets the Anona runtime layer, and is registered for `create -t `. - **Templates page** in the dashboard: list, build status, build log, delete. MCP tool `list_templates`. - Template, build-concurrency and context-size quotas per organization. ## Preview links, public ports, pause when idle - **Preview links**: signed, expiring links to one port of a sandbox (`anona-compute preview`, `POST /v1/sandboxes/{id}/preview-links`, MCP `get_preview_url`, the dashboard's **Share preview**), revocable at once. - **Public ports**: make one port public without opening the rest (`anona-compute preview --public`). - **Pause when idle**: a sandbox pauses itself after a set time with no traffic, sessions or processes (`PUT /v1/sandboxes/{id}/auto-pause`, `autoPause: {"idleMinutes": n}` on create, or the dashboard's **No limit (pauses when idle)**). ## Webhooks, OpenTelemetry export, DOM live view, thumbnails - **Webhooks**: signed (Standard Webhooks) callbacks for sandbox lifecycle events, with retries, a delivery log and resend. See [Webhooks and telemetry](/guides/webhooks). - **OpenTelemetry export**: per-sandbox CPU, memory and network metrics and lifecycle spans to your OTLP/HTTP endpoint. - **DOM live view**: the page open in a sandbox's Chrome, replayed in your browser. - **Thumbnails**: a WebP of each sandbox every 3 seconds, on the dashboard and at `GET /v1/sandboxes/{id}/thumbnail`. - New API key scope `integrations:manage` for the webhook and export settings. ## Network settings, credential vault, usage - **Team egress settings**: the default network policy of new sandboxes, on the dashboard's **Network** page or `GET`/`PUT /v1/network`, with `GET /v1/sandboxes/{id}/network/effective` to see which layer a sandbox's policy came from. - **Change a running sandbox's network**: E2B's `update_network` (`PUT /sandboxes/{id}/network`) now works. - **Credential vault**: store API tokens once and reference them in header rules as `${vault.}`; the egress proxy injects them and the sandbox never sees the value. - **Usage and billing**: `anona-compute usage`, the MCP `get_usage` tool, and `GET /v1/usage` and `/v1/billing` with the new `usage:read` and `billing:read` API key scopes. - **CLI & agent sessions**: a dashboard page lists every machine signed in with `anona-compute login` and revokes them; sign-outs, revocations and role changes now reach the API within seconds. ## Developer preview **Sandboxes** - E2B-compatible API: create (v1 and v2), list, get, kill, connect, pause, resume, timeout and refresh, with E2B-shaped errors. Checked with the stock `e2b` SDK 2.20.3, 2.51.0 and 2.61.0. - Long-running sandboxes: `timeout=-1` for no deadline on plans without a maximum lifetime, and pause instead of kill at timeout. - Templates `base`, `python`, `node`, `code-interpreter`, `desktop` and `coding-agent`. - Egress policy at create: plan defaults, allow and deny lists, and a platform floor that always blocks private ranges and cloud metadata. - Every sandbox request passes the sandbox edge, which checks a per-sandbox token. **Access** - Live view of the terminal and the desktop, embeddable in your page, with short-lived viewer tokens and revoke-all. - SSH with 10-minute certificates: `anona-compute ssh`, `scp`, `--print-config` for editors. **Tools** - `anona-compute` CLI: browser sign-in, lifecycle, `exec`, `run`, `connect`, `ssh`, `scp`, `view`, `desktop`, profiles. - `anona-compute mcp`: an MCP server for Claude Code, Codex, Cursor and other clients, over stdio or loopback HTTP. - SDK extras `anona` (Python) and `@anona/sdk` (TypeScript): `sandbox.live` and `sandbox.ssh`. - Dashboard: organizations and roles, scoped API keys with expiry and sandbox lists, sandboxes with live view, usage and invoice drafts. ## Coming soon | Area | What | |---|---| | Distribution | `anona` on PyPI, `@anona/sdk` on npm, `anona-compute` release downloads | | Sandboxes | Fork, named snapshots, sandbox logs and metrics, resume on incoming traffic | | Access | SSH host certificates | | Account | Payments and issued invoices, self-set spend caps, team invitations | | Platform | More regions |