T06: out-of-band self-verification

Checks written as plain functions over a serialized Evidence Pack, outside the
framework - no Oracle, no Runner, no Verdict aggregation. 12 tests that they
hold, 12 that they can fail. All four td://self identifiers covered.

The substantive check is verdict reproducibility from S3 evidence alone,
asserted on failing runs as well as passing ones.

F-0003 (open): actor isolation leaves no trace in ordinary evidence - the
self-test catches a shared memory store only because the harness plants
per-actor canaries. Isolation is currently a property of a scenario written
to expose it, not of runs in general. The mirror-image case is noted too: a
guarantee enforced by construction cannot be verified by observing real runs,
so four green self-tests are not four equivalent proofs. Carried to T10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1629012@bnt-lap001
Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39
This commit is contained in:
tegwick 2026-08-22 23:38:23 +02:00
parent de25673c5d
commit 5734b280c6
12 changed files with 619 additions and 6 deletions

View file

View file

@ -0,0 +1,219 @@
"""Out-of-band verification of test-driver's own foundational guarantees.
These checks are deliberately **not** written using test-driver. Using the
framework to establish that the framework's oracles are independent is a system
certifying itself: any flaw serious enough to matter would likely be shared by
both the thing under test and the thing testing it.
So: plain functions over an Evidence Pack, plain pytest assertions, no `Oracle`,
no `Runner`, no `Verdict` aggregation. They read the same artefact an auditor
would read six months later, and nothing else.
Each check returns a list of violation strings empty means satisfied. Returning
data rather than asserting keeps the checks usable both as tests and, later, as
the `td://self/...` verification assets themselves.
Registry of self-verification identifiers:
td://self/actor-isolation
td://self/oracle-independence
td://self/evidence-reproducibility
td://self/intent-independence
A check that cannot fail is worth nothing, so every check here has a
corresponding case in `test_checks_can_fail.py` that feeds it a deliberately
broken artefact and asserts it complains.
"""
from __future__ import annotations
import json
from typing import Any, Iterable, Mapping
SELF_CHECKS = (
"td://self/actor-isolation",
"td://self/oracle-independence",
"td://self/evidence-reproducibility",
"td://self/intent-independence",
)
# Provenance values that may back an assertion capable of producing FAIL.
ADMISSIBLE_PROVENANCE = frozenset({"human", "spec", "agent-from-spec"})
def _observations(pack: Mapping[str, Any]) -> list[dict]:
return list(pack.get("observations", ()))
def _snapshots_by_step(pack: Mapping[str, Any]) -> dict[str, dict]:
return {
obs["step_id"]: obs["data"]
for obs in _observations(pack)
if obs["kind"] == "state_snapshot"
}
# --- td://self/actor-isolation -------------------------------------------
def check_actor_isolation(
pack: Mapping[str, Any],
secrets_by_actor: Mapping[str, str],
memories_by_actor: Mapping[str, Mapping[str, Any]],
) -> list[str]:
"""No actor may hold or emit anything private to another actor.
Checked two ways, because either alone is weak:
* **memory** actor A's private store must not contain actor B's secret;
* **evidence** an observation attributed to actor A must not carry B's
secret, which would mean the leak escaped into the record even if A's
memory looked clean.
"""
violations: list[str] = []
for actor, memory in memories_by_actor.items():
rendered = json.dumps(memory, default=str)
for other, secret in secrets_by_actor.items():
if other != actor and secret in rendered:
violations.append(
f"actor {actor!r} holds {other!r}'s private value in memory"
)
for obs in _observations(pack):
collector = obs.get("collector")
if collector not in secrets_by_actor:
continue
rendered = json.dumps(obs.get("data"), default=str)
for other, secret in secrets_by_actor.items():
if other != collector and secret in rendered:
violations.append(
f"observation {obs['id']} collected by {collector!r} "
f"carries {other!r}'s private value"
)
return violations
# --- td://self/oracle-independence ---------------------------------------
def check_no_actor_collected_judgment(
pack: Mapping[str, Any], actor_ids: Iterable[str]
) -> list[str]:
"""S2 and S3 evidence must never be attributed to an actor."""
actors = set(actor_ids)
return [
f"{obs['stratum']} observation {obs['id']} was collected by actor "
f"{obs['collector']!r}"
for obs in _observations(pack)
if obs["stratum"] in ("S2", "S3") and obs["collector"] in actors
]
def check_verdicts_follow_from_judgment_evidence(
pack: Mapping[str, Any], assertions: Iterable[Any]
) -> list[str]:
"""Every recorded verdict must be reproducible from S3 evidence alone.
This is the substantive independence check. Re-evaluating each assertion
against the stored snapshots with no actor, no driver and no live system in
reach must reproduce exactly what the run reported. If it does not, then
something outside the judgment stratum influenced the verdict, whatever the
architecture diagram claims.
"""
violations: list[str] = []
snapshots = _snapshots_by_step(pack)
recorded = {(v["assertion_id"], v["step_id"]): v["verdict"] for v in pack["verdicts"]}
for assertion in assertions:
for (assertion_id, step_id), verdict in recorded.items():
if assertion_id != assertion.id:
continue
snapshot = snapshots.get(step_id)
if snapshot is None:
violations.append(
f"verdict {assertion_id}@{step_id} has no S3 snapshot to rest on"
)
continue
try:
expected = "PASS" if assertion.predicate(snapshot) else "FAIL"
except KeyError:
expected = "INCONCLUSIVE"
if expected != verdict:
violations.append(
f"verdict {assertion_id}@{step_id} recorded {verdict}, but the "
f"retained judgment evidence yields {expected}"
)
return violations
# --- td://self/evidence-reproducibility -----------------------------------
def check_evidence_supports_every_verdict(pack: Mapping[str, Any]) -> list[str]:
"""A verdict with no evidence behind it is an EVIDENCE_FAILURE, not a result."""
violations: list[str] = []
snapshots = _snapshots_by_step(pack)
steps_with_surface = {
obs["step_id"] for obs in _observations(pack) if obs["kind"] == "realization"
}
for verdict in pack["verdicts"]:
step_id = verdict["step_id"]
if step_id not in snapshots:
violations.append(
f"verdict {verdict['assertion_id']}@{step_id} has no state snapshot"
)
if step_id not in steps_with_surface:
violations.append(
f"verdict {verdict['assertion_id']}@{step_id} has no record of how "
"the step was performed"
)
if not pack.get("sut_version"):
violations.append("evidence does not identify the version under test")
if not pack.get("finished_at"):
violations.append("evidence does not record when the run ended")
return violations
def check_runs_agree(first: Mapping[str, Any], second: Mapping[str, Any]) -> list[str]:
"""Two runs from the same seed must reach the same judgments."""
def key(pack):
return sorted(
(v["assertion_id"], v["step_id"], v["verdict"]) for v in pack["verdicts"]
)
if key(first) != key(second):
return ["two runs from the same initial state disagreed"]
if first["sut_version"] != second["sut_version"]:
return ["runs were taken against different versions"]
return []
# --- td://self/intent-independence ----------------------------------------
def check_intent_independence(pack: Mapping[str, Any]) -> list[str]:
"""No verdict may rest on intent derived from the implementation.
Independence of *components* does not give independence of *belief*. A
verdict is only worth something if the claim behind it came from somewhere
the implementation could not reach.
"""
violations: list[str] = []
index = pack.get("provenance_index", {})
for verdict in pack["verdicts"]:
assertion_id = verdict["assertion_id"]
provenance = index.get(assertion_id)
if provenance is None:
violations.append(
f"assertion {assertion_id!r} produced a verdict with no recorded "
"provenance — its independence cannot be audited"
)
elif provenance not in ADMISSIBLE_PROVENANCE:
violations.append(
f"assertion {assertion_id!r} has provenance {provenance!r}, which is "
"derived from the implementation it constrains"
)
return violations

View file

@ -0,0 +1,188 @@
"""A self-test that cannot fail is worth nothing.
Every check in `checks.py` is fed a deliberately broken artefact here, and must
complain. Without this file, the self-verification suite would be a row of green
ticks with no evidence that any of them is load-bearing.
Two kinds of break appear below, and the difference is worth noticing:
* **structural breaks** the guarantee is actually violated in a running
system (actors sharing memory). The check must catch it from the evidence.
* **artefact breaks** the guarantee cannot be violated through the framework
because the architecture forbids it, so the check is exercised against a
hand-built pack instead. Where that applies it is stated explicitly, together
with the separate assertion that the framework does refuse.
"""
from __future__ import annotations
import copy
import json
import pytest
from testdriver import Actor, Cast, Oracle, Runner, World
from testdriver.runner import CollectorIndependenceError
from lab.mutations import ObservationChannel, build_lab
from testdriver import DirectDriver, StateObserver
from scenarios.alice_bob_carol import USE_CASE, build
from tests.selfverification.checks import (
check_actor_isolation,
check_evidence_supports_every_verdict,
check_intent_independence,
check_no_actor_collected_judgment,
check_runs_agree,
check_verdicts_follow_from_judgment_evidence,
)
from tests.selfverification.test_self_verification import SECRETS, run_and_serialize
# --- td://self/actor-isolation -------------------------------------------
def test_shared_actor_memory_is_caught():
"""A structural break: two actors handed the same private store.
This is what an isolation bug actually looks like when the same agent
technology executes several actors in one process nobody writes
`bob.memory = alice.memory`, but a shared default or a cached client
achieves it by accident.
"""
lab, tokens = build_lab()
shared: dict = {}
cast = Cast()
for name in ("alice", "bob", "carol"):
actor = Actor(name, name.title(), credentials={"token": tokens[name]})
object.__setattr__(actor, "_memory", shared) # the leak
cast.add(actor)
_, _, _, asset, _ = build()
world = World("w-leaky", lab, lab.version, cast=cast)
driver = DirectDriver(lab, tokens)
observer = StateObserver(ObservationChannel(lab), asset.scenario.watches)
for actor_id, secret in SECRETS.items():
world.cast[actor_id].remember(f"private-{actor_id}", secret)
result = Runner(world, driver, observer, Oracle()).run(asset)
pack = json.loads(result.evidence.to_json())
memories = {a.id: {k: a.recall(k) for k in a.known_keys()} for a in world.cast}
violations = check_actor_isolation(pack, SECRETS, memories)
assert violations, "shared actor memory went undetected"
assert any("holds" in v for v in violations)
def test_a_secret_leaking_into_the_record_is_caught():
"""An artefact break: the leak escapes into evidence even if memory is clean."""
pack, memories, _ = run_and_serialize()
tampered = copy.deepcopy(pack)
for obs in tampered["observations"]:
if obs["collector"] == "alice":
obs["data"]["stolen"] = SECRETS["bob"]
break
assert check_actor_isolation(tampered, SECRETS, memories)
# --- td://self/oracle-independence ---------------------------------------
def test_the_framework_refuses_to_let_an_actor_collect_judgment_evidence():
"""The architecture prevents this, so it cannot be produced by a real run."""
world, driver, observer, asset, oracle = build()
observer.name = "bob"
with pytest.raises(CollectorIndependenceError):
Runner(world, driver, observer, oracle).run(asset)
def test_actor_collected_judgment_evidence_is_caught_in_the_record():
"""...and if it ever did appear in a pack, the out-of-band check sees it.
Both halves are needed. The first says the door is locked; this one says we
would notice if someone came through the window.
"""
pack, _, actors = run_and_serialize()
tampered = copy.deepcopy(pack)
for obs in tampered["observations"]:
if obs["stratum"] == "S3":
obs["collector"] = "bob"
break
assert check_no_actor_collected_judgment(tampered, actors)
def test_a_verdict_that_contradicts_its_evidence_is_caught():
"""The strongest of the checks: verdicts must follow from S3 alone."""
pack, _, _ = run_and_serialize()
tampered = copy.deepcopy(pack)
tampered["verdicts"][0]["verdict"] = "FAIL" # a verdict nothing supports
assertions = list(USE_CASE.claims) + list(USE_CASE.invariants)
violations = check_verdicts_follow_from_judgment_evidence(tampered, assertions)
assert violations
assert "retained judgment evidence yields" in violations[0]
def test_a_pass_silently_replacing_a_fail_is_caught():
"""The specific corruption the project most needs to notice."""
pack, _, _ = run_and_serialize("M15")
tampered = copy.deepcopy(pack)
for verdict in tampered["verdicts"]:
if verdict["verdict"] == "FAIL":
verdict["verdict"] = "PASS"
assertions = list(USE_CASE.claims) + list(USE_CASE.invariants)
assert check_verdicts_follow_from_judgment_evidence(tampered, assertions)
# --- td://self/evidence-reproducibility -----------------------------------
def test_a_verdict_without_a_snapshot_is_caught():
pack, _, _ = run_and_serialize()
tampered = copy.deepcopy(pack)
tampered["observations"] = [
o for o in tampered["observations"] if o["kind"] != "state_snapshot"
]
assert check_evidence_supports_every_verdict(tampered)
def test_a_verdict_without_a_record_of_how_the_step_ran_is_caught():
pack, _, _ = run_and_serialize()
tampered = copy.deepcopy(pack)
tampered["observations"] = [
o for o in tampered["observations"] if o["kind"] != "realization"
]
violations = check_evidence_supports_every_verdict(tampered)
assert any("how" in v for v in violations)
def test_evidence_that_does_not_name_the_version_is_caught():
pack, _, _ = run_and_serialize()
tampered = copy.deepcopy(pack)
tampered["sut_version"] = ""
assert check_evidence_supports_every_verdict(tampered)
def test_disagreeing_runs_are_caught():
first, _, _ = run_and_serialize()
second, _, _ = run_and_serialize("M15")
assert check_runs_agree(first, second)
# --- td://self/intent-independence ----------------------------------------
def test_implementation_derived_intent_is_caught():
pack, _, _ = run_and_serialize("M15")
tampered = copy.deepcopy(pack)
tampered["provenance_index"]["c-bob-revoked"] = "agent-from-implementation"
violations = check_intent_independence(tampered)
assert violations
assert "derived from the implementation" in violations[0]
def test_unrecorded_provenance_is_caught():
"""Absent provenance is not a benign omission — it makes the verdict
unauditable, which for this framework is the same as unusable."""
pack, _, _ = run_and_serialize()
tampered = copy.deepcopy(pack)
tampered["provenance_index"] = {}
assert check_intent_independence(tampered)

View file

@ -0,0 +1,110 @@
"""The framework's foundational guarantees, verified from outside the framework.
These read an Evidence Pack the way an auditor would as JSON, with no live
objects and use plain pytest. No Oracle, no Runner, no Verdict aggregation.
"""
from __future__ import annotations
import json
import pytest
from testdriver import Runner
from scenarios.alice_bob_carol import USE_CASE, build
from tests.selfverification.checks import (
SELF_CHECKS,
check_actor_isolation,
check_evidence_supports_every_verdict,
check_intent_independence,
check_no_actor_collected_judgment,
check_runs_agree,
check_verdicts_follow_from_judgment_evidence,
)
SECRETS = {
"alice": "alice-only-9f3c",
"bob": "bob-only-2a71",
"carol": "carol-only-55e0",
}
def run_and_serialize(*mutations: str):
"""Run, then throw away everything except the serialized evidence.
Deliberate: the checks must work from the artefact alone, exactly as they
would months later, with no access to the objects that produced it.
"""
world, driver, observer, asset, oracle = build(*mutations)
for actor_id, secret in SECRETS.items():
world.cast[actor_id].remember("private", secret)
result = Runner(world, driver, observer, oracle).run(asset)
pack = json.loads(result.evidence.to_json())
memories = {a.id: {k: a.recall(k) for k in a.known_keys()} for a in world.cast}
return pack, memories, list(world.cast.actors)
def test_the_registry_is_complete():
assert len(SELF_CHECKS) == 4
# --- td://self/actor-isolation -------------------------------------------
def test_actor_isolation():
pack, memories, _ = run_and_serialize()
assert check_actor_isolation(pack, SECRETS, memories) == []
# --- td://self/oracle-independence ---------------------------------------
def test_no_actor_collected_judgment_evidence():
pack, _, actors = run_and_serialize()
assert check_no_actor_collected_judgment(pack, actors) == []
@pytest.mark.parametrize("mutations", [(), ("M15",), ("M11",), ("M01", "M15")])
def test_verdicts_are_reproducible_from_judgment_evidence_alone(mutations):
"""Holds on passing and failing runs alike — a check that only works when
everything is green is not verifying independence, it is verifying luck."""
pack, _, _ = run_and_serialize(*mutations)
assertions = list(USE_CASE.claims) + list(USE_CASE.invariants)
assert check_verdicts_follow_from_judgment_evidence(pack, assertions) == []
# --- td://self/evidence-reproducibility -----------------------------------
def test_every_verdict_is_supported_by_retained_evidence():
pack, _, _ = run_and_serialize()
assert check_evidence_supports_every_verdict(pack) == []
def test_two_runs_from_the_same_seed_agree():
first, _, _ = run_and_serialize()
second, _, _ = run_and_serialize()
assert check_runs_agree(first, second) == []
assert first["run_id"] != second["run_id"]
def test_reproducibility_holds_on_a_failing_run():
first, _, _ = run_and_serialize("M15")
second, _, _ = run_and_serialize("M15")
assert check_runs_agree(first, second) == []
assert any(v["verdict"] == "FAIL" for v in first["verdicts"])
# --- td://self/intent-independence ----------------------------------------
def test_intent_independence():
pack, _, _ = run_and_serialize()
assert check_intent_independence(pack) == []
def test_intent_independence_holds_when_a_claim_fails():
"""The moment that matters: a FAIL is an accusation, and an accusation
resting on implementation-derived intent is worthless."""
pack, _, _ = run_and_serialize("M15")
assert check_intent_independence(pack) == []