Repository navigation
feat(sandbox): platform hooks for usage limits (runtime metering + concurrency gate) - #210
Conversation
✅ Deploy Preview for staging-void-cloud ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e446a161c
ℹ️ 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".
Two issues found in PR review (#210): 1. Acquire used the raw caller-supplied id while release used ctx.id.name (the normalized lowercase DO name), so mixed-case ids leaked their concurrency lease until the alarm reconciliation swept the slot. Acquire now derives sandboxId from the SDK stub so it matches what onStop/onActivityExpired pass to release. 2. The gate fetch was not wrapped in try/catch, so a service-binding transport error (proxy unreachable, internal throw) broke sandbox acquisition entirely — contradicting the documented fail-open intent that only explicit 409 denials should propagate. Only SandboxLimitError now escapes; all other errors fall through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e358e3d97
ℹ️ 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7aeeab12ba
ℹ️ 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".
Adds VoidPlatformSandbox extends Sandbox in void/sandbox that emits
container-runtime metering events to the platform USAGE_EVENTS_QUEUE
binding and gates new acquisitions on a platform sandbox-gate (via the
__VOID_PROXY service binding). getSandbox() becomes async (Promise<T>)
to support the gate round-trip and now throws SandboxLimitError with a
typed reason ('concurrency' | 'runtime_budget') on denial.
Codegen swaps the user-worker entry's Sandbox import from
@cloudflare/sandbox to void/sandbox so existing user subclasses inherit
the platform overrides transparently. When the platform bindings are
absent (local dev), all paths no-op silently.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The platform proxy's auth middleware requires { projectId, aiToken }
in the request body for HMAC verification (same as the AI and ISR
paths). Without these fields, /sandbox/acquire and /sandbox/release
return 400 and the framework's fail-open behavior turns the entire
concurrency gate into a no-op in production.
Match the runtime/ai.ts and runtime/isr.ts convention: read
__VOID_PROJECT_ID and __VOID_PROXY_TOKEN from raw env, include both
in acquire/release bodies. Skip the gate call entirely when either
is absent (local dev without platform bindings).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two issues found in PR review (#210): 1. Acquire used the raw caller-supplied id while release used ctx.id.name (the normalized lowercase DO name), so mixed-case ids leaked their concurrency lease until the alarm reconciliation swept the slot. Acquire now derives sandboxId from the SDK stub so it matches what onStop/onActivityExpired pass to release. 2. The gate fetch was not wrapped in try/catch, so a service-binding transport error (proxy unreachable, internal throw) broke sandbox acquisition entirely — contradicting the documented fail-open intent that only explicit 409 denials should propagate. Only SandboxLimitError now escapes; all other errors fall through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Read project id from the existing __PROJECT_ID slot injected for AI/ISR instead of introducing a parallel __VOID_PROJECT_ID binding. The two carry the same value and are deployed atomically. - Parallelize onStart() storage IO (multi-key get + object-form put) and pair the alarm put + setAlarm; only re-arm when an active window exists. - Trim narrative comments to leave WHY-only notes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7aeeab1 to
521cb5d
Compare
…dbox Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…l on gate release - onStart no longer sets the heartbeat alarm inside Promise.all before super.onStart() — DOs have a single alarm slot and super can clobber it. Set alarm after super, matching the alarm() override pattern. - notifyGateRelease is now wrapped in ctx.waitUntil at the onStop/onActivityExpired call sites so the release POST survives lifecycle teardown. Otherwise the gate's acquire counter can be stuck until the stale-slot sweep, manifesting as false concurrency denials. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c513cdb1d5
ℹ️ 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".
…imitError to 429 - onStart no longer persists START_TS/LAST_FLUSH_TS before super.onStart(). If super threw, the timestamps survived and the next start's orphan reconciliation branch billed wall-clock time during which no container ever ran. Write timestamps (and re-arm the heartbeat) only after super succeeds, using a fresh Date.now() so they reflect container-ready time. - kitchen-sink playground now returns 429 with the limit reason for SandboxLimitError instead of letting the generic catch surface a 500. Reference implementations get copied — quota denials shouldn't look like server errors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f588ff96a1
ℹ️ 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".
…etry re-billing After emitting the orphan-reconciliation runtime event in onStart(), delete the START_TS/LAST_FLUSH_TS keys before calling super.onStart(). Otherwise a failed super (bad image, platform scheduling error) leaves the orphan anchor intact, and the next retry detects the same anchor and emits another reconciliation event for an ever-growing window — double-counting runtime already billed. The round-3 phantom-metering protection is preserved: a failed super on a fresh start (no prior orphan) still writes nothing, so the next retry is a clean fresh start. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 13c3cf9bc3
ℹ️ 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.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 13c3cf9. Configure here.
…ent stub parity
- VoidPlatformSandbox.alarm() now fires a fire-and-forget refresh POST to
/sandbox/refresh via waitUntil, so the per-account concurrency DO's
STALE_MS=1h sweep doesn't reap slots for long-running sessions. The
heartbeat already runs every 5 min, well inside the sweep window.
- onStart() wraps super.onStart() in try/catch; on throw, releases the
acquired gate lease through waitUntil and re-throws. Otherwise a bad
image / scheduling failure leaked the lease until the 1h sweep,
manifesting as false concurrency denials on immediate retries.
- The client-side stub for void/sandbox now matches the real shape:
async getSandbox() that throws "server-only" instead of returning a
bare {} synchronously, and a SandboxLimitError class so client code
that does `instanceof` doesn't crash with a TypeError.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15d3927864
ℹ️ 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".
getSandbox is async in this PR — the two code samples in the guide were still showing synchronous usage, which would surface .exec / .writeFile on a Promise. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…d deploy) (#210) The `schema-compat` CI gate started failing on every prod deploy since #207, blocking the `deploy` job with: delete from "projects" violates FK "builds_project_id_projects_id_fk" Key (id)=(proj_test74) is still referenced from table "builds" Root cause is a test-isolation bug, not a real migration incompatibility: - `clearTables()` never deleted `builds` (or the github tables), and `builds` references deployments/projects/users WITHOUT `onDelete: 'cascade'` (a prod project delete removes its builds explicitly, so the schema omits cascade by design). A leftover `builds` row therefore blocks the deletes. - Only `schema-compat` runs against ONE shared real Postgres (`VOID_TEST_DATABASE_URL` set -> `fileParallelism: false`); regular integration uses a fresh per-file PGlite, so build-row residue never crosses files and the bug stays hidden. #207 appended `createBuild` tests to api-oidc/api-webhooks, tipping the latent bug into a hard failure. Fix: delete `builds`, `githubConnections`, `githubInstallations` before `deployments`/`projects`/`users` in `clearTables`, mirroring the `clearBuildTables` helper the build-creating test files already use. Verified against a real local Postgres (reproduces the exact `proj_test74` FK failure pre-fix; full integration suite 942/942 green post-fix). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary
Companion framework change for the void-platform
feat/sandbox-usage-limitswork. Adds the hooks that let the platform enforce per-account sandbox usage caps without changing the user-facingvoid/sandboxergonomics (modulo one breaking signature change — see below).VoidPlatformSandbox extends Sandboxexported fromvoid/sandbox. It overridesonStart/alarm/onStop/onActivityExpiredto emit per-window container-runtime metering events to a platformUSAGE_EVENTS_QUEUEqueue producer, with a 5-minute heartbeat alarm and crash-recovery reconciliation.SandboxLimitError(typedreason: 'concurrency' | 'runtime_budget') thrown bygetSandbox()when the platform denies acquisition.getSandbox()is nowasync(returnsPromise<T>) so it canawaitthe platform sandbox-gate (via the existing__VOID_PROXYservice binding) before returning the DO stub. The gate POST body matches the existing AI/ISR proxy convention:{ projectId, aiToken, sandboxId }.router/compile.ts) swaps the user-worker entry'sSandboximport from@cloudflare/sandboxtovoid/sandboxso user subclasses (extends Sandbox) transparently inherit the platform overrides.void dev), every new path no-ops silently — no behavior change for non-platform users.Breaking change
getSandbox()returnsPromise<T>instead ofT. Existing callers mustawaitit.Test plan
vp run check(typecheck) — clean forpackages/void.vp test run packages/void/test/unit/sandbox-runtime— 11 tests passing (gate denial, fail-open on 500, crash-recovery reconciliation, no-op-when-unbound, identity-vars-missing skip path).vp test run packages/void/test/unit/compile— codegen tests assert the swap tovoid/sandboxand verify user-subclass + default cases.playground/kitchen-sink.🤖 Generated with Claude Code
Note
Medium Risk
Medium risk because it introduces a breaking API change (
getSandbox()is now async) and adds platform-dependent network/queue side effects in sandbox lifecycle hooks that could affect sandbox availability and billing if misconfigured.Overview
void/sandboxnow provides a platform-wrappedVoidPlatformSandboxthat layers runtime metering (5-minute heartbeat + crash-recovery reconciliation) and gate coordination (acquire/refresh/release) on top of Cloudflare’s Sandbox DO, emitting usage events toUSAGE_EVENTS_QUEUEwhen bound.getSandbox()is nowasyncand consults the__VOID_PROXYgate before returning the stub, throwing a typedSandboxLimitErroron explicit 409 denials and failing open on other gate errors; codegen switches worker entry sandbox exports to useVoidPlatformSandboxso userextends Sandboxsubclasses inherit platform hooks.Docs and the kitchen-sink example are updated to
await getSandbox()and to treatSandboxLimitErroras a user-facing 429, and the client stub forvoid/sandboxis updated to throw loudly and mirror the new API shape; unit tests were added/updated to cover codegen and sandbox runtime behaviors.Reviewed by Cursor Bugbot for commit 827e9ae. Bugbot is set up for automated code reviews on this repo. Configure here.