Provider plugins
July 14, 2026 · View on GitHub
AgentBox ships five built-in providers (docker, daytona, hetzner, vercel, e2b),
but the provider surface is open: anyone can publish a provider plugin as its
own npm package and users can add it with agentbox plugin add — no changes to
AgentBox itself. This doc is the authoring + operating guide. The user-facing
version lives on the docs site at /docs/build-a-provider.
Two reference packages live under examples/:
agentbox-provider-sample— a stub backend that throws onprovision. The smallest thing that plugs in; read it first to see the contract.agentbox-provider-example— a real, working provider (Vercel-backed): a faithful copy of the built-in Vercel provider, repackaged as a plugin built only on@madarco/agentbox-provider-sdk. It exercises the whole surface —prepare(base-snapshot bake),buildAttach(no-SSH PTY bridge), an id-addressedcheckpointoverride, the box-runtime asset split, and prepared-state. Copy this when building a real cloud provider.
How it works
@madarco/agentbox-provider-sdkis the single public dependency a plugin needs. It re-exports the whole provider-facing surface (Provider,CloudBackend,ProviderModule,createCloudProvider, doctor + prepared-state helpers,resolveSharedRuntimeAsset, …) with the private internal packages inlined, so a plugin never touches AgentBox internals. It carries aSDK_API_VERSIONthat gates compatibility.- The published
@madarco/agentboxCLI bundles the five built-ins. A plugin is not bundled — the user installs it, thenagentbox plugin add <pkg>records it in~/.agentbox/plugins.json, and the CLI + host relay load it at runtime via a plainimport()of the recorded entry (the extension seam). - A plugin needs zero edits to AgentBox:
ProviderNameis an open string, the provider registry/doctor/config all consult the runtime set, and per-provider config falls back to the generic keys (a plugin manages its own base image via its prepared-state).
What a provider plugin must ship
A package named agentbox-provider-<name> (or @scope/agentbox-provider-<name>)
that:
- Depends on
@madarco/agentbox-provider-sdk(^1). - Declares its contract version in
package.json:{ "agentbox": { "providerApiVersion": 1 } } - Exports a
providerModule(orproviderModulesfor a multi-provider package) — the uniform surface AgentBox loads it through:
Onlyimport { createCloudProvider, type CloudBackend, type ProviderModule } from '@madarco/agentbox-provider-sdk'; const backend: CloudBackend = { name: 'myprovider', async provision(req) { /* create the VM/sandbox, return { sandboxId } */ }, async get(id) { /* … */ }, async start(h) {}, async stop(h) {}, async pause(h) {}, async resume(h) {}, async destroy(h) {}, async state(h) { return 'running'; }, async exec(h, cmd, opts) { /* … */ }, async uploadFile(h, local, remote) {}, async downloadFile(h, remote, local) {}, async listFiles(h, dir) { return []; }, async previewUrl(h, port) { return { url: `https://…:${port}` }; }, // optional: createSnapshot/deleteSnapshot (checkpoints), list (prune), // refreshPreviewUrl, signedPreviewUrl, attachArgv, renewTimeout, … }; const provider = createCloudProvider(backend, { defaultResources: { cpu: 2, memory: 4, disk: 40 } }); export const providerModule: ProviderModule = { provider, backend, ensureCredentials: async () => { /* first-run login; persist a token */ }, readCredStatus: () => ({ configured: true }), currentBaseFingerprintLive: async () => undefined, doctorChecks: async () => [{ label: 'credentials', status: 'ok', detail: 'configured' }], };provideranddoctorChecksare required.createCloudProvidersupplies the entire lifecycle (workspace seeding, ctl launch, relay wiring, preview URLs, checkpoints, cp) on top of the thin ~13-methodCloudBackend— "a cloud is one file."
Box-side runtime (VPS-style providers)
A provider that bakes a base image by installing files onto a throwaway host (like
hetzner/digitalocean) needs the provider-neutral box runtime — ctl.cjs and the
shims. Do not vendor your own; pull them from the running CLI so they stay
version-locked to it:
import { resolveSharedRuntimeAsset } from '@madarco/agentbox-provider-sdk';
const ctl = resolveSharedRuntimeAsset('ctl.cjs'); // absolute host path; scp it to the box
Ship only your provider-specific pieces (an install-box.sh, a
custom-system-CLAUDE.md). Providers that build from a Dockerfile don't need any
of this. The agentbox-provider-example package is the worked example: its
runtime-assets.ts resolves the shared runtime via resolveSharedRuntimeAsset
and vendors only scripts/provision.sh + scripts/custom-system-CLAUDE.md.
Prepare, attach, and checkpoints (cloud providers)
A cloud provider that bakes its base image (no Dockerfile) typically overrides
three optional capabilities on top of createCloudProvider. The SDK re-exports
the helpers each needs, so it's all buildable on @madarco/agentbox-provider-sdk alone:
prepare— boot a builder sandbox, run your installer, snapshot it. Bake the host's static agent config into the snapshot withstageClaudeStaticForUpload/stageCodexStaticForUpload/stageOpencodeStaticForUpload. Persist the result in your own~/.agentbox/<name>-prepared.jsonviaread/writePreparedStateRaw+preparedStatePathFor(these accept a plugin's open-string provider name).buildAttach— for a provider with no SSH, render the shared inner tmux command withrenderInnerCommand+hostTermForCloudand return your own transport's argv.checkpoint— if your cloud's snapshots are id-addressed (an opaque id you can't name, like Vercel/E2B), the scaffold's default (which drivesbackend.createSnapshot(handle, name)) doesn't fit. Override the whole capability and store the snapshot id in the manifest withwriteCloudCheckpointManifest/listCloudCheckpoints/resolveCloudCheckpoint/removeCloudCheckpointDir/currentCloudBaseFingerprint. If your snapshots are name-addressed, just implementbackend.createSnapshot/deleteSnapshotand skip the override.
Credentials & config
- Persist your API token however you like; the convention is a 0600
~/.agentbox/secrets.enventry read on demand (see the built-in providers'env-loader.ts/credentials.ts). agentbox create --provider <name> [--image/--size/--checkpoint …]— the CLI flags override per-create. For a persistent base image, write it into your own~/.agentbox/<name>-prepared.jsonfrom yourprovider.prepare()and read it back in the backend when it sees the image sentinel (agentbox/box:dev). AgentBox does not pin a plugin's image into its own config.
Certify the backend
Copy the cloud-backend contract suite
(packages/sandbox-cloud/test/mock-backend-contract.test.ts +
makeMockCloudBackend), swap in your backend, and ensure every test passes.
Operating a plugin (users)
npm i -g agentbox-provider-myprovider # or install anywhere resolvable
agentbox plugin add agentbox-provider-myprovider # validates + records it (a path also works)
agentbox plugin list
agentbox doctor # shows your provider's group
agentbox create --provider myprovider # first create triggers ensureCredentials
agentbox plugin remove myprovider # unregister (does not uninstall the npm package)
Trust
A provider plugin runs in-process with full host + credential access — it is
trusted code, exactly like the CLI. agentbox plugin add is the consent boundary:
it names the package + version and warns before recording. Only add plugins you
trust. AgentBox does not sandbox plugin code (a provider legitimately needs to
provision infrastructure and handle secrets).
Compatibility
The CLI loads a plugin only if its providerApiVersion is in the CLI's supported
set (SUPPORTED_SDK_API_VERSIONS). An incompatible plugin is refused at
plugin add and skipped (with a warning) at load — it never crashes the CLI.
A plugin pinned to an older SDK tolerates config keys added after it shipped.
The SDK inlines @agentbox/config, so your plugin carries a snapshot of the key
registry taken when you built it — and users run it against a CLI that keeps
adding keys. An unknown key is therefore skipped with a warning, never an
error: the config layer it appears in still loads, and create proceeds. (The
host CLI is the one that tells the user; a plugin's copy stays silent, because it
registers no warning sink.) You don't need to republish every time AgentBox adds
a config key. Wrong types, renamed keys, and agentbox config set <bad-key>
still fail loud — the registry is authoritative for keys it does know.
The same rule holds in-box for agentbox.yaml: agentbox-ctl (baked into an
image that may be months old) skips keys it doesn't recognize rather than
refusing to boot.
Publishing the SDK (maintainers)
The SDK ships to npm as @madarco/agentbox-provider-sdk (source at
packages/provider-sdk). It's a self-contained bundle — tsup inlines the internal
@agentbox/* packages (noExternal), so the only runtime deps a consumer pulls
are execa + yaml. Publish it in lockstep with a CLI release whose
SUPPORTED_SDK_API_VERSIONS includes the SDK's SDK_API_VERSION; additive
surface changes are a minor bump, a breaking Provider/CloudBackend/
ProviderModule change is a major bump and an SDK_API_VERSION bump.
# 1. Prove the *published artifact* is complete (packs, installs the tarball in a
# throwaway dir, asserts every export the example depends on — a file:/workspace
# link can't catch a missing file or an export that didn't ship).
pnpm --filter @madarco/agentbox-provider-sdk pack:test
# 2. Publish (manual — needs the npm token). prepublishOnly rebuilds dist;
# publishConfig.access=public handles the scoped-package default.
cd packages/provider-sdk && npm publish