Skip to content

feat(sandbox): platform hooks for usage limits (runtime metering + concurrency gate) - #210

Merged
Brooooooklyn merged 10 commits into
mainfrom
feat/sandbox-usage-limits
May 19, 2026
Merged

Brooooooklyn merged 10 commits into
mainfrom
feat/sandbox-usage-limits

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented May 19, 2026 •

Copy link
Copy Markdown
Member

Summary

Companion framework change for the void-platform feat/sandbox-usage-limits work. Adds the hooks that let the platform enforce per-account sandbox usage caps without changing the user-facing void/sandbox ergonomics (modulo one breaking signature change — see below).

  • New VoidPlatformSandbox extends Sandbox exported from void/sandbox. It overrides onStart / alarm / onStop / onActivityExpired to emit per-window container-runtime metering events to a platform USAGE_EVENTS_QUEUE queue producer, with a 5-minute heartbeat alarm and crash-recovery reconciliation.
  • New SandboxLimitError (typed reason: 'concurrency' | 'runtime_budget') thrown by getSandbox() when the platform denies acquisition.
  • getSandbox() is now async (returns Promise<T>) so it can await the platform sandbox-gate (via the existing __VOID_PROXY service binding) before returning the DO stub. The gate POST body matches the existing AI/ISR proxy convention: { projectId, aiToken, sandboxId }.
  • Codegen (router/compile.ts) swaps the user-worker entry's Sandbox import from @cloudflare/sandbox to void/sandbox so user subclasses (extends Sandbox) transparently inherit the platform overrides.
  • When the platform bindings are absent (local void dev), every new path no-ops silently — no behavior change for non-platform users.

Breaking change

getSandbox() returns Promise<T> instead of T. Existing callers must await it.

- const sandbox = getSandbox('default');
+ const sandbox = await getSandbox('default');

Test plan

  • vp run check (typecheck) — clean for packages/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 to void/sandbox and verify user-subclass + default cases.
  • Integration verified by the companion platform PR via end-to-end deploy of 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/sandbox now provides a platform-wrapped VoidPlatformSandbox that 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 to USAGE_EVENTS_QUEUE when bound.

getSandbox() is now async and consults the __VOID_PROXY gate before returning the stub, throwing a typed SandboxLimitError on explicit 409 denials and failing open on other gate errors; codegen switches worker entry sandbox exports to use VoidPlatformSandbox so user extends Sandbox subclasses inherit platform hooks.

Docs and the kitchen-sink example are updated to await getSandbox() and to treat SandboxLimitError as a user-facing 429, and the client stub for void/sandbox is 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.

@netlify

netlify Bot commented May 19, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for staging-void-cloud ready!

Name Link
🔨 Latest commit 827e9ae
🔍 Latest deploy log https://app.netlify.com/projects/staging-void-cloud/deploys/6a0c60773a465600084deba5
😎 Deploy Preview https://deploy-preview-210--staging-void-cloud.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/void/src/runtime/sandbox.ts Outdated
Comment thread packages/void/src/runtime/sandbox.ts Outdated
Comment thread packages/void/src/runtime/sandbox.ts Outdated
Brooooooklyn added a commit that referenced this pull request May 19, 2026
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/void/src/runtime/sandbox.ts Outdated
Comment thread packages/void/src/runtime/sandbox.ts
Comment thread packages/void/src/runtime/sandbox.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/void/src/runtime/sandbox.ts Outdated
Brooooooklyn and others added 4 commits May 19, 2026 17:07
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>
@Brooooooklyn
Brooooooklyn force-pushed the feat/sandbox-usage-limits branch from 7aeeab1 to 521cb5d Compare May 19, 2026 09:09
Brooooooklyn and others added 2 commits May 19, 2026 17:13
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/void/src/runtime/sandbox.ts
Comment thread packages/void/src/runtime/sandbox.ts Outdated
Comment thread playground/kitchen-sink/routes/api/sandbox.ts
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/void/src/runtime/sandbox.ts
Comment thread packages/void/src/runtime/sandbox.ts Outdated
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/void/src/runtime/sandbox.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/void/src/runtime/sandbox.ts
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/void/src/runtime/sandbox.ts
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>
@Brooooooklyn
Brooooooklyn merged commit 9c03e41 into main May 19, 2026
10 checks passed
@Brooooooklyn
Brooooooklyn deleted the feat/sandbox-usage-limits branch May 19, 2026 13:50
cpojer pushed a commit that referenced this pull request Sep 11, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants