feat: per-org base snapshots for fast sandbox startup#9
Conversation
On a cold sandbox create for an org, look up a per-org base snapshot by
name (the org repo name, e.g. `org-rostrum-pacific-<uuid>`). If one
exists, boot from it and run `git fetch --depth=1 && git reset --hard
origin/<branch>` instead of a full clone. If none exists, fall back to
the current full-clone path and fire a background Vercel Workflow to
build the snapshot for next time.
Target outcome (measured in a follow-up): 75s git_clone phase → ~3s
fetch+reset, taking new-sandbox startup from ~76s to ~5s.
Approach:
- No DB changes. Snapshots are looked up via `Snapshot.list({ name })`
using the build-sandbox name as the filter key. No refresh scheduling:
drift is absorbed by the per-session fetch+reset step.
- `packages/sandbox`: new `Source.prebuilt` flag; when set, skip the
initial git clone and run fetch+reset. New `ConnectOptions.forceCreate`
flag bypasses reconnect-first for named ephemeral build sandboxes.
- `refreshBaseSnapshot` gains `sandboxName` + `githubToken` options so
build sandboxes can be named (so their snapshots are filter-able) and
can authenticate clones via credential brokering.
- `apps/web`: new `extractOrgRepoName` parses the repo name from the
clone URL; `findOrgSnapshot` queries the SDK; `kickBuildOrgSnapshotWorkflow`
starts the async builder; `buildOrgSnapshotWorkflow` clones the org
repo shallowly inside a build sandbox and snapshots it.
- First session for an org is still the slow path (transparent fallback);
every subsequent session hits the per-org snapshot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughIntroduces a new workflow system for building and caching organization-specific base snapshots, integrating with sandbox creation to detect existing org snapshots or trigger their background construction. Changes support a "prebuilt" mode for sandboxes that reuses pre-cloned snapshots via git fetch/reset instead of recloning. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Handler as createSandboxHandler
participant Extractor as extractOrgRepoName
participant Finder as findOrgSnapshot
participant Kicker as kickBuildOrgSnapshotWorkflow
participant Workflow as buildOrgSnapshotWorkflow
participant Snapshot as Snapshot Service
Client->>Handler: Create sandbox (repoUrl)
Handler->>Extractor: Extract org/repo from URL
Extractor-->>Handler: org name
alt Org name found
Handler->>Finder: Query for existing org snapshot
Finder->>Snapshot: List snapshots by name
Snapshot-->>Finder: snapshots (or error)
alt Snapshot exists and created
Finder-->>Handler: snapshot ID
Handler-->>Client: Sandbox with prebuilt source
else No snapshot found
Finder-->>Handler: null
Handler->>Kicker: Trigger workflow async
Kicker->>Workflow: Start buildOrgSnapshotWorkflow
Workflow->>Snapshot: Clone repo, refreshBaseSnapshot
Snapshot-->>Workflow: snapshot ID
Handler-->>Client: Sandbox with default snapshot (workflow building in background)
end
else No org name
Handler-->>Client: Sandbox with default snapshot
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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.
Actionable comments posted: 5
🧹 Nitpick comments (3)
packages/sandbox/vercel/snapshot-refresh.ts (1)
99-115: LGTM — option plumbing is correct.
sandboxNamegatingforceCreatecorrectly routes throughVercelSandbox.create()perconnect.ts:128-147, ensuring the build sandbox tags snapshots with the org name for laterSnapshot.list({ name })lookups.Minor nit:
options.githubToken !== undefinedwill forward an empty string, which then falls back to the default (no credential brokering) insidebuildGitHubCredentialBrokeringPolicy. Consideroptions.githubToken ? { githubToken: options.githubToken } : {}for clearer intent. Current callers trim-to-undefined so this is effectively unreachable today.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sandbox/vercel/snapshot-refresh.ts` around lines 99 - 115, The conditional currently uses options.githubToken !== undefined which will pass an empty string through; change the args to only include githubToken when it's a non-empty truthy value (e.g., use a truthy check rather than !== undefined) so connectSnapshotSandbox(...) receives no githubToken field when callers intentionally supply empty string; update the object construction in snapshot-refresh.ts (the options block passed to connectSnapshotSandbox) to gate githubToken using options.githubToken truthiness to avoid forwarding empty strings that then fall back inside buildGitHubCredentialBrokeringPolicy.apps/web/app/workflows/build-org-snapshot.ts (1)
41-62: Swallowed failure silently re-triggers on every subsequent session.On
success: false, the workflow logs and returns a result object. Nothing prevents the next session'sfindOrgSnapshotfrom finding no ready snapshot and kicking yet another build that will fail for the same reason (bad clone URL, bad token, rate limit). For repos that permanently fail to clone, every session for that org pays both the full-clone cost and kicks a doomed workflow.Consider short-circuiting with a negative cache (e.g., a small "recent failed org" TTL in Redis/KV) or at least surfacing the error to an alertable channel so it doesn't silently burn workflow quota.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/workflows/build-org-snapshot.ts` around lines 41 - 62, The workflow currently returns { success: false } from buildOrgSnapshotWorkflow after buildSnapshotStep fails but does not record that failure, causing findOrgSnapshot to repeatedly trigger doomed rebuilds; update buildOrgSnapshotWorkflow to record a negative cache entry (e.g., set a Redis/KV key for the org/sandbox with a short TTL) when catching an error and include the error details in that entry, and ensure the existing findOrgSnapshot path consults that negative cache before scheduling a new build; alternatively (or in addition) emit the error to the alerting channel/function used by the app so permanent failures are surfaced.apps/web/lib/sandbox/build-org-snapshot-kick.ts (1)
15-27: Useafter()to prevent the serverless runtime from dropping the workflow start request.
void start(...)returns a dangling promise. In Vercel's serverless runtime, the function instance can be frozen as soon as the enclosing Route Handler returns itsResponse, potentially cancelling the in-flight HTTP request thatstart()makes to the workflow service.While the handler tolerates kick failure (fallback to full clone, and next session re-kicks), silent dropped kicks add unnecessary ~75s full-clones until a request survives long enough to register the workflow.
Wrap the call in
after()fromnext/server(Next.js 15.1+, available in your 16.2.1 environment) to keep the runtime alive:Proposed fix
import "server-only"; +import { after } from "next/server"; import { start } from "workflow/api"; import { buildOrgSnapshotWorkflow } from "@/app/workflows/build-org-snapshot"; interface KickBuildOrgSnapshotInput { cloneUrl: string; sandboxName: string; } export function kickBuildOrgSnapshotWorkflow(input: KickBuildOrgSnapshotInput) { - void start(buildOrgSnapshotWorkflow, [input]).then( + after( + start(buildOrgSnapshotWorkflow, [input]).then( (run) => console.log( `[build-org-snapshot] Started workflow run ${run.runId} for '${input.sandboxName}' (${input.cloneUrl})`, ), (error) => console.error( `[build-org-snapshot] Failed to start workflow for '${input.sandboxName}' (${input.cloneUrl}):`, error, ), - ); + ), + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/sandbox/build-org-snapshot-kick.ts` around lines 15 - 27, The kickBuildOrgSnapshotWorkflow uses a dangling promise from start(...) which can be dropped by Vercel; import after from 'next/server' and wrap the start call in after(...) so the runtime stays alive until the request completes (e.g., replace the standalone void start(buildOrgSnapshotWorkflow, [input]).then(..., ...) with after(() => start(buildOrgSnapshotWorkflow, [input]).then(run => console.log(...), error => console.error(...)))); keep the same logging handlers and reference the existing kickBuildOrgSnapshotWorkflow and start symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/app/workflows/build-org-snapshot.ts`:
- Around line 28-36: The command construction in refreshBaseSnapshot uses direct
string interpolation of input.cloneUrl into commands (commands: [`git clone
--depth=1 ${input.cloneUrl} .`]) which allows shell metacharacters to cause
command injection; update the build-org-snapshot workflow to avoid passing an
interpolated shell string to the sandbox: either (1) validate/allow-list
input.cloneUrl at the workflow boundary (reuse parseGitHubUrl or an equivalent
strict URL validator) and reject unsafe values, or (2) change how commands are
invoked by refreshBaseSnapshot so it accepts argv-style exec (no shell
expansion) and pass the git clone arguments as an array (i.e., call the sandbox
API with an exec-style command rather than a single bash -c string) so
input.cloneUrl is never interpreted by a shell; make the change around the
refreshBaseSnapshot call and the commands parameter and ensure input.cloneUrl is
referenced safely (or replaced by the validated URL) before passing to
refreshBaseSnapshot.
In `@apps/web/lib/recoupable/extract-org-repo-name.ts`:
- Around line 1-11: The current ORG_REPO_URL_PATTERN in extractOrgRepoName
accepts any recoupable/<repo>, letting non-org repos like recoupable/open-agents
be treated as org snapshots; tighten the regex to only capture repo names that
follow the generated-org naming convention (e.g. start with "org-") by changing
ORG_REPO_URL_PATTERN to require the captured group to begin with "org-" (update
the pattern used in extractOrgRepoName accordingly), and add a regression test
that verifies a URL like https://github.com/recoupable/open-agents returns null
while https://github.com/recoupable/org-... returns the repo name.
In `@apps/web/lib/sandbox/find-org-snapshot.ts`:
- Around line 15-22: The Snapshot.list call uses limit: 5 which can miss older
ready snapshots; update the call in find-org-snapshot to use limit: 25 so
client-side filtering has a larger window to find a snapshot with status ===
"created" (i.e., keep using Snapshot.list({ name: sandboxName, sortOrder:
"desc", limit: 25 }), then continue to find ready via result.snapshots.find((s)
=> s.status === "created") and return ready?.id ?? null).
In `@packages/sandbox/vercel/sandbox.ts`:
- Around line 600-622: The code currently sets refBranch = source.branch ??
"main" inside the prebuilt snapshot branch handling (in the block guarded by
source.prebuilt && baseSnapshotId) which breaks repos whose default branch is
not "main"; change this to (a) if source.branch is provided use it, (b) if
source.branch is undefined and the session is a new branch (isNewBranch true)
resolve the remote HEAD to determine the default branch (e.g. use git
symbolic-ref refs/remotes/origin/HEAD or git remote show origin via
sdk.runCommand) and use that name, or (c) if source.branch is undefined and this
is not a new branch, skip the fetch/reset entirely and rely on the existing
snapshot state; update references to refBranch, the fetch/reset sdk.runCommand
calls, and error messages accordingly so we no longer hardcode "main".
In `@packages/sandbox/vercel/snapshot-refresh.test.ts`:
- Around line 174-183: The test fixture uses a GitHub-token-like string
("ghs_secret") which triggers secret scanners; update the value passed to
refreshBaseSnapshot (in the test data object used by the refreshBaseSnapshot
call) to a neutral non-secret-looking string (e.g., "fake-token" or
"TEST_TOKEN") so the input to the githubToken field is clearly a fixture and not
a credential; ensure you change the githubToken property in the
refreshBaseSnapshot call and any related test assertions that depend on its
exact value.
---
Nitpick comments:
In `@apps/web/app/workflows/build-org-snapshot.ts`:
- Around line 41-62: The workflow currently returns { success: false } from
buildOrgSnapshotWorkflow after buildSnapshotStep fails but does not record that
failure, causing findOrgSnapshot to repeatedly trigger doomed rebuilds; update
buildOrgSnapshotWorkflow to record a negative cache entry (e.g., set a Redis/KV
key for the org/sandbox with a short TTL) when catching an error and include the
error details in that entry, and ensure the existing findOrgSnapshot path
consults that negative cache before scheduling a new build; alternatively (or in
addition) emit the error to the alerting channel/function used by the app so
permanent failures are surfaced.
In `@apps/web/lib/sandbox/build-org-snapshot-kick.ts`:
- Around line 15-27: The kickBuildOrgSnapshotWorkflow uses a dangling promise
from start(...) which can be dropped by Vercel; import after from 'next/server'
and wrap the start call in after(...) so the runtime stays alive until the
request completes (e.g., replace the standalone void
start(buildOrgSnapshotWorkflow, [input]).then(..., ...) with after(() =>
start(buildOrgSnapshotWorkflow, [input]).then(run => console.log(...), error =>
console.error(...)))); keep the same logging handlers and reference the existing
kickBuildOrgSnapshotWorkflow and start symbols.
In `@packages/sandbox/vercel/snapshot-refresh.ts`:
- Around line 99-115: The conditional currently uses options.githubToken !==
undefined which will pass an empty string through; change the args to only
include githubToken when it's a non-empty truthy value (e.g., use a truthy check
rather than !== undefined) so connectSnapshotSandbox(...) receives no
githubToken field when callers intentionally supply empty string; update the
object construction in snapshot-refresh.ts (the options block passed to
connectSnapshotSandbox) to gate githubToken using options.githubToken truthiness
to avoid forwarding empty strings that then fall back inside
buildGitHubCredentialBrokeringPolicy.
🪄 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: deffd445-f96d-4a7e-b0f0-6f86d053131e
📒 Files selected for processing (14)
apps/web/app/workflows/build-org-snapshot.tsapps/web/lib/recoupable/extract-org-repo-name.test.tsapps/web/lib/recoupable/extract-org-repo-name.tsapps/web/lib/sandbox/build-org-snapshot-kick.tsapps/web/lib/sandbox/create-sandbox-handler.tsapps/web/lib/sandbox/find-org-snapshot.tspackages/sandbox/factory.tspackages/sandbox/index.tspackages/sandbox/types.tspackages/sandbox/vercel/config.tspackages/sandbox/vercel/connect.tspackages/sandbox/vercel/sandbox.tspackages/sandbox/vercel/snapshot-refresh.test.tspackages/sandbox/vercel/snapshot-refresh.ts
| const result = await refreshBaseSnapshot({ | ||
| baseSnapshotId: DEFAULT_SANDBOX_BASE_SNAPSHOT_ID, | ||
| sandboxName: input.sandboxName, | ||
| sandboxTimeoutMs: BUILD_SANDBOX_TIMEOUT_MS, | ||
| commandTimeoutMs: BUILD_COMMAND_TIMEOUT_MS, | ||
| githubToken, | ||
| commands: [`git clone --depth=1 ${input.cloneUrl} .`], | ||
| log: (message) => console.log(`[build-org-snapshot] ${message}`), | ||
| }); |
There was a problem hiding this comment.
Potential command injection via cloneUrl interpolation.
commands: [\git clone --depth=1 ${input.cloneUrl} .`]is ultimately passed tobash -c 'cd "${cwd}" && ${command}'inside the sandbox. AcloneUrl containing shell metacharacters (;, &&, `` ``, $(...)) would execute arbitrary commands in the build sandbox VM. The current upstream caller validates through `parseGitHubUrl`, so this is not exploitable today, but this workflow is now an exported entry point and shouldn't depend on every future caller sanitizing input.
Either validate/allow-list the URL at the workflow boundary, or use a form that doesn't go through a shell (single-shot argv exec). A cheap hardening step:
Proposed fix
+ if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+?(\.git)?$/.test(input.cloneUrl)) {
+ throw new Error(
+ `[build-org-snapshot] Refusing to clone non-GitHub URL: '${input.cloneUrl}'`,
+ );
+ }
+
const result = await refreshBaseSnapshot({Also note: git clone --depth=1 <url> . with no --branch captures the remote's default branch into the snapshot. Combined with the "main" fallback in packages/sandbox/vercel/sandbox.ts prebuilt fast path, this breaks any repo whose default isn't main (flagged separately on that file).
📝 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.
| const result = await refreshBaseSnapshot({ | |
| baseSnapshotId: DEFAULT_SANDBOX_BASE_SNAPSHOT_ID, | |
| sandboxName: input.sandboxName, | |
| sandboxTimeoutMs: BUILD_SANDBOX_TIMEOUT_MS, | |
| commandTimeoutMs: BUILD_COMMAND_TIMEOUT_MS, | |
| githubToken, | |
| commands: [`git clone --depth=1 ${input.cloneUrl} .`], | |
| log: (message) => console.log(`[build-org-snapshot] ${message}`), | |
| }); | |
| if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+?(\.git)?$/.test(input.cloneUrl)) { | |
| throw new Error( | |
| `[build-org-snapshot] Refusing to clone non-GitHub URL: '${input.cloneUrl}'`, | |
| ); | |
| } | |
| const result = await refreshBaseSnapshot({ | |
| baseSnapshotId: DEFAULT_SANDBOX_BASE_SNAPSHOT_ID, | |
| sandboxName: input.sandboxName, | |
| sandboxTimeoutMs: BUILD_SANDBOX_TIMEOUT_MS, | |
| commandTimeoutMs: BUILD_COMMAND_TIMEOUT_MS, | |
| githubToken, | |
| commands: [`git clone --depth=1 ${input.cloneUrl} .`], | |
| log: (message) => console.log(`[build-org-snapshot] ${message}`), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/workflows/build-org-snapshot.ts` around lines 28 - 36, The
command construction in refreshBaseSnapshot uses direct string interpolation of
input.cloneUrl into commands (commands: [`git clone --depth=1 ${input.cloneUrl}
.`]) which allows shell metacharacters to cause command injection; update the
build-org-snapshot workflow to avoid passing an interpolated shell string to the
sandbox: either (1) validate/allow-list input.cloneUrl at the workflow boundary
(reuse parseGitHubUrl or an equivalent strict URL validator) and reject unsafe
values, or (2) change how commands are invoked by refreshBaseSnapshot so it
accepts argv-style exec (no shell expansion) and pass the git clone arguments as
an array (i.e., call the sandbox API with an exec-style command rather than a
single bash -c string) so input.cloneUrl is never interpreted by a shell; make
the change around the refreshBaseSnapshot call and the commands parameter and
ensure input.cloneUrl is referenced safely (or replaced by the validated URL)
before passing to refreshBaseSnapshot.
| const ORG_REPO_URL_PATTERN = | ||
| /^https:\/\/github\.com\/recoupable\/([^/]+?)(?:\.git)?\/?$/; | ||
|
|
||
| /** | ||
| * Extracts the repo name from a Recoupable org clone URL. | ||
| * Example: `https://github.com/recoupable/org-rostrum-pacific-<uuid>` → `org-rostrum-pacific-<uuid>`. | ||
| * Returns `null` for any URL that does not match the expected pattern. | ||
| */ | ||
| export function extractOrgRepoName(cloneUrl: string): string | null { | ||
| const match = cloneUrl.match(ORG_REPO_URL_PATTERN); | ||
| return match?.[1] ?? null; |
There was a problem hiding this comment.
Constrain extraction to generated org repositories.
Line 2 currently accepts any recoupable/<repo> value, so https://github.com/recoupable/open-agents would be treated as an org snapshot name. That can build or reuse snapshots outside the intended per-org namespace.
Proposed tightening
const ORG_REPO_URL_PATTERN =
- /^https:\/\/github\.com\/recoupable\/([^/]+?)(?:\.git)?\/?$/;
+ /^https:\/\/github\.com\/recoupable\/(org-[A-Za-z0-9_-]+)(?:\.git)?\/?$/;Add a matching regression test:
+ test("returns null for non-org repos under recoupable", () => {
+ expect(
+ extractOrgRepoName("https://github.com/recoupable/open-agents"),
+ ).toBeNull();
+ });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/lib/recoupable/extract-org-repo-name.ts` around lines 1 - 11, The
current ORG_REPO_URL_PATTERN in extractOrgRepoName accepts any
recoupable/<repo>, letting non-org repos like recoupable/open-agents be treated
as org snapshots; tighten the regex to only capture repo names that follow the
generated-org naming convention (e.g. start with "org-") by changing
ORG_REPO_URL_PATTERN to require the captured group to begin with "org-" (update
the pattern used in extractOrgRepoName accordingly), and add a regression test
that verifies a URL like https://github.com/recoupable/open-agents returns null
while https://github.com/recoupable/org-... returns the repo name.
| try { | ||
| const result = await Snapshot.list({ | ||
| name: sandboxName, | ||
| sortOrder: "desc", | ||
| limit: 5, | ||
| }); | ||
| const ready = result.snapshots.find((s) => s.status === "created"); | ||
| return ready?.id ?? null; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
@vercel/sandbox Snapshot.list options status filter
💡 Result:
In the @vercel/sandbox SDK, Snapshot.list is a static method on the Snapshot class used to list snapshots for the project. Official SDK documentation confirms its existence but does not provide detailed options in available snippets. From the corresponding Vercel REST API endpoints (which the SDK likely wraps), the list snapshots operation supports these query parameters (options): - project: string (optional) - Project ID or name. - limit: number (optional) - Max results per page (e.g., 1-50). - since: number (optional) - Unix timestamp (ms) to filter snapshots created after. - until: number (optional) - Unix timestamp (ms) to filter snapshots created before. - teamId/slug: string (optional) - For team-scoped requests. - Additional in v2: name, cursor, sortOrder (asc/desc). No explicit 'status' filter parameter is documented in the API query params. Snapshots have a status field ("failed", "created", "deleted") returned in results, which can be filtered client-side after fetching. Example usage (inferred from docs): await Snapshot.list({ limit: 20, since: Date.now - 246060*1000 }); For full details, see SDK reference at https://vercel.com/docs/vercel-sandbox/sdk-reference and REST API at https://vercel.com/docs/rest-api/sandboxes/list-snapshots.
Citations:
- 1: https://vercel.com/docs/rest-api/sandboxes/list-snapshots
- 2: https://docs.vercel.com/docs/rest-api/sandboxes/list-snapshots
- 3: https://vercel.com/docs/rest-api/sandboxes-v2-beta/list-snapshots
- 4: https://vercel.com/docs/vercel-sandbox/sdk-reference
- 5: https://examples.vercel.com/docs/vercel-sandbox/sdk-reference
Widen limit: 5 to reduce risk of missing ready snapshots during churn.
If the 5 most-recent snapshots are all in a non-created state (e.g., building, failed, expired), this returns null and falls back to the slow ~75s full-clone path even though older ready snapshots exist. Given the PR explicitly tolerates concurrent duplicate builds, multiple in-flight snapshots for the same org name are plausible.
The Snapshot.list API does not support server-side filtering by status, so client-side filtering is required. Increase limit to 25 to scan a wider window for ready snapshots:
Proposed fix
const result = await Snapshot.list({
name: sandboxName,
sortOrder: "desc",
- limit: 5,
+ limit: 25,
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/lib/sandbox/find-org-snapshot.ts` around lines 15 - 22, The
Snapshot.list call uses limit: 5 which can miss older ready snapshots; update
the call in find-org-snapshot to use limit: 25 so client-side filtering has a
larger window to find a snapshot with status === "created" (i.e., keep using
Snapshot.list({ name: sandboxName, sortOrder: "desc", limit: 25 }), then
continue to find ready via result.snapshots.find((s) => s.status === "created")
and return ready?.id ?? null).
| if (source?.prebuilt && baseSnapshotId) { | ||
| const refBranch = source.branch ?? "main"; | ||
| const fetchResult = await sdk.runCommand({ | ||
| cmd: "git", | ||
| args: ["fetch", "--depth=1", "origin", refBranch], | ||
| cwd: workingDirectory, | ||
| }); | ||
| if (fetchResult.exitCode !== 0) { | ||
| throw new Error( | ||
| `Failed to fetch '${refBranch}' in prebuilt snapshot for '${source.url}' (exit code ${fetchResult.exitCode})`, | ||
| ); | ||
| } | ||
| const resetResult = await sdk.runCommand({ | ||
| cmd: "git", | ||
| args: ["reset", "--hard", `origin/${refBranch}`], | ||
| cwd: workingDirectory, | ||
| }); | ||
| if (resetResult.exitCode !== 0) { | ||
| throw new Error( | ||
| `Failed to reset to 'origin/${refBranch}' in prebuilt snapshot for '${source.url}' (exit code ${resetResult.exitCode})`, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Hardcoded "main" fallback diverges from repo's default branch and will break on non-main repos.
Two cases are broken:
isNewBranch === true:source.branchisundefined, so the fast path fetches/resets toorigin/main. The non-prebuiltgit clonepath in the same function (line 574) clones without--branch, i.e. whatever the remote HEAD points to. For a repo whose default ismaster/develop/trunk, these two paths now produce different starting commits.- Repos whose default branch isn't
main: the base snapshot built bybuild-org-snapshot.tsrunsgit clone --depth=1 <cloneUrl> .(no branch flag) so the checkout is the default branch. Then a session withbranch: "main"omitted would trygit fetch --depth=1 origin main→ fatal on any repo without amainbranch, surfacing asFailed to fetch 'main' ...and a 502.
Prefer resolving the remote HEAD when no branch is specified, e.g. via git remote show origin / git symbolic-ref refs/remotes/origin/HEAD, or skip the fetch/reset and rely on the snapshot's state when source.branch is undefined and !source.newBranch.
Proposed fix sketch
- if (source?.prebuilt && baseSnapshotId) {
- const refBranch = source.branch ?? "main";
+ if (source?.prebuilt && baseSnapshotId) {
+ let refBranch = source.branch;
+ if (!refBranch) {
+ const headResult = await sdk.runCommand({
+ cmd: "bash",
+ args: [
+ "-c",
+ "git remote set-head origin -a >/dev/null && git symbolic-ref --short refs/remotes/origin/HEAD | sed 's#^origin/##'",
+ ],
+ cwd: workingDirectory,
+ });
+ refBranch = (await headResult.stdout()).trim() || "main";
+ }
const fetchResult = await sdk.runCommand({🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/sandbox/vercel/sandbox.ts` around lines 600 - 622, The code
currently sets refBranch = source.branch ?? "main" inside the prebuilt snapshot
branch handling (in the block guarded by source.prebuilt && baseSnapshotId)
which breaks repos whose default branch is not "main"; change this to (a) if
source.branch is provided use it, (b) if source.branch is undefined and the
session is a new branch (isNewBranch true) resolve the remote HEAD to determine
the default branch (e.g. use git symbolic-ref refs/remotes/origin/HEAD or git
remote show origin via sdk.runCommand) and use that name, or (c) if
source.branch is undefined and this is not a new branch, skip the fetch/reset
entirely and rely on the existing snapshot state; update references to
refBranch, the fetch/reset sdk.runCommand calls, and error messages accordingly
so we no longer hardcode "main".
| await refreshBaseSnapshot( | ||
| { | ||
| baseSnapshotId: "snap-current", | ||
| sandboxTimeoutMs: 600_000, | ||
| sandboxName: "org-rostrum-pacific-abc", | ||
| githubToken: "ghs_secret", | ||
| commands: [ | ||
| "git clone --depth=1 https://github.com/recoupable/org-rostrum-pacific-abc .", | ||
| ], | ||
| }, |
There was a problem hiding this comment.
Avoid token-looking fixture strings.
Line 179 uses a GitHub-token-like ghs_ prefix. Even though it is fake, this can create noisy secret-scanner findings; use a neutral fixture value instead.
Suggested cleanup
- githubToken: "ghs_secret",
+ githubToken: "test-github-token",- githubToken: "ghs_secret",
+ githubToken: "test-github-token",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/sandbox/vercel/snapshot-refresh.test.ts` around lines 174 - 183, The
test fixture uses a GitHub-token-like string ("ghs_secret") which triggers
secret scanners; update the value passed to refreshBaseSnapshot (in the test
data object used by the refreshBaseSnapshot call) to a neutral
non-secret-looking string (e.g., "fake-token" or "TEST_TOKEN") so the input to
the githubToken field is clearly a fixture and not a credential; ensure you
change the githubToken property in the refreshBaseSnapshot call and any related
test assertions that depend on its exact value.
There was a problem hiding this comment.
6 issues found across 14 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="apps/web/lib/sandbox/build-org-snapshot-kick.ts">
<violation number="1" location="apps/web/lib/sandbox/build-org-snapshot-kick.ts:19">
P2: Avoid logging raw `cloneUrl`; it can contain credentials or other sensitive user-provided data.</violation>
</file>
<file name="apps/web/lib/sandbox/find-org-snapshot.ts">
<violation number="1" location="apps/web/lib/sandbox/find-org-snapshot.ts:19">
P2: Hard-coding `limit: 5` can miss an existing ready snapshot and trigger unnecessary slow-path sandbox startup.</violation>
</file>
<file name="apps/web/lib/sandbox/create-sandbox-handler.ts">
<violation number="1" location="apps/web/lib/sandbox/create-sandbox-handler.ts:115">
P2: Enabling `prebuilt` for new-branch requests can break sandbox creation on repos whose default branch is not `main`.</violation>
</file>
<file name="apps/web/app/workflows/build-org-snapshot.ts">
<violation number="1" location="apps/web/app/workflows/build-org-snapshot.ts:22">
P2: Do not throw when `GITHUB_TOKEN` is unset; this makes the snapshot workflow fail early and prevents base snapshots from being created.</violation>
</file>
<file name="packages/sandbox/vercel/sandbox.ts">
<violation number="1" location="packages/sandbox/vercel/sandbox.ts:601">
P1: The prebuilt sync path hardcodes `main` when `source.branch` is absent, which breaks repos that use a different default branch.</violation>
<violation number="2" location="packages/sandbox/vercel/sandbox.ts:614">
P1: Using `git reset --hard origin/<branch>` here updates commit content but does not switch HEAD to that branch, so branch-aware flows can run against the wrong branch name.</violation>
</file>
You're on the cubic free plan with 20 free PR reviews remaining this month. Upgrade for unlimited reviews.
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| // the tip of the requested branch with a shallow fetch + hard reset so any | ||
| // drift since the snapshot was built is caught up in one round-trip. | ||
| if (source?.prebuilt && baseSnapshotId) { | ||
| const refBranch = source.branch ?? "main"; |
There was a problem hiding this comment.
P1: The prebuilt sync path hardcodes main when source.branch is absent, which breaks repos that use a different default branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sandbox/vercel/sandbox.ts, line 601:
<comment>The prebuilt sync path hardcodes `main` when `source.branch` is absent, which breaks repos that use a different default branch.</comment>
<file context>
@@ -594,6 +594,33 @@ ${hostLine}${portLines}${runtimeEnvLine}`;
+ // the tip of the requested branch with a shallow fetch + hard reset so any
+ // drift since the snapshot was built is caught up in one round-trip.
+ if (source?.prebuilt && baseSnapshotId) {
+ const refBranch = source.branch ?? "main";
+ const fetchResult = await sdk.runCommand({
+ cmd: "git",
</file context>
| } | ||
| const resetResult = await sdk.runCommand({ | ||
| cmd: "git", | ||
| args: ["reset", "--hard", `origin/${refBranch}`], |
There was a problem hiding this comment.
P1: Using git reset --hard origin/<branch> here updates commit content but does not switch HEAD to that branch, so branch-aware flows can run against the wrong branch name.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sandbox/vercel/sandbox.ts, line 614:
<comment>Using `git reset --hard origin/<branch>` here updates commit content but does not switch HEAD to that branch, so branch-aware flows can run against the wrong branch name.</comment>
<file context>
@@ -594,6 +594,33 @@ ${hostLine}${portLines}${runtimeEnvLine}`;
+ }
+ const resetResult = await sdk.runCommand({
+ cmd: "git",
+ args: ["reset", "--hard", `origin/${refBranch}`],
+ cwd: workingDirectory,
+ });
</file context>
| args: ["reset", "--hard", `origin/${refBranch}`], | |
| args: ["checkout", "-B", refBranch, `origin/${refBranch}`], |
| void start(buildOrgSnapshotWorkflow, [input]).then( | ||
| (run) => | ||
| console.log( | ||
| `[build-org-snapshot] Started workflow run ${run.runId} for '${input.sandboxName}' (${input.cloneUrl})`, |
There was a problem hiding this comment.
P2: Avoid logging raw cloneUrl; it can contain credentials or other sensitive user-provided data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/sandbox/build-org-snapshot-kick.ts, line 19:
<comment>Avoid logging raw `cloneUrl`; it can contain credentials or other sensitive user-provided data.</comment>
<file context>
@@ -0,0 +1,27 @@
+ void start(buildOrgSnapshotWorkflow, [input]).then(
+ (run) =>
+ console.log(
+ `[build-org-snapshot] Started workflow run ${run.runId} for '${input.sandboxName}' (${input.cloneUrl})`,
+ ),
+ (error) =>
</file context>
| const result = await Snapshot.list({ | ||
| name: sandboxName, | ||
| sortOrder: "desc", | ||
| limit: 5, |
There was a problem hiding this comment.
P2: Hard-coding limit: 5 can miss an existing ready snapshot and trigger unnecessary slow-path sandbox startup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/sandbox/find-org-snapshot.ts, line 19:
<comment>Hard-coding `limit: 5` can miss an existing ready snapshot and trigger unnecessary slow-path sandbox startup.</comment>
<file context>
@@ -0,0 +1,30 @@
+ const result = await Snapshot.list({
+ name: sandboxName,
+ sortOrder: "desc",
+ limit: 5,
+ });
+ const ready = result.snapshots.find((s) => s.status === "created");
</file context>
| repo: repoUrl, | ||
| branch: isNewBranch ? undefined : branch, | ||
| newBranch: isNewBranch ? branch : undefined, | ||
| prebuilt: !!orgSnapshotId, |
There was a problem hiding this comment.
P2: Enabling prebuilt for new-branch requests can break sandbox creation on repos whose default branch is not main.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/sandbox/create-sandbox-handler.ts, line 115:
<comment>Enabling `prebuilt` for new-branch requests can break sandbox creation on repos whose default branch is not `main`.</comment>
<file context>
@@ -92,10 +95,24 @@ export async function handleCreateSandboxRequest(
repo: repoUrl,
branch: isNewBranch ? undefined : branch,
newBranch: isNewBranch ? branch : undefined,
+ prebuilt: !!orgSnapshotId,
};
</file context>
| ); | ||
|
|
||
| const githubToken = process.env.GITHUB_TOKEN?.trim() || undefined; | ||
| if (!githubToken) { |
There was a problem hiding this comment.
P2: Do not throw when GITHUB_TOKEN is unset; this makes the snapshot workflow fail early and prevents base snapshots from being created.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/workflows/build-org-snapshot.ts, line 22:
<comment>Do not throw when `GITHUB_TOKEN` is unset; this makes the snapshot workflow fail early and prevents base snapshots from being created.</comment>
<file context>
@@ -0,0 +1,62 @@
+ );
+
+ const githubToken = process.env.GITHUB_TOKEN?.trim() || undefined;
+ if (!githubToken) {
+ throw new Error(
+ "[build-org-snapshot] GITHUB_TOKEN is not set; cannot clone org repo",
</file context>
Summary
Targets the 75-second
git clonethat PR #8's instrumentation showed accounts for 99.3% of sandbox startup time (from 76,116ms total: clone = 75,599ms, VM provisioning = 234ms, everything else = ~280ms combined).Replaces the per-session full-repo clone with a per-org base snapshot pattern. On a cold sandbox create:
org-rostrum-pacific-cebcc866-...) viaSnapshot.list({ name })./vercel/sandbox. Rungit fetch --depth=1 origin <branch> && git reset --hard origin/<branch>to absorb any drift since the snapshot was built. Expected ~1-3s instead of ~75s.Expected outcome: new-sandbox startup goes from ~76s → ~5s for the 99% of sessions where a per-org snapshot exists.
Design choices (per discussion)
Snapshot.list({ name })on it.origin/mainis absorbed by the per-sessionfetch --depth=1 + reset --hard. The snapshot itself is "build once, use forever" — the user explicitly chose this over webhooks/cron.sandboxLifecycleWorkflow(use workflow/use step); kicked fire-and-forget from the create handler.extractOrgRepoName, parsing the conventionhttps://github.com/recoupable/<repoName>→<repoName>.Files
Package (
packages/sandbox/):Source.prebuilt?: boolean— when true, skip clone and rungit fetch + reset --hard origin/<branch>instead.ConnectOptions.forceCreate?: boolean— bypass reconnect-first for named ephemeral build sandboxes (so the build path always creates fresh, even when the name was used in a prior run).refreshBaseSnapshotgainssandboxName?+githubToken?so build sandboxes can be named (filter-able) and authenticate clones via credential brokering.App (
apps/web/):lib/recoupable/extract-org-repo-name.ts(+ tests) — parses the repo name from the clone URL.lib/sandbox/find-org-snapshot.ts—Snapshot.list({ name, sortOrder: \"desc\" })and returns the most recentcreatedsnapshot id, or null.lib/sandbox/build-org-snapshot-kick.ts— fire-and-forgetstart(buildOrgSnapshotWorkflow, [...]).app/workflows/build-org-snapshot.ts— callsrefreshBaseSnapshot({ baseSnapshotId, sandboxName: orgRepoName, commands: [\git clone --depth=1 ${cloneUrl} .`] })`.lib/sandbox/create-sandbox-handler.ts— wires the lookup + fallback + kick into the existing flow.Concurrency / dedup
If two sessions for the same org race when no snapshot exists yet, both kick the build workflow. Both runs create snapshots with the same name, both succeed, future lookups pick the most recent. Wasted compute on one extra build, harmless. We could dedup later by checking workflow run state if it becomes a problem.
Test plan
extractOrgRepoName(6 cases),refreshBaseSnapshotwith newsandboxName + githubToken + forceCreateflow.bun run checkclean,turbo typecheckclean across all 4 packages.vm_provision~250ms +git_cloneskipped + new fetch/reset step.Notes for review
findOrgSnapshotrelies on the Vercel SDK's auto-credential resolution viaVERCEL_OIDC_TOKEN(already present in Vercel runtime). No new env vars.🤖 Generated with Claude Code
Summary by cubic
Speed up sandbox startup by using per-org base snapshots and skipping the full repo clone. First run falls back to a full clone and kicks a background build; later sessions start in ~3–5s.
@vercel/sandboxSnapshot.list({ name }); background workflow builds snapshots when missing.Source.prebuiltto skipgit cloneand run shallowgit fetch --depth=1+git reset --hard origin/<branch>.ConnectOptions.forceCreateto always create fresh named build sandboxes.refreshBaseSnapshotnow acceptssandboxNameandgithubTokenfor named, authenticated builds.Written for commit cbf39e0. Summary will update on new commits.
Summary by CodeRabbit