Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
a82a83b
fix: preserve scan draft and sealed artifact consistency
codex Oct 5, 2026
665b373
fix: preserve completed coverage and candidate identities across draf…
codex Oct 5, 2026
0fdd714
Preserve unfinished surface progress and checkpoint identities
codex Oct 5, 2026
02c1094
fix: preserve checkpoint ownership and sibling identities
codex Oct 5, 2026
2910a96
fix: retain newly checkpointed sibling findings during recovery
codex Oct 5, 2026
a754498
fix: preserve established identities across equivalent checkpoints
codex Oct 5, 2026
160b811
fix: match checkpoints across report metadata enrichment
codex Oct 5, 2026
2711617
fix(scans): retain checkpoint history through identity repair
codex Oct 5, 2026
30e937b
fix(scans): retain sibling ambiguity across saved checkpoints
codex Oct 5, 2026
58fbf0a
fix(scans): align candidate identities during checkpoint recovery
codex Oct 5, 2026
a356c64
fix(scans): preserve retained finding identities across interruptions
codex Oct 5, 2026
94bdad5
fix(scans): reconcile raw checkpoints before assigning identities
codex Oct 5, 2026
1a5661e
fix(scans): retain saved report ownership and sibling identities
codex Oct 5, 2026
0f15e34
fix(scans): preserve saved identities and streamline checkpoint recovery
codex Oct 5, 2026
de576b4
fix(scans): reconcile saved revisions and allocate distinct identities
codex Oct 5, 2026
9b35b42
fix(scans): preserve semantic identities and bound worker ownership
codex Oct 5, 2026
2c2717e
Merge main and migrate recovery fixtures
codex Oct 5, 2026
f63ce6c
fix(scans): preserve accepted revisions and published identities
codex Oct 5, 2026
af4fd88
fix: preserve reconciled finding groups through publication
codex Oct 5, 2026
3b07ef8
fix: reconcile checkpoint siblings as one-to-one batches
codex Oct 5, 2026
4c3876b
Preserve semantic identities during recovery matching
codex Oct 5, 2026
7d52c0c
Preserve report identity across checkpoint revisions
codex Oct 5, 2026
7e6f50e
fix: retain accepted evidence through scan recovery
codex Oct 5, 2026
91434ef
fix: preserve scan evidence and follow-up state
codex Oct 5, 2026
62e9227
Preserve sealed artifact aliases and resumed draft progress
codex Oct 5, 2026
3730ddc
Preserve valid findings across optional export and metadata failures
codex Oct 5, 2026
f4d1652
fix(scans): retain saved ancestry and worker revision ownership
codex Oct 5, 2026
d48b665
test(scans): distinguish malformed identities from recoverable metadata
codex Oct 5, 2026
6ac335c
Merge main into PR branch
mldangelo-oai Oct 5, 2026
d3084af
merge: preserve published main updates in saved scan fixes
codex Oct 5, 2026
58af63e
fix: preserve checkpoint ownership when matching siblings
codex Oct 5, 2026
a07f0e8
fix: reconcile live finding identity before saved history
codex Oct 6, 2026
0407d00
Merge latest main into PR branch
mldangelo-oai Oct 6, 2026
b8ce065
merge: preserve upstream scan integration
codex Oct 6, 2026
086b559
fix: reconcile repeated worker findings one-to-one
codex Oct 6, 2026
7671159
Merge latest main into PR branch
mldangelo-oai Oct 6, 2026
9eec43a
fix: validate preserved finding identities
codex Oct 6, 2026
58dc541
merge: preserve published scan draft updates
codex Oct 6, 2026
db05b3e
merge: sync current dependency locks
codex Oct 6, 2026
f495552
Merge latest main into PR branch
mldangelo-oai Oct 6, 2026
d58d57e
Merge commit 'f495552a10d21953e148e85559a457eb54937a96' into mdangelo…
codex Oct 6, 2026
036d212
fix: preserve canonical finding identity after revision
codex Oct 6, 2026
e445b16
fix: preserve revised identities beside assigned siblings
codex Oct 6, 2026
630e1e5
Merge latest main into PR branch
mldangelo-oai Oct 6, 2026
070f297
Merge release metadata
codex Oct 6, 2026
09ae4e8
fix: preserve resolved scan recovery state
codex Oct 6, 2026
6489182
Merge main and reconcile scan contract validation
codex Oct 6, 2026
2a8ae1e
fix: retain the required scoring system for scored findings
codex Oct 6, 2026
3d3a192
fix(drafts): retain independent findings during history matching
codex Oct 6, 2026
b08ae30
Preserve independent findings when recovery matches revisions
codex Oct 6, 2026
cb397ec
Count authored finding identities once per recovery batch
codex Oct 6, 2026
efa77f1
Merge main and cover finalized artifacts on native platforms
mldangelo-oai Oct 7, 2026
253ffa6
Merge main and reuse the shared resume fixture
mldangelo-oai Oct 7, 2026
98f3b7b
fix: preserve ambiguous findings and eligible recovery revisions
mldangelo-oai Oct 7, 2026
b474f8a
Preserve saved identities across partial and enriched drafts
mldangelo-oai Oct 7, 2026
27288c6
Merge main to update Codex CLI and SDK
mldangelo-oai Oct 7, 2026
af01e18
Merge current main before recovery identity repair
mldangelo-oai Oct 7, 2026
227e599
Preserve distinct report identities during recovery
mldangelo-oai Oct 7, 2026
49899de
Honor newer duplicate collapse during recovery
mldangelo-oai Oct 7, 2026
faae290
Preserve JSON scalar types during saved report recovery
mldangelo-oai Oct 7, 2026
d4e238d
Merge current main for final integration
mldangelo-oai Oct 7, 2026
898b7a3
Preserve independent findings across draft revisions and recovery
mldangelo-oai Oct 7, 2026
7dce657
Preserve ambiguous worker findings during interrupted recovery
mldangelo-oai Oct 7, 2026
1978c56
Merge main and preserve scan draft recovery identities
mldangelo-oai Oct 10, 2026
63fc2c5
chore: merge main and resolve integration conflicts
mldangelo-oai Oct 11, 2026
3b3e66c
fix(artifacts): preserve JSON scalar identity through recovery and ma…
mldangelo-oai Oct 11, 2026
04671da
fix(artifacts): retain canonical draft compatibility during reconcili…
mldangelo-oai Oct 11, 2026
34bae43
fix(recovery): match structured provenance and publication identities
mldangelo-oai Oct 11, 2026
f936f75
fix(checkpoints): preserve canonical parent coverage IDs
mldangelo-oai Oct 11, 2026
2e943c0
fix(recovery): preserve anonymous primary-location identities
mldangelo-oai Oct 11, 2026
392cbd8
Preserve committed parent context during saved scan recovery
mldangelo-oai Oct 11, 2026
9e576cb
Keep reducer scope and canonical ordering during recovery
mldangelo-oai Oct 11, 2026
3f153d3
Bind recovery to admitted publication ancestry
mldangelo-oai Oct 11, 2026
66dcea6
Merge concurrent canonical coverage fix into recovery
mldangelo-oai Oct 11, 2026
94cd5da
Preserve authored identities when recovering terminal findings
mldangelo-oai Oct 11, 2026
426a814
Unify candidate whitespace semantics across saved-result consumers
mldangelo-oai Oct 12, 2026
ded8582
Keep superseded terminal drafts out of accepted-head cutoffs
mldangelo-oai Oct 12, 2026
49e0c1c
test(recovery): make legacy fixture mutations explicit
mldangelo-oai Oct 12, 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
341 changes: 301 additions & 40 deletions plugins/codex-security/mcp-app/src/artifact-scan-draft.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,33 @@ export async function recordCodexSecurityScanDraft(
input,
async (args: string[]) => {
const draft = await readJson(args[args.indexOf("--draft-path") + 1]);
const rawContents = await fs.readFile(
let rawContents = await fs.readFile(
args[args.indexOf("--checkpoint-path") + 1],
"utf8",
);
const checkpoint = JSON.parse(rawContents);
const { complete, scope, threatModel } = draft.manifest.scan;
const projected = {
scanId: checkpoint.scanId,
...(complete === undefined ? {} : { complete }),
...(scope === undefined ? {} : { scope }),
...(threatModel === undefined ? {} : { threatModel }),
findings: draft.findings.findings,
coverage: draft.coverage,
};
if (context.mode === "deep") {
const head = await readJson(context.root, "checkpoint-head.json").catch(
(error) => {
if (error.code !== "ENOENT") throw error;
return undefined;
},
);
checkpoint.publication = {
previousCheckpoint: head?.checkpoint ?? null,
previousInputCheckpoint: head?.inputCheckpoint ?? null,
};
rawContents = JSON.stringify(checkpoint);
}
const rawName =
createHash("sha256").update(rawContents).digest("hex") + ".json";
await replaceArtifactText(
Expand All @@ -51,16 +73,23 @@ export async function recordCodexSecurityScanDraft(
),
rawContents,
);
const { complete, scope, threatModel } = draft.manifest.scan;
await draftApi.saveScanDraftCheckpoint(
{ ...context, layout: "worker" },
projected,
false,
);
await replaceArtifactJson(
await artifactDestination(
context,
["checkpoint-head.json"],
"scan checkpoint head",
),
{
scanId: checkpoint.scanId,
...(complete === undefined ? {} : { complete }),
...(scope === undefined ? {} : { scope }),
...(threatModel === undefined ? {} : { threatModel }),
findings: draft.findings.findings,
coverage: draft.coverage,
checkpoint:
createHash("sha256")
.update(JSON.stringify(projected))
.digest("hex") + ".json",
...(checkpoint.publication ? { inputCheckpoint: rawName } : {}),
},
);
for (const [key, name] of [
Expand Down Expand Up @@ -182,3 +211,23 @@ export const surfaceDisposition = ({
id: string;
disposition: string;
}) => ({ id, disposition });

/** Stop after the admitted raw checkpoint and before the projected checkpoint. */
export async function interruptDraftAfterRaw(action: () => Promise<unknown>) {
const rename = fs.rename;
let checkpoints = 0;
fs.rename = async (source, destination) => {
if (
path.basename(path.dirname(String(destination))) === "checkpoints" &&
++checkpoints === 2
)
throw new Error("interrupted projected checkpoint");
return rename(source, destination);
};
try {
await assert.rejects(action(), /interrupted projected checkpoint/);
assert.equal(checkpoints, 2);
} finally {
fs.rename = rename;
}
}
109 changes: 109 additions & 0 deletions plugins/codex-security/mcp-app/tests/scan-identity-recovery-fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { execFile } from "node:child_process";
import { readdir, utimes } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import type { draftFixture } from "./scan-draft-recovery-fixture.ts";

const execFileAsync = promisify(execFile);
export type FixtureFinding = Record<string, unknown> & {
title: string;
summary: string;
severity: { level: string };
locations: Array<{ path: string; startLine: number }>;
provenance: Record<string, unknown>;
extensions?: Record<string, unknown>;
identity?: { anchor?: string; instance?: string; [key: string]: unknown };
};
export type RecoveredFinding = FixtureFinding & {
identity: { anchor: string; instance?: string; [key: string]: unknown };
findingId: string;
};
export type DraftFixture = ReturnType<typeof draftFixture>;
export const finding = (
title: string,
metadata: Record<string, unknown> = {},
): FixtureFinding => ({
ruleId: "fixture.review",
title,
summary: "Synthetic saved evidence must retain its identity.",
severity: { level: "low" },
confidence: { level: "high", rationale: "Synthetic persistence fixture." },
taxonomy: { category: "other", cwe: [] },
locations: [{ path: "src/example.py", startLine: 1 }],
remediation: "Complete the review.",
provenance: { source: "local_plugin" },
...metadata,
});

export async function recoverAndFinalize(
normal: DraftFixture,
recovered: DraftFixture,
workers: Record<string, unknown>[] = [],
details = false,
replay = false,
provenanceDetails = false,
source: "saved" | "published" = "saved",
): Promise<{
normal: RecoveredFinding[];
recovered: RecoveredFinding[];
warnings: unknown[];
historySummaries: string[];
preservedIdentities: unknown[][];
normalCoverage: Record<string, unknown>;
recoveredCoverage: Record<string, unknown>;
}> {
const { stdout } = await execFileAsync(
process.env.PYTHON?.trim() || "python3",
[
"-c",
`import json,sys
from pathlib import Path
sys.path.insert(0,sys.argv[1])
from workbench_saved_results import merge_saved_results
from finalize_scan_contract import _prepare_scan_finalization
normal,recovered=map(Path,sys.argv[2:4])
scan_id=sys.argv[4]
manifest=json.loads((normal/'scan-manifest.json').read_text())
coverage=json.loads((normal/'coverage.json').read_text())
binding={'status':'failed','allowedTargetKinds':[manifest['scan']['target']['kind']],'target':manifest['scan']['target'],'scope':manifest['scan']['scope'],'coverageMode':coverage['mode']}
warnings=[]
recovery=merge_saved_results(recovered,scan_id,binding,json.loads(sys.argv[5]),warnings,stopped=True,reason='Synthetic interruption',scan_mode=sys.argv[10]) if sys.argv[9]=='saved' else tuple(json.loads((recovered/name).read_text()) for name in ['scan-manifest.json','findings.json','coverage.json'])
if json.loads(sys.argv[7]):
replayed=merge_saved_results(recovered,scan_id,binding,json.loads(sys.argv[5]),[],stopped=True,reason='Synthetic interruption',frozen_source_digests=recovery[0]['scan']['preservedSources'],scan_mode=sys.argv[10])
assert replayed == recovery, 'Frozen recovery changed the retained documents'
ordinary=(manifest,json.loads((normal/'findings.json').read_text()),coverage)
results=[]
preserved_identities=[]
for root,documents in [(normal,ordinary),(recovered,recovery)]:
documents[0]['scan'].update(id=scan_id,producer={'name':'codex-security-plugin','version':'0.1.0'},status='failed',startedAt='2026-05-31T18:00:00Z',completedAt='2026-05-31T18:09:00Z')
for document in documents[1:]: document['scanId']=scan_id
prepared=_prepare_scan_finalization(root,completion_warnings=warnings,draft_documents=documents)
preserved_identities.append([row.get('provenance',{}).get('preservedIdentity') for row in prepared[3]['findings']])
results.append([{'title':row['title'],'identity':row['identity'],'fingerprints':row['fingerprints'],'findingId':row['findingId'],'occurrenceId':row['occurrenceId'],'workerMetadata':row.get('provenance',{}).get('workerId'),**({'evidenceProvenance':{key:row.get('provenance',{}).get(key,[]) for key in ['sourceFindingIds','sourceFindings','previousFindings','originalCandidates']}} if json.loads(sys.argv[8]) else {}),**({'summary':row['summary'],'locations':row['locations'],'severity':row['severity'],'candidateMetadata':{'provenance':row.get('provenance',{}).get('candidateId'),'extensions':row.get('extensions')}} if json.loads(sys.argv[6]) else {})} for row in prepared[3]['findings']])
print(json.dumps({'normal':results[0],'recovered':results[1],'warnings':warnings,'normalCoverage':ordinary[2],'recoveredCoverage':recovery[2],'preservedIdentities':preserved_identities,'historySummaries':[previous.get('summary') for row in recovery[1]['findings'] for previous in row.get('provenance',{}).get('previousFindings',[]) if isinstance(previous,dict)]}))`,
fileURLToPath(new URL("../../scripts", import.meta.url)),
normal.root,
recovered.root,
normal.context.scanId!,
JSON.stringify(workers),
JSON.stringify(details),
JSON.stringify(replay),
JSON.stringify(provenanceDetails),
source,
normal.context.mode!,
],
);
return JSON.parse(stdout);
}

export async function dateDraftFiles(
root: string,
time: number,
): Promise<void> {
for (const entry of await readdir(root, { withFileTypes: true })) {
const target = path.join(root, entry.name);
if (entry.isDirectory()) await dateDraftFiles(target, time);
else await utimes(target, time, time);
}
}
27 changes: 17 additions & 10 deletions plugins/codex-security/mcp-app/tests/test_artifact_scan_draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,19 +268,19 @@ try {
"checkpoints",
acceptedHead.checkpoint,
);
assert.notEqual(
assert.equal(
acceptedHeadDraft.complete,
false,
"a rejected incomplete write must not become the authoritative checkpoint head",
"new late work must remain incomplete in the authoritative checkpoint",
);
assert.deepEqual(acceptedHeadDraft.findings, [finding]);
const acceptedResult = await readJson(rejectedIncompleteRoot, "result.json");
assert.equal(
acceptedResult.coverage.deferred.some(
(item: FixtureFinding) => item.candidateId === "late-incomplete-review",
),
false,
"a rejected incomplete write must not change the completed result",
true,
"new late work must survive alongside the completed candidate",
);

const deferredDoesNotRejectContext = await createWorkerContext(
Expand Down Expand Up @@ -2377,13 +2377,13 @@ try {
),
[
{ anchor: "candidate-authored-instance", instance: "dss-147-a" },
{ anchor: "candidate-authored-instance", instance: "dss-147-a" },
{ anchor: "candidate-authored-instance", instance: "dss-147-a-2" },
{
anchor: "candidate-authored-instance",
instance: "ledger-row-c",
},
],
"duplicate stable instance sources remain collisions for finalization",
"generated instances avoid an existing authored identity",
);

const collisionFindings = [
Expand Down Expand Up @@ -2450,10 +2450,17 @@ try {
assert.deepEqual(
standardFindings.map((item: FixtureFinding) => item.identity),
collisionCase.findings.map(
(item: FixtureFinding) =>
item.identity ?? collisionCase.originalIdentity,
(item: FixtureFinding, index) =>
item.identity ?? {
...collisionCase.originalIdentity,
...(index === 0
? {}
: {
instance: `${(collisionCase.originalIdentity as { instance?: string }).instance}-${index + 1}`,
}),
},
),
`${collisionCase.label} identity collisions retain the existing Standard shape`,
`${collisionCase.label} identities preserve authored values and split generated collisions`,
);
assert.deepEqual(
standardFindings.map((item: FixtureFinding) => item.provenance),
Expand Down Expand Up @@ -2487,7 +2494,7 @@ try {
assert.deepEqual(
deepFindings.map((item: FixtureFinding) => item.provenance),
collisionCase.findings.map((item: FixtureFinding, index) =>
index === 1 || index === 2
item.identity !== undefined && (index === 1 || index === 2)
? {
...item.provenance,
preservedIdentity: collisionCase.originalIdentity,
Expand Down
Loading
Loading