a9308edc5d
Adds the tamper-evident event stream that makes every DV auditable. Each
measurable moment is appended as one JSON object per line to an append-only
output/sor-runs/<ts>/events.jsonl over the closed vocabulary {consent_request,
consent_accept, consent_reject, circuit_build, hop_add, bridge_forward,
churn_kill, churn_spawn, rebuild_start, rebuild_done}; on close the file is
SHA-256'd and the digest is sealed exactly once into manifest.json (the single
sanctioned None->hash completion of the manifest).
Records carry only metadata (fingerprints, byte counts, latencies, decisions),
never message plaintext, so the zero-knowledge relay property is preserved. The
deterministic replay_fixture_circuit emits a seed-driven event stream with no
engine, socket, or traffic (containment-safe) and is the basis of
instrument-validation gate item 6.
- cmd_chat/sor/events.py: EventLog (append-only, per-record schema),
replay_fixture_circuit, replay_and_seal.
- cmd_chat/sor/provenance.py: seal_manifest (seal-once events SHA + stop ts).
- tests: replay -> schema-valid JSONL whose SHA matches the manifest;
append-only (original prefix byte-identical after further appends);
seed-determinism modulo timestamps; seal-once immutability.
Live emit wiring into the _sor handlers lands with R4/R5.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
124 lines
4.0 KiB
Python
124 lines
4.0 KiB
Python
"""R3 — immutable event log acceptance checks.
|
|
|
|
Acceptance predicate (roadmap R3): replaying a fixture circuit produces a
|
|
schema-valid JSONL whose SHA-256 matches the manifest; the file is append-only
|
|
(no in-place edits). This is also instrument-validation gate item 6.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from cmd_chat.sor.events import (
|
|
EVENT_TYPES,
|
|
EventLog,
|
|
replay_and_seal,
|
|
replay_fixture_circuit,
|
|
validate_record,
|
|
)
|
|
from cmd_chat.sor.provenance import Node, RunManifest
|
|
|
|
KNOWN_PUB_B64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
|
|
|
|
|
def _manifest(root, run_id="20260719T010203Z", seed=123456789):
|
|
return RunManifest(
|
|
run_id=run_id,
|
|
sor_seed=seed,
|
|
topology="1-house",
|
|
selector="static",
|
|
churn_schedule_id="fixture-churn-0",
|
|
nodes=[Node(role="host", persona_pub_b64=KNOWN_PUB_B64, engine="docker")],
|
|
engine_kind="docker",
|
|
worktree_root=root,
|
|
)
|
|
|
|
|
|
def _read_lines(path):
|
|
return [json.loads(l) for l in path.read_text().splitlines() if l.strip()]
|
|
|
|
|
|
def test_replay_schema_valid_and_hash_matches_manifest(tmp_path):
|
|
run_dir = tmp_path / "run"
|
|
sealed = replay_and_seal(run_dir, _manifest(tmp_path), pool=5, hops=3, rebuilds=2)
|
|
|
|
events_path = run_dir / "events.jsonl"
|
|
assert events_path.is_file()
|
|
|
|
# Every line schema-valid, uses a known event type, and carries the seed.
|
|
records = _read_lines(events_path)
|
|
assert len(records) > 0
|
|
for rec in records:
|
|
validate_record(rec)
|
|
assert rec["event"] in EVENT_TYPES
|
|
assert rec["seed"] == 123456789
|
|
|
|
# SHA-256 of the closed file matches what was sealed into the manifest.
|
|
disk_sha = hashlib.sha256(events_path.read_bytes()).hexdigest()
|
|
assert sealed["events"]["sha256"] == disk_sha
|
|
manifest_doc = json.loads((run_dir / "manifest.json").read_text())
|
|
assert manifest_doc["events"]["sha256"] == disk_sha
|
|
assert manifest_doc["timestamps"]["stop_utc"] is not None
|
|
|
|
|
|
def test_replay_is_deterministic_modulo_timestamps(tmp_path):
|
|
# Same seed -> identical circuit-build sequence -> identical event bodies
|
|
# (drop the wall-clock ts). Two independent runs must match field-for-field.
|
|
a_dir = tmp_path / "a"
|
|
b_dir = tmp_path / "b"
|
|
replay_fixture_circuit(a_dir, "runA", 42, pool=6, hops=3, rebuilds=3)
|
|
replay_fixture_circuit(b_dir, "runB", 42, pool=6, hops=3, rebuilds=3)
|
|
|
|
def _strip(path, run_id):
|
|
out = []
|
|
for r in _read_lines(path):
|
|
r = dict(r)
|
|
r.pop("ts")
|
|
r["run_id"] = "X" # normalize the only other per-run field
|
|
out.append(r)
|
|
return out
|
|
|
|
assert _strip(a_dir / "events.jsonl", "runA") == _strip(b_dir / "events.jsonl", "runB")
|
|
|
|
|
|
def test_append_only_no_in_place_edits(tmp_path):
|
|
run_dir = tmp_path / "run"
|
|
log = EventLog(run_dir, "runX", 7)
|
|
log.emit("circuit_build", circuit_id="c0", hop_index=0)
|
|
first_bytes = (run_dir / "events.jsonl").read_bytes()
|
|
first_len = len(first_bytes)
|
|
|
|
log.emit("hop_add", node_fp="00000001", circuit_id="c0", hop_index=0, bytes_=10, latency_ms=1.0)
|
|
log.close()
|
|
|
|
after = (run_dir / "events.jsonl").read_bytes()
|
|
# The file only grew; the original prefix is byte-identical (append-only).
|
|
assert len(after) > first_len
|
|
assert after[:first_len] == first_bytes
|
|
|
|
|
|
def test_seal_is_once_only(tmp_path):
|
|
run_dir = tmp_path / "run"
|
|
replay_and_seal(run_dir, _manifest(tmp_path), rebuilds=1)
|
|
# A second seal attempt on the same manifest is refused (immutability).
|
|
from cmd_chat.sor.provenance import seal_manifest
|
|
|
|
with pytest.raises(ValueError):
|
|
seal_manifest(run_dir, "0" * 64)
|
|
|
|
|
|
def test_unknown_event_rejected(tmp_path):
|
|
log = EventLog(tmp_path / "run", "runX", 1)
|
|
with pytest.raises(ValueError):
|
|
log.emit("not_a_real_event")
|
|
log.close()
|
|
|
|
|
|
def test_closed_log_refuses_emit(tmp_path):
|
|
log = EventLog(tmp_path / "run", "runX", 1)
|
|
log.emit("circuit_build", circuit_id="c0")
|
|
log.close()
|
|
with pytest.raises(RuntimeError):
|
|
log.emit("circuit_build", circuit_id="c1")
|