feat(vercel): Vercel Sandbox provider adapter - #16
Conversation
Implements VercelSandboxRuntime against the shared SandboxRuntime and WorkflowRuntime ports, following the Freestyle adapter's structure (76cc7463): SDK-free config/capabilities, the vendor SDK isolated behind structural interfaces in one dynamically-imported module, and a construction-time reconciliation that throws if any capability claims `true` without the implementation behind it. Provider-specific decisions worth calling out: - Identity is the sandbox *name*, not an opaque id, so `RuntimeHandle.id` carries the name and the configured prefix is a real ownership boundary. stop/start/destroy refuse any name outside it. - `getById` goes through `list`, not `Sandbox.get`, because `get` resumes and bills the sandbox as a side effect; a crash-recovery scan must not be able to wake every handle it inspects. - `Sandbox.list` supports server-side `namePrefix` and `tags` filters, so label lookup is a real search. Rows are still re-checked against the requested tags in process: a server filter that were ever ignored would hand back a foreign sandbox as a warm lease. - Delete verification treats a same-name row with a different `createdAt` as proof our sandbox is gone rather than as a survivor, and retains the registration when absence cannot be verified so cleanup stays retryable. - Commands run as `sh -c` with cwd/env passed through provider fields, so no caller value is spliced into a command string. - `retryDeadlineMs` caps a whole logical operation including the SDK's internal `async-retry` attempts, carried via AsyncLocalStorage so concurrent operations cannot abort each other. - `launchDetached` is omitted rather than faked: create resolves only once the sandbox is running, so there is no mid-boot handle to hand back. - `acquire()` returns an AsyncDisposable for `await using` scopes. All behavioral capabilities are declared false pending live evidence, including `cleanupVerified`, which is implemented and unit-tested but not yet promoted. `neverIdle` is settled false: every Vercel sandbox carries a termination deadline. Dependency: exact optional peer + dev dependency on @vercel/sandbox@3.0.1 (integrity sha512-q/Ne1UaqZ4PWmOj0kTl/KByAR9ZRxOgJCH8WNWcV3ERgb00lO6TQKwS1dSsypHSBHHDfjihByMKX/mlfjIw/cg==, shasum 30a7a6d9d6366b20ed46fa59a157274455419227). The packument carries no gitHead; the upstream commit d7c3bf55d520c6e5b5381ed87285967b30ecc083 is taken from the package's SLSA v1 provenance attestation. Full provenance and the active-CPU-versus-wall-clock pricing analysis are in docs/vercel.md. Tests: 45 mocked contract tests. Suite 202 -> 247, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Session-Id: 5fab24e9-9a59-4f9e-8f5c-f2bbc9dda832
…ndary Three hardening changes, two adopted from modal-adapter-0821's parallel lane review and one they prompted. 1. Composite operations now share ONE budget across their round trips. Previously each round trip took a fresh copy of the timeout, so `runScript(timeoutMs: 60_000)` could wait 120s (command, then output), and `uploadBundle`'s worst case was (parents + 2) x fileTimeoutMs — unbounded in the number of directories a bundle spans. A `Deadline` fixes `expiresAt` once and every later call draws from what is left. Applied to runScript/collect, uploadBundle, stop, start, destroy, and the list pagination and settle/delete poll loops. The timeout error now names which cap fired — the operation budget or the SDK retry ceiling. Reporting "did not complete within 60000ms" after 25ms because the retry ceiling cut it short sends a reader hunting for a slow network instead of at the ceiling they configured. 2. The vendor boundary is a checked assignment, not `as unknown as`. Each SDK result is assigned through the structural interface, so a vendor signature change breaks the build in the one file that owns the boundary instead of surfacing at runtime against a live sandbox. I probed assignability against the real .d.ts first; it holds with no contortions. `create` narrows to the image or runtime branch rather than passing an object that claims both are possible. 3. `reconcileVercelCapabilities` is exported and takes a structural surface, so its failure paths are unit-testable with broken fakes — including the check that will gate the cleanupVerified promotion. Deliberately NOT adopted from that lane: their rule that `start`/`stop` present while `lifecycle` is not true should throw. Under-claiming is how a capability waits for live evidence here, the orchestrator reads the declaration rather than method presence, and a test now pins that this under-claim is intentional. Evidence the tests constrain behaviour: rather than claim a red-baseline I cannot honestly claim (tests were written alongside the implementation), I mutation-tested six invariants one at a time — dropped the client-side tag re-verification, the ownership guard in destroy, the createdAt reuse guard, the sh -c wrapping, the registration retention on unverified destroy, and the upload verification. Each was caught (1, 1, 1, 2, 1, 1 failures respectively), and the file restored byte-identical (sha256 609d4e26267b69a22f3581cdf35c42b245cafe2fdaee15d97b49b2bffcf59bf3). Also lands, per sandbox-lead-claude-0820b's rulings: - scripts/vercel-bench.ts, the live harness: ledger-before-use with an fsync before the create call goes out, hard caps on count/vCPUs/lifetime, and an independent post-run prefix audit reporting destroyed and verifiedGone as separate numbers. - docs/vercel.md states the credential block explicitly and dates it, so no capability cell can be misread as measured. Tests: 46 vercel contract tests (was 45). Suite 247 -> 250, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Session-Id: 5fab24e9-9a59-4f9e-8f5c-f2bbc9dda832
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 883daefd16
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
All reported issues were addressed across 11 files
You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
The provenance section previously asserted an upstream commit taken from
the SLSA attestation without resolving it. modal-adapter-0821's lane hit
the case that makes that a real gap: a packument `gitHead` that is
present but resolves to "No commit found" in either candidate repo, and a
`repository` field pointing at a different language's client entirely. A
present-but-unresolvable gitHead is worse than a missing one, because it
reads as provenance and is not.
So the claims are now verified rather than quoted, and recorded as a
ladder because these mechanisms are not universally available:
1. SLSA attestation — present here, but only 69 of 241 packages in this
tree have one. The next provider may not.
2. Registry signature — 241 of 241 verified via `npm audit signatures`.
3. Tarball hash — verified by download; SHA-1 and SHA-512 both match the
packument. Always available, so this is the floor.
4. gitHead — absent here, which is why rung 1 does the work. Resolved the
attested commit with `gh api repos/vercel/sandbox/commits/d7c3bf55…`
("Version Packages (#284)", 2026-08-20T08:54:45Z), and checked the
`repository` field against the attestation rather than trusting it.
Both name github.com/vercel/sandbox, so no mismatch here.
No code change; docs only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Session-Id: 5fab24e9-9a59-4f9e-8f5c-f2bbc9dda832
…untime The harness shipped with unverified safety guards, which is not a standard I get to apply to the adapter and not to the thing that will create billing resources. Prompted by modal-adapter-0821, whose harness tests its guards before touching a real account. Splits the core out of the CLI into src/vercel/bench.ts. The CLI keeps only credentials, durable ledger I/O, and argv; every guard now has a test: - ledger-before-use: the fake's own launch call is interleaved into the same log, so the ordering is observed rather than assumed. Also covers a failed create, where the intent record is the only trace. - bounded cost: the cap is refused up front and asserts nothing was created, because a bound checked after the fact is not a bound. - verified cleanup: everything created is destroyed; teardown still runs when the measurement throws; destroyed and verifiedGone are reported separately when verification fails. - the delete that lied: destroy resolves successfully but the sandbox is still listed. Only the independent sweep catches this, and it is the failure mode that would otherwise promote cleanupVerified on nothing. - burst probe: a partially-rejected burst still tears down what it did admit. - cost arithmetic: pins that the memory line is identical at 100%, 10% and 0% CPU. That is the whole correction to the "cheapest provider" premise, so it is enforced rather than left to a comment. Mutation-tested the guards themselves: ledgering after create (2 fails), dropping the bounds check (1), conflating destroyed with verifiedGone (1), skipping the sweep (2), and billing memory on active CPU rather than wall clock (1) were all caught. Moving teardown out of `finally` was NOT caught, and that is honest to record: it is an equivalent mutant, since the catch above is total and no behaviour differs. Keeping `finally` anyway — it stays correct if anyone later makes that catch selective — with a comment saying so. Restored byte-identical (sha256 b6ff57f032371456834b0c9c4d5f9799f214f99f24575a4030c78dab4ebbd9e1). Not exported from the barrel: internal measurement tooling, not API. Tests: 11 new. Suite 250 -> 261, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Session-Id: 5fab24e9-9a59-4f9e-8f5c-f2bbc9dda832
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Runtime (src/vercel/runtime.ts):
- Withdraw startScript/getScriptStatus/getScriptLogs. Vercel's runCommand has
no admission key, so a retry after a lost response would submit a second
command rather than reconcile with the first. The port's asyncExec is
all-or-nothing; the honest reading is asyncExec:false until the SDK offers
idempotent submission (or this adapter builds reconciliation on top of it).
- findAllByLabels now paginates incrementally: with a small limit the first
match on page one skips the rest of the listing, instead of materialising
every page and truncating. Short-circuits on limit<=0.
- countByLabels short-circuits on maxCount<=0.
- validateNamePrefix reserves the full 17 characters the generated suffix
uses ("-<16 hex>") rather than 10.
- validatePorts enforces the provider's documented 15-port ceiling.
- uploadBundle's verification step now runs through the shared bundle
deadline instead of starting a fresh runScript budget after mkdirs/writes
have already consumed most of fileTimeoutMs.
Benchmark (src/vercel/bench.ts + scripts/vercel-bench.ts):
- Sweep runs in the outer finally so a failed measurement, a failed burst
probe, or even a ledger error on run-end cannot strand admitted burst
sandboxes with the cleanup audit unfinished.
- Destroy failure is classified: only VercelDestroyVerificationError counts
as destroyed-but-unverified; any other error is a submission failure and
leaves destroyed=false so cleanup counts stay honest.
- cleanup.created now includes burst admits, matching destroyed and
verifiedGone which already do.
- Readiness probe fails fast on a nonzero or null exit code rather than
publishing a green-looking latency for a broken sandbox.
- CLI persists a labelled failure report even if runBenchmark rejects, so
the ledger, the results file, and the exit code all agree.
- sweepError surfaces in the report; clean is false whenever the sweep did
not certify itself.
Docs / config:
- package.json: relax @vercel/sandbox peer to ">=3.0.1 <4.0.0" for parity
with the other providers and the structural-interface drift-tolerance
design.
- docs/vercel.md + src/vercel/config.ts: correct the asyncExec capability
row and the "two forms" doc that should have said three.
Tests: cover port limit, prefix length, sweep-failure surfacing, readiness
probe failure, destroy-submit-vs-verification classification, and burst
admits in cleanup.created; replace the flaky wall-clock assertion with
timeoutMs/outputCalls checks that the shared budget already proves.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Session-Id: a0528f13-7875-43cb-9b06-cbfcc2e9fe33
Review backlog cleared — merge-readyThreads resolved: 19 / 19 (16 previously unresolved + 3 already resolved). Fixes landed in 3356fcaP1
P2
P3
CIGreen per-workflow (single
Coverage grew from 260 to 267 tests: added port-limit, prefix-length, sweep-failure, readiness-probe (nonzero + null), destroy-submit-vs-verification, and burst-admits-in-cleanup.created assertions. |
Adds
VercelSandboxRuntimeagainst the sharedSandboxRuntime/WorkflowRuntimeports, following the Freestyle adapter's structure (76cc7463).Review status: review-ready, pending live evidence. Vercel is not yet in the team's 1Password, so the n=1 canary and n=7 benchmark have not run — zero sandboxes created, zero spend. Every behavioral capability ships
false, anddocs/vercel.mddates the block explicitly so no cell can be misread as measured. The live run will only flip capability cells and add an economics-doc row; it does not change the code under review here.Provider-specific decisions
RuntimeHandle.idcarries the name, which makes the configured prefix a real ownership boundary rather than a labelling convention —stop/start/destroythrowVercelForeignSandboxErroroutside it, even for a handle attachedowned: true.getByIdgoes throughlist, notSandbox.get.getresumes the session as a side effect: it boots a VM and starts billing. A crash-recovery scan over a hundred handles must not be able to wake every one of them.Sandbox.listtakesnamePrefixandtags), unlike Freestyle's degraded[]. Rows are still re-checked against the requested tags in process — a server filter that were ever ignored would hand back a foreign sandbox as a warm lease, which is worse than no lease at all.createdAtcounts as proof ours is gone rather than as a survivor. Failed verification retains the registration so cleanup stays retryable.sh -cwithcwd/envpassed through provider fields, so no caller value is spliced into a command string.retryDeadlineMsceiling that spans the SDK's internalasync-retry. Errors name which cap fired.launchDetachedis omitted, not faked — create resolves only once the sandbox is running, so there is no mid-boot handle to hand back.acquire()returns anAsyncDisposableforawait usingscopes (harvested from the opencoredev/sandbox-sdk eval).Capabilities
All behavioral cells are
falsepending live evidence, includingcleanupVerified— implemented and unit-tested, but not promoted without a dated live-canary comment, matching the discipline Freestyle used.neverIdleisfalsefor a different reason: it is settled false, because every Vercel sandbox carries a termination deadline.Construction-time reconciliation walks all three registries and throws on any
trueclaim lacking its implementation.reconcileVercelCapabilitiesis exported and takes a structural surface so its failure paths are unit-tested with broken fakes.Economics correction
The premise that active-CPU billing makes Vercel dramatically cheapest for I/O-heavy agents needed a correction, documented in
docs/vercel.md: Active CPU genuinely excludes I/O wait, but Provisioned Memory is billed on wall clock, not active time. At the default 2 vCPU / 4 GB shape, one wall-clock hour costs $0.341 at 100% CPU, $0.110 at 10%, and $0.085 at 0% — a ~3.1x spread with a $0.0848/h floor set by memory. Cross-provider comparison has to be made against that floor. Active-CPU billing makes the CPU line small for I/O-bound agents; wall-clock memory sets the floor, and stopping the sandbox promptly — not merely leaving it idle — is what removes it.Testing
46 mocked contract tests (charter minimum was 20). Suite 202 -> 250, 0 failures, 3 pre-existing skips.
npm run buildandtsc --noEmitclean.Because the tests were written alongside the implementation, a red-baseline claim would have been fiction. Instead, six invariants were mutation-tested one at a time — client-side tag re-verification, the ownership guard in destroy, the
createdAtreuse guard, thesh -cwrapping, registration retention on unverified destroy, and upload verification. Each was caught by the suite, and the file restored byte-identical.Dependency provenance
@vercel/sandbox@3.0.1, exact optional peer + dev dependency, Apache-2.0. The packument carries nogitHead(monorepo release-bot publish), so the upstream commitd7c3bf55d520c6e5b5381ed87285967b30ecc083is taken from the package's SLSA v1 provenance attestation rather than fabricated. Integrity hash, shasum, and attestation URL are all indocs/vercel.md. The SDK is imported in exactly one dynamically-imported module and assigned through a structural interface, so vendor drift breaks the build rather than a live sandbox.🤖 Generated with Claude Code
Summary by cubic
Adds a Vercel Sandbox adapter behind the
SandboxRuntime/WorkflowRuntimeports and a live-benchmark harness; previously Vercel was unsupported. Recent updates withdraw non‑idempotent async exec APIs, enforce provider limits, improve pagination/verification, and harden cleanup and reporting.Identity is the sandbox name; start/stop/destroy refuse names outside the configured prefix. getById uses list to avoid waking/billing stopped sandboxes; tag lookup uses server filters plus in‑process rechecks. Delete verification treats same‑name/different‑createdAt as “ours is gone” and retains registrations when absence can’t be proven. Commands run via sh -c with explicit cwd/env. One deadline is shared across each logical operation; errors name whether the op budget or the SDK retry ceiling fired.
launchDetachedis omitted;acquire()returns anAsyncDisposable. Async exec is now declared false andstartScript/getScriptStatus/getScriptLogsare removed. Enforces the 15‑port ceiling and reserves 17 chars for the generated name suffix;findAllByLabelspaginates incrementally and short‑circuits on limit≤0;countByLabelsshort‑circuits on maxCount≤0;uploadBundleverification runs within the shared bundle deadline. The SDK sits behind one dynamic import with checked structural typing; capability reconciliation throws on anytrueclaim without an implementation. All behavioral capabilities remain false pending live evidence.Benchmark harness: sweep runs in the outer finally; destroy failures are classified (only verified‑delete errors count as unverified); cleanup.created includes burst admits; readiness probe fails fast on nonzero/null exit; CLI persists a labeled failure report;
sweepErrorsurfaces and marks clean=false. Internal tooling; not exported.Install optional peer
@vercel/sandbox(>=3.0.1 <4.0.0).Provide token, teamId, projectId, namePrefix, and defaultHomeDir; ambient
VERCEL_*and on‑disk OAuth are ignored.Do not rely on async exec or detached launch; use
runScriptandacquire()disposal. Orchestrators must read declared capabilities; behavioral flags stay false until live evidence promotes them (no code change required).Written for commit 3356fca. Summary will update on new commits.