Skip to content

docs(sandbox): add POST /api/sandbox + GET /api/sandbox/status reference#192

Merged
sweetmantech merged 2 commits intomainfrom
docs/sandbox-loading
May 7, 2026
Merged

docs(sandbox): add POST /api/sandbox + GET /api/sandbox/status reference#192
sweetmantech merged 2 commits intomainfrom
docs/sandbox-loading

Conversation

@sweetmantech
Copy link
Copy Markdown
Collaborator

@sweetmantech sweetmantech commented May 7, 2026

Summary

Adds the OpenAPI spec and reference pages for the two session-scoped sandbox endpoints needed to drive chat's "loading sandbox..." UX on session entry: POST /api/sandbox (provision or resume) and GET /api/sandbox/status (poll readiness).

This is the docs half of the next route-by-route port from open-agents into api — sequel to PR #185 (GET /api/sessions/{sessionId}) and PR #186 (POST /api/sessions). The matching api PR will follow once this lands.

Why these two endpoints first

The chat product wants the open-agents UX where a new session lands on a page that shows "loading sandbox..." until a sandbox is bound and ready (restored from a per-org snapshot when one exists, freshly provisioned otherwise). That UX needs exactly two backend calls:

  1. POST /api/sandbox — trigger provision/restore on session entry
  2. GET /api/sandbox/status — poll until status === "active"

POST /api/sessions (already shipped) only creates the session row + initial chat — it does not spawn a sandbox. So without these two endpoints, chat cannot drive the loading state.

Scope: deliberately partial

The full open-agents /api/sandbox/* namespace also includes extend, activity, reconnect, and snapshot. Those are deferred to a follow-up docs PR alongside their api handlers — matching the lesson from PR #187 (don't document fields/endpoints api can't deliver yet). Each of the deferred routes is small and reuses the same session-scoped sandbox handle pattern, so adding them later is mechanical.

Provenance

Response and request shapes captured directly from open-agents production handlers:

  • apps/web/lib/sandbox/create-sandbox-handler.ts (POST handler)
  • apps/web/app/api/sandbox/status/route.ts (GET handler, including the SandboxStatusResponse type literal)

The lifecycle state enum mirrors the one already in Session schema in sessions.json (provisioning / active / hibernating / hibernated / restoring / archived / failed) so clients can reuse the same type across endpoints.

File layout

File Purpose
api-reference/openapi/sandbox.json OpenAPI 3.1 spec for the two endpoints + their schemas
api-reference/sandbox/create.mdx Frontmatter-only reference for POST /api/sandbox
api-reference/sandbox/status.mdx Frontmatter-only reference for GET /api/sandbox/status
docs.json New "Session Sandboxes" group under "Agents & Sandboxes" tab

Singular sandbox/ deliberately distinguishes these from the legacy account-scoped plural /api/sandboxes routes that already live at api-reference/sandboxes/. The two namespaces will coexist during the migration window.

Error envelope

Uses { status: "error", error } to match the api convention established by PR #185/#186 (Sessions). The open-agents handlers themselves emit just { error } — the api port will normalize to the api shape, which is what this docs spec describes.

Test plan

  • npx mintlify@latest dev renders without errors
  • New "Session Sandboxes" group appears under "Agents & Sandboxes" tab
  • "Create or restore session sandbox" page renders POST request body + 200/400/401/403/404/502 responses
  • "Get session sandbox status" page renders the sessionId query param + lifecycle envelope schema

What follows this PR

  1. api PR — handler under apps/api/app/api/sandbox/, Privy Bearer via validateAuthContext, Supabase via lib/supabase/sessions/* reusing the abstraction merged in #507
  2. open-agents PR — point UI at RECOUPABLE_API_BASE_URL + Privy Bearer for these two routes, delete the local handlers once preview is green
  3. Follow-up docs PR — extend / activity / reconnect / snapshot

🤖 Generated with Claude Code


Summary by cubic

Adds docs for the session-scoped sandbox APIs that power chat’s “loading sandbox…” flow on session entry: POST /api/sandbox (create or resume) and GET /api/sandbox/status (poll readiness). Includes an OpenAPI 3.1 spec and reference pages scoped to these two routes.

  • New Features
    • Added OpenAPI 3.1 spec for POST /api/sandbox and GET /api/sandbox/status.
    • Added reference pages: Create or restore session sandbox, Get session sandbox status.
    • Introduced a “Session Sandboxes” group in the Agents & Sandboxes docs.
    • Standardized error envelope to { status: "error", error } to match Sessions API.
    • Clearly separated singular session-scoped sandbox/ from legacy account-scoped /api/sandboxes; other routes (extend, activity, reconnect, snapshot) will follow later.
    • Made prose provider-agnostic; kept literal mode: "vercel" enum to document current response payload.

Written for commit 4049169. Summary will update on new commits.

Adds the OpenAPI spec and reference pages for the two session-scoped
sandbox endpoints needed to drive the chat "loading sandbox..." UX
when entering a session: provision/restore the sandbox, then poll
status until lifecycle reaches "active".

Scoped to just these two endpoints intentionally — extend, activity,
reconnect, and snapshot will follow in a separate docs PR once the
matching api handlers are queued. Captures the current open-agents
production response shape so the eventual frontend cutover is byte-
identical (createdAt/timeout/currentBranch/mode/timing for create,
and status/hasSnapshot/lifecycleVersion/lifecycle envelope for
status). Files distinguish from the legacy /api/sandboxes plural
namespace by living under api-reference/sandbox/ (singular) and
openapi/sandbox.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@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 9 minutes and 2 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 607dbd18-cc89-480d-90ab-3940f659854a

📥 Commits

Reviewing files that changed from the base of the PR and between 8cdd415 and 4049169.

📒 Files selected for processing (4)
  • api-reference/openapi/sandbox.json
  • api-reference/sandbox/create.mdx
  • api-reference/sandbox/status.mdx
  • docs.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/sandbox-loading

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.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="api-reference/openapi/sandbox.json">

<violation number="1" location="api-reference/openapi/sandbox.json:300">
P2: OpenAPI 3.1 schema uses `nullable`, which should be replaced with `type` unions including `"null"`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

},
"state": {
"type": "string",
"nullable": true,
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: OpenAPI 3.1 schema uses nullable, which should be replaced with type unions including "null".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-reference/openapi/sandbox.json, line 300:

<comment>OpenAPI 3.1 schema uses `nullable`, which should be replaced with `type` unions including `"null"`.</comment>

<file context>
@@ -0,0 +1,356 @@
+              },
+              "state": {
+                "type": "string",
+                "nullable": true,
+                "enum": [
+                  "provisioning",
</file context>

Comment thread api-reference/openapi/sandbox.json Outdated
Comment thread api-reference/openapi/sandbox.json Outdated
Addresses review feedback to keep the public docs provider-agnostic
in description text. Kept the literal `mode` enum value `"vercel"`
and its description since that documents the actual response payload
the server returns today — changing it would misrepresent the API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sweetmantech
Copy link
Copy Markdown
Collaborator Author

Pushed 4049169 addressing the review feedback:

Applied your two suggestions verbatim — dropped "Vercel Sandbox" → "Sandbox" in both flagged spots. Also caught one more "Vercel Sandbox" mention in the 502 response description (line 91) and applied the same intent there.

Kept the literal "vercel" enum value for mode (line 239) and its description (line 241) — that's the actual response payload the server returns today (mode: "vercel" in create-sandbox-handler.ts). Removing it would misrepresent the API contract. Happy to revisit if/when the server stops emitting that literal.

Skipped the cubic-dev-ai P2 about nullabletype: ["string", "null"]. The merged sessions.json uses nullable: true consistently, so matching that convention is more valuable than chasing the spec on this PR. If we want to migrate, it's a clean follow-up across both files.

@sweetmantech sweetmantech merged commit 2e9099a into main May 7, 2026
3 checks passed
@sweetmantech sweetmantech deleted the docs/sandbox-loading branch May 7, 2026 13:08
sweetmantech added a commit to recoupable/api that referenced this pull request May 7, 2026
…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>
sweetmantech added a commit to recoupable/api that referenced this pull request May 7, 2026
…open-agents (#522) (#524)

* 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



* 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



* 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)



* 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>
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