Add audit taskflows v2: contested, reproduction-gated vulnerability discovery - #99
Add audit taskflows v2: contested, reproduction-gated vulnerability discovery#99anticomputer wants to merge 12 commits into
Conversation
Five-stage pipeline (survey, hunt, contest, reproduce, report) whose finding lifecycle is enforced by a new finding_ledger MCP server rather than by prompt text. Adds the audit_v2 taskflows, personalities, prompts, toolboxes, the container images and a corpus render test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
Open a file-backed database instead of falling back to in-memory when FINDING_LEDGER_DIR does not exist yet, which silently dropped a run's findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
Relocate finding_ledger under mcp_servers/audit_v2/ and add
repo_survey{,_models}.py with its toolbox: components and entry points keyed by
trust boundary, replacing repo_context in the v2 survey and hunt. Adds tests, a
temp-dir conftest, and Go in the base image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
…or endpoint repo_survey now upserts components and entry points so repeated passes enrich one row. Hunters file without a pre-check and the dedup task unions provenance. adjudication uses api_type responses; the run script drops stale containers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
Add attribute_findings (a whole run in one call) and drop proposed_by from store_finding, so provenance comes from the runner's branch records. hunt.yaml captures its typed response for the outputs gate. Add engagement.yaml framing to the model-facing tasks, set backend/api_type per model slot, and remove grok-4.5. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
…ude-opus-4.8 Require a benign marker observed reaching the sink rather than a weaponised exploit, across reproduction_engineer.yaml, reproduction_rules.yaml, reproduce.yaml, the store_reproduction_attempt wording and the README. Switch the reproduction slot from claude-opus-5 to claude-opus-4.8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
Add open_state_engine in schema_init.py, used by both the finding_ledger and repo_survey backends instead of each duplicating the mkdir, file-backed engine and race-tolerant create_all. Also rewords two leftover reproduction docstrings to reachability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
There was a problem hiding this comment.
Pull request overview
This PR introduces audit taskflows v2, a new, multi-stage vulnerability discovery pipeline that adds adversarial validation and runtime reachability reproduction, while keeping the existing v1 audit taskflows unchanged. It adds new MCP-backed durable stores (survey + finding ledger), new taskflows/prompts/personalities for the five-stage lifecycle, container tooling for reproduction, and a corpus gate to ensure shipped taskflows and prompts lint and render correctly.
Changes:
- Add audit_v2 pipeline taskflows (survey/hunt/contest/reproduce/report) plus shared prompts and personas for each role.
- Add new audit_v2 MCP servers and durable SQLite-backed backends (repo survey + finding ledger) with concurrency-tolerant schema initialization.
- Add end-to-end validation support: tests for stores and corpus gating, a reproduction container image + publishing hooks, and a runner script for the v2 pipeline.
Show a summary per file
| File | Description |
|---|---|
| tests/test_taskflow_corpus.py | Adds corpus gate covering YAML validation, taskflow linting, and audit_v2 prompt rendering/over checks. |
| tests/test_repo_survey.py | Adds behavioral tests for the audit_v2 repo survey backend (dedup/validation/durability + toolbox wiring). |
| tests/test_finding_ledger.py | Adds extensive tests for ledger lifecycle rules, dedup, attribution, durability, and MCP tool exposure. |
| tests/conftest.py | Forces MCP servers’ state/log dirs into a temp location to avoid mutating user/global state during tests. |
| src/seclab_taskflows/toolboxes/container_shell_reproduction.yaml | Introduces a dedicated reproduction toolbox pointing at the reproduction container image. |
| src/seclab_taskflows/toolboxes/audit_v2_repo_survey.yaml | Wires the repo survey MCP server as a toolbox with operator guidance. |
| src/seclab_taskflows/toolboxes/audit_v2_finding_ledger.yaml | Wires the finding ledger MCP server as a toolbox with lifecycle rules guidance. |
| src/seclab_taskflows/taskflows/audit_v2/survey.yaml | Stage 1: fetch source, map components + entry points, and publish typed outputs. |
| src/seclab_taskflows/taskflows/audit_v2/hunt.yaml | Stage 2: multi-model hunting fanout, post-hunt attribution, candidate fetch + dedup. |
| src/seclab_taskflows/taskflows/audit_v2/contest.yaml | Stage 3: prosecution/defense/adjudication fanouts using the ledger as join point. |
| src/seclab_taskflows/taskflows/audit_v2/reproduce.yaml | Stage 4: sequential reproduction of confirmed findings inside a sandboxed container. |
| src/seclab_taskflows/taskflows/audit_v2/report.yaml | Stage 5: report drafting from the ledger + a verification pass against recorded evidence. |
| src/seclab_taskflows/taskflows/audit_v2/README.md | Documents audit_v2 goals, lifecycle, model roles, running instructions, and safety constraints. |
| src/seclab_taskflows/prompts/audit_v2/engagement.yaml | Shared “authorized audit” framing and scope constraints. |
| src/seclab_taskflows/prompts/audit_v2/evidence_rules.yaml | Shared non-speculation and citation requirements for all audit stages. |
| src/seclab_taskflows/prompts/audit_v2/finding_contract.yaml | Defines what qualifies as a “finding” and filing rules for hunters. |
| src/seclab_taskflows/prompts/audit_v2/contest_rules.yaml | Defines adversarial contest mechanics, roles, and allowed positions. |
| src/seclab_taskflows/prompts/audit_v2/reproduction_rules.yaml | Defines reachability-focused reproduction requirements and outcome recording. |
| src/seclab_taskflows/prompts/audit_v2/severity_rubric.yaml | Defines severity levels and how to apply preconditions/reachability. |
| src/seclab_taskflows/personalities/vulnerability_hunter.yaml | Persona for hunt stage emphasizing taint/path tracing and non-padding. |
| src/seclab_taskflows/personalities/exploit_prosecutor.yaml | Persona for prosecution role in contest stage. |
| src/seclab_taskflows/personalities/exploit_defender.yaml | Persona for defense role in contest stage. |
| src/seclab_taskflows/personalities/finding_adjudicator.yaml | Persona for adjudication role with citation checking focus. |
| src/seclab_taskflows/personalities/reproduction_engineer.yaml | Persona for reproduction role emphasizing literal observed evidence. |
| src/seclab_taskflows/mcp_servers/audit_v2/schema_init.py | Adds race-tolerant schema initialization for concurrent server startups. |
| src/seclab_taskflows/mcp_servers/audit_v2/repo_survey_models.py | Defines SQLAlchemy schema + projections for repo survey wire contracts. |
| src/seclab_taskflows/mcp_servers/audit_v2/repo_survey.py | Implements repo survey backend + MCP tool surface. |
| src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger_models.py | Defines SQLAlchemy models + constants for ledger lifecycle. |
| src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py | Implements ledger backend + MCP tool surface + lifecycle enforcement. |
| src/seclab_taskflows/containers/base/Dockerfile | Installs upstream Go toolchain into the base image to avoid old Debian Go. |
| src/seclab_taskflows/containers/reproduction/Dockerfile | Adds a reproduction container image with debugging/runtime tooling. |
| src/seclab_taskflows/configs/model_config_audit_v2.yaml | Defines default v2 role→model mapping and backend/api_type per role. |
| src/seclab_taskflows/configs/model_config_audit_v2_lowercost.yaml | Provides a cheaper drop-in model mapping for exploratory runs. |
| scripts/build_container_images.sh | Adds a reproduction build target and includes it in all. |
| scripts/audit_v2/run_audit_v2.sh | Adds a runner for audit_v2 stages with model override and stage selection. |
| .github/workflows/publish-container-images.yml | Publishes the new reproduction image to GHCR. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Suppressed comments (2)
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:370
store_reproduction_attemptalso does not verify that therepoargument matches the finding’srepo. This can record reproduction attempts against the wrong repository context, which is especially risky because reproduced findings become immune to later adjudication.
with Session(self.engine) as session:
finding = session.get(Finding, finding_id)
if finding is None:
return f"No finding with id {finding_id}"
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:286
adjudicate_findingdoes not verify that therepoargument matchesfinding.repo. This allows a caller to confirm/reject a finding while supplying a different repo context, which breaks the per-repository isolation the pipeline relies on (and can lead to state changes being attributed to the wrong audit run).
with Session(self.engine) as session:
finding = session.get(Finding, finding_id)
if finding is None:
return f"No finding with id {finding_id}"
if finding.state == STATE_REPRODUCED:
- Files reviewed: 36/36 changed files
- Comments generated: 5
- Review effort level: Lite
Route attribute_finding through a shared _repo_mismatch helper and add the same check to store_contest_verdict, adjudicate_finding and store_reproduction_attempt in finding_ledger.py; refuse an entry point whose component lives in another repo in repo_survey.py. Add regression tests for each guard, fix the tautological corpus assertion, and correct the README reproduction model and lifecycle outcome. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
|
Addressed the review feedback in 84e409f:
Added regression tests for each of the four new cross-repo guards. Full suite: 333 passed, 1 xfailed; ruff clean. |
There was a problem hiding this comment.
Review details
Suppressed comments (2)
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:453
get_finding()returnsverdictsandreproduction_attemptsfrom SQLAlchemy queries without an explicit ordering. SQLite often appears to preserve insertion order, but this is not guaranteed, and it can make the API nondeterministic (and can maketests/test_finding_ledger.py::TestAdjudication::test_adjudication_is_recorded_as_a_verdictflaky if row order changes). Ordering these lists byid(orcreated_at) keeps results stable.
data["verdicts"] = [
verdict_to_dict(v)
for v in session.query(ContestVerdict).filter_by(finding_id=finding_id).all()
]
data["reproduction_attempts"] = [
attempt_to_dict(a)
for a in session.query(ReproductionAttempt).filter_by(finding_id=finding_id).all()
]
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:572
- The MCP tool docstring for
find_similar_findingssays it is "to avoid duplicates", but the audit_v2 design explicitly relies on hunters filing independently and not pre-checking for duplicates (dedup happens later). This wording could steer models into calling this tool during hunting and reduce the corroboration signal the pipeline is designed to preserve.
def find_similar_findings(
owner: str = Field(description="The owner of the GitHub repository"),
repo: str = Field(description="The name of the GitHub repository"),
component: str = Field(description="Component to match", default=""),
vuln_class: str = Field(description="Vulnerability class to match", default=""),
):
"""Find existing findings in the same component and class, to avoid duplicates."""
repo = process_repo(owner, repo)
return json.dumps(backend.find_similar_findings(repo, component, vuln_class))
- Files reviewed: 36/36 changed files
- Comments generated: 0 new
- Review effort level: Lite
…ledger Add normalize_repo and a normalizes_repo decorator in mcp_servers/utils.py and apply it to every repo-taking method of the finding ledger and repo survey backends, so per-repo lookups and cross-repo guards no longer depend on each caller lowercasing first. Have process_repo build on normalize_repo. Add a report stage branch that explains an empty ledger instead of returning the raw empty finding list. Cover both with regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
There was a problem hiding this comment.
Review details
Suppressed comments (2)
tests/conftest.py:28
tempfile.mkdtemp()creates a real directory that is never cleaned up. This will leaveseclab-taskflows-tests-*directories behind on developer machines/CI runners. Also,_TMPis created even when all relevant env vars are already set, which is unnecessary.
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:581- The
find_similar_findingstool docstring says it's "to avoid duplicates", but audit_v2 intentionally preserves duplicates until the dedicated deduplication step to capture cross-model convergence signal. This wording is likely to push models toward the wrong behavior.
"""Find existing findings in the same component and class, to avoid duplicates."""
- Files reviewed: 37/37 changed files
- Comments generated: 0 new
- Review effort level: Lite
Check the positional target against an owner/repo shape in run_audit_v2.sh, right after the empty check and before any container or stage work, so a mistyped target fails immediately with an example instead of several minutes into a stage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
There was a problem hiding this comment.
Review details
Suppressed comments (2)
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:445
get_findings(..., state=...)accepts any string and will silently return an empty list for typos (for exampleconfirmedwith a trailing space). That can mask configuration errors and make later stages think there are no findings.
Validate state against FINDING_STATES and fail fast on unknown values.
@normalizes_repo
def get_findings(self, repo, state=None):
with Session(self.engine) as session:
query = session.query(Finding).filter_by(repo=repo)
if state:
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:582
- The tool docstring says
find_similar_findingsis “to avoid duplicates”, but the v2 pipeline explicitly instructs hunters not to use it for filing decisions (it’s intended for the later dedup step / related-work lookup). This mismatch can lead to the wrong stage relying on it.
Update the docstring to reflect its intended use in audit_v2.
def find_similar_findings(
owner: str = Field(description="The owner of the GitHub repository"),
repo: str = Field(description="The name of the GitHub repository"),
component: str = Field(description="Component to match", default=""),
vuln_class: str = Field(description="Vulnerability class to match", default=""),
):
"""Find existing findings in the same component and class, to avoid duplicates."""
repo = process_repo(owner, repo)
- Files reviewed: 37/37 changed files
- Comments generated: 1
- Review effort level: Lite
…indows Add a dispose() to the finding ledger and repo survey backends and call it from the test fixtures and the corpus test, so the SQLite file handle is released before the temporary directory is removed; on Windows the tempdir teardown raised PermissionError while the engine still held the file open. Normalize backslashes in test_taskflow_corpus._dotted so glob paths on Windows map to the same dotted module names as on POSIX, which restores the known lint-error xfail and the .audit_v2. membership checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
There was a problem hiding this comment.
Review details
Suppressed comments (6)
src/seclab_taskflows/mcp_servers/audit_v2/repo_survey.py:141
store_entry_point()storesfileverbatim. Normalizing whitespace avoids creating duplicate entry points for the same site when the caller includes leading/trailing spaces or newlines.
trust_boundary = _require(trust_boundary, TRUST_BOUNDARIES, "trust_boundary")
line = int(line or 0)
with Session(self.engine) as session:
src/seclab_taskflows/mcp_servers/audit_v2/repo_survey.py:213
get_component()fetches entry points bycomponent_idonly. While cross-repo attachment is guarded on write, filtering byrepohere makes the read path robust against any accidental cross-repo records (or future schema changes) and prevents leaking entry points from another repo if the DB is ever inconsistent.
rows = (
session.query(EntryPoint)
.filter(EntryPoint.component_id == component_id)
.all()
)
src/seclab_taskflows/mcp_servers/audit_v2/repo_survey.py:90
store_component()useslocationas the deduplication key, but it is not normalized. If a model emits trailing/leading whitespace (common in LLM outputs), the same component can be stored twice under slightly different spellings and the hunt fan-out will duplicate work.
This issue also appears on line 139 of the same file.
kind = _require(kind, COMPONENT_KINDS, "kind", default=KIND_OTHER)
with Session(self.engine) as session:
src/seclab_taskflows/mcp_servers/audit_v2/schema_init.py:44
create_all_tolerating_races()only retries when the error string contains "already exists". Under concurrent SQLite DDL, a common transient failure mode is also "database is locked"; treating that as retryable improves reliability when many servers start at once.
except OperationalError as exc:
if "already exists" not in str(exc).lower():
raise
# Another server won this table. It may still be creating the
# rest, so pause and re-check rather than assuming all are ready.
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:590
- The
find_similar_findingstool docstring says it is "to avoid duplicates", but the audit_v2 design (and prompts) explicitly say hunters should not use it to decide whether to file. Aligning this docstring with the intended usage prevents confusing future prompt/tool authors.
"""Find existing findings in the same component and class, to avoid duplicates."""
tests/conftest.py:18
tempfile.mkdtemp()is never cleaned up, so local test runs will leaveseclab-taskflows-tests-*directories behind. Registering anatexitcleanup keeps the "set env before imports" behavior but avoids accumulating temp directories.
- Files reviewed: 37/37 changed files
- Comments generated: 1
- Review effort level: Lite
Open the finding ledger and repo survey engine with check_same_thread=False in schema_init.py, so a pooled connection opened by one fan-out branch can be reused from another thread without raising. Verify the Go tarball in the base container Dockerfile against a pinned per-architecture SHA256 before extracting it, so a tampered or truncated download fails the build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dadec5f9-3bf8-449c-84d6-db45be19bb6a
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/seclab_taskflows/mcp_servers/audit_v2/finding_ledger.py:590
find_similar_findingstool docstring suggests using it “to avoid duplicates”, but the audit_v2 finding contract explicitly says hunters must not use it to decide whether to file (they should file first and let the later dedup step merge). This mismatch is likely to cause incorrect tool use by models and reduce the convergence signal the pipeline relies on.
"""Find existing findings in the same component and class, to avoid duplicates."""
- Files reviewed: 37/37 changed files
- Comments generated: 0 new
- Review effort level: Lite
What this adds
Audit taskflows v2: a five-stage vulnerability-discovery pipeline that lives alongside the existing v1 taskflows in its own
audit_v2namespace and does not change them.The stages are survey (decompose the repo into components and map where untrusted data crosses a trust boundary), hunt (three model families hunt each component in parallel, then a dedup pass folds convergent findings), contest (a prosecutor argues a finding is real, a defender from a different model family argues it is not, and an adjudicator from a third family decides), reproduce (stand the target up in a container and dynamically show the flow reaches the sink), and report (write the findings up, then verify every claim against the ledger).
The lifecycle is enforced in code, not prompts
A finding's state is derived by the new
finding_ledgerMCP server and never set by a model. No tool accepts a state as an argument: a model records evidence, and the backend decides what that evidence entitles the finding to. Adjudication is the only path toconfirmedand requires that both advocates actually filed; reproduction only promotes fromconfirmed; a reproduced finding is immune to later adjudication. The ledger is also where the stages meet: multi-model fan-outs do not feed the implicit last-tool-result channel, so a store that survives a crash is the only place they can share state.Reproduction proves reachability, not exploitation
The reproduce stage validates that attacker-controlled input actually reaches the dangerous sink at runtime, using a benign, distinctive marker or lightweight instrumentation rather than a weaponised exploit. That is enough for a maintainer to confirm a bug and write the patch, and it is also what keeps the public models willing to do the work: framed as exploitation, the newest models soft-refuse under content filtering; framed as reachability, they validate cleanly and record literal container output.
Model configuration
Three families do the analysis so no advocate ever grades a sibling's case:
gpt-5.6-sol,claude-opus-*andgemini-3.6-flash. Every slot pins its backend and API type explicitly (responses where available, native/v1/messagesfor Anthropic with adaptive thinking, chat completions for Gemini).grok-4.5is deliberately absent from every role: CAPI rejects any vulnerability-analysis content sent to it with403 permission-denied, failed checkSAFETY_CHECK_TYPE_CYBER, down to a five-line snippet. The reproduction slot runsclaude-opus-4.8rather thanclaude-opus-5, which soft-refuses the agentic reproduction loop under content filtering.Validation
Every stage has been run live end-to-end against a vulnerable test app: three bugs (OS command injection, SQL injection, path traversal) are found by multiple families, deduplicated with their provenance unioned, confirmed through the contest, and each reproduced in a container with a control case and a benign marker observed reaching the sink. 329 tests pass and every audit_v2 prompt renders through a corpus gate.
Dependency note
The live pipeline needs the OpenAI SDK bump in seclab-taskflow-agent#288 to run on Python 3.14; without it the Responses streaming path raises on a
typing.Unionattribute.