Skip to content

fix(sandbox): terminal kill_switch_off + drop resume-time relock - #1675

Merged
2witstudios merged 3 commits into
masterfrom
pu/sprites-terminal-killswitch
Jun 22, 2026
Merged

2witstudios merged 3 commits into
masterfrom
pu/sprites-terminal-killswitch

Conversation

@2witstudios

@2witstudios 2witstudios commented Jun 22, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #1674 (which merged at the interim relock version). Two changes — plus a required deploy step for terminals to actually work.

1. Terminal kill_switch_off — code half (load-bearing, not sufficient alone)

Agent chat (web) can run code, but terminals were denied with "Terminal access denied: kill_switch_off".

Root cause: the terminal PTY auth runs in the realtime service. isCodeExecutionEnabled() / resolveSpritesToken() / getSandboxSessionSecret() resolved their values through getValidatedEnv(), which validates process.env against the full web schema. The pagespace-realtime Fly app does not carry CSRF_SECRET / ENCRYPTION_KEY (the schema requires both when NODE_ENV != test), so getValidatedEnv() throws there — the kill-switch read catches it and returns false, denying every terminal even if the flag were set. The same throw blanks SPRITES_API_TOKEN / SANDBOX_SESSION_SECRET. Agent chat works because it runs in web, where the full schema validates.

Fix: read these three cross-service values directly from process.env, decoupled from full web-env validation. New env-reads.test.ts locks it.

⚠️ This code change alone does NOT enable terminals — it stops the throw, but the three vars must also be present in realtime's env (see Deploy below).

2. Drop the resume-time egress relock (the agreed simplification)

Reverts the interim relock/warm-window/get(options) (Codex P2): policy persists across hibernation, allowlist is static deny-all, dropped wakes are recovered by the per-op cold-start retry, future widening is bounded by the 24h reclaim. Terminal P1 wake kept via ensureSpriteAwake (warms a resumed VM before the PTY). Net −69 lines.

Required deploy step (separate, in PageSpace-Deploy)

Set the three vars on the realtime Fly app (token + secret must match the web app's):

flyctl secrets set --app pagespace-realtime \
  CODE_EXECUTION_ENABLED=true \
  SPRITES_API_TOKEN=<same as web> \
  SANDBOX_SESSION_SECRET=<same as web>

(or add CODE_EXECUTION_ENABLED="true" to fly.realtime.toml [env] + the two secrets.) Until this is done, terminals stay kill_switch_off.

Validation

  • @pagespace/lib + realtime typecheck, lint
  • 250 sandbox unit tests green (incl. new env-reads regression + ensureSpriteAwake)

🤖 Generated with Claude Code

2witstudios and others added 2 commits June 22, 2026 17:35
…+ per-op retry

Simplify back toward the Sprites docs' model ("configure once, hibernate, wake
on demand") after review reflection. The previous commit added a warm-window +
relock + get(options) path to reapply egress on every multi-hour resume; that is
unnecessary complexity:

- A Sprite retains its network policy across hibernation (platform-persisted), so
  a resume is NOT running unprotected — reapplying only propagates allowlist
  *changes*, and the allowlist is currently a static empty deny-all, so there is
  nothing to propagate today.
- A dropped first wake on resume is already recovered by the per-op cold-start
  retry: runCommand (runSpawnedWithWakeRetry) and writeFile/readFile
  (fsWithWakeRetry). So the conversation path needs neither relock nor warm.

Reverted: warmWindowMs / relock plan flag (lifecycle), get(options) on the
client seam (types + session-manager + driver), terminal relock plumbing.

Kept (load-bearing, was Codex P1 — the original "terminal stuck connecting"
symptom on a resumed VM): the terminal PTY still needs an awake VM because
openPtyShell's bash spawn isn't wrapped in the cold-start retry. Now handled by
a dedicated exported ensureSpriteAwake(sprite), called in the realtime path only
on resume (fresh creates are warmed by the acquire's mkdir).

Net −69 lines vs the relock commit. lib+realtime typecheck, lint, 247 sandbox
tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBkG69PYNEYBYR6pF3TPRP
…ess.env, not full-schema env

Agent chat (web) could run code but terminals were denied with "Terminal access
denied: kill_switch_off". Root cause: the terminal PTY auth runs in the realtime
service, and isCodeExecutionEnabled() / resolveSpritesToken() /
getSandboxSessionSecret() read their values via getValidatedEnv(), which parses
process.env against the FULL web schema (DATABASE_URL, CSRF_SECRET,
ENCRYPTION_KEY, …). realtime is a lean Socket.IO server that doesn't carry the
whole web env, so getValidatedEnv() THROWS there; the kill-switch read catches
the throw and returns false → every terminal denied even with
CODE_EXECUTION_ENABLED=true. (The same throw also blanked SPRITES_API_TOKEN and
SANDBOX_SESSION_SECRET on the terminal path.) The agent path works because it
runs in web, where the full schema validates.

Fix: read these three cross-service values directly from process.env. They are a
non-secret feature flag and two individual secrets — gating them on full web-env
validation is the bug. Behavior in web is unchanged; realtime now sees the flag
and credentials it actually has. New regression test (env-reads.test.ts) locks
the decoupling by reading the flags with the rest of the env absent.

lib+realtime typecheck, lint, 250 sandbox tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBkG69PYNEYBYR6pF3TPRP
@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@2witstudios, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 38 minutes and 48 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6a53b12-3847-44b1-a013-11af89effcc5

📥 Commits

Reviewing files that changed from the base of the PR and between e9042ec and bb8f21c.

📒 Files selected for processing (3)
  • apps/realtime/src/index.ts
  • packages/lib/src/services/sandbox/__tests__/env-reads.test.ts
  • packages/lib/src/services/sandbox/session-manager.ts
📝 Walkthrough

Walkthrough

Removes the relock/warm-window concept from the sandbox lifecycle planner and ExecSandboxClient.get, renames the internal wake helper to ensureSpriteAwake and exports it, switches three env-read helpers (isCodeExecutionEnabled, resolveSpritesToken, getSandboxSessionSecret) from getValidatedEnv to direct process.env reads, and integrates ensureSpriteAwake into the realtime terminal PTY resume path for hibernated sprites.

Changes

Sandbox Lifecycle and Client Refactor

Layer / File(s) Summary
Public type contract changes
packages/lib/src/services/sandbox/lifecycle.ts, packages/lib/src/services/sandbox/sandbox-client/types.ts
SandboxLifecyclePlan resume variant drops relock; PlanLifecycleInput removes warmWindowMs; ExecSandboxClient.get removes options?: SandboxCreateOptions.
Direct process.env reads replacing getValidatedEnv
packages/lib/src/services/sandbox/can-run-code.ts, packages/lib/src/services/sandbox/sandbox-client/sprites.ts, packages/lib/src/services/sandbox/session-manager.ts, packages/lib/src/services/sandbox/__tests__/env-reads.test.ts
isCodeExecutionEnabled, resolveSpritesToken, and getSandboxSessionSecret each replaced getValidatedEnv() + try/catch with a direct process.env read; new test suite covers all three helpers.
Lifecycle planner removes warmWindowMs and relock
packages/lib/src/services/sandbox/lifecycle.ts, packages/lib/src/services/sandbox/__tests__/lifecycle.test.ts
planSandboxLifecycle no longer accepts or computes warmWindowMs; resume plans are returned without a relock field; tests updated with simplified resume expectations and hibernated-session coverage.
ensureSpriteAwake export and get simplification
packages/lib/src/services/sandbox/sandbox-client/sprites.ts, packages/lib/src/services/sandbox/sandbox-client/__tests__/sprites.test.ts
Internal wakeSprite renamed and exported as ensureSpriteAwake; fsWithWakeRetry updated to call it; ExecSandboxClient.get removes options and egress relock logic; tests assert cheap-reconnect behavior and ensureSpriteAwake retry on WebSocket failure.
Session managers drop relock options
packages/lib/src/services/sandbox/session-manager.ts, packages/lib/src/services/sandbox/terminal-session-manager.ts, packages/lib/src/services/sandbox/__tests__/session-manager.test.ts
Both session managers call deps.client.get({ sandboxId }) without options; terminal-session-manager comment updated noting warming is handled externally; session-manager tests remove relock assertions.
Realtime terminal wake on hibernated sprite resume
apps/realtime/src/index.ts
makeTerminalCheckAuth imports ensureSpriteAwake and awaits it when sandboxResult.resumed is true, before PTY session auditing and budget charging.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant makeTerminalCheckAuth
  participant createSpritesSandboxClient
  participant ensureSpriteAwake
  participant PTYSession

  Client->>makeTerminalCheckAuth: terminal auth request (sandboxId)
  makeTerminalCheckAuth->>createSpritesSandboxClient: get({ sandboxId })
  createSpritesSandboxClient-->>makeTerminalCheckAuth: sprite (resumed=true)
  makeTerminalCheckAuth->>ensureSpriteAwake: ensureSpriteAwake(sprite)
  Note over ensureSpriteAwake: exec retry loop until<br/>WebSocket open or error
  ensureSpriteAwake-->>makeTerminalCheckAuth: VM warmed
  makeTerminalCheckAuth->>PTYSession: open PTY, audit session, charge budget
  PTYSession-->>Client: PTY session established
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • 2witstudios/PageSpace#1664: Also modifies packages/lib/src/services/sandbox/can-run-code.ts; this PR changes the CODE_EXECUTION_ENABLED env-read mechanism in the same file.
  • 2witstudios/PageSpace#1667: Also modifies packages/lib/src/services/sandbox/sandbox-client/sprites.ts, specifically the createSpritesSandboxClient instantiation pattern that this PR further changes the get method contract of.
  • 2witstudios/PageSpace#1672: Introduces the realtime PTY terminal flow in apps/realtime/src/index.ts that this PR augments with the ensureSpriteAwake resume step.

Poem

🐇 Hop, hop! No more relock to fuss,
The warm window's gone without much muss.
process.env reads direct and clean,
The sleepy sprite gets woken keen!
A single wake before the PTY—
Now hibernated sandboxes fly! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the two main changes: fixing the terminal kill_switch_off bug and removing resume-time relock logic from the sandbox lifecycle.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/sprites-terminal-killswitch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: e9042ec6a4

ℹ️ 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 apps/realtime/src/index.ts Outdated
// connecting. A fresh create was just warmed by the acquire's mkdir, so only
// resumes need this extra wake.
if (sandboxResult.resumed) {
await ensureSpriteAwake(sprite);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release the slot if terminal wake fails

When ensureSpriteAwake throws after acquireCodeExecutionSlot has succeeded (for example, a resumed hibernated Sprite exhausts the wake retry or the SDK errors), this rejection bypasses the only failure branch that calls releaseSlot() and the terminal handler never receives a releaseSlot callback to clean up. Because the concurrency semaphore is in-process, the user's slot remains held and later terminal/code execution can be denied with concurrency_limit; release the slot in a catch around this post-acquire wake path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bb8f21c. In makeTerminalCheckAuth, both sdk.getSprite(...) and ensureSpriteAwake(sprite) are now wrapped in a try/catch after the slot is acquired: on any throw it calls releaseSlot(), logs 'Terminal sandbox wake failed', and returns { ok:false, reason:'provision_failed' } — so a failed resume wake no longer leaks the in-process concurrency slot. Left open for verification.

} catch {
return '';
}
return process.env.SANDBOX_SESSION_SECRET ?? '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject short sandbox session secrets

This direct read is now used specifically in realtime, where the full env schema no longer runs, so a non-empty but too-short SANDBOX_SESSION_SECRET is accepted even though the schema requires at least 32 characters. acquireConversationSandbox/acquireTerminalSandbox only check that the secret is non-empty before deriving HMAC-based sandbox names, so a misconfigured weak secret enables sandbox acquisition instead of failing closed as the previous validated read did; keep the direct read but return '' unless the value meets the documented minimum.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in bb8f21c. getSandboxSessionSecret now returns '' for a secret shorter than 32 chars (return secret.length >= 32 ? secret : ''), so a too-short SANDBOX_SESSION_SECRET fail-closes exactly like an absent one rather than deriving HMAC session keys from a weak secret — restoring the web schema's >=32 guard on the realtime path. Added a regression assertion in env-reads.test.ts. Left open for verification.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/realtime/src/index.ts`:
- Around line 112-120: The ensureSpriteAwake(sprite) call in the resumed sprite
path can throw an error, which would exit the function before the execution slot
can be released back to the caller. Add error handling around the
ensureSpriteAwake(sprite) call to ensure that if it throws, the error is caught
and the slot release is still properly returned or executed before rethrowing or
handling the error, preventing the concurrency slot from being permanently
allocated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88328a87-d79c-4e7a-ac36-bbe1c3bc0134

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7e22c and e9042ec.

📒 Files selected for processing (11)
  • apps/realtime/src/index.ts
  • packages/lib/src/services/sandbox/__tests__/env-reads.test.ts
  • packages/lib/src/services/sandbox/__tests__/lifecycle.test.ts
  • packages/lib/src/services/sandbox/__tests__/session-manager.test.ts
  • packages/lib/src/services/sandbox/can-run-code.ts
  • packages/lib/src/services/sandbox/lifecycle.ts
  • packages/lib/src/services/sandbox/sandbox-client/__tests__/sprites.test.ts
  • packages/lib/src/services/sandbox/sandbox-client/sprites.ts
  • packages/lib/src/services/sandbox/sandbox-client/types.ts
  • packages/lib/src/services/sandbox/session-manager.ts
  • packages/lib/src/services/sandbox/terminal-session-manager.ts

Comment thread apps/realtime/src/index.ts
…ssion secret (review)

FIX A: in makeTerminalCheckAuth, the getSprite handle lookup and ensureSpriteAwake
both run after the concurrency slot is acquired and can throw (SDK error, or a
resumed Sprite exhausting its wake retries). Wrap both in a try/catch that releases
the slot, warns, and returns provision_failed so the in-process semaphore no longer
leaks a slot on wake failure.

FIX B: getSandboxSessionSecret now treats a non-empty but <32-char secret as unset
(returns ''), mirroring the web schema's >=32-char guard that realtime bypasses, so
acquisition fail-closes rather than deriving HMAC session keys from a weak secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBkG69PYNEYBYR6pF3TPRP
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.

1 participant