Migration from 1.x
aiod stands in for the 1.x Python server. The public port and gateway are unchanged on the prebuilt images: most clients migrate by switching the image tag.
The v1 surface is documented in v1 API Reference. The v2 surface is in the API Reference.
Migrate with the prebuilt images
Replace ghcr.io/agent-infra/sandbox:latest (the 1.x AIO image) with aio-daemon, or with aio-computer for GUI use. The run command keeps the 1.x flags plus --shm-size 4g for Chromium; the commands, the gateway port and the startup timing are in Quick Start.
Then check the client against this list:
- API key —
SANDBOX_API_KEYstill works. JWT and tickets are gone. See Authentication. - Removed routes — a client calling proxy mapping, skills CRUD, or page-level browser automation needs a replacement. See Removed routes.
- SDK coverage — the v1 SDKs still reach most of the surface, and the three calls that fail send a method, a route, or an id the daemon does not serve. See 1.x SDK compatibility.
- Changed defaults on the images — Code Server and JupyterLab are off, no kernel is prewarmed, and the shell and code backends are pinned to
native. See The images.
Route map: v1 to v2
/metrics needs the key. The exact routes per plane follow; the capability pages show usage in one style at a time, chosen with the API Preference switch in the sidebar.
Commands
v2 treats each run as a resource: POST /v2/commands returns a command_id, output is read by offset per stream or streamed as NDJSON on the same route, and sessions are optional under /v2/commands/sessions; v1 keyed every call by session_id and created the session on first use. v2 also adds shell and args, and a session-wide env.
Response envelope (v1 and most v2):
success— whether the call succeededmessage— a one-line summarydata— the payload;nullwhen there is nonehint— optional guidance, otherwisenull
Validation failures answer 422 with errors: [{location, message, type}], as in 1.x. File errors carry these fields under data:
errno— the OS error numbererrno_name— its symbolic name, such asENOENTerror_type— the kind of failure:not_found,permission_denied, …exception_type— the 1.x exception class name, such asFileNotFoundErrormessage— a one-line summaryoperation— the file operation that failedpath— the path involvedretryable— whether a retry can succeed
Status codes:
400— malformed request401— missing or wrong API key403— input denied by the OS404— unknown route or object422— failed validation429— session or watcher capacity reached501— not implemented on this daemon: a Linux-only route on Windows, or/v1/jupyterwithoutipykernel503— capability unavailable right now
Files
v2 keys every call by path, reads and listings are GET, and ?user= selects the owner; v1 mixed file and path keys with a per-call sudo flag.
Terminals
v2 treats a terminal as a resource under /v2/pty/sessions/{id}, with explicit verbs (exec, input, signal, screen, resize) and a durable WebSocket attach; v1 multiplexed all of it through /v1/shell/exec {id} and /v1/shell/ws.
Code
v2 has one entry point, POST /v2/code/execute {language, code, session_id?}, with sessions under /v2/code/sessions; v1 split execution by language, each route with its own session semantics.
Browser
/v2/browser/* aliases /v1/browser/*: same routes, same bodies. v2 adds no page-level routes, since page automation belongs to CDP clients and the DevTools relay lives in the proxy.
Desktop
The desktop surface is v2-only, under /v2/computer/*; /v1/browser/actions and /v1/display/record remain as aliases for 1.x clients.
Sandbox
GET /v2/sandbox returns identity, workspace, and capabilities as one JSON object; /v1/sandbox keeps the 1.x layout, a text summary in data with detail, version, and workspace at the top level. The v2 name for the workspace is workspace_dir; /v1/sandbox sends home_dir as well, at the top level and under detail.system, so the 1.x model still parses.
Removed routes
The routes below no longer exist on aiod; calling them answers 404. Two kinds: some are gone outright with no replacement, and some moved to a different entry point.
Gone outright, no replacement:
/v1/skills/*(create/read/update/delete),/v1/sandbox/hooks, andSANDBOX_SHUTDOWN_HOOKS/v1/util/convert_to_markdown- The
stdinfield ofPOST /v1/nodejs/execute; unknown body fields are ignored /v1/sandbox/observe/*- Standalone Node REPL servers (
DISABLE_NODEJS_REPL,NODEJS_REPL_PORT*) WORKSPACErelocation (there is no--workspaceflag)/v1/browser/captcha/*,/v1/browser/state/*,/v1/browser/restart, the/v1/browser/network/*sub-routesroute/headers/scoped_headers/export_har(network/requestsstill works), and/v1/browser/proxy.pac
Moved to a different entry point:
- Runtime proxy-mapping API → a static
PROXY_MAPon the images /v1/mcp/servers,/v1/mcp/<server>/tools, and the other management routes → the/mcphub (see MCP below)/v1/browser/page/*page automation → drive the page over CDP (BROWSER_REMOTE_DEBUGGING_HOST/_PORT, default127.0.0.1:9222); the daemon does not proxy DevTools itself, a deployment has to expose it (nginx does on the images)POST /ticketsand JWT auth → useAIO_API_KEY(see Authentication below)
These /v1/browser/* routes still answer:
navigatescreenshotclickfillevaluateuploadconfigcdpsnapshottabscookiesinfonetwork/requestsactions
Authentication
AIO_API_KEY (--api-key) is the only credential, and it is optional. With a key set, every request except the public routes below must carry it; SANDBOX_API_KEY on the images sets the same key.
The three forms are equivalent; pick one:
Authorization: Bearer <key>x-api-key: <key>?api_key=<key>— deprecated, but the only option for WebSocket and VNC URLs, which cannot carry headers
These routes answer without a key:
//health/v1/ping/v1/openapi.json/v2/openapi.json/internal/auth
Everything else, /metrics included, needs the key once one is set. The images' gateway also accepts X-AIO-API-Key.
Send the key as a bearer token:
The 1.x ticket/JWT mechanism is gone entirely: POST /tickets, GET /auth, and JWT_PUBLIC_KEY verification no longer exist. A deployment that only configured JWT_PUBLIC_KEY is now unauthenticated, with a warning at boot — set AIO_API_KEY explicitly instead.
WebSocket and VNC URLs have no headers to use, so the key goes in the URL, for example ws://127.0.0.1:8091/v2/pty/sessions/SESSION_ID/ws?api_key=$KEY.
Identity: user and AIO_DEFAULT_USER
On commands, PTY, and code, user (per request) or AIO_DEFAULT_USER (at startup) is the execution identity. The process runs as that account.
On file routes it is only the ownership identity. The daemon still reads and writes with its own privileges. Objects it creates belong to that account.
This applies on Linux only. Windows answers 400 for an explicit user, and 503 where AIO_DEFAULT_USER would apply.
On the images, the daemon runs as root with AIO_DEFAULT_USER=gem; commands run as gem.
The working directory is not a confinement boundary. File routes can reach the whole host filesystem. Isolation is the sandbox's job, not the daemon's.
MCP
EXTRA_MCP_SERVERS (or --mcp-servers) registers loopback, stateless streamable-HTTP MCP servers. Their tools are listed next to the 10 built-in ones on POST /mcp.
Only http://127.0.0.1|localhost|[::1] targets are accepted. These are skipped with a warning:
- remote servers
command(stdio) servers- session-requiring servers
The 1.x management surface is gone:
MCP_FILTER_SERVERShidden- per-entry timeouts
/v1/mcp/*management routes
List tools with tools/list on /mcp instead. On the AIO image, mcp-server-browser starts together with the browser.
The images
AIO image
aio-daemon bundles aiod with Chromium, VNC, Python and Node toolchains, and an nginx gateway.
Computer image
aio-computer adds an XFCE desktop and the computer-use worker on top of the AIO image.
Environment variables
Image-only environment variables:
The Computer image adds AIO_DESKTOP=xfce and ENABLE_DBUS=true, which starts the session D-Bus that AT-SPI needs. Chromium runs with --force-renderer-accessibility.
Changed defaults
JUPYTER_POOL_SIZE maps to AIO_KERNEL_PREWARM: the default is 0, nothing warmed. AIO_SHELL_BACKEND=native and AIO_CODE_BACKEND=native are pinned on the images. They are not auto-detected.
Build a custom image
Two starting points cover most cases. Add layers on top of FROM enterprise-public-cn-beijing.cr.volces.com/vefaas-public/aio-daemon:1.0.0 (or aio-computer), or copy the aiod binary into any image you already have. See Deployment.
The daemon resolves tools at request time from PATH. Installing packages in the Dockerfile is enough:
- Python packages and
ipykernelfor code execution nodefor JavaScriptrgandtmuxfor search and terminals
GET /v1/capabilities reports what the image ended up with.
Services in the images run under supervisord. An extra long-running process becomes a supervisord program: place a .conf file in /opt/gem/supervisord/. The image's /etc/supervisord.conf includes that directory.
Lifecycle hooks run in the image entrypoint, each a shell command string:
RUN_HOOK_INIT— runs before anything startsRUN_HOOK_PRE_SERVICES— runs before services startRUN_HOOK_POST_READY— runs after the daemon is healthy
RUN_HOOKS_STRICT=true makes a failing hook fatal (default false).
The daemon deliberately limits some capabilities. A custom image can lift each of them:
1.x SDK compatibility
The 1.x SDKs call the v1 routes and work against aiod without changes: Python agent-sandbox 0.0.31 and TypeScript @agent-infra/sandbox 1.0.17 were verified against aiod 0.9.x. The Go SDK github.com/agent-infra/sandbox-sdk-go v0.0.5 was verified against aiod 0.9.2: it exposes the same modules in Go casing (file.GrepFiles, code.ExecuteCode, nodejs.GetInfo), every method in the table below behaves as its Python counterpart, and the same three calls fail. The SDKs cover v1 only; /v2/* is called over HTTP, described in the API Reference and served by a running daemon at /v2/openapi.json.
Verified methods
Every response parses into the SDK's 1.x model. The TypeScript SDK exposes the same modules with camelCase names (file.grepFiles, code.executeCode, nodejs.getInfo); sandbox, bash, shell, file, code, nodejs, browser.getInfo and browserTabs.list were verified, and its desktop and Jupyter methods send the same requests as the Python ones above. A TypeScript call returns { ok, body } on success and { ok, error } on failure, with the payload at body.data.
Calls that fail
Three calls fail on the request itself:
close has a workaround: pass the id a tab listing reports instead of the position. Python accepts it as written; TypeScript needs a cast; Go types the argument as int and cannot express it, so the route has to be called over HTTP.
Removed routes
These modules call routes the daemon no longer serves; they answer 404:
browser_page.*,browser_state.*,browser_captcha.*,browser.restart,browser.get_proxy_pacmcp.list_mcp_servers,mcp.list_mcp_toolsskills.*sandbox.list_hooks,sandbox.register_hook,sandbox.remove_hook,sandbox.observe_*proxy.*util.convert_to_markdownauth.create_ticket