7ed23704ea
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>
112 lines
3.4 KiB
Python
112 lines
3.4 KiB
Python
"""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},
|
|
}
|
|
)
|