R2: run manifest / provenance writer for reproducible, auditable runs
write_manifest() freezes at circuit-experiment start everything needed to
reproduce and audit a SOR run and writes an immutable
output/sor-runs/<ts>/manifest.json: the R1 --sor-seed, topology/selector/churn
schedule id, one persona fingerprint per participating node, worktree git SHA,
isolated engine kind + image digest, pip/cargo dependency freeze, and
start/stop timestamps. The events.jsonl SHA-256 field is reserved for R3.
Provenance only: no forwarder, no engine spawned. Defense-in-depth to keep
containment load-bearing, the schema refuses to record a non-isolated
("local") engine at run or node level; the binding assertion still lands with
the R4 forwarder.
- cmd_chat/sor/provenance.py: Node/RunManifest, node_fingerprint (bit-for-bit
mirror of persona.rs::fingerprint_of), git/deps capture, explicit
dependency-free schema validator.
- hh/src/persona.rs: fingerprint_of known-vector parity test.
- tests: R2 acceptance predicate (manifest present + schema-valid, non-empty
seed, >=1 fingerprint/node), immutability, containment rejection of local
engines, and cross-language fingerprint parity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""R2 — provenance manifest acceptance checks.
|
||||
|
||||
Acceptance predicate (roadmap R2): a manifest is present + schema-valid after a
|
||||
run, carries a non-empty seed, and >=1 persona fingerprint per participating
|
||||
node. The fingerprint parity constant below is locked identically in
|
||||
hh/src/persona.rs::tests::fingerprint_of_known_vector.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from cmd_chat.sor.provenance import (
|
||||
Node,
|
||||
RunManifest,
|
||||
node_fingerprint,
|
||||
validate_manifest,
|
||||
write_manifest,
|
||||
)
|
||||
|
||||
# Known 32-byte pubkey (bytes 0..32) -> base64 -> fingerprint. Same vector as
|
||||
# the Rust persona test.
|
||||
KNOWN_PUB_B64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
KNOWN_FINGERPRINT = "630dcd29"
|
||||
|
||||
|
||||
def _fixture_manifest(root):
|
||||
return RunManifest(
|
||||
run_id="20260719T000000Z",
|
||||
sor_seed=123456789,
|
||||
topology="1-house",
|
||||
selector="static",
|
||||
churn_schedule_id="fixture-churn-0",
|
||||
nodes=[
|
||||
Node(role="host", persona_pub_b64=KNOWN_PUB_B64, engine="docker"),
|
||||
Node(role="hop", persona_pub_b64="QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNk", engine="docker"),
|
||||
],
|
||||
engine_kind="docker",
|
||||
engine_image_digest="sha256:deadbeef",
|
||||
worktree_root=root,
|
||||
)
|
||||
|
||||
|
||||
def test_fingerprint_parity_with_rust():
|
||||
assert node_fingerprint(KNOWN_PUB_B64) == KNOWN_FINGERPRINT
|
||||
|
||||
|
||||
def test_manifest_written_and_schema_valid(tmp_path):
|
||||
run_dir = tmp_path / "sor-runs" / "20260719T000000Z"
|
||||
doc = write_manifest(run_dir, _fixture_manifest(tmp_path))
|
||||
|
||||
# present on disk
|
||||
path = run_dir / "manifest.json"
|
||||
assert path.is_file()
|
||||
on_disk = json.loads(path.read_text())
|
||||
assert on_disk == doc
|
||||
|
||||
# schema-valid, non-empty seed, >=1 fingerprint per node (the R2 predicate)
|
||||
validate_manifest(on_disk)
|
||||
assert on_disk["sor_seed"] == 123456789
|
||||
assert len(on_disk["nodes"]) >= 1
|
||||
for n in on_disk["nodes"]:
|
||||
assert n["fingerprint"]
|
||||
assert on_disk["nodes"][0]["fingerprint"] == KNOWN_FINGERPRINT
|
||||
|
||||
|
||||
def test_manifest_is_immutable(tmp_path):
|
||||
run_dir = tmp_path / "run"
|
||||
write_manifest(run_dir, _fixture_manifest(tmp_path))
|
||||
with pytest.raises(FileExistsError):
|
||||
write_manifest(run_dir, _fixture_manifest(tmp_path))
|
||||
|
||||
|
||||
def test_containment_rejects_local_engine(tmp_path):
|
||||
m = _fixture_manifest(tmp_path)
|
||||
m.engine_kind = "local"
|
||||
with pytest.raises(ValueError):
|
||||
write_manifest(tmp_path / "run2", m)
|
||||
|
||||
|
||||
def test_containment_rejects_local_node_engine(tmp_path):
|
||||
m = _fixture_manifest(tmp_path)
|
||||
m.nodes[1].engine = "local"
|
||||
with pytest.raises(ValueError):
|
||||
write_manifest(tmp_path / "run3", m)
|
||||
|
||||
|
||||
def test_invalid_pubkey_rejected():
|
||||
with pytest.raises(ValueError):
|
||||
node_fingerprint("!!! not base64 !!!")
|
||||
|
||||
|
||||
def test_validator_catches_empty_nodes():
|
||||
with pytest.raises(ValueError):
|
||||
validate_manifest(
|
||||
{
|
||||
"schema_version": "sor-manifest/1",
|
||||
"run_id": "x",
|
||||
"created_utc": "t",
|
||||
"sor_seed": 1,
|
||||
"topology": "1-house",
|
||||
"selector": "static",
|
||||
"churn_schedule_id": "c",
|
||||
"nodes": [],
|
||||
"engine": {"kind": "docker", "image_digest": None},
|
||||
"git": {},
|
||||
"deps": {},
|
||||
"timestamps": {"start_utc": "t", "stop_utc": None},
|
||||
"events": {"path": "events.jsonl", "sha256": None},
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user