Update to Codex Security
Snapshot Oct 7, 2026 · 06:02 UTC · version 0.1.32
Package or technical metadata updated
Package contents changed in 77 files: .app.json, .codex-plugin/plugin.json, .mcp.json, …. Open the file diff to inspect the edits.
Observed in package metadata. These changes alone do not establish a new customer-facing feature.
Package file
e4d5b22326ee380de5d779f7b5ba590c8d1bee9a80e8869fb2bd7f4def8e974d
a7bbe314059c7311e169474823e3978f267e560c3e961ba49fb94032da9843fe
Package file
480
479
Package file
3ddcc187fe2f1aff961bf55444e70bfbc3f825b6185ab3f29860d1a813a395f9
527d1a0db286884729572d400b0ad9b0ff852d4a12b36228ff5fbef180f7de9d
Package file
40b84baf510169eadfd5f0fe0ad95001caedb7bc662d77c9ac2a4fbec7dd0845
7c5968060f6e848859ce02450d6e02e67029d8d39011bbda1059b19de6309ebf
Compare saved observations
Download comparison JSONChanged files
examples/completed-scan/report.md →
mcp/native/THIRD_PARTY_NOTICES.txt →
mcp/native/darwin-arm64/unix.node →
mcp/native/darwin-x64/unix.node →
mcp/native/linux-arm64-gnu/unix.node →
mcp/native/linux-arm64-musl/unix.node →
mcp/native/linux-x64-gnu/unix.node →
mcp/native/linux-x64-musl/unix.node →
mcp/native/win32-arm64/windows.node →
mcp/native/win32-x64/windows.node →
references/artifact-storage.md →
references/config-preflight.md →
references/scan-artifacts.md →
schemas/coverage.schema.json →
schemas/scan-manifest.schema.json →
schemas/tools/scan-draft.schema.json →
scripts/deep_scan_workbench.py →
scripts/filesystem_identity.py →
scripts/finalize_scan_contract.py →
scripts/generate_in_scope_files.py →
scripts/generate_rank_input.py →
scripts/launch_codex_security_mcp →
scripts/normalize_candidates.py →
scripts/report_projection.py →
scripts/threat_model_projection.py →
scripts/windows_scan_local_files.py →
scripts/workbench/storage.py →
scripts/workbench_constants.py →
scripts/workbench_finding_index.py →
scripts/workbench_native_indexes.py →
scripts/workbench_progress.py →
scripts/workbench_publication.py →
scripts/workbench_remediation.py →
scripts/workbench_saved_results.py →
scripts/workbench_scan_start.py →
scripts/workbench_scan_usage.py →
scripts/workbench_severity.py →
scripts/workbench_source_excerpt.py →
scripts/workbench_target_state.py →
scripts/workbench_validation.py →
skills/assess-patch-risk/SKILL.md →
skills/assess-patch-risk/scripts/validate_patch_risk_assessment.py →
skills/attack-path-analysis/SKILL.md →
skills/deep-security-scan/SKILL.md →
skills/finding-discovery/SKILL.md →
skills/propose-security-hardening/references/proposal-format.md →
skills/security-diff-scan/SKILL.md →
skills/security-scan/SKILL.md →
skills/security-scan/references/desktop-scan.md →
skills/security-scan/references/scan-artifacts-and-ledger.md →
skills/threat-model/SKILL.md →
skills/track-findings/SKILL.md →
skills/track-findings/references/github-security-advisories.md →
skills/track-findings/references/jira.md →
skills/triage-finding/SKILL.md →
skills/triage-finding/agents/openai.yaml →
skills/triage-finding/references/ticket-intake.md →
skills/triage-finding/references/triage-result-contract.md →
scripts/workbench_saved_results.py
--- before +++ after @@ -12,10 +12,9 @@ import sqlite3 import stat import sys -from collections.abc import Callable, Iterator -from dataclasses import dataclass +from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path -from types import ModuleType from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) @@ -25,18 +24,25 @@ _populate_unsealed_artifact_envelope, _populate_unsealed_manifest_envelope, _prepare_scan_finalization, + _read_json, + _read_saved_threat_model, _read_scan_local_json, _read_scan_local_json_bytes, + _read_scan_local_json_with_metadata, _recover_unsealed_findings, _remove_scan_local_file_if_exists, _validate_completion_binding, + _validate_resolved_deferred, + _validate_schema_node, _write_prepared_scan_finalization, finalize_scan, finding_candidate_id, open_scan_local_file_descriptor, write_scan_local_bytes, + write_threat_model_projection_if_possible, ) from workbench_constants import PHASES +from workbench_target import committed_diff_snapshot_digest from workbench_validation import path_within_scope _PUBLISHED_OUTPUTS = ( @@ -44,6 +50,7 @@ "coverage.json", "scan-manifest.json", "report.md", + "threatmodel.md", "report.html", "exports/results.sarif", ) @@ -55,29 +62,59 @@ ) -@dataclass(frozen=True) -class WorkbenchDbContext: - ARTIFACTS: dict[str, str] - artifact_path: Callable[..., Path | None] - deep_scan: ModuleType - expected_coverage_mode: Callable[..., str] - handoff: ModuleType - index_findings: Callable[..., None] - now: Callable[[], str] - optional_text: Callable[..., str | None] - parse_scan_cost: Callable[..., dict[str, Any] | None] - published_manifest_digest: Callable[..., str] - read_json_object: Callable[[Path], dict[str, Any]] - require_canonical_scan_directory: Callable[[Path], Path] - require_recorded_manifest_digest: Callable[..., None] - require_scan: Callable[..., Any] - require_uuid: Callable[[str, str], str] - require_workspace: Callable[..., Any] - scan_completion_lock: Callable[..., Any] - scan_context: Callable[..., dict[str, Any]] - verify_manifest_binding: Callable[..., None] - workbench_completion_binding: Callable[..., dict[str, Any]] - workspace_state: Callable[..., dict[str, Any]] +def threat_model_fields(db: Any, scan: sqlite3.Row) -> dict[str, Any]: + scan_dir = Path(scan["scan_dir"]) + fields: dict[str, Any] = {"threatModelAvailable": False} + try: + db.require_recorded_manifest_digest(scan, scan_dir) + db.verify_manifest_binding( + scan, _read_scan_local_json(scan_dir, db.ARTIFACTS["manifest"], "scan manifest") + ) + saved_model = _read_saved_threat_model(scan_dir) + if saved_model is not None: + description = saved_model[0] + fields.update( + threatModelAvailable=True, + threatModelProvenance=description["provenance"], + ) + if description["path"] is not None: + fields["threatModelPath"] = description["path"] + except (ContractError, OSError, SystemExit): + # Unavailable optional model data must not prevent reading the saved scan. + pass + return fields + + +def refresh_completed_scan( + db: Any, + connection: sqlite3.Connection, + scan: sqlite3.Row, + cost_json: str | None, +) -> dict[str, Any]: + warnings = json.loads(scan["completion_warnings_json"]) + scan_dir = db.require_canonical_scan_directory(Path(scan["scan_dir"])) + db.require_recorded_manifest_digest(scan, scan_dir) + db.verify_manifest_binding(scan, db.read_json_object(scan_dir / db.ARTIFACTS["manifest"])) + try: + manifest, _, _ = finalize_scan( + scan_dir, + expected_coverage_mode=db.expected_coverage_mode(scan), + projection_warnings=warnings, + ) + except ContractError as exc: + raise SystemExit(str(exc)) from exc + db.verify_manifest_binding(scan, manifest) + manifest_digest = db.published_manifest_digest(scan_dir, manifest) + db.pin_legacy_manifest_digest(connection, scan["id"], manifest_digest) + if cost_json is not None and scan["recipe_json"] is not None: + db.scan_usage.reconcile_completed_scan_cost(connection, scan, cost_json) + if warnings != json.loads(scan["completion_warnings_json"]): + with connection: + connection.execute( + "UPDATE scans SET completion_warnings_json = ? WHERE id = ?", + (json.dumps(warnings), scan["id"]), + ) + return db.scan_context(connection, scan["id"]) def _encoded(value: Any) -> bytes: @@ -118,35 +155,51 @@ ) -def _saved_result_paths(scan_dir: Path, workers: list[Any]) -> Iterator[tuple[str, str | None]]: - latest_reducer = _latest_successful_reducer(workers) +def _checkpoint_paths(scan_dir: Path, directory: str) -> list[str]: + return [ + f"{directory}/{name}" + for name in _children(scan_dir, directory) + if re.fullmatch(r"[0-9a-f]{64}\.json", name) + ] - def checkpoints(directory: str, kind: str | None = None) -> Iterator[tuple[str, str | None]]: - for name in _children(scan_dir, directory): - if re.fullmatch(r"[0-9a-f]{64}\.json", name): - yield f"{directory}/{name}", kind - yield from checkpoints("checkpoints") +def _worker_outputs(scan_dir: Path, worker: Any) -> list[tuple[str, int]]: + output = Path(worker["artifact_dir"]).relative_to(scan_dir) + attempts = (output.parent if output.name == "output" else output) / "attempts" + archived = [ + ((attempts / name).as_posix(), int(name.split("-")[1])) + for name in _children(scan_dir, attempts.as_posix()) + if re.fullmatch(r"attempt-\d+", name) + ] + attempt = int(worker["attempt"] or 0) if "attempt" in worker.keys() else 0 + if worker["kind"] == "discovery" and not attempt: + attempt = max((attempt for _, attempt in archived), default=0) + 1 + return [(output.as_posix(), attempt), *archived] + + +def _saved_result_paths(scan_dir: Path, workers: list[Any]) -> Iterator[tuple[str, str | None]]: + latest_reducer = _latest_successful_reducer(workers) + yield "checkpoint-head.json", None + for directory in ("checkpoint-heads", "checkpoints"): + yield from ((path, None) for path in _checkpoint_paths(scan_dir, directory)) for worker in workers: if worker["kind"] not in {"dedup", "discovery"}: continue try: - output = Path(worker["artifact_dir"]).relative_to(scan_dir).as_posix() + outputs = _worker_outputs(scan_dir, worker) except (TypeError, ValueError): continue - attempts = (Path(output).parent if Path(output).name == "output" else Path(output)) / ( - "attempts" - ) - directories = [output] + [ - (attempts / name).as_posix() - for name in _children(scan_dir, attempts.as_posix()) - if re.fullmatch(r"attempt-\d+", name) - ] - for directory in directories: - checkpoint_paths = list(checkpoints(f"{directory}/checkpoints", worker["kind"])) + for directory, _ in outputs: + if worker["kind"] == "discovery": + yield f"{directory}/checkpoint-head.json", worker["kind"] + yield from ( + (path, worker["kind"]) + for path in _checkpoint_paths(scan_dir, f"{directory}/checkpoint-heads") + ) + checkpoint_paths = _checkpoint_paths(scan_dir, f"{directory}/checkpoints") if worker["kind"] == "discovery" or checkpoint_paths: yield f"{directory}/result.json", worker["kind"] - yield from checkpoint_paths + yield from ((path, worker["kind"]) for path in checkpoint_paths) if worker["result_manifest_path"] and ( worker["kind"] == "discovery" or (latest_reducer is not None and worker["id"] == latest_reducer["id"]) @@ -160,17 +213,143 @@ continue +def _checkpoint_head_directory(relative: str) -> Path | None: + path = Path(relative) + if path.name == "checkpoint-head.json": + return path.parent + if path.parent.name == "checkpoint-heads": + return path.parent.parent + return None + + +def _capture_saved_source( + scan_dir: Path, + relative: str, + scan_id: str, + *, + kind: str | None = None, + snapshot_head: bool = True, + write: bool = True, +) -> dict[str, tuple[str, int]]: + if not snapshot_head or Path(relative).name != "checkpoint-head.json": + _, digest, observed = _read_saved_result(scan_dir, relative, scan_id, kind=kind) + return {relative: (digest, observed)} + head, _, observed = _read_saved_result(scan_dir, relative, scan_id) + observation = {"checkpoint": head["checkpoint"], "observedAtNs": str(observed)} + directory = Path(relative).parent + selected = (directory / "checkpoints" / observation["checkpoint"]).as_posix() + _, selected_digest, selected_time = _read_saved_result(scan_dir, selected, scan_id) + digest = _digest(observation) + snapshot = (directory / "checkpoint-heads" / f"{digest}.json").as_posix() + # Capture the selected file even if the worker created it after directory enumeration. + if write and not (scan_dir / snapshot).exists(): + write_scan_local_bytes(scan_dir, snapshot, _encoded(observation)) + return { + snapshot: (digest, int(observation["observedAtNs"])), + selected: (selected_digest, selected_time), + } + + +def _is_source_order_snapshot(relative: str) -> bool: + path = Path(relative) + return path.parent == Path("source-order") and bool( + re.fullmatch(r"[0-9a-f]{64}\.json", path.name) + ) + + def _read_saved_result( scan_dir: Path, relative: str, scan_id: str, *, kind: str | None = None -) -> tuple[dict[str, Any], str]: - draft = _read_scan_local_json(scan_dir, relative, "Saved scan checkpoint") +) -> tuple[dict[str, Any], str, int]: + draft, _, metadata = _read_scan_local_json_with_metadata( + scan_dir, relative, "Saved scan checkpoint" + ) + directory = _checkpoint_head_directory(relative) + if directory is not None: + checkpoint = draft.get("checkpoint") + if not isinstance(checkpoint, str) or not re.fullmatch(r"[0-9a-f]{64}\.json", checkpoint): + raise ContractError("checkpoint head does not name a saved checkpoint") + _read_saved_result(scan_dir, (directory / "checkpoints" / checkpoint).as_posix(), scan_id) + if Path(relative).name == "checkpoint-head.json": + return draft, _digest([draft, metadata.st_mtime_ns]), metadata.st_mtime_ns + observed = draft.get("observedAtNs") + if not isinstance(observed, str) or not re.fullmatch(r"-?[0-9]+", observed): + raise ContractError("checkpoint head has no observation time") + return draft, _digest(draft), int(observed) if draft.get("scanId") != scan_id: raise ContractError("checkpoint belongs to a different scan") - if not isinstance(draft.get("findings"), list) or not isinstance( - draft.get("coverage", {} if kind == "dedup" else None), dict + if not _is_source_order_snapshot(relative) and ( + not isinstance(draft.get("findings"), list) + or not isinstance(draft.get("coverage", {} if kind == "dedup" else None), dict) ): raise ContractError("checkpoint has no semantic findings or coverage") - return draft, _digest(draft) + return draft, _digest(draft), metadata.st_mtime_ns + + +def _frozen_source_times(scan_dir: Path, scan_id: str, sources: dict[str, str]) -> dict[str, int]: + times: dict[str, int] = {} + snapshots = [path for path in sources if _is_source_order_snapshot(path)] + for path in snapshots: + record, digest, _ = _read_saved_result(scan_dir, path, scan_id) + if digest != sources[path] or not isinstance(record.get("sources"), dict): + raise ContractError("saved source ordering changed after the scan stopped") + for relative, observation in record["sources"].items(): + if ( + not isinstance(observation, dict) + or relative not in sources + or observation.get("digest") != sources[relative] + or not isinstance(observation.get("observedAtNs"), str) + or not re.fullmatch(r"-?[0-9]+", observation["observedAtNs"]) + ): + raise ContractError("saved source ordering does not match its frozen sources") + observed = int(observation["observedAtNs"]) + if relative in times and times[relative] != observed: + raise ContractError("saved source ordering has conflicting observations") + times[relative] = observed + if snapshots and sources.keys() - set(snapshots) - times.keys(): + raise ContractError("saved source ordering is incomplete") + return times + + +def _freeze_source_times( + scan_dir: Path, scan_id: str, sources: dict[str, str], times: dict[str, int] +) -> None: + # Identical result rewrites must not change the order of frozen review evidence. + observations = { + path: {"digest": digest, "observedAtNs": str(times[path])} + for path, digest in sources.items() + if not _is_source_order_snapshot(path) + } + if not observations: + return + record = {"scanId": scan_id, "sources": observations} + digest = _digest(record) + path = f"source-order/{digest}.json" + if not (scan_dir / path).exists(): + write_scan_local_bytes(scan_dir, path, _encoded(record)) + if _read_saved_result(scan_dir, path, scan_id)[1] != digest: + raise ContractError("saved source ordering does not match its digest") + sources[path] = digest + + +def _parent_scan_draft( + scan_id: str, + parent_scan: dict[str, Any], + findings: dict[str, Any], + coverage: dict[str, Any], +) -> dict[str, Any]: + parent = { + "scanId": scan_id, + "findings": findings.get("findings"), + "coverage": coverage, + **{ + key: parent_scan[key] + for key in ("scope", "threatModel", "complete") + if key in parent_scan + }, + } + if not isinstance(parent["findings"], list): + raise ContractError("Saved parent draft has no findings array") + return parent def _read_saved_parent_result( @@ -188,29 +367,34 @@ or coverage.get("scanId", scan_id) != scan_id ): raise ContractError("Saved parent documents belong to a different scan") - parent = { - "scanId": scan_id, - "findings": findings.get("findings"), - "coverage": coverage, - **{ - key: parent_scan[key] - for key in ("scope", "threatModel", "complete") - if key in parent_scan - }, - } - if not isinstance(parent["findings"], list): - raise ContractError("Saved parent draft has no findings array") - return manifest, parent + return manifest, _parent_scan_draft(scan_id, parent_scan, findings, coverage) -def _source_digests(value: Any, label: str) -> dict[str, str]: +def _source_digests(value: Any, error: str) -> dict[str, str]: if not isinstance(value, dict) or not all( isinstance(relative, str) and isinstance(digest, str) for relative, digest in value.items() ): - raise ContractError(f"{label} source digests are malformed.") + raise ContractError(error) return value +def _retained_source_state(value: Any) -> tuple[dict[str, str], str | None]: + if isinstance(value, dict) and isinstance(value.get("sources"), dict): + sources = _source_digests( + value["sources"], "Saved stopped-scan source digests are malformed." + ) + model_source = value.get("threatModelSource") + if not isinstance(model_source, str) or model_source not in sources: + raise ContractError("Saved stopped-scan model source is outside its checkpoint set.") + return sources, model_source + return _source_digests(value, "Saved stopped-scan source digests are malformed."), None + + +def _encode_retained_sources(sources: dict[str, str], model_source: list[str]) -> str: + state = {"sources": sources, "threatModelSource": model_source[0]} if model_source else sources + return json.dumps(state, sort_keys=True) + + def _saved_results_changed(db: Any, connection: Any, scan: Any) -> bool: try: scan_dir = db.require_canonical_scan_directory(Path(scan["scan_dir"])) @@ -234,7 +418,7 @@ if manifest_path is None: if frozen_sources is not None: - return bool(_source_digests(json.loads(frozen_sources), "Frozen stopped-scan")) + return bool(_retained_source_state(json.loads(frozen_sources))[0]) return has_saved_source() if scan["seal_manifest_digest"] is None: try: @@ -252,14 +436,22 @@ if not isinstance(manifest_scan, dict): return True published_sources = _source_digests( - manifest_scan.get("preservedSources", {}), "Published scan" + manifest_scan.get("preservedSources", {}), + "Published scan source digests are malformed.", ) current_sources = dict(published_sources) + paths.update({path: None for path in published_sources if _is_source_order_snapshot(path)}) for path in paths: try: - _, current_sources[path] = _read_saved_result( - scan_dir, path, scan["id"], kind=paths[path] + captured = _capture_saved_source( + scan_dir, + path, + scan["id"], + kind=paths[path], + snapshot_head=path not in published_sources, + write=False, ) + current_sources.update({path: value[0] for path, value in captured.items()}) except (ContractError, OSError, ValueError): continue return current_sources != published_sources @@ -273,7 +465,7 @@ include_parent = True raw_frozen_sources = scan["retained_source_digests_json"] if raw_frozen_sources is not None: - frozen_sources = _source_digests(json.loads(raw_frozen_sources), "Saved stopped-scan") + frozen_sources, _ = _retained_source_state(json.loads(raw_frozen_sources)) include_parent = False manifest_path = db.artifact_path(scan_dir, db.ARTIFACTS["manifest"], required=False) @@ -291,7 +483,8 @@ ): if "preservedSources" in manifest_scan: published_sources = _source_digests( - manifest_scan["preservedSources"], "Published scan" + manifest_scan["preservedSources"], + "Published scan source digests are malformed.", ) include_parent = not published_sources if published_sources: @@ -312,21 +505,30 @@ ).fetchall() paths = dict(_saved_result_paths(scan_dir, workers)) recovery_sources = dict(frozen_sources or {}) + source_times = _frozen_source_times(scan_dir, scan["id"], recovery_sources) for relative, expected_digest in recovery_sources.items(): try: - _, digest = _read_saved_result(scan_dir, relative, scan["id"], kind=paths.get(relative)) + _, digest, observed = _read_saved_result( + scan_dir, relative, scan["id"], kind=paths.get(relative) + ) except (ContractError, OSError, ValueError) as exc: raise ContractError("Frozen stopped-scan checkpoint set is incomplete.") from exc if digest != expected_digest: raise ContractError("checkpoint changed after the scan stopped") + if not _is_source_order_snapshot(relative): + source_times.setdefault(relative, observed) for relative in paths.keys() - recovery_sources.keys(): try: - _, recovery_sources[relative] = _read_saved_result( - scan_dir, relative, scan["id"], kind=paths[relative] - ) + captured = _capture_saved_source(scan_dir, relative, scan["id"], kind=paths[relative]) except (ContractError, OSError, ValueError): continue + for path, (digest, observed) in captured.items(): + if path in recovery_sources and recovery_sources[path] != digest: + raise ContractError("checkpoint changed after the scan stopped") + recovery_sources[path] = digest + source_times.setdefault(path, observed) + _freeze_source_times(scan_dir, scan["id"], recovery_sources, source_times) return recovery_sources, include_parent @@ -358,15 +560,10 @@ else finding.get("identity") ) if not isinstance(identity, dict): - extensions = finding.get("extensions") - source = str( - (extensions.get("candidateId") if isinstance(extensions, dict) else None) - or finding.get("title") - or "finding" - ) - identity = { - "anchor": re.sub(r"[^a-z0-9._/-]+", "-", source.lower()).strip("._/-") or "finding" - } + normalized = dict(finding) + normalized.pop("identity", None) + _ensure_finding_identity(normalized) + identity = normalized["identity"] locations = finding.get("locations", []) if not isinstance(locations, list): locations = [] @@ -459,6 +656,257 @@ ) +def _deferred_rows(coverage: dict[str, Any]) -> list[Any]: + rows = coverage.get("deferred", []) + return rows if isinstance(rows, list) else [] + + +def _resolved_deferred_rows(draft: dict[str, Any], schema: dict[str, Any]) -> list[dict[str, Any]]: + if draft.get("complete") is False: + return [] + coverage = draft["coverage"] + rows = coverage.get("resolvedDeferred", []) + try: + # Invalid closure metadata cannot discard the evidence it names. + _validate_schema_node(rows, schema, "coverage.resolvedDeferred") + _validate_resolved_deferred({**coverage, "deferred": _deferred_rows(coverage)}) + except ContractError: + return [] + return rows + + +def _merge_tied_parent_observations( + current: dict[str, Any], previous: dict[str, Any] +) -> dict[str, Any]: + merged = copy.deepcopy(current) + for finding in previous["findings"]: + if finding not in merged["findings"]: + merged["findings"].append(copy.deepcopy(finding)) + coverage = merged["coverage"] + for field in ("surfaces", "explicitExclusions", "deferred", "openQuestions"): + rows = previous["coverage"].get(field, []) + output = coverage.setdefault(field, []) + if isinstance(rows, list) and isinstance(output, list): + for row in rows: + if row not in output: + output.append(copy.deepcopy(row)) + pending_ids = { + identity + for row in _deferred_rows(coverage) + if isinstance(row, dict) + for identity in (row.get("id"), row.get("candidateId")) + if isinstance(identity, str) + } + closure_schema = _read_json( + Path(__file__).resolve().parent.parent / "schemas" / "coverage.schema.json" + )["properties"]["resolvedDeferred"] + closures = {} + for observed in (current, previous): + for row in _resolved_deferred_rows(observed, closure_schema): + if row["id"] not in pending_ids: + closures.setdefault(row["id"], copy.deepcopy(row)) + coverage.pop("resolvedDeferred", None) + if closures: + coverage["resolvedDeferred"] = list(closures.values()) + if current.get("complete") is False or previous.get("complete") is False: + merged["complete"] = False + if ( + coverage.get("deferred") + or merged.get("complete") is False + or ( + isinstance(coverage.get("surfaces"), list) + and any( + isinstance(row, dict) and row.get("disposition") == "needs_follow_up" + for row in coverage["surfaces"] + ) + ) + or previous["coverage"].get("completeness") == "partial" + ): + coverage["completeness"] = "partial" + return merged + + +def _saved_coverage_id(item: dict[str, Any]) -> str: + return item.get("candidateId") or f"saved-{_digest(item)[:16]}" + + +def _deferred_candidate_id( + row: dict[str, Any], + owner: str | None, + ambiguous_deferred: set[tuple[str | None, str]], +) -> str | None: + identity = row.get("candidateId") or row.get("id") + if not isinstance(identity, str): + return None + if (owner, identity) in ambiguous_deferred and not any( + key in row for key in ("candidateId", "candidate", "finding") + ): + return None + return identity + + +def _generic_surface_updates( + sources: list[tuple[str, dict[str, Any], str | None]], + source_order: dict[str, tuple[int, int]], + closed_deferred: dict[tuple[str | None, str], tuple[tuple[int, int], dict[str, Any], str]], + active_deferred: dict[tuple[str | None, str], tuple[tuple[int, int], dict[str, Any], str]], + resolved_candidates: dict[tuple[str | None, str], str], + reopened_generic: set[tuple[str | None, str]], + surface_schema: dict[str, Any], + deferred_rows: dict[str, list[Any]], + ambiguous_deferred: set[tuple[str | None, str]], +) -> tuple[set[int], list[dict[str, Any]]]: + if not closed_deferred and not reopened_generic: + return set(), [] + + def linked(row: dict[str, Any], identity: str | None) -> bool: + surface_ids = row.get("surfaceIds", []) + return identity is not None and ( + row.get("id") == identity or (isinstance(surface_ids, list) and identity in surface_ids) + ) + + saved_surfaces: dict[tuple[str | None, str], list[tuple[str, dict[str, Any]]]] = {} + for relative, draft, owner in sources: + rows = draft["coverage"].get("surfaces", []) + for row in rows if isinstance(rows, list) else []: + if isinstance(row, dict) and isinstance(row.get("id"), str): + saved_surfaces.setdefault((owner, row["id"]), []).append((relative, row)) + replaced: set[int] = set() + updates: list[dict[str, Any]] = [] + for relative, draft, owner in sources: + closed_ids = { + identity + for (saved_owner, identity), (_, _, source) in closed_deferred.items() + if saved_owner == owner and source == relative + } + reopened_ids = { + identity + for (saved_owner, identity), (_, _, source) in active_deferred.items() + if saved_owner == owner and source == relative and (owner, identity) in reopened_generic + } + if not closed_ids and not reopened_ids: + continue + current = draft["coverage"].get("surfaces", []) + for surface in current if isinstance(current, list) else []: + if not isinstance(surface, dict): + continue + reopening = surface.get("disposition") == "needs_follow_up" + work_ids = reopened_ids if reopening else closed_ids + if not work_ids: + continue + identity = surface.get("id") + if not isinstance(identity, str): + continue + matches = saved_surfaces[(owner, identity)] + # Older writers could assign one ID to distinct surfaces in a draft. + # A closure cannot identify which of those observations it replaces. + by_source = dict(matches) + if any(row != by_source[saved_path] for saved_path, row in matches): + continue + if any( + "candidateId" in row or "candidate" in row or "finding" in row for _, row in matches + ): + continue + if any( + row is not surface + and source_order[saved_path] >= source_order[relative] + and row.get("disposition") != surface.get("disposition") + for saved_path, row in matches + ): + continue + + if not reopening and any( + saved_owner == owner + and ( + not isinstance(row.get("id") or row.get("candidateId"), str) + or ( + isinstance(row.get("id"), str) + and (owner, row["id"]) in ambiguous_deferred + and not any(key in row for key in ("candidateId", "candidate", "finding")) + ) + ) + and linked(row, identity) + for saved_path, _, saved_owner in sources + for row in deferred_rows[saved_path] + if isinstance(row, dict) + ): + continue + if not reopening and any( + saved_owner == owner + and (owner, deferred_id) not in closed_deferred + and ( + (candidate_id := _deferred_candidate_id(row, owner, ambiguous_deferred)) is None + or (owner, candidate_id) not in resolved_candidates + ) + and linked(row, identity) + for (saved_owner, deferred_id), (_, row, _) in active_deferred.items() + ): + continue + # An accepted checkpoint can update a saved surface by ID without + # optional surfaceIds links on its generic task. + latest_surface = max(matches, key=lambda match: source_order[match[0]])[1] + update = copy.deepcopy(latest_surface) + refs = update.setdefault("receiptRefs", []) + if isinstance(refs, list): + for _, row in matches: + previous_refs = row.get("receiptRefs", []) + for ref in previous_refs if isinstance(previous_refs, list) else []: + if ref not in refs: + refs.append(ref) + try: + _validate_schema_node(update, surface_schema, "coverage.surfaces") + except ContractError: + continue + replaced.update( + id(row) + for _, row in matches + if reopening + or row.get("disposition") in {"needs_follow_up", surface.get("disposition")} + ) + if update not in updates: + updates.append(update) + return replaced, updates + + +@contextmanager +def preserve_parent_head_on_error(scan_dir: Path) -> Iterator[None]: + """Keep rejected completion attempts from becoming accepted parent observations.""" + head_path = scan_dir / "checkpoint-head.json" + previous = None + try: + head_path.lstat() + except FileNotFoundError: + pass + else: + descriptor = open_scan_local_file_descriptor( + scan_dir, "checkpoint-head.json", "Saved parent checkpoint head" + ) + with os.fdopen(descriptor, "rb") as handle: + metadata = os.fstat(handle.fileno()) + previous = (handle.read(), metadata) + directories = ("checkpoints", "checkpoint-heads") + previous_files = { + relative for directory in directories for relative in _checkpoint_paths(scan_dir, directory) + } + try: + yield + except ContractError: + if previous is None: + _remove_scan_local_file_if_exists(scan_dir, "checkpoint-head.json") + else: + payload, metadata = previous + write_scan_local_bytes(scan_dir, "checkpoint-head.json", payload) + os.utime(head_path, ns=(metadata.st_atime_ns, metadata.st_mtime_ns)) + current_files = { + relative + for directory in directories + for relative in _checkpoint_paths(scan_dir, directory) + } + for relative in current_files - previous_files: + _remove_scan_local_file_if_exists(scan_dir, relative) + raise + + def merge_saved_results( scan_dir: Path, scan_id: str, @@ -470,14 +918,25 @@ reason: str, frozen_source_digests: dict[str, str] | None = None, allow_frozen_legacy_parent: bool = False, + frozen_model_source: str | None = None, + selected_model_source: list[str] | None = None, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]] | None: """Read only bound parent/worker files; return an unsealed loss-preserving union.""" initial_warnings = set(warnings) + try: + source_times = _frozen_source_times(scan_dir, scan_id, frozen_source_digests or {}) + except (ContractError, OSError, ValueError) as exc: + raise ContractError("Frozen stopped-scan checkpoint set is incomplete.") from exc parent: dict[str, Any] | None = None parent_manifest: dict[str, Any] | None = None + parent_is_canonical = False + parent_modified = 0 if frozen_source_digests is None or allow_frozen_legacy_parent: try: parent_manifest, parent = _read_saved_parent_result(scan_dir, scan_id) + # Without an accepted head, file-authored coverage is a full replacement. + parent_modified = (scan_dir / "coverage.json").lstat().st_mtime_ns + parent_is_canonical = True except (ContractError, OSError, ValueError) as exc: if not stopped: raise @@ -487,25 +946,66 @@ parent = None if parent_manifest is not None and parent is not None: parent_scan = parent_manifest["scan"] + try: + previous_head, _, head_modified = _read_saved_result( + scan_dir, "checkpoint-head.json", scan_id + ) + except (ContractError, OSError, ValueError): + head_modified = None + if head_modified is not None: + # A partial tool publication must not outrank its accepted head. + parent_modified = min( + (scan_dir / name).lstat().st_mtime_ns + for name in ("findings.json", "coverage.json", "scan-manifest.json") + ) if not parent_scan.get("sealedAt") or allow_frozen_legacy_parent: + head_path = scan_dir / "checkpoint-head.json" + tied_observations = False + if head_modified == parent_modified: + previous_parent, _, _ = _read_saved_result( + scan_dir, f"checkpoints/{previous_head['checkpoint']}", scan_id + ) + if previous_parent != parent: + # Tied observations cannot decide which pending work came last. + parent = _merge_tied_parent_observations(parent, previous_parent) + parent_is_canonical = False + tied_observations = True payload = _encoded(parent) parent_digest = hashlib.sha256(payload).hexdigest() parent_checkpoint = f"checkpoints/{parent_digest}.json" - write_scan_local_bytes(scan_dir, parent_checkpoint, payload) + checkpoint_path = scan_dir / parent_checkpoint + if not checkpoint_path.exists(): + write_scan_local_bytes(scan_dir, parent_checkpoint, payload) + # A recovery copy must not appear newer than the review it copies. + os.utime(checkpoint_path, ns=(parent_modified, parent_modified)) + if head_modified is None or head_modified < parent_modified or tied_observations: + write_scan_local_bytes( + scan_dir, + "checkpoint-head.json", + _encoded({"checkpoint": checkpoint_path.name}), + ) + os.utime(head_path, ns=(parent_modified, parent_modified)) if frozen_source_digests is not None: + captured = _capture_saved_source(scan_dir, "checkpoint-head.json", scan_id) frozen_source_digests = { **frozen_source_digests, parent_checkpoint: parent_digest, + **{path: value[0] for path, value in captured.items()}, } sources: list[tuple[str, dict[str, Any], str | None]] = [] parent_preserved_sources: dict[str, str] = {} source_digests: dict[str, str] = {} + source_order: dict[str, tuple[int, int]] = {} + worker_attempts: dict[str, tuple[str, int]] = {} + saved_heads: dict[str, str] = {} if parent_manifest: recorded = parent_manifest["scan"].get("preservedSources", {}) if isinstance(recorded, dict): parent_preserved_sources = recorded source_digests.update(parent_preserved_sources) + if frozen_source_digests is None: + source_times.update(_frozen_source_times(scan_dir, scan_id, recorded)) paths: dict[str, str | None] = {} reducer_paths: set[str] = set() current_results: set[str] = set() @@ -520,57 +1020,43 @@ except ValueError: warnings.append("Skipped a reducer result outside the scan directory.") - def checkpoints(directory: str, worker_id: str | None) -> None: - for name in _children(scan_dir, directory): - if re.fullmatch(r"[0-9a-f]{64}\.json", name): - paths[f"{directory}/{name}"] = worker_id + def checkpoints(directory: str, worker_id: str | None, attempt: int = 0) -> None: + if worker_id is not None: + root = Path(directory).parent.as_posix() + worker_attempts[root] = (worker_id, attempt) + paths[f"{root}/checkpoint-head.json"] = worker_id + head_snapshots = (Path(directory).parent / "checkpoint-heads").as_posix() + for saved_directory in (directory, head_snapshots): + paths.update(dict.fromkeys(_checkpoint_paths(scan_dir, saved_directory), worker_id)) + paths["checkpoint-head.json"] = None checkpoints("checkpoints", None) for worker in workers: try: - output = Path(worker["artifact_dir"]).relative_to(scan_dir).as_posix() + outputs = _worker_outputs(scan_dir, worker) except (TypeError, ValueError): warnings.append("Skipped a worker checkpoint outside the scan directory.") continue if worker["kind"] == "dedup": - - def reducer_output(directory: str, attempt: int, reducer_worker: Any) -> None: - result_path = f"{directory}/result.json" - checkpoint_paths = [ - f"{directory}/checkpoints/{name}" - for name in _children(scan_dir, f"{directory}/checkpoints") - if re.fullmatch(r"[0-9a-f]{64}\.json", name) - ] + for directory, attempt in outputs: + checkpoint_paths = _checkpoint_paths(scan_dir, f"{directory}/checkpoints") if not checkpoint_paths: - return - paths[result_path] = None - for checkpoint_path in checkpoint_paths: - paths[checkpoint_path] = None - reducer_paths.update([result_path, *checkpoint_paths]) - reducer_outputs.append((reducer_worker, result_path, checkpoint_paths, attempt)) - - reducer_output(output, int(worker["attempt"] or 0), worker) - attempts = ( - Path(output).parent if Path(output).name == "output" else Path(output) - ) / "attempts" - for name in _children(scan_dir, attempts.as_posix()): - match = re.fullmatch(r"attempt-(\d+)", name) - if match: - reducer_output((attempts / name).as_posix(), int(match.group(1)), worker) + continue + result_path = f"{directory}/result.json" + retained_paths = [result_path, *checkpoint_paths] + paths.update(dict.fromkeys(retained_paths)) + reducer_paths.update(retained_paths) + reducer_outputs.append((worker, result_path, checkpoint_paths, attempt)) continue if worker["kind"] != "discovery": continue + output, attempt = outputs[0] paths[f"{output}/result.json"] = worker["id"] current_results.add(f"{output}/result.json") - checkpoints(f"{output}/checkpoints", worker["id"]) - attempts = ( - Path(output).parent if Path(output).name == "output" else Path(output) - ) / "attempts" - for name in _children(scan_dir, attempts.as_posix()): - if re.fullmatch(r"attempt-\d+", name): - archived = (attempts / name).as_posix() - paths[f"{archived}/result.json"] = worker["id"] - checkpoints(f"{archived}/checkpoints", worker["id"]) + for archived, archived_attempt in outputs[1:]: + paths[f"{archived}/result.json"] = worker["id"] + checkpoints(f"{archived}/checkpoints", worker["id"], archived_attempt) + checkpoints(f"{output}/checkpoints", worker["id"], attempt) if worker["result_manifest_path"]: try: current_path = Path(worker["result_manifest_path"]).relative_to(scan_dir).as_posix() @@ -579,7 +1065,26 @@ except ValueError: warnings.append("Skipped a worker result outside the scan directory.") + if frozen_source_digests is None: + for relative, worker_id in list(paths.items()): + if Path(relative).name != "checkpoint-head.json": + continue + del paths[relative] + try: + captured = _capture_saved_source(scan_dir, relative, scan_id) + paths.update({path: worker_id for path in captured}) + except (ContractError, OSError, ValueError) as exc: + if (scan_dir / relative).exists(): + warnings.append(f"Preserved unreadable checkpoint {relative}: {exc}") + if frozen_source_digests is not None: + source_digests.update( + { + path: digest + for path, digest in frozen_source_digests.items() + if _is_source_order_snapshot(path) + } + ) paths = { relative: worker_id for relative, worker_id in paths.items() @@ -591,12 +1096,17 @@ for relative, worker_id in paths.items(): try: - draft, digest = _read_saved_result( + draft, digest, observed = _read_saved_result( scan_dir, relative, scan_id, kind="dedup" if relative in reducer_paths else None ) if frozen_source_digests is not None and frozen_source_digests[relative] != digest: raise ContractError("checkpoint changed after the scan stopped") source_digests[relative] = digest + source_times.setdefault(relative, observed) + source_order[relative] = (0, source_times[relative]) + if _checkpoint_head_directory(relative) is not None: + saved_heads[relative] = draft["checkpoint"] + continue # Recovery expects coverage, but reducer results only contain findings # and context. Add an empty value after hashing the original result. sources.append((relative, {"coverage": {}, **draft}, worker_id)) @@ -607,6 +1117,37 @@ if frozen_source_digests.keys() - source_digests.keys(): raise ContractError("Frozen stopped-scan checkpoint set is incomplete.") + # Frozen observations retain checkpoint selection even if a worker moves its head. + headed_workers = { + worker_attempts[_checkpoint_head_directory(head).as_posix()][0] + for head in saved_heads + if _checkpoint_head_directory(head) != Path(".") + } + for relative, _, worker_id in sources: + if worker_id not in headed_workers: + continue + directory = Path(relative).parent + if directory.name == "checkpoints": + directory = directory.parent + _, attempt = worker_attempts.get(directory.as_posix(), (worker_id, 0)) + source_order[relative] = (attempt, source_order[relative][1]) + selected_observations: dict[str, tuple[int, int]] = {} + parent_heads: list[tuple[int, str]] = [] + for head, checkpoint in saved_heads.items(): + directory = _checkpoint_head_directory(head) + selected = (directory / "checkpoints" / checkpoint).as_posix() + if selected not in source_order: + raise ContractError("Checkpoint head is outside the saved source set.") + observed = source_order[head][1] + if directory == Path("."): + order = (0, max(source_order[selected][1], observed)) + parent_heads.append((observed, selected)) + else: + _, attempt = worker_attempts[directory.as_posix()] + order = (attempt, observed) + selected_observations[selected] = max(selected_observations.get(selected, order), order) + source_order.update(selected_observations) + drafts_by_path = {relative: draft for relative, draft, _ in sources} latest_reducer_key = ( (reducer["completed_at"] or "", reducer["id"], int(reducer["attempt"] or 0)) @@ -627,8 +1168,67 @@ latest_reducer_key = candidate_key latest_reducer = result_path + if parent_heads: + latest_observation = max(observed for observed, _ in parent_heads) + for observed, parent_path in parent_heads: + if observed != latest_observation: + continue + draft = drafts_by_path[parent_path] + modified = source_order[parent_path][1] + if parent is None or modified > parent_modified: + parent = draft + parent_modified = modified + parent_is_canonical = False + elif modified == parent_modified and draft != parent: + parent = _merge_tied_parent_observations(parent, draft) + parent_is_canonical = False if parent is None and latest_reducer is not None: - parent = next((draft for relative, draft, _ in sources if relative == latest_reducer), None) + parent = drafts_by_path[latest_reducer] + + all_sources = ([("parent", parent, None)] if parent else []) + sources + # Older checkpoints can omit IDs already assigned in their published output. + for field in ("deferred", "surfaces"): + named_rows: dict[str | None, list[dict[str, Any]]] = {} + for _, draft, owner in all_sources: + rows = draft["coverage"].get(field, []) + named_rows.setdefault(owner, []).extend( + row + for row in (rows if isinstance(rows, list) else []) + if isinstance(row, dict) and isinstance(row.get("id"), str) + ) + for _, draft, owner in all_sources: + rows = draft["coverage"].get(field, []) + if not isinstance(rows, list): + continue + reserved = { + row["id"] + for row in rows + if isinstance(row, dict) and isinstance(row.get("id"), str) + } + for row in rows: + if not isinstance(row, dict) or "id" in row: + continue + if field == "deferred" and any( + key in row for key in ("candidateId", "candidate", "finding") + ): + continue + content = {"receiptRefs": [], **row} if field == "surfaces" else row + identity = next( + ( + named["id"] + for named in named_rows[owner] + if named["id"] not in reserved + and { + **({"receiptRefs": []} if field == "surfaces" else {}), + **{key: value for key, value in named.items() if key != "id"}, + } + == content + ), + None, + ) + if identity is not None: + row["id"] = identity + reserved.add(identity) if parent is None and not sources: return None @@ -641,6 +1241,13 @@ ): return None + if ( + frozen_source_digests is None + or any(_is_source_order_snapshot(path) for path in source_digests) + or allow_frozen_legacy_parent + ): + _freeze_source_times(scan_dir, scan_id, source_digests, source_times) + target_kind = binding["allowedTargetKinds"][0] if ( target_kind == "git_worktree" @@ -651,15 +1258,9 @@ target = {"kind": target_kind, **binding["target"]} if target["kind"] == "git_diff" and "snapshotDigest" not in target: diff_kind = {"commit": "commit", "branch_diff": "range"}[binding["coverageMode"]] - digest = hashlib.sha256( - b"codex-security-diff/v1\0" - + diff_kind.encode() - + b"\0" - + target["baseRevision"].encode() - + b"\0" - + target["headRevision"].encode() - ).hexdigest() - target["snapshotDigest"] = f"codex-security-snapshot/v1:sha256:{digest}" + target["snapshotDigest"] = committed_diff_snapshot_digest( + diff_kind, target["baseRevision"], target["headRevision"] + ) manifest = ( copy.deepcopy(parent_manifest) if parent_manifest @@ -668,6 +1269,27 @@ for key in ("sealedAt", "artifacts"): manifest["scan"].pop(key, None) manifest["scan"]["preservedSources"] = source_digests + # Completion follows the selected parent, including legacy terminal drafts + # that omit the optional marker, rather than an older canonical manifest. + if parent is not None: + manifest["scan"].pop("complete", None) + if "complete" in parent: + manifest["scan"]["complete"] = parent["complete"] + for key in ("scope", "threatModel"): + if isinstance(parent.get(key), dict): + manifest["scan"][key] = copy.deepcopy(parent[key]) + if isinstance(manifest["scan"].get("scope"), dict): + manifest["scan"]["scope"].update(copy.deepcopy(binding["scope"])) + if frozen_model_source is not None: + # Publication retries retain the choice made with the frozen source set. + model = drafts_by_path.get(frozen_model_source, {}).get("threatModel") + if not isinstance(model, dict): + raise ContractError("Frozen stopped-scan model source is unavailable.") + manifest["scan"]["threatModel"] = copy.deepcopy(model) + if paths[frozen_model_source] is not None: + manifest["scan"]["threatModel"]["origin"] = "recovered" + if selected_model_source is not None: + selected_model_source[:] = [frozen_model_source] coverage = ( copy.deepcopy(parent["coverage"]) if parent and parent["coverage"] @@ -696,7 +1318,7 @@ for field in ("surfaces", "explicitExclusions", "deferred") for item in (coverage.get(field) if isinstance(coverage.get(field), list) else []) } - if parent_manifest + if parent_is_canonical else set() ) findings: list[dict[str, Any]] = [] @@ -725,32 +1347,180 @@ ) return bool(document["findings"]) - all_sources = ([("parent", parent, None)] if parent else []) + sources - current_drafts = ([(None, parent)] if parent else []) + [ - (worker_id, draft) for relative, draft, worker_id in sources if relative in current_results + source_order["parent"] = (0, parent_modified) + deferred_rows = { + relative: _deferred_rows(draft["coverage"]) for relative, draft, _ in all_sources + } + # A legacy source can contain independent tasks with the same explicit ID. + # Later rewrites cannot make an ID-only closure identify one of those tasks. + ambiguous_deferred: set[tuple[str | None, str]] = set() + for relative, _, owner in all_sources: + by_id: dict[str, dict[str, Any]] = {} + candidate_aliases = { + identity + for row in deferred_rows[relative] + if isinstance(row, dict) + and any(key in row for key in ("candidateId", "candidate", "finding")) + for identity in (row.get("id"), row.get("candidateId")) + if isinstance(identity, str) + } + for row in deferred_rows[relative]: + if not isinstance(row, dict) or not isinstance(identity := row.get("id"), str): + continue + if any(key in row for key in ("candidateId", "candidate", "finding")): + continue + if identity in candidate_aliases or (identity in by_id and row != by_id[identity]): + ambiguous_deferred.add((owner, identity)) + by_id[identity] = row + current_drafts = ([("parent", parent, None)] if parent else []) + [ + source for source in sources if source[0] in current_results | selected_observations.keys() + ] + # Generic closures belong to one logical scan or worker, just like candidates. + # Keep them when recovering a terminal checkpoint without its canonical write. + closed_deferred: dict[tuple[str | None, str], tuple[tuple[int, int], dict[str, Any], str]] = {} + + candidate_ids: set[tuple[str | None, str]] = set() + accepted_deferred_orders: dict[tuple[str | None, str], tuple[int, int]] = {} + active_deferred: dict[tuple[str | None, str], tuple[tuple[int, int], dict[str, Any], str]] = {} + coverage_schema = _read_json( + Path(__file__).resolve().parent.parent / "schemas" / "coverage.schema.json" + )["properties"] + for relative, draft, owner in all_sources: + order = source_order[relative] + for item in deferred_rows[relative]: + if not isinstance(item, dict): + continue + if isinstance(item.get("candidateId"), str) or "candidate" in item or "finding" in item: + candidate_ids.update( + (owner, identity) + for identity in (item.get("id"), item.get("candidateId")) + if isinstance(identity, str) + ) + identity = item.get("id") or item.get("candidateId") + if isinstance(identity, str): + key = (owner, identity) + previous = active_deferred.get(key) + if previous is None or order > previous[0]: + active_deferred[key] = (order, item, relative) + if ( + relative == "parent" + or relative in current_results + or relative in selected_observations + ): + try: + # A malformed update cannot discard valid saved evidence. + _validate_schema_node( + item, coverage_schema["deferred"]["items"], "coverage.deferred" + ) + except ContractError: + continue + accepted_deferred_orders[key] = max( + accepted_deferred_orders.get(key, order), order + ) + for closure in _resolved_deferred_rows(draft, coverage_schema["resolvedDeferred"]): + key = (owner, closure["id"]) + previous = closed_deferred.get(key) + if previous is None or order > previous[0]: + # The parent comes first, preserving its reason on equal timestamps. + closed_deferred[key] = (order, closure, relative) + reopened_rows: list[tuple[str | None, dict[str, Any]]] = [] + reopened_generic: set[tuple[str | None, str]] = set() + for key, (order, _, _) in list(closed_deferred.items()): + # Equal timestamps cannot distinguish closure from reopened work. + reopened = [ + active + for (owner, identity), active in active_deferred.items() + if owner == key[0] + and (identity == key[1] or active[1].get("candidateId") == key[1]) + and active[0] >= order + ] + if key in candidate_ids or key in ambiguous_deferred or reopened: + del closed_deferred[key] + for _, item, _ in reopened: + reopened_rows.append((key[0], item)) + if key not in candidate_ids: + reopened_generic.add(key) + # Retain every distinct ambiguous task even when a terminal result supersedes + # its source. Existing output ID normalization keeps each row distinct. + for relative, _, owner in all_sources: + for row in deferred_rows[relative]: + if ( + isinstance(row, dict) + and isinstance(identity := row.get("id"), str) + and (owner, identity) in ambiguous_deferred + and not any(key in row for key in ("candidateId", "candidate", "finding")) + and (owner, row) not in reopened_rows + ): + reopened_rows.append((owner, row)) + parent_closures = [ + closure for (owner, _), (_, closure, _) in closed_deferred.items() if owner is None ] + coverage.pop("resolvedDeferred", None) + if parent_closures: + coverage["resolvedDeferred"] = copy.deepcopy(parent_closures) + if parent: + coverage["deferred"] = [ + row + for row in deferred_rows["parent"] + if not isinstance(row, dict) + or not isinstance(row.get("id"), str) + or (None, row["id"]) not in closed_deferred + ] resolved: dict[tuple[str | None, str], str] = {} - for owner, draft in current_drafts: + + ordered_candidates = { + (owner, identity) + for owner, row in reopened_rows + if (identity := _deferred_candidate_id(row, owner, ambiguous_deferred)) is not None + and (owner, identity) in candidate_ids + } + ordered_outcomes: dict[tuple[str | None, str], tuple[tuple[int, int], str]] = {} + + outcomes: list[tuple[str, str | None, str, str]] = [] + for relative, draft, owner in current_drafts: for finding in draft["findings"]: if ( isinstance(finding, dict) and valid_finding(finding) and (candidate_id := finding_candidate_id(finding)) ): - resolved.setdefault((owner, candidate_id), "reported") + outcomes.append((relative, owner, candidate_id, "reported")) for field in ("surfaces", "explicitExclusions"): items = draft["coverage"].get(field, []) for item in items if isinstance(items, list) else []: if ( isinstance(item, dict) and isinstance(item.get("candidateId"), str) - and item.get("disposition") in {"reported", "rejected", "not_applicable"} + and item.get("disposition") in {"rejected", "not_applicable"} ): - resolved.setdefault((owner, item["candidateId"]), item["disposition"]) + outcomes.append((relative, owner, item["candidateId"], item["disposition"])) + ordered_candidates.update( + (owner, candidate_id) + for relative, owner, candidate_id, _ in outcomes + if owner is not None and relative in selected_observations + ) + # Reopened work and selected checkpoint outcomes follow the saved source order. + for relative, owner, candidate_id, disposition in outcomes: + key = (owner, candidate_id) + if key not in ordered_candidates: + if relative == "parent" or relative in current_results: + resolved.setdefault(key, disposition) + continue + order = source_order[relative] + if any( + saved_owner == owner + and _deferred_candidate_id(row, owner, ambiguous_deferred) == candidate_id + and modified >= order + for (saved_owner, _), (modified, row, _) in active_deferred.items() + ): + continue + if key not in ordered_outcomes or order > ordered_outcomes[key][0]: + resolved[key] = disposition + ordered_outcomes[key] = (order, relative) # Only the current parent may claim that another worker finding was absorbed. # A superseded checkpoint must not suppress a newer independent result. - for draft in [parent] if parent else []: - for finding in draft["findings"]: + if parent: + for finding in parent["findings"]: if valid_finding(finding): canonical_key = _finding_key(finding) for retained in _retained_findings(finding): @@ -787,26 +1557,155 @@ represented_candidate_history.setdefault(candidate_key, set()).add( _digest(_finding_content(original["finding"])) ) - resolved.setdefault(candidate_key, "reported") + replaced_surfaces, surface_updates = _generic_surface_updates( + all_sources, + source_order, + closed_deferred, + active_deferred, + resolved, + reopened_generic, + coverage_schema["surfaces"]["items"], + deferred_rows, + ambiguous_deferred, + ) + replaced_rows = { + "surfaces": replaced_surfaces, + "deferred": { + id(row) + for relative, _, owner in all_sources + for row in deferred_rows[relative] + if isinstance(row, dict) + and isinstance(identity := row.get("id"), str) + and (key := (owner, identity)) not in candidate_ids + and key not in ambiguous_deferred + and (updated := accepted_deferred_orders.get(key)) is not None + and updated > source_order[relative] + }, + } + for field, replaced in replaced_rows.items(): + if parent and isinstance(coverage.get(field), list): + previous = parent["coverage"].get(field, []) + if isinstance(previous, list): + removed_parent = [row for row in previous if id(row) in replaced] + coverage[field] = [row for row in coverage[field] if row not in removed_parent] + if isinstance(coverage.get("surfaces"), list): + for surface in surface_updates: + if surface not in coverage["surfaces"]: + coverage["surfaces"].append(surface) + for surface in coverage["surfaces"]: + if isinstance(surface, dict): + surface.setdefault("receiptRefs", []) + + # Reopened work survives a superseded checkpoint, but current candidate + # outcomes still apply. Parent closures cannot remove another worker's row. + for owner, item in reopened_rows: + if (identity := _deferred_candidate_id(item, owner, ambiguous_deferred)) is not None and ( + owner, + identity, + ) in resolved: + continue + pending = coverage.setdefault("deferred", []) + if isinstance(pending, list) and item not in pending: + pending.append(copy.deepcopy(item)) + ambiguous_surface_ids = { + (owner, identity) + for owner, row in reopened_rows + if isinstance(row.get("id"), str) + and (owner, row["id"]) in ambiguous_deferred + and not any(key in row for key in ("candidateId", "candidate", "finding")) + for identity in [ + row["id"], + *(row.get("surfaceIds", []) if isinstance(row.get("surfaceIds", []), list) else []), + ] + if isinstance(identity, str) + } + for _, draft, owner in all_sources: + surfaces = draft["coverage"].get("surfaces", []) + by_id: dict[str, dict[str, Any]] = {} + for surface in surfaces if isinstance(surfaces, list) else []: + if not isinstance(surface, dict) or not isinstance(identity := surface.get("id"), str): + continue + if identity in by_id and surface != by_id[identity]: + ambiguous_surface_ids.add((owner, identity)) + by_id[identity] = surface + for _, draft, owner in all_sources: + surfaces = draft["coverage"].get("surfaces", []) + for surface in surfaces if isinstance(surfaces, list) else []: + if ( + isinstance(surface, dict) + and isinstance(surface.get("id"), str) + and (owner, surface["id"]) in ambiguous_surface_ids + and surface.get("disposition") == "needs_follow_up" + and not any(key in surface for key in ("candidateId", "candidate", "finding")) + and isinstance(coverage.get("surfaces"), list) + ): + retained_surface = {**surface, "receiptRefs": surface.get("receiptRefs", [])} + if retained_surface not in coverage["surfaces"]: + coverage["surfaces"].append(copy.deepcopy(retained_surface)) + selected_terminal_orders: dict[str, tuple[int, int]] = {} + for relative, draft, worker_id in sources: + if ( + worker_id is not None + and relative in selected_observations + and draft.get("complete") is not False + ): + order = source_order[relative] + selected_terminal_orders[worker_id] = max( + selected_terminal_orders.get(worker_id, order), order + ) + terminal_worker_orders: dict[str | None, tuple[int, int]] = {} + for relative, draft, worker_id in sources: + if relative in current_results and draft.get("complete") is not False: + order = source_order[relative] + terminal_worker_orders[worker_id] = max( + terminal_worker_orders.get(worker_id, order), order + ) for relative, draft, worker_id in all_sources: + worker_result_order = terminal_worker_orders.get(worker_id) + selected_coverage_superseded = worker_id in selected_terminal_orders and ( + source_order[relative] < selected_terminal_orders[worker_id] + or ( + relative in current_results + and source_order[relative] <= selected_terminal_orders[worker_id] + ) + ) + selected_candidates = { + candidate_id + for (owner, candidate_id), (_, source) in ordered_outcomes.items() + if owner == worker_id and source == relative + } superseded = ( worker_id is None and parent is not None and parent.get("complete") is not False and relative != "parent" + and source_order[relative] <= (0, parent_modified) and (not stopped_parent_seal or relative in parent_preserved_sources) ) or ( relative not in current_results - and any( - saved_worker == worker_id - and saved_path in current_results - and current.get("complete") is not False - for saved_path, current, saved_worker in sources + and worker_result_order is not None + and ( + relative not in selected_observations + or source_order[relative] < worker_result_order ) ) + # A failed result write can leave pending work outside the accepted result. + accepted_order = ( + (0, parent_modified) + if worker_id is None and parent is not None + else worker_result_order + ) + retain_pending = ( + superseded + and (worker_id in headed_workers or (worker_id is None and parent_heads)) + and accepted_order is not None + and source_order[relative] >= accepted_order + and any(isinstance(row, dict) for row in deferred_rows[relative]) + ) if ( - (relative != "parent" or not parent_manifest) + (relative != "parent" or not parent_is_canonical) and not superseded + and not selected_coverage_superseded and ( draft.get("complete") is False or draft["coverage"].get("completeness") != "complete" @@ -814,16 +1713,56 @@ and coverage.get("completeness") in {"complete", "unknown"} ): coverage["completeness"] = "partial" - if ( + skip_superseded_findings = ( superseded and not stopped and all(valid_finding(finding) for finding in (parent["findings"] if parent else [])) - ): + ) + if skip_superseded_findings and not selected_candidates and not retain_pending: continue - if "threatModel" not in manifest["scan"] and isinstance(draft.get("threatModel"), dict): - manifest["scan"]["threatModel"] = copy.deepcopy(draft["threatModel"]) + if ( + not skip_superseded_findings + and "threatModel" not in manifest["scan"] + and isinstance(draft.get("threatModel"), dict) + ): + model = draft["threatModel"] + model_path = relative + checkpoint_dir = Path(relative).parent + prefer_worker_head = worker_id is not None and ( + checkpoint_dir.name == "checkpoints" or draft.get("complete") is False + ) + if worker_id is not None: + if checkpoint_dir.name != "checkpoints": + checkpoint_dir /= "checkpoints" + selected_models = [ + path + for path in selected_observations + if Path(path).parent == checkpoint_dir + and isinstance(drafts_by_path[path].get("threatModel"), dict) + ] + if selected_models: + head_path = max(selected_models, key=source_order.__getitem__) + current = drafts_by_path[head_path] + # A terminal checkpoint is committed before result.json is replaced. + # Use the admitted observation, including its frozen ordering on retries. + if not prefer_worker_head and current.get("complete") is not False: + prefer_worker_head = source_order[head_path] >= source_order[relative] + if prefer_worker_head: + model = current["threatModel"] + model_path = head_path + manifest["scan"]["threatModel"] = copy.deepcopy(model) + if worker_id is not None: + manifest["scan"]["threatModel"]["origin"] = "recovered" + if selected_model_source is not None and worker_id is not None: + selected_model_source[:] = [model_path] for value in draft["findings"]: - if relative == "parent" and parent_manifest: + if skip_superseded_findings and not ( + isinstance(value, dict) + and (candidate_id := finding_candidate_id(value)) in selected_candidates + and resolved.get((worker_id, candidate_id)) == "reported" + ): + continue + if relative == "parent" and parent_is_canonical: finding = copy.deepcopy(value) _ensure_finding_identity(finding, candidate_only=True) provenance = finding.get("provenance") if isinstance(finding, dict) else None @@ -846,7 +1785,6 @@ if not isinstance(value, dict): warnings.append(f"Retained malformed finding evidence in {relative}.") continue - source_value = copy.deepcopy(value) finding = copy.deepcopy(value) candidate_id = finding_candidate_id(finding) if relative != "parent" and resolved.get((worker_id, candidate_id)) in { @@ -909,24 +1847,19 @@ historical_contents = set() if mapped_key is not None: key = mapped_key - represented_by_parent = ( - _digest(_finding_content(source_value)) in historical_contents - ) + represented_by_parent = _digest(_finding_content(value)) in historical_contents if key in finding_positions: retained = findings[finding_positions[key]] if finding != retained: - if represented_by_parent: - previous = copy.deepcopy(source_value) - previous_history = previous.get("provenance", {}).pop( - "previousFindings", [] - ) - elif _finding_strength(finding) > _finding_strength(retained): + if not represented_by_parent and _finding_strength(finding) > _finding_strength( + retained + ): previous = copy.deepcopy(retained) previous_history = previous["provenance"].pop("previousFindings", []) retained = finding findings[finding_positions[key]] = retained else: - previous = copy.deepcopy(source_value) + previous = copy.deepcopy(value) previous_history = previous.get("provenance", {}).pop( "previousFindings", [] ) @@ -960,10 +1893,14 @@ continue finding_positions[key] = len(findings) findings.append(finding) - if superseded: + if superseded and not selected_candidates and not retain_pending: continue for field in ("surfaces", "explicitExclusions", "deferred", "openQuestions"): + if superseded and field not in {"surfaces", "explicitExclusions", "deferred"}: + continue items = draft["coverage"].get(field, []) + if field == "deferred": + items = deferred_rows[relative] if not isinstance(items, list): continue output = coverage.setdefault(field, []) @@ -972,6 +1909,40 @@ # recovery and warnings rather than silently changing its contract. continue for item in items: + # A selected terminal checkpoint replaces ordinary coverage, while + # candidate evidence and pending tasks retain their own reconciliation. + if ( + selected_coverage_superseded + and field != "deferred" + and not ( + isinstance(item, dict) + and any(key in item for key in ("candidateId", "candidate", "finding")) + ) + ): + continue + # A selected outcome must retain its evidence even if its result write failed. + if superseded and not ( + isinstance(item, dict) + and ( + (field == "deferred" and retain_pending) + or ( + isinstance(item.get("candidateId"), str) + and item["candidateId"] in selected_candidates + and item.get("disposition") + == resolved.get((worker_id, item["candidateId"])) + ) + ) + ): + continue + if id(item) in replaced_rows.get(field, ()): + continue + if ( + field == "deferred" + and isinstance(item, dict) + and isinstance(item.get("id"), str) + and (worker_id, item["id"]) in closed_deferred + ): + continue if field == "openQuestions" and isinstance(item, str): item = {"question": item.strip()} if ( @@ -995,14 +1966,20 @@ history.append(copy.deepcopy(finding)) if ( isinstance(item, dict) - and (worker_id, item.get("candidateId")) in resolved + and isinstance( + identity := _deferred_candidate_id(item, worker_id, ambiguous_deferred) + if field == "deferred" + else item.get("candidateId"), + str, + ) + and (worker_id, identity) in resolved and (field == "deferred" or item.get("disposition") == "needs_follow_up") ): continue + if field == "surfaces" and isinstance(item, dict): + item = {**item, "receiptRefs": item.get("receiptRefs", [])} if isinstance(item, dict) and "id" not in item: semantic_item = dict(item) - if field == "surfaces": - semantic_item.setdefault("receiptRefs", []) if any( isinstance(existing, dict) and {key: value for key, value in existing.items() if key != "id"} @@ -1036,7 +2013,10 @@ if isinstance(item.get("id"), str): used.add(item["id"]) continue - item.setdefault("id", item.get("candidateId") or f"saved-{_digest(item)[:16]}") + item.setdefault("id", _saved_coverage_id(item)) + if not isinstance(item["id"], str): + # Preserve malformed rows for per-record recovery, including frozen replay. + continue if item["id"] in used: item["id"] = f"{item['id']}-{_digest(item)[:16]}" used.add(item["id"]) @@ -1081,6 +2061,9 @@ except ContractError: path = scan_dir / relative if path.exists() or path.is_symlink(): + if relative == "threatmodel.md": + # This optional projection will not replace an unsafe destination. + continue raise snapshots[relative] = None finally: @@ -1112,13 +2095,17 @@ if scan["status"] != "failed": return False frozen_source_digests: dict[str, str] | None = None + model_source: list[str] = [] + saved_model_source: str | None = None raw_frozen_sources = scan["retained_source_digests_json"] + if raw_frozen_sources is not None: + frozen_source_digests, saved_model_source = _retained_source_state( + json.loads(raw_frozen_sources) + ) + if saved_model_source is not None: + model_source.append(saved_model_source) if recovery_source_digests is not None: frozen_source_digests = recovery_source_digests - elif raw_frozen_sources is not None: - frozen_source_digests = _source_digests( - json.loads(raw_frozen_sources), "Saved stopped-scan" - ) scan_dir = db.require_canonical_scan_directory(Path(scan["scan_dir"])) deep_run = connection.execute( "SELECT status FROM deep_scan_runs WHERE scan_id = ?", (scan_id,) @@ -1141,12 +2128,10 @@ ] def record_publication(manifest: dict[str, Any], findings: dict[str, Any]) -> None: - retained_sources = manifest.get("scan", {}).get("preservedSources") - if not isinstance(retained_sources, dict) or not all( - isinstance(relative, str) and isinstance(source_digest, str) - for relative, source_digest in retained_sources.items() - ): - raise ContractError("Stopped scan source digests could not be frozen.") + retained_sources = _source_digests( + manifest.get("scan", {}).get("preservedSources"), + "Stopped scan source digests could not be frozen.", + ) digest = db.published_manifest_digest(scan_dir, manifest) timestamp = db.now() with connection: @@ -1176,7 +2161,7 @@ "updated_at = ? WHERE id = ? AND status = 'failed'", ( digest, - json.dumps(retained_sources, sort_keys=True), + _encode_retained_sources(retained_sources, model_source), json.dumps(list(dict.fromkeys(warnings))), timestamp, scan_id, @@ -1190,6 +2175,7 @@ existing_path = db.artifact_path(scan_dir, db.ARTIFACTS["manifest"], required=False) existing_scan = db.read_json_object(existing_path).get("scan", {}) if existing_path else {} + existing = None if scan["seal_manifest_digest"] is not None or ( isinstance(existing_scan, dict) and ( @@ -1198,30 +2184,35 @@ ): db.require_recorded_manifest_digest(scan, scan_dir) existing, existing_findings, _ = finalize_scan( - scan_dir, expected_coverage_mode=db.expected_coverage_mode(scan) + scan_dir, + expected_coverage_mode=db.expected_coverage_mode(scan), + projection_warnings=warnings, ) db.verify_manifest_binding(scan, existing) if existing_scan.get("status") == outcome: existing_sources = existing_scan.get("preservedSources") if frozen_source_digests is None: - if not isinstance(existing_sources, dict) or not all( - isinstance(relative, str) and isinstance(digest, str) - for relative, digest in existing_sources.items() - ): - raise ContractError("Stopped scan source digests could not be frozen.") - frozen_source_digests = existing_sources + frozen_source_digests = _source_digests( + existing_sources, "Stopped scan source digests could not be frozen." + ) if existing_sources == frozen_source_digests: if ( raw_frozen_sources is not None and scan["seal_manifest_digest"] is not None and not publication_follow_up_warnings + and warnings == stored_warnings ): return True record_publication(existing, existing_findings) return True if recovery_source_digests is None: raise ContractError("Stopped scan sources changed after terminal publication.") - binding = {**db.workbench_completion_binding(scan, scan["completed_at"]), "status": outcome} + binding = { + **db.workbench_completion_binding(scan, scan["completed_at"], existing), + "status": outcome, + } + if recovery_source_digests is not None: + model_source.clear() documents = merge_saved_results( scan_dir, scan_id, @@ -1237,6 +2228,8 @@ f"{scan['failure_message'] or ''}" ).strip(), frozen_source_digests=frozen_source_digests, + frozen_model_source=model_source[0] if model_source else None, + selected_model_source=model_source, allow_frozen_legacy_parent=( include_parent_with_recovery or ( @@ -1258,18 +2251,22 @@ (json.dumps(unpublished_warnings), db.now(), scan_id), ) return False - if frozen_source_digests is None: - retained_sources = documents[0].get("scan", {}).get("preservedSources") - if not isinstance(retained_sources, dict) or not all( - isinstance(relative, str) and isinstance(digest, str) - for relative, digest in retained_sources.items() - ): - raise ContractError("Stopped scan source digests could not be frozen.") + if frozen_source_digests is None or ( + recovery_source_digests is None and saved_model_source is None and model_source + ): + retained_sources = _source_digests( + documents[0].get("scan", {}).get("preservedSources"), + "Stopped scan source digests could not be frozen.", + ) with connection: connection.execute( "UPDATE scans SET retained_source_digests_json = ? " - "WHERE id = ? AND retained_source_digests_json IS NULL", - (json.dumps(retained_sources, sort_keys=True), scan_id), + "WHERE id = ? AND retained_source_digests_json IS ?", + ( + _encode_retained_sources(retained_sources, model_source), + scan_id, + raw_frozen_sources, + ), ) prepared = _prepare_scan_finalization( scan_dir, @@ -1280,7 +2277,9 @@ ) snapshots = _snapshot_published_outputs(scan_dir) try: - manifest, findings, _ = _write_prepared_scan_finalization(prepared) + manifest, findings, _ = _write_prepared_scan_finalization( + prepared, projection_warnings=warnings + ) db.verify_manifest_binding(scan, manifest) record_publication(manifest, findings) except BaseException: @@ -1399,6 +2398,28 @@ "The scan stopped; its saved checkpoint was retained without replacing sealed results." ) scan_dir = db.require_canonical_scan_directory(Path(scan["scan_dir"])) + if ( + args.expected_draft_digest is not None + and args.expected_draft_digest != _scan_draft_digest(scan_dir) + ): + raise SystemExit( + "scan_draft_conflict: canonical scan results changed; reconcile the saved checkpoint again." + ) + try: + relative = Path(args.draft_path).relative_to(scan_dir).as_posix() + except ValueError as exc: + raise SystemExit( + "Scan draft must be inside the registered scan drafts directory." + ) from exc + if not re.fullmatch(r"drafts/[0-9a-fA-F-]+\.json", relative): + raise SystemExit("Scan draft must be inside the registered scan drafts directory.") + draft = _read_scan_local_json(scan_dir, relative, "Staged scan draft") + manifest, findings, coverage = draft["manifest"], draft["findings"], draft["coverage"] + binding = db.workbench_completion_binding(scan, db.now()) + # Save scan IDs without sealing the draft. + _populate_unsealed_manifest_envelope(manifest, manifest["scan"], binding) + _populate_unsealed_artifact_envelope(manifest, findings, coverage, binding) + _validate_completion_binding(manifest, findings, coverage, binding) if args.checkpoint_path is not None: try: checkpoint_relative = Path(args.checkpoint_path).relative_to(scan_dir).as_posix() @@ -1421,33 +2442,15 @@ f"checkpoints/{checkpoint_digest}.json", checkpoint_contents, ) - if ( - args.expected_draft_digest is not None - and args.expected_draft_digest != _scan_draft_digest(scan_dir) - ): - raise SystemExit( - "scan_draft_conflict: canonical scan results changed; reconcile the saved checkpoint again." - ) - try: - relative = Path(args.draft_path).relative_to(scan_dir).as_posix() - except ValueError as exc: - raise SystemExit( - "Scan draft must be inside the registered scan drafts directory." - ) from exc - if not re.fullmatch(r"drafts/[0-9a-fA-F-]+\.json", relative): - raise SystemExit("Scan draft must be inside the registered scan drafts directory.") - draft = _read_scan_local_json(scan_dir, relative, "Staged scan draft") - manifest, findings, coverage = draft["manifest"], draft["findings"], draft["coverage"] - binding = db.workbench_completion_binding(scan, db.now()) - # Validate on copies: saved canonical documents remain ordinary unsealed drafts. - copied_manifest = copy.deepcopy(manifest) - copied_findings = copy.deepcopy(findings) - copied_coverage = copy.deepcopy(coverage) - _populate_unsealed_manifest_envelope(copied_manifest, copied_manifest["scan"], binding) - _populate_unsealed_artifact_envelope( - copied_manifest, copied_findings, copied_coverage, binding + checkpoint = _parent_scan_draft(scan_id, manifest["scan"], findings, coverage) + checkpoint_contents = _encoded(checkpoint) + checkpoint_name = f"{hashlib.sha256(checkpoint_contents).hexdigest()}.json" + checkpoint_relative = f"checkpoints/{checkpoint_name}" + if not (scan_dir / checkpoint_relative).exists(): + write_scan_local_bytes(scan_dir, checkpoint_relative, checkpoint_contents) + write_scan_local_bytes( + scan_dir, "checkpoint-head.json", _encoded({"checkpoint": checkpoint_name}) ) - _validate_completion_binding(copied_manifest, copied_findings, copied_coverage, binding) for filename, document in ( ("findings.json", findings), ("coverage.json", coverage), @@ -1458,9 +2461,17 @@ filename, (json.dumps(document, allow_nan=False, indent=2) + "\n").encode(), ) + model_warning = write_threat_model_projection_if_possible(scan_dir, manifest) # Accepted Standard drafts are evidence of review or report assembly, # even when the parent omitted its explicit progress call. - if scan["mode"] == "standard": + model_only_checkpoint = ( + manifest["scan"].get("complete") is False + and isinstance(manifest["scan"].get("threatModel"), dict) + and not findings.get("findings") + and not coverage.get("surfaces") + and not coverage.get("deferred") + ) + if scan["mode"] == "standard" and not model_only_checkpoint: phase = "discovery" if manifest["scan"].get("complete") is False else "reporting" earlier = PHASES[: PHASES.index(phase)] placeholders = ",".join("?" for _ in earlier) @@ -1481,7 +2492,11 @@ ) except sqlite3.Error as exc: print(f"Could not save scan progress: {exc}", file=sys.stderr) - return {"scanId": scan_id, "status": "draft_written"} + return { + "scanId": scan_id, + "status": "draft_written", + **({"warnings": [model_warning]} if model_warning else {}), + } def _scan_draft_digest(scan_dir: Path) -> str: @@ -1494,9 +2509,10 @@ except FileNotFoundError: digest.update(b"missing\0") continue - _, contents = _read_scan_local_json_bytes(scan_dir, filename, filename) digest.update(b"present\0") - digest.update(contents) + descriptor = open_scan_local_file_descriptor(scan_dir, filename, filename) + with os.fdopen(descriptor, "rb") as handle: + digest.update(handle.read()) digest.update(b"\0") return digest.hexdigest()
Full technical diff · 148 changed fields
changed /files/.app.json/sha256
"e4d5b22326ee380de5d779f7b5ba590c8d1bee9a80e8869fb2bd7f4def8e974d"
"a7bbe314059c7311e169474823e3978f267e560c3e961ba49fb94032da9843fe"
changed /files/.app.json/size
480
479
changed /files/.codex-plugin~1plugin.json/sha256
"3ddcc187fe2f1aff961bf55444e70bfbc3f825b6185ab3f29860d1a813a395f9"
"527d1a0db286884729572d400b0ad9b0ff852d4a12b36228ff5fbef180f7de9d"
changed /files/.mcp.json/sha256
"40b84baf510169eadfd5f0fe0ad95001caedb7bc662d77c9ac2a4fbec7dd0845"
"7c5968060f6e848859ce02450d6e02e67029d8d39011bbda1059b19de6309ebf"
changed /files/.mcp.json/size
1649
1679
changed /files/examples~1completed-scan~1report.md/sha256
"c2083b2f763ed5ddcee4524ee01025c9b8996e1e9740263d751494b7a61fe82e"
"83b7c709c0b49d1500b52e61a88ea70b8423ebebec38308b1294cf094838b66f"
changed /files/examples~1completed-scan~1report.md/size
2945
4116
changed /files/integrity.json/sha256
"d8f7683d4ed54915958087c1532c15a0eb0e87db1424addea133794979b609cc"
"6b342df66fb9b5ce7fc8c9de0e974b59d21340cdc06167df46d1a19bd4ea9441"
changed /files/integrity.json/size
17616
17465
changed /files/mcp~1helpers.mjs.br.part-000/sha256
"a771236cf650b155f4afc89169897ca199bafaa673187b781904df8bee8af96a"
"6299f73c92c4f924bffc6b1bed001480603473967006b3e75941bc49a21f18d5"
changed /files/mcp~1helpers.mjs.br.part-000/size
6539
63398
changed /files/mcp~1native~1THIRD_PARTY_NOTICES.txt/sha256
"bcb3277e216ef458f18b80ddf97e1b7c2827e920a2c432077ff6f7d060281445"
"16ac63908855f5bb7eab864923cc1855b406bd7051dfcd70d7e69684f6c69955"
changed /files/mcp~1native~1darwin-arm64~1unix.node/sha256
"1895d5afd007ce02210075683413be6088ea20af8a8608a42ac82b7d9bf90181"
"093e87d8de3d2676fc9a2a1430f571bf4fc970b2bbef4c7ded0bf8924e45f473"
changed /files/mcp~1native~1darwin-arm64~1unix.node/size
371616
354352
changed /files/mcp~1native~1darwin-x64~1unix.node/sha256
"94aba646f50e669b873b44f78512886dcf78be87a13feb95471e687fdc1f9f91"
"57c45d212d35ce7599afc7b9b21693e18443753231bad1fb5146a4e224db68ec"
changed /files/mcp~1native~1darwin-x64~1unix.node/size
364544
343296
changed /files/mcp~1native~1linux-arm64-gnu~1unix.node/sha256
"bd89532fbd21d251d9c645d6026864b0a08b16565bedb018e627e459cae0ddef"
"6b352aaaac21f9ad9b772481429ba7bc8433a0e44ca5ffe433ad47289c3007f6"
changed /files/mcp~1native~1linux-arm64-gnu~1unix.node/size
465648
465088
changed /files/mcp~1native~1linux-arm64-musl~1unix.node/sha256
"65475c3deb4ed355739ad7395e39204f5ffcc945c61b2e42ca357560de3fe7de"
"3a55ec4f42202c58a29654701c31a8deab386969d14e2ab85f23e81345b4fedb"
changed /files/mcp~1native~1linux-arm64-musl~1unix.node/size
397992
332456
changed /files/mcp~1native~1linux-x64-gnu~1unix.node/sha256
"d19c3d41b2993543a1296dc38d2183df4a947349b45f3c43618c4ecde74e3cac"
"ed2061078400010852a9f8d3ecdf7bc0a008d13adc9e9d5c6954f5cb686a2a2a"
changed /files/mcp~1native~1linux-x64-gnu~1unix.node/size
386424
365352
changed /files/mcp~1native~1linux-x64-musl~1unix.node/sha256
"46fa397234fad923a2e325f80e2a833ba48c79b32bbb7f0452fb7f0791de5d5e"
"c2e4c07ac6dde175464c6315937652465b200e5f28590898663523beaf4e9ab6"
changed /files/mcp~1native~1linux-x64-musl~1unix.node/size
385688
369304
changed /files/mcp~1native~1win32-arm64~1windows.node/sha256
"df8dd512fd8d92a8af3fede3f00a235732f847c6f3da91a2b9a702650ddd7439"
"19f78e88fd8934038201c26b044123bc05db74f5d7065df10a32583ccbcf7a55"
changed /files/mcp~1native~1win32-arm64~1windows.node/size
460800
419840
changed /files/mcp~1native~1win32-x64~1windows.node/sha256
"f961856d20361508fa3b5ad05a4fb04f07d0dff4d7b60f3cfc377467b518c17b"
"5aed21edb5fe06efac49d9bd3899d6242f60b1bfac2b710b967a76c924d98298"
changed /files/mcp~1native~1win32-x64~1windows.node/size
496128
453120
changed /files/mcp~1server.mjs.br.part-000/sha256
"0259e86ae3ceb3c555521d959fd9cddb0d67e2921fc1db9fd27a55d849a48806"
"7ef9c7387b405b51e2aec793a86696bf793506a8f34155199035aa515b3a415a"
changed /files/mcp~1server.mjs.br.part-001/sha256
"3c6c93cd01ac739436e91ffe2685c5267672cf18794dbe1b19ac1ac5f93d421f"
"94d6412009e284ecbc17bd14d07e0116d8e74e572a62198912ac5a03f95a50c2"
changed /files/mcp~1server.mjs.br.part-001/size
122535
126028
changed /files/references~1artifact-storage.md/sha256
"851076fcdc93c7adb87f3e26ff103c5546b875e2bc08ddb676fbbca0827899b1"
"ccce9c76f5d044fbce333d6129fbb55eb63f512c15c2ebeeca405e296f38325b"
changed /files/references~1artifact-storage.md/size
6975
7632
changed /files/references~1config-preflight.md/sha256
"6ba05681c8d94968842fc98553b58b8fc5f1ea3da45834a41dd60179c8bbb9f3"
"5894f9fd7efcae7034404a2c062ee3d7417520753be16ddcdb3acf5b884f048d"
changed /files/references~1config-preflight.md/size
18881
19162
changed /files/references~1core-scan.md/sha256
"77b082eb8613cf93427ff730e4ae5d85b0a0dca37c02a8af1ea69f679ac3d1d9"
"9c0eca53147383a89cd345eac7791ea75b954f7f083448ef25019d8f9ba30afe"
changed /files/references~1core-scan.md/size
19422
22978
changed /files/references~1final-report.md/sha256
"9fe5a42b37964def3a8394449c3502e8b6ee50b8f85e6525313e23ba2dcd5040"
"9c34375584c8ae76959b0eae038b86b2b8ed7874517c249fcae8715314c76298"
changed /files/references~1final-report.md/size
29052
29931
changed /files/references~1scan-artifacts.md/sha256
"d8d416b359b9a46bd3d9cf9fb487d1ff2dc6eb32dc26e93ab47de7b1d240f479"
"eecdccac30b8ad65bb702dc9ac8d9718dcb0b95532a94a61195268d22bdf504b"
changed /files/references~1scan-artifacts.md/size
10800
11243
changed /files/references~1scan-contract.md/sha256
"8a84ca15feef973044bfe619b4e586bf9323b9cbc2f386b961138c76ebd6d189"
"b362636c29d1e8b18053481abe2f97d7bb099a97040f5b63aa2702a41c1d62c8"
changed /files/references~1scan-contract.md/size
14043
14537
changed /files/references~1threat-model.md/sha256
"f53d10c617d45d671e7cb15408f87c4f36b29026e2a6a59d1457881780b6818a"
"f3dd2d05d7f0c61b13d29a78e810b367772fe22d5ac58adbbc00080a9495706e"
changed /files/references~1threat-model.md/size
14690
15203
changed /files/schemas~1coverage.schema.json/sha256
"7964b132998ca4dcdd19c75f5d92483e1d44cb71462237709b968ec548c10652"
"6ac14e659e884e9582d8bd9ba480f6364ca17309e1a3dee9469ab025a3380b11"
changed /files/schemas~1coverage.schema.json/size
4670
5198
changed /files/schemas~1scan-manifest.schema.json/sha256
"265a48629113f77cd65a3127f1f7e95d3c39ae60e868685837a6aa31d4133310"
"c621d4136ac81741ac8da1960c7f5e92ade22c0b5490e378ea4339b09e45b6f3"
changed /files/schemas~1scan-manifest.schema.json/size
8048
9513
changed /files/schemas~1tools~1scan-draft.schema.json/sha256
"d5970c06e2ec00d1da5b0e84b677244f32046fd99c81901e608b7974b9ab0d89"
"151f9dffc83e1c5b4f0a7fba20b90fb3196fdaa9719180eb0009ba2b50f75d48"
changed /files/schemas~1tools~1scan-draft.schema.json/size
21535
23475
changed /files/scripts~1config_preflight.py/sha256
"37b9f4cee4c4dddade0e8599ea8df19aee29a99bd1be956b553ab46c3b63d407"
"f4d4103650977d078b71f49da6b1e287b6db2f2b7b5fb08e943722a6d869354e"
changed /files/scripts~1config_preflight.py/size
35211
35176
changed /files/scripts~1deep_scan_workbench.py/sha256
"c659739775e192dee0f90a42549a1763fb95228fb5fd17579812ab454bfc7d21"
"5012b337503f44e086fbf021be9db8340f8ba992fe5169c5534752e900947cf5"
changed /files/scripts~1deep_scan_workbench.py/size
88194
84709
changed /files/scripts~1filesystem_identity.py/sha256
"2b3a22761f28faa2f45f192ff14b17fd80f1f43d9d4174007c51d21f4cbf47c1"
"5298f2012b6e48ab1106837a5440ef2b3966383e2321507a47593010bb4dd01f"
changed /files/scripts~1filesystem_identity.py/size
926
893
changed /files/scripts~1finalize_scan_contract.py/sha256
"7f191c50ded50600b0a70be1e11b724f0355f166a690312226990d88e895d159"
"3fa92d8e2ccadb032cf458f0fde444785e8ee5ced0abe6346c285b07ef153e27"
changed /files/scripts~1finalize_scan_contract.py/size
121437
133770
changed /files/scripts~1finding_preview.py/sha256
"0df185e83a55cfcd9da151054de32b04e8f0bc0ceee3a2d9269d0d119fc12854"
"ebdf8e7b7e465c0937e73e945e3593d659b2f15ff1db08d0b4a2d3b106174c4d"
changed /files/scripts~1finding_preview.py/size
15968
16089
changed /files/scripts~1generate_in_scope_files.py/sha256
"e59583c111b4f3edd3f57e114bfb7f62a7dbad8424b4851bdfd23a8219336806"
"0b420d786b17e4f91451b5b686fdb4d6beee3913eebb952f8fcbf220aec6230e"
changed /files/scripts~1generate_in_scope_files.py/size
11801
11646
changed /files/scripts~1generate_rank_input.py/sha256
"0c0ead74ae98b4adfbd6a2eacb91ea492b11e60434c4259ae36213c0ddc5f3f8"
"e7c0bddbdb7e9236f7431d84b86be4c3fc202daa8d1306d6baa930110635b671"
changed /files/scripts~1generate_rank_input.py/size
47421
17899
changed /files/scripts~1launch_codex_security_mcp/sha256
"b82af11478dc85f666b6ec0919c53034dd287c29ffcb1af7e5debbb425618c5d"
"56794e736a1e8f588f9959a2e707ea3ed9bc7d4d2b796c132e91d5fbed59e600"
changed /files/scripts~1launch_codex_security_mcp/size
1228
1793
removed /files/scripts~1normalize_candidates.py
{
"sha256": "8da9b66deec6320a3e55c033a097480bedd340f0442b31077999a2e2d82e851e",
"size": 13136
}Field is absent
changed /files/scripts~1rank_preview.py/sha256
"013885da19308feeb0bcea8968097ba8eee8b063b23a6ca5af8a7269ca777afa"
"627d0b89c9d4436162ab9bcb80f7aa562605695e784dc612307c7eda6fef3a7f"
changed /files/scripts~1rank_preview.py/size
34987
34726
changed /files/scripts~1report_projection.py/sha256
"474de8e927442b1c5c7fc4adb5addd98906ae86db05da1aa20bd54ad5fca48fb"
"1b24978a3cdb3c336ac27a2b39fb35457bade13a4aad0782710fe38ac39965cf"
changed /files/scripts~1report_projection.py/size
41587
41098
changed /files/scripts~1snapshot_sqlite.py/sha256
"a501b0f4b6656f4f9b6a389835e73f0d8105051191646b64af4268f0d4a3eaee"
"9e0be851702f0962070ece6e0e2959e3955fc99126f68452d175fc22bc38423b"
changed /files/scripts~1snapshot_sqlite.py/size
852
1084
added /files/scripts~1threat_model_projection.py
Field was absent
{
"sha256": "32e4d5cd370a49cdc5658c3ded3d4d63ba7f41a44b34b67da87321048d088111",
"size": 6711
}changed /files/scripts~1windows_scan_local_files.py/sha256
"3034ec0b2e5d29fb8edbbd7237b91b1063b0245cdc546d85209129fef1ab2fbf"
"c26276695435d134c1354048c3c74c3c6b0ed203158a93b63b67ef0b6bfab843"
changed /files/scripts~1windows_scan_local_files.py/size
24480
24919
changed /files/scripts~1workbench~1storage.py/sha256
"973275d28c851b1b8408f7ac59e89a2a68057f2aecb4831e6aa2396c83a39e6d"
"5e389073520fb7668defd433228befd9e7b9c8309c1b6024bffd6213d59f1d88"
changed /files/scripts~1workbench~1storage.py/size
608
973
changed /files/scripts~1workbench_cli.py/sha256
"74752d032261afd831876d774724b150b35d6c8f0a3f70574c5c8ddad20f31b3"
"959e951fa5daf326049ee61bd0773999be92600b65a9062e56b4f62fc4e2aa55"
changed /files/scripts~1workbench_cli.py/size
21184
21260
changed /files/scripts~1workbench_constants.py/sha256
"06548d28c6d66b99f6c11f63918101c6cce39df6346fc6c5e977b9d09cd2a950"
"0ff12b11b2ae9f71c1de6cff0afc234b035379bb1193f1ca9a56ddeb7aa32cf7"
changed /files/scripts~1workbench_constants.py/size
2261
2187
changed /files/scripts~1workbench_db.py/sha256
"01d7d27013f24cf26cbca4a6e2331d7eb1bd0afc5e570c44376cd3e2e4fd5cb7"
"94bf11d7d74202ae7efbdd4f70a036393d4ba013ffc2ada44172a59be41f97f2"
changed /files/scripts~1workbench_db.py/size
149800
146186
changed /files/scripts~1workbench_finding_index.py/sha256
"410f4c2fb2bd063130e945d06aad63582b6a3e6fed9c448dcf740d85a6c3ce78"
"4618f994786fa2300e78592fa2868bc0a585368127027c1940a0e504b0254265"
changed /files/scripts~1workbench_finding_index.py/size
4191
3938
changed /files/scripts~1workbench_native_indexes.py/sha256
"0ada4190b7050fde77c3f04a8f36b1f6479da368aa1ed39c9587ce23645ba468"
"114dbf1e4050302948c4b8f24d69454f127d128e3bc35e34a5f3ec9dd3b187cd"
changed /files/scripts~1workbench_native_indexes.py/size
9984
9870
changed /files/scripts~1workbench_progress.py/sha256
"d106181710b59c3cd2d66a26d4a4556741d02d606999224a36cfb09771092a6e"
"42c7d0b8b4d253095e1062922cd9e59b10b966ac314bef092cb78cad2217b967"
changed /files/scripts~1workbench_progress.py/size
13897
13296
changed /files/scripts~1workbench_publication.py/sha256
"7813a522da6924e065a5a517efc54e80529c686773d084297f760e405b1cecd5"
"9312a2962b797ed355d6b05fb375da6325aeb5c5ead5d52f2fb7114ef3ad9635"
changed /files/scripts~1workbench_publication.py/size
20574
20346
changed /files/scripts~1workbench_remediation.py/sha256
"d0c7a4b3a283aea128802b4c1a4295580bc2ebd83c6c7108eb1ce288aaa20017"
"96585805c4956917bb473c3b60493a3c15daa2028b074b756e36358a9ac76525"
changed /files/scripts~1workbench_remediation.py/size
7815
7281
changed /files/scripts~1workbench_saved_results.py/sha256
"db445b651bd677359ef0a5c2bbb70e896eccd60becbd80e8c7a77aa3010f289c"
"436b23a4d169006437985bb352bfd792da9e465c4c7506dce31bfd5e78524c6d"
changed /files/scripts~1workbench_saved_results.py/size
72501
119202
changed /files/scripts~1workbench_scan_start.py/sha256
"8ce5f6e6d34670026f50da234c881074bd4864f41c9c574f6b2232ef968c7b1e"
"647e54ce40a7dc5b17d6abe547406f22ca0cb4eee628938b4c21629eb8e73c35"
changed /files/scripts~1workbench_scan_start.py/size
8210
7963
changed /files/scripts~1workbench_scan_usage.py/sha256
"4fb61d682ba05839ad57d33a8a2c9a9ffaa51ea7721c2cc7d629d2d687e2607a"
"21d845f8967c9206351e54f94fd2b4bbfe834434e35b642992d7a829b76cd94e"
changed /files/scripts~1workbench_scan_usage.py/size
22317
22094
changed /files/scripts~1workbench_schema.py/sha256
"6a382c792b39c0a41118a2d4325b967f3cb0dca2a2e6e84411e4e72a675372e7"
"25ef8550ded14785fa47b19f1fa358422d8883affc94641c84e68c172e800cc0"
changed /files/scripts~1workbench_schema.py/size
58474
51783
changed /files/scripts~1workbench_severity.py/sha256
"6313c3302a40504e8b1ebaabcc081bdcbbb97c9b348a7d63c381ea04042e898d"
"ec74f540595e8052e295846303f31988329e0dc68fcebdd92572673555b1c99d"
changed /files/scripts~1workbench_severity.py/size
4489
4625
changed /files/scripts~1workbench_source_excerpt.py/sha256
"0cd15d26c3a1feadf1858d1be25f9703ad09307ab1ecfb866260b8c1991cf74e"
"5681d4a5c547404f687c644f54432c10e0770b6ba4c1e6be321664579a252a24"
changed /files/scripts~1workbench_source_excerpt.py/size
2989
3210
changed /files/scripts~1workbench_target.py/sha256
"bc21dc8cb392a5639d787fde956f6b4d2769bf9c3f1cb805f164557df787563c"
"08ed149295b9347ac37f2b8523c822a35f165283422c7644dabecfa236b81b72"
changed /files/scripts~1workbench_target.py/size
28827
32451
changed /files/scripts~1workbench_target_state.py/sha256
"f48897fb7937e7474126dc88f85bcc359a1a99fb845a77f5d896dbe8b506b45a"
"9c133f67397ab4b90ce1275edc09a7e6d05900236209bc72e8f113b9f0cde287"
changed /files/scripts~1workbench_target_state.py/size
1953
1920
changed /files/scripts~1workbench_validation.py/sha256
"3cc29fc7a342dc9b55976c37c641c1e7a2abf91625fb9abfb1165a59e251ad29"
"11259b8ce1e135c5a86677964f29e6331e65e5fd80a1931c80f7fac507cccb13"
changed /files/scripts~1workbench_validation.py/size
6773
6547
changed /files/skills~1assess-patch-risk~1SKILL.md/sha256
"b7337a909fe0ea7f3b203178df60161ea42236405c0bb8697d43ec5736be744b"
"c3b62a8c1d6135945a055134f50329047bb908972fc7b320a592edbc3d38b419"
changed /files/skills~1assess-patch-risk~1SKILL.md/size
8679
9511
removed /files/skills~1assess-patch-risk~1scripts~1validate_patch_risk_assessment.py
{
"sha256": "b57a804b1e30c94ed01d5a3a36f8a31b15a6e3b9f751a41c5ade07ff35019d77",
"size": 6645
}Field is absent
changed /files/skills~1attack-path-analysis~1SKILL.md/sha256
"9f9f45603c68a1e796d8151b3373842aa7e89f0e1dc8e985f7ab1bb342547444"
"9465d2750182acbf6dd8f61e60bb47dc1ca502be5cc3f4a71da7ad39935d1eb1"
changed /files/skills~1attack-path-analysis~1SKILL.md/size
8567
8568
changed /files/skills~1deep-security-scan~1SKILL.md/sha256
"e3e758d1a6b96b42453fc631b58e04e851cec71318b18d53e2d1c344f6edc4db"
"d6aed0d9b47065a1384a96d0a45c843b0cda8ba48e3d2cbe96c65357c6e55ee3"
changed /files/skills~1deep-security-scan~1SKILL.md/size
13815
14096
changed /files/skills~1finding-discovery~1SKILL.md/sha256
"1d3a159a5ed83722ddf7369dad9f7788e283454a8ee963cef35ccd8102ea8b0e"
"14cc797e750ca4bdd8b060c4cf96d7d32f25b132aa8f80d0a0837683fe010790"
changed /files/skills~1finding-discovery~1SKILL.md/size
25450
25598
changed /files/skills~1propose-security-hardening~1references~1proposal-format.md/sha256
"6c0000722629098365afbf6809c3402121f90438600fb574fbc2c77aeb649ba4"
"72b93c32f1cbe7d25a3d347d0894e7b642942e68f2d8a3c0409bc398a41b06e7"
changed /files/skills~1propose-security-hardening~1references~1proposal-format.md/size
25476
25933
changed /files/skills~1security-diff-scan~1SKILL.md/sha256
"0a4c519ad713585876ea7eb0a8af4b59892c86746f4c69851db9ab347b7fad2f"
"f85962d46f141227d794a25253a39232bc4e1ad756509ecd3d8769b85b735136"
changed /files/skills~1security-diff-scan~1SKILL.md/size
5449
6102
changed /files/skills~1security-scan~1SKILL.md/sha256
"5b8f5d7debeca14c6b37e8e7ba737671362b8eb4b7f49e693c99c6bd04bc8fa0"
"104e2f93fc965c34e91970f517a89e330d5b29dbb243dcea5d95d83f14d135cf"
changed /files/skills~1security-scan~1SKILL.md/size
6975
9007
changed /files/skills~1security-scan~1references~1desktop-scan.md/sha256
"fc20c0a72913cf88f99879091c5b64e4161a42670e84bff29b629210006cd860"
"62a3b3baac5773c36eb291c74c0304dd97157aff5b89dd56fa8b0fed08803389"
changed /files/skills~1security-scan~1references~1desktop-scan.md/size
4428
4747
changed /files/skills~1security-scan~1references~1scan-artifacts-and-ledger.md/sha256
"443084c974eb80747a6dc4e092c7fca55f412ca5ab3f14e2d27dc1223cc690a4"
"c95780a7af36fd42716f4c909bb57e68b438769b13f8d6f02ac06ffa73c838ff"
changed /files/skills~1security-scan~1references~1scan-artifacts-and-ledger.md/size
13628
14716
changed /files/skills~1threat-model~1SKILL.md/sha256
"f45f02d9f607ec6797b730c71b15310f971403193cc56a984a1cbb1710b495ee"
"42568e50c787c4c0bfb9495ad67e72ab6c1a951bb910ecba2c67cff8961c4d56"
changed /files/skills~1threat-model~1SKILL.md/size
3149
3606
changed /files/skills~1track-findings~1SKILL.md/sha256
"8e3726e86ef0df509f1a635961f268e1bf04a4622a83e21c1d079f546cc61dcb"
"a6d2f30e6d5ca7bd9d74d83017f2f5481c5829585941208a1673b82c683d292b"
changed /files/skills~1track-findings~1SKILL.md/size
20543
11371
changed /files/skills~1track-findings~1references~1github-security-advisories.md/sha256
"f56010d265d0ce56e555dd73a92777a6d3be8f5d31bd2af7ff163423a667d10e"
"5e583aa2f77db32e4983b8b4bb857b1c7eaebfa1f43fc833b551c96acab70d4e"
changed /files/skills~1track-findings~1references~1github-security-advisories.md/size
4065
3288
changed /files/skills~1track-findings~1references~1jira.md/sha256
"05affbdefbfd8054d08f16e5fe15729ade440b5627a4561f8693bd97fb3a3ea2"
"bb1f7bb6fa638f20f1caad35b04482ae33f0cdbf2ab9c617e7805575dff457a0"
changed /files/skills~1track-findings~1references~1jira.md/size
5995
3027
changed /files/skills~1triage-finding~1SKILL.md/sha256
"70b0e771f286443ea32bbf466ad378b36f9123b4aa171ab02c6680fdbdb704f9"
"83081950bd4b29ff37cf82d137aec164532ae56b03dbaad3f4dd1d5423e758a7"
changed /files/skills~1triage-finding~1SKILL.md/size
27559
10004
changed /files/skills~1triage-finding~1agents~1openai.yaml/sha256
"1d53cfba14878c05745309eec3f682f70c5e8d49ecc033628cf832a5dd27c9cb"
"4a4094210aa6360cf503f5adf7be1e590e27898f3163da1b713632d2828e8563"
changed /files/skills~1triage-finding~1agents~1openai.yaml/size
770
400
changed /files/skills~1triage-finding~1references~1ticket-intake.md/sha256
"bd545655e0402f5f326f34191a67b6dd30c382914e9ae81b2e66ad745860e78a"
"693b415fdfcdcf8e6d5539677ff89c79bb36995ae22916d87866ce2c25113439"
changed /files/skills~1triage-finding~1references~1ticket-intake.md/size
6250
3645
changed /files/skills~1triage-finding~1references~1triage-result-contract.md/sha256
"321027c102cf36119376ef86f0701a4a7c6fc22e46d28fd240639aae156b134a"
"709c1d803c001a47c884ba10ac97f8c1434c3ce06a75033778343a662b25729b"
changed /files/skills~1triage-finding~1references~1triage-result-contract.md/size
4411
4447
Full snapshot data
{
"files": {
".app.json": {
"sha256": "a7bbe314059c7311e169474823e3978f267e560c3e961ba49fb94032da9843fe",
"size": 479
},
".codex-plugin/plugin.json": {
"sha256": "527d1a0db286884729572d400b0ad9b0ff852d4a12b36228ff5fbef180f7de9d",
"size": 1582
},
".mcp.json": {
"sha256": "7c5968060f6e848859ce02450d6e02e67029d8d39011bbda1059b19de6309ebf",
"size": 1679
},
"OWNERS": {
"sha256": "49be76d15302e4f158b3ef907e9802a955ab9c0a59a039827eab4cb6221f0e5f",
"size": 41
},
"assets/logo.png": {
"sha256": "9b9c2b09b2fa064611fb62307d321d5c2ea70cf0789f7ce34cdb0fc0d9190b3a",
"size": 99567
},
"examples/completed-scan/coverage.json": {
"sha256": "d55b9b98d48323b4659dfee6531ec83ec538f2084325412b7188bf85ad68b01b",
"size": 467
},
"examples/completed-scan/findings.json": {
"sha256": "a6dc4521d6478828224fbafc33585401a47bfeb0e56e07b5d2886e8a29937f2f",
"size": 1844
},
"examples/completed-scan/report.md": {
"sha256": "83b7c709c0b49d1500b52e61a88ea70b8423ebebec38308b1294cf094838b66f",
"size": 4116
},
"examples/completed-scan/scan-manifest.json": {
"sha256": "d245ae9fc62a676293c6821e562d1a2d7d59db12d5b8dc99b85f7c5f1462fe00",
"size": 1245
},
"integrity.json": {
"sha256": "6b342df66fb9b5ce7fc8c9de0e974b59d21340cdc06167df46d1a19bd4ea9441",
"size": 17465
},
"mcp/helpers.mjs": {
"sha256": "c272f3761e5c405619b26e4d0fd621df5ac189f2ee1bb32b975480e9835f9ca8",
"size": 1097
},
"mcp/helpers.mjs.br.part-000": {
"sha256": "6299f73c92c4f924bffc6b1bed001480603473967006b3e75941bc49a21f18d5",
"size": 63398
},
"mcp/native/COPYRIGHT-library.html": {
"sha256": "0a65bb747c49c7bb816cbc7188319bd6e4e8d08091c1190b8a3c0971c47968ed",
"size": 279302
},
"mcp/native/THIRD_PARTY_NOTICES.txt": {
"sha256": "16ac63908855f5bb7eab864923cc1855b406bd7051dfcd70d7e69684f6c69955",
"size": 308404
},
"mcp/native/darwin-arm64/unix.node": {
"sha256": "093e87d8de3d2676fc9a2a1430f571bf4fc970b2bbef4c7ded0bf8924e45f473",
"size": 354352
},
"mcp/native/darwin-x64/unix.node": {
"sha256": "57c45d212d35ce7599afc7b9b21693e18443753231bad1fb5146a4e224db68ec",
"size": 343296
},
"mcp/native/licenses/Apache-2.0.txt": {
"sha256": "074e6e32c86a4c0ef8b3ed25b721ca23aca83df277cd88106ef7177c354615ff",
"size": 10280
},
"mcp/native/licenses/BSD-2-Clause.txt": {
"sha256": "f32fb3b417a194167cfad068223fc975ba96c5960513a10f66a3c28720aec1df",
"size": 1267
},
"mcp/native/licenses/MIT.txt": {
"sha256": "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0",
"size": 1078
},
"mcp/native/licenses/Unicode-3.0.txt": {
"sha256": "f5062c9a188d81dfe66b56db4182dcf9e4b17c0d9b0d311a8e20b3a1b075c443",
"size": 1995
},
"mcp/native/linux-arm64-gnu/unix.node": {
"sha256": "6b352aaaac21f9ad9b772481429ba7bc8433a0e44ca5ffe433ad47289c3007f6",
"size": 465088
},
"mcp/native/linux-arm64-musl/unix.node": {
"sha256": "3a55ec4f42202c58a29654701c31a8deab386969d14e2ab85f23e81345b4fedb",
"size": 332456
},
"mcp/native/linux-x64-gnu/unix.node": {
"sha256": "ed2061078400010852a9f8d3ecdf7bc0a008d13adc9e9d5c6954f5cb686a2a2a",
"size": 365352
},
"mcp/native/linux-x64-musl/unix.node": {
"sha256": "c2e4c07ac6dde175464c6315937652465b200e5f28590898663523beaf4e9ab6",
"size": 369304
},
"mcp/native/win32-arm64/windows.node": {
"sha256": "19f78e88fd8934038201c26b044123bc05db74f5d7065df10a32583ccbcf7a55",
"size": 419840
},
"mcp/native/win32-x64/windows.node": {
"sha256": "5aed21edb5fe06efac49d9bd3899d6242f60b1bfac2b710b967a76c924d98298",
"size": 453120
},
"mcp/server.mjs": {
"sha256": "c58624aa4c4bd3efbb67601c9c2e98759f167f608320a6cd342814b2f2e6a636",
"size": 1096
},
"mcp/server.mjs.br.part-000": {
"sha256": "7ef9c7387b405b51e2aec793a86696bf793506a8f34155199035aa515b3a415a",
"size": 140000
},
"mcp/server.mjs.br.part-001": {
"sha256": "94d6412009e284ecbc17bd14d07e0116d8e74e572a62198912ac5a03f95a50c2",
"size": 126028
},
"preflight/capability-profiles.toml": {
"sha256": "543a0f6a0e81cbbac1fe43e2e3d44d6163a6792384ab9c768ac519295d9ec8a1",
"size": 2350
},
"references/artifact-storage.md": {
"sha256": "ccce9c76f5d044fbce333d6129fbb55eb63f512c15c2ebeeca405e296f38325b",
"size": 7632
},
"references/config-preflight.md": {
"sha256": "5894f9fd7efcae7034404a2c062ee3d7417520753be16ddcdb3acf5b884f048d",
"size": 19162
},
"references/core-scan.md": {
"sha256": "9c0eca53147383a89cd345eac7791ea75b954f7f083448ef25019d8f9ba30afe",
"size": 22978
},
"references/desktop-config-preflight.md": {
"sha256": "d03e7c604a0459509a4a3238289c3ad0725811e8b11fb7d0044ce8bcee6a7a26",
"size": 1587
},
"references/final-report.md": {
"sha256": "9c34375584c8ae76959b0eae038b86b2b8ed7874517c249fcae8715314c76298",
"size": 29931
},
"references/finding-detail-fields.md": {
"sha256": "9e4dcc5633ca1f31291688606e4ec0d56067ed485e748ac9e7d433984f9ab451",
"size": 11920
},
"references/sarif-adapter.md": {
"sha256": "32725cc851d998c9b33f0232c435d720383c86bc13526b04d356b85d08101cc5",
"size": 2341
},
"references/scan-artifacts.md": {
"sha256": "eecdccac30b8ad65bb702dc9ac8d9718dcb0b95532a94a61195268d22bdf504b",
"size": 11243
},
"references/scan-contract.md": {
"sha256": "b362636c29d1e8b18053481abe2f97d7bb099a97040f5b63aa2702a41c1d62c8",
"size": 14537
},
"references/scan-prologue.md": {
"sha256": "1d23ab6db651134b54307cdea873ffa711a83458bda563f1e126f35e83b278e9",
"size": 4812
},
"references/security-guidance.md": {
"sha256": "a0f49ad3bd09fa6180a100d81326b75bf87a2d49893ca9e37b64ab151c759a80",
"size": 1801
},
"references/static-finding-assessment.md": {
"sha256": "78de933caef1c8d971dab83fbb71877602a1197a52eaa7a59d6ff7bc399d0bc7",
"size": 3949
},
"references/threat-model.md": {
"sha256": "f3dd2d05d7f0c61b13d29a78e810b367772fe22d5ac58adbbc00080a9495706e",
"size": 15203
},
"schemas/coverage.schema.json": {
"sha256": "6ac14e659e884e9582d8bd9ba480f6364ca17309e1a3dee9469ab025a3380b11",
"size": 5198
},
"schemas/definitions/artifact-common.schema.json": {
"sha256": "8187236867a2397515571d937deac92dcbf23d3db5e330d32b1f24aced4b9abc",
"size": 1065
},
"schemas/definitions/discovery-candidate.schema.json": {
"sha256": "b0cf54fc1ae1947db0f6a6f73e17d1124d528226de763fb89489d05fd612331b",
"size": 2951
},
"schemas/findings.schema.json": {
"sha256": "a480337cc0fa4c48c44fc7be17c6c4348767815570775cda80f2aaf797b8e56c",
"size": 19167
},
"schemas/patch-risk-assessment.schema.json": {
"sha256": "536492481c5b67910ce8d3754d61c7d5a44c5cb78fe3f081e8f5d1620d6ae9c9",
"size": 6854
},
"schemas/scan-manifest.schema.json": {
"sha256": "c621d4136ac81741ac8da1960c7f5e92ade22c0b5490e378ea4339b09e45b6f3",
"size": 9513
},
"schemas/tools/candidate-attack-paths.schema.json": {
"sha256": "f6fcb643b4b975466c4717356a0768b0572812cd832578d8714557f5e3d16935",
"size": 6055
},
"schemas/tools/candidate-validations.schema.json": {
"sha256": "c871eb3462b7873c1774e6aade62acdd570636e43c0ac3c7b41b7041ca5627e9",
"size": 4916
},
"schemas/tools/deep-reducer.schema.json": {
"sha256": "5f0c25f826445251415986097c41127018d19b6c690cd99720fd9365893b0028",
"size": 2221
},
"schemas/tools/discovery-candidates.schema.json": {
"sha256": "dcd2031d64015c69b415c66e57d6e90cbdef91152a445a6f409c14d80153afa6",
"size": 2473
},
"schemas/tools/review-items.schema.json": {
"sha256": "acc4f4909446a62811728dd1d6581456053da8d26f774b7af208a1856f86ac5b",
"size": 2250
},
"schemas/tools/scan-draft.schema.json": {
"sha256": "151f9dffc83e1c5b4f0a7fba20b90fb3196fdaa9719180eb0009ba2b50f75d48",
"size": 23475
},
"schemas/tools/worker-threat-model.schema.json": {
"sha256": "a00f875918794661316a033bd2d03426d61ab1e41d4a36c548c07525dcfde478",
"size": 435
},
"scripts/config_preflight.py": {
"sha256": "f4d4103650977d078b71f49da6b1e287b6db2f2b7b5fb08e943722a6d869354e",
"size": 35176
},
"scripts/deep_scan_config.py": {
"sha256": "44229bc7b2e7f4c7654e2e9ed91650a28630eeb00fcc38358f19d69739771f8c",
"size": 5042
},
"scripts/deep_scan_defaults.json": {
"sha256": "89bdb96ff721f0f1d509f3c5c83f5fc89022f14406d0a84df781b7e0e42cde66",
"size": 143
},
"scripts/deep_scan_workbench.py": {
"sha256": "5012b337503f44e086fbf021be9db8340f8ba992fe5169c5534752e900947cf5",
"size": 84709
},
"scripts/filesystem_identity.py": {
"sha256": "5298f2012b6e48ab1106837a5440ef2b3966383e2321507a47593010bb4dd01f",
"size": 893
},
"scripts/finalize_scan_contract.py": {
"sha256": "3fa92d8e2ccadb032cf458f0fde444785e8ee5ced0abe6346c285b07ef153e27",
"size": 133770
},
"scripts/finding_preview.py": {
"sha256": "ebdf8e7b7e465c0937e73e945e3593d659b2f15ff1db08d0b4a2d3b106174c4d",
"size": 16089
},
"scripts/generate_in_scope_files.py": {
"sha256": "0b420d786b17e4f91451b5b686fdb4d6beee3913eebb952f8fcbf220aec6230e",
"size": 11646
},
"scripts/generate_rank_input.py": {
"sha256": "e7c0bddbdb7e9236f7431d84b86be4c3fc202daa8d1306d6baa930110635b671",
"size": 17899
},
"scripts/launch_codex_security_mcp": {
"sha256": "56794e736a1e8f588f9959a2e707ea3ed9bc7d4d2b796c132e91d5fbed59e600",
"size": 1793
},
"scripts/launch_codex_security_mcp.cmd": {
"sha256": "2539c0914bb130d76629e8176ac93762cd0051677ac2c525e7ba524b0b828f80",
"size": 2582
},
"scripts/rank_preview.py": {
"sha256": "627d0b89c9d4436162ab9bcb80f7aa562605695e784dc612307c7eda6fef3a7f",
"size": 34726
},
"scripts/report_projection.py": {
"sha256": "1b24978a3cdb3c336ac27a2b39fb35457bade13a4aad0782710fe38ac39965cf",
"size": 41098
},
"scripts/reserved_artifact_paths.json": {
"sha256": "58ba1cf42437ac3af65c85a66eeff46f22572e45081fa2620f4b241173efcc50",
"size": 186
},
"scripts/snapshot_sqlite.py": {
"sha256": "9e0be851702f0962070ece6e0e2959e3955fc99126f68452d175fc22bc38423b",
"size": 1084
},
"scripts/threat_model_projection.py": {
"sha256": "32e4d5cd370a49cdc5658c3ded3d4d63ba7f41a44b34b67da87321048d088111",
"size": 6711
},
"scripts/validate_scan_contract.py": {
"sha256": "38a5eb0126ea06b22eb82c5829e4ffcbebd2c1e55f99634454622fedd7b9e1a0",
"size": 3803
},
"scripts/validate_tracking_source.py": {
"sha256": "802fb3062456128c7adceb6048183811120255b222018e123e10c5ba0caa4bdc",
"size": 2813
},
"scripts/windows_scan_local_files.py": {
"sha256": "c26276695435d134c1354048c3c74c3c6b0ed203158a93b63b67ef0b6bfab843",
"size": 24919
},
"scripts/workbench/__init__.py": {
"sha256": "3635fffd60f51d5a5d889e20c91df803c667f8b1b29cbcf68c96cb2e0a9c33ed",
"size": 58
},
"scripts/workbench/handoff.py": {
"sha256": "83606054cc47064514a612d8ce16353cce669fe07b61852d73bc0c94b75c8ee5",
"size": 8914
},
"scripts/workbench/storage.py": {
"sha256": "5e389073520fb7668defd433228befd9e7b9c8309c1b6024bffd6213d59f1d88",
"size": 973
},
"scripts/workbench_cli.py": {
"sha256": "959e951fa5daf326049ee61bd0773999be92600b65a9062e56b4f62fc4e2aa55",
"size": 21260
},
"scripts/workbench_constants.py": {
"sha256": "0ff12b11b2ae9f71c1de6cff0afc234b035379bb1193f1ca9a56ddeb7aa32cf7",
"size": 2187
},
"scripts/workbench_dashboard.py": {
"sha256": "5770b77ff551cde46c22fae9239ef8ce28254320da77876e0b936dc35b829967",
"size": 5990
},
"scripts/workbench_db.py": {
"sha256": "94bf11d7d74202ae7efbdd4f70a036393d4ba013ffc2ada44172a59be41f97f2",
"size": 146186
},
"scripts/workbench_feedback.py": {
"sha256": "675bc5897444a39950297f74353fa6fd296f73d3a5ac87d294647ae8425151dd",
"size": 4120
},
"scripts/workbench_finding_index.py": {
"sha256": "4618f994786fa2300e78592fa2868bc0a585368127027c1940a0e504b0254265",
"size": 3938
},
"scripts/workbench_finding_workflows.py": {
"sha256": "de44bfc28c0a4d2021643a50d5a42408f4853bb4a21cf9d1f4ffb43a695b06ce",
"size": 8754
},
"scripts/workbench_findings.py": {
"sha256": "b78e2ca68f29cf002b6215985225e1b3996c8edd08601569945405d42c33b301",
"size": 8881
},
"scripts/workbench_native_indexes.py": {
"sha256": "114dbf1e4050302948c4b8f24d69454f127d128e3bc35e34a5f3ec9dd3b187cd",
"size": 9870
},
"scripts/workbench_progress.py": {
"sha256": "42c7d0b8b4d253095e1062922cd9e59b10b966ac314bef092cb78cad2217b967",
"size": 13296
},
"scripts/workbench_publication.py": {
"sha256": "9312a2962b797ed355d6b05fb375da6325aeb5c5ead5d52f2fb7114ef3ad9635",
"size": 20346
},
"scripts/workbench_remediation.py": {
"sha256": "96585805c4956917bb473c3b60493a3c15daa2028b074b756e36358a9ac76525",
"size": 7281
},
"scripts/workbench_saved_results.py": {
"sha256": "436b23a4d169006437985bb352bfd792da9e465c4c7506dce31bfd5e78524c6d",
"size": 119202
},
"scripts/workbench_scan_history.py": {
"sha256": "920a4c6448c4ee87500e4488fdfa5a294dfa0696f94f55f2e2cbe544e5acf60f",
"size": 48177
},
"scripts/workbench_scan_start.py": {
"sha256": "647e54ce40a7dc5b17d6abe547406f22ca0cb4eee628938b4c21629eb8e73c35",
"size": 7963
},
"scripts/workbench_scan_usage.py": {
"sha256": "21d845f8967c9206351e54f94fd2b4bbfe834434e35b642992d7a829b76cd94e",
"size": 22094
},
"scripts/workbench_schema.py": {
"sha256": "25ef8550ded14785fa47b19f1fa358422d8883affc94641c84e68c172e800cc0",
"size": 51783
},
"scripts/workbench_severity.py": {
"sha256": "ec74f540595e8052e295846303f31988329e0dc68fcebdd92572673555b1c99d",
"size": 4625
},
"scripts/workbench_source_excerpt.py": {
"sha256": "5681d4a5c547404f687c644f54432c10e0770b6ba4c1e6be321664579a252a24",
"size": 3210
},
"scripts/workbench_target.py": {
"sha256": "08ed149295b9347ac37f2b8523c822a35f165283422c7644dabecfa236b81b72",
"size": 32451
},
"scripts/workbench_target_state.py": {
"sha256": "9c133f67397ab4b90ce1275edc09a7e6d05900236209bc72e8f113b9f0cde287",
"size": 1920
},
"scripts/workbench_validation.py": {
"sha256": "11259b8ce1e135c5a86677964f29e6331e65e5fd80a1931c80f7fac507cccb13",
"size": 6547
},
"skills/assess-patch-risk/SKILL.md": {
"sha256": "c3b62a8c1d6135945a055134f50329047bb908972fc7b320a592edbc3d38b419",
"size": 9511
},
"skills/assess-patch-risk/agents/openai.yaml": {
"sha256": "fec61f321e18022579cac25ffb33286f48cc316172a0b8d502f2bf4faecba025",
"size": 274
},
"skills/assess-patch-risk/references/risk-rubric.md": {
"sha256": "078a71175191580791685808980cb1815dc97265cfee8c0ddad1f4d3b8203b4a",
"size": 5049
},
"skills/attack-path-analysis/SKILL.md": {
"sha256": "9465d2750182acbf6dd8f61e60bb47dc1ca502be5cc3f4a71da7ad39935d1eb1",
"size": 8568
},
"skills/attack-path-analysis/agents/openai.yaml": {
"sha256": "d9355cbcf0bc81098c9e837d9d4e8644e598410d052a862ab11cf91ef9ccb633",
"size": 261
},
"skills/attack-path-analysis/references/attack-path-facts.md": {
"sha256": "8c8f1abb46ddf045d76908df78e3a273da8d61aaa0f37fc992534a3ddfe42430",
"size": 2882
},
"skills/attack-path-analysis/references/severity-policy.md": {
"sha256": "44719bb96a1065df7683a4f7968145efd3b3f365ec95c22a4bed5b20b5d8a0a1",
"size": 15288
},
"skills/deep-security-scan/SKILL.md": {
"sha256": "d6aed0d9b47065a1384a96d0a45c843b0cda8ba48e3d2cbe96c65357c6e55ee3",
"size": 14096
},
"skills/deep-security-scan/agents/openai.yaml": {
"sha256": "3f8d781bf372b0d053a017eabaebf947d5d48d9be2b82cb67da211128924c3f1",
"size": 192
},
"skills/define-security-policy/SKILL.md": {
"sha256": "2817e1a03980a7ebea478c8d9e6c622e3c6205123e767a1e32f0204c06729cc7",
"size": 6256
},
"skills/define-security-policy/agents/openai.yaml": {
"sha256": "8c40b0f8bd1eb7feabdcea69f12c2a7d18e492e96893c0702507ffd808b8e2b4",
"size": 210
},
"skills/finding-discovery/SKILL.md": {
"sha256": "14cc797e750ca4bdd8b060c4cf96d7d32f25b132aa8f80d0a0837683fe010790",
"size": 25598
},
"skills/finding-discovery/agents/openai.yaml": {
"sha256": "c044dd83a7624b90ed19f11adc899202685ce64e32502f1d46225f69f9a2c962",
"size": 220
},
"skills/fix-finding/SKILL.md": {
"sha256": "ba7311f43b2c0742da0d5c6bdb2f0b26601c09cc59747cf8c62a09f8d0bb86d2",
"size": 9477
},
"skills/fix-finding/agents/openai.yaml": {
"sha256": "5dac397bf3a17e45283691b656179eb56d19e34881af380f098d7c455bce52d7",
"size": 225
},
"skills/propose-security-hardening/SKILL.md": {
"sha256": "ad07d88da7c2bc9e551849cdde686b0daad380ed0aac798e6c06444681d75959",
"size": 22791
},
"skills/propose-security-hardening/agents/openai.yaml": {
"sha256": "1bcab19d48eb4b768c0dc435bd2e738f1471c2345cce5865bb31bdca5be0b134",
"size": 293
},
"skills/propose-security-hardening/references/proposal-format.md": {
"sha256": "72b93c32f1cbe7d25a3d347d0894e7b642942e68f2d8a3c0409bc398a41b06e7",
"size": 25933
},
"skills/security-diff-scan/SKILL.md": {
"sha256": "f85962d46f141227d794a25253a39232bc4e1ad756509ecd3d8769b85b735136",
"size": 6102
},
"skills/security-diff-scan/agents/openai.yaml": {
"sha256": "f3d31e09befbb8532b53fdccb63a34c0b894a7fb96215b40e84647cd9f8e1b50",
"size": 284
},
"skills/security-scan/SKILL.md": {
"sha256": "104e2f93fc965c34e91970f517a89e330d5b29dbb243dcea5d95d83f14d135cf",
"size": 9007
},
"skills/security-scan/agents/openai.yaml": {
"sha256": "5952ae0b0f7d378b7e6ab3fe793335a0e26e5d15bd2d7fdd14ecdd795607ceb6",
"size": 271
},
"skills/security-scan/references/desktop-scan.md": {
"sha256": "62a3b3baac5773c36eb291c74c0304dd97157aff5b89dd56fa8b0fed08803389",
"size": 4747
},
"skills/security-scan/references/scan-artifacts-and-ledger.md": {
"sha256": "c95780a7af36fd42716f4c909bb57e68b438769b13f8d6f02ac06ffa73c838ff",
"size": 14716
},
"skills/threat-model/SKILL.md": {
"sha256": "42568e50c787c4c0bfb9495ad67e72ab6c1a951bb910ecba2c67cff8961c4d56",
"size": 3606
},
"skills/threat-model/agents/openai.yaml": {
"sha256": "a3533478c2548248ef07c9afbbe0a7451f1fa1882883b4def0906d8113b7dd6c",
"size": 171
},
"skills/track-findings/SKILL.md": {
"sha256": "a6d2f30e6d5ca7bd9d74d83017f2f5481c5829585941208a1673b82c683d292b",
"size": 11371
},
"skills/track-findings/agents/openai.yaml": {
"sha256": "39e73c0003afc8bd9d3cf53c027d8274e724dc41f46e3cd6863ad34bb4de4200",
"size": 279
},
"skills/track-findings/references/github-security-advisories.md": {
"sha256": "5e583aa2f77db32e4983b8b4bb857b1c7eaebfa1f43fc833b551c96acab70d4e",
"size": 3288
},
"skills/track-findings/references/jira.md": {
"sha256": "bb1f7bb6fa638f20f1caad35b04482ae33f0cdbf2ab9c617e7805575dff457a0",
"size": 3027
},
"skills/triage-finding/SKILL.md": {
"sha256": "83081950bd4b29ff37cf82d137aec164532ae56b03dbaad3f4dd1d5423e758a7",
"size": 10004
},
"skills/triage-finding/agents/openai.yaml": {
"sha256": "4a4094210aa6360cf503f5adf7be1e590e27898f3163da1b713632d2828e8563",
"size": 400
},
"skills/triage-finding/references/github-rest-intake.md": {
"sha256": "f026bef041ca92f3d9b47a131794aa54e98cf8d7716fae3f71ab53e8ab09c79f",
"size": 9314
},
"skills/triage-finding/references/ticket-intake.md": {
"sha256": "693b415fdfcdcf8e6d5539677ff89c79bb36995ae22916d87866ce2c25113439",
"size": 3645
},
"skills/triage-finding/references/triage-result-contract.md": {
"sha256": "709c1d803c001a47c884ba10ac97f8c1434c3ce06a75033778343a662b25729b",
"size": 4447
},
"skills/validation/SKILL.md": {
"sha256": "3c4660f79b12a92728e6bb25027064c3a911d0960d39aeafc8fda9e42ce517a5",
"size": 12845
},
"skills/validation/agents/openai.yaml": {
"sha256": "7516bcd4748b55566694284d7d953a7db0c94d5a2123895fabb36d7d9d7d74e4",
"size": 183
},
"skills/validation/references/validation-guidance.md": {
"sha256": "42f7a9d85b78d0deb5e6eda82b8c59ed101c40d36065833b9cf39fe50699e255",
"size": 26823
},
"skills/verify-fix/SKILL.md": {
"sha256": "3d4530484c7e4c18b6afe7c6527247e96517dce1ebd1bda9d349cd367ecb0ae3",
"size": 3025
},
"skills/verify-fix/agents/openai.yaml": {
"sha256": "ac35fc4704cde35b6bf103e16a16d0a44f2b9c2671e1634292cf285905cf40f3",
"size": 235
},
"skills/vulnerability-writeup/SKILL.md": {
"sha256": "d08582d9963ce3bfbc3fe81b6e85a01e6fe306713d7951bfd8dd2fbc3df75de4",
"size": 27640
},
"skills/vulnerability-writeup/agents/openai.yaml": {
"sha256": "7987566daac59e3dd6c5211fe020f71fc4c862843c231d10c28a45697d680f9e",
"size": 290
},
"skills/vulnerability-writeup/references/report-format.md": {
"sha256": "60568466f7057562d7359dc3a396632c760c147582603896d7e569a77487d0b1",
"size": 14682
}
}
}SHA-256 of public snapshot: 8e8a2a0d24309bd11d1c54794bd1027470e5f7bc759784322ed68d7643d69ec8