Agent Sandbox Registry

python-sdk

Agents API — Hosting Coding Agents in Agent Sandbox

docs/en/design/agents-api.md

Agents API — Hosting Coding Agents in Agent Sandbox

Status: server side implemented, Python client (section 7) not (design revised after two review rounds, see agents-api-review.md). The CLI has no abx agent command; the Agents API is the one way to run an agent in a sandbox. Not shipped: section 7 (the Python client agent_sandbox.agents with Agent, Runner, Session, sb.agents) does not exist in the SDK, and only codex and qwen-code have a local backend. claude-code, cursor and command are declared adapters that GET /v1/agents/runtimes reports as implemented: false; text below about their behaviour was not checked against a real binary. The shipped routes are in the Agents API reference. Chinese status page: zh (a short status page, not a translation).

Agent Sandbox gains an agent hosting layer: a coding agent CLI (Codex CLI, Cursor cursor-agent, Claude Code, Qwen Code, or any custom command) runs inside a sandbox, is configured declaratively, and is driven either through a Python client that mirrors the OpenAI Agents SDK, or through HTTP surfaces that mirror OpenAI's Responses API and (a documented subset of) OpenAI's Agents API so the official openai Python client can talk to it.


1. What "the Agents API" is (research summary)

"OpenAI Agents API" is overloaded. The research (fetched 2026-10-12) found four distinct surfaces; the design treats each differently.

# Surface What it is Our relation
1 Agents API (beta, /v1/agents/sessions, header OpenAI-Beta: agents=v1) A managed Codex harness. A session holds a conversation, turns run the agent, items (message, reasoning, command_execution, mcp_call, function_call, ...) are the saved work, events (agent.session.*, agent.output.command_execution_output.delta, error) are streamed as SSE. Execution environments: none, openai_hosted, self_hosted (the customer runs codex exec-server --remote <url> --environment-id <id> with CODEX_API_KEY inside their own sandbox). Inputs are events: agent.session.input.message, .cancel, .tool_result. The closest conceptual match to "host a coding agent in a sandbox". We serve a subset of this wire (sessions/turns/items/events) from the sandbox, and we ship a small helper to connect an Agent Sandbox as a self_hosted environment of OpenAI's own service.
2 Responses API (/v1/responses) Stateless-by-default model API with agentic tool use: input, instructions, previous_response_id, store, background, stream, SSE response.* events with sequence_number, /cancel, stream resume with starting_after. The lingua franca: every OpenAI client and the Agents SDK can use it via base_url. We serve a faithful subset with one coding agent turn = one response.
3 OpenAI Agents SDK (openai-agents-python) Client library: Agent, Runner.run / run_sync / run_streamed, RunResult(.final_output, .new_items, .last_response_id), stream events (RawResponsesStreamEvent, RunItemStreamEvent, AgentUpdatedStreamEvent), Session memory protocol, handoffs, guardrails, tracing, MCP server classes, SandboxAgent, experimental codex_tool, custom models through OpenAIResponsesModel(AsyncOpenAI(base_url=...)). We mirror names and shapes of Agent, Runner, RunResult, stream events and Session in agent_sandbox.agents, without depending on the package. Because surface 2 is served, the real Agents SDK also works against a sandbox via base_url.
4 Assistants API (deprecated, sunset 2026) Threads/runs. Out of scope (deprecated; migration guide points to Responses/Conversations).

Sources

OpenAI (all fetched via the .md variants listed in https://developers.openai.com/api/docs/llms.txt):

Coding agents:


2. Scope decisions

In scope

  1. Runtime adapters (server side): codex, claude-code, cursor, qwen-code, and generic command; third-party adapters through a plugin interface.
  2. Hosted agent runtime (agent_sandbox.server.agents), a new capability group agents of the sandbox HTTP server. Disabled by default. It spawns agent CLIs per turn, keeps sessions, normalises their output to a common event model, enforces limits, redacts secrets.
  3. Two HTTP wires over that runtime:
  4. Responses wire — POST /v1/responses (+ GET, DELETE, /cancel, input_items, GET /v1/models) with streaming SSE.
  5. Agents wire (subset) — /v1/agents/sessions (create, retrieve, list, delete), /events (post inputs, stream events), /turns, /items.
  6. Python client agent_sandbox.agents: Agent/AgentConfig, Runner, RunResult, stream events, Session, declarative agent.yaml, AgentHost (bootstrap + connection), one-liner sandbox.agents.run(...).
  7. OpenAI self-hosted environment helper: run codex exec-server --remote … --environment-id … in an Agent Sandbox so the OpenAI-hosted Agents API can use it.
  8. Docs (en/zh), examples, offline tests with fake agent binaries.

Out of scope (and why)

Item Reason
Vaults, webhooks, traces, artifacts, subagents, computer_use, openai_hosted environments of the Agents wire Not meaningful for a self-contained sandbox; return 404/400 with a clear error.
Client-executed function tools (tool_result input, requires_action) The CLIs execute their tools internally; there is no hook to pause a CLI turn and return a result. Rejected with 400 unsupported_parameter. MCP tools are the supported extension point.
Hosting a model (/v1/chat/completions) We host agents, not models; model/provider is passed through to the CLI.
Mid-turn steering CLIs are one-shot processes per turn; extra input is queued (the Agents API also has a queued turn status). Documented deviation.
Persisting full event history across server restarts Only session records, native resume ids and final items are persisted (no secrets).
Memory limits via rlimits RLIMIT_AS breaks Node/V8; use sandbox sizing. We ship wall-clock, idle, output-size and CPU-seconds limits.

Why the server hosts the adapters (single source of truth)

Alternative considered: the client would drive CLIs through sandbox.commands.stream and parse output locally. Rejected: (a) the parser/adapter logic would have to exist twice if we also want an in-sandbox HTTP surface; (b) the openai client and non-Python callers could not use it; (c) sessions would die with the client. Instead: adapters live only in agent-sandbox-server; the Python client is a thin HTTP client plus bootstrap. Server-sdk stays stdlib-only (YAML is optional there; JSON is the wire format).


3. Architecture

 your code ── agent_sandbox.agents (Agent / Runner / Session) ─┐
 openai.OpenAI(base_url=…/v1) ───────────────────────────────┤  HTTPS + X-Access-Token / Bearer
 openai-agents (OpenAIResponsesModel) ───────────────────────┘
                                                              ▼
 ┌─────────────────────────── sandbox ──────────────────────────────────┐
 │ SandboxServer  (group "agents" enabled)                               │
 │  /v1/responses   /v1/agents/sessions/**   /v1/models   /agents/**     │
 │        │ wires: responses_wire.py / sessions_wire.py (pure mappers)   │
 │        ▼                                                              │
 │  AgentRuntime ── SessionStore ── Turn worker threads                  │
 │        │ Invocation(argv, env, stdin, cwd, files)                     │
 │        ▼                                                              │
 │  AgentAdapter (codex | claude-code | cursor | qwen-code | command | ext)│
 │        │ subprocess (own process group, allowlisted env)              │
 │        ▼  JSONL on stdout ──► adapter parser ──► RuntimeEvent         │
 └──────────────────────────────────────────────────────────────────────┘

Layering (respects AGENTS.md): agent_sandbox.server.agents imports only the stdlib and agent_sandbox.server.{router,types}; agent_sandbox.agents (client) never imports agent_sandbox.server.

Enabling the group

CapabilityGroup.AGENTS = "agents" is an opt-in group (like dev_tools, browser): its 70 endpoints are declared in server/endpoints.py and off by default. Ways to enable:

With the group disabled all /v1/responses, /v1/agents/**, /v1/vaults/** routes answer 404 endpoint_disabled and name the rule (default:opt-in:agents). No other default behaviour changes.

Authentication

Existing routes keep X-Access-Token only. For routes of group agents, the server additionally accepts Authorization: Bearer <token> (same token, constant-time compare), because the official openai client can only send Authorization. With no token configured the server is open (existing local mode). Bootstrap always sets a random token on the dedicated instance.


4. Agent configuration

One schema, three spellings: Python, YAML/JSON file, HTTP JSON.

# agent.yaml
name: reviewer                     # unique id on the host; used as the OpenAI "model"
runtime: codex                     # codex | claude-code | cursor | qwen-code | command | <plugin>
model: gpt-5-codex                 # passed to the CLI (optional → CLI default)
instructions: |                    # system-level instructions
  You are a careful code reviewer.
provider:                          # model endpoint for the CLI
  base_url: https://llm.example.com/v1
  api_key: ${OPENAI_API_KEY}       # resolved on the *client*; ends up in memory only
  wire_api: responses              # codex only: responses | chat
workspace:
  path: /workspace/repo            # cwd of the agent (default /workspace)
  git: {url: https://github.com/o/r.git, ref: main, token: ${GITHUB_TOKEN}}
  upload: ./                       # client-side directory to upload (skips .git, node_modules, .env …)
env: {LOG_LEVEL: debug}            # non-secret child env
secrets: {NPM_TOKEN: ${NPM_TOKEN}} # secret child env: never logged, never returned
mcp_servers:
  docs:  {url: https://mcp.example.com/mcp, headers: {Authorization: "Bearer ${DOCS_TOKEN}"}}
  files: {command: npx, args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]}
permissions: full_access           # read_only | workspace_write | full_access (default)
limits:
  turn_timeout_s: 1800
  idle_timeout_s: 300
  max_turns: 40                    # CLI-side agent-turn cap where supported
  max_budget_usd: 5                # claude only
  max_output_bytes: 8388608
  cpu_seconds: null
extra_args: ["--some-flag"]        # appended verbatim (escape hatch)
normalised meaning codex claude-code qwen-code cursor
read_only inspect only sandbox_mode="read-only" --permission-mode plan --approval-mode plan --mode ask (no --force)
workspace_write edit + run inside workspace sandbox_mode="workspace-write" --permission-mode acceptEdits + --allowedTools Read,Edit,Write,Bash --approval-mode auto-edit --force
full_access (default) the sandbox is the isolation boundary sandbox_mode="danger-full-access" --permission-mode bypassPermissions (+ IS_SANDBOX=1 when root) --approval-mode yolo --force

The default is full_access because (1) the CLI's own inner sandbox (Landlock/seccomp) commonly fails inside containers, and (2) the Agent Sandbox VM is the security boundary; the real risk is credential exposure, handled by the env allowlist (section 9).

Python spelling

from agent_sandbox.agents import Agent, Provider, Workspace, MCPServer, Limits

agent = Agent(
    name="reviewer", runtime="codex", model="gpt-5-codex",
    instructions="You are a careful code reviewer.",
    provider=Provider(base_url="https://llm.example.com/v1", api_key=os.environ["OPENAI_API_KEY"]),
    workspace=Workspace(git="https://github.com/o/r.git", ref="main"),
    mcp_servers={"docs": MCPServer(url="https://mcp.example.com/mcp", headers={"Authorization": f"Bearer {tok}"})},
    limits=Limits(turn_timeout_s=900),
)
agent = Agent.from_yaml("agent.yaml")          # needs the `declarative` extra (PyYAML)
agent = Agent.from_dict({...})

Credential defaults

If provider.api_key is omitted the client leaves the choice to the runtime's conventional environment: OPENAI_API_KEY/CODEX_API_KEY (codex), ANTHROPIC_API_KEY (claude-code), CURSOR_API_KEY (cursor), OPENAI_API_KEY (qwen-code), read from the local environment by Agent.with_env_credentials() (explicit, never implicit). The server never forwards its own environment to the child except the allowlist.


5. Runtime adapter interface

class AgentAdapter(ABC):
    name: ClassVar[str]                    # "codex"
    aliases: ClassVar[tuple[str, ...]] = ()  # ("claude", "cursor-agent", ...)
    binary: ClassVar[str]                  # executable looked up on PATH
    install_steps: ClassVar[tuple[tuple[str, ...], ...]] = ()  # e.g. (("npm","i","-g","@openai/codex"),)
    capabilities: ClassVar[AdapterCapabilities]   # resume, partial_deltas, mcp, system_prompt, base_url, max_turns, budget
    prompt_transport: ClassVar[Literal["stdin", "argv"]] = "stdin"

    def validate(self, spec: AgentSpec) -> list[str]: ...        # config errors (e.g. cursor + base_url)
    def build_invocation(self, ctx: TurnContext) -> Invocation: ...
    def new_parser(self, ctx: TurnContext) -> EventParser: ...
    def health_argv(self) -> list[str]: ...                      # default [binary, "--version"]
    def credential_env(self, spec) -> dict[str, str]: ...        # provider → env mapping (secret values)
    def classify_failure(self, returncode, stderr_tail) -> RuntimeErrorInfo: ...  # → code/message

Third-party extension

# my_pkg/adapter.py
from agent_sandbox.server.agents import AgentAdapter, register_adapter

class AiderAdapter(AgentAdapter): ...
register_adapter(AiderAdapter)

Discovery order: built-ins → importlib.metadata entry-point group agent_sandbox.agent_adapters (value module:Class) → modules named in ABX_AGENTS_ADAPTER_MODULES (comma list, imported at server start) → register_adapter() at runtime. Name clashes: later registration wins only if override=True, otherwise error. A declarative adapter needs no code: runtime: command with command: {argv: [...], parser: "text"|"jsonl-claude"|"jsonl-codex", resume_argv: [...]} covers most "prompt in, text/JSONL out" tools.

Built-in adapters

codex claude-code qwen-code cursor command
binary / install codex · npm i -g @openai/codex claude · npm i -g @anthropic-ai/claude-code qwen · npm i -g @qwen-code/qwen-code cursor-agent · curl https://cursor.com/install -fsS \| bash user supplied
invocation (turn 1) codex exec --json --skip-git-repo-check -C <cwd> [-m M] -c sandbox_mode=… -c approval_policy="never" [-c k=v…] - (cwd is also the process cwd; -C and -s are not passed on resume, where only -c overrides are portable) claude -p --output-format stream-json --verbose --include-partial-messages [--model M] [--permission-mode …] [--append-system-prompt …] [--mcp-config F] [--max-turns N] [--max-budget-usd X] qwen -p … --output-format stream-json --include-partial-messages [--model M] [--approval-mode …] [--append-system-prompt …] cursor-agent -p --output-format stream-json --stream-partial-output [--model M] [--force] <prompt>
resume codex exec resume <thread_id> … --resume <session_id> --resume <session_id> --resume <chat_id> resume_argv or none
prompt via stdin (-) stdin stdin argv (no stdin support); a prompt starting with - is prefixed with Task: so it cannot be parsed as a flag stdin or argv
native session id thread.started.thread_id system/init.session_id / result.session_id system/session_start.session_id system/init.session_id n/a
credentials CODEX_API_KEY (+ custom provider block -c model_providers.abx={name,base_url,env_key,wire_api} -c model_provider="abx") ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, optional ANTHROPIC_AUTH_TOKEN; CLAUDE_CODE_OAUTH_TOKEN OPENAI_API_KEY, OPENAI_BASE_URL, --auth-type openai CURSOR_API_KEY; base_url unsupported (validation error) none
MCP -c mcp_servers.<n>.command/args/url/bearer_token_env_var/env_http_headers --mcp-config <turn_dir>/mcp.json with ${VAR} placeholders <workdir>/.qwen/settings.json mcpServers (placeholders) — experimental <workdir>/.cursor/mcp.json (${env:VAR} placeholders) — experimental n/a
system prompt preamble on first turn --append-system-prompt --append-system-prompt preamble on first turn preamble
exit semantics 0 ok 0 ok, result.is_error 0 ok, 53 turn limit, 55 budget, 130 interrupted 0 ok 0 ok

Qwen/Cursor MCP note: those CLIs have no per-invocation MCP flag, so the adapter writes a project-scoped config file each turn (<workdir>/.qwen/settings.json, <workdir>/.cursor/mcp.json) that contains only ${VAR} placeholders for secrets (the values travel in the child env). The adapter merges only the mcpServers key, never overwrites a file it did not create without merging, and adds the path to .git/info/exclude when the workdir is a git repo. These two paths are the least verified (no live binary here) and are marked experimental in GET /v1/agents/runtimes.

Codex JSONL → events: thread.started → session; item.started/completed with item.type ∈ agent_message (→ message), reasoning (→ reasoning), command_execution (→ tool_start/tool_end kind=command, aggregated_output, exit_code), mcp_tool_call (→ kind=mcp, server,tool,arguments, result/error), file_change (→ kind=file_change), web_search, todo_list; turn.completed.usage → usage; turn.failed/error → error. Codex has no token deltas in exec --json; messages arrive complete (the wires synthesise a single delta, see 6.3).

Claude/Qwen stream-json: system/init → session; stream_event with content_block_delta.text_delta → text_delta, thinking_delta → reasoning delta; assistant messages with tool_use blocks → tool_start, text blocks → message (de-duplicated against streamed deltas by message id); user messages with tool_result → tool_end; result → usage + result. Cursor: system/init, assistant (partial deltas when --stream-partial-output, then one consolidated duplicate that is dropped by accumulated-text comparison), tool_call started/completed (call_id, <name>ToolCall.{args,result}), result.

Install & health


6. Runtime semantics

6.1 Sessions, turns, items

Session  id=agsess_<hex>  agent=<name>  status idle|in_progress|failed
  native_session_id (resume handle)   created_at  last_active_at  metadata
  turns: Turn id=turn_<hex>  status queued|in_progress|completed|failed|cancelled
         started_at completed_at  error{code,message}  usage
         items: message | reasoning | command_execution | mcp_call | file_change
  events: ring buffer of RuntimeEvent(seq, type, turn_id, item_id, data)  (default 20 000)

6.2 Process execution

subprocess.Popen(argv, cwd, env, stdin=PIPE|DEVNULL, stdout=PIPE, stderr=PIPE, start_new_session=True); one reader thread per pipe; a watchdog enforces turn_timeout_s, idle_timeout_s (no stdout/stderr bytes) and max_output_bytes; stderr kept as a redacted ring (last 16 KiB) used only for failure classification and error.message. Lines longer than 4 MiB are truncated. Exit-code/parse failures are mapped by adapter.classify_failure() to the Agents-API error code set (authentication_error, rate_limit_exceeded, request_timeout, context_length_exceeded, sandbox_error (binary missing/not executable), invalid_request, internal_error, …). Two extra codes exist on the Responses wire only: runtime_not_installed, output_limit_exceeded (the Agents wire maps them to sandbox_error).

6.3 Event model (neutral) and wire mapping

RuntimeEvent types: turn.queued, turn.started, item.added, item.text.delta, item.reasoning.delta, item.output.delta, item.done, usage, turn.completed|failed|cancelled, log (redacted, debug only).

Responses wire (stream:true, SSE event: <type>\ndata: {…}\n\n, every event has sequence_number):

response.created → response.in_progress
  per item:  response.output_item.added
             [message]   response.content_part.added → response.output_text.delta* → response.output_text.done → response.content_part.done
             [reasoning] response.reasoning_summary_part.added → response.reasoning_summary_text.delta* → …done
             [tool]      (mcp_call) response.mcp_call.in_progress → response.mcp_call_arguments.done → response.mcp_call.completed|failed
             response.output_item.done
→ response.completed | response.failed | response.incomplete   (then `data: [DONE]` is NOT sent; OpenAI does not)

Item mapping: assistant text → message (output_text); reasoning summary → reasoning (summary); every CLI tool activity → mcp_call with server_label=<runtime> (e.g. codex), name ∈ command_execution, file_change, web_search, or the real MCP tool name for MCP calls, arguments (JSON string), output, error, status. Rationale: mcp_call is a server-executed item, so the OpenAI clients/Agents SDK treat it as already done and never try to execute it locally (function_call or local_shell_call would trigger client-side execution/errors). Codex delivers whole messages: we emit one output_text.delta carrying the full text, then done, so delta consumers work unchanged. usage → response.usage (input_tokens, output_tokens, total_tokens, details). Unknown token info → zeros (usage is never null on completed to satisfy typed clients).

Agents wire (/v1/agents/sessions/{id}/events SSE, data = event JSON with type and event_id):

agent.session.created (create only) → agent.session.in_progress
→ agent.session.turn.created → agent.session.turn.in_progress
→ agent.session.turn.item.added → [content_part.added → output_text.delta* → output_text.done]
→ agent.session.turn.item.done            (message | reasoning | command_execution | mcp_call)
   agent.output.command_execution_output.delta  (streamed command output)
→ agent.session.turn.completed|failed|cancelled → agent.session.idle  (or agent.session.failed)
→ `error` events for non-terminal problems

The official SDK's AgentSessionStream.get_final_result() and AgentTurnResultCollector consume exactly this (turn created → item.done messages → turn completed → idle) and are used as an oracle.

6.4 HTTP endpoints

All under group agents. JSON errors on /v1/** use the OpenAI envelope {"error": {"message", "type", "param", "code"}}; native /agents/** routes use the existing Agent Sandbox {"error", "type"} shape (as of 2026-10-13 every agents route is under /v1).

The table below is the original design. The shipped list is the 70 routes of the Agents API reference; /agents/runtimes[/install|/doctor] became GET /v1/agents/runtimes (no install/doctor), /agents/definitions became /v1/agents CRUD, and /agents/health does not exist.

Method Path Purpose
GET /v1/models, /v1/models/{id} registered agents as model objects (owned_by: "agent-sandbox")
POST /v1/responses run one turn (stream or not, background supported)
GET /v1/responses/{id} retrieve; ?stream=true&starting_after=N replays/continues SSE
DELETE /v1/responses/{id} forget response (and its turn)
POST /v1/responses/{id}/cancel cancel a running/background response
GET /v1/responses/{id}/input_items the prompt items of the turn
POST /v1/agents/sessions create session; optional input (+ stream)
GET /v1/agents/sessions · /{id} list (limit, after, order) · retrieve
DELETE /v1/agents/sessions/{id} delete (kills active turn)
POST /v1/agents/sessions/{id}/events {"events":[{type:"agent.session.input.message",input:[…]} \| {type:"agent.session.input.cancel"}]}
GET /v1/agents/sessions/{id}/events SSE stream (Last-Event-ID/?after= resume)
GET /v1/agents/sessions/{id}/turns · /{turn_id} turns
GET /v1/agents/sessions/{id}/items · /turns/{turn_id}/items items
GET /v1/agents/runtimes adapters (implemented, installed, default)
GET POST DELETE /v1/agents[/{agent_id}] register agent definitions (secrets are write-only)

/v1/responses request handling

6.5 Compatibility boundary (summary)

Area Faithful Documented deviation
Responses create / retrieve / cancel / delete / stream request fields above; object shape and event names/order/sequence_number; starting_after resume; background previous_response_id must be latest; function tools & structured output rejected; sampling params ignored; tool activity is mcp_call items; codex has no token deltas; store semantics limited to this server's memory/state dir
Responses GET /models list/retrieve ids are agent names
Agents sessions (subset) create/retrieve/list/delete, input.message, input.cancel, events SSE, turns, items, status machine, error codes, pagination envelope, OpenAI-Beta header tolerated only self_hosted-shaped environment; no vaults/artifacts/subagents/traces/webhooks/tool_result/requires_action; agent.model selects a model within the registered agent's runtime; steering → queued
Agents SDK (Python client) Agent, Runner.run/run_sync/run_streamed, RunResult(final_output, new_items, last_response_id, raw_responses?, usage), stream events (raw_response_event, run_item_stream_event, agent_updated_stream_event), Session protocol no handoffs/guardrails/tracing/function tools (runtime tools are the CLI's); Agent.tools takes MCP servers only; output_type limited to str and Pydantic models parsed from final text

6.6 Streaming semantics, stateless callers and idempotency


7. Python client (agent_sandbox.agents)

from agent_sandbox import Sandbox
from agent_sandbox.agents import Agent, Runner, Session

async with await Sandbox.create(template="base") as sb:
    agent = Agent.from_yaml("agent.yaml")

    # one-liner (a runtime name is enough: sb.agents.run("codex", "…") uses Agent.codex())
    result = await sb.agents.run(agent, "Fix the failing tests")        # bootstraps + runs
    print(result.final_output)
    print(await sb.agents.host(agent).diff())                          # git diff of the workspace

    # explicit host (reuse across calls; sessions keep conversation continuity)
    async with sb.agents.host(agent) as host:                         # AgentHost
        session = Session(host)                                        # sessions mirror Agents SDK
        r1 = await Runner.run(agent, "Add a CLI flag --json", host=host, session=session)
        r2 = await Runner.run(agent, "Now write tests for it", host=host, session=session)

        stream = Runner.run_streamed(agent, "Refactor utils.py", host=host)
        async for ev in stream.stream_events():
            if ev.type == "raw_response_event" and ev.data.type == "response.output_text.delta":
                print(ev.data.delta, end="")
        await stream.cancel()   # or stream.final_output after iteration

Already have a URL? AgentHost.connect(url, token=…) skips the bootstrap (also works for hosts started outside the SDK, or a process on the laptop).

Bootstrap (AgentHost via sandbox.agents.host(agent))

  1. commands.run("python3 -c 'import agent_sandbox.server'"); if missing and install=True (default) → pip install agent-sandbox-server==<client version> (lockstep releases; configurable server_package=/index_url).
  2. Ensure the runtime CLI: GET /agents/runtimes after the server is up; if not installed and install=True → POST …/install.
  3. Write the agent definition: PUT /agents/definitions/{name} (secrets in the body, TLS to the sandbox endpoint, write-only).
  4. Start: commands.start("python3 -m agent_sandbox.server.agents serve --port 9100") with env ABX_SERVER_TOKEN=<random 32 B>, ABX_AGENTS_ALLOW_INSTALL=1 (never on argv). Wait for /health.
  5. Workspace: git clone through GIT_CONFIG_COUNT/KEY/VALUE env (http.extraHeader) — token never on argv, never in .git/config; or upload a local directory via sandbox.files (same skip list as DeployModule: .git, node_modules, .venv, __pycache__, .tox, .env).
  6. URL = sandbox.network.get_url(9100); auth = the random bootstrap token (Authorization: Bearer) plus sandbox.network.get_access_headers().

Existing wrappers

The built-in agent wrappers (agent/builtin.py, BUILTIN_AGENTS) and api/deploy.py were deleted. The Agents API is the one way to run an agent in a sandbox.

OpenAI self-hosted environment helper

from agent_sandbox.agents import connect_openai_environment
proc = await connect_openai_environment(sb, session.environment.remote_url,
                                        session.environment.id,
                                        executor_api_key=os.environ["OPENAI_EXECUTOR_API_KEY"])

Installs @openai/codex@alpha if needed and starts codex exec-server --remote <url> --environment-id <id> with CODEX_API_KEY only in the process env. Lets users drive OpenAI's hosted harness with an Agent Sandbox as the executor — the complementary direction to hosting the CLI.


8. Failure modes

Failure Behaviour
Binary missing turn fails runtime_not_installed (Agents wire: sandbox_error), message includes the install hint; GET /v1/agents/runtimes shows it before running
Bad credentials / 401/403 on stderr or result.is_error authentication_error, redacted message
429 / overloaded rate_limit_exceeded / server_overloaded
Timeout (wall/idle) process group killed, request_timeout; partial items retained
Output too large killed, output_limit_exceeded
Parser exception / malformed JSONL line logged as log, tolerated; fatal parser error → internal_error
Client disconnect mid-SSE stream handler ends; the turn keeps running (sessions are server-owned); re-attach via GET …/events?after= or GET /v1/responses/{id}?stream=true&starting_after=
Server restart in-flight turns marked failed; sessions reload; CLI-native resume still works
Cancel race idempotent; terminal state wins
Disk/HOME wiped resume fails → invalid_request with hint
Prompt starts with - (argv transport, cursor) prefixed with Task:; stdin used wherever the CLI supports it
Two clients post to one session second is queued; no interleaving

9. Security

  1. No secrets on argv. Secrets only in child env (and for MCP/provider config files only ${VAR} placeholders). Prompt via stdin where supported (argv for cursor, which cannot read stdin; prompts are not secrets by policy, and are visible to ps inside the sandbox).
  2. Child env allowlist: PATH, HOME, USER, LOGNAME, SHELL, LANG, LC_*, TERM, TMPDIR, TZ, SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS, HTTP(S)_PROXY, NO_PROXY, NPM_CONFIG_*, plus names in ABX_AGENTS_PASSTHROUGH_ENV, plus adapter credential env, plus spec env/secrets. ABX_SERVER_TOKEN and every other server variable are not inherited.
  3. Redaction: Redactor(secret_values) replaces any secret value (≥ 6 chars) and common token shapes (sk-…, ghp_…, Bearer …, x-api-key) in every event text, tool output, stderr tail, error message and persisted item. The server logs no request bodies.
  4. Write-only secrets: GET /v1/agents returns secret names (provider.api_key: "***"), never values; the state dir contains none.
  5. Workspace confinement for workspace.path: must be absolute, no .., created if absent; the agent itself is not confined (full_access) — documented.
  6. Auth: Bearer accepted only on agents routes; constant-time compare; bootstrap token is per host and random.
  7. Resource abuse: body size cap (existing 10 MB), session/turn caps, concurrency cap, output cap, SSE per-session subscriber cap (32).
  8. State files 0600; per-turn temp dir 0700 and removed; MCP config files contain placeholders only.
  9. Install endpoint disabled unless ABX_AGENTS_ALLOW_INSTALL=1.
  10. The agents group is remote code execution by design (it runs a CLI that runs shell commands). Therefore: default-disabled; when ABX_AGENTS_LOCK_DEFINITIONS=1 the /v1/agents write routes return 403 and only definitions from ABX_AGENTS_CONFIG can be run (use this when a token is handed to a less-trusted caller); runtime: command and extra_args can be disabled with ABX_AGENTS_ALLOW_COMMAND=0 (default 1).
  11. Input validation: agent names [A-Za-z0-9._-]{1,64}; adapter names from the registry only; workspace.git.url scheme ∈ {https, ssh, git} and must not start with - (clone uses git clone -- <url>); workspace.path absolute, no ..; env var names [A-Za-z_][A-Za-z0-9_]* and not in a deny list (LD_PRELOAD, LD_LIBRARY_PATH, PATH overrides are allowed only through explicit env, never via secrets); MCP server names [A-Za-z0-9_-]{1,40}.
  12. Redaction covers encodings: each secret is also matched URL-encoded, JSON-escaped and as Bearer <secret>; base64 or arbitrarily transformed copies are not detectable (documented residual risk).
  13. Residual risks (documented, not solved): with full_access the agent's own shell tool may read the CLI's environment, so a prompt-injected agent can exfiltrate provider.api_key/secrets. Mitigations: Codex excludes *KEY*/*SECRET*/*TOKEN* variables from its tool shells by default; prefer short-lived/scoped keys, a model gateway key, sandbox egress policy, and workspace_write/read_only. A server warning is logged when an agent is registered with full_access and secrets.

10. Backward compatibility

11. Examples (examples/agents/)

As shipped: complete_case.py (starts a local server with stand-in codex and qwen binaries and walks the whole session flow without a model key), hosted_codex.py, hosted_qwen.py (HTTP clients for a running server), local_files_and_session.py, codex_agent.py, browser_automation.py. The 01-08 numbered examples of the design (they need the Python client of section 7) were not written.

12. Testing strategy