Skip to content

feat: ship Project V2 sandbox builder - #3

Merged
svg8bit merged 3 commits into
mainfrom
codex/drops-studio-v2-sandbox-builder
Jul 30, 2026
Merged

feat: ship Project V2 sandbox builder#3
svg8bit merged 3 commits into
mainfrom
codex/drops-studio-v2-sandbox-builder

Conversation

@svg8bit

@svg8bit svg8bit commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • real multi-file Project V2 source model and migration
  • Vercel Sandbox Node 24 runtime with build/test/preview evidence
  • editable Project Studio files, logs, history, deploy and runnable export
  • backward-compatible V1 legacy adapter and all 12 recipes

Validation

See committed release evidence and repository test suite. Production is unchanged.

Summary by CodeRabbit

  • New Features

    • Added Project V2 Builder workspace for editing files, reviewing diffs, running checks, viewing logs, and managing previews.
    • Added verified sandbox builds, checkpoints, restore, runtime controls, and downloadable project archives.
    • Added GitHub repository inspection, import, and publishing workflows.
    • Added Vercel preview deployment, status, logs, cancellation, and rollback support.
    • Added Project Data storage APIs and Drops platform integration foundations.
  • Security & Reliability

    • Added stronger validation, rate limits, secret protection, same-origin checks, and safer provider error handling.
  • Documentation & Tests

    • Added Builder V2, security, sandbox operations, integration setup, cost-control documentation, and extensive automated coverage.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
drops-studio Ready Ready Preview Jul 30, 2026 9:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Project V2 Platform

Layer / File(s) Summary
Environment configuration and documentation
.env.example, AGENTS.md, DESIGN.md, vercel.json, package.json, docs/BUILDER_V2.md, docs/GITHUB_AND_DEPLOY_SETUP.md, docs/SANDBOX_OPERATIONS.md, docs/V2_COST_CONTROLS.md, docs/V2_SECURITY_MODEL.md, docs/design/v2-reference/README.md
New env vars, workstream rules, design references, cron job, dependencies, and comprehensive Builder V2/security/sandbox/cost documentation.
Project V2 data model, validation, hashing, files, diff, checkpoints, migration, export
lib/project-v2-types.ts, lib/project-v2-validator.ts, lib/project-v2-path.ts, lib/project-v2-hash.ts, lib/project-v2-files.ts, lib/project-checkpoint-v2.ts, lib/project-file-diff.ts, lib/project-v2-migration.ts, lib/project-v2-export.ts, lib/project-v2-sync-client.ts, lib/project-types.ts, tests/project-v2-*, tests/project-checkpoint-v2.test.mjs, tests/project-file-diff.test.mjs
Canonical ProjectV2 schema, Zod validation, deterministic hashing, safe path handling, atomic file operations, checkpoint create/restore, V1→V2 migration, ZIP export, and cloud sync client.
Snapshot and release-receipt storage
db/project-v2-snapshots.ts, db/project-v2-release-receipts.ts, tests/project-v2-snapshot-storage.test.mjs
Optimistic-concurrency snapshot persistence and server-issued private release receipts (local/durable backends).
Runtime adapters and browser checker
lib/project-runtime-adapter.ts, lib/legacy-html-runtime-adapter.ts, lib/vercel-sandbox-runtime-adapter.ts, lib/vercel-agent-browser-checker.ts, tests/project-runtime-adapter.test.mjs, tests/vercel-sandbox-runtime*.test.mjs, tests/vercel-agent-browser-checker.test.mjs
Runtime adapter contract, legacy HTML preview adapter, full Vercel Sandbox execution/checkpoint/cleanup adapter, and automated browser verification.
Builder agent
lib/builder-agent/*, tests/builder-agent-*, tests/builder-live-flow.test.mjs, tests/builder-provider-security.test.mjs
AI agent orchestration with repair loop, tool permission/audit policy, provider resolution/security, editing tools, and workspace session tying runtime+repository.
Project-data capability storage
lib/project-data/*, tests/project-data-v2.test.mjs
HMAC-signed capability tokens, memory/web-storage backends, quota-enforced CRUD store.
Shared security/rate-limit helpers
lib/access-tier.ts, lib/artifact-security.ts, lib/request-rate-limit.ts, lib/provider-response-boundary.ts, tests/access-tier.test.mjs, tests/artifact-security.test.mjs
Studio actor resolver, expanded secret detection, broadened durable rate-limit config, bounded provider JSON reader.
API routes
app/api/builder/*, app/api/projects/v2/route.ts, app/api/deployments/vercel/route.ts, app/api/integrations/github/route.ts, app/api/project-data/route.ts, app/api/agent/plan/route.ts, app/api/access/route.ts, app/api/workspace/patch/route.ts, app/api/projects/publish/route.ts, app/api/dropsbot/*, related tests
New builder/runtime/cleanup routes, Project V2 CRUD, deployment/integration routes, and OIDC/rate-limit hardening across existing routes.
GitHub and Vercel integration libraries
lib/github-integration.ts, lib/vercel-deployment.ts, related tests
Repository inspect/import/publish and preview deployment creation/polling/log libraries.
Drops platform modules
lib/drops-platform/*, tests/drops-platform-contracts.test.mjs
DropsBot/DropsTab capability contracts, generated app primitives, wallet intelligence rules engine.
Project V2 template materialization
lib/project-template-materializer.ts, lib/project-template-ui.ts, lib/project-template-dropstab.ts, related tests
Generates Next.js starter files/components/templates per preset.
Studio Builder UI
components/project-v2-studio-surface.tsx, components/project-v2-workspace.tsx, components/project-v2-workspace-model.ts, components/project-v2-code-editor.tsx, components/project-studio.tsx, components/drops-studio.tsx, CSS modules, related tests
New Builder surface/workspace UI and wiring into existing Studio components.
End-to-end tests
e2e/**
Playwright fixtures/specs for Project V2 cloud sync, studio flow, accessibility, and updated navigation selectors.

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 }
Loading
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
Loading

Possibly related PRs

  • svg8bit/drops-studio#1: Both PRs modify app/api/agent/plan/route.ts, one hardening OIDC/request handling and the other extending the planning schema.
  • svg8bit/drops-studio#2: Both PRs touch app/api/access/route.ts and app/api/agent/plan/route.ts readiness/quota logic.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: shipping the Project V2 sandbox builder.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/drops-studio-v2-sandbox-builder

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

app/api/access/route.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: expand is not a function
at Minimatch.braceExpand (/node_modules/minimatch/minimatch.js:271:10)
at Minimatch.make (/node_modules/minimatch/minimatch.js:180:33)
at new Minimatch (/node_modules/minimatch/minimatch.js:156:8)
at doMatch (/node_modules/@eslint/config-array/dist/cjs/index.cjs:422:13)
at match (/node_modules/@eslint/config-array/dist/cjs/index.cjs:756:11)
at /node_modules/@eslint/config-array/dist/cjs/index.cjs:772:10
at Array.some ()
at pathMatches (/node_modules/@eslint/config-array/dist/cjs/index.cjs:767:44)
at /node_modules/@eslint/config-array/dist/cjs/index.cjs:1368:8
at FlatConfigArray.forEach ()

app/api/agent/plan/route.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: expand is not a function
at Minimatch.braceExpand (/node_modules/minimatch/minimatch.js:271:10)
at Minimatch.make (/node_modules/minimatch/minimatch.js:180:33)
at new Minimatch (/node_modules/minimatch/minimatch.js:156:8)
at doMatch (/node_modules/@eslint/config-array/dist/cjs/index.cjs:422:13)
at match (/node_modules/@eslint/config-array/dist/cjs/index.cjs:756:11)
at /node_modules/@eslint/config-array/dist/cjs/index.cjs:772:10
at Array.some ()
at pathMatches (/node_modules/@eslint/config-array/dist/cjs/index.cjs:767:44)
at /node_modules/@eslint/config-array/dist/cjs/index.cjs:1368:8
at FlatConfigArray.forEach ()

app/api/builder/agent/route.ts

Oops! Something went wrong! :(

ESLint: 9.39.4

TypeError: expand is not a function
at Minimatch.braceExpand (/node_modules/minimatch/minimatch.js:271:10)
at Minimatch.make (/node_modules/minimatch/minimatch.js:180:33)
at new Minimatch (/node_modules/minimatch/minimatch.js:156:8)
at doMatch (/node_modules/@eslint/config-array/dist/cjs/index.cjs:422:13)
at match (/node_modules/@eslint/config-array/dist/cjs/index.cjs:756:11)
at /node_modules/@eslint/config-array/dist/cjs/index.cjs:772:10
at Array.some ()
at pathMatches (/node_modules/@eslint/config-array/dist/cjs/index.cjs:767:44)
at /node_modules/@eslint/config-array/dist/cjs/index.cjs:1368:8
at FlatConfigArray.forEach ()

  • 106 others

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +392 to +395
(remote.project.revision === project.revision &&
remote.project.contentHash !== project.contentHash)
) {
const saved = await saveProjectV2ToCloud(project, remote.storageRevision);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +377 to +379
access.access?.authenticated &&
access.access.account?.connected &&
access.access.account.projectSync,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +139 to +143
// 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] },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +345 to +347
const deployment = input.wait === false
? created
: await waitForVercelDeployment({ credentials: auth, deploymentId: created.id });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread lib/github-integration.ts
Comment on lines +393 to +396
await requestJson(`${repoPath}/git/refs`, token, fetchImpl, {
method: "POST",
body: JSON.stringify({ ref: `refs/heads/${branch}`, sha: baseSha }),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +698 to +700
const previewUrl = sandboxState?.status === "running"
? verifiedProjectV2PreviewUrl(project.preview)
: null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +293 to +300
["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",
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +480 to +484
this.#project = await this.#repository.saveAuthorized(
this.actorId,
next,
expectedRevision,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

cloudSaved conflates two independent saves and under-reports the account save.

cloudSaved = builderSnapshotSaved means a successful saveMemberProjectToCloud is 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, then builderSnapshotSaved.

🤖 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 win

Project V2 cloud write is coupled to every legacy save.

persistProject now writes next.projectV2 on every project save, even when only spec/HTML changed. Combined with projectV2CloudRevisionRef.current ?? 0, an un-hydrated ref (e.g. cloud sync becoming available after load) will send revision 0 and trigger a spurious PROJECT_V2_REVISION_CONFLICT that 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 win

Toast 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 win

A corrupt web-storage entry becomes permanently unrecoverable.

read maps any malformed/foreign-project value to storage_unavailable, and both compareAndSwap and deleteProject call read first — so once the key holds garbage (or a snapshot for a different projectId), the user can neither overwrite nor delete it. Treat a corrupt entry as absent for the write/delete paths (e.g. removeItem then 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 lift

Publish is not idempotent: a retry with the same conversationId fails on branch creation and leaves partial state.

branch is derived deterministically from conversationId, so a retried or resumed publish hits POST /git/refs for an existing ref, GitHub answers 422, and requestJson throws — after the first attempt may already have created the branch/blobs. Detect the existing-ref case (or GET the 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 win

Branch ref path is percent-encoded, so the slash in drops-studio/<id> becomes %2F. encodeURIComponent(branch) escapes the separator that GitHub's git/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 by safeBranchSuffix), 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 win

No timeout on outbound GitHub calls.

requestJson (and the token fetch at Line 222) have no AbortSignal, so a hung GitHub connection pins a request thread until the platform's function timeout — and importGitHubRepository/publishProjectToGitHub issue many of these serially. The provider calls in app/api/agent/plan/route.ts use AbortSignal.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 win

Bound the project-data body before buffering. request.text() reads the full stream into memory before the size check, so chunked uploads can bypass content-length and spike memory. Reuse readBoundedRequestBody here.

🤖 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 win

Read-only actions consume the daily deploy budget. status, logs, and cancel share the vercel-project-deploy namespace with a 24-hour max of 12. A client polling deployment status (see components/project-v2-studio-surface.tsx Line 822) exhausts the daily deployment allowance after a dozen polls and then gets 429 for real deploys. Consider consuming the limit after action is 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 win

No timeout on any provider fetch. Neither vercelJson nor the logs fetch passes an AbortSignal, so a stalled Vercel connection holds the route handler until the platform's 300 s ceiling — and waitForVercelDeployment can stall on its very first poll despite its own bounded deadline. Add signal: AbortSignal.timeout(...) (respecting any caller-supplied init.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 win

Same-origin check ignores the proxy-visible origin. Both app/api/deployments/vercel/route.ts (Lines 50-55) and app/api/integrations/github/route.ts (Lines 42-47) compare the Origin header against ${x-forwarded-proto}://${host} in addition to request.nextUrl.origin. This route compares only against nextUrl.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 lift

Checkpoint capture/restore contract allows partial snapshots to delete files. captureCheckpoint(handle, id, revision, paths) accepts an arbitrary subset of paths, but both adapters treat checkpoint.files as the complete project on restore, so any uncaptured file disappears from the runtime workspace.

  • lib/legacy-html-runtime-adapter.ts#L219-L262: stop replacing record.files wholesale on Line 251 — merge the checkpoint over existing files, or require paths to 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 win

Add sortBy: "name" to this namePrefix query. updatedAt is a numeric timestamp here; the missing sort key is what can make Sandbox.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_package is marked external but approval is automatic.

external: true tools reach the public npm registry from the Sandbox, yet approval defaults to "automatic" here, so createBuilderToolApproval returns "approved" without any user decision. publish_project is 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 win

Discarded previews leak running dev servers in the Sandbox.

#saveAndSync sets #preview = null (Line 679) without calling this.#runtime.stopProcess(...) on the previous preview command, and runReleaseGate starts a fresh preview on every attempt. Across the orchestrator's repair loop that leaves several detached next dev processes 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 win

Hand-built Response breaks compressed bodies and null-body statuses.

Two concrete failures in this path:

  1. node:https does not decompress. The SDK's accept-encoding header is forwarded verbatim (Line 188), so a content-encoding: gzip reply is copied into the synthetic Response along with the raw compressed bytes (Lines 226-230). Unlike a real fetch response, undici will not decode a manually constructed body, so response.json() fails. Force accept-encoding: identity and drop content-encoding/content-length when rebuilding headers.
  2. new Response(buffer, { status }) throws TypeError for null-body statuses (204/205/304) because Buffer.concat([]) is still a non-null body. Pass null when 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 win

No 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 full runReleaseGate({ 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 as abortSignal bounds 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.replace with a string pattern interprets $ sequences in model, corrupting generated sources.

model is JSON derived from user/AI-controlled values (spec.name, spec.blueprint.modules, emptyState). A $&, $`, $' or $1 anywhere in those strings is expanded as a replacement pattern, so the emitted crypto-product.tsx no longer contains the intended product model and can fail to parse. Use a replacer function (or replaceAll with 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 win

A Project V2 materialization failure now fails the whole V1 build.

materializeProjectV2Template validates and hashes the full V2 file set and throws on any violation (see lib/project-template-materializer.ts Lines 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 win

Duplicate provider header/model resolution.

This block re-implements requestHeaders(provider) / providerSelection(provider) from components/project-v2-studio-surface.tsx (lines 102-134), and already diverges: the surface version also sends x-provider-key for custom, while this one sends it for custom too but skips gateway/free differently. 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-origin lets 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-origin unless 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 lift

Snapshot sync re-runs on every project revision.

syncSnapshot closes over the whole project object, so its identity changes on each revision and the effect at lines 431-446 re-issues /api/access plus a full loadProjectV2FromCloud after 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 on project.id and 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

runTask has no in-flight guard and clobbers busy.

Unlike runBuilder, this path never checks busy, so a second task (or a task started while a build is running) overwrites busy and the finally clears it while the first operation is still pending — re-enabling all controls mid-run. setBusy(null) also runs without a mounted.current check, 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 win

Update the Vercel token matcher lib/artifact-security.ts:27-32 only covers vercel_ and vca_, but Vercel tokens use vcp_, vci_, vca_, vcr_, and vck_. 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

VERCEL should not count as Blob readiness (lib/request-rate-limit.ts:100-105). Require VERCEL_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 lift

Require 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 setting providerVerified, 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 lift

Implement 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 win

Emit a valid table structure.

A role="row" requires cell children, but this generated table emits raw text inside each row. Use native table/tr/td elements (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 win

Reject out-of-range rate-limit timestamps. nonNegativeInteger still allows values that overflow the Date range after multiplying by 1,000, so toISOString() can throw on a malformed header. Clamp the converted milliseconds to the valid Date range 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 win

Missing duplicate-name check for environment entries.

assertUniqueIds is called for integrations, permissions, tasks, runs, logs and checkpoints, but not for project.environment, even though each entry's name functions as its identity key. Two entries with the same name but conflicting secret/scope/required values 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 win

Add request timeouts to cloud sync fetches.

None of loadProjectV2FromCloud, saveProjectV2ToCloud, or deleteProjectV2FromCloud set a signal/timeout, unlike other builder fetches in this codebase (e.g. AbortSignal.timeout(120_000) in project-studio.tsx). A stalled /api/projects/v2 request 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 win

Set project-data to setup-required in the migration manifest.
Migrated V1 projects shouldn’t advertise Project Data as connected; the V2 template and cost-controls doc both require setup-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

📥 Commits

Reviewing files that changed from the base of the PR and between b41e1f2 and 129af40.

⛔ Files ignored due to path filters (11)
  • docs/design/v2-reference/01-project-studio.png is excluded by !**/*.png
  • docs/design/v2-reference/02-homepage.png is excluded by !**/*.png
  • docs/design/v2-reference/03-templates.png is excluded by !**/*.png
  • docs/design/v2-reference/04-ai-build-plan.png is excluded by !**/*.png
  • docs/design/v2-reference/05-integrations.png is excluded by !**/*.png
  • docs/design/v2-reference/06-workflows.png is excluded by !**/*.png
  • docs/design/v2-reference/07-preview-testing.png is excluded by !**/*.png
  • docs/design/v2-reference/08-history-checkpoints-diff.png is excluded by !**/*.png
  • docs/design/v2-reference/09-publish-deploy.png is excluded by !**/*.png
  • docs/design/v2-reference/10-new-app-ai-builder.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (119)
  • .env.example
  • AGENTS.md
  • DESIGN.md
  • app/api/access/route.ts
  • app/api/agent/plan/route.ts
  • app/api/builder/agent/route.ts
  • app/api/builder/cleanup/route.ts
  • app/api/builder/runtime/route.ts
  • app/api/builder/shared.ts
  • app/api/deployments/vercel/route.ts
  • app/api/dropsbot/events/route.ts
  • app/api/dropsbot/webhooks/route.ts
  • app/api/integrations/github/route.ts
  • app/api/project-data/route.ts
  • app/api/projects/publish/route.ts
  • app/api/projects/v2/route.ts
  • app/api/workspace/patch/route.ts
  • app/styles/project-studio.responsive.css
  • components/drops-studio.tsx
  • components/project-studio.tsx
  • components/project-v2-code-editor.tsx
  • components/project-v2-studio-surface.module.css
  • components/project-v2-studio-surface.tsx
  • components/project-v2-workspace-model.ts
  • components/project-v2-workspace.module.css
  • components/project-v2-workspace.stories.tsx
  • components/project-v2-workspace.tsx
  • db/project-v2-release-receipts.ts
  • db/project-v2-snapshots.ts
  • docs/BUILDER_V2.md
  • docs/GITHUB_AND_DEPLOY_SETUP.md
  • docs/SANDBOX_OPERATIONS.md
  • docs/V2_COST_CONTROLS.md
  • docs/V2_SECURITY_MODEL.md
  • docs/design/v2-reference/README.md
  • e2e/contracts/member-project-cloud.spec.ts
  • e2e/contracts/project-v2-studio.spec.ts
  • e2e/fixtures/project-v2-ui-test.ts
  • e2e/products/free-prompt-game.spec.ts
  • e2e/proofs/director-flow.spec.ts
  • lib/access-tier.ts
  • lib/artifact-security.ts
  • lib/builder-agent/index.ts
  • lib/builder-agent/orchestrator.ts
  • lib/builder-agent/policy.ts
  • lib/builder-agent/providers.ts
  • lib/builder-agent/tools.ts
  • lib/builder-agent/types.ts
  • lib/builder-agent/workspace.ts
  • lib/drops-platform/dropsbot.ts
  • lib/drops-platform/dropstab.ts
  • lib/drops-platform/index.ts
  • lib/drops-platform/primitives.ts
  • lib/drops-platform/rules-engine.ts
  • lib/drops-platform/testkit.ts
  • lib/github-integration.ts
  • lib/legacy-html-runtime-adapter.ts
  • lib/project-checkpoint-v2.ts
  • lib/project-data/backend.ts
  • lib/project-data/capability.ts
  • lib/project-data/index.ts
  • lib/project-data/store.ts
  • lib/project-data/types.ts
  • lib/project-data/validation.ts
  • lib/project-file-diff.ts
  • lib/project-runtime-adapter.ts
  • lib/project-template-dropstab.ts
  • lib/project-template-materializer.ts
  • lib/project-template-ui.ts
  • lib/project-types.ts
  • lib/project-v2-export.ts
  • lib/project-v2-files.ts
  • lib/project-v2-hash.ts
  • lib/project-v2-migration.ts
  • lib/project-v2-path.ts
  • lib/project-v2-sync-client.ts
  • lib/project-v2-types.ts
  • lib/project-v2-validator.ts
  • lib/provider-response-boundary.ts
  • lib/request-rate-limit.ts
  • lib/vercel-agent-browser-checker.ts
  • lib/vercel-deployment.ts
  • lib/vercel-sandbox-runtime-adapter.ts
  • lib/workspace-ai-provider.ts
  • package.json
  • tests/access-tier.test.mjs
  • tests/artifact-security.test.mjs
  • tests/builder-agent-repair-loop.test.mjs
  • tests/builder-agent-route.test.mjs
  • tests/builder-agent-tools.test.mjs
  • tests/builder-live-flow.test.mjs
  • tests/builder-provider-security.test.mjs
  • tests/drops-platform-contracts.test.mjs
  • tests/dropsbot-webhook.test.mjs
  • tests/external-integration-route-security.test.mjs
  • tests/github-integration.test.mjs
  • tests/helpers/materialize-project-v2-starter.mjs
  • tests/platform-model-routing.test.mjs
  • tests/product-routing.test.mjs
  • tests/project-checkpoint-v2.test.mjs
  • tests/project-data-v2.test.mjs
  • tests/project-file-diff.test.mjs
  • tests/project-runtime-adapter.test.mjs
  • tests/project-template-materializer.test.mjs
  • tests/project-v2-export.test.mjs
  • tests/project-v2-files.test.mjs
  • tests/project-v2-hash.test.mjs
  • tests/project-v2-migration.test.mjs
  • tests/project-v2-path.test.mjs
  • tests/project-v2-snapshot-storage.test.mjs
  • tests/project-v2-validator.test.mjs
  • tests/project-v2-workspace.test.mjs
  • tests/vercel-agent-browser-checker.test.mjs
  • tests/vercel-deployment.test.mjs
  • tests/vercel-sandbox-runtime-cleanup.test.mjs
  • tests/vercel-sandbox-runtime.test.mjs
  • tests/workspace-ai-provider.test.mjs
  • tests/workspace-ai-route.test.mjs
  • vercel.json

Comment on lines +308 to +335
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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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/**' || true

Repository: 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'}`);
}
JS

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant