Agent Sandbox Registry

python-sdk

08 - Template building UX

docs/en/archive/server-sdk-v2/08-template-building-ux.md

08 - Template building UX

Status: archive. Not the current specification.

Status: target design, not implemented. The shipped server uses endpoint ids and one policy engine; see README section 1 for what exists and section 2 for what does not.

Part of Server SDK v2. How a template author (human or AI coding agent) gets a correct, minimal, policy-controlled server into an image. Related: 06, base-image-and-server-sdk.md. v2.1: the server: block is compiled with strict unknown-key rejection (IF-31) and a schema version; the docker build injects the server package for Alibaba base-image templates, PEP 668 safe (IF-22, IF-13); presets use one keyword root= (B-8); legacy is never implicit on a new template; the journeys were executed as eight sample templates (B-11..B-13).

1. Goals

2. The server: section of template.yaml

server:                              # all keys optional; absent section = legacy default server (today's behaviour)
  version: 1                         # (v2.1) schema version of the block; written into server.json; unknown versions are refused by the runtime
  preset: ci-runner                  # name or list of names (03 section 4; hyphen and underscore spellings both accepted); params as siblings (root, commands, ...)
  root: /workspace                   # (v2.1, B-8) the ONE keyword for the preset's root(s); `paths:` and `workspace:` are accepted aliases
  enable: ["agents.codex.**"]       # extra selectors to allow (unscoped; prefer rules for scoped grants)
  disable: ["terminal.*", files.delete]       # sugar for deny rules
  rules:                             # full-fidelity rules (03 section 10 schema)
    - allow: [files.download, files.stat, files.list]
      paths: {roots: [/tmp], deny: ["**/.env"], max_bytes: 10MB}
  custom:                            # declarative named commands -> custom.<name> ops (safest way to run things)
    grade:
      argv: ["python3", "/workspace/grade.py", "--submission", "{submission}"]
      params: {submission: {type: path, root: /workspace/submissions}}
      cwd: /workspace
      timeout: 60s
      effects: [exec]
      principals: [role:grader]
  auth:
    tokens: [{id: app, roles: [dev], token_env: APP_API_KEY}]
  agents: [codex]                    # adapter names or declarative definitions
  agent:
    codex: {llm: {base_url_env: LLM_BASE_URL, api_key_env: LLM_API_KEY, model: gpt-5-codex}, permissions: workspace-write}
  install: {mode: build, mirrors: {npm: "https://registry.npmmirror.com"}}   # (v2.1) build = prepare at image build; the recommended FC setting
  audit: {sink: stderr}
  load: [my_template.ops]            # python modules with @op (shipped in the image)

3. Python flavour (when YAML is not enough)

# server.py  (COPY into the image; `agent-sandbox-server serve --load /app/server.py`)
from agent_sandbox.server import Policy, op, ops, presets

@op("grader.run", effects="exec", paths=("submission",), summary="Grade a submission directory")
def run(ctx, submission: str, timeout: int = 60) -> dict:
    r = ctx.run(["python3", "/workspace/grade.py", ctx.paths["submission"]], timeout=timeout)   # ctx.run = admitted subprocess helper
    return {"exit_code": r.exit_code, "stdout": r.stdout[-4000:]}

policy = presets.minimal() + Policy().allow(ops.grader.run, paths="/workspace/submissions", principals=["role:grader"])
# convention: `serve --load server.py` imports the module; a module-level `policy` (Policy) becomes the author-layer policy
# (or call start(policy=policy) under `if __name__ == "__main__":`)

ctx.run(argv, ...) is the sanctioned subprocess helper for custom ops: no shell, scrubbed env, rlimits and process-group handling from the same launcher as commands.run.

4. Journeys

4.1 Download-only file service

server: {preset: download-only, root: /data}

Dockerfile: generated by the builder (06 section 8; PEP 668 safe) - nothing else installed.

4.2 Grader / CI runner

server:
  preset: ci-runner
  root: /workspace                  # read-only workspace by default (v2.1, B-9); writable: true is an opt-in and then commands must not run files the caller can write
  rules:
    - allow: commands.run
      commands: {allow: ["pytest -q {path}", "python3 /workspace/grade.py"], cwd: /workspace, timeout: 120s}
  auth: {tokens: [{id: ci, roles: [runner], token_env: CI_TOKEN}]}      # CI_TOKEN must be set and >= 16 chars, else the server refuses to start

4.3 Codex (or any CLI agent) behind the OpenAI Agents API

See 05 section 12: preset: agent-host, agents: [codex], llm: in agent.codex. Image has no codex baked in; first use installs it. For a self-contained image add install: {mode: build} and the build runs prepare (recommended on FC). The qoder, qwen_code and cursor adapters are flagged experimental in agents.runtimes.list.

4.4 Several agents + browser

server:
  preset: agent-host
  agents: [codex, claude_code]
  enable: ["browser.navigate", "browser.screenshot", "browser.content"]
  rules:
    - allow: browser.navigate
      urls: {allow: ["https://*.example.com/*"], block_private: true}
  install: {mode: lazy}

5. Local development loop

  1. agent-sandbox-server init --preset ci-runner --root /workspace > server.yaml (annotated starter).
  2. agent-sandbox-server describe --effective --config server.yaml - exactly which ops are mounted, with scopes (the thing the AI/human should read before shipping).
  3. agent-sandbox-server check-config --config server.yaml --strict; explain --op files.download --arg path=/etc/passwd [--principal ID] to try attacks offline (one spelling, N-4; serve --explain-denials turns on verbose 404 hints in development, N-6).
  4. agent-sandbox-server serve --config server.yaml --port 9000 locally; drive with ServerClient("http://127.0.0.1:9000") or abx server call.
  5. agent-sandbox-server doctor inside a built image (abx connect then run it) to see base-image contract, tools, writable dirs.
  6. abx template build.

6. Migrating existing templates

Today v2
commands.py calling start() with registry.command(...) works unchanged with the compatibility policy presets.legacy_compat(), which is applied only when no policy is given; as soon as you add a policy, legacy custom.*/ext.* are no longer mounted unless you allow them, and agent-sandbox-server check-config --legacy-diff lists exactly the v1 ops the new policy drops (B-6; spike legacy_diff, templates/t8_v1_migration.py)
commands.py implementing a whole API (codex-agent-api) delete; preset: agent-host, agents: [codex]; keep template.yaml env mapping as llm:
Dockerfile RUN npm i -g @openai/codex remove (lazy install) or replace by RUN agent-sandbox-server prepare (needs network at build time)
Dockerfile RUN pip install agent-sandbox-server on the Alibaba base image fails (PEP 668); use the builder injection (generated) or --break-system-packages / venv
ABX_SERVER_DISABLED_GROUPS=... env in Dockerfile still works; prefer disable:
capabilities: [shell, files] unchanged (client-side gating)

codex-agent-api before/after: 15 routes hand-registered with table.route(...) in group COMMANDS (so ABX_SERVER_DISABLED_GROUPS=commands would also kill them, and the default /v1/agents/{agent_id} literal-vs-variable order was a latent bug) -> generated from the registry, individually policy-addressable (agents.codex.sessions.*), usable with the stock openai SDK.

7. Author-facing errors and lints (messages are part of the UX)

8. Interaction with the other design tracks