feat: ship Project V2 sandbox builder - #3
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces Drops Studio Builder V2: a canonical Project V2 file model, an AI builder-agent with tool-based editing and release gating, a Vercel Sandbox runtime adapter (plus legacy HTML adapter), GitHub/Vercel integration libraries, capability-signed project-data storage, drops-platform market/wallet modules, a new Studio Builder UI surface, and extensive supporting API routes, documentation, and tests. ChangesProject V2 Platform
Estimated code review effort: 5 (Critical) | ~180+ minutes Sequence Diagram(s)sequenceDiagram
participant Client as Studio UI
participant Route as /api/builder/agent
participant Session as BuilderAgentSession
participant Runtime as VercelSandboxRuntimeAdapter
participant Checker as VercelAgentBrowserChecker
participant Receipt as ReleaseReceiptStore
Client->>Route: POST prompt + provider credentials
Route->>Session: runBuilderAgent(request)
Session->>Runtime: ensure() / writeProject()
Session->>Runtime: install / typecheck / lint / test / build
Runtime-->>Session: command results
Session->>Runtime: startPreview()
Runtime-->>Session: preview URL
Session->>Checker: check(previewUrl)
Checker-->>Session: render/interaction/error evidence
Session->>Session: create checkpoint if gate passes
Route->>Receipt: writeProjectV2ReleaseReceipt(checkpoint)
Route-->>Client: result { releaseGate, previewUrl }
sequenceDiagram
participant Client as Studio UI
participant Route as /api/deployments/vercel
participant Storage as Project V2 Snapshot Storage
participant Receipt as ReleaseReceiptStore
participant Vercel as Vercel Deployment API
Client->>Route: POST deploy { studioProjectId }
Route->>Storage: loadAuthorized(actorId, projectId)
Storage-->>Route: ProjectV2 snapshot
Route->>Route: verify checkpoint gate matches revision/contentHash
Route->>Receipt: hasProjectV2ReleaseReceipt(descriptor)
Receipt-->>Route: receipt confirmed
Route->>Vercel: createVercelPreviewDeployment(files)
Vercel-->>Route: deployment record
Route->>Vercel: waitForVercelDeployment() / getVercelDeploymentLogs()
Route-->>Client: deployment status + readiness
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
app/api/access/route.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: expand is not a function app/api/agent/plan/route.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: expand is not a function app/api/builder/agent/route.tsOops! Something went wrong! :( ESLint: 9.39.4 TypeError: expand is not a function
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 129af400a0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| (remote.project.revision === project.revision && | ||
| remote.project.contentHash !== project.contentHash) | ||
| ) { | ||
| const saved = await saveProjectV2ToCloud(project, remote.storageRevision); |
There was a problem hiding this comment.
Surface equal-revision hash conflicts before saving
When two tabs independently edit the same base revision, both can produce the same revision number with different hashes. This branch reloads the remote storage revision and then uses it to overwrite the remote project, so the server CAS succeeds and silently discards the other tab's work. Treat the equal-revision/hash-mismatch case as a conflict rather than saving the local copy with the freshly read storage revision.
AGENTS.md reference: AGENTS.md:L105-L105
Useful? React with 👍 / 👎.
| access.access?.authenticated && | ||
| access.access.account?.connected && | ||
| access.access.account.projectSync, |
There was a problem hiding this comment.
Persist signed-guest projects before Sandbox builds
For every signed guest, /api/access reports authenticated: false and a disconnected account, so this gate always selects local mode even when durable Project V2 storage is configured. runBuilder subsequently rejects persisted: false, making the documented signed-guest builder route and Free Auto Sandbox verification unreachable for all guest users despite the storage and builder APIs accepting guest actors.
Useful? React with 👍 / 👎.
| // Approval evidence is resolved server-side. Tool names are intentionally | ||
| // absent from the public JSON body so a model cannot approve its own call. | ||
| const approvedTools = await (dependencies.resolveApprovedTools?.(request) ?? []); | ||
| const result = await runBuilderAgent( | ||
| { ...parsed.data, approvedTools: [...approvedTools] }, |
There was a problem hiding this comment.
Wire a continuation path for approved agent tools
In the production POST path no dependency supplies resolveApprovedTools, while the strict public request schema excludes approval evidence, so this expression always resolves to an empty list. If the model requests delete_file, rename_file, restore_checkpoint, or publish_project, the response becomes approval-required, but no subsequent request can approve and continue that operation; add a server-backed, user-consented approval continuation instead of permanently denying these tools.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
| const deployment = input.wait === false | ||
| ? created | ||
| : await waitForVercelDeployment({ credentials: auth, deploymentId: created.id }); |
There was a problem hiding this comment.
Return the deployment receipt when waiting fails
Once createVercelPreviewDeployment succeeds, a polling timeout or transient status-request failure from waitForVercelDeployment jumps to the outer catch and returns only an error, discarding created.id even though the external deployment continues. The client therefore cannot persist, inspect, or cancel that deployment, and retrying creates another one; preserve and return the accepted deployment receipt with a nonterminal state when bounded waiting fails.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
| await requestJson(`${repoPath}/git/refs`, token, fetchImpl, { | ||
| method: "POST", | ||
| body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: baseSha }), | ||
| }); |
There was a problem hiding this comment.
Reuse or resume the deterministic GitHub branch
The UI derives conversationId from the stable project ID and revision, so every retry targets the same branch, but this code unconditionally creates that branch and treats GitHub's already-exists response as failure. A successful publish retried after reload—or any attempt retried after a later blob, commit, or PR step failed—can never resume and leaves partial external state behind; inspect the existing branch/PR and return or continue the idempotent operation.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
| const [alert, setAlert] = useState("Not approved"); | ||
| const coin = market.snapshot?.coins.find((item) => item.symbol === active.symbol); | ||
| const marketContext = coin ? coin.symbol + " · " + coin.marketCapLabel + " market cap · " + changeLabel(coin.change24h) : active.context; | ||
| return <Shell state="Fixture wallet events · no custody or trading"><div className="split"><Card title="Tracked wallets" label="MONITORING ONLY"><label htmlFor="wallet">Wallet address</label><div className="inline"><input id="wallet" placeholder="0x… or Solana address" value={wallet} onChange={(event) => setWallet(event.target.value)} /><Button onClick={() => setWallet("")}>Save locally</Button></div><p className="muted">Remote wallet CRUD stays Setup required until the documented provider confirms it.</p><div className="choices">{events.map((event) => <button className={active.id === event.id ? "choice active" : "choice"} type="button" key={event.id} onClick={() => setActive(event)}><strong>{event.wallet}</strong><span>{event.action}</span></button>)}</div></Card><Card title="Enrichment context" label="DROPS TAB /COINS + RULES"><Pill>{active.wallet}</Pill><h3>{active.action}</h3><p>{marketContext}</p><p className="muted">Unlock and funding context is not claimed by this coins-only capability.</p><div className="metrics two"><Metric label="Relevance" value="82 / 100" detail="Rule-based demo score" /><Metric label="Market evidence" value={market.snapshot?.evidence.verified ? "Verified" : "Demo"} detail="Provider state shown above" /></div><div className="actions"><Button onClick={() => setAlert("Approved locally · Telegram setup required")}>Approve alert</Button><Pill>{alert}</Pill></div></Card></div><Card title="Event workflow" label="NORMALIZE → ENRICH → SCORE → APPROVE"><Steps items={[active.action, marketContext, alert]} /></Card></Shell>; |
There was a problem hiding this comment.
Make Save locally retain the entered wallet
In the generated smart-money-copy product, clicking the primary “Save locally” action only clears wallet; the tracked-wallet collection is the immutable fixture array, so the entered address is immediately lost and never appears in the monitoring workflow. Store the validated address in local state and render it as a tracked wallet so this preset's core action is actually runnable.
AGENTS.md reference: AGENTS.md:L8-L8
Useful? React with 👍 / 👎.
| const previewUrl = sandboxState?.status === "running" | ||
| ? verifiedProjectV2PreviewUrl(project.preview) | ||
| : null; |
There was a problem hiding this comment.
Bind the iframe to the provider-confirmed preview URL
Whenever the Sandbox itself is running, this renders the mutable persisted project.preview.url rather than the previewUrl and command receipt returned by the runtime status request. If the preview process stopped, the Sandbox restarted, or client-restored metadata contains an old URL, an unrelated or stale page is presented as the live verified preview; require a matching non-null provider status URL before enabling the iframe.
AGENTS.md reference: AGENTS.md:L111-L111
Useful? React with 👍 / 👎.
| ["status", "logs", "cancel"].includes(action) && | ||
| !headerCredential(request, "x-vercel-access-token") | ||
| ) { | ||
| throw new VercelDeploymentError( | ||
| "Connect a session-only Vercel token to inspect or cancel an existing deployment.", | ||
| 403, | ||
| "VERCEL_CONNECTION_REQUIRED", | ||
| ); |
There was a problem hiding this comment.
Use platform credentials for owned deployment management
Members can create a deployment with the configured platform token when no session token is supplied, but every later status, logs, or cancel request in that same mode is rejected here before the actor-owned deployment receipt is checked. Supplying a personal token generally cannot manage a deployment owned by the platform project, so successfully created platform previews cannot be inspected or canceled after the initial response; allow the server-owned credentials after validating ownedDeploymentId.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
| this.#project = await this.#repository.saveAuthorized( | ||
| this.actorId, | ||
| next, | ||
| expectedRevision, | ||
| ); |
There was a problem hiding this comment.
Report checkpoint restoration only after runtime sync
The restored Project V2 revision is committed to private storage before the Sandbox is ensured and restored. If either following provider operation fails, the endpoint reports restoration failure and the UI does not reload, even though canonical source has already been replaced; the user's next action then encounters an unexpected revision conflict. Stage the runtime restore before the CAS, or return an explicit applied-source receipt that makes the client reload the committed revision.
AGENTS.md reference: AGENTS.md:L109-L109
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
components/drops-studio.tsx (1)
1602-1631: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
cloudSavedconflates two independent saves and under-reports the account save.
cloudSaved = builderSnapshotSavedmeans a successfulsaveMemberProjectToCloudis reported as not cloud-saved whenever the V2 snapshot save failed, so the user is told the project only lives in this browser while a durable account revision exists. Track the two results separately and compose the message from both.🐛 Proposed fix
- await saveMemberProjectToCloud(project, 0); - cloudSaved = builderSnapshotSaved; + await saveMemberProjectToCloud(project, 0); + cloudSaved = true;and branch the toast on
cloudSaved && builderSnapshotSaved,cloudSaved, thenbuilderSnapshotSaved.🤖 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 `@components/drops-studio.tsx` around lines 1602 - 1631, Update the save result tracking in the project-opening flow so the successful saveMemberProjectToCloud call independently sets cloudSaved, without depending on builderSnapshotSaved. Adjust the toast conditions to check cloudSaved && builderSnapshotSaved first, then cloudSaved, then builderSnapshotSaved, preserving the existing messages and fallback behavior.components/project-studio.tsx (2)
626-654: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winProject V2 cloud write is coupled to every legacy save.
persistProjectnow writesnext.projectV2on every project save, even when only spec/HTML changed. Combined withprojectV2CloudRevisionRef.current ?? 0, an un-hydrated ref (e.g. cloud sync becoming available after load) will send revision0and trigger a spuriousPROJECT_V2_REVISION_CONFLICTthat blocks the whole save path. Consider skipping the V2 write when the V2 content hash is unchanged, or lazily resolving the current storage revision before writing.🤖 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 `@components/project-studio.tsx` around lines 626 - 654, Decouple the Project V2 write in persistProject from saves where the V2 content is unchanged, using the existing V2 content hash/state to detect changes before calling saveProjectV2ToCloud. Ensure an unhydrated projectV2CloudRevisionRef does not send revision 0; if a V2 write is required, resolve the current storage revision first and update the ref before saving, while preserving legacy-only save behavior.
2461-2480: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winToast text is wrong for the Project V2 archive path.
When the V2 branch runs, the fixed toast still says "Runnable app + source ZIP downloaded" (and
Git-ready deployment package created for …), which misdescribes the exported artifact.🐛 Proposed fix
+ const v2Archive = Boolean( + currentProject.projectV2 && + currentProject.projectV2.manifest.framework.name !== "legacy-html", + ); setToast( nextHost ? `Git-ready deployment package created for ${nextHost}` - : "Runnable app + source ZIP downloaded", + : v2Archive + ? "Project V2 source archive downloaded" + : "Runnable app + source ZIP downloaded", );🤖 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 `@components/project-studio.tsx` around lines 2461 - 2480, Update the toast selection after the archive download in the project export flow to distinguish the Project V2 branch from the legacy archive branch. Use wording that accurately describes the V2 exported archive, including the nextHost variant if applicable, while preserving the existing legacy toast text for the projectArchive path.
🟠 Major comments (29)
lib/project-data/backend.ts-73-122 (1)
73-122: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winA corrupt web-storage entry becomes permanently unrecoverable.
readmaps any malformed/foreign-project value tostorage_unavailable, and bothcompareAndSwapanddeleteProjectcallreadfirst — so once the key holds garbage (or a snapshot for a differentprojectId), the user can neither overwrite nor delete it. Treat a corrupt entry as absent for the write/delete paths (e.g.removeItemthen continue) instead of hard-failing forever.🤖 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 `@lib/project-data/backend.ts` around lines 73 - 122, Update the storage handling used by compareAndSwap and deleteProject so malformed or foreign-project entries are treated as absent: remove the invalid value and continue with the expected empty revision instead of propagating read’s storage_unavailable error. Preserve read’s existing error behavior for callers that only load data, and ensure valid snapshots still undergo normal revision conflict checks.lib/github-integration.ts-385-396 (1)
385-396: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftPublish is not idempotent: a retry with the same
conversationIdfails on branch creation and leaves partial state.
branchis derived deterministically fromconversationId, so a retried or resumed publish hitsPOST /git/refsfor an existing ref, GitHub answers 422, andrequestJsonthrows — after the first attempt may already have created the branch/blobs. Detect the existing-ref case (orGETthe ref first) and continue from it instead of aborting.🤖 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 `@lib/github-integration.ts` around lines 385 - 396, The publish flow around the deterministic branch in the relevant integration method must be idempotent: before creating the ref, detect whether `refs/heads/${branch}` already exists and reuse its SHA, or handle the existing-ref response without aborting. Only create the branch when absent, then continue subsequent publish steps using the resolved ref while preserving failures for unrelated GitHub errors.Source: Coding guidelines
lib/github-integration.ts-428-431 (1)
428-431: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winBranch ref path is percent-encoded, so the slash in
drops-studio/<id>becomes%2F.encodeURIComponent(branch)escapes the separator that GitHub'sgit/refs/heads/{ref}path requires literally, so the ref update targets a non-existent ref; the test mock happens to match the encoded form and hides it.
lib/github-integration.ts#L428-L431: interpolate the branch without percent-encoding the/(branch names are already normalized bysafeBranchSuffix), e.g. encode each segment separately.tests/github-integration.test.mjs#L65-L65: update the mock and assertion to expect/git/refs/heads/drops-studio/thread-42.🤖 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 `@lib/github-integration.ts` around lines 428 - 431, Update the branch ref URL construction in lib/github-integration.ts lines 428-431 to preserve the literal slash in branch names while encoding individual segments, using the normalized value from safeBranchSuffix; update the mock and assertion in tests/github-integration.test.mjs line 65 to expect /git/refs/heads/drops-studio/thread-42.lib/github-integration.ts-157-201 (1)
157-201: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winNo timeout on outbound GitHub calls.
requestJson(and the token fetch at Line 222) have noAbortSignal, so a hung GitHub connection pins a request thread until the platform's function timeout — andimportGitHubRepository/publishProjectToGitHubissue many of these serially. The provider calls inapp/api/agent/plan/route.tsuseAbortSignal.timeout(45_000); match that here.🛡️ Proposed fix
const response = await fetchImpl(`${GITHUB_API}${path}`, { ...init, headers: { @@ }, cache: "no-store", + signal: init.signal ?? AbortSignal.timeout(20_000), });🤖 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 `@lib/github-integration.ts` around lines 157 - 201, Update requestJson and the token-fetch flow used by importGitHubRepository and publishProjectToGitHub to pass AbortSignal.timeout(45_000) to every outbound GitHub fetch, including the token request near the referenced flow. Preserve existing request options while ensuring each provider call is bounded by the 45-second timeout used in app/api/agent/plan/route.ts.Source: Coding guidelines
app/api/project-data/route.ts-106-117 (1)
106-117: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBound the project-data body before buffering.
request.text()reads the full stream into memory before the size check, so chunked uploads can bypasscontent-lengthand spike memory. ReusereadBoundedRequestBodyhere.🤖 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 `@app/api/project-data/route.ts` around lines 106 - 117, Update requestBody to use the existing readBoundedRequestBody helper instead of request.text() and the subsequent encoded-size check, ensuring the request stream is limited while being read and preserving the quota_exceeded behavior for oversized bodies.app/api/deployments/vercel/route.ts-274-301 (1)
274-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRead-only actions consume the daily deploy budget.
status,logs, andcancelshare thevercel-project-deploynamespace with a 24-hour max of 12. A client polling deployment status (seecomponents/project-v2-studio-surface.tsxLine 822) exhausts the daily deployment allowance after a dozen polls and then gets 429 for real deploys. Consider consuming the limit afteractionis known, with a separate higher-max namespace for the inspect/cancel actions.🤖 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 `@app/api/deployments/vercel/route.ts` around lines 274 - 301, The deployment rate limit is consumed before determining whether the request is read-only. In the route handler, parse the request and resolve action before calling consumeRequestLimit; use the existing deployment namespace and limits only for deploy actions, and apply a separate higher-cap namespace for status, logs, and cancel while preserving their authorization checks and responses.lib/vercel-deployment.ts-175-189 (1)
175-189: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winNo timeout on any provider fetch. Neither
vercelJsonnor the logs fetch passes anAbortSignal, so a stalled Vercel connection holds the route handler until the platform's 300 s ceiling — andwaitForVercelDeploymentcan stall on its very first poll despite its own bounded deadline. Addsignal: AbortSignal.timeout(...)(respecting any caller-suppliedinit.signal).🛠️ Bound each provider call
async function vercelJson( path: string, credentials: VercelDeploymentCredentials, fetchImpl: FetchLike, init: RequestInit = {}, ): Promise<Record<string, unknown>> { const response = await fetchImpl(`${VERCEL_API_ORIGIN}${path}${query(safeIdentifier(credentials.teamId))}`, { ...init, headers: { authorization: `Bearer ${safeToken(credentials.accessToken)}`, "content-type": "application/json", ...(init.headers ?? {}), }, cache: "no-store", + signal: init.signal ?? AbortSignal.timeout(30_000), });Also applies to: 339-345
🤖 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 `@lib/vercel-deployment.ts` around lines 175 - 189, Add a bounded abort signal to provider requests in vercelJson and the logs-fetch path, using AbortSignal.timeout with the established provider timeout duration. Preserve caller cancellation by combining the timeout with any existing init.signal rather than overwriting it, so every Vercel fetch—including the first waitForVercelDeployment poll—cannot stall indefinitely.app/api/projects/v2/route.ts-83-94 (1)
83-94: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winSame-origin check ignores the proxy-visible origin. Both
app/api/deployments/vercel/route.ts(Lines 50-55) andapp/api/integrations/github/route.ts(Lines 42-47) compare theOriginheader against${x-forwarded-proto}://${host}in addition torequest.nextUrl.origin. This route compares only againstnextUrl.origin, so behind a proxy/CDN where the internal origin differs from the browser-visible one, every PUT/DELETE sync fails with 403.🛠️ Align with the sibling routes
const origin = request.headers.get("origin"); if (!origin && process.env.NODE_ENV !== "production") return; try { - if (!origin || new URL(origin).origin !== request.nextUrl.origin) throw new Error(); + if (!origin) throw new Error(); + const host = request.headers.get("host")?.split(",")[0]?.trim(); + const protocol = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().replace(/:$/, "") + || request.nextUrl.protocol.replace(/:$/, ""); + const visible = host ? `${protocol}://${host}` : request.nextUrl.origin; + const parsed = new URL(origin).origin; + if (parsed !== request.nextUrl.origin && parsed !== visible) throw new Error(); } catch {🤖 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 `@app/api/projects/v2/route.ts` around lines 83 - 94, Update requireSameOrigin to accept the proxy-visible origin derived from x-forwarded-proto and host, alongside request.nextUrl.origin, matching the validation used by the sibling deployment and GitHub routes. Preserve rejection of missing or non-matching origins and the existing development-mode exception.lib/legacy-html-runtime-adapter.ts-219-262 (1)
219-262: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftCheckpoint capture/restore contract allows partial snapshots to delete files.
captureCheckpoint(handle, id, revision, paths)accepts an arbitrary subset of paths, but both adapters treatcheckpoint.filesas the complete project on restore, so any uncaptured file disappears from the runtime workspace.
lib/legacy-html-runtime-adapter.ts#L219-L262: stop replacingrecord.fileswholesale on Line 251 — merge the checkpoint over existing files, or requirepathsto cover the full file set at capture time.lib/vercel-sandbox-runtime-adapter.ts#L866-L901: apply the same rule before calling#writeRevision, which materializes only the checkpoint files as the new revision root.🤖 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 `@lib/legacy-html-runtime-adapter.ts` around lines 219 - 262, Partial checkpoint restores currently replace the complete runtime file set and delete uncaptured files. In lib/legacy-html-runtime-adapter.ts lines 219-262, update captureCheckpoint/restoreCheckpoint to merge checkpoint files over the existing record.files, or enforce full-file capture; in lib/vercel-sandbox-runtime-adapter.ts lines 866-901, apply the same rule before `#writeRevision` so uncaptured files remain present.lib/vercel-sandbox-runtime-adapter.ts-917-945 (1)
917-945: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd
sortBy: "name"to thisnamePrefixquery.updatedAtis a numeric timestamp here; the missing sort key is what can makeSandbox.list()reject and stop idle cleanup from running.🤖 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 `@lib/vercel-sandbox-runtime-adapter.ts` around lines 917 - 945, Add sortBy: "name" to the provider.list query in cleanupIdle alongside namePrefix: "ds2-" so Sandbox.list accepts the request and idle cleanup can proceed. Preserve the existing limit, credentials, inspection, and cleanup behavior.lib/builder-agent/policy.ts-21-22 (1)
21-22: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
install_packageis marked external but approval is automatic.
external: truetools reach the public npm registry from the Sandbox, yetapprovaldefaults to"automatic"here, socreateBuilderToolApprovalreturns"approved"without any user decision.publish_projectis the only external tool gated on approval.🔒️ Proposed fix
- install_package: policy("runtime:network", 300_000, { external: true }), + install_package: policy("runtime:network", 300_000, { approval: "user", external: true }),As per coding guidelines: "Every external or destructive tool must require explicit approval and have timeout, quota, audit-record, bounded-output, and idempotency behavior."
🤖 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 `@lib/builder-agent/policy.ts` around lines 21 - 22, Update the install_package policy definition to require explicit user approval instead of relying on the automatic default, while preserving its external designation and existing timeout. Ensure it follows the approval configuration used by publish_project and does not alter the run_command policy.Source: Coding guidelines
lib/builder-agent/workspace.ts-671-688 (1)
671-688: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDiscarded previews leak running dev servers in the Sandbox.
#saveAndSyncsets#preview = null(Line 679) without callingthis.#runtime.stopProcess(...)on the previous preview command, andrunReleaseGatestarts a fresh preview on every attempt. Across the orchestrator's repair loop that leaves several detachednext devprocesses bound to the same Sandbox, competing for the preview port and burning vCPU until the sandbox is destroyed. Stop the prior process before dropping the reference.🔒️ Proposed fix
async `#saveAndSync`(next: ProjectV2): Promise<ProjectV2> { const expectedRevision = this.#project.revision; this.#project = await this.#repository.saveAuthorized( this.actorId, next, expectedRevision, ); this.#runtimeSyncedRevision = 0; + if (this.#preview && this.#handle) { + await this.#runtime + .stopProcess(this.#handle, this.#preview.commandId) + .catch(() => undefined); + } this.#preview = null;🤖 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 `@lib/builder-agent/workspace.ts` around lines 671 - 688, Update `#saveAndSync` to stop the existing `#preview` process through this.#runtime.stopProcess(...) before clearing the preview reference. Ensure the previous preview is stopped whenever present, then set `#preview` to null so runReleaseGate can start a fresh process without leaving detached dev servers.lib/builder-agent/providers.ts-186-231 (1)
186-231: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHand-built
Responsebreaks compressed bodies and null-body statuses.Two concrete failures in this path:
node:httpsdoes not decompress. The SDK'saccept-encodingheader is forwarded verbatim (Line 188), so acontent-encoding: gzipreply is copied into the syntheticResponsealong with the raw compressed bytes (Lines 226-230). Unlike a realfetchresponse, undici will not decode a manually constructed body, soresponse.json()fails. Forceaccept-encoding: identityand dropcontent-encoding/content-lengthwhen rebuilding headers.new Response(buffer, { status })throwsTypeErrorfor null-body statuses (204/205/304) becauseBuffer.concat([])is still a non-null body. Passnullwhen there are no bytes or the status forbids a body.🐛 Proposed fix
const request = httpsRequest(target, { method: outbound.method, - headers: Object.fromEntries(outbound.headers.entries()), + headers: { + ...Object.fromEntries(outbound.headers.entries()), + "accept-encoding": "identity", + },const headers = new Headers(); for (const [name, value] of Object.entries(incoming.headers)) { - if (value === undefined || name.toLowerCase() === "set-cookie") continue; + const key = name.toLowerCase(); + if ( + value === undefined || + key === "set-cookie" || + key === "content-encoding" || + key === "content-length" + ) continue; headers.set(name, Array.isArray(value) ? value.join(", ") : String(value)); } - finish(undefined, new Response(Buffer.concat(chunks), { status, headers })); + const body = Buffer.concat(chunks); + const nullBody = status === 204 || status === 205 || status === 304 || body.byteLength === 0; + finish(undefined, new Response(nullBody ? null : body, { status, headers }));🤖 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 `@lib/builder-agent/providers.ts` around lines 186 - 231, Update the custom-provider HTTPS request in the outbound request flow to force the request Accept-Encoding to identity. When rebuilding response headers, omit content-encoding and content-length, and construct the Response with a null body for empty responses or body-forbidden statuses (204, 205, and 304); otherwise preserve the collected buffer body.lib/builder-agent/orchestrator.ts-257-271 (1)
257-271: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winNo overall budget across repair attempts.
Each of the up to four attempts gets a fresh
AbortSignal.timeout(AGENT_TIMEOUT_MS)and each iteration runs a fullrunReleaseGate({ install: true })(install → typecheck → lint → tests → build → preview → browser). Worst case is ~16 minutes of provider time plus four complete Sandbox gates for a single request, which is both a latency and a Sandbox-cost hazard. A single deadline created before the loop and reused asabortSignalbounds the whole turn.♻️ Proposed fix
+ const deadline = AbortSignal.timeout(AGENT_TIMEOUT_MS); for (let attempt = 0; attempt <= MAX_AUTOMATIC_REPAIRS; attempt += 1) { let output: BuilderAgentRunnerOutput; try { output = await runner.generate({ prompt: agentPrompt(request, attempt, gate), - abortSignal: AbortSignal.timeout(AGENT_TIMEOUT_MS), + abortSignal: deadline, });🤖 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 `@lib/builder-agent/orchestrator.ts` around lines 257 - 271, Use one overall deadline for the repair loop in the orchestrator, creating the timeout AbortSignal before the attempt loop and reusing it in every runner.generate call instead of creating a fresh timeout per attempt. Preserve the existing retry count and gate behavior while ensuring all automatic repairs share the same AGENT_TIMEOUT_MS budget.lib/project-template-ui.ts-313-328 (1)
313-328: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
String.replacewith a string pattern interprets$sequences inmodel, corrupting generated sources.
modelis JSON derived from user/AI-controlled values (spec.name,spec.blueprint.modules,emptyState). A$&,$`,$'or$1anywhere in those strings is expanded as a replacement pattern, so the emittedcrypto-product.tsxno longer contains the intended product model and can fail to parse. Use a replacer function (orreplaceAllwith a function) to disable pattern expansion.🐛 Proposed fix
return selectCategoryComponent( - COMPONENT_TEMPLATE.replace("__PRODUCT_MODEL__", model), + COMPONENT_TEMPLATE.replace("__PRODUCT_MODEL__", () => model), COMPONENT_NAME_BY_PRESET[spec.presetId], );🤖 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 `@lib/project-template-ui.ts` around lines 313 - 328, Update the String.replace call in the source generation flow around COMPONENT_TEMPLATE and selectCategoryComponent to pass the replacement through a callback function rather than a string, preventing $ sequences in model from being interpreted as replacement patterns. Preserve the existing __PRODUCT_MODEL__ substitution and generated output for all other values.components/drops-studio.tsx-1574-1584 (1)
1574-1584: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winA Project V2 materialization failure now fails the whole V1 build.
materializeProjectV2Templatevalidates and hashes the full V2 file set and throws on any violation (seelib/project-template-materializer.tsLines 240/307). Placing it inline after the quality gate means a V2-only defect discards an otherwise valid compiled V1 project instead of degrading to the legacy artifact.🛡️ Proposed fix
- const projectV2 = await materializeProjectV2Template({ - id: projectId, - spec, - now, - }); + let projectV2: GeneratedProject["projectV2"]; + try { + projectV2 = await materializeProjectV2Template({ id: projectId, spec, now }); + } catch { + projectV2 = undefined; + } const project: GeneratedProject = { id: projectId, spec, html, - projectV2, + ...(projectV2 ? { projectV2 } : {}),🤖 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 `@components/drops-studio.tsx` around lines 1574 - 1584, Isolate the materializeProjectV2Template call in the V1 build flow so its validation failure does not abort the otherwise successful V1 artifact. Catch the materialization error, continue constructing the GeneratedProject from the compiled V1 fields, and include projectV2 only when materialization succeeds; preserve the existing V1 quality-gate behavior.components/project-studio.tsx-1602-1621 (1)
1602-1621: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winDuplicate provider header/model resolution.
This block re-implements
requestHeaders(provider)/providerSelection(provider)fromcomponents/project-v2-studio-surface.tsx(lines 102-134), and already diverges: the surface version also sendsx-provider-keyforcustom, while this one sends it forcustomtoo but skipsgateway/freedifferently. Extract one shared helper so the builder-agent contract stays consistent.🤖 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 `@components/project-studio.tsx` around lines 1602 - 1621, Replace the duplicated provider header and model resolution around activeProvider with shared helpers matching requestHeaders(provider) and providerSelection(provider) from project-v2-studio-surface.tsx. Reuse those helpers for the builder-agent request so key handling, provider exceptions, and model selection remain consistent across both surfaces.components/project-v2-workspace.tsx-1042-1056 (1)
1042-1056: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
allow-scripts+allow-same-originlets untrusted preview code drop its own sandbox.The preview renders AI-generated application code. With both tokens the framed document can remove the sandbox attribute for its own origin and gains full access to storage/cookies on the sandbox host; if a preview URL ever resolves same-origin with Studio, it becomes full DOM access to this app. Drop
allow-same-originunless a concrete Next.js preview requirement is documented, and confirm the sandbox host is always a distinct origin.🤖 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 `@components/project-v2-workspace.tsx` around lines 1042 - 1056, Update the iframe sandbox configuration in the preview rendering block to remove allow-same-origin, retaining only the permissions required for the verified preview. Confirm that previewUrl resolves to a distinct origin from Studio and document any concrete framework requirement before adding same-origin access back.Source: Linters/SAST tools
components/project-v2-studio-surface.tsx-362-446 (1)
362-446: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy liftSnapshot sync re-runs on every project revision.
syncSnapshotcloses over the wholeprojectobject, so its identity changes on each revision and the effect at lines 431-446 re-issues/api/accessplus a fullloadProjectV2FromCloudafter every file save. On a project with many files that is a heavy, repeated round-trip on the editing hot path. Consider keying the callback onproject.idand reading the latest project from a ref.🤖 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 `@components/project-v2-studio-surface.tsx` around lines 362 - 446, Prevent snapshot synchronization from re-running on every project revision: update syncSnapshot to depend on project.id rather than the full project object, and read the latest project through a maintained ref when comparing or saving snapshots. Keep the effect’s existing initial sync and status handling unchanged while ensuring project.id changes still create a new callback and synchronize the new project.components/project-v2-studio-surface.tsx-728-775 (1)
728-775: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
runTaskhas no in-flight guard and clobbersbusy.Unlike
runBuilder, this path never checksbusy, so a second task (or a task started while a build is running) overwritesbusyand thefinallyclears it while the first operation is still pending — re-enabling all controls mid-run.setBusy(null)also runs without amounted.currentcheck, unlike the rest of the component.🐛 Proposed fix
async function runTask(taskId: string) { + if (busy) return; const task = project.tasks.find((candidate) => candidate.id === taskId); if (!task) throw new Error("The selected task is not declared by this project."); setBusy(`task:${taskId}`); @@ } finally { - setBusy(null); + if (mounted.current) setBusy(null); } }🤖 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 `@components/project-v2-studio-surface.tsx` around lines 728 - 775, Update runTask to reject starting when another operation is already in flight, using the existing busy state/guard pattern from runBuilder so concurrent tasks cannot overwrite it. Ensure its cleanup only calls setBusy(null) when the component is still mounted, matching the established mounted.current handling elsewhere in the component.lib/artifact-security.ts-5-7 (1)
5-7: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winUpdate the Vercel token matcher
lib/artifact-security.ts:27-32only coversvercel_andvca_, but Vercel tokens usevcp_,vci_,vca_,vcr_, andvck_. Expand the regex so leaked tokens still get redacted.🤖 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 `@lib/artifact-security.ts` around lines 5 - 7, Update the Vercel token matcher in the artifact security patterns to recognize prefixes vcp_, vci_, vca_, vcr_, and vck_, while preserving redaction of existing vercel_ tokens and the surrounding matcher behavior.lib/request-rate-limit.ts-100-105 (1)
100-105: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
VERCELshould not count as Blob readiness (lib/request-rate-limit.ts:100-105). RequireVERCEL_OIDC_TOKEN(or another real credential) here; otherwise Blob-backed rate limiting will still be attempted, retried, and only then fall back.🤖 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 `@lib/request-rate-limit.ts` around lines 100 - 105, Update durableBackendConfigured to stop treating process.env.VERCEL as Blob readiness; require BLOB_READ_WRITE_TOKEN or BLOB_STORE_ID together with VERCEL_OIDC_TOKEN so Blob-backed rate limiting is only attempted when a real credential is available.lib/drops-platform/dropstab.ts-143-176 (1)
143-176: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftRequire response-backed evidence before claiming live DropsTab data.
provider: "dropstab"plus a non-demo mode is caller-supplied metadata, not proof of a successful provider response. This currently marks data as verified, labels it live, and reports endpoints available without provider evidence.
lib/drops-platform/dropstab.ts#L143-L176: require verified response evidence in the input before settingproviderVerified, live labeling, or endpoint availability.tests/drops-platform-contracts.test.mjs#L50-L69: supply verified-response evidence for the live case and add an enum-only case that remains unverified.As per coding guidelines, never claim connections succeeded without verified provider evidence.
🤖 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 `@lib/drops-platform/dropstab.ts` around lines 143 - 176, Require explicit verified-response evidence in createDropsTabProviderEvidence before setting providerVerified, live labels, or endpoint availability; provider and non-demo mode alone must remain unverified. Update lib/drops-platform/dropstab.ts (143-176) and adjust tests/drops-platform-contracts.test.mjs (50-69) to provide evidence for the live case and cover an enum-only case that remains unverified.Source: Coding guidelines
lib/drops-platform/primitives.ts-35-39 (1)
35-39: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftImplement the advertised sorting and search behaviors.
The generated “sortable” market table has no sorting logic, and coin search emits only an uncontrolled input. These contracts currently generate static UI while describing runnable features.
lib/drops-platform/primitives.ts#L35-L39: generate sortable market rows and state/controls needed to change sort order.lib/drops-platform/primitives.ts#L45-L49: generate a client-side controlled query that filters the normalized coin snapshot.tests/drops-platform-contracts.test.mjs#L87-L106: materialize and render generated modules, then verify sorting and filtering rather than only matching source strings.As per coding guidelines, presets must produce category-native, editable, runnable, publishable products and must not misrepresent static screens as completed products.
🤖 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 `@lib/drops-platform/primitives.ts` around lines 35 - 39, Update the market-table preset in lib/drops-platform/primitives.ts:35-39 to generate client-side sort state, controls, and row ordering for the advertised market fields; update the coin-search preset in lib/drops-platform/primitives.ts:45-49 to use a controlled query that filters the normalized coin snapshot; extend tests/drops-platform-contracts.test.mjs:87-106 to materialize and render both generated modules and assert sorting and filtering behavior rather than source-string presence.Source: Coding guidelines
lib/drops-platform/primitives.ts-39-39 (1)
39-39: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winEmit a valid table structure.
A
role="row"requires cell children, but this generated table emits raw text inside each row. Use nativetable/tr/tdelements (or complete ARIA cell roles) so generated projects do not ship an Axe violation. As per coding guidelines, serious or critical Axe findings block completion.🤖 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 `@lib/drops-platform/primitives.ts` at line 39, Update the DropsMarketTable markup in moduleSource to emit a valid native table structure: use table with rows containing td cells, while preserving the existing market label, item mapping, symbol fallback, and keys. Do not leave raw text directly under row elements or rely on incomplete ARIA roles.Source: Coding guidelines
lib/drops-platform/dropstab.ts-124-125 (1)
124-125: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winReject out-of-range rate-limit timestamps.
nonNegativeIntegerstill allows values that overflow theDaterange after multiplying by 1,000, sotoISOString()can throw on a malformed header. Clamp the converted milliseconds to the validDaterange before serializing.🤖 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 `@lib/drops-platform/dropstab.ts` around lines 124 - 125, Update the rate-limit timestamp handling in the resetAt assignment to clamp resetSeconds converted to milliseconds within the valid Date range before constructing the Date and calling toISOString(). Preserve the existing behavior for valid values and continue assigning retryAfterMs unchanged.lib/project-v2-validator.ts-371-380 (1)
371-380: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMissing duplicate-name check for
environmententries.
assertUniqueIdsis called for integrations, permissions, tasks, runs, logs and checkpoints, but not forproject.environment, even though each entry'snamefunctions as its identity key. Two entries with the samenamebut conflictingsecret/scope/requiredvalues would pass validation, leaving ambiguous env-var resolution downstream.🛡️ Proposed fix
assertUniqueIds(project.checkpoints, "Project checkpoints"); + const environmentNames = new Set<string>(); + for (const variable of project.environment) { + if (environmentNames.has(variable.name)) { + throw new Error(`Project environment contains duplicate name ${variable.name}.`); + } + environmentNames.add(variable.name); + }🤖 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 `@lib/project-v2-validator.ts` around lines 371 - 380, Update validateProjectV2 to validate uniqueness of project.environment entries using their name as the identity key, alongside the existing assertUniqueIds checks. Ensure duplicate environment names are rejected before downstream validation continues, while preserving the current checks for all other project collections.lib/project-v2-sync-client.ts-47-98 (1)
47-98: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd request timeouts to cloud sync fetches.
None of
loadProjectV2FromCloud,saveProjectV2ToCloud, ordeleteProjectV2FromCloudset asignal/timeout, unlike other builder fetches in this codebase (e.g.AbortSignal.timeout(120_000)inproject-studio.tsx). A stalled/api/projects/v2request can hang the Studio save/load/delete flow indefinitely.♻️ Proposed fix (repeat for each fetch)
const response = await fetch(`/api/projects/v2?id=${encodeURIComponent(projectId)}`, { credentials: "same-origin", cache: "no-store", headers: { accept: "application/json" }, + signal: AbortSignal.timeout(20_000), });🤖 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 `@lib/project-v2-sync-client.ts` around lines 47 - 98, Add a request timeout signal using the established 120-second timeout pattern to the fetch options in loadProjectV2FromCloud, saveProjectV2ToCloud, and deleteProjectV2FromCloud. Preserve all existing request behavior while ensuring stalled cloud-sync requests are aborted.lib/project-v2-migration.ts-20-53 (1)
20-53: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winSet
project-datatosetup-requiredin the migration manifest.
Migrated V1 projects shouldn’t advertise Project Data as connected; the V2 template and cost-controls doc both requiresetup-required, and this path has no durable backend behind it.🤖 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 `@lib/project-v2-migration.ts` around lines 20 - 53, Update the project-data entry in defaultIntegrations to use status "setup-required" instead of "available"; leave its other manifest fields unchanged.Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 81fe1304-cef6-423b-a3af-38203f5b1e94
⛔ Files ignored due to path filters (11)
docs/design/v2-reference/01-project-studio.pngis excluded by!**/*.pngdocs/design/v2-reference/02-homepage.pngis excluded by!**/*.pngdocs/design/v2-reference/03-templates.pngis excluded by!**/*.pngdocs/design/v2-reference/04-ai-build-plan.pngis excluded by!**/*.pngdocs/design/v2-reference/05-integrations.pngis excluded by!**/*.pngdocs/design/v2-reference/06-workflows.pngis excluded by!**/*.pngdocs/design/v2-reference/07-preview-testing.pngis excluded by!**/*.pngdocs/design/v2-reference/08-history-checkpoints-diff.pngis excluded by!**/*.pngdocs/design/v2-reference/09-publish-deploy.pngis excluded by!**/*.pngdocs/design/v2-reference/10-new-app-ai-builder.pngis excluded by!**/*.pngpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (119)
.env.exampleAGENTS.mdDESIGN.mdapp/api/access/route.tsapp/api/agent/plan/route.tsapp/api/builder/agent/route.tsapp/api/builder/cleanup/route.tsapp/api/builder/runtime/route.tsapp/api/builder/shared.tsapp/api/deployments/vercel/route.tsapp/api/dropsbot/events/route.tsapp/api/dropsbot/webhooks/route.tsapp/api/integrations/github/route.tsapp/api/project-data/route.tsapp/api/projects/publish/route.tsapp/api/projects/v2/route.tsapp/api/workspace/patch/route.tsapp/styles/project-studio.responsive.csscomponents/drops-studio.tsxcomponents/project-studio.tsxcomponents/project-v2-code-editor.tsxcomponents/project-v2-studio-surface.module.csscomponents/project-v2-studio-surface.tsxcomponents/project-v2-workspace-model.tscomponents/project-v2-workspace.module.csscomponents/project-v2-workspace.stories.tsxcomponents/project-v2-workspace.tsxdb/project-v2-release-receipts.tsdb/project-v2-snapshots.tsdocs/BUILDER_V2.mddocs/GITHUB_AND_DEPLOY_SETUP.mddocs/SANDBOX_OPERATIONS.mddocs/V2_COST_CONTROLS.mddocs/V2_SECURITY_MODEL.mddocs/design/v2-reference/README.mde2e/contracts/member-project-cloud.spec.tse2e/contracts/project-v2-studio.spec.tse2e/fixtures/project-v2-ui-test.tse2e/products/free-prompt-game.spec.tse2e/proofs/director-flow.spec.tslib/access-tier.tslib/artifact-security.tslib/builder-agent/index.tslib/builder-agent/orchestrator.tslib/builder-agent/policy.tslib/builder-agent/providers.tslib/builder-agent/tools.tslib/builder-agent/types.tslib/builder-agent/workspace.tslib/drops-platform/dropsbot.tslib/drops-platform/dropstab.tslib/drops-platform/index.tslib/drops-platform/primitives.tslib/drops-platform/rules-engine.tslib/drops-platform/testkit.tslib/github-integration.tslib/legacy-html-runtime-adapter.tslib/project-checkpoint-v2.tslib/project-data/backend.tslib/project-data/capability.tslib/project-data/index.tslib/project-data/store.tslib/project-data/types.tslib/project-data/validation.tslib/project-file-diff.tslib/project-runtime-adapter.tslib/project-template-dropstab.tslib/project-template-materializer.tslib/project-template-ui.tslib/project-types.tslib/project-v2-export.tslib/project-v2-files.tslib/project-v2-hash.tslib/project-v2-migration.tslib/project-v2-path.tslib/project-v2-sync-client.tslib/project-v2-types.tslib/project-v2-validator.tslib/provider-response-boundary.tslib/request-rate-limit.tslib/vercel-agent-browser-checker.tslib/vercel-deployment.tslib/vercel-sandbox-runtime-adapter.tslib/workspace-ai-provider.tspackage.jsontests/access-tier.test.mjstests/artifact-security.test.mjstests/builder-agent-repair-loop.test.mjstests/builder-agent-route.test.mjstests/builder-agent-tools.test.mjstests/builder-live-flow.test.mjstests/builder-provider-security.test.mjstests/drops-platform-contracts.test.mjstests/dropsbot-webhook.test.mjstests/external-integration-route-security.test.mjstests/github-integration.test.mjstests/helpers/materialize-project-v2-starter.mjstests/platform-model-routing.test.mjstests/product-routing.test.mjstests/project-checkpoint-v2.test.mjstests/project-data-v2.test.mjstests/project-file-diff.test.mjstests/project-runtime-adapter.test.mjstests/project-template-materializer.test.mjstests/project-v2-export.test.mjstests/project-v2-files.test.mjstests/project-v2-hash.test.mjstests/project-v2-migration.test.mjstests/project-v2-path.test.mjstests/project-v2-snapshot-storage.test.mjstests/project-v2-validator.test.mjstests/project-v2-workspace.test.mjstests/vercel-agent-browser-checker.test.mjstests/vercel-deployment.test.mjstests/vercel-sandbox-runtime-cleanup.test.mjstests/vercel-sandbox-runtime.test.mjstests/workspace-ai-provider.test.mjstests/workspace-ai-route.test.mjsvercel.json
| function assertPackageManifest( | ||
| files: ProjectCanonicalSnapshotV2["files"], | ||
| manifest: ProjectManifestV2, | ||
| ): void { | ||
| const source = files["package.json"]?.content; | ||
| if (!source) throw new Error("Project V2 requires package.json."); | ||
| let value: Record<string, unknown>; | ||
| try { | ||
| const parsed = JSON.parse(source) as unknown; | ||
| if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(); | ||
| value = parsed as Record<string, unknown>; | ||
| } catch { | ||
| throw new Error("package.json must contain one valid JSON object."); | ||
| } | ||
| const scripts = parsePackageRecord(value.scripts, "scripts"); | ||
| const dependencies = parsePackageRecord(value.dependencies, "dependencies"); | ||
| const devDependencies = parsePackageRecord(value.devDependencies, "devDependencies"); | ||
| for (const name of Object.keys(scripts)) { | ||
| if (/^(?:pre|post)?(?:install|publish|pack|prepare)$/.test(name.toLowerCase())) { | ||
| throw new Error(`package.json lifecycle script ${name} is blocked.`); | ||
| } | ||
| } | ||
| assertExactVersions(dependencies); | ||
| assertExactVersions(devDependencies); | ||
| assertSameRecord(scripts, manifest.scripts, "scripts"); | ||
| assertSameRecord(dependencies, manifest.dependencies, "dependencies"); | ||
| assertSameRecord(devDependencies, manifest.devDependencies, "devDependencies"); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- lib/project-v2-validator.ts (relevant section) ---\n'
nl -ba lib/project-v2-validator.ts | sed -n '300,340p'
printf '\n--- docs/V2_SECURITY_MODEL.md (relevant section) ---\n'
nl -ba docs/V2_SECURITY_MODEL.md | sed -n '118,135p'
printf '\n--- search for lifecycle-script validation elsewhere ---\n'
rg -n "pre\\|post|lifecycle|prepare|publish|pack|install" lib docs -g '!**/node_modules/**' || trueRepository: svg8bit/drops-studio
Length of output: 249
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const names = [
'build','dev','lint','test','typecheck','start',
'prebuild','postbuild','pretest','posttest','prestart','poststart',
'preinstall','postinstall','prepare','prepublish','postpublish',
'prepack','postpack','publish'
];
function blocked(name) {
return /^(?:pre|post)?(?:install|publish|pack|prepare)$/.test(name.toLowerCase());
}
for (const n of names) {
console.log(`${n}\t${blocked(n) ? 'BLOCKED' : 'ALLOWED'}`);
}
JSRepository: svg8bit/drops-studio
Length of output: 488
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- lib/project-v2-validator.ts (relevant section) ---'
nl -ba lib/project-v2-validator.ts | sed -n '300,340p'
echo
echo '--- task schema / npm run usage ---'
rg -n "npm run|run <|script|timeoutMs|build|test|typecheck|dev" lib/project-v2-validator.ts docs/V2_SECURITY_MODEL.md -n
echo
echo '--- docs/V2_SECURITY_MODEL.md (relevant section) ---'
nl -ba docs/V2_SECURITY_MODEL.md | sed -n '120,130p'Repository: svg8bit/drops-studio
Length of output: 253
Block generic pre*/post* lifecycle scripts here (lib/project-v2-validator.ts:308-335). The current regex only blocks install/publish/pack/prepare, but npm run <script> also executes matching pre<script>/post<script> hooks, so prebuild/postbuild and pretest/posttest still slip through and can bypass the structured-argv guarantee.
🤖 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 `@lib/project-v2-validator.ts` around lines 308 - 335, Update the
lifecycle-script validation in assertPackageManifest so generic pre* and post*
scripts are rejected, including hooks such as prebuild, postbuild, pretest, and
posttest, while preserving case-insensitive matching and existing blocked
lifecycle names. Ensure the validation prevents any pre/post hook from bypassing
the structured-argv guarantee.
Summary
Validation
See committed release evidence and repository test suite. Production is unchanged.
Summary by CodeRabbit
New Features
Security & Reliability
Documentation & Tests