Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9488d0b
Simplify native draft publication and executable selection
codex Sep 29, 2026
b1e8c7d
Simplify composed scans to source grouping and host publication
codex Sep 29, 2026
4b8a8b2
Consolidate completed-merge quality checks and publication replay
codex Sep 29, 2026
a82318f
refactor: share scan accounting and prepared worker inputs
codex Sep 29, 2026
a9e9835
Consolidate native fixtures, bundle options, and lifecycle guidance
codex Sep 29, 2026
92ca4ba
Retire legacy resume cases and retain grouping boundary coverage
codex Sep 29, 2026
a23b0bb
Record whether composed scans started a paid merge
codex Sep 29, 2026
0c10246
Preserve shared home environment lookup semantics
codex Sep 29, 2026
45df36c
fix: preserve clean-scan recovery and comparison defaults
codex Sep 29, 2026
1c7df9c
Simplify scan storage, recovery, and assessment reuse
codex Sep 29, 2026
6862619
Focus composition integration on runtime wiring and current draft ide…
codex Sep 29, 2026
6e857a3
Use migrated workbench schema in SDK history fixtures
codex Sep 29, 2026
4ba91c3
refactor: narrow completed-result loading and retire migration flag
codex Sep 29, 2026
7b71fdb
Preserve sealed finalization and host-only usage semantics
codex Sep 29, 2026
8147363
Keep composition checks focused on observable runtime state
codex Sep 29, 2026
91cdb3a
Verify preserved child report and evidence filenames
codex Sep 29, 2026
dc8de9f
Remove duplicated timeout source-shape assertion
codex Sep 29, 2026
bd79c86
Align resume fixtures with explicit recovery summaries
codex Sep 29, 2026
1c63a5f
Preserve current checkpoint recovery and accepted finding identity
codex Sep 29, 2026
dee8e41
Retain new evidence from late incomplete draft writers
codex Sep 29, 2026
198acb6
Reuse resume metadata while refreshing sealed completion status
codex Sep 29, 2026
66f0613
Return verified sealed resume metadata from native registration
codex Sep 29, 2026
4ec4688
fix: align completion diagnostics and worker reuse assertions
codex Sep 29, 2026
8fe5011
Align worker permission and stopped recovery fixtures
codex Sep 29, 2026
5d2e6e0
Share the exact merge source registry with prompt construction
codex Sep 29, 2026
9419169
Recover budget-limited clean scans without a merge session
codex Sep 29, 2026
61f7c94
Exercise sealed usage failures at the status refresh
codex Sep 29, 2026
e2117d6
Preserve malformed checkpoint evidence during pending migration
codex Sep 29, 2026
c858d88
Merge upstream scan lifecycle and projection fixes
codex Sep 29, 2026
817a067
Use indexed visibility lookups for finding history
codex Sep 29, 2026
9cf4b7f
Reuse result context and completed-turn contracts
codex Sep 29, 2026
6db279e
Fix packaged runtime module and declaration contracts
codex Sep 29, 2026
5138132
Install the type dependency required by Codex SDK declarations
codex Sep 29, 2026
a5e97e6
Reuse loaded composition state in internal projections
codex Sep 29, 2026
3684273
Reuse severity totals for scan finding counts
codex Sep 29, 2026
3e89e6c
Merge current Deep Scan worker isolation and recovery fixes
codex Sep 29, 2026
2b56ad5
Use SQLite transaction contexts for simple writes
codex Sep 29, 2026
dd37888
Merge current Deep Scan worker and resume fixes
codex Sep 29, 2026
a1fa875
fix(scan): preserve sealed legacy session identity
codex Sep 29, 2026
8cc86f2
Preserve verified sealed native scan recovery
codex Sep 29, 2026
ab783a9
ci: use the job deadline for Unix test shards
codex Sep 29, 2026
c571a0b
Check installed launcher behavior in package smoke
codex Sep 29, 2026
569a3a1
merge: integrate upstream scan simplifications
codex Sep 29, 2026
2e1e27b
fix: preserve projected report references in saved findings
codex Sep 29, 2026
7c611ae
fix: preserve saved review counts in scan progress
codex Sep 29, 2026
c1a3302
fix: index pending checkpoints before publishing history
codex Sep 29, 2026
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
1 change: 0 additions & 1 deletion .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,6 @@ jobs:
sudo apt-get update
sudo apt-get install --yes ripgrep
- name: Test
timeout-minutes: 10
working-directory: sdk/typescript
env:
TEMP: ${{ runner.temp }}
Expand Down
3 changes: 3 additions & 0 deletions evals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ being embedded in its source tree or shipped npm runtime.
- [Triage finding](triage-finding/README.md): Promptfoo input contracts, OSS
calibration, and SastBench. This suite owns its private package and lockfile.

- [Completed-report merge](../sdk/typescript/scripts/merge-eval/README.md):
synthetic grouping quality checks and deterministic publication replay.

Model runs are opt-in. CI still runs the deterministic triage helper checks.
245 changes: 104 additions & 141 deletions plugins/codex-security/mcp-app/src/artifact-scan-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
type SemanticScan,
type SemanticCoverage,
} from "../../../../sdk/typescript/src/scan-semantics.js";
import { createHash, randomUUID } from "node:crypto";
import { createHash } from "node:crypto";
import { writePreparedScanDraft } from "../../../../sdk/typescript/src/scan-draft-publication.js";
import { promises as fs } from "node:fs";
import { join, sep } from "node:path";
import type * as z from "zod/v4";
Expand All @@ -26,7 +27,7 @@ import {
artifactDestination,
readArtifactJsonObject,
readArtifactText,
replaceArtifactJson,
replaceArtifactText,
} from "./artifact-io.js";
import {
loadArtifactZodSchema,
Expand Down Expand Up @@ -61,6 +62,7 @@ type PublishScanDraft = (
draft: PreparedScanDraft,
expectedDigest: string | undefined,
checkpoint: ScanDraftInput,
reconciledCheckpointIds: readonly string[],
) => Promise<void>;

const schemaDocuments = [commonSchema, scanDraftDocument] as SchemaDocument[];
Expand Down Expand Up @@ -93,13 +95,18 @@ export async function recordCodexSecurityScanDraft(
// checkpoints into them.
const preserved =
context.mode === "deep" && parsed.complete !== false
? { input: parsed, previousDigest: undefined }
? { input: parsed, previousDigest: undefined, checkpointIds: [] }
: await preserveScanDraft(context, parsed);
const reconciled = preserved.input;
const hardening = await readExistingHardeningPortfolio(context);
const draft = prepareSemanticScanDraft(context, reconciled, hardening);
try {
await publishDraft(draft, preserved.previousDigest, parsed);
await publishDraft(
draft,
preserved.previousDigest,
parsed,
preserved.checkpointIds,
);
return {
scanId: reconciled.scanId,
findingCount: draft.findings.findings.length,
Expand All @@ -124,54 +131,50 @@ export async function recordCodexSecurityScanDraftViaWorkbench(
return recordCodexSecurityScanDraft(
context,
input,
async (draft, expectedDigest, checkpoint) => {
const checkpointPath = await artifactDestination(
context,
["drafts", `${randomUUID()}.checkpoint.json`],
"staged scan checkpoint",
);
const draftPath = await artifactDestination(
context,
["drafts", `${randomUUID()}.json`],
"staged scan draft",
);
async (draft, expectedDigest, checkpoint, reconciledCheckpointIds) => {
try {
const { handoffClaimToken: _claim, ...snapshot } = checkpoint;
await Promise.all([
replaceArtifactJson(checkpointPath, snapshot),
replaceArtifactJson(draftPath, draft),
]);
const arguments_ = [
"write-scan-draft",
"--scan-id",
input.scanId,
"--draft-path",
draftPath,
"--checkpoint-path",
checkpointPath,
];
if (expectedDigest !== undefined) {
arguments_.push("--expected-draft-digest", expectedDigest);
}
if (context.handoffClaimToken) {
arguments_.push("--claim-token", context.handoffClaimToken);
}
try {
await runWorkbench(arguments_);
} catch (error) {
if (!workbenchScanDraftConflict(error)) throw error;
throw Object.assign(
new Error(
"The canonical scan draft changed while this checkpoint was being reconciled.",
),
{ code: "scan_draft_conflict" },
);
}
} finally {
await Promise.all([
fs.rm(checkpointPath, { force: true }),
fs.rm(draftPath, { force: true }),
]);
await writePreparedScanDraft(
{
scanDir: context.root,
expectedDigest,
reconciledCheckpointIds,
claimToken: context.handoffClaimToken,
writer: {
restore: async (relative, contents) => {
const path = await artifactDestination(
context,
relative.split("/"),
"staged scan draft",
);
await replaceArtifactText(
path,
Buffer.from(contents).toString("utf8"),
);
},
remove: async (relative) => {
const path = await artifactDestination(
context,
relative.split("/"),
"staged scan draft",
);
await fs.rm(path, { force: true });
},
},
workbench: (args) => runWorkbench([...args]),
onCleanupError: (error) =>
console.warn("Could not remove staged scan draft:", error),
},
checkpoint,
draft,
);
} catch (error) {
if (!workbenchScanDraftConflict(error)) throw error;
throw Object.assign(
new Error(
"The canonical scan draft changed while this checkpoint was being reconciled.",
),
{ code: "scan_draft_conflict" },
);
}
},
signal,
Expand All @@ -181,7 +184,11 @@ export async function recordCodexSecurityScanDraftViaWorkbench(
async function preserveScanDraft(
context: ArtifactContext,
input: ScanDraftInput,
): Promise<{ input: ScanDraftInput; previousDigest: string }> {
): Promise<{
input: ScanDraftInput;
previousDigest: string;
checkpointIds: string[];
}> {
const currentCheckpointName = scanDraftCheckpointName(input);
let result = structuredClone(input);
const previousState = await readPreviousScanDraft(context);
Expand All @@ -191,28 +198,15 @@ async function preserveScanDraft(
"scan checkpoint: saved result belongs to a different scan.",
);
const current = await readCurrentCheckpoints(context, currentCheckpointName);
const sources: ScanDraftInput[] = previous ? [previous, ...current] : current;
const inputs = current.map(({ input }) => input);
const sources: ScanDraftInput[] = previous ? [previous, ...inputs] : inputs;
if (input.complete === false) {
const final = sources.find((source) => source.complete !== false);
if (final) result = structuredClone(final);
if (final) {
result = structuredClone(final);
sources.push(input);
}
}
// Older drafts used the deferred row's id as a candidate alias. Keep its
// scope without promoting legacy IDs into the stricter candidateId field.
const historicalCandidateIds = new Set(
sources.flatMap((source) =>
source.coverage.surfaces.flatMap((surface) =>
typeof surface.candidateId === "string" ? [surface.candidateId] : [],
),
),
);
for (const scan of [result, ...sources])
for (const row of scan.coverage.deferred)
if (
row.candidateId === undefined &&
typeof row.id === "string" &&
historicalCandidateIds.has(row.id)
)
row.candidateScoped = true;
const resolvedSurfaces = resolvedCoverageSurfaceIds(result.coverage, sources);
const retainedScope = sources.find(
(source) => source.scope !== undefined,
Expand Down Expand Up @@ -378,81 +372,62 @@ async function preserveScanDraft(
};
result.coverage = preserveScanCoverage(result.coverage, previousCoverage);
}
return { input: result, previousDigest: previousState.digest };
return {
input: result,
previousDigest: previousState.digest,
checkpointIds: current.map(({ name }) => name),
};
}

async function readCurrentCheckpoints(
context: ArtifactContext,
excludedCheckpoint: string,
): Promise<ScanDraftInput[]> {
const checkpointRoot = join(context.root, "checkpoints");
const checkpointRootMetadata = await lstatIfExists(checkpointRoot);
if (checkpointRootMetadata === undefined) return [];
if (
checkpointRootMetadata.isSymbolicLink() ||
!checkpointRootMetadata.isDirectory()
) {
): Promise<Array<{ name: string; input: ScanDraftInput }>> {
// A scan created before the pending-directory boundary is imported once by
// the locked writer. Historical files remain available as evidence afterward.
const components = (await lstatIfExists(
join(context.root, "checkpoints", "pending", ".initialized"),
))
? ["checkpoints", "pending"]
: ["checkpoints"];
const root = join(context.root, ...components);
const metadata = await lstatIfExists(root);
if (metadata === undefined) return [];
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
throw new Error(
"scan checkpoint: current checkpoint set is not a safe directory.",
);
}
const [canonicalRoot, canonicalCheckpointRoot] = await Promise.all([
fs.realpath(context.root),
fs.realpath(checkpointRoot),
fs.realpath(root),
]);
if (!canonicalCheckpointRoot.startsWith(canonicalRoot + sep)) {
throw new Error(
"scan checkpoint: current checkpoint set escaped its artifact directory.",
);
}

const checkpoints: Array<{
input: ScanDraftInput;
modifiedMs: number;
name: string;
}> = [];
for (const entry of await fs.readdir(canonicalCheckpointRoot, {
withFileTypes: true,
})) {
if (
!entry.isFile() ||
!entry.name.endsWith(".json") ||
entry.name === excludedCheckpoint
)
const checkpoints: Array<{ name: string; input: ScanDraftInput }> = [];
for (const entry of await fs.readdir(root, { withFileTypes: true })) {
if (!entry.name.endsWith(".json") || entry.name === excludedCheckpoint)
continue;
const checkpointPath = join(canonicalCheckpointRoot, entry.name);
const checkpointMetadata = await fs.lstat(checkpointPath);
if (checkpointMetadata.isSymbolicLink() || !checkpointMetadata.isFile()) {
throw new Error(
"scan checkpoint: current checkpoint is not a safe file.",
);
}
// Markers precede history writes and can be acknowledged while we read.
const contents = await readOptionalArtifactText(
context,
["checkpoints", entry.name],
"current scan checkpoint",
);
if (contents === undefined) continue;
const input = parsePersistedScanDraft(
parseJsonObject(
await readArtifactText(
context,
["checkpoints", entry.name],
"current scan checkpoint",
),
"current scan checkpoint",
),
parseJsonObject(contents, "current scan checkpoint"),
);
if (input.scanId !== context.scanId) {
if (input.scanId !== context.scanId)
throw new Error(
"scan checkpoint: current checkpoint belongs to a different scan.",
);
}
checkpoints.push({
input,
modifiedMs: Number(checkpointMetadata.mtimeMs),
name: entry.name,
});
checkpoints.push({ name: entry.name, input });
}
checkpoints.sort(
(left, right) =>
right.modifiedMs - left.modifiedMs || right.name.localeCompare(left.name),
);
return checkpoints.map(({ input }) => input);
return checkpoints;
}

function scanDraftCheckpointName(input: ScanDraftInput): string {
Expand Down Expand Up @@ -521,14 +496,14 @@ async function lstatIfExists(
async function readOptionalArtifactText(
context: ArtifactContext,
components: readonly string[],
label = "previous scan draft",
): Promise<string | undefined> {
try {
return await readArtifactText(context, components, "previous scan draft");
return await readArtifactText(context, components, label);
} catch (error) {
if (
error instanceof Error &&
error.message ===
"previous scan draft: the requested artifact is unavailable."
error.message === `${label}: the requested artifact is unavailable.`
) {
return undefined;
}
Expand Down Expand Up @@ -711,22 +686,10 @@ function coverageHasOutstandingWork(coverage: SemanticCoverage): boolean {
}

function findingCandidateId(finding: JsonObject): string | undefined {
const provenance = finding.provenance;
if (
isObject(provenance) &&
typeof provenance.candidateId === "string" &&
provenance.candidateId.trim()
) {
return provenance.candidateId;
}
const extensions = finding.extensions;
if (isObject(extensions)) {
for (const field of ["candidateId", "reportId", "ledgerRowId"] as const) {
const value = extensions[field];
if (typeof value === "string" && value.trim()) return value;
}
}
return undefined;
return [
isObject(finding.provenance) ? finding.provenance.candidateId : undefined,
isObject(finding.extensions) ? finding.extensions.candidateId : undefined,
].find((id): id is string => typeof id === "string" && id.trim().length > 0);
}

/** Return the existing sealed documents only after workbench completion succeeds. */
Expand Down
Loading
Loading