Delegated Runner Contract
Read this when:
- adding a delegated-run provider that does not expose SSH;
- designing a hosted runner model, deployment, or sandbox image for Crabbox;
- deciding whether a provider PR has enough proof to merge.
Delegated-run providers are not SSH leases. Crabbox cannot assume rsync, OpenSSH, a shell-compatible bootstrap, or direct process control. The provider owns workspace transport and command execution, while Crabbox owns the local CLI surface, config, claims, slugs, timing records, and normalized status output.
Delegated warmup adapters with post-readiness core bookkeeping can opt into WarmupRequest.BeforeComplete. Invoke it synchronously once after successful acquisition and retention, before final completion/timing output, and include its duration in the total. It has no error return and grants no cleanup or allocation-retry authority. Never invoke it on allocation failure. Blacksmith uses this boundary for best-effort portal inventory; other adapters need no change unless they adopt that finalization contract.
This page defines the minimum portable runner contract a new delegated provider must satisfy before it should become a built-in provider.
#Why This Exists
Several hosted execution platforms can run code only through a prediction, sandbox API, job, or deployment. A provider adapter can still fit Crabbox when that remote side implements the same small runner shape.
Without this contract, each provider PR has to invent its own answers for:
- how the checkout reaches the runner;
- how argv, shell mode, workdir, env, and timeout are represented;
- what a successful platform run means versus a successful user command;
- how logs are streamed without duplication;
- how cancellation and cleanup work;
- what live proof is needed before merge.
Provider-specific APIs can differ. The Crabbox-facing behavior should not.
#Minimum Runtime Shape
A delegated runner must provide one command execution operation with these semantics:
{
"archive": "data:application/gzip;base64,... or https://...",
"command": ["npm", "test"],
"shell": false,
"workdir": "/workspace/crabbox",
"env": {
"EXAMPLE_FLAG": "1"
},
"timeoutSecs": 3600,
"syncDelete": true
}
The provider adapter may translate this into a provider-specific request, but the meaning must stay stable:
archivecontains the Crabbox sync archive or a pointer to it.commandis argv whenshell=false.commandis a single shell string, usually incommand[0], whenshell=true.workdiris an absolute, dedicated workspace directory on the runner.envcontains only explicitly forwarded command environment values. Provider API tokens and Crabbox auth variables must be stripped.timeoutSecsis the command execution budget, not necessarily the platform billing timeout.syncDelete=truemeans the runner should remove files from the previous workspace contents that are absent from the new archive, if it reuses state.
If the provider cannot support one of these fields, the adapter must document that limitation and reject the corresponding Crabbox option instead of silently changing behavior.
#Minimum Result Shape
The runner output must distinguish platform lifecycle from command exit status:
{
"exitCode": 0,
"stdout": "optional final stdout\n",
"stderr": "optional final stderr\n",
"artifacts": []
}
Rules:
- A provider-level
succeededstate is not enough for Crabbox success. The runner result must includeexitCode. exitCode == 0maps to a successful Crabbox command.exitCode != 0maps to the user command's exit code.- Missing or malformed result JSON is a provider failure, not user command success.
- Provider
failed,canceled, or timeout states must include redacted provider diagnostics when available.
Streaming providers should stream stdout/stderr as the command runs. Polling providers must print only new log content on each poll; repeated complete-log responses must be deduplicated by the adapter.
#Workspace Transport
The first supported transport is a bounded gzip archive. Provider adapters must choose one explicit upload path:
- data URL for small workspaces;
- signed object URL owned by the operator or coordinator;
- provider-native file upload;
- runner-side fetch from a short-lived URL.
The adapter must enforce a maximum archive size before creating a paid or stateful provider resource. If large workspaces need object storage, that is a separate feature and should not be implied by a data-URL MVP.
Do not silently upload source code to an unspecified third-party store.
#Credentials And Env
Provider credentials are control-plane credentials. They are never runner env.
Required behavior:
- Read provider API tokens from environment or the provider's native auth store, not from repo YAML or argv.
- Reject or require explicit operator approval when repo config selects a non-default API URL while inheriting an ambient provider token.
- Do not follow cross-origin redirects with authorization headers.
- Redact tokens from HTTP errors, JSON errors, logs, docs examples, and recorded run output.
- Strip provider auth variables from the forwarded runner env summary and request payload.
Forwarded user env still follows the normal Crabbox allowlist rules. A delegated runner must not inherit the full local process environment by default.
#Claims, Status, And Stop
A delegated provider must map provider resources into Crabbox's normal local claim model:
- create or reuse a Crabbox lease ID and slug;
- record the provider's raw run, session, prediction, or sandbox ID in the claim;
- resolve
statusandstopby lease ID, slug, or documented raw provider ID; - avoid listing unrelated account resources that cannot be identified as Crabbox-created;
- cancel or stop the provider resource on local context cancellation when the provider supports it.
If a provider resource is created before local claim setup fails, the adapter must cancel that resource before returning the error.
#Shared Sandbox Lifecycle
E2B, Modal, and Cloudflare Sandbox use shared.RunDelegatedSandbox in internal/providers/shared/delegated.go. The lifecycle sequences preflight, archive preparation, acquisition or authorized reuse, setup, sync or workspace preparation, command preparation, execution, retention, cleanup, and reporting. The adapters still own provider APIs, claim authorization and scope binding, operation locks, credential filtering, archive transport, and actual execution. It does not apply to delegated jobs, Blacksmith, or SSH leases.
New sandbox runs prepare and check the archive before resource creation. Reused sandboxes are resolved and authorized first, then use the existing archive sync helper. --no-sync skips the archive and transfer but still prepares the workdir. --sync-only skips command preparation/execution and follows normal retention and cleanup rules. Modal now uses the shared staged workspace replacement when sync.delete is enabled, so failed upload or extraction does not first erase the previous workspace.
New sandboxes are cleaned up unless --keep is set, or --keep-on-failure is set and pre-cleanup work fails. This includes setup, sync, command preparation, command execution, timeout, and cancellation failures. Reused sandboxes are never automatically deleted by a run. Failure before successful acquisition does not create a session handle or grant deletion authority: partial-create rollback stays in the adapter, including failures while establishing a claim.
Each automatic sandbox cleanup is attempted once, with an uncanceled context bounded to 30 seconds (15 seconds for Cloudflare Sandbox). Provider command transport cleanup runs before sandbox deletion, including for retained sandboxes. Adapter operation locks remain held through finalization. E2B still verifies the exact live claim before deletion; Cloudflare still resolves and verifies reused claims under its operation lock. Cloudflare refreshes retained claim activity after failed setup/sync as well as command completion.
The first failure determines the command exit code and normalized outcome. A nonzero user command exit is preserved if cleanup also fails; cleanup errors are added as diagnostics. Setup/sync errors retain their existing CLI error codes but are classified as provider failures, not user command exits. A transport failure returns code 1; its cancellation or deadline cause is retained for outcome classification. Cleanup failure after otherwise successful work returns code 1 with errorKind=provider-error. A failed deletion leaves the claim and session.kept=true so the cleanup command remains available.
Timing JSON is emitted once after retention and cleanup, including early failures, and agrees with the returned exit code, status, error kind, and total duration. Total duration includes remote cleanup; command duration excludes command preparation and cleanup. An output-writer failure is returned without replacing an earlier failure or retrying cleanup; no successful timing emission can be guaranteed when the writer itself fails.
These rules correct Modal's earlier omission of setup/sync failures from --keep-on-failure, E2B/Modal's warning-only cleanup failures, and timing records that could report success before cleanup or retained-claim bookkeeping failed.
#Unsupported SSH Features
Delegated providers must reject SSH-only run features unless they implement an equivalent contract:
--fresh-pr;- uploaded POSIX scripts, except module-run providers that explicitly treat scripts as module source;
--full-resyncand checksum rsync;- local stdout/stderr capture files;
- capture-on-fail bundles;
--downloadunlessFeatureRunDownloadsis implemented;- artifact globs unless
FeatureRunArtifactsis implemented; --emit-proofunlessFeatureRunProofis implemented;- env helpers;
--stop-after;- Actions hydration;
- SSH, VNC, WebVNC, and code-server.
Use RejectDelegatedSyncOptionsForSpec as the default guard. Add capability flags only when the provider really implements the corresponding behavior.
#Provider Spec
A runner-backed delegated provider should normally declare:
core.ProviderSpec{
Name: "example",
Family: "example",
Kind: core.ProviderKindDelegatedRun,
Targets: []core.TargetSpec{{OS: core.TargetLinux}},
Features: core.FeatureSet{core.FeatureArchiveSync, core.FeatureRunSession},
Coordinator: core.CoordinatorNever,
}
Do not advertise SSH, desktop, browser, code, Tailscale, coordinator support, or artifact/download/proof features until those paths are implemented and tested.
#Live Proof Bar
Offline tests are necessary but not sufficient for a new hosted delegated provider. Before merge, attach redacted proof from a guarded live run against a compatible runner.
Minimum proof:
crabbox providers --jsonshows the provider metadata and truthful capabilities.- Missing-auth
doctororrunfails before provider resource creation. - A live run creates one provider resource, uploads or stages a small workspace, executes a command, streams logs without duplication, maps
exitCode=0to success, and records timing. - A live nonzero command maps to the command exit code, not generic provider success.
statusresolves the created claim.stopor non-kept cleanup cancels/deletes the provider resource.- Final
listdoes not show leftover Crabbox-owned resources. - Logs and errors do not expose provider tokens, upload URLs with credentials, private file paths, or forwarded secret values.
If no live provider credentials or compatible runner deployment exist yet, keep the provider PR unmerged and land only reusable contract, tests, or docs that do not advertise the provider as usable.
#Review Checklist
Before reviewing a delegated provider PR, check:
- The runner input and output schema is documented.
- Success depends on runner
exitCode, not only provider terminal state. - Token resolution, redirect behavior, and redaction are tested.
- Provider auth env is stripped from runner payloads.
- Archive size is bounded before paid resource creation.
- Claim setup failure cancels already-created resources.
listavoids unrelated account resources.- Unsupported SSH-only flags fail with clear errors.
- Provider metadata does not overstate lifecycle, artifacts, SSH, or coordinator support.
- Live proof exists or the PR is intentionally not merge-ready.