feat(api): cut over four read/provision endpoints to recoupable api#28
feat(api): cut over four read/provision endpoints to recoupable api#28sweetmantech wants to merge 4 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis 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 ChangesSandbox API Client-Side Migration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
KISS
- actual: forwarding API requests.
- required: update the client to directly call Recoup API. use getAccessToken from privy to get bearer auth.
| * @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( |
There was a problem hiding this comment.
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>
|
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:
New surface:
Updated callers (each pulls a fresh token per request):
`bun run check` and `tsc --noEmit` clean. Pre-existing `chat-post-finish-usage.test.ts` failure unchanged (also on `origin/main`). |
There was a problem hiding this comment.
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}`, { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (16)
apps/web/app/api/sandbox/reconnect/route.test.tsapps/web/app/api/sandbox/reconnect/route.tsapps/web/app/api/sandbox/route.test.tsapps/web/app/api/sandbox/route.tsapps/web/app/api/sandbox/status/route.test.tsapps/web/app/api/sandbox/status/route.tsapps/web/app/api/sessions/[sessionId]/route.tsapps/web/app/sessions/[sessionId]/chats/[chatId]/session-chat-context.tsxapps/web/hooks/use-sandbox-create.tsapps/web/lib/chat/auto-commit-direct.test.tsapps/web/lib/chat/auto-commit-direct.tsapps/web/lib/recoupable/fetch-recoup.tsapps/web/lib/recoupable/sandbox-api-types.tsapps/web/lib/sandbox/create-sandbox-handler.tsapps/web/lib/sandbox/create-sandbox.tspackages/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
| 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; | ||
| }; | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.tsxattemptReconnection(line 454):(await response.json()) as ReconnectResponsesession-chat-context.tsxsyncSandboxStatus(line 535):(await response.json()) as SandboxStatusResponsecreate-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.
| 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>
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):
Branch routing (`branch` / `isNewBranch` body fields) is intentionally ignored going forward — sandboxes always provision on the repo's default branch.
What's deleted
What's new
Test plan
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
lib/recoupable/fetch-recoupto call the API withAuthorization: Bearer <privy-token>.lib/recoupable/sandbox-api-types.Refactors
POST /api/sandbox,GET /api/sandbox/status,GET /api/sandbox/reconnect, andGET /api/sessions/[sessionId]; UI now usesfetchRecoup.DELETE /api/sandboxandPATCH/DELETE /api/sessions/[sessionId]remain server-routed.use-sandbox-create,lib/sandbox/create-sandbox, and chat reconnect/status to fetch with a fresh Privy token; droppedbranch/isNewBranch(provision on the repo’s default branch).lib/recoupable/forward-to-apiand related route tests; updated comments in auto-commit files.VERCEL_TEAM_ID/VERCEL_PROJECT_ID/VERCEL_TOKENwhen 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
New Features