Agent Sandbox Registry

python-sdk

Decision: Discovery API two-layer model (local static + remote authenticated)

无中文版

.agents/notes/proposed/feature/2026-09-04-discovery-api-two-layer.md

Decision: Discovery API two-layer model (local static + remote authenticated)

Status: proposed Task: #103, #105, #118

Problem

The SDK's Sandbox.list_commands() already implements basic command discovery (api/sandbox.py returns plain dicts), but there are three gaps: (1) it can only discover commands declared in local YAML and cannot see Python commands registered in-project via @sandbox.register; (2) zero authentication, zero gating (one of only two public APIs across the whole repo that skips check_capability); (3) argument entries are missing a type field. A clean two-layer discovery model needs to be defined.

Decision

Adopt a two-layer discovery model: Layer 1 local static discovery + Layer 2 server remote discovery.

Layer 1: local static discovery (no auth, no server needed)

  1. YAML branch (already implemented): _find_local_template() in api/capability.py scans ~/.abx/templates/**/template.yaml and parses custom_commands: + capabilities:. Triggered by resolve_capabilities() during Sandbox.create() / Sandbox.connect().
  2. Python command module introspection (gap, filled by @sandbox.register): the decorator produces CustomCommand objects at import time and injects them into the same custom_commands dict. CLI introspection only needs to import the user module to discover them.
  3. Permissions: purely local — only reads the ~/.abx/templates directory or imports Python modules. Auth is neither required nor desirable.

Layer 2: server remote discovery (runtime-registered commands)

⚠ Key change (2026-09-05): the original ADR considered only the platform's GET /templates/{id} as a remote discovery source and listed "user-built server exposing a discovery endpoint" as Rejected. That decision has been overturned. The new agent_sandbox.server module (see 2026-09-05-sandbox-server-module.md) provides a GET /commands discovery endpoint and becomes the primary source for Layer 2.

  1. The server module provides a GET /commands discovery endpoint: once agent_sandbox.server is up, GET https://{port}-{sandbox_id}.{domain}/commands returns every registered command with its argument schema. This is the standard discovery path for runtime-registered commands.
  2. Platform template discovery (auxiliary): GET /templates/{id} in protocol/template.py plus auth_headers = await self._auth.get_headers() at transport/http.py:84 injects L1 credentials automatically. abx template info is a live end-to-end proof point. The wiring gap: api/capability.py:196 is a one-liner # 3. TODO(Phase2): online fallback.
  3. Auth reuse:
  4. Platform plane → AuthProvider.get_headers() (ApiKeyAuth or AkSkAuth in transport/auth.py).
  5. envd plane → EnvdTokenManager.get_headers() (4 headers).
  6. Server module discovery endpoint → shares the same transport auth as command execution (X-Access-Token).
  7. Not NetworkModule.get_access_headers(). That one is gated by the ports capability (api/network.py:63), and ports ∉ DEFAULT_CAPABILITIES. Using it for discovery auth would impose a capability prerequisite that most sandboxes lack, directly violating the "discovery permission ⊆ execution permission" principle.

Auth principle

  1. Discovery permission ⊆ execution permission:
  2. Identity-level: automatically holds once Layer 2 is wired. Discovery and execution share the same L1 identity; an invalid token causes platform_request's raise_for_status() to trip first.
  3. Command-level (fine-grained): does not hold today and we cannot unilaterally make it hold. The platform returns a template-level manifest; fine-grained filtering requires the server to filter by caller identity (outside the protocol client's control). V1 explicitly states "only identity-level auth is guaranteed; per-command least-visibility is not".
  4. Current state (Layer 1 only): zero auth, leak surface confined to local ~/.abx/templates, direction is safe (discovery broader than execution).

Current list_commands() defects

  1. Missing argument type field: CustomCommandArg (models/template.py) has no type; list_commands() outputs args with only name/required/ default/description. To be completed by the M1 milestone of the @sandbox.register ADR.
  2. Zero auth is the correct design (Layer 1): it is a sync method reading a local dict without any network call — auth neither should nor can be added. Once Layer 2 is wired, platform_request handles auth automatically.

O5 platform custom_commands carriage

  1. Whether GET /templates/{id} responses carry custom_commands is TBD. TemplateInfo in models/template.py has no such field, only a generic metadata: dict[str, Any]. custom_commands is a client-side YAML extension in this repo, not part of the E2B/FC template model. Real E2E verification is required (#103): run abx template info <id> --json and inspect whether the response contains metadata / customCommands. This validation directly determines whether M6b (remote discovery wiring) is "one function call" or "requires platform-side support".

API Design

# Layer 1: local discovery (implemented, to be enhanced)
commands = sandbox.list_commands()
# → [{"name": "demo", "description": "...",
#     "args": [{"name": "x", "type": "int", "required": True,
#               "default": None, "description": "..."}]}]

capabilities = sandbox.capabilities
# → frozenset({"shell", "files", "code"})

# Layer 2: remote discovery (to be wired)
# resolve_capabilities() 3rd fallback:
#   template_info = await template_protocol.get(template_id)  # GET /templates/{id}
#   # auth_headers auto-injected by platform_request (Authorization: Bearer)
#   return parse_metadata(template_info.metadata)

Alternatives considered

Dependencies

Test Strategy

Acceptance criteria

Evidence