P3.31: durable pending human-triage surface + CLI approve path (#63) - #76
Merged
Conversation
Adds failing tests proving the gap: a single-iteration run's own events never reach the exported action log (no post-loop export), a report left pending by one DocumentationAgent instance cannot be approved by a fresh one pointed at the same reports_dir, and no durable pending-triage count exists. Bundles the small additive observability_snapshot.schema.json field (pending_human_triage_count, optional, v1-additive per contracts/README.md) and its already-passing tests -- the two remaining red tests (documentation-agent persistence, campaign post-loop export) are fixed in the next commit. 5 failing: tests/redteam/test_campaign.py::test_post_loop_action_log_export_includes_last_iterations_own_events tests/redteam/test_documentation_agent.py::test_pending_report_persisted_with_suffix_until_approved tests/redteam/test_documentation_agent.py::test_pending_report_persisted_by_one_agent_is_approvable_by_a_fresh_instance tests/redteam/test_documentation_agent.py::test_stale_pending_file_dropped_once_filed_exists tests/redteam/test_documentation_agent.py::test_corrupt_persisted_report_raises_loudly_not_silently_ignored Refs #63
… export (issue #63) DocumentationAgent now persists pending reports too (<report_id>.pending-human-approval.json, same suffix convention tools/build_vuln_reports.py already used for VULN-0004) and loads persisted filed/pending reports back from reports_dir on construction -- so approve() is reachable from a fresh process/instance without any per-report reconstruction script. A filed report wins over a stale pending leftover of the same exploit_id on load (self-healing after a crash between persisting the filed file and unlinking the pending one). A reports_dir this module can't parse raises loudly instead of silently losing a pending report. run_campaign now exports the action log unconditionally after the loop, not only at the top of each iteration via emit_snapshot -- a single-iteration run's own events (directive_issued through exploit_recorded/vuln_report_*) previously never reached the exported jsonl at all. Updates docs/ATO_EVIDENCE_PACKET.md and docs/DEMO_SCRIPT.md test-count claims (346->352 with sibling, 240->246 in CI) per tests/test_doc_test_counts.py, and tests/tools/test_approve_vuln_0004_gate.py's now-stale "pending reports are never persisted" assertion. 352 passed (with sibling checkout). Refs #63
tools/run_campaign.py's main() took no arguments and had no --approve/--list-pending mode at all -- there was no CLI path to approve a durably-pending report. 6 failing tests calling main(argv) directly (no live model/target, throwaway tmp_path reports_dir/db). 6 failing: tests/tools/test_run_campaign_cli.py::test_approve_requires_reports_dir tests/tools/test_run_campaign_cli.py::test_list_pending_requires_reports_dir tests/tools/test_run_campaign_cli.py::test_cli_lists_and_approves_a_report_left_pending_by_a_separate_process tests/tools/test_run_campaign_cli.py::test_approve_unknown_exploit_id_fails_without_writing tests/tools/test_run_campaign_cli.py::test_approve_refuses_when_pending_report_drifts_from_its_source_exploit_record tests/tools/test_run_campaign_cli.py::test_never_auto_approves_no_default_exploit_id Refs #63
…s (issue #63) tools/run_campaign.py gains: --list-pending --reports-dir PATH --approve EXPLOIT_ID --reports-dir PATH [--db-path PATH] [--approved-by NAME] Neither touches a live model or target -- both construct a DocumentationAgent(reports_dir=...), which now loads persisted pending reports back from disk (previous commit), so a report left pending by a prior `run` invocation is approvable with no bespoke per-report script. When --db-path names a persisted exploit DB, --approve re-derives the report from the original exploit record via build_vuln_report and refuses (exit 1, nothing approved) on any field-value drift from the persisted pending artifact -- the same verify-then-approve discipline tools/approve_vuln_0004.py established, generalized to any exploit_id. Default `run` behaviour is UNCHANGED: ExploitDB(":memory:") and DocumentationAgent(reports_dir=None) stay the default (a quick demo/smoke run should still leave nothing behind) -- --reports-dir/--db-path are opt-in. A stderr NOTE fires when reports_dir is unset (pending reports from this run won't survive) or when reports_dir is set without db_path (exploit IDs restart at EXP-0001 and may collide with prior durable reports). Updates docs/ATO_EVIDENCE_PACKET.md and docs/DEMO_SCRIPT.md test-count claims (352->358 with sibling, 246->252 in CI) per tests/test_doc_test_counts.py. 358 passed (with sibling checkout). Refs #63
…fix=) _persist and _persist_pending were identical apart from the filename suffix; one method with a suffix keyword covers both call sites. No behavior change -- 358 passed before and after.
…crashes on an auto-loaded pending report Deep-review (BLOCKER, CONFIRMED) found this diff regressed tools/approve_vuln_0004.py's own designed use case: DocumentationAgent now auto-loads persisted pending reports at construction time, so main()'s DocumentationAgent(reports_dir=_REPORTS_DIR) already has the pending EXP-0004 report loaded by the time _file_pending() re-drives file_report() on it -- colliding with the one-exploit-one-report duplicate-rejection guard and crashing with DocumentationAgentError instead of approving. Reproduced directly (see commit history): a fresh DocumentationAgent pointed at a reports_dir holding a genuinely-pending report raised DocumentationAgentError from _file_pending. Fixed by using the already-loaded pending report directly (documentation.get_pending()) instead of re-filing when it's present; _file_pending stays as a fallback for the (no longer reachable, but harmless) case where it isn't. Also fixed a second-order double-unlink: DocumentationAgent.approve() now removes the persisted pending file itself, so main()'s own _PENDING_PATH.unlink() would FileNotFoundError right after -- made missing_ok=True. Also noted (PRE-EXISTING, not fixed here -- out of scope): the success print's _FILED_PATH.relative_to(_REPO_ROOT) crashes if reports_dir isn't under the repo root, the same class of bug issue #64 already fixed in the sibling tools/build_vuln_report_p3_54.py via _display_path. Flagged in the PR description for a separate pass. New regression test (tests/tools/test_approve_vuln_0004_rerun.py) drives the real main() end-to-end against a scratch reports_dir and asserts it approves cleanly instead of raising, plus a second idempotent run. Updates docs/ATO_EVIDENCE_PACKET.md and docs/DEMO_SCRIPT.md test-count claims (358->359 with sibling, 252->253 in CI). 359 passed (with sibling checkout). Refs #63
…ainst itself, not the exploit record The already_loaded branch set pre_approval from documentation.get_pending() -- read straight back off _PENDING_PATH by DocumentationAgent's own auto-load -- and compared it against pending_on_disk, the same file. The comparison always passed regardless of tampering; record = _build_exploit_record() was built and never used. Proof: file a legitimate pending VULN-0004 in a scratch dir, hand-edit severity to "low" and clinical_impact to "DOCTORED: ...", run main(). Before: rc=0, doctored artifact filed as owner-approved. After: rc=1, nothing written. Fix: reconstructed_body now comes from build_vuln_report(record, ...) -- independent re-derivation from the trusted exploit record -- and that is what gets compared against the on-disk artifact. already_loaded/pre_approval is used only to satisfy approve()'s in-memory precondition, never as the comparison target. Red-first: tests/tools/test_approve_vuln_0004_tamper.py (quoted failing output in this session: "assert 0 == 1" before the fix).
…k, no --approved-by default, missing --db-path refused Four proven attacks against tools/run_campaign.py --approve: 1. With no --db-path, a hand-written VULN-0099.pending-human-approval.json approved cleanly with NO provenance check at all. 2. --db-path pointing at a missing file: ExploitDB(path) creates an empty sqlite, silently downgrading to "warning: skipping the cross-check" and approving as-is (rc=0) -- the one safety flag failed open on the most likely operator error (a typo). 3. --approved-by defaulted to "owner", so no explicit human identity was ever required. 4. The report body was never printed before being stamped approved. Fix: - --db-path + --approved-by are now both required for --approve, enforced at argparse time; --unverified-i-vouch-without-db-check is the explicit, loud escape hatch for a genuinely DB-less report (prints a WARNING). - A --db-path that doesn't already exist, or has no record for the exploit_id, is now a hard refusal (exit 1) -- never silently created, never downgraded to a skipped check. - The pending report body is printed before documentation.approve() is called. - Also fixes FIX 5's related bug: the rebuild now derives force_human_gate from the STORED (trusted) exploit record's category via FORCE_HUMAN_GATE_CATEGORIES, never from pending["requires_human_gate"] (the field under verification), and carries fix_validation_status through from the pending report so a legitimately-updated report doesn't spuriously fail the cross-check. Red-first: 6 new tests in tests/tools/test_run_campaign_cli.py reproducing each attack (quoted failing output in this session before the fix: attack 1 rc=0 with a hand-written report; attack 2 rc=0 + "warning: no exploit record ... skipping the field-for-field cross-check" against a typo'd --db-path that ExploitDB silently created).
…paign; post-loop export in try/finally; --reports-dir without --db-path refuses to start Reproduced: a second run_campaign() invocation against the same durable --reports-dir with in-memory (default) exploit numbering restarts at EXP-0001, collides with the pending VULN-0001 report run 1 left on disk, and documentation.file_report() raises DocumentationAgentError uncaught -- killing the campaign mid-loop. Because the post-loop action_log export sat after the loop with no try/finally, the crashed run's own action log (including everything logged before the crash) was never exported at all. Fix: - documentation.file_report() is now wrapped in try/except DocumentationAgentError like every sibling component call in the loop (orchestrator/red_team/target_client/judge) -- records a vuln_report_filing_failed signal and continues; the confirmed exploit stays safely recorded in db (db.add_record already ran unconditionally before this call). - The entire iteration loop is now wrapped in try/finally so action_log.export_jsonl(action_log_ref) always runs, even if some other exception the loop doesn't explicitly catch escapes an iteration. - tools/run_campaign.py: `run` mode with --reports-dir but no --db-path now refuses to start (parser.error) instead of emitting a stderr NOTE and continuing into the same collision. Red-first: tests/redteam/test_campaign.py (test_duplicate_report_filing_does_not_crash_the_campaign, test_action_log_exports_even_when_an_iteration_raises_uncaught) and tests/tools/test_run_campaign_cli.py (test_run_mode_reports_dir_without_db_path_refuses_to_start) -- quoted failing output in this session: uncaught DocumentationAgentError from redteam/campaign.py:436, and "assert False" on action_log_ref.exists() before the fix.
…oved evidence via a filename/report_id mismatch _load_persisted() keyed loaded reports by exploit_id taken from FILE CONTENT, never checking the filename or enforcing report_id uniqueness. Reproduced: a file named weird-name.pending-human-approval.json carrying report_id: VULN-0001, exploit_id: EXP-0002 caused --approve EXP-0002 to overwrite the already-filed, already-approved VULN-0001.json, and the stale source file was never removed (_remove_pending_file unlinked a path constructed from report_id, not the file's actual source path -- which never existed under that constructed name). Fix: - _load_persisted() now rejects (DocumentationAgentError, fail loud like every other load-time defect this method already catches) any persisted file whose name does not match "<its own claimed report_id><suffix>". - report_id uniqueness is enforced across all loaded reports -- two different exploit_ids claiming the same report_id is rejected even when both files are individually correctly named. - Each pending report's real source path is now tracked in _pending_paths (populated on load AND on file_report's own persist), and _remove_pending_file unlinks that tracked path -- not a path reconstructed from report_id -- so approval always removes the file that was actually read. Red-first: tests/redteam/test_documentation_agent.py (test_load_rejects_a_pending_file_whose_name_does_not_match_its_own_report_id, test_load_rejects_duplicate_report_id_across_different_exploit_ids, test_approve_removes_the_actual_source_path_not_a_report_id_guess) -- quoted failing output in this session: "DID NOT RAISE DocumentationAgentError" and "AttributeError: 'DocumentationAgent' object has no attribute '_pending_paths'" before the fix.
…on, doc-count refresh - tools/run_campaign.py: --list-pending and --approve now catch DocumentationAgentError from DocumentationAgent(reports_dir=...) at the CLI boundary (clean stderr message + rc 1) instead of letting a raw traceback escape when reports_dir contains an unrelated/malformed JSON file. Red-first: tests/tools/test_run_campaign_cli.py (test_list_pending_on_a_directory_with_unrelated_json_fails_cleanly_not_a_traceback, test_approve_on_a_directory_with_unrelated_json_fails_cleanly_not_a_traceback). - contracts/README.md + observability_snapshot.schema.json: the changelog claimed "pre-#63 producers and consumers stay valid" for the new pending_human_triage_count field -- false for a consumer validating against its own pinned pre-#63 copy of the schema (additionalProperties: false, unchanged): it will reject any snapshot now carrying the field. Reworded to state the one-directional truth (producers stay valid; consumers must update their own schema copy first). - pending_human_triage_count is two different numbers under one name: the observability_snapshot field counts only the vuln_reports passed to emit_snapshot (this run's own accumulator), while --list-pending's identically-named printed key scans an entire --reports-dir on disk. Documented in the schema field description, redteam/observability/findings.py's docstring, AND docs/ARCHITECTURE.md's Observability Layer section (not just a Python docstring, per the brief). - docs/ATO_EVIDENCE_PACKET.md: reverted two stray Mermaid colour mutations that were collateral from a scripted replacement (#e05253 -> #e05252, #3b3590 -> #3b3520). - Documented the new CLI approval path (issue #63/#66) in docs/DEMO_SCRIPT.md as a new Beat 5, plus a "What this proves" bullet -- a new operator-facing approval path shipping undocumented at v3.0.0 is not acceptable. - Refreshed stale test-count claims in docs/ATO_EVIDENCE_PACKET.md and docs/DEMO_SCRIPT.md (359/253 -> 375/269) to match the live suite after this PR's new tests -- tests/test_doc_test_counts.py now passes again. (tools/run_campaign.py's --db-path/rebuild fixes for the untrusted requires_human_gate field and missing fix_validation_status were already fixed and tested as part of the FIX 2 commit.)
…xcept; drop dead pre_approval assignment tools/run_campaign.py: _cmd_list_pending and _cmd_approve had identical try/except DocumentationAgentError blocks around DocumentationAgent construction -- folded into a shared _load_documentation(args) helper. tools/approve_vuln_0004.py: pyflakes flagged pre_approval as assigned but never used after the FIX 1 re-derivation change (main() no longer compares against it). The already_loaded branch's assignment was pure dead computation; the _file_pending() call in the other branch is kept for its side effect (populating documentation._pending) with the return value simply no longer captured. No behavior change -- full suite still 375 passed.
franciszver
added a commit
that referenced
this pull request
Jul 26, 2026
…, upstream filings, and kickoff-constraint gap (issue #59) Extends tests/test_release_notes.py red-first with the corrected facts, then rewrites docs/RELEASE_NOTES_v3.0.0.md to match: - #63 and #68 are CLOSED (P3.31/P3.34) -- rewrites the limitations section as gaps found and closed, not open, and adds the three new limitations P3.31 introduced (pending_human_triage_count is per-run not directory-wide, --approve's --db-path opt-out, --reports-dir requiring --db-path). - All four findings are now filed upstream (#167-#170); corrects the false "no upstream issue for VULN-0001/2/3" paragraph and notes #169/#170 as evidence against upstream #130's closure premise, without demanding a reopen. - Owns the kickoff-brief hard-constraint gap explicitly: intent (no shared context) is met; the parenthesised OS-process mechanism is not, per ARCHITECTURE.md and the ATO packet, both already public. - Corrects the stale "redteam.observability not in the forbidden import set" claim -- it now is (test_judge_agent.py), and adds the symmetric Red-Team-side AST scan. - Re-derives test counts post-rebase (405 with sibling / 299 in CI, 106 skipped) across RELEASE_NOTES/ATO_EVIDENCE_PACKET/DEMO_SCRIPT. Rebase note: docs/ATO_EVIDENCE_PACKET.md and docs/DEMO_SCRIPT.md conflicts were resolved by taking main's post-#74/#75/#76 versions wholesale (verified main's wording already supersedes this branch's pre-rebase edits to the same passages), then bumping only the test counts to the new post-rebase live total. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTi1zKDS1ajefLSmM9oL7E
franciszver
added a commit
that referenced
this pull request
Jul 26, 2026
… by the rebase judge.py's (case, response, attempt_id) triple was cited at judge.py:96-100,343 -- score()'s actual signature moved to 348-357 as other main PRs (#74/#75/#76) touched the file. campaign.py's try:-guarded component calls were cited at ...,436 for the documentation.file_report try: block -- it moved to 446. Both verified against the current file on disk before correcting; no other citation in the doc drifted (spot-checked judge.py:44-47, chat.py:570-594, the recording path, and the JSON-report field claims against the live tree -- all still accurate). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTi1zKDS1ajefLSmM9oL7E
franciszver
added a commit
that referenced
this pull request
Jul 26, 2026
…, upstream filings, and kickoff-constraint gap (issue #59) Extends tests/test_release_notes.py red-first with the corrected facts, then rewrites docs/RELEASE_NOTES_v3.0.0.md to match: - #63 and #68 are CLOSED (P3.31/P3.34) -- rewrites the limitations section as gaps found and closed, not open, and adds the three new limitations P3.31 introduced (pending_human_triage_count is per-run not directory-wide, --approve's --db-path opt-out, --reports-dir requiring --db-path). - All four findings are now filed upstream (#167-#170); corrects the false "no upstream issue for VULN-0001/2/3" paragraph and notes #169/#170 as evidence against upstream #130's closure premise, without demanding a reopen. - Owns the kickoff-brief hard-constraint gap explicitly: intent (no shared context) is met; the parenthesised OS-process mechanism is not, per ARCHITECTURE.md and the ATO packet, both already public. - Corrects the stale "redteam.observability not in the forbidden import set" claim -- it now is (test_judge_agent.py), and adds the symmetric Red-Team-side AST scan. - Re-derives test counts post-rebase (405 with sibling / 299 in CI, 106 skipped) across RELEASE_NOTES/ATO_EVIDENCE_PACKET/DEMO_SCRIPT. Rebase note: docs/ATO_EVIDENCE_PACKET.md and docs/DEMO_SCRIPT.md conflicts were resolved by taking main's post-#74/#75/#76 versions wholesale (verified main's wording already supersedes this branch's pre-rebase edits to the same passages), then bumping only the test counts to the new post-rebase live total. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTi1zKDS1ajefLSmM9oL7E
franciszver
added a commit
that referenced
this pull request
Jul 26, 2026
… by the rebase judge.py's (case, response, attempt_id) triple was cited at judge.py:96-100,343 -- score()'s actual signature moved to 348-357 as other main PRs (#74/#75/#76) touched the file. campaign.py's try:-guarded component calls were cited at ...,436 for the documentation.file_report try: block -- it moved to 446. Both verified against the current file on disk before correcting; no other citation in the doc drifted (spot-checked judge.py:44-47, chat.py:570-594, the recording path, and the JSON-report field claims against the live tree -- all still accurate). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTi1zKDS1ajefLSmM9oL7E
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #63
A vuln report routed to
pending_human_approvalpreviously had no durable surface:ActionLog.export_jsonlwas only ever called fromemit_snapshotat the top of a campaign iteration, so a--iterations 1run's own events (includingexploit_recorded/vuln_report_pending_human_approval) never reached the exported log;tools/run_campaign.pyalways usedExploitDB(":memory:")/DocumentationAgent(reports_dir=None); and there was noapprovepath reachable from the CLI at all. Approving VULN-0004 required a bespoke reconstruction script (tools/approve_vuln_0004.py) becauseDocumentationAgent._pendingwas in-memory only and the filing process had exited (issue #66's sharpening comment on #63).What changed
ActionLogexport (redteam/campaign.py):run_campaignnow callsaction_log.export_jsonl(action_log_ref)unconditionally after the loop (in atry/finally, hardened further below), so a run's own events are never lost regardless of iteration count, an injectedsnapshot_fn, or an exception escaping the loop.DocumentationAgentpersists pending reports too (redteam/agents/documentation.py): a pending report is now written to<reports_dir>/<report_id>.pending-human-approval.json(same suffix conventiontools/build_vuln_reports.pyalready used for VULN-0004), and__init__loads every persisted filed/pending report fromreports_dirback into memory at construction. This is what makesapprove()reachable across a process boundary — no bespoke reconstruction script needed. A filed report wins over a stale pending leftover of the sameexploit_id(self-healing after a crash between persisting-filed and unlinking-pending). A reports_dir this module can't parse raisesDocumentationAgentErrorloudly rather than silently losing a pending report.redteam/observability/findings.pygainspending_human_triage_count();emit_snapshotwires it into a new optional, additive v1observability_snapshot.schema.jsonfieldpending_human_triage_count(seecontracts/README.md's versioning rule — new optional property, not added torequired, staysv1).tools/run_campaign.py):--list-pending --reports-dir PATHand--approve EXPLOIT_ID --reports-dir PATH --approved-by NAME (--db-path PATH | --unverified-i-vouch-without-db-check). Neither touches a live model/target. When--db-pathnames a persisted exploit DB,--approvere-derives the report from the original exploit record viabuild_vuln_reportand refuses to approve on any field-value drift from the persisted pending artifact — the same verify-then-approve disciplinetools/approve_vuln_0004.pyestablished, generalized to any exploit_id.runbehaviour is unchanged:ExploitDB(":memory:")/DocumentationAgent(reports_dir=None)stay the default (a quick demo/smoke run perdocs/DEMO_SCRIPT.mdshould still leave nothing behind).--reports-dir/--db-pathare opt-in.tools/approve_vuln_0004.py's own designed re-run flow (a genuinely-pending report would crash the script via the duplicate-rejection guard, plus a double-unlink on the pending file). Fixed and covered by a new regression test.docs/ATO_EVIDENCE_PACKET.md/docs/DEMO_SCRIPT.mdpertests/test_doc_test_counts.py.Cold-review remediation (this revision)
A cold review of the original state above returned DO-NOT-MERGE with 4 blockers and several majors/minors, all fixed in follow-up commits on this same branch, each red-first and gated (
declutter→sec-audit→deep-review):tools/approve_vuln_0004.pyself-comparison (BLOCKER): thealready_loadedbranch compared the auto-loaded pending report againstpending_on_disk— the same file — which always passed regardless of tampering. Proven: a hand-tamperedVULN-0004pending artifact (severity: "low", doctoredclinical_impact) filed cleanly (rc=0) before the fix. Fixed: the comparison now uses an independent re-derivation viabuild_vuln_report(record, ...);already_loadedis used only to satisfyapprove()'s in-memory precondition.--approvefailed open (BLOCKER): no--db-pathmeant no provenance check at all (a hand-written pending report approved cleanly); a--db-pathnaming a non-existent file silently created an empty sqlite DB and downgraded to a warning;--approved-bydefaulted to"owner". Fixed: the cross-check is now required by default (explicit--unverified-i-vouch-without-db-checkescape hatch, loud WARNING), a missing--db-pathfile is a hard refusal,--approved-byhas no default, and the report body is printed before stamping.--reports-dirwithout--db-pathcrashed the run (BLOCKER): the documented "merely collides" flag combo actually raised an uncaughtDocumentationAgentErrorfromfile_reportmid-loop, and — because the post-loop export had notry/finally— silently discarded that run's own action log. Fixed:file_reportis now wrapped like its sibling component calls, the whole loop is intry/finally, andrunmode refuses to start with this flag combo instead of emitting a NOTE.report_idmismatch could overwrite approved evidence (MAJOR):_load_persistedkeyed reports byexploit_idfrom file content, never checking the filename orreport_iduniqueness. Proven: aweird-name.pending-human-approval.jsonclaimingreport_id: VULN-0001, exploit_id: EXP-0002caused--approve EXP-0002to overwrite the already-filed, already-approvedVULN-0001.json. Fixed: a persisted file whose name doesn't match its own claimedreport_idis now rejected at load,report_idcollisions acrossexploit_ids are rejected, and each pending report's real source path is tracked and unlinked on approval (not reconstructed fromreport_id).--list-pending/--approveno longer leak a raw traceback for a malformedreports_dir;contracts/README.mdand the schema's field description no longer overclaim "consumers stay valid" (true for producers, not for a consumer pinned to a pre-P3.31: pending human-triage reports have no durable surface #63 schema copy);pending_human_triage_count's two different scopes (snapshot-field vs.--list-pending's directory-wide count) are now documented in the schema, the Python docstring, anddocs/ARCHITECTURE.md; the--db-pathrebuild now derivesforce_human_gatefrom the trusted exploit record's category (not the untrusted field under verification) and carriesfix_validation_statusthrough; two stray Mermaid colour mutations indocs/ATO_EVIDENCE_PACKET.mdreverted; the new CLI approval path is now documented as Beat 5 ofdocs/DEMO_SCRIPT.md; doc test-count claims refreshed to match the live suite.Verification
pending_human_triage_countfield shipped bundled with its own already-passing tests in the same commit (see that commit's own message). Every fix in the cold-review remediation above is genuinely red-first, with failing output quoted in each commit message.declutter(folded a duplicated CLIreports_dir-load try/except into one helper; removed a dead variable assignment pyflakes flagged after the FIX 1 change),sec-audit(re-ran all four of the cold reviewer's proven attacks against the fixed code — each now refused, fail-closed, with a clean CLI message rather than a traceback),deep-review(semantic diff ofredteam/campaign.pyisolated to exactly the intendedtry/finallywrap + one newtry/except, no accidental logic change from the reindentation).docs/vuln_reports/*.json(VULN-0001..0004) sha256-verified byte-identical before and after every commit in this remediation — never touched, never run against with any builder/approve tool.Test plan
python -m pytest tests/ -q→ 375 passeddocs/vuln_reports/*.jsonsha256 unchanged (verified before this remediation and after every commit)DocumentationAgentinstance,delit, approve with a fresh instance viatools/run_campaign.py --approve ... --reports-dir ... --db-path ... --approved-by ...— works with no bespoke script