diff --git a/sdk/typescript/_bundled_plugin/skills/deep-security-scan/SKILL.md b/sdk/typescript/_bundled_plugin/skills/deep-security-scan/SKILL.md index 09ec124a..90e00d79 100644 --- a/sdk/typescript/_bundled_plugin/skills/deep-security-scan/SKILL.md +++ b/sdk/typescript/_bundled_plugin/skills/deep-security-scan/SKILL.md @@ -19,7 +19,7 @@ Treat the discovery-to-parent handoff as a hard phase boundary: 4. Run `$codex-security:attack-path-analysis` once in compact Deep candidate mode. 5. Record complete semantic findings, coverage, and threat-model context with `record_codex_security_scan_draft`. 6. Only then call `complete_codex_security_scan`. -7. Read the completed scan with `get_codex_security_completed_scan`. +7. Use the completion metadata and generated artifact paths. Read `get_codex_security_completed_scan` only when a requested structured or benchmark output requires the full sealed documents. 8. Return a final answer or benchmark JSON only after completion succeeds and the generated `report.md` exists. Include the completion result's measured total, input, and cached input token counts in a user-facing final response, explicitly label partial coverage, and say when measurement is unavailable. Do not jump from the discovery manifest directly to completion. A returned `manifestPath` names discovery evidence, not the outer `scan-manifest.json`. @@ -128,10 +128,10 @@ After accepting the terminal manifest, continue in the same turn. A discovery ma - The workbench derives the authoritative target, scope paths, finding identities, coverage mode, and repository inventory strategy. Do not include those derived fields in draft arguments. - An MCP `-32602` input rejection or an explicitly identified pre-write coverage-semantics rejection writes no artifact. Correct the named semantic fields and retry the same scan at most twice. Stop after the first accepted draft; do not blindly retry an ambiguous write. - Detailed vulnerability write-ups and hardening are optional, exactly as in the ordinary scan. Invoke `$codex-security:vulnerability-writeup` or `$codex-security:propose-security-hardening` only when the corresponding additional output is requested. -6. After the draft succeeds, complete the scan once by calling `complete_codex_security_scan({ scanId, handoffClaimToken? })` so the workbench validates and seals the contract, generates `report.md`, and indexes findings. Read the canonical final result with `get_codex_security_completed_scan({ scanId, handoffClaimToken? })`. Do not call completion before the draft is accepted. +6. After the draft succeeds, complete the scan once by calling `complete_codex_security_scan({ scanId, handoffClaimToken? })` so the workbench validates and seals the contract, generates `report.md`, and indexes findings. Use its completion metadata; read `get_codex_security_completed_scan({ scanId, handoffClaimToken? })` only when a requested structured or benchmark output requires the full sealed documents. Do not call completion before the draft is accepted. 7. Include the completion result's measured total, input, and cached input token counts in the final user-facing response. Explicitly label partial coverage; if measurement is unavailable, say so instead of reporting zero or estimating. -If the parent cannot run a required tail phase, record the canonical draft after the bounded no-write correction above, or read the completed scan, stop immediately and surface the exact blocker. Do not call completion with missing artifacts, return a final report or no-findings result, satisfy a structured output schema, or emit benchmark JSON. +If the parent cannot run a required tail phase, record the canonical draft after the bounded no-write correction above, or retrieve completed documents required for a requested structured output, stop immediately and surface the exact blocker. Do not call completion with missing artifacts, return a final report or no-findings result, satisfy a structured output schema, or emit benchmark JSON. Keep the workbench phase monotonic. Canonical threat-model synthesis happens after discovery, so leave the live phase at discovery until validation begins rather than moving it backward to `threat_model`. Continue publishing validation, attack-path, reporting, and validated-finding progress through `update_codex_security_scan_progress`. diff --git a/sdk/typescript/tests-ts/completed-scan-handoff.test.ts b/sdk/typescript/tests-ts/completed-scan-handoff.test.ts new file mode 100644 index 00000000..6aefeb54 --- /dev/null +++ b/sdk/typescript/tests-ts/completed-scan-handoff.test.ts @@ -0,0 +1,47 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { brotliDecompressSync } from "node:zlib"; +import { expect, test } from "bun:test"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +test("does not request completed findings after a prompt-only scan", async () => { + const parts = await Promise.all( + ["000", "001"].map((part) => + readFile(join(PLUGIN_ROOT, "mcp", `server.mjs.br.part-${part}`)), + ), + ); + const runtime = brotliDecompressSync(Buffer.concat(parts)).toString("utf8"); + const source = /function promptOnlyScanResult\([^\n]*\) \{[\s\S]*?\n\}/u.exec( + runtime, + )?.[0]; + expect(source).toBeDefined(); + + const promptOnlyScanResult = new Function( + "isJsonObject2", + "string2", + "toolErrorResult", + `${source}\nreturn promptOnlyScanResult;`, + )( + (value: unknown) => value !== null && typeof value === "object", + () => ({ uuid: () => ({ safeParse: () => ({ success: true }) }) }), + (message: string) => ({ content: [{ text: message }], isError: true }), + ) as (input: { + startDisposition: string; + scan: { scanId: string; scanDir: string; handoffStatus: string }; + workspace: { results: { scanId: string } }; + }) => { content: { text: string }[]; isError?: boolean }; + + const scanId = "00000000-0000-4000-8000-000000000000"; + const result = promptOnlyScanResult({ + startDisposition: "created", + scan: { scanId, scanDir: "/tmp/scan", handoffStatus: "delivered" }, + workspace: { results: { scanId } }, + }); + + expect(result.isError).toBeUndefined(); + expect(result.content[0]?.text).toContain("complete_codex_security_scan"); + expect(result.content[0]?.text).not.toContain( + "get_codex_security_completed_scan", + ); + expect(runtime).toContain('name: "get_codex_security_completed_scan"'); +});