Agent Sandbox Registry

python-sdk

Decision: agent_sandbox.server — User Opt-in In-Container HTTP Server Module

无中文版

.agents/notes/implemented/architecture/2026-09-05-sandbox-server-module.md

Decision: agent_sandbox.server — User Opt-in In-Container HTTP Server Module

Status: implemented Implemented: 2026-09-21 Task: #118

Problem

Users need to register and execute custom commands inside the sandbox container. The original approach (source-delivery execution: files.write to deliver a script + commands.run to execute it) requires the full chain on every call, with high latency, no persistent state, and no concurrency support. A persistent in-container HTTP server is needed to carry custom commands, while introducing no third-party dependencies (the container may have no pip package-management environment).

Decision

Add an agent_sandbox.server module: a stdlib-only, zero-dependency in-container HTTP server that the user opt-in starts.

Core design principles

  1. stdlib-only, zero dependencies: uses only the Python standard library (http.server, json, subprocess, os, shutil) with no third-party packages. Reason: the in-container environment is uncontrolled and may have no pip/virtualenv; we cannot assume the user image pre-installs any Python package.

  2. User opt-in, not a fallback agent: the server module is never auto-deployed or auto-started. The user must explicitly call sandbox.server.start(), which delivers the server source into the container via files.write and starts it in the background via commands.start. envd remains the base carrier for standard capabilities (files/process/code/PTY); the server module does not replace envd.

  3. Operates directly on the local filesystem, not bound to envd: the built-in commands (upload/download/runshell) call the Python standard library directly to operate on the container's local filesystem and processes, without going through the envd API. Reasons: (a) it avoids an extra server→envd network hop; (b) the server runs inside the container and has direct filesystem and process access.

  4. ports capability gating: the server module requires the template to declare capabilities: [..., ports]. ports ∉ DEFAULT_CAPABILITIES = {shell, files, code}. The client reaches the server via https://{port}-{sandbox_id}.{domain}, relying on the Gateway's routeDynamic dynamic port routing.

Final implementation scale

The server module ultimately expanded to 42 endpoints, managed in 8 CapabilityGroups (CORE, COMMANDS, FILE_OPS, PROCESS, SYSTEM, TERMINAL, DEV_TOOLS, BROWSER). It uses a declarative RouteTable + CapabilityGroup mechanism instead of hard-coded if/elif routing.

Built-in commands

  1. upload: upload a file to the container. The request body carries the file content and target path.
  2. download: download a file from the container. The request body carries the source path; the response carries the file content.
  3. runshell: run a shell command. The request body carries the command string; the response carries stdout/stderr/exit_code.

WIRE CONTRACT (client↔server communication protocol)

  1. Command execution: POST /commands/{name}
  2. Request Content-Type: application/json
  3. Request Body: {"key": "value", ...} (the command's kwargs)
  4. Success Response: 200 OK, {"result": <return_value>}
  5. Error Response: 4xx/5xx, {"error": "<message>", "type": "<exception_class>"}

  6. Command discovery: GET /commands

  7. Response: 200 OK, {"commands": [{"name": "upload", "args": [...]}, ...]}

    Note: an early ADR showed a bare-array format in its example; the actual implementation wraps it in a {"commands": [...]} object.

  8. Health check: GET /health

    • Response: 200 OK, {"status": "ok"}

Client API

  1. sandbox.server.start(port=8080): deliver the server source into the container + start it in the background + wait for the health check to pass.
  2. sandbox.server.call(command_name, **kwargs): send a POST /commands/{name} request and parse the JSON response.
  3. sandbox.server.discover(): send a GET /commands request and return the command list.

API Design

# Client side
from agent_sandbox import Sandbox

sandbox = await Sandbox.create(template="my-template")

# Start the in-container server (opt-in)
await sandbox.server.start(port=8080)

# Call a built-in command
result = await sandbox.server.call("runshell", cmd="ls -la /home/user")
# → POST https://8080-{sandbox_id}.{domain}/commands/runshell
# → Body: {"cmd": "ls -la /home/user"}
# ← {"result": {"stdout": "...", "stderr": "", "exit_code": 0}}

# Upload a file
await sandbox.server.call("upload", path="/home/user/script.py", content="print('hello')")

# Download a file
result = await sandbox.server.call("download", path="/home/user/output.txt")

# Discover all commands
commands = await sandbox.server.discover()
# → GET https://8080-{sandbox_id}.{domain}/commands
# ← [{"name": "upload", ...}, {"name": "download", ...}, {"name": "runshell", ...}]
# In-container server module (agent_sandbox/server/)
# Pure stdlib, zero dependencies

from http.server import HTTPServer, BaseHTTPRequestHandler
import json, subprocess, os, shutil

class SandboxHandler(BaseHTTPRequestHandler):
    """Handles /commands/{name}, /commands, and /health requests"""
    ...

class SandboxServer:
    """Command registration + HTTP server lifecycle management"""
    def register(self, name: str, handler: Callable) -> None: ...
    def start(self, host: str = "0.0.0.0", port: int = 8080) -> None: ...

Alternatives considered

Dependencies

Test Strategy

Acceptance criteria

Evidence