From b83b973dbb2e4e42e8c6de7ef5dcb54711b3a3ec Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Sun, 30 Aug 2026 19:43:55 -0400 Subject: [PATCH] fix(patches): require validated finding authority --- src/lib/tools/catalog.ts | 3 +- src/mastra/config/editor.ts | 2 +- src/mastra/tools/patch-loop.ts | 11 +- tests/integration/patch-loop-tool.test.ts | 127 ++++++++++++++-------- 4 files changed, 92 insertions(+), 51 deletions(-) diff --git a/src/lib/tools/catalog.ts b/src/lib/tools/catalog.ts index 65af5ce13..7313ca3eb 100644 --- a/src/lib/tools/catalog.ts +++ b/src/lib/tools/catalog.ts @@ -419,7 +419,8 @@ const localToolOptions: RuntimeToolOption[] = [ { id: "tool:patchLoopTool", label: "Patch loop", - description: "Start a passive patch plan from a validated finding or explicit user request.", + description: + "Plan a passive patch proposal for a persisted validated finding; direct user-request admission requires an app-owned deliberate action.", kind: "tool", risk: "passive", }, diff --git a/src/mastra/config/editor.ts b/src/mastra/config/editor.ts index 05fe0f9b4..66d1d92b1 100644 --- a/src/mastra/config/editor.ts +++ b/src/mastra/config/editor.ts @@ -235,7 +235,7 @@ export const securityResearchEditorToolConfig: Record }, patchLoopTool: { description: - "Start a passive patch loop from a validated finding or explicit user request, including branch plan, planned tests, and optional diff artifact evidence.", + "Plan a passive patch proposal for a persisted validated finding. Direct user-request admission is not exposed until the app provides a server-owned deliberate action.", }, graphQlImportTool: { description: diff --git a/src/mastra/tools/patch-loop.ts b/src/mastra/tools/patch-loop.ts index 3abae9132..7c6b3e6c9 100644 --- a/src/mastra/tools/patch-loop.ts +++ b/src/mastra/tools/patch-loop.ts @@ -55,12 +55,10 @@ type FindingRow = { export const patchLoopTool = createTool({ id: "security-patch-loop", description: - "Plans a patch loop from a validated finding or explicit user request. Coding-agent execution remains unavailable unless the app provides an isolated clone and confined ACP launcher; it never falls back to a host process.", + "Plans a patch loop from a server-resolved validated finding. Explicit user-request admission requires an app-owned action and cannot be asserted by the model. Coding-agent execution remains unavailable unless the app provides an isolated clone and confined ACP launcher; it never falls back to a host process.", inputSchema: z.object({ findingId: z.string().optional(), - explicitRequest: z.string().optional(), objective: z.string().optional(), - requestedBy: z.string().optional(), targetId: z.string().optional(), projectScoped: z.boolean().optional(), baseBranch: z.string().optional(), @@ -142,9 +140,7 @@ export const patchLoopTool = createTool({ projectId, ...(threadId ? { threadId } : {}), ...(input.findingId ? { findingId: input.findingId } : {}), - ...(input.explicitRequest ? { explicitRequest: input.explicitRequest } : {}), ...(input.objective ? { objective: input.objective } : {}), - ...(input.requestedBy ? { requestedBy: input.requestedBy } : {}), ...(input.targetId ? { targetId: input.targetId } : {}), ...(typeof input.projectScoped === "boolean" ? { projectScoped: input.projectScoped } : {}), ...(input.baseBranch ? { baseBranch: input.baseBranch } : {}), @@ -190,7 +186,10 @@ export const patchLoopTool = createTool({ status: "refused" as const, projectId, reason: result.reason, - message: result.message, + message: + result.reason === "validated-finding-or-explicit-request-required" + ? "This tool requires a persisted validated finding. Direct user-request admission requires an app-owned deliberate action." + : result.message, }; } diff --git a/tests/integration/patch-loop-tool.test.ts b/tests/integration/patch-loop-tool.test.ts index 392aa5588..7c0b7bf84 100644 --- a/tests/integration/patch-loop-tool.test.ts +++ b/tests/integration/patch-loop-tool.test.ts @@ -1,17 +1,55 @@ -import { describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { patchLoopTool } from "../../src/mastra/tools/patch-loop"; +import { withDatabase } from "../../src/server/db/client"; const requestContext = (values: Record) => ({ get: (key: string) => values[key], }); -describe("patch loop Mastra tool", () => { +const suffix = `${process.pid}-${Date.now()}`; +const projectId = `patch-tool-project-${suffix}`; +const threadId = `patch-tool-thread-${suffix}`; +const findingId = `patch-tool-finding-${suffix}`; + +describe.sequential("patch loop Mastra tool", () => { + beforeAll(async () => { + await withDatabase(async (db) => { + await db.query("INSERT INTO projects (id, name, slug) VALUES ($1, $2, $3)", [ + projectId, + "Patch tool test", + `patch-tool-${suffix}`, + ]); + await db.query("INSERT INTO chat_threads (id, project_id, title) VALUES ($1, $2, $3)", [ + threadId, + projectId, + "Patch tool test", + ]); + await db.query( + `INSERT INTO findings (id, project_id, thread_id, title, severity, status, summary) + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + findingId, + projectId, + threadId, + "Validated CSRF regression", + "high", + "validated", + "The persisted finding has deterministic reproduction evidence.", + ], + ); + }); + }); + + afterAll(async () => { + await withDatabase((db) => db.query("DELETE FROM projects WHERE id = $1", [projectId])); + }); + it("requires project context before starting patch planning", async () => { await expect( patchLoopTool.execute?.( { - explicitRequest: "Please patch the CSRF guard regression.", + findingId: "finding-1", projectScoped: true, }, { requestContext: requestContext({}) } as never, @@ -19,15 +57,47 @@ describe("patch loop Mastra tool", () => { ).rejects.toThrow(/projectId/); }); - it("starts a passive patch loop from an explicit project-scoped request", async () => { + it("plans from a persisted validated finding while keeping ACP unavailable", async () => { + const result = await patchLoopTool.execute?.( + { + findingId, + projectScoped: true, + executionMode: "coding-agent", + }, + { requestContext: requestContext({ projectId, threadId }) } as never, + ); + + expect(result).toMatchObject({ + status: "ready", + projectId, + patch: { + projectId, + threadId, + findingId, + source: "validated-finding", + gate: "validated-finding", + codingAgentRun: { + status: "unavailable", + reason: "coding-agent-runner-unavailable", + }, + }, + }); + }); + + it("does not treat legacy model-authored request fields as user authority", async () => { const result = await patchLoopTool.execute?.( { explicitRequest: "Please patch the CSRF guard regression.", - objective: "Restore CSRF token validation.", requestedBy: "user", + objective: "Restore CSRF token validation.", projectScoped: true, + executionMode: "coding-agent", + codingAgent: { + harnessId: "codex-acp", + modelRef: "llm://openai/gpt-5.4", + effectiveModelId: "gpt-5.4", + }, baseBranch: "main", - repositoryPath: "/workspace/app", testCommands: [ { command: "pnpm test -- csrf", @@ -35,45 +105,16 @@ describe("patch loop Mastra tool", () => { reason: "Regression coverage for CSRF guard.", }, ], - }, - { requestContext: requestContext({ projectId: "project-1", threadId: "thread-1" }) } as never, + } as never, + { requestContext: requestContext({ projectId, threadId }) } as never, ); - expect(result).toMatchObject({ - status: "ready", - projectId: "project-1", - patch: { - projectId: "project-1", - threadId: "thread-1", - source: "explicit-request", - objective: "Restore CSRF token validation.", - requestedBy: "user", - baseBranch: "main", - repositoryPath: "/workspace/app", - gate: "explicit-user-request", - targetScope: "project", - }, + expect(result).toEqual({ + status: "refused", + projectId, + reason: "validated-finding-or-explicit-request-required", + message: + "This tool requires a persisted validated finding. Direct user-request admission requires an app-owned deliberate action.", }); - const ready = result as { - status: "ready"; - patch?: { - branchName: string; - testCommands: unknown[]; - }; - }; - expect(ready.status).toBe("ready"); - expect(ready.patch?.branchName).toMatch( - /^patch\/project-1\/request-restore-csrf-token-validation-/, - ); - expect(ready.patch?.testCommands).toEqual([ - { - id: "test-1", - order: 1, - command: "pnpm test -- csrf", - cwd: "/workspace/app", - reason: "Regression coverage for CSRF guard.", - status: "planned", - }, - ]); }); });