Skip to content

promote: test → main (POST /api/sandbox + GET /api/sandbox/status)#524

Merged
sweetmantech merged 1 commit intomainfrom
test
May 7, 2026
Merged

promote: test → main (POST /api/sandbox + GET /api/sandbox/status)#524
sweetmantech merged 1 commit intomainfrom
test

Conversation

@sweetmantech
Copy link
Copy Markdown
Contributor

@sweetmantech sweetmantech commented May 7, 2026

Promotes the just-landed sandbox routes from test to main.

What lands on main

Verification

🤖 Generated with Claude Code


Summary by cubic

Promotes the sandbox API from test to main: POST /api/sandbox provisions or resumes a session-bound sandbox, and GET /api/sandbox/status reports lifecycle to power the chat “loading sandbox…” UX.

  • New Features

    • Added POST /api/sandbox: validates GitHub repo URL, authenticates, derives deterministic sandboxName from sessionId for idempotent resume, persists sandbox_state/lifecycle, clears stale snapshot fields, and returns { createdAt, timeout, currentBranch, mode, timing }. Uses a 30m default timeout, port 3000, and forwards GITHUB_TOKEN when set for private repos.
    • Added GET /api/sandbox/status: DB-only; returns { status, hasSnapshot, lifecycleVersion, lifecycle }. Reports active only when runtime metadata exists and the sandbox isn’t expired (10s buffer).
  • Bug Fixes

    • Fixed loading-state regression by treating the type-only stub { type: "vercel" } as no_sandbox until a real sandboxName is present.
    • Return 400 for invalid GitHub URLs to avoid confusing provider errors.
    • Dropped branch and isNewBranch from POST /api/sandbox; currentBranch now reflects the resolved sandbox state (per docs docs(sandbox): drop branch + isNewBranch from POST /api/sandbox docs#194).

Written for commit 42c701c. Summary will update on new commits.

…open-agents (#522)

* feat(sandbox): port POST /api/sandbox + GET /api/sandbox/status from open-agents

Implements the two session-scoped sandbox endpoints required to drive
the chat "loading sandbox..." UX on session entry — matching the
contract documented in recoupable/docs#192 (now merged on main).

POST /api/sandbox provisions or resumes a Sandbox via the abstraction
inlined in #507. When sessionId is supplied, the deterministic
sandboxName ensures resume idempotency and the resolved sandbox state
is persisted onto the session row (sandbox_state, lifecycle_state =
"active", lifecycle_version bumped, sandbox_expires_at,
last_activity_at) so subsequent GET /api/sandbox/status calls report
the sandbox as active.

GET /api/sandbox/status is DB-only — reads the session row, computes
status as "active" when sandbox_state is set and not expired (10s
buffer to match open-agents), otherwise "no_sandbox". hasSnapshot is
true when snapshot_url is set. Mirrors the lifecycle envelope shape
from open-agents so the frontend cutover is byte-identical.

Files follow existing api conventions:
- Route shells in app/api/sandbox/ delegate to handlers in lib/sandbox/
- Auth via validateAuthContext (Privy Bearer or x-api-key)
- Validation via Zod (validateCreateSandboxBody)
- Supabase ops in lib/supabase/sessions/ (one fn per file)
- Error envelope { status: "error", error } matches sessions PRs

TDD red → green:
- 7 new test files added covering validator, helper, Supabase wrapper,
  both handlers, and the two route shells
- 30 new tests, all passing (was 2461, now 2491)
- pnpm lint:check clean

Out of scope (deferred to follow-up PRs):
- Org-snapshot lookup / kickBuildOrgSnapshotWorkflow (cold-start opt)
- Skill installation (installSessionGlobalSkills)
- Lifecycle workflow kick (no workflow infra in api yet)
- DELETE /api/sandbox + PUT /api/sandbox/snapshot (no UI callers
  identified during the open-agents grep audit)
- /api/sandbox/{extend,activity,reconnect,snapshot} sub-routes — to
  follow once these two land and the chat UX is validated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sandbox): treat type-stub sandbox_state as no_sandbox in /status

Smoke test against the preview deployment caught a regression that
defeated the entire loading-state UX this PR exists to enable: GET
/api/sandbox/status reported `"active"` immediately after POST
/api/sessions, before any sandbox had been provisioned.

Root cause: POST /api/sessions (PR #515) inserts `sandbox_state` as
the type stub `{ type: "vercel" }`. The previous `isSandboxActive`
check `if (!row.sandbox_state) return false` saw a truthy object and
fell through; with `sandbox_expires_at = null` (no expiry yet), the
function returned true.

Fix: introduce `hasRuntimeSandboxState(state)` that distinguishes the
type stub from real runtime metadata by requiring a non-empty
`sandboxName` (set by `getSessionSandboxName(sessionId)` in POST
/api/sandbox and preserved by the abstraction's `getState()`).
Mirrors open-agents' equivalent helper.

TDD red → green:
- Regression test pinned to the exact production scenario
  (sandbox_state = {type:"vercel"}, sandbox_expires_at = null,
  lifecycle_state = "provisioning") asserting status=no_sandbox
- Companion test asserting status=active once sandboxName is set
- 6 unit tests for the new helper covering null/undefined, scalars,
  type stub, populated state, and empty-string sandboxName edge case
- Confirmed RED before implementing, GREEN after
- Suite: 2491 → 2499 (+8 new tests), pnpm lint:check clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(sandbox): SRP/KISS extractions + Tier 1 correctness fixes

Addresses review feedback on PR #522 and the "missing from open-agents"
audit:

User-flagged review comments:
- SRP: extract `buildSource` to lib/sandbox/buildSource.ts
- YAGNI: drop `isNewBranch` from POST /api/sandbox — chat never sets it
  (note: docs PR #192 still documents it; will open follow-up docs PR
   to drop from sandbox.json)
- SRP: extract `isoToEpochMs` to lib/sandbox/isoToEpochMs.ts
- SRP: extract `buildLifecycle` to lib/sandbox/buildLifecycle.ts
- SRP: extract `isSandboxActive` to lib/sandbox/isSandboxActive.ts
- KISS: rename lib/supabase/sessions/updateSessionSandboxState.ts ->
  updateSession.ts, generalize signature to (id, TablesUpdate<"sessions">)

Tier 1 correctness gaps from the open-agents comparison:
1. GitHub URL validation via parseGitHubRepoUrl in
   validateCreateSandboxBody — bad URLs now return a clean 400 instead
   of falling through to a confusing 502 from the sandbox provider
2. Service GitHub token plumbed into connectSandbox options via new
   lib/github/getServiceGithubToken.ts — private repos can now clone
3. snapshot_url + snapshot_created_at cleared on fresh provision so
   GET /api/sandbox/status no longer surfaces stale snapshot URLs from
   prior runs

TDD red -> green:
- 5 new unit-test files for the extracted helpers (buildSource,
  isoToEpochMs, buildLifecycle, isSandboxActive, getServiceGithubToken)
- updateSession.test.ts replaces updateSessionSandboxState.test.ts
- Updated validator + handler tests for the contract changes
  (drop isNewBranch, add bad-URL 400 cases, assert githubToken plumbing,
   assert snapshot_url/snapshot_created_at: null in update payload)
- Confirmed RED before each implementation
- Suite: 2499 -> 2516 (+17 net new tests), pnpm lint:check clean

Files net delta: -241 / +70 lines (extractions + handler shrinks)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(sandbox): drop branch from POST /api/sandbox contract

YAGNI/KISS per review feedback — chat always works off the repo's
default branch, so the explicit `branch` input adds no value.

- Drop `branch` from createSandboxBodySchema
- Inline the now-trivial source object in createSandboxHandler
  (`{ repo: body.repoUrl }`) and delete `lib/sandbox/buildSource.ts`
  + its test
- Read `currentBranch` for the response from the sandbox handle's
  own `currentBranch` property (whatever the SDK actually checked
  out), falling back to "main" — no longer derives from a request
  field that no longer exists

Tests: TDD red -> green.
- Validator test asserts `branch` is stripped from the body even
  if a client still sends it
- Handler test asserts `currentBranch` in the response comes from
  `sandbox.currentBranch` (mocked to "release/v2") not from input
- Suite stays at 2516 (-1 from buildSource.test deletion +1 new
  currentBranch test)

Pairs with docs PR recoupable/docs#194 (merged) which already
removed `branch` and `isNewBranch` from the published OpenAPI spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview May 7, 2026 4:22pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 35 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4293cd74-53ba-46c1-9f21-35c6a8c672e9

📥 Commits

Reviewing files that changed from the base of the PR and between 2c83920 and 42c701c.

⛔ Files ignored due to path filters (12)
  • app/api/sandbox/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • app/api/sandbox/status/__tests__/route.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by app/**
  • lib/github/__tests__/getServiceGithubToken.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/buildLifecycle.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/createSandboxHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/getSandboxStatusHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/getSessionSandboxName.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/hasRuntimeSandboxState.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/isSandboxActive.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/isoToEpochMs.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/sandbox/__tests__/validateCreateSandboxBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/sessions/__tests__/updateSession.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (12)
  • app/api/sandbox/route.ts
  • app/api/sandbox/status/route.ts
  • lib/github/getServiceGithubToken.ts
  • lib/sandbox/buildLifecycle.ts
  • lib/sandbox/createSandboxHandler.ts
  • lib/sandbox/getSandboxStatusHandler.ts
  • lib/sandbox/getSessionSandboxName.ts
  • lib/sandbox/hasRuntimeSandboxState.ts
  • lib/sandbox/isSandboxActive.ts
  • lib/sandbox/isoToEpochMs.ts
  • lib/sandbox/validateCreateSandboxBody.ts
  • lib/supabase/sessions/updateSession.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test

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 and usage tips.

@sweetmantech sweetmantech merged commit 7cab27a into main May 7, 2026
7 checks passed
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