Agent Sandbox Registry

python-sdk

Provider authoring evaluation — how hard is it to add a provider?

docs/en/design/provider-authoring-evaluation.md

Provider authoring evaluation — how hard is it to add a provider?

Status: Evaluation (evidence-based) with a resolution pass applied (see §10) · Subject: the opensandbox exec-provider plugin (awesome-providers/opensandbox) written against the SDK docs, then driven against a real local OpenSandbox server. The plugin source is that catalog, not this repository. · Companion docs: providers-and-credentials.md (normative design), provider-opensandbox-mapping.md (research that fed the plugin), ../guide/providers.md (user/author guide) · 中文: ../../zh/design/provider-authoring-evaluation.md

Method. One author (an AI coding agent) started from the repository docs and the reference plugin, built the plugin, and recorded every place the docs, the host adapters or the tooling forced a detour. Claims are tagged [ran] (reproduced by running something; the command is in §9), [read] (read in source or docs) or [est] (estimate). "Time" figures are wall-clock estimates of the session, not instrumented measurements.


0. Verdict

Adding a provider is feasible and mostly pleasant: the wire protocol is small, the manifest does a lot of the boring work (config schema, env aliases, secrets, consent, integrity), and the installer/receipt machinery behaved correctly in every flow except two bugs (§5, both fixed since, §10). A complete plugin (lifecycle, pause/resume, streaming exec, 9 file operations, process control, port URLs; 1.7 k lines of dependency-free Node plus 1.3 k lines of tests) went from zero to a green contract run in roughly an hour, and to full live coverage in a few more.

The friction is concentrated in three places, none of them the protocol itself:

  1. The host side of the abstraction is thinner than the spec. Sandbox.create hard-codes secure=True, has no per-create provider options, never calls ports.url, lazily starts commands.start(), and run_code has no fallback (§4).
  2. The author tooling is under-documented and partly broken. The documented contract-test command collects zero tests; the kit has five tests; there is no scaffold, no mock host and no "EPP client" for plugin authors, so every author re-builds a JSON-RPC test client (§3, §6).
  3. Cross-component drift. Parameter names and optional methods in the spec, the Python adapters, the reference plugin and the (future) CLI are not pinned by one machine-readable schema (§3.3).

1. What was built

Item Result
Runtime choice Dependency-free Node ESM (Node >= 20, built-in fetch). The mapping doc recommended stdlib Python but did not mandate it. Node was chosen because the abx CLI is a Node program (no extra runtime for CLI users), fetch + ReadableStream make SSE streaming simple, and the installer forbids dependencies anyway. Cost: Python-SDK-only users need node on PATH. A Python port would be a mechanical translation.
Capabilities sandbox.lifecycle (create/connect/get/list/kill), sandbox.timeout, sandbox.pause (pause/resume), exec.run, exec.stream, process.manage (list/kill/signal; no stdin), files.rw (read incl. ranges, write incl. mode, list, stat, mkdir, remove, move; extensions search/chmod), ports.url, sessions (detached commands). Declared unsupported (E8004): pty, code.interpret, templates.*, files.watch, files.transfer, process.stdin.
Configuration 13 manifest fields; canonical ABX_OPENSANDBOX_* plus native aliases (OPEN_SANDBOX_API_KEY, OPEN_SANDBOX_DOMAIN, ...). Secrets (api_key, registry_password) flagged secret.
Size provider.mjs + src/ 1 670 lines; tests 1 262 lines (24 unit, 21 mock-server, 2 live-EPP, 5 kit, 10 pytest SDK scenarios).
Quirks absorbed execd upload needs the metadata part as a file part; mode is octal digits written as a decimal integer; DELETE /files vs /directories are type-specific; a second kill returns 404 (swallowed to keep kill idempotent); TTL minimum 60 s (clamped); metadata must follow Kubernetes label rules (validated up front, -32006); renew-expiration takes an absolute time (derived from the server Date header, so client clock skew does not matter); legacy execd v1.1.0 has no argv and strips line endings (detected and handled).

2. Test results (local OpenSandbox server 1.1.1, Docker runtime)

Tier Command Result [ran]
Node unit + mock server node tests/run.mjs 45/45 pass
Node live (EPP/1 direct) OPENSANDBOX_LIVE=1 node tests/run.mjs live 2/2 pass (full lifecycle/exec/files/ports/pause/error mapping, wrong key)
Shared contract kit pytest ... --provider-under-test opensandbox (with the collection workaround, §6.1) 5/5 pass (now 18/18 with python -m agent_sandbox.providers test opensandbox, §10)
SDK scenarios (Sandbox.create(provider="opensandbox")) pytest tests/contract 10/10 pass (5 kit + 5 scenarios: streaming order and incrementality, exit codes, env/cwd, unicode files, E4001 for missing files, set_timeout, pause/resume, process list, E8004, unknown id)
Config precedence provider_e2e.py --stages env 6/6: canonical beats native for base URL and API key; native alone suffices; wrong key is E1001
Installer flows provider_install_e2e.py 36/36 checks pass, plus 2 non-fatal findings (§5) (now 39/39, no findings, §10)
Everything provider_e2e.py 12/12

The installer flows covered: local-path install; file:// bare-repo install pinned to a 40-hex commit (refused without --allow-insecure-transport; unpinned refused non-interactively); receipt tree_sha256 equal to an independent digest of the installed tree and of the source tree (git and local installs hash identically); tamper detection (doctor integrity check fails, recovers when restored; data/ excluded); update (pinned no-op, --check, refused without consent, applied with --yes, idempotent, rollback when the new version fails its handshake); uninstall keeps data/, --purge removes it; --link needs --yes.


3. Scorecard (1 = blocked, 3 = workable with detours, 5 = frictionless)

Dimension Score (original → after §10) Why (original assessment)
Protocol clarity (EPP/1 spec) 4 → 4.5 One table lists every method, params, result and feature; conventions (seconds on the wire, ordering of exec.output/exec.exit, idempotent retry set, 8 MiB line limit) are stated. Misses: optional/extension methods, param names for files.move, chunking (see §6).
Manifest and config schema 4.5 Declarative fields, secret, env aliases, choices, default, core.inherit, security block; the host resolves precedence. The one trap: defaults are delivered to the plugin, so a manifest default on base_url would silently beat the domain alternative. Not documented.
Error model 4 JSON-RPC codes map to stable SDK codes, retryable flag, sdk_code hint. Gaps: no code for "connection refused" (surfaces as generic E8000); the set of sdk_code values a plugin may name is documented only by example (E2001, E4001), the full list is the _SDK_CODE_CLASSES table in providers/epp.py.
Installer, store, trust 4 → 4.5 Correct and strict in all flows; consent and receipt design is excellent. Two bugs (§5) and missing CLI flags.
Contract tests / author tooling 2 → 4 Kit exists but collects nothing as documented, has 5 tests, no streaming/pause/timeout/process/file-edge coverage, no mock host, no scaffold or abx provider init.
Host adapters (completeness of the abstraction) 3 → 4 The exec adapters cover the core; but see §4: hard-coded secure, no per-create options, lazy start, no ports.url wiring, no code fallback.
Reference plugin 3.5 local-process is compact and readable; it is Python, covers no HTTP/streaming/auth, and shows none of the real-world problems (clock skew, retries, cancel).
Docs discoverability 3 → 3.5 Design doc is exhaustive but 700+ lines; the author path is spread over design + guide + code; the guide's Writing a provider section shows the manifest only; there is no walk-through of the first EPP exchange or of the methods a plugin must answer.
Debuggability 3.5 stderr is captured and shown on crash; doctor is great. No way to replay a request trace, no --trace-epp.
Offline testability 3 → 3.5 Authors must write their own mock server and EPP client (both done here; reusable).
Overall 3.5 → 4.0 / 5 See §10 for what moved each score.

3.1 Time to first working plugin [est]

Milestone Time Notes
Read design + mapping + reference plugin ~15 min The mapping doc did most of the thinking; without it this is the long pole (API reconnaissance of the target platform dominates, not EPP).
Skeleton: manifest + initialize + doctor green ~10 min local-process as a template is enough.
Lifecycle + exec + files, first green kit run ~35 min The kit failed once on a missing sandbox.connect (spec lists connect or get; the host calls connect).
Full capability set, error mapping, mock + live tests a few hours Almost all of it target-platform quirks (§1), not EPP.

For a platform with a clean REST API and no execd-style quirks, a 150-line plugin should be a ~30 minute job given a scaffold; today the missing scaffold and mock host add roughly 20 minutes.

3.2 What went well

3.3 Cross-component drift (the main long-term risk)

The same protocol surface is described in four places: the design table, the Python adapters, the reference plugin and (soon) the Node CLI. Examples seen: files.move params (path… in the table vs source/destination in the adapter; the plugin accepts both); sandbox.connect vs sandbox.get; string process_id for exec.start vs integer pid in ProcessInfo/kill; optional extensions (files.search, files.chmod, exec.start{detached}) that the plugin implements (they are needed by the mapping) but that neither the table nor any host adapter mentions, so nothing calls them yet. A single JSON Schema (or the existing conformance fixtures extended to per-method request/response schemas) would remove this class of bug.


4. Abstraction leaks (host-side behaviour an author cannot influence)

# Leak Effect Suggested fix
L1 Sandbox.create always sends secure: true A plugin cannot honour a user's secure=False; the plugin had to ignore the flag and add a secure_access config field (Kubernetes gateway only) Send secure only when the user set it; document its meaning for non-envd providers
L2 No per-create provider options Image pull policy, network policy, registry auth, platform, GPU must be global config fields Pass provider_options (already accepted by Sandbox.create) through to sandbox.create as options
L3 ports.url is never called network.get_url is computed client-side from the sandbox domain, so the plugin's ports.url is dead code in the SDK; works only when the CLI wires it Route network.get_url through ports.url when the feature is declared
L4 commands.start() is lazy for exec providers exec.start is only sent when the returned StreamReader is iterated; "start in background and poll" silently does nothing Eagerly send exec.start in the adapter and buffer into the queue
L5 ProcessInfo carries only pid, command and status Plugin-side data (user, start time, cpu) is dropped Pass optional fields through
L6 Sandbox.run_code raises E8004 for exec providers without code.interpret No portable "run Python in the sandbox" fallback even though exec.run could do it Optional shell-based fallback (python3 -c) in the adapter, behind a feature flag
L7 Capability resolution warns Could not resolve capabilities for template 'base' on every create Log noise on every non-default provider Skip resolution for non-built-in providers, or resolve to the provider's declared features
L8 process_id is a string on exec streams but pid: int in commands.kill(pid) Plugins that cannot expose numeric pids (execd command ids) cannot be killed via the SDK Make kill accept str | int and pass through
L9 No chunked files.read/write 8 MiB line limit caps a file at ~6 MiB; the plugin caps reads at 4 MiB Add offset/length to files.read (plugin already supports) and a chunked write (append) in the spec
L10 An unreachable server surfaces as generic E8000 Users see "provider failure" for a typo in base_url Add a connection-failure code (or sdk_code) that plugins can request

5. Defects found in the installer (not in the plugin)

# Defect Evidence Fix
B1 data/ is lost when an update is rolled back. _swap_in moves the old data/ into the staged tree before verify runs; on verify failure it deletes final (now holding data/) and restores the trash copy, which no longer has data/. provider_install_e2e.py prints [BUG ] data/ survives an update that was rolled back... Move data/ back into the trash copy before remove_tree(final) (or copy instead of move until verify passes).
B2 python -m agent_sandbox.providers update has no --allow-insecure-transport. Git sources installed with the flag cannot be updated from the command line (E8012); the receipt does not record that the flag was used. [GAP ] CLI 'update' cannot reach a file:// source Add the flag to update; consider recording insecure_transport: true in the receipt so update can reuse it after consent.
B3 The documented contract-test command collects 0 tests (§6.1). --pyargs agent_sandbox.providers.testing finds nothing because neither the module name (testing.py) nor the class name (ProviderContractTests) matches pytest's defaults See §6.1.

6. Documentation gaps

6.1 Contract kit instructions are wrong

guide/providers.md says pytest -p agent_sandbox.providers.testing --pyargs agent_sandbox.providers.testing --provider-under-test my-cloud. That collects 0 items (pytest exits 5, which a lenient CI step can ignore). The subclass form shown just above it in the guide works. The command line form needs:

pytest -p agent_sandbox.providers.testing -o python_classes=ProviderContractTests \
       -o python_files=testing.py --pyargs agent_sandbox.providers.testing \
       --provider-under-test my-cloud

The subclass form (class TestMine(ProviderContractTests): provider_id = "my-cloud") is what opensandbox/tests/contract/test_contract.py uses. Recommended: make the kit collectable by default (__test__ = True, rename the class TestProviderContract, or ship a pytest11 entry point with pytest_collect_file), and make "collected zero tests" an error in the docs' example (--strict-style check).

6.2 Missing or hard-to-find information

# Gap Where it should live
D1 Defaults declared in the manifest are sent in initialize.config; a default on a field with alternatives (base_url vs domain) blocks the alternative Manifest reference, config fields
D2 The complete list of accepted data.sdk_code values (E4001 for files, E2001 for templates, ...); today only the code lists them, and a bare -32002 for a missing file becomes "sandbox not found" (E8005) EPP error table
D3 The host calls sandbox.connect; get alone is not enough EPP method table (mark required vs optional)
D4 Which methods are required for each feature, and which optional extras a host may call (exec.start{detached}, files.search, files.chmod) Feature table with a "methods" column
D5 What exec.start does with a $/cancel for the request id of a running stream (the plugin must kill the process); the spec only says cancellation exists EPP section
D6 Update re-consent rules and that update of a pinned install is a no-op without --ref guide/providers.md update section (the rule is there, the no-op is not)
D7 data/ excluded from the digest, tests/ included (a plugin's tests bloat every install and change the digest) Authoring checklist
D8 install --link --yes as the development loop is documented under Install but not linked from Writing a provider Author guide cross-link
D9 The guide's Testing a plugin covers only the contract kit: no mock-host helper, no sample EPP client, no advice on offline tests New "Testing your plugin" section; this plugin's tests/epp-client.mjs and tests/mock/ are a starting point
D10 Canonical env names for a provider id with a dash/underscore, and the precedence between ABX_<ID>_*, native aliases, .env, TOML Environment variables guide (the table exists, the rule for aliases is implicit)
D11 Repository pytest runs from a directory containing an unrelated conftest.py fail at collection; authors need --noconftest/--confcutdir Contributing doc
D12 pip install -e client-sdk fails when the workspace uses a uv source for server-sdk Contributing doc / packaging

6.3 Doc fixes applied vs deferred

Applied (docs I own): the opensandbox plugin README now lives in awesome-providers/opensandbox (README.md / README.zh-CN.md: install, config, capability matrix, test tiers, known limits), e2e/opensandbox/README.md (how to run the provider E2E; the harness installs the plugin and does not vendor it), examples/providers/README.md (points at the catalog). Deferred (files owned by other workstreams): everything in §6.1, D1-D12, and the corrections in §7.


7. Concrete fixes, in priority order

Priority Fix Touches Size
P0 B1 data loss on failed update installer._swap_in ~5 lines + test
P0 Correct the contract-test command and make the kit collectable (§6.1) providers/testing.py, guide/providers.md (en/zh) small
P1 provider update --allow-insecure-transport (B2) and abx provider update parity providers/__main__.py, CLI small
P1 L1/L2: honour secure, pass provider_options into sandbox.create exec_provider.ExecSandboxProtocol.create, spec table small
P1 L4: eager exec.start in ExecProcessProtocol.start exec_provider.py small
P1 L3: route network.get_url through ports.url Sandbox/network API medium
P2 Machine-readable method schemas (one JSON Schema per method, shared by SDK, CLI, kit) plus kit tests generated from them fixtures/, testing.py medium
P2 Extend the kit: streaming order and exactly one exit, cancel, set_timeout, pause/resume, process list/kill, file edge cases (binary, missing -> E4001, overwrite), idempotent kill, -32006 on bad params providers/testing.py medium
P2 Author scaffold: python -m agent_sandbox.providers init <id> --runtime node|python generating manifest, entry, EPP test client new module medium
P2 Mark required vs optional methods and extension methods in the EPP table (D3, D4) providers-and-credentials.md small
P3 L5-L10 exec_provider.py, models small each
P3 --trace-epp / ABX_EPP_TRACE=1 to log requests/responses (redacted) providers/epp.py small
P3 Exclude tests/ from the digest or document it (D7) store._walk_regular, docs small

8. Observations on the EPP/1 design itself


9. Reproducing this evaluation

cd e2e/opensandbox && ./local-up.sh up && eval "$(./local-up.sh env)"
python3 provider_e2e.py                 # clones awesome-providers unless ABX_OPENSANDBOX_PLUGIN is set
python3 provider_install_e2e.py
git clone --depth 1 https://github.com/alibaba-agent-sandbox/awesome-providers.git
cd awesome-providers/opensandbox && node tests/run.mjs

See e2e/opensandbox/README.md for the environment variables each script reads (no secrets are written anywhere; the local API key is a throwaway development value).


10. Resolution status

Everything below was fixed in the SDK and re-verified against the local OpenSandbox server (provider_install_e2e.py: 39/39 checks and no [GAP]/[BUG] line; provider_e2e.py: 12/12 including 18 contract tests and 23 SDK scenarios; Node plugin suite: 46/46).

Item Status What changed
B1 data/ lost on rollback Fixed installer._swap_in moves data/ back before restoring the previous tree, on both failure paths (failed verify, failed swap). Regression tests inject each failure and compare file contents and modes.
B2 update lacks --allow-insecure-transport Fixed python -m agent_sandbox.providers update takes --allow-insecure-transport and --no-verify. The receipt deliberately does not remember the flag (see the guide).
B3 / §6.1 contract command collects 0 tests Fixed python -m agent_sandbox.providers test <id> (fails on 0 collected tests, isolates from the current directory's pytest config) and pytest -p agent_sandbox.providers.testing --pyargs agent_sandbox.providers.contract --provider-under-test <id>. Guide, design doc and zh copies corrected.
Kit has 5 tests (§3) Fixed 18 tests: exec stream order and single exit, exit code/stderr/env, timeout enforcement, binary and unicode files, directories and move, typed not-found, kill idempotence, set_timeout, pause/resume, process list, ports.url, E8004 for undeclared features, secret redaction; --provider-option KEY=VALUE.
L1 hard-coded secure=True Fixed Sandbox.create(secure=None): sent to a plugin only when the caller set it; the built-in provider still behaves as True.
L2 no per-create options Fixed provider_options reach sandbox.create as options (secret fields removed). The OpenSandbox plugin accepts a whitelist per create (network_policy, platform, default_image, ...) and never lets options change the server address or key.
L3 ports.url never called Fixed await sandbox.network.resolve(port) / resolve_url(port) ask the plugin when ports.url is declared. get_url() stays synchronous and local (no API break).
L4 lazy commands.start() Documented, not changed Not a provider-layer bug: the built-in envd stream is lazy in exactly the same way, so an "eager exec adapter" would make providers differ. The guide says so and shows the nohup ... & / sessions alternatives.
L8 process_id string vs pid int Fixed pid is int \| str end to end (ProcessInfo.pid, commands.kill/send_signal/send_input, the exec adapter); the table says string \| integer.
§3.3 drift: files.move names, connect vs get Fixed One machine-readable table, epp-methods.json (mirrored byte-identically to the CLI repo, identity-tested). The adapters are tested against it in both directions. files.move is source/destination; sandbox.get is required and sandbox.connect optional with an automatic fallback to get on -32601.
Silent hang on a plugin that never ends a command Fixed (found while extending the kit) The streaming adapter raised a bare asyncio.TimeoutError; it now raises CommandTimeoutError (E3001).
D1, D2, D3, D4, D5, D6, D7, D8, D9, D11 Fixed (docs) Guide Writing a provider / Testing a plugin and the design doc's method table and conventions (en and zh). D7 is documented rather than changed (a plugin's tests/ stays in the digest). D11 is covered by a note in the testing section.
D10, D12 Open Environment-variable rule for ids with dashes is in the configuration reference already; uv source packaging is a contributing-doc item outside the provider docs.
L5 extra ProcessInfo fields, L6 run_code fallback, L7 capability-resolution log noise, L9 chunked files, L10 connection-refused code Open Recorded for a later pass; the optional extension params (files.read{offset,length}, files.write{mode,append}) are in the method table so plugins can implement them ahead of the host.
Scaffold (provider init), --trace-epp, per-method result schemas Open Parameters are schema-checked; results are only described.

Scorecard movement. Contract tests 2 → 4 (collectable, 18 tests, one command); host adapters 3 → 4 (L1-L4, L8); installer 4 → 4.5 (B1, B2); protocol clarity 4 → 4.5 (one table with required/optional/extension and exact names); docs discoverability and offline testability +0.5 each. Overall 3.5 → 4.0.

11. Addendum 2026-10-13: review of the abstraction against a live server

Verdict: keep the abstraction; harden the edges. EPP/1 exec plugins, abx-provider.toml, the installer and the contract kit were enough to run OpenSandbox for real (Docker, server 1.1.1) from both hosts: install pinned from git, doctor, 18/18 contract cases, create, exec, binary files, timeout, pause/resume, connect, kill, tamper detection. Evidence: 2026-10-13-opensandbox-provider-e2e. No new protocol method was needed.

What the review found and changed (TS and Python together):

Finding Change
A missing sandbox was E8005 for plugins and E2009 for the built-in provider sandbox.get/connect/set_timeout/pause/resume map a plugin's "not found" to SandboxNotFoundError (E2009); a plugin may also name sdk_code: "E2009". The contract kit now requires E2009 for an unknown id
rest presets ignored [features] supported (only exec plugins enforced it) feature_gate wraps the four protocol classes; an undeclared feature is E8004 before any HTTP request
exec records inherited the built-in region = "cn-hangzhou" and timeout = 300 record_to_info leaves the region empty and derives the timeout from created_at/expires_at when the plugin reports both
--yes accepted a moving git ref in some paths --yes never accepts an unpinned ref; interactive review or --allow-unpinned does. Same rule for update. The refusal prints the commit to pin
A plugin root without abx-provider.toml gave a bare "file not found" the error says the file is missing at the plugin root and how to point at the subdirectory
provider list did not show what a provider can do a Features column (sandbox(lifecycle,pause) exec(run,stream) ...), also in --json and in python -m agent_sandbox.providers list
Duplicated consent logic in the exec and rest providers; an unused listV2 in the TS exec adapter consented_inherit helper; the adapter method was removed

Redundancy check. exec_provider (EPP adapters) and rest_provider (E2B preset) share no code beyond the helpers above and are not redundant: they are the two ways a backend is reached (own wire protocol vs E2B wire protocol). epp.ts / epp.py is only the JSON-RPC client and error mapping. Provider.capabilities() and FEATURE_TO_CAPABILITY are unused; capability resolution still comes from the template (open).

Trust model check. The installer already fetched without a checkout, allowed only regular files, recorded a tree digest, verified it on every start and rolled back a failed update. Confirmation and pinning were present but had a hole (--yes plus a branch); that is closed. provider install from a git URL is therefore not experimental.

Open: template commands ignore --provider; the capability set is not derived from provider features; no pause/resume CLI command; SandboxInfo still has built-in defaults for region and timeout; the awesome-providers catalog needs the abx rename (cli/e2e/providers/awesome-providers-abx-rename.patch).