• English
  • 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 keySANDBOX_API_KEY still 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

    Planev2v1 (compatible)
    CommandsPOST /v2/commands/v1/bash/*
    Files/v2/fs/*, /v2/watch/v1/file/*
    Terminals/v2/pty/sessions/*/v1/shell/*
    Code/v2/code/*/v1/code/*, /v1/jupyter/*, /v1/nodejs/*
    Browser/v2/browser/* (alias)/v1/browser/*
    Desktop/v2/computer/*/v1/browser/actions, /v1/display/record
    MCPPOST /mcpPOST /mcp
    Ops/v2/sandbox, /health, /metrics/v1/sandbox, /v1/capabilities

    /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

    Operationv2v1
    Run a commandPOST /v2/commandsPOST /v1/bash/exec
    Read outputGET /v2/commands/{command_id}POST /v1/bash/output
    Write stdinPOST /v2/commands/{command_id}/stdinPOST /v1/bash/write
    KillPOST /v2/commands/{command_id}/killPOST /v1/bash/kill
    Create sessionPOST /v2/commands/sessionsPOST /v1/bash/sessions/create
    List sessionsGET /v2/commands/sessionsGET /v1/bash/sessions
    Close sessionDELETE /v2/commands/sessions/{session_id}POST /v1/bash/sessions/{session_id}/close

    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 succeeded
    • message — a one-line summary
    • data — the payload; null when there is none
    • hint — optional guidance, otherwise null

    Validation failures answer 422 with errors: [{location, message, type}], as in 1.x. File errors carry these fields under data:

    • errno — the OS error number
    • errno_name — its symbolic name, such as ENOENT
    • error_type — the kind of failure: not_found, permission_denied, …
    • exception_type — the 1.x exception class name, such as FileNotFoundError
    • message — a one-line summary
    • operation — the file operation that failed
    • path — the path involved
    • retryable — whether a retry can succeed

    Status codes:

    • 400 — malformed request
    • 401 — missing or wrong API key
    • 403 — input denied by the OS
    • 404 — unknown route or object
    • 422 — failed validation
    • 429 — session or watcher capacity reached
    • 501 — not implemented on this daemon: a Linux-only route on Windows, or /v1/jupyter without ipykernel
    • 503 — capability unavailable right now

    Files

    Operationv2v1
    ReadGET /v2/fs/readPOST /v1/file/read
    WritePOST /v2/fs/writePOST /v1/file/write
    EditPOST /v2/fs/editPOST /v1/file/str_replace_editor, POST /v1/file/replace
    StatGET /v2/fs/statPOST /v1/file/stat
    List directoryGET /v2/fs/listPOST /v1/file/list
    Find by nameGET /v2/fs/searchPOST /v1/file/find, POST /v1/file/glob
    Search contentsPOST /v2/fs/grepPOST /v1/file/grep, POST /v1/file/search
    Create directoryPOST /v2/fs/mkdirPOST /v1/file/mkdir
    CopyPOST /v2/fs/copyPOST /v1/file/copy
    MovePOST /v2/fs/movePOST /v1/file/move
    DeletePOST /v2/fs/deletePOST /v1/file/delete
    UploadPOST /v2/fs/uploadPOST /v1/file/upload
    DownloadGET /v2/fs/downloadGET /v1/file/download
    Directory as tarGET/PUT /v2/fs/tree
    Create watcherPOST /v2/watchPOST /v1/file/watch
    Poll watcherGET /v2/watch/{id}/pollPOST /v1/file/watch/{id}/poll
    Stream watcherGET /v2/watch/{id}/eventsGET /v1/file/watch/{id}/events
    Stop watcherDELETE /v2/watch/{id}DELETE /v1/file/watch/{id}

    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

    Operationv2v1
    Create sessionPOST /v2/pty/sessionsPOST /v1/shell/sessions/create
    List sessionsGET /v2/pty/sessionsGET /v1/shell/sessions
    Inspect sessionGET /v2/pty/sessions/{id}
    Resize sessionPATCH /v2/pty/sessions/{id}
    Close sessionDELETE /v2/pty/sessions/{id}DELETE /v1/shell/sessions/{session_id}
    Run a commandPOST /v2/pty/sessions/{id}/execPOST /v1/shell/exec
    Type inputPOST /v2/pty/sessions/{id}/inputPOST /v1/shell/write
    SignalPOST /v2/pty/sessions/{id}/signalPOST /v1/shell/kill
    Read screenGET /v2/pty/sessions/{id}/screenPOST /v1/shell/view
    Attach WebSocketGET /v2/pty/sessions/{id}/wsGET /v1/shell/ws
    Anonymous shellGET /v2/pty/ws

    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

    Operationv2v1
    ExecutePOST /v2/code/executePOST /v1/code/execute
    Runtime infoGET /v2/code/infoGET /v1/code/info
    Stateful sessions/v2/code/execute with session_id, GET /v2/code/sessions/v1/code/execute with session_id, /v1/jupyter/*, /v1/nodejs/*

    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

    Operationv2v1
    Browser REST tools/v2/browser/*/v1/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

    Operationv2v1
    Desktop actions/v2/computer/*/v1/browser/actions
    Recording/v2/computer/record/v1/display/record

    The desktop surface is v2-only, under /v2/computer/*; /v1/browser/actions and /v1/display/record remain as aliases for 1.x clients.

    Sandbox

    Operationv2v1
    Sandbox infoGET /v2/sandboxGET /v1/sandbox
    Capability probeGET /v1/capabilities
    Python packagesGET /v2/sandbox/packages?lang=pythonGET /v1/sandbox/packages/python
    Node.js packagesGET /v2/sandbox/packages?lang=nodejsGET /v1/sandbox/packages/nodejs

    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, and SANDBOX_SHUTDOWN_HOOKS
    • /v1/util/convert_to_markdown
    • The stdin field of POST /v1/nodejs/execute; unknown body fields are ignored
    • /v1/sandbox/observe/*
    • Standalone Node REPL servers (DISABLE_NODEJS_REPL, NODEJS_REPL_PORT*)
    • WORKSPACE relocation (there is no --workspace flag)
    • /v1/browser/captcha/*, /v1/browser/state/*, /v1/browser/restart, the /v1/browser/network/* sub-routes route/headers/scoped_headers/export_har (network/requests still works), and /v1/browser/proxy.pac

    Moved to a different entry point:

    • Runtime proxy-mapping API → a static PROXY_MAP on the images
    • /v1/mcp/servers, /v1/mcp/<server>/tools, and the other management routes → the /mcp hub (see MCP below)
    • /v1/browser/page/* page automation → drive the page over CDP (BROWSER_REMOTE_DEBUGGING_HOST/_PORT, default 127.0.0.1:9222); the daemon does not proxy DevTools itself, a deployment has to expose it (nginx does on the images)
    • POST /tickets and JWT auth → use AIO_API_KEY (see Authentication below)

    These /v1/browser/* routes still answer:

    • navigate
    • screenshot
    • click
    • fill
    • evaluate
    • upload
    • config
    • cdp
    • snapshot
    • tabs
    • cookies
    • info
    • network/requests
    • actions

    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:

    KEY=<key>
    
    curl -H "Authorization: Bearer $KEY" "$BASE_URL/v2/sandbox"

    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_SERVERS
    • hidden
    • 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:

    VariableDefaultMeaning
    SANDBOX_API_KEYunsetalias of AIO_API_KEY
    BROWSER_START_MODEasyncasync keeps the browser off the readiness path; auto waits for it; manual (the Computer image default) starts no browser until the desktop's Browser launcher is used
    DISABLE_CODE_SERVERtrueCode Server IDE is off by default
    DISABLE_JUPYTERtruethe /jupyter Lab UI is off; the /v1/jupyter API still works
    EXTRA_MCP_SERVERSunsetsame registry format as --mcp-servers
    MCP_SERVER_BROWSER_PORT8100port mcp-server-browser listens on
    DISABLE_MCP_BROWSERunsettrue stops mcp-server-browser from starting
    PROXY_SERVER / PROXY_MAPunsetstatic proxy mapping (the 1.x runtime mapping API is gone)
    WORKSPACEaccount homemust stay the account's home directory

    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 ipykernel for code execution
    • node for JavaScript
    • rg and tmux for 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 starts
    • RUN_HOOK_PRE_SERVICES — runs before services start
    • RUN_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:

    Daemon limitIn a custom image
    The /mcp hub aggregates only loopback, stateless MCP servers.Run the MCP server inside the image as a supervisord program, and list it in EXTRA_MCP_SERVERS.
    No runtime proxy-mapping API.Set PROXY_SERVER / PROXY_MAP at start.
    No skills CRUD API.Put skill directories on disk and point AIO_SKILLS_PATH at them; the sandbox_load_skill MCP tool reads them.
    Code-server and JupyterLab are off by default.Set DISABLE_CODE_SERVER=false (served at /code-server/) and DISABLE_JUPYTER=false (served at /jupyter).
    The daemon never launches a browser.The image starts Chromium; BROWSER_EXTRA_ARGS adds flags, HOMEPAGE sets the start page.
    No warm kernel by default.Set AIO_KERNEL_PREWARM=1.
    The ref suite (snapshot, ref click/fill/upload) runs on the built-in CDP backend.Point AIO_AGENT_BROWSER_BIN at an agent-browser CLI to delegate it; the AIO image ships one.
    The daemon never starts a desktop.Use aio-computer, or provide an X11 display in your image and run computer-use next to aiod.

    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

    ModuleMethods
    sandboxget_context, get_python_packages, get_nodejs_packages
    shellexec_command and the session methods
    bashexec, output, write, kill, create_session, sessions, close_session
    fileevery method except watch_events: it streams Server-Sent Events, which the generated clients decode as JSON (verified in Go); read that route over HTTP
    jupyterexecute_code, get_info, create_session, list_sessions, delete_session
    nodejsexecute_code, get_info, create_session, list_sessions, get_session, delete_session
    codeexecute_code, get_info
    browserget_info, screenshot, set_config, execute_action
    browser_tabslist, create
    browser_cookies, browser_networkget_cookies, get_requests
    displayrecord

    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:

    CallWhy
    nodejs.update_sessionSends PATCH /v1/nodejs/sessions/{id}, which answers 404
    browser_tabs.activateSends PUT; the route takes POST and answers 405
    browser_tabs.closeTyped as a tab index; the route keys on the CDP target id and answers 404 no tab with id 1

    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_pac
    • mcp.list_mcp_servers, mcp.list_mcp_tools
    • skills.*
    • sandbox.list_hooks, sandbox.register_hook, sandbox.remove_hook, sandbox.observe_*
    • proxy.*
    • util.convert_to_markdown
    • auth.create_ticket