From 6a61e547a175e8d70e2fcd4426eb4b90badb17a8 Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:56:24 -0400 Subject: [PATCH 1/3] Run executable controls for the verdict evidence predicate The draft predicate in #297 says that a scope lift must come from a statement the peer signed, and that a quarantine record must be bound and have an outcome after review_by. Add a test file that runs a pinned corpus of signed records against a reference reader, with accepted and refused cases for each rule, and a workflow that installs the corpus from its own dependency group and requires it. Signed-off-by: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> --- .../workflows/verdict-evidence-controls.yml | 64 +++++++++ pyproject.toml | 8 ++ tests/test_verdict_evidence_controls.py | 126 ++++++++++++++++++ uv.lock | 9 ++ 4 files changed, 207 insertions(+) create mode 100644 .github/workflows/verdict-evidence-controls.yml create mode 100644 tests/test_verdict_evidence_controls.py diff --git a/.github/workflows/verdict-evidence-controls.yml b/.github/workflows/verdict-evidence-controls.yml new file mode 100644 index 0000000..bc29920 --- /dev/null +++ b/.github/workflows/verdict-evidence-controls.yml @@ -0,0 +1,64 @@ +# Runs the executable controls for the draft verdict evidence predicate. +# +# The draft (docs/predicates/acs-verdict-evidence-v1.md, #297) says in prose that a +# scope lift must come from a statement the peer signed, and that a quarantine record +# must be bound and must have an outcome after review_by. The controls are signed +# records with accepted and refused cases. They come from the verdict-evidence +# dependency group, pinned in uv.lock, so this job is the only place they install. +# The guard job in deploy-pages.yml collects the same test file and skips it there. +# +# The ACS-Core suite stays in tests/ as files, per #19. This job does not change that. +name: Verdict evidence controls + +on: + pull_request: + paths: + - "docs/predicates/**" + - "tests/test_verdict_evidence_controls.py" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/verdict-evidence-controls.yml" + push: + branches: ["main", "integration"] + paths: + - "docs/predicates/**" + - "tests/test_verdict_evidence_controls.py" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/verdict-evidence-controls.yml" + workflow_dispatch: + +permissions: {} + +concurrency: + group: verdict-evidence-controls-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + controls: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.9.9" + + # --locked refuses a lockfile that drifted from pyproject.toml, so the controls + # that run are the ones the lockfile pins. + - name: Install the controls from the lockfile + run: uv sync --locked --group verdict-evidence + + # With this set, a missing package fails the run instead of skipping it. + - name: Run the controls + env: + ACS_REQUIRE_VERDICT_EVIDENCE: "1" + run: uv run --locked --group verdict-evidence pytest -v tests/test_verdict_evidence_controls.py diff --git a/pyproject.toml b/pyproject.toml index 52430de..34f8c75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,10 @@ dev = [ "pytest>=8.0", "jsonschema>=4.25.0",] # than dev, so the docs build and the test gate never install it. The lockfile pins the # version with hashes, and Dependabot moves it under the usual cooldown. zizmor = [ "zizmor>=1.30.1",] +# The verdict evidence controls in tests/test_verdict_evidence_controls.py, run by +# .github/workflows/verdict-evidence-controls.yml. A named group, like zizmor, so the +# docs build and the guard job never install it. The lockfile pins the commit. +verdict-evidence = [ "agent-evidence-vectors",] [tool.pytest.ini_options] # The deploy workflow runs a bare `uv run pytest -v` as the gate the site build @@ -21,3 +25,7 @@ zizmor = [ "zizmor>=1.30.1",] # collection to tests/ keeps that gate about the guards it was written for. # Suites that carry their own dependencies run from their own workflow. testpaths = ["tests"] + +[tool.uv.sources] +# Pinned to the commit that carries the controls until a release on PyPI does. +agent-evidence-vectors = { git = "https://github.com/probityai/agent-evidence-vectors", rev = "9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c" } diff --git a/tests/test_verdict_evidence_controls.py b/tests/test_verdict_evidence_controls.py new file mode 100644 index 0000000..fa0a2c0 --- /dev/null +++ b/tests/test_verdict_evidence_controls.py @@ -0,0 +1,126 @@ +"""Executable controls for the draft verdict evidence predicate (docs/predicates, #297). + +The draft says two things in prose. A `witness_scope` above SELF must come from a +statement the peer authored and signed, naming the requirement it checked and the +artifact it used, never from a field the runner writes. A quarantine record must be +bound, and `review_by` must have an outcome. These tests run a pinned corpus of +signed records against a reference reader. Each refused case is one change from an +accepted twin: a wrong peer key, a peer statement the runner signed, the wrong vector, +the wrong requirement, an absent peer artifact, a different transcript, altered +quarantine fields and an overdue `review_by`. + +The corpus and the reader come from the `verdict-evidence` dependency group, pinned in +uv.lock. The guard job does not install that group, so there the module is skipped. +The verdict-evidence-controls workflow installs it and sets +ACS_REQUIRE_VERDICT_EVIDENCE=1, which turns a missing package into a failure. + +To run a verifier of your own against the same cases, set +ACS_VERDICT_EVIDENCE_VERIFIER to its command line. It is called as +` /case.json --json`, prints one JSON object with `decision` and +`reason`, and exits 0 for supported, 1 for contradicted or not_established, 2 for no +verdict. +""" +import importlib.util +import json +import os +import re +import shlex +import subprocess +import sys +from pathlib import Path + +import pytest + +REQUIRED = os.environ.get("ACS_REQUIRE_VERDICT_EVIDENCE") == "1" +MODULE = "agent_evidence_vectors.verdict_evidence" + +if importlib.util.find_spec("agent_evidence_vectors") is None or ( + importlib.util.find_spec(MODULE) is None): + if REQUIRED: + raise RuntimeError( + f"{MODULE} is not installed; run `uv sync --locked --group verdict-evidence`") + pytest.skip("the verdict-evidence dependency group is not installed", + allow_module_level=True) + +from agent_evidence_vectors.verdict_evidence import run as controls # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] +PREDICATE_DOC = ROOT / "docs" / "predicates" / "acs-verdict-evidence-v1.md" +MANIFEST = json.loads((controls.PACKAGED_CORPUS / "MANIFEST.json").read_text()) +CASES = MANIFEST["cases"] +RUN = [sys.executable, "-m", f"{MODULE}.run"] + +# One refusal for each rule the draft states in prose. +DRAFT_RULES = { + "peer_artifact_unverified:untrusted_key": "wrong peer key, or the runner's own key", + "peer_artifact_unverified:wrong_vector": "the peer checked another vector", + "peer_artifact_unverified:wrong_requirement": "the peer checked another requirement", + "peer_artifact_unverified:not_supplied": "the record names a peer artifact nobody holds", + "peer_artifact_unverified:transcript_digest_mismatch": "the retained transcript differs", + "witness_scope_misderived": "PEER or EXTERNAL with no peer statement behind it", + "field_invalid:provenance.lift_state": "a lift field written by the runner", + "quarantine_digest_mismatch": "a quarantine field edited after the digest", + "quarantine_overdue": "review_by has passed", +} + + +def _report(tmp_path, *extra): + out = tmp_path / "out" + done = subprocess.run([*RUN, "--output", str(out), *extra], capture_output=True, + text=True, check=False) + report = json.loads((out / "report.json").read_text()) if out.exists() else None + return done, report + + +def test_the_reference_reader_answers_every_case_as_expected(tmp_path): + done, report = _report(tmp_path) + assert done.returncode == 0, done.stdout + done.stderr + assert report["answered"] == report["total"] == len(CASES) + assert report["failures"] == [] + + +@pytest.mark.parametrize("reason", sorted(DRAFT_RULES)) +def test_each_rule_in_the_draft_has_a_refused_case_with_an_accepted_twin(reason): + by_id = {c["id"]: c for c in CASES} + refused = [c for c in CASES if c["expected"]["reason"] == reason] + assert refused, f"no case for: {DRAFT_RULES[reason]}" + for case in refused: + assert by_id[case["twin"]]["expected"]["decision"] == "supported" + + +def test_no_accepted_scope_lift_rests_on_a_runner_key(): + for case in CASES: + if case["expected"]["reason"] in ("verdict_verified_peer", "verdict_verified_external"): + spec = json.loads((controls.PACKAGED_CORPUS / "cases" / case["id"] / + "case.json").read_text()) + assert "peer_statement" in spec["supplied"] + assert not set(spec["policy"]["peer_keys"]) & set(spec["policy"]["trusted_keys"]) + + +@pytest.mark.skipif(not PREDICATE_DOC.exists(), reason="the predicate document lands with #297") +def test_the_corpus_uses_the_predicate_type_the_document_states(): + uris = re.findall(r"`(https://[^`]+acs-verdict-evidence-v1\.md)`", PREDICATE_DOC.read_text()) + assert MANIFEST["verdictPredicate"] in uris + + +def _stub(tmp_path, decision, code): + script = tmp_path / f"stub_{decision}.py" + script.write_text("import json, sys\n" + f"print(json.dumps({{'decision': {decision!r}, 'reason': 'stub'}}))\n" + f"sys.exit({code})\n") + return f"{shlex.quote(sys.executable)} {shlex.quote(str(script))}" + + +@pytest.mark.parametrize(("decision", "code"), [("supported", 0), ("contradicted", 1)]) +def test_a_verifier_that_answers_one_way_fails_the_run(tmp_path, decision, code): + done, report = _report(tmp_path, "--verifier", _stub(tmp_path, decision, code)) + assert done.returncode == 1, done.stdout + done.stderr + assert report["failures"], "a constant verifier must fail some case" + + +@pytest.mark.skipif("ACS_VERDICT_EVIDENCE_VERIFIER" not in os.environ, + reason="no verifier under test named") +def test_the_named_verifier_answers_every_case_as_expected(tmp_path): + done, report = _report(tmp_path, "--verifier", os.environ["ACS_VERDICT_EVIDENCE_VERIFIER"]) + assert done.returncode == 0, done.stdout + done.stderr + assert report["answered"] == report["total"] diff --git a/uv.lock b/uv.lock index dd6b15d..85be738 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,9 @@ dev = [ { name = "jsonschema" }, { name = "pytest" }, ] +verdict-evidence = [ + { name = "agent-evidence-vectors" }, +] zizmor = [ { name = "zizmor" }, ] @@ -33,8 +36,14 @@ dev = [ { name = "jsonschema", specifier = ">=4.25.0" }, { name = "pytest", specifier = ">=8.0" }, ] +verdict-evidence = [{ name = "agent-evidence-vectors", git = "https://github.com/probityai/agent-evidence-vectors?rev=9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c" }] zizmor = [{ name = "zizmor", specifier = ">=1.30.1" }] +[[package]] +name = "agent-evidence-vectors" +version = "0.19.1" +source = { git = "https://github.com/probityai/agent-evidence-vectors?rev=9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c#9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c" } + [[package]] name = "attrs" version = "26.1.0" From 4f45bc4ee91cdc6350bb1aa13817ae980541eb2d Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:48:59 -0400 Subject: [PATCH 2/3] Pin the verdict evidence group to the green vectors commit The earlier pin pointed at a commit whose own CI was red for a stale intake record. The module and corpus are unchanged between the two. Signed-off-by: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 34f8c75..43f176f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,4 +28,4 @@ testpaths = ["tests"] [tool.uv.sources] # Pinned to the commit that carries the controls until a release on PyPI does. -agent-evidence-vectors = { git = "https://github.com/probityai/agent-evidence-vectors", rev = "9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c" } +agent-evidence-vectors = { git = "https://github.com/probityai/agent-evidence-vectors", rev = "c2ffcedd69875bca97d3f3aca76cd45cc2a2da63" } diff --git a/uv.lock b/uv.lock index 85be738..605c36b 100644 --- a/uv.lock +++ b/uv.lock @@ -36,13 +36,13 @@ dev = [ { name = "jsonschema", specifier = ">=4.25.0" }, { name = "pytest", specifier = ">=8.0" }, ] -verdict-evidence = [{ name = "agent-evidence-vectors", git = "https://github.com/probityai/agent-evidence-vectors?rev=9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c" }] +verdict-evidence = [{ name = "agent-evidence-vectors", git = "https://github.com/probityai/agent-evidence-vectors?rev=c2ffcedd69875bca97d3f3aca76cd45cc2a2da63" }] zizmor = [{ name = "zizmor", specifier = ">=1.30.1" }] [[package]] name = "agent-evidence-vectors" version = "0.19.1" -source = { git = "https://github.com/probityai/agent-evidence-vectors?rev=9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c#9b2a003ce4812d2402b85e0a5792c2f60d8d4e8c" } +source = { git = "https://github.com/probityai/agent-evidence-vectors?rev=c2ffcedd69875bca97d3f3aca76cd45cc2a2da63#c2ffcedd69875bca97d3f3aca76cd45cc2a2da63" } [[package]] name = "attrs" From 33c687509c40a66f9a1f091cf2a1486541aa9a5f Mon Sep 17 00:00:00 2001 From: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:00:58 -0400 Subject: [PATCH 3/3] Bind the peer statement to the verdict and test the digest rule Re-pins the group to a vectors commit whose peer statement carries the verdict the peer reached, so one statement cannot lift a record that reports another result. Adds the two refusals from the review on #33: a reused statement and a digest that leaves out expiry_disposition. Signed-off-by: Sankalp Gilda <23521054+astrogilda@users.noreply.github.com> --- pyproject.toml | 2 +- tests/test_verdict_evidence_controls.py | 2 ++ uv.lock | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 43f176f..c2cf640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,4 +28,4 @@ testpaths = ["tests"] [tool.uv.sources] # Pinned to the commit that carries the controls until a release on PyPI does. -agent-evidence-vectors = { git = "https://github.com/probityai/agent-evidence-vectors", rev = "c2ffcedd69875bca97d3f3aca76cd45cc2a2da63" } +agent-evidence-vectors = { git = "https://github.com/probityai/agent-evidence-vectors", rev = "617cf55151b2142c5ff4eed4aa2f4eeb5a8261c5" } diff --git a/tests/test_verdict_evidence_controls.py b/tests/test_verdict_evidence_controls.py index fa0a2c0..ef7bbc9 100644 --- a/tests/test_verdict_evidence_controls.py +++ b/tests/test_verdict_evidence_controls.py @@ -59,6 +59,8 @@ "peer_artifact_unverified:transcript_digest_mismatch": "the retained transcript differs", "witness_scope_misderived": "PEER or EXTERNAL with no peer statement behind it", "field_invalid:provenance.lift_state": "a lift field written by the runner", + "peer_disagrees": "the peer saw another result, or its statement is reused for another verdict", + "payload_digest_fields_incomplete": "a digest that leaves out a field, such as expiry_disposition", "quarantine_digest_mismatch": "a quarantine field edited after the digest", "quarantine_overdue": "review_by has passed", } diff --git a/uv.lock b/uv.lock index 605c36b..d1628ae 100644 --- a/uv.lock +++ b/uv.lock @@ -36,13 +36,13 @@ dev = [ { name = "jsonschema", specifier = ">=4.25.0" }, { name = "pytest", specifier = ">=8.0" }, ] -verdict-evidence = [{ name = "agent-evidence-vectors", git = "https://github.com/probityai/agent-evidence-vectors?rev=c2ffcedd69875bca97d3f3aca76cd45cc2a2da63" }] +verdict-evidence = [{ name = "agent-evidence-vectors", git = "https://github.com/probityai/agent-evidence-vectors?rev=617cf55151b2142c5ff4eed4aa2f4eeb5a8261c5" }] zizmor = [{ name = "zizmor", specifier = ">=1.30.1" }] [[package]] name = "agent-evidence-vectors" version = "0.19.1" -source = { git = "https://github.com/probityai/agent-evidence-vectors?rev=c2ffcedd69875bca97d3f3aca76cd45cc2a2da63#c2ffcedd69875bca97d3f3aca76cd45cc2a2da63" } +source = { git = "https://github.com/probityai/agent-evidence-vectors?rev=617cf55151b2142c5ff4eed4aa2f4eeb5a8261c5#617cf55151b2142c5ff4eed4aa2f4eeb5a8261c5" } [[package]] name = "attrs"