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.
provideris a command group,--provider/--opt/--all-providersand qualified ids work,Sandbox.create/connect/list/killByIdselect the provider,SandboxNotFoundError(E2009) exists,network.resolveroutes throughports.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 byproviders-and-credentials-cli.md; this file only says where each piece plugs in.
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
- Version bump:
package.jsonandsrc/**/_versionto0.3.0in lockstep withpython-sdk(_version.py). The reference plugins declarecompat.requires = { cli = ">=0.3" }; tests currently passhostVersion: "0.3.0"explicitly. After the bump, drop that override intests/_support/plugins.ts(TEST_HOST_VERSION). package.json:filesalready shipsdist; no change needed becausesrc/providers/**compiles into it. Add"./providers"toexportsonly if a library entry is wanted (optional). The manifest parser uses the in-reposrc/utils/toml_plain.ts; no new dependency was added.models/errors.ts: addSandboxNotFoundError(codeE2009, mirror of Pythonmodels/errors.py).providers/errors.ts#exitCodeFormust map it to exit code 4 (the Python mapping already does).protocol/sandbox.ts: raiseSandboxNotFoundErroron HTTP 404 forget,connect,set_timeout,pause,resume(andkillreturnsfalse, unchanged), as Python does.models/sandbox.ts:SandboxConfiggainssecure?: booleanandprovider_options?: Record<string, unknown>. The built-in adapter currently usesObject.assignto carry them; replace with real fields.ProcessInfo.pidbecomesnumber | string(exec providers use stringprocess_id; the adapter currently returns a plain object for string ids).api/network.ts: addresolve(port)/resolve_url(port)that go throughports.urlfor non-envd providers (Pythonapi/network.pygained +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:
BuildInfraOptionsgainsprovider?: string | nullandprovider_options?: Record<string, unknown> | null._buildInfrabecomes:const p = getProvider(resolveProviderId(opts.provider)); ifp.id === BUILTIN_PROVIDER_IDrun the existing code path verbatim (keeps every mock/spy-based test byte-identical); otherwiseconst infra = await p.buildInfra(loadLayeredConfig(overrides))and mapProviderInfrato the same tuple shape._buildInfratherefore becomesasynconly on the plugin branch; keep a sync fast path for built-in (or make the tuple return a promise andawaitit at the 4 call sites).Sandbox.create(template, { provider, provider_options, secure, ... }): passprovider_optionsandsecureintoSandboxConfig; callp.require("sandbox.create")-style feature gates through the adapters (already done insideExecProvider).Sandbox.connect(ref)/killById(ref):const [pid, sid] = parseSandboxRef(ref); a prefix selects the provider (E8030ProviderMismatchErrorwhen it conflicts with an explicit--provider). Un-prefixed ids use the ambient provider.Sandbox.list({ provider })unchanged for one provider.listAllProviders()(new static) iterateslistProviders(), calls each provider's list, tolerates per-provider failure (returns{ results, errors }), and tags rows withqualify(providerId, id).- Instances produced by an exec provider carry
envd_urlwhen the plugin returns one: keep the existing envd transport for exec/files/PTY (hybrid mode, implemented inExecProvider). - Un-skip the two
it.todotests intests/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
- Register the command group: in
GROUP_COMMANDS(src/cli/command_surface.ts) addprovider: "commands/provider:provider";abx --helplists it under its own Providers heading. - Root group options (root-level and leaf-level, leaf wins; spec §4):
--provider ID(envvar: "ABX_PROVIDER"), validated with the id grammar; stored on the click context asctx.obj.provider.--opt KEY=VALUE(repeatable). Parse withparseProviderOptionsfromsrc/providers/testing.ts(move it toproviders/options.tsif you prefer; it is re-exported from the barrel). Refuse secret fields (ConfigField.secret) with the spec's exit-2 text.abx --helpepilog: add theRelated commandsline (spec §0) — regenerate the root help golden.- 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 includesSandboxNotFoundError), timeouts 5, quota 6, config/trust/mismatch 2, otherwise 1. ABX_WARN_DEPRECATED=1: nothing to do inmain.ts;warnLegacyOncereads 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:
- Key resolution (
get/set/delete KEY): the layer-firstresolveKeylooks up the one environment variable for the key. Qualifiedproviders.<id>.<key>and--providerselect the provider. An unknown key exits 2 and lists the keys that exist (spec §3.2). - Reading values:
loadLayeredConfig()/explainConfig()forget/list(source labels). Output channel/format unchanged. - Writing:
config setwrites the one environment variable for the key (secrets in~/.abx/.env, other keys under their config key inconfig.toml).deleteremoves that stored value. get/set/delete/listaccept--providerand--all.config listshowssandbox_base_url,sandbox_domain, and the other keys from the registry.explainConfig()reports the one variable that decided each key. Secrets are masked.config listshows the one name for each key and masks secrets. There is no second name to audit or rewrite.- 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):
create(≈export const create): add--provider, repeatable--opt, and--secure/--no-secure; passprovider,provider_options,securetoSandbox.create. Provider-scoped flags (--region,--access-key-*…) are valid only for the built-in provider; for others raise exit 2 with the spec text. Printqualify(providerId, id)and add"provider"to--jsononly when the provider is not the built-in (byte-identical otherwise).list(≈list_cmd): add--providerand--all-providers(JSON: addproviderkey per row, only for non-builtin or in all-providers mode; per-provider failures go to stderr as warnings, exit 0 unless all fail).kill,connect, and every command taking a sandbox id (files, process, system —sandbox_files.ts,sandbox_process.ts,sandbox_system.ts): accept qualifiedprovider:idviaparseSandboxRef; callSandbox.connect(ref, { provider }).loadConfig({ region })call sites (create/kill): keep for the built-in provider; non-builtin goes throughloadLayeredConfig.info: add aProviderrow only for non-builtin providers.- The root
--provider/--optvalues are read fromctx.obj; leaf values override.
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
- The server env (
_spawnBackground, around theenvbuild formcp start) must forwardABX_PROVIDERand anyABX_<P>_*provider-scoped variables only when explicitly present in the process env; it must never forward LLM/GitHub keys to providers (the registry already enforces this at provider construction — nothing extra to do). loadConfig().api_keyat the auth-token helper (≈ line 162) stays: it is the built-in key lookup used for MCP auth.- MCP tools that create/list/kill sandboxes: add optional
providerargument (string) tosandbox_create,sandbox_list,sandbox_kill,sandbox_connecttool schemas; accept qualified ids. JSON results addprovideronly for non-builtin. mcp installcopy rules (spec §4.3): the generated client config keeps provider selection throughABX_PROVIDERonly, never secrets.
8. Documentation and goldens
- Regenerate goldens:
config list(text/JSON),config --help,abx --help; add goldens forprovidercommands. - Update
docs/en/reference,docs/en/guide/{environment-variables,authentication,troubleshooting}.md, adddocs/{en,zh}/guide/providers.md; keep zh mirrors in lockstep;SKILL.md,llms.txt,README*.md. - Docs must not contain absolute local paths (
scripts/check-no-local-paths.sh).
9. Verification checklist after wiring
npx tsc -p tsconfig.json --noEmitandnpm run lint.npx vitest run tests/test_providers tests/test_transport/test_config_names.test.ts tests/test_utils(currently 338 passed, 2 todo in providers).- Differential cases under
parity/diff/with the built-in provider only: every case byte-identical except theconfig listones. abx provider test <id>againstlocal-process,e2b-compatible, and (if a local OpenSandbox server is running)opensandbox; the last is skipped politely otherwise.- 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):
templatecommands ignore--provider.abx --provider opensandbox template deploy ...runs the Alibaba Cloud ACR flow (ACR credentials missing). They should checktemplates.read/templates.buildand answerE8004like every other feature-gated command.- The capability set is not derived from provider features.
abx sandbox capabilitiesandsandbox.capabilitiesreportcode, files, shellfor OpenSandbox, which declares nocode.interpret;Provider.capabilities()andFEATURE_TO_CAPABILITYexist but nothing calls them. Image-based providers also logCould not resolve capabilities for template '<image>'on every create. abx sandbox command runon a plugin without an HTTP data plane fails withthis._http_client._createEnvdClient is not a function. The Python SDK has_require_data_plane("named commands")and answersE8004; port that guard to_callServerCommandinsrc/api/sandbox.ts.SandboxInfodefaults (region = "cn-hangzhou",timeout = 300) belong to the built-in platform.recordToInfonow overrides them for plugins, so a plugin that reports no region and no lifetime shows an empty Region row and the requested timeout; the model should make both optional.- No CLI for pause/resume:
Sandbox.pause()/resume()work (verified from Python), but there is noabx sandbox pausecommand in this tree. - The catalog still ships
ebx-provider.toml. Applye2e/providers/awesome-providers-abx-rename.patchinalibaba-agent-sandbox/awesome-providers; until thenabx provider installof the catalog fails withE8014(noabx-provider.tomlat the plugin root).
Older gaps:
FilesystemProtocol.watchDiris native-only; exec providers returnE8004forfiles.watchunless hybrid.models/errorsprovider classes live inproviders/errors.ts; move or re-export if the framework wants one error module.- Legacy
transport/config.tsis bridged byLayeredConfig.toTransportConfig(); it can be removed onceconfig_cmd.tsandmcp.tsstop importingloadConfig. - No entry-point discovery of providers in Node (installed plugins and
registerProvider()only).