Agent Sandbox Registry

agent-sandbox

Provider wiring plan (follow-up edits to existing CLI modules)

无中文版

docs/design/provider-wiring-plan.md

Provider wiring plan (follow-up edits to existing CLI modules)

Status (2026-10-13): wiring is done for the sandbox path; this file is now the record of the plan plus the list of what is still open. provider is a command group, --provider / --opt / --all-providers and qualified ids work, Sandbox.create/connect/list/killById select the provider, SandboxNotFoundError (E2009) exists, network.resolve routes through ports.url, and an OpenSandbox server was driven end to end from the CLI (evidence). Sections 1 to 8 below are kept as the original plan; ignore any item that the code already does. The open items are in the section "10. Known gaps". Earlier status: handoff note, no existing command module edited. Grammar, help text and JSON shapes are owned by providers-and-credentials-cli.md; this file only says where each piece plugs in.

中文版: ../zh/design/provider-wiring-plan.md

0. What already exists (do not rewrite)

Area Modules Entry points to call
Key registry src/utils/config_keys.ts (generated by scripts/gen-config-keys.mjs from tests/fixtures/config-keys.json), src/utils/config_resolver.ts findKeyByAlias, envNames, aliasNames, providerEnvName; resolveKey, resolveProviderId (layer-first)
Layered config src/transport/config_layers.ts loadLayeredConfig, LayeredConfig.toTransportConfig(), explainConfig, auditConfig, planMigration, applyMigration, warnLegacyOnce
Provider core src/providers/ (index.ts barrel) getProvider, listProviders, resolveProviderId, ambientProviderId, parseSandboxRef, qualify, install, update, uninstall, doctor, defineProviderContractTests, runContractSuite, exitCodeFor
Built-in provider src/providers/builtin.ts AlicloudExpressProvider wraps today's HttpClient + SandboxProtocol/ProcessProtocol/FilesystemProtocol/CodeInterpreterProtocol unchanged
Plugins exec_provider.ts (EPP/1), rest_provider.ts selected by manifest kind

Everything below is wiring. Names are the Python names, snake_case on the wire.

1. Preconditions

  1. Version bump: package.json and src/**/_version to 0.3.0 in lockstep with python-sdk (_version.py). The reference plugins declare compat.requires = { cli = ">=0.3" }; tests currently pass hostVersion: "0.3.0" explicitly. After the bump, drop that override in tests/_support/plugins.ts (TEST_HOST_VERSION).
  2. package.json: files already ships dist; no change needed because src/providers/** compiles into it. Add "./providers" to exports only if a library entry is wanted (optional). The manifest parser uses the in-repo src/utils/toml_plain.ts; no new dependency was added.
  3. models/errors.ts: add SandboxNotFoundError (code E2009, mirror of Python models/errors.py). providers/errors.ts#exitCodeFor must map it to exit code 4 (the Python mapping already does).
  4. protocol/sandbox.ts: raise SandboxNotFoundError on HTTP 404 for get, connect, set_timeout, pause, resume (and kill returns false, unchanged), as Python does.
  5. models/sandbox.ts: SandboxConfig gains secure?: boolean and provider_options?: Record<string, unknown>. The built-in adapter currently uses Object.assign to carry them; replace with real fields. ProcessInfo.pid becomes number | string (exec providers use string process_id; the adapter currently returns a plain object for string ids).
  6. api/network.ts: add resolve(port) / resolve_url(port) that go through ports.url for non-envd providers (Python api/network.py gained +22 lines). Built-in behaviour unchanged.

2. src/api/sandbox.ts (the API layer)

Current seam: _buildInfra(opts) at the top builds loadConfig → createAuthProvider → HttpClient → SandboxProtocol, and Sandbox.create/connect/list/killById call it (lines around create, connect, list, killById).

Edits:

  1. BuildInfraOptions gains provider?: string | null and provider_options?: Record<string, unknown> | null.
  2. _buildInfra becomes: const p = getProvider(resolveProviderId(opts.provider)); if p.id === BUILTIN_PROVIDER_ID run the existing code path verbatim (keeps every mock/spy-based test byte-identical); otherwise const infra = await p.buildInfra(loadLayeredConfig(overrides)) and map ProviderInfra to the same tuple shape. _buildInfra therefore becomes async only on the plugin branch; keep a sync fast path for built-in (or make the tuple return a promise and await it at the 4 call sites).
  3. Sandbox.create(template, { provider, provider_options, secure, ... }): pass provider_options and secure into SandboxConfig; call p.require("sandbox.create")-style feature gates through the adapters (already done inside ExecProvider).
  4. Sandbox.connect(ref) / killById(ref): const [pid, sid] = parseSandboxRef(ref); a prefix selects the provider (E8030 ProviderMismatchError when it conflicts with an explicit --provider). Un-prefixed ids use the ambient provider.
  5. Sandbox.list({ provider }) unchanged for one provider. listAllProviders() (new static) iterates listProviders(), calls each provider's list, tolerates per-provider failure (returns { results, errors }), and tags rows with qualify(providerId, id).
  6. Instances produced by an exec provider carry envd_url when the plugin returns one: keep the existing envd transport for exec/files/PTY (hybrid mode, implemented in ExecProvider).
  7. Un-skip the two it.todo tests in tests/test_providers/test_sandbox_provider.test.ts (Sandbox.connect('provider:id') mismatch → E8030, and the built-in default path).

api/commands.ts: no routing change; Commands.run/start/... already take the ProcessProtocol from the sandbox. Only needed: accept string pids in kill/connect/send_stdin signatures (follows the ProcessInfo.pid widening).

3. src/cli/main.ts and command_surface.ts

  1. Register the command group: in GROUP_COMMANDS (src/cli/command_surface.ts) add provider: "commands/provider:provider"; abx --help lists it under its own Providers heading.
  2. Root group options (root-level and leaf-level, leaf wins; spec §4):
  3. --provider ID (envvar: "ABX_PROVIDER"), validated with the id grammar; stored on the click context as ctx.obj.provider.
  4. --opt KEY=VALUE (repeatable). Parse with parseProviderOptions from src/providers/testing.ts (move it to providers/options.ts if you prefer; it is re-exported from the barrel). Refuse secret fields (ConfigField.secret) with the spec's exit-2 text.
  5. abx --help epilog: add the Related commands line (spec §0) — regenerate the root help golden.
  6. Top-level error handler: replace the existing status-code switch with exitCodeFor(err) only for provider-typed errors; keep the current mapping for the rest (so goldens stay unchanged): auth 3, not-found 4 (now includes SandboxNotFoundError), timeouts 5, quota 6, config/trust/mismatch 2, otherwise 1.
  7. ABX_WARN_DEPRECATED=1: nothing to do in main.ts; warnLegacyOnce reads the env at call time. Make sure _resetDeprecationWarnings() is called from the test setup used by the CLI test suites.

4. New file src/cli/commands/provider.ts

One group provider and subcommands, each a thin wrapper over the library (names per spec §5; all honour --json/--quiet/--ci/--no-color):

Command Calls Notes
list listProviders() table columns per spec §5.1; marks default
info ID getProvider(id) + manifest/receipt shows features, fields (secrets masked), trust level, digest
install SOURCE [--ref --sha256 --subdir --link --yes --allow-insecure-transport --force] install(source, { confirm, verify: verifyHandshake, … }) confirm prints InstallPlan consent screen; non-TTY without --yes → exit 2
uninstall ID [--purge] uninstall data/ kept unless --purge; if default → reset with message
update [ID \| --all] [--check] [--yes] [--allow-insecure-transport] update --check is read-only; re-consent on risk aspects (diffAspects)
default [ID] write [core] provider in config.toml (use toml_write.ts) show effective value and source; note when ABX_PROVIDER overrides
config ID [KEY [VALUE]] provider schema via providerSchema(id) storage rules per spec §3.3
doctor [ID] doctor(id, { handshake: true }) renders checks installed/integrity/config/handshake; exit 1 on any failure
test ID [--opt …] [-k FILTER] runContractSuite(id, { options, filter }) + summarizeContract prints per-case ok/skip/fail; exit 1 on failure; SkipContractTest → skipped

Errors raised by the library are ProviderError subclasses carrying code (E80xx) and suggestion; render them with the existing error formatter and exit through exitCodeFor.

5. src/cli/commands/config_cmd.ts

config_cmd.ts resolves each key by its one name. Replace the old multi-name tables with that rule, in this order:

  1. Key resolution (get/set/delete KEY): the layer-first resolveKey looks up the one environment variable for the key. Qualified providers.<id>.<key> and --provider select the provider. An unknown key exits 2 and lists the keys that exist (spec §3.2).
  2. Reading values: loadLayeredConfig()/explainConfig() for get/list (source labels). Output channel/format unchanged.
  3. Writing: config set writes the one environment variable for the key (secrets in ~/.abx/.env, other keys under their config key in config.toml). delete removes that stored value.
  4. get/set/delete/list accept --provider and --all. config list shows sandbox_base_url, sandbox_domain, and the other keys from the registry.
  5. explainConfig() reports the one variable that decided each key. Secrets are masked.
  6. config list shows the one name for each key and masks secrets. There is no second name to audit or rewrite.
  7. Provider-scoped keys (acr_*, team_id, region, plugin fields) — config set <provider>:<key> or with --provider; secrets go to ~/.abx/.env (0600).

Gap to close first: config-keys.json has no description_zh/required; if config list needs zh descriptions, regenerate the fixture from the Python repo (the drift test will force the update).

6. src/cli/commands/sandbox.ts

Edit sites (line numbers drift; use the command names):

sandbox.yaml (provider: key): in the sandbox spec loader (declarative create path, src/cli/commands/sandbox.ts and src/api/_yaml.ts consumers) add optional provider: <id> and provider_options: {…}; precedence: CLI flag > sandbox.yaml > ambient. Unknown/secret option names are rejected with the same exit 2 text as --opt.

7. src/cli/commands/mcp.ts

8. Documentation and goldens

9. Verification checklist after wiring

  1. npx tsc -p tsconfig.json --noEmit and npm run lint.
  2. npx vitest run tests/test_providers tests/test_transport/test_config_names.test.ts tests/test_utils (currently 338 passed, 2 todo in providers).
  3. Differential cases under parity/diff/ with the built-in provider only: every case byte-identical except the config list ones.
  4. abx provider test <id> against local-process, e2b-compatible, and (if a local OpenSandbox server is running) opensandbox; the last is skipped politely otherwise.
  5. Cross-implementation: install a plugin with the Node installer, load it with the Python SDK (digests equal), and vice versa.

10. Known gaps (carry into the review)

Open after the 2026-10-13 OpenSandbox run (each has a verified symptom):

Older gaps: