Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 156 additions & 5 deletions app/api/builder/agent/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ProjectRuntimeAdapter,
RuntimeAuditSink,
} from "../../../../lib/project-runtime-adapter.ts";
import type { ProjectV2 } from "../../../../lib/project-v2-types.ts";
import {
ServerBuilderAuditSink,
SnapshotBuilderProjectRepository,
Expand All @@ -57,13 +58,111 @@
projectId: z.string().regex(/^[a-z0-9][a-z0-9:._-]{0,127}$/i),
prompt: z.string().min(1).max(20_000),
mode: z.enum(["build", "edit", "repair"]),
buildRequestId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{7,95}$/).optional(),
provider: z.object({
provider: z.enum(["free", "gateway", "openai", "anthropic", "openrouter", "kimi", "custom"]),
model: z.string().min(1).max(192).optional(),
baseUrl: z.string().url().max(2_000).optional(),
}).strict(),
}).strict();

function autoBuildRunId(requestId: string): string {
return `auto:${requestId}`;
}

function withAutoBuildRun(
project: ProjectV2,
requestId: string,
status: "running" | "succeeded" | "failed" | "stopped",
): ProjectV2 {
const id = autoBuildRunId(requestId);
const prior = project.runs.find((run) => run.id === id);
const now = new Date().toISOString();
const runs = [
...project.runs.filter((run) => run.id !== id),
{
id,
taskId: "build",
projectRevision: project.revision,
status,
runtime: "vercel-sandbox" as const,
startedAt: status === "running" ? now : prior?.startedAt ?? now,
...(status === "running"
? {}
: { finishedAt: now, exitCode: status === "succeeded" ? 0 : null }),
logIds: prior?.logIds ?? [],
auditEventIds: prior?.auditEventIds ?? [],
},
].slice(-256);
const retainedRunIds = new Set(runs.map((run) => run.id));
return {
...project,
runs,
logs: project.logs.filter((log) => retainedRunIds.has(log.runId)),
updatedAt: now,
};
Comment on lines +96 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the log metadata type and every writer of project logs / runIds.
set -euo pipefail

rg -nP -C4 'ProjectV2LogMetadataV2' --type=ts | head -60

echo '--- runId assignments ---'
rg -nP -C3 '\brunId\s*:' --type=ts --type=tsx -g '!**/node_modules/**' | head -120

echo '--- writers into project.logs ---'
rg -nP -C5 '\blogs\s*:\s*\[' --type=ts -g '!**/node_modules/**' | head -120

Repository: svg8bit/drops-studio

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -u

echo '--- candidate files ---'
git ls-files | rg '(^|/)(route\.ts|.*project.*|.*builder.*|.*studio.*)\.(ts|tsx|js|jsx)$' | head -200

echo '--- target implementation ---'
sed -n '1,150p' app/api/builder/agent/route.ts 2>/dev/null || true

echo '--- log type and project log references ---'
rg -n -C3 'ProjectV2LogMetadataV2|project\.logs|logs:\s*project\.logs|runId' --glob '*.{ts,tsx,js,jsx}' . | head -300

Repository: svg8bit/drops-studio

Length of output: 28010


🏁 Script executed:

#!/bin/bash
set -u

echo '--- route claim/settle call sites ---'
rg -n -C8 'withAutoBuildRun|claimAutoBuildRequest|settleAutoBuildRequest|runIntelligentBuilderAgent|runBuilderAgent' app/api/builder/agent/route.ts

echo '--- route remainder ---'
sed -n '145,360p' app/api/builder/agent/route.ts

echo '--- workspace run/log materialization ---'
sed -n '70,165p' lib/builder-agent/workspace.ts
sed -n '780,835p' lib/builder-agent/workspace.ts

echo '--- project run/log schema and persistence validation ---'
sed -n '100,140p' lib/project-v2-types.ts
sed -n '155,180p' lib/project-v2-validator.ts
sed -n '360,400p' lib/project-v2-validator.ts

echo '--- runtime lifecycle references ---'
rg -n -C5 'toProjectExecution|materializ|runId|executeCommand|runCommand|saveAuthorized' lib/builder-agent lib/project-runtime-adapter.ts lib/vercel-sandbox-runtime-adapter.ts app/api/builder --glob '*.{ts,tsx}' | head -400

Repository: svg8bit/drops-studio

Length of output: 48773


🏁 Script executed:

#!/bin/bash
set -u

echo '--- preview run creation and live statuses ---'
rg -n -C12 'startPreview|`#preview`|runId:|exitCode: null|appendCommandMetadata' lib/builder-agent/workspace.ts lib/vercel-sandbox-runtime-adapter.ts

echo '--- retention-related tests and fixtures ---'
rg -n -C5 '256|2048|withAutoBuildRun|appendCommandMetadata|status:\s*"running"|Project log .*unknown run' --glob '*.{ts,tsx,js,jsx}' app lib e2e tests 2>/dev/null | head -300

echo '--- pure retention behavior probe ---'
python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class Run:
    id: str
    status: str

`@dataclass`
class Log:
    id: str
    runId: str

def with_auto_build_run(runs, logs, request_id, status):
    run_id = f"auto:{request_id}"
    prior = next((r for r in runs if r.id == run_id), None)
    next_runs = [r for r in runs if r.id != run_id]
    next_runs.append(Run(run_id, status))
    next_runs = next_runs[-256:]
    retained = {r.id for r in next_runs}
    return next_runs, [log for log in logs if log.runId in retained]

runs = [Run(f"run-{i}", "running" if i == 0 else "succeeded") for i in range(256)]
logs = [Log("log-live", "run-0")]
next_runs, next_logs = with_auto_build_run(runs, logs, "request-1", "running")
print("live run retained:", any(r.id == "run-0" for r in next_runs))
print("live log retained:", any(log.id == "log-live" for log in next_logs))
print("new auto run retained:", any(r.id == "auto:request-1" for r in next_runs))
PY

Repository: svg8bit/drops-studio

Length of output: 50376


🏁 Script executed:

python3 - <<'PY'
runs = [
    {"id": f"run-{i}", "status": "running" if i == 0 else "succeeded"}
    for i in range(256)
]
logs = [{"id": "log-live", "runId": "run-0"}]

next_runs = [run for run in runs if run["id"] != "auto:req-1"]
next_runs.append({"id": "auto:req-1", "status": "running"})
next_runs = next_runs[-256:]
retained = {run["id"] for run in next_runs}
next_logs = [log for log in logs if log["runId"] in retained]

print({
    "live_run_retained": "run-0" in retained,
    "live_log_retained": any(log["id"] == "log-live" for log in next_logs),
    "auto_run_retained": "auto:req-1" in retained,
})
PY

Repository: svg8bit/drops-studio

Length of output: 242


Preserve logs for active runs during retention. .slice(-256) can evict a running run, and the following filter then deletes its persisted logs during automatic-build claim or settlement. Retain active runs before pruning. The validator already rejects logs whose runId does not reference a project run.

🤖 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/builder/agent/route.ts` around lines 96 - 103, Update the
run-retention logic before the returned project object so active runs with
status “running” are retained even when they fall outside the latest 256 runs.
Build the retained run set by preserving those active runs alongside the newest
256 entries, then continue filtering logs through retainedRunIds so persisted
logs for active runs are not removed.

}

async function claimAutoBuildRequest(input: {
actorId: string;
project: ProjectV2;
repository: BuilderProjectRepository;
requestId: string;
}): Promise<{ status: "claimed" | "running"; project: ProjectV2 }> {
let project = input.project;
for (let attempt = 0; attempt < 2; attempt += 1) {
const existing = project.runs.find(
(run) => run.id === autoBuildRunId(input.requestId),
);
if (existing?.status === "running" || existing?.status === "queued") {
return { status: "running", project };
}
try {
project = await input.repository.saveAuthorized(
input.actorId,
withAutoBuildRun(project, input.requestId, "running"),
project.revision,
);
return { status: "claimed", project };
} catch (error) {
const current = await input.repository.loadAuthorized(
input.actorId,
project.id,
);
if (!current) throw error;
const claimed = current.runs.find(
(run) => run.id === autoBuildRunId(input.requestId),
);
if (claimed?.status === "running" || claimed?.status === "queued") {
return { status: "running", project: current };
}
if (attempt === 1) throw error;
project = current;
}
}
return { status: "running", project };
}

async function settleAutoBuildRequest(input: {
actorId: string;
projectId: string;
repository: BuilderProjectRepository;
requestId: string;
status: "succeeded" | "failed" | "stopped";
}): Promise<ProjectV2 | null> {
const current = await input.repository.loadAuthorized(input.actorId, input.projectId);
if (!current) return null;
const run = current.runs.find(
(item) => item.id === autoBuildRunId(input.requestId),
);
if (!run || run.status !== "running") return current;
return input.repository.saveAuthorized(
input.actorId,
withAutoBuildRun(current, input.requestId, input.status),
current.revision,
);
}

const ALL_AGENT_PERMISSIONS = new Set([
"files:read",
"files:write",
Expand Down Expand Up @@ -240,14 +339,42 @@
400,
);
}
if (parsed.data.buildRequestId && parsed.data.mode !== "build") {
return builderJson(
{
code: "BUILDER_INVALID_REQUEST",
error: "Automatic build request IDs are valid only for initial builds.",
},
400,
);
}
const repository = dependencies.repository ?? new SnapshotBuilderProjectRepository();
const project = await repository.loadAuthorized(actorId, parsed.data.projectId);
let project = await repository.loadAuthorized(actorId, parsed.data.projectId);
if (!project) {
return builderJson(
{ code: "BUILDER_PROJECT_NOT_FOUND", error: "Project V2 was not found." },
404,
);
}
if (parsed.data.buildRequestId) {
const claim = await claimAutoBuildRequest({
actorId,
project,
repository,
requestId: parsed.data.buildRequestId,
});
project = claim.project;
if (claim.status === "running") {
return builderJson(
{
code: "BUILDER_REQUEST_IN_PROGRESS",
status: "running",
project,
},
202,
);
}
}
const audit = dependencies.audit ?? new ServerBuilderAuditSink();
const sandboxRuntime =
dependencies.runtime ?? new VercelSandboxRuntimeAdapter({ audit });
Expand All @@ -264,7 +391,7 @@
try {
const session = new BuilderAgentSession({
actorId,
requestId: randomUUID(),
requestId: parsed.data.buildRequestId ?? randomUUID(),

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how BuilderAgentSession stores and consumes requestId.
set -euo pipefail

fd -t f 'session.ts' lib/builder-agent

echo '--- requestId usages in builder-agent ---'
rg -nP -C5 '\brequestId\b' lib/builder-agent --type=ts | head -150

echo '--- audit record shapes ---'
rg -nP -C5 'record\s*\(' lib/builder-agent --type=ts | head -80

Repository: svg8bit/drops-studio

Length of output: 13683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- session construction and requestId propagation ---'
rg -n -C8 'new BuilderAgentSession|buildRequestId|requestId:|runBuilderAgent|BuilderAgentSession' app lib --glob '*.{ts,tsx,js,jsx}' | head -300

echo '--- all audit sink implementations and requestId consumers ---'
rg -n -C8 'BuilderAgentAuditSink|record\(event|actorHash|requestId|rate.?limit|audit' app lib --glob '*.{ts,tsx,js,jsx}' | head -500

echo '--- candidate files ---'
fd -t f . app lib | rg '(builder|audit|session|rate|runtime|workspace)' | head -200

Repository: svg8bit/drops-studio

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- exact builder-agent audit sink references ---'
rg -n 'BuilderAgentAuditSink|MemoryBuilderAgentAuditSink|audit:\s*|audit\.record|new BuilderAgentSession|buildRequestId' app lib tests __tests__ 2>/dev/null || true

echo '--- route request construction ---'
sed -n '330,430p' app/api/builder/agent/route.ts

echo '--- complete builder-agent source file list ---'
fd -t f . lib/builder-agent app/api/builder/agent

echo '--- requestId field consumers outside declarations and event construction ---'
rg -n 'requestId' app lib --glob '*.{ts,tsx,js,jsx}' \
  | rg -v 'interface |type |requestId: context\.requestId|requestId: input\.requestId|requestId: dependencies\.services\.requestId|readonly requestId|requestId: string|requestId\?:|providerRequestId|STUDIO_BUILD_REQUEST_PARAM' \
  | head -250

Repository: svg8bit/drops-studio

Length of output: 22889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- auto-build request claim and run storage ---'
sed -n '45,175p' app/api/builder/agent/route.ts

echo '--- server builder audit sink ---'
sed -n '1,150p' app/api/builder/shared.ts

echo '--- build route completion and response paths ---'
sed -n '470,555p' app/api/builder/agent/route.ts

echo '--- runtime route session construction ---'
sed -n '180,225p' app/api/builder/runtime/route.ts

echo '--- runtime and builder audit sink types/implementations ---'
rg -n -C12 'interface RuntimeAuditSink|class .*AuditSink|ServerBuilderAuditSink|record\(event' lib app/api/builder --glob '*.{ts,tsx}'

Repository: svg8bit/drops-studio

Length of output: 28610


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

files = {
    "route": Path("app/api/builder/agent/route.ts").read_text(),
    "shared": Path("app/api/builder/shared.ts").read_text(),
    "policy": Path("lib/builder-agent/policy.ts").read_text(),
    "runtime": Path("lib/project-runtime-adapter.ts").read_text(),
}

checks = {
    "auto-build storage calls are actor-authorized": (
        "saveAuthorized(\n        input.actorId," in files["route"]
        and "loadAuthorized(\n        input.actorId," in files["route"]
    ),
    "builder audit event retains requestId and actorHash separately": (
        "requestId: input.requestId" in files["policy"]
        and "actorHash: runtimeActorHash(input.actorId)" in files["policy"]
    ),
    "runtime audit event retains requestId and actorHash separately": (
        "requestId: context.requestId" in files["runtime"]
        and "actorHash: runtimeActorHash(context.actorId)" in files["runtime"]
    ),
    "server sink writes event without a composite correlation key": (
        'JSON.stringify({ source: "drops-studio-builder", ...event })' in files["shared"]
    ),
    "memory audit sink does not key records by requestId": (
        "this.events.push(structuredClone(event))" in files["policy"]
    ),
}

for name, result in checks.items():
    print(f"{name}: {'yes' if result else 'no'}")
PY

Repository: svg8bit/drops-studio

Length of output: 468


Scope audit correlation by actor. Auto-build storage is actor-scoped, but audit events emit the client-controlled requestId as a standalone field. Require audit consumers to match both actorHash and requestId to prevent cross-actor correlation collisions.

🤖 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/builder/agent/route.ts` at line 394, Update the audit event
correlation around the requestId field to include the actorHash alongside the
requestId, and require consumers to match both values when locating auto-build
records. Preserve the existing generated requestId fallback while preventing
client-controlled IDs from correlating across actors.

project,
repository,
runtime: sandboxRuntime,
Expand All @@ -282,8 +409,9 @@
request,
parsed.data.provider,
);
const { buildRequestId: _buildRequestId, ...builderInput } = parsed.data;

Check warning on line 412 in app/api/builder/agent/route.ts

View workflow job for this annotation

GitHub Actions / ui-quality

'_buildRequestId' is assigned a value but never used
const agentRequest = {
...parsed.data,
...builderInput,
provider: remembered.selection,
approvedTools: [...approvedTools],
};
Expand Down Expand Up @@ -372,9 +500,21 @@
);
}
}
const settledProject = parsed.data.buildRequestId
? await settleAutoBuildRequest({
actorId,
projectId: result.project.id,
repository,
requestId: parsed.data.buildRequestId,
status: result.releaseGate.ok ? "succeeded" : "failed",
})
: null;
const settledResult = settledProject
? { ...result, project: settledProject }
: result;
return builderJson(
{
result,
result: settledResult,
...(intelligence
? {
intelligence: {
Expand All @@ -386,8 +526,19 @@
}
: {}),
},
result.status === "blocked" ? 422 : 200,
settledResult.status === "blocked" ? 422 : 200,
);
} catch (error) {
if (parsed.data.buildRequestId) {
await settleAutoBuildRequest({
actorId,
projectId: project.id,
repository,
requestId: parsed.data.buildRequestId,
status: request.signal.aborted ? "stopped" : "failed",
}).catch(() => undefined);
}
throw error;
} finally {
request.signal.removeEventListener("abort", abortForDisconnectedClient);
}
Expand Down
2 changes: 1 addition & 1 deletion app/api/builder/cleanup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function handleBuilderCleanupRequest(
const adapter = dependencies.runtime ?? new VercelSandboxRuntimeAdapter();
const result = await adapter.cleanupIdle({
idleBefore: new Date(now.getTime() - minutes * 60_000),
limit: 100,
limit: 50,
});
return response({
idleMinutes: minutes,
Expand Down
2 changes: 1 addition & 1 deletion app/styles/project-studio.inspector.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion components/drops-studio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1867,7 +1867,8 @@ export function DropsStudio({ hero }: { hero: ReactNode }) {
createdAt: now,
updatedAt: now,
};
const studioHref = `/studio/${project.id}?panel=director&autobuild=1`;
const buildRequestId = crypto.randomUUID();
const studioHref = `/studio/${project.id}?panel=director&autobuild=1&buildRequest=${encodeURIComponent(buildRequestId)}`;
void router.prefetch(studioHref);
void warmProjectExperience(spec);
const stored = await saveProjectSafely(project, {
Expand Down
Loading
Loading