Skip to content

feat(api): cut over four read/provision endpoints to recoupable api#28

Closed
sweetmantech wants to merge 4 commits into
mainfrom
feat/cutover-sandbox-read-endpoints
Closed

feat(api): cut over four read/provision endpoints to recoupable api#28
sweetmantech wants to merge 4 commits into
mainfrom
feat/cutover-sandbox-read-endpoints

Conversation

@sweetmantech
Copy link
Copy Markdown
Contributor

@sweetmantech sweetmantech commented May 8, 2026

Summary

Replaces the local handler implementations of four endpoints with thin proxies that forward to the recoupable api. The Privy access token (already in the `privy-token` cookie) is re-emitted as `Authorization: Bearer` so api's `validateAuthContext` authenticates the same account.

Routes cut over (api parity confirmed across PRs #533–#535):

  • `POST /api/sandbox` (DELETE stays local — endpoint not yet ported)
  • `GET /api/sandbox/status`
  • `GET /api/sandbox/reconnect`
  • `GET /api/sessions/[sessionId]` (PATCH/DELETE stay local)

Branch routing (`branch` / `isNewBranch` body fields) is intentionally ignored going forward — sandboxes always provision on the repo's default branch.

What's deleted

  • `lib/sandbox/create-sandbox-handler.ts` — only caller was the POST route, which is now a proxy.
  • Three route-handler test files (`reconnect`, `status`, root) — their behavioral coverage is now in the api repo's test suite.

What's new

  • `lib/recoupable/forward-to-api.ts` — thin proxy helper. Reads `privy-token` cookie, forwards method/body/query to api with `Authorization: Bearer`, streams upstream response back. 401 when no cookie, 502 when upstream throws.
  • `lib/recoupable/forward-to-api.test.ts` — 5 focused tests (no cookie, GET forward, POST forward + Content-Type, upstream throw, status passthrough).

Test plan

  • `bun run check` — clean (format + lint)
  • `bun run --cwd apps/web typecheck` — clean
  • `bun test apps/web/lib/recoupable/forward-to-api.test.ts` — 5/5 pass
  • Smoke on preview: load a session in chat, exercise status polling and a reconnect, verify behavior matches main
  • Confirm `POST /api/sandbox` still provisions (already smoked end-to-end on the api side in #534, including in-sandbox `git config` verification)

Pre-existing failure note: `apps/web/app/workflows/chat-post-finish-usage.test.ts` also fails on `origin/main` — not introduced by this PR.

🤖 Generated with Claude Code


Summary by cubic

Shifted four read/provision endpoints to the recoupable API; the client now calls it directly with a Privy access token. Also inject explicit Vercel credentials so sandboxes target the intended project in prod.

  • New Features

    • Added lib/recoupable/fetch-recoup to call the API with Authorization: Bearer <privy-token>.
    • Added client-safe response types in lib/recoupable/sandbox-api-types.
  • Refactors

    • Removed local handlers for POST /api/sandbox, GET /api/sandbox/status, GET /api/sandbox/reconnect, and GET /api/sessions/[sessionId]; UI now uses fetchRecoup.
    • DELETE /api/sandbox and PATCH/DELETE /api/sessions/[sessionId] remain server-routed.
    • Updated use-sandbox-create, lib/sandbox/create-sandbox, and chat reconnect/status to fetch with a fresh Privy token; dropped branch/isNewBranch (provision on the repo’s default branch).
    • Deleted lib/recoupable/forward-to-api and related route tests; updated comments in auto-commit files.
    • Sandbox SDK now passes explicit VERCEL_TEAM_ID/VERCEL_PROJECT_ID/VERCEL_TOKEN when set and logs the credential source on each SDK call for diagnostics.

Written for commit 0a54075. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

Refactor

  • Sandbox server routes reorganized: provisioning and status/reconnect flows moved to a recoupable API; some server GET endpoints removed.

New Features

  • Client now uses Privy access tokens for sandbox creation, reconnection, and status polling.
  • Added a direct recoupable API fetch helper and shared client types for sandbox responses.

Replaces the local handler implementations with thin proxies that
forward to the recoupable api. The Privy access token (already in
the `privy-token` cookie) is re-emitted as `Authorization: Bearer`
so api's `validateAuthContext` can authenticate the same account.

Routes cut over:
- `POST /api/sandbox` (DELETE handler stays local — endpoint not
  yet ported)
- `GET /api/sandbox/status`
- `GET /api/sandbox/reconnect`
- `GET /api/sessions/[sessionId]` (PATCH/DELETE stay local)

The api side already has parity: gitUser plumbing (#534), failed-state
self-heal + paused-state hasSnapshot (#535), reconnect transient-error
preservation + lifecycle recovery + expires sync (#535).

Branch routing (`branch` / `isNewBranch` body fields) is intentionally
ignored going forward — sandboxes always provision on the repo's
default branch.

Changes:

- `lib/recoupable/forward-to-api.ts` (new) — thin proxy: reads
  `privy-token` cookie, forwards request method/body/query to the
  api endpoint with `Authorization: Bearer`, streams the upstream
  response back. 401 when no cookie, 502 when the upstream throws.
  Five-test focused vitest covering each path.
- `app/api/sandbox/route.ts` — POST collapses to a one-liner; DELETE
  unchanged.
- `app/api/sandbox/status/route.ts` — full handler replaced with a
  proxy. `SandboxStatusResponse` type kept exported so the web
  client's existing type imports still resolve.
- `app/api/sandbox/reconnect/route.ts` — same shape as status.
  `ReconnectResponse` + `ReconnectStatus` types kept exported.
- `app/api/sessions/[sessionId]/route.ts` — GET collapses to proxy;
  PATCH and DELETE stay local.
- `lib/sandbox/create-sandbox-handler.ts` — deleted (only caller was
  the POST route, which is now a proxy).
- Three route-handler test files deleted (their behavioral coverage
  is now in the api repo's test suite).
- Two stale comment references in `auto-commit-direct.{ts,test.ts}`
  updated to point at the new owner of gitUser config.

Verification:
- `bun run check` — clean (format + lint)
- `bun run --cwd apps/web typecheck` — clean
- `bun test apps/web/lib/recoupable/forward-to-api.test.ts` — 5/5 pass
- Pre-existing `chat-post-finish-usage.test.ts` failure also reproduces
  on origin/main; not introduced here.

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

vercel Bot commented May 8, 2026

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

Project Deployment Actions Updated (UTC)
open-agents Ready Ready Preview May 8, 2026 0:50am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0ef4ea5-810d-4aec-9ad1-bf852c679883

📥 Commits

Reviewing files that changed from the base of the PR and between e89c6c2 and 0a54075.

📒 Files selected for processing (1)
  • packages/sandbox/vercel/sandbox.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/sandbox/vercel/sandbox.ts

📝 Walkthrough

Walkthrough

This PR migrates sandbox provisioning and status management from server-routed API endpoints to direct client-side calls to the recoupable API. Server routes for reconnect, status, and sandbox provisioning are deleted; clients now authenticate via Privy bearer tokens and call recoupable endpoints directly through a new fetchRecoup helper. Type contracts, hook signatures, and documentation are updated accordingly.

Changes

Sandbox API Client-Side Migration

Layer / File(s) Summary
Response Type Contracts
apps/web/lib/recoupable/sandbox-api-types.ts
New module defines SandboxStatusResponse, ReconnectStatus, and ReconnectResponse types for recoupable API responses, with lifecycle timing and status fields.
Client-Side Fetch Helper
apps/web/lib/recoupable/fetch-recoup.ts
New fetchRecoup helper wraps browser fetch to call recoupable API endpoints with explicit Privy bearer token authentication.
Remove Server-Routed Endpoints
apps/web/app/api/sandbox/reconnect/route.ts, route.test.ts
apps/web/app/api/sandbox/status/route.ts, route.test.ts
apps/web/app/api/sandbox/route.ts, route.test.ts
apps/web/app/api/sessions/[sessionId]/route.ts
apps/web/lib/sandbox/create-sandbox-handler.ts
Delete /api/sandbox/reconnect, /api/sandbox/status, POST /api/sandbox, GET /api/sessions/[sessionId], and handleCreateSandboxRequest handler. All associated tests removed. Docs updated to note POST /api/sandbox is no longer served here.
Update Client Calls with Privy Auth
apps/web/lib/sandbox/create-sandbox.ts
apps/web/hooks/use-sandbox-create.ts
apps/web/app/sessions/[sessionId]/chats/[chatId]/session-chat-context.tsx
createSandbox now takes accessToken and calls recoupable API via fetchRecoup; removes branch/isNewBranch params. Hook obtains token via usePrivy().getAccessToken() and throws if unavailable. Chat context reconnect and status sync now fetch tokens and use fetchRecoup instead of direct fetch.
Vercel Credentials Configuration
packages/sandbox/vercel/sandbox.ts
Add getExplicitVercelCredentials() to read VERCEL_TEAM_ID, VERCEL_PROJECT_ID, VERCEL_TOKEN from environment and spread into SDK create/get config.
Documentation Updates
apps/web/lib/chat/auto-commit-direct.ts, .test.ts
Update comments to state git author identity comes from recoupable /api/sandbox instead of Privy session.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • recoupable/open-agents#5: Related changes to sandbox creation and recoupable API integration; overlaps with create-sandbox handler and client token forwarding.
  • recoupable/open-agents#3: Related Privy-based auth migration and client-side token plumbing for sandbox operations.
  • recoupable/open-agents#13: Related additions of Privy/Recoup token plumbing in chat/session flows and client fetch changes.

Poem

🐰 I hopped from route to client-side,

Tokens clutched, no servers to bide,
fetchRecoup sings with Bearer bright,
Types in paw, the calls take flight,
Vercel keys snug — the sandbox hops light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: cutting over four API endpoints (sandbox POST, sandbox/status GET, sandbox/reconnect GET, sessions GET) to call the recoupable API instead of local handlers.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cutover-sandbox-read-endpoints

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
Contributor Author

Choose a reason for hiding this comment

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

KISS

  • actual: forwarding API requests.
  • required: update the client to directly call Recoup API. use getAccessToken from privy to get bearer auth.

Comment thread apps/web/app/api/sandbox/status/route.ts Outdated
Comment thread apps/web/app/api/sandbox/route.ts
Comment thread apps/web/app/api/sessions/[sessionId]/route.ts
* @returns A Response from api, or 401 if the user has no Privy
* token, or 502 if the upstream call fails.
*/
export async function forwardToApi(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

YAGNI - this file can be removed.

Reworked per review: scrap the same-origin proxy and have the
browser hit recoupable api directly with a Privy access token from
`usePrivy().getAccessToken()` as `Authorization: Bearer`. Smaller
surface, one fewer network hop, no server-side fan-out from
open-agents.

Cutover behavior is the same as before:
- `POST /api/sandbox` (provision)
- `GET /api/sandbox/status`
- `GET /api/sandbox/reconnect`
- `GET /api/sessions/[sessionId]` (server components already use
  `getSessionByIdCached` directly; no client GET callers existed)

Branch routing (`branch` / `isNewBranch`) stays ignored — sandboxes
provision on the repo's default branch.

Changes:

- New `lib/recoupable/fetch-recoup.ts` — tiny client helper:
  `fetch(${RECOUPABLE_API_BASE_URL}${path}, { Authorization: Bearer })`.
- New `lib/recoupable/sandbox-api-types.ts` — `SandboxStatusResponse`,
  `ReconnectResponse`, `ReconnectStatus` types live in a client-safe
  module so `"use client"` components can import them without
  pulling in the deleted route files.
- `lib/sandbox/create-sandbox.ts` — takes `accessToken: string` as
  first arg, uses `fetchRecoup`, drops `branch` / `isNewBranch`
  params (ignored by api anyway).
- `hooks/use-sandbox-create.ts` — fetches a Privy token via
  `getAccessToken()` and forwards it; `SessionFields` shrinks to
  `{ id, cloneUrl }`.
- `session-chat-context.tsx` — adds `usePrivy()`; the reconnect
  probe and status sync now call `fetchRecoup` with a fresh token
  on every request; useCallback deps include `getAccessToken`.

Deleted:
- `lib/recoupable/forward-to-api.ts` + its test
- `app/api/sandbox/status/route.ts` (entire file — no remaining caller)
- `app/api/sandbox/reconnect/route.ts` (entire file — same)
- `POST` handler from `app/api/sandbox/route.ts` (DELETE remains —
  endpoint not yet ported)
- `GET` handler from `app/api/sessions/[sessionId]/route.ts`
  (PATCH/DELETE remain)

Verification:
- `bun run check` — clean (format + lint)
- `bun run --cwd apps/web typecheck` — clean
- Pre-existing `chat-post-finish-usage.test.ts` failure unchanged
  (also fails on `origin/main`)

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

Pushed `126b70d` reworking the cutover per review. Replaces the same-origin proxy with client-direct calls to recoupable api using `usePrivy().getAccessToken()` as `Authorization: Bearer`.

Addresses all five comments:

Comment Resolution
/sandbox/reconnect/route.ts proxy File deleted; `session-chat-context.tsx` calls api directly.
/sandbox/status/route.ts proxy File deleted; `session-chat-context.tsx` calls api directly.
/sandbox POST proxy POST handler removed (DELETE stays); `createSandbox` calls api directly.
/sessions/[id] GET proxy GET handler removed (PATCH/DELETE stay). Server components already use `getSessionByIdCached` directly; no client GET callers existed.
`forward-to-api.ts` YAGNI File + test deleted. Replaced with a 12-line `fetch-recoup.ts` that just adds the Bearer header.

New surface:

  • `lib/recoupable/fetch-recoup.ts` — `(token, path, init?) => fetch(${API}${path}, { ...init, Authorization: Bearer ${token} })`
  • `lib/recoupable/sandbox-api-types.ts` — moved `SandboxStatusResponse` / `ReconnectResponse` / `ReconnectStatus` to a client-safe module (the route files where they used to live are gone).

Updated callers (each pulls a fresh token per request):

  • `hooks/use-sandbox-create.ts` — `getAccessToken` from `usePrivy()`, forwards to `createSandbox`
  • `session-chat-context.tsx` — `getAccessToken` from `usePrivy()`; reconnect probe + status sync use `fetchRecoup`

`bun run check` and `tsc --noEmit` clean. Pre-existing `chat-post-finish-usage.test.ts` failure unchanged (also on `origin/main`).

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 11 files (changes from recent commits).

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="apps/web/lib/recoupable/fetch-recoup.ts">

<violation number="1" location="apps/web/lib/recoupable/fetch-recoup.ts:27">
P2: Header merging is incorrect for non-object `RequestInit.headers` values; normalize with `new Headers(init.headers)` before setting Authorization.</violation>
</file>

You're on the cubic free plan with 19 free PR reviews remaining this month. Upgrade for unlimited reviews.

path: string,
init: RequestInit = {},
): Promise<Response> {
return fetch(`${RECOUPABLE_API_BASE_URL}${path}`, {
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: Header merging is incorrect for non-object RequestInit.headers values; normalize with new Headers(init.headers) before setting Authorization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/recoupable/fetch-recoup.ts, line 27:

<comment>Header merging is incorrect for non-object `RequestInit.headers` values; normalize with `new Headers(init.headers)` before setting Authorization.</comment>

<file context>
@@ -0,0 +1,34 @@
+  path: string,
+  init: RequestInit = {},
+): Promise<Response> {
+  return fetch(`${RECOUPABLE_API_BASE_URL}${path}`, {
+    ...init,
+    headers: {
</file context>

The Vercel Sandbox SDK prefers VERCEL_OIDC_TOKEN (auto-injected on
every Vercel deployment, scoped to that deployment's project) over
env-var auto-detection. So in deployed environments, sandboxes always
land in the deploying project's namespace — even when
VERCEL_TEAM_ID / VERCEL_PROJECT_ID / VERCEL_TOKEN are explicitly set
in the environment for a different project.

Patch: when all three explicit credentials are present in `process.env`,
spread them into every `VercelSandboxSDK.create` and `.get` call. This
forces the SDK to bypass OIDC and use the access-token path, which
honors the explicit project ID. Falls back to OIDC when env vars are
absent (the default path).

Unblocks recoup/open-agents pointing its sandbox traffic at recoup/api's
project, so api can provision sandboxes that open-agents' workflows
and IDE handlers can also see and manipulate (sandbox names are unique
per project per the Vercel docs, but a team-scoped access token can
target any project in the team explicitly).

Tests: 29/29 pass. Lint + format + typecheck clean.

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

@coderabbitai coderabbitai Bot left a comment

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/web/lib/recoupable/sandbox-api-types.ts`:
- Around line 9-40: Replace the plain TypeScript types SandboxStatusResponse,
ReconnectResponse and ReconnectStatus with Zod schemas (e.g.,
sandboxStatusSchema, reconnectSchema and reconnectStatusSchema) and export their
inferred types via z.infer<typeof ...>; then update the callers that currently
cast JSON responses—attemptReconnection, syncSandboxStatus and the
sandbox-creation consumer (where response is cast to { mode: string } &
SandboxInfo)—to call .parse() on the parsed JSON using the appropriate schema so
malformed responses throw and are handled instead of silently corrupting state.
Ensure the lifecycle nested object is validated in both schemas and optional
fields like expiresAt are marked optional in the reconnect schema.
🪄 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: bf17e23a-d90a-424a-a3f8-13ce8044a620

📥 Commits

Reviewing files that changed from the base of the PR and between f6b05d8 and e89c6c2.

📒 Files selected for processing (16)
  • apps/web/app/api/sandbox/reconnect/route.test.ts
  • apps/web/app/api/sandbox/reconnect/route.ts
  • apps/web/app/api/sandbox/route.test.ts
  • apps/web/app/api/sandbox/route.ts
  • apps/web/app/api/sandbox/status/route.test.ts
  • apps/web/app/api/sandbox/status/route.ts
  • apps/web/app/api/sessions/[sessionId]/route.ts
  • apps/web/app/sessions/[sessionId]/chats/[chatId]/session-chat-context.tsx
  • apps/web/hooks/use-sandbox-create.ts
  • apps/web/lib/chat/auto-commit-direct.test.ts
  • apps/web/lib/chat/auto-commit-direct.ts
  • apps/web/lib/recoupable/fetch-recoup.ts
  • apps/web/lib/recoupable/sandbox-api-types.ts
  • apps/web/lib/sandbox/create-sandbox-handler.ts
  • apps/web/lib/sandbox/create-sandbox.ts
  • packages/sandbox/vercel/sandbox.ts
💤 Files with no reviewable changes (6)
  • apps/web/app/api/sandbox/status/route.ts
  • apps/web/app/api/sandbox/reconnect/route.test.ts
  • apps/web/app/api/sandbox/status/route.test.ts
  • apps/web/lib/sandbox/create-sandbox-handler.ts
  • apps/web/app/api/sandbox/route.test.ts
  • apps/web/app/api/sandbox/reconnect/route.ts

Comment on lines +9 to +40
export type SandboxStatusResponse = {
status: "active" | "no_sandbox";
hasSnapshot: boolean;
lifecycleVersion: number;
lifecycle: {
serverTime: number;
state: string | null;
lastActivityAt: number | null;
hibernateAfter: number | null;
sandboxExpiresAt: number | null;
};
};

export type ReconnectStatus =
| "connected"
| "expired"
| "not_found"
| "no_sandbox";

export type ReconnectResponse = {
status: ReconnectStatus;
hasSnapshot: boolean;
/** Timestamp (ms) when sandbox expires. Only present when status is "connected". */
expiresAt?: number;
lifecycle: {
serverTime: number;
state: string | null;
lastActivityAt: number | null;
hibernateAfter: number | null;
sandboxExpiresAt: number | null;
};
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Define these as Zod schemas and derive types via z.infer.

These shapes describe responses from the recoupable api, which is now a separate service the browser talks to directly — so contract drift on the api side will silently corrupt client state (e.g. applyLifecycleTiming reading undefined fields, or setSandboxInfo({ createdAt, timeout }) storing NaN) instead of failing fast. The downstream callers all rely on this:

  • session-chat-context.tsx attemptReconnection (line 454): (await response.json()) as ReconnectResponse
  • session-chat-context.tsx syncSandboxStatus (line 535): (await response.json()) as SandboxStatusResponse
  • create-sandbox.ts (line 92): (await response.json()) as { mode: string } & SandboxInfo

Switching to Zod schemas here and .parse()-ing the body in each consumer gives you a real boundary check.

♻️ Proposed schema definitions
+import { z } from "zod";
+
+const lifecycleSchema = z.object({
+  serverTime: z.number(),
+  state: z.string().nullable(),
+  lastActivityAt: z.number().nullable(),
+  hibernateAfter: z.number().nullable(),
+  sandboxExpiresAt: z.number().nullable(),
+});
+
+export const sandboxStatusResponseSchema = z.object({
+  status: z.enum(["active", "no_sandbox"]),
+  hasSnapshot: z.boolean(),
+  lifecycleVersion: z.number(),
+  lifecycle: lifecycleSchema,
+});
+export type SandboxStatusResponse = z.infer<typeof sandboxStatusResponseSchema>;
+
+export const reconnectStatusSchema = z.enum([
+  "connected",
+  "expired",
+  "not_found",
+  "no_sandbox",
+]);
+export type ReconnectStatus = z.infer<typeof reconnectStatusSchema>;
+
+export const reconnectResponseSchema = z.object({
+  status: reconnectStatusSchema,
+  hasSnapshot: z.boolean(),
+  /** Timestamp (ms) when sandbox expires. Only present when status is "connected". */
+  expiresAt: z.number().optional(),
+  lifecycle: lifecycleSchema,
+});
+export type ReconnectResponse = z.infer<typeof reconnectResponseSchema>;
-export type SandboxStatusResponse = { ... };
-export type ReconnectStatus = ...;
-export type ReconnectResponse = { ... };

Then in session-chat-context.tsx:

- const data = (await response.json()) as ReconnectResponse;
+ const data = reconnectResponseSchema.parse(await response.json());
- const data = (await response.json()) as SandboxStatusResponse;
+ const data = sandboxStatusResponseSchema.parse(await response.json());

As per coding guidelines: "Use Zod schemas for validation and derive types with z.infer".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type SandboxStatusResponse = {
status: "active" | "no_sandbox";
hasSnapshot: boolean;
lifecycleVersion: number;
lifecycle: {
serverTime: number;
state: string | null;
lastActivityAt: number | null;
hibernateAfter: number | null;
sandboxExpiresAt: number | null;
};
};
export type ReconnectStatus =
| "connected"
| "expired"
| "not_found"
| "no_sandbox";
export type ReconnectResponse = {
status: ReconnectStatus;
hasSnapshot: boolean;
/** Timestamp (ms) when sandbox expires. Only present when status is "connected". */
expiresAt?: number;
lifecycle: {
serverTime: number;
state: string | null;
lastActivityAt: number | null;
hibernateAfter: number | null;
sandboxExpiresAt: number | null;
};
};
import { z } from "zod";
const lifecycleSchema = z.object({
serverTime: z.number(),
state: z.string().nullable(),
lastActivityAt: z.number().nullable(),
hibernateAfter: z.number().nullable(),
sandboxExpiresAt: z.number().nullable(),
});
export const sandboxStatusResponseSchema = z.object({
status: z.enum(["active", "no_sandbox"]),
hasSnapshot: z.boolean(),
lifecycleVersion: z.number(),
lifecycle: lifecycleSchema,
});
export type SandboxStatusResponse = z.infer<typeof sandboxStatusResponseSchema>;
export const reconnectStatusSchema = z.enum([
"connected",
"expired",
"not_found",
"no_sandbox",
]);
export type ReconnectStatus = z.infer<typeof reconnectStatusSchema>;
export const reconnectResponseSchema = z.object({
status: reconnectStatusSchema,
hasSnapshot: z.boolean(),
/** Timestamp (ms) when sandbox expires. Only present when status is "connected". */
expiresAt: z.number().optional(),
lifecycle: lifecycleSchema,
});
export type ReconnectResponse = z.infer<typeof reconnectResponseSchema>;
🤖 Prompt for 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.

In `@apps/web/lib/recoupable/sandbox-api-types.ts` around lines 9 - 40, Replace
the plain TypeScript types SandboxStatusResponse, ReconnectResponse and
ReconnectStatus with Zod schemas (e.g., sandboxStatusSchema, reconnectSchema and
reconnectStatusSchema) and export their inferred types via z.infer<typeof ...>;
then update the callers that currently cast JSON responses—attemptReconnection,
syncSandboxStatus and the sandbox-creation consumer (where response is cast to {
mode: string } & SandboxInfo)—to call .parse() on the parsed JSON using the
appropriate schema so malformed responses throw and are handled instead of
silently corrupting state. Ensure the lifecycle nested object is validated in
both schemas and optional fields like expiresAt are marked optional in the
reconnect schema.

Temporary diagnostic to verify the e89c6c2 cross-project credential
patch is actually taking effect at runtime. Logs which credentials are
detected in process.env (without leaking the token) and whether the
SDK call is going to use explicit creds vs falling back to OIDC.

Will revert once we confirm the patch is wired correctly.

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