mirror of
https://github.com/saymrwulf/crisis.git
synced 2026-09-05 19:20:35 +00:00
A new sibling Python package, `crisis_agents`, that lifts the Crisis
protocol from "consensus between machines" to "consensus between AI
agents". Threat model: a team of sub-agents normally talks freely
with its orchestrator (the "mothership"); when the team's boundary
opens and an external agent of unknown trust joins, the mothership
activates the Crisis layer so byzantine equivocation is detectable.
Two-phase orchestration model:
Phase 1 — closed team, no Crisis: agents emit claims directly, the
mothership collects them flat.
Phase 2 — boundary opens: every subsequent claim is wrapped into a
Crisis Message with the agent's stable process_id and a PoW nonce,
delivered into per-agent LamportGraphs, and after each turn the
mothership scans for mutations via LamportGraph.find_mutations.
Phase 3 — proof: when an alarm fires, the mothership emits a
replayable JSON proof-of-malfeasance document with the contradictory
witnesses, their delivery sets, and DAG cross-references showing
which honest agents saw what.
Modules:
- claim.py Claim dataclass + JSON round-trip
- boundary.py membership tracker + open() trigger
- agent.py CrisisAgent abstract + MockAgent + MockByzantineAgent
(the latter equivocates by emitting two variants to
disjoint peer subsets at the same logical turn)
- mothership.py orchestrator driving both phases, building Crisis
Messages from Claims, per-agent LamportGraphs, log
- alarm.py scan_for_mutations: same-agent same-turn distinct
digests with non-identical delivery sets, verified
spacelike via LamportGraph.are_spacelike on the
honest-agent graphs
- proof.py build_proof + ProofDocument + JSON serializer +
verify_proof_self_consistent
- cli.py `crisis-agents demo` + `crisis-agents verify`
- scenarios/ fact_check: reference doc + 6 statements + scripted
honest/byzantine agents producing a deterministic
equivocation on statement s03
Tests: 50 new tests across test_claim, test_boundary, test_mothership,
test_alarm, test_proof, test_demo_fact_check. End-to-end test runs the
fact_check scenario, asserts exactly one alarm raised, proof is built,
re-serialized JSON passes self-consistency. Full suite (existing
crisis + new crisis_agents) green in 0.77s — 145 tests.
Out of scope (deliberately): visualization (separate CrisisViz upgrade
later), real TCP gossip (agents talk via in-process function calls in
the mothership), false-claim detection without equivocation (an
agent that consistently lies but never equivocates is out-voted, not
"caught"; catching it would require a ground-truth oracle).
Reuse from existing crisis package: Message, Vertex, LamportGraph,
LamportGraph.find_mutations, ProofOfWorkWeight, digest. The new code
is a thin adapter layer; the protocol substrate did the heavy lifting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
"""End-to-end test: fact_check scenario runs cleanly and emits a valid proof."""
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
from crisis_agents.alarm import scan_for_mutations
|
||
from crisis_agents.cli import main as cli_main
|
||
from crisis_agents.mothership import Mothership
|
||
from crisis_agents.proof import (
|
||
build_proof,
|
||
verify_proof_self_consistent,
|
||
)
|
||
from crisis_agents.scenarios import build_fact_check_scenario
|
||
|
||
|
||
class TestFactCheckEndToEnd:
|
||
|
||
def test_scenario_loads(self):
|
||
s = build_fact_check_scenario()
|
||
assert s.name == "fact_check"
|
||
assert len(s.honest_agents) == 3
|
||
assert s.byzantine_joiner.name == "agent_delta"
|
||
assert "Pluto" in s.reference_doc
|
||
|
||
def test_runs_through_both_phases_and_raises_one_alarm(self):
|
||
s = build_fact_check_scenario()
|
||
m = Mothership()
|
||
for agent in s.honest_agents:
|
||
m.add_agent(agent)
|
||
|
||
m.run_closed_phase(num_turns=s.closed_phase_turns)
|
||
assert len(m.run_result.closed_log) == 3 * 6 # 3 agents × 6 statements
|
||
assert m.all_graphs() == {} # no DAG in closed phase
|
||
|
||
m.open_boundary(s.byzantine_joiner)
|
||
m.run_crisis_phase(num_turns=s.crisis_phase_turns)
|
||
|
||
# The byzantine emitted two contradictory variants of s03;
|
||
# honest agents emitted nothing in the Crisis phase (their script
|
||
# was exhausted in the closed phase).
|
||
assert len(m.run_result.crisis_log) == 2
|
||
|
||
alarms = scan_for_mutations(m)
|
||
assert len(alarms) == 1
|
||
a = alarms[0]
|
||
assert a.accused_agent == "agent_delta"
|
||
assert a.statement_id == "s03"
|
||
assert a.spacelike_verified is True
|
||
|
||
def test_proof_is_self_consistent_and_round_trips(self, tmp_path):
|
||
s = build_fact_check_scenario()
|
||
m = Mothership()
|
||
for agent in s.honest_agents:
|
||
m.add_agent(agent)
|
||
m.run_closed_phase(num_turns=s.closed_phase_turns)
|
||
m.open_boundary(s.byzantine_joiner)
|
||
m.run_crisis_phase(num_turns=s.crisis_phase_turns)
|
||
|
||
alarm = scan_for_mutations(m)[0]
|
||
proof = build_proof(m, alarm)
|
||
out = tmp_path / "proof.json"
|
||
out.write_text(proof.to_json())
|
||
|
||
# Reload, re-verify
|
||
from crisis_agents.proof import ProofDocument
|
||
reloaded = ProofDocument.from_json(out.read_text())
|
||
assert verify_proof_self_consistent(reloaded).ok
|
||
|
||
|
||
class TestCli:
|
||
|
||
def test_cli_demo_runs(self, tmp_path, capsys):
|
||
exit_code = cli_main(["demo", "--scenario", "fact_check",
|
||
"--out-dir", str(tmp_path)])
|
||
assert exit_code == 0
|
||
captured = capsys.readouterr()
|
||
assert "crisis-agents demo" in captured.out
|
||
assert "Phase 1" in captured.out
|
||
assert "Phase 2" in captured.out
|
||
assert "alarm" in captured.out.lower()
|
||
# A proof file landed
|
||
proofs = list(tmp_path.glob("proof_*.json"))
|
||
assert len(proofs) == 1
|
||
# The proof file is valid JSON
|
||
obj = json.loads(proofs[0].read_text())
|
||
assert obj["accused_agent"] == "agent_delta"
|
||
|
||
def test_cli_verify_passes_on_valid_proof(self, tmp_path, capsys):
|
||
# First produce a proof via the demo
|
||
cli_main(["demo", "--scenario", "fact_check", "--out-dir", str(tmp_path)])
|
||
proof_path = next(tmp_path.glob("proof_*.json"))
|
||
capsys.readouterr() # drain demo output
|
||
|
||
exit_code = cli_main(["verify", str(proof_path)])
|
||
assert exit_code == 0
|
||
out = capsys.readouterr().out
|
||
assert "self-consistent: True" in out
|
||
|
||
def test_cli_unknown_scenario(self, capsys):
|
||
exit_code = cli_main(["demo", "--scenario", "nonexistent"])
|
||
assert exit_code == 2
|