crisis/tests/test_demo_fact_check.py

105 lines
3.5 KiB
Python
Raw Normal View History

Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
"""End-to-end test: the fact_check scenario walks the decentralized flow
and produces a quorum-ratified proof."""
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
import json
from pathlib import Path
from crisis_agents.cli import main as cli_main
from crisis_agents.mothership import Mothership
from crisis_agents.proof import (
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
ProofDocument,
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
build_proof,
verify_proof_self_consistent,
)
from crisis_agents.scenarios import build_fact_check_scenario
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
from crisis_agents.vote import quorum_for
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
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
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
def test_runs_through_all_phases(self):
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
s = build_fact_check_scenario()
m = Mothership()
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
for a in s.honest_agents:
m.add_agent(a)
crisis_agents: drop the wall-clock, drive asynchronously to quiescence The previous driver imposed a synchronous turn-counted clock that the Crisis paper explicitly forbids — Crisis is supposed to work in asynchronous P2P networks, with any synchronicity being virtual and derived inside the consensus algorithm from the DAG structure, not imposed externally by a coordinator. This commit removes the wall clock. What changed in the engine: - `Mothership.run_crisis_phase(num_turns, gossip_rounds_per_turn)` is replaced by `run_until_quiescent(max_steps=200)`. The loop interleaves three concerns on each iteration — emissions, gossip, and alarm emissions — until none make progress. Termination is by quiescence, not by a fixed turn count. `max_steps` is a safety bound (loop-iteration cap), not an exposed clock. - `Mothership.run_closed_phase(num_turns)` becomes `run_closed_phase(max_steps=50)`. Same quiescence model — the closed-phase conversation runs until no agent has more to say. - Agents grew `pending_alarm_claims()`: each agent checks its own graph for un-alarmed mutations and produces AlarmClaims directly. The driver loop calls this every iteration, so alarms emit and propagate in the same loop as regular emissions and gossip — no separate "alarm phase." - `Mothership.emit_alarms_from_detectors()` and the explicit `run_gossip_round()` step are no longer needed by callers; both are subsumed by the async loop. `run_gossip_round()` stays as a helper but tests no longer call it externally. What changed in the agent interface: - `CrisisAgent.next_turn(turn, received_claims)` becomes `try_emit()` — no arguments. Agents in an async network don't see a global tick. They decide based on their own internal state. - `CrisisAgent.observe(claim)` is the new optional callback the closed-phase loop uses to feed context into agents that care (overridden by LiveClaudeAgent to populate its prompt buffer). - `pending_alarm_claims()` is idempotent: an internal `_already_alarmed` set tracks claims this agent has emitted, so the loop calls it every step without flooding the network with duplicate alarms. What changed in the dataclass schema: - `AlarmClaim.detected_at_turn` -> `emitted_at_step`. The word "turn" implies a global clock; "step" is a per-agent sequence number used only for log ordering — local, not networked. - `ClosedPhaseEntry.turn` and `CrisisPhaseEntry.turn` -> `step`. Same rename, same reasoning. - `Scenario.closed_phase_turns` and `Scenario.crisis_phase_turns` are gone. The scenario no longer prescribes how many turns; it just provides agents and lets the async loop run them out. What changed in the CLI: - Phase 3 reports "drove to quiescence in N step(s)" with a breakdown of regular emissions / gossip transfers / alarm emissions, instead of "ran N turns". - `QuiescenceReport` (new dataclass) carries the run statistics back from `run_until_quiescent`/`run_closed_phase` — steps taken, emissions made, gossip transfers, alarm claims emitted, plus whether termination was via quiescence or max-step cap. New regression tests (`test_async_quiescence.py`): - `test_run_until_quiescent_terminates`: the loop must exit. - `test_two_runs_produce_identical_final_state`: determinism check — if anything in the loop depended on real wall time, this would fail. - `test_max_steps_bound_caps_runtime`: setting max_steps=1 exits immediately and `QuiescenceReport.reached_quiescence` reflects reality. - `test_no_turn_argument_exposed_to_agents`: introspects `CrisisAgent.try_emit` signature; fails if anyone re-adds a `turn` parameter. - `test_no_turn_field_on_alarmclaim`: introspects the dataclass fields; fails if `detected_at_turn` reappears. - `test_alarms_propagate_through_async_loop_alone`: the loop alone (no manual emit_alarms / run_gossip_round) ratifies an alarm. - `test_quiescence_report_counts_match_logs`: sanity check that the report's emission count equals the crisis log length. Suite: 163 -> 170 tests, all green in 0.79s. Behavioral end-state is identical to the previous (synchronous) version: same fact-check scenario, same byzantine equivocation, same proof JSON shape, same three signers, same quorum-met outcome. The difference is structural: the protocol now matches the paper's async shape, and a future port to actual TCP gossip + concurrent agents needs no change to this engine. CrisisViz: still untouched. The `crisis_data.json` pipeline that drives the visualizer is orthogonal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 20:06:56 +00:00
m.run_closed_phase()
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
m.open_boundary(s.byzantine_joiner)
crisis_agents: drop the wall-clock, drive asynchronously to quiescence The previous driver imposed a synchronous turn-counted clock that the Crisis paper explicitly forbids — Crisis is supposed to work in asynchronous P2P networks, with any synchronicity being virtual and derived inside the consensus algorithm from the DAG structure, not imposed externally by a coordinator. This commit removes the wall clock. What changed in the engine: - `Mothership.run_crisis_phase(num_turns, gossip_rounds_per_turn)` is replaced by `run_until_quiescent(max_steps=200)`. The loop interleaves three concerns on each iteration — emissions, gossip, and alarm emissions — until none make progress. Termination is by quiescence, not by a fixed turn count. `max_steps` is a safety bound (loop-iteration cap), not an exposed clock. - `Mothership.run_closed_phase(num_turns)` becomes `run_closed_phase(max_steps=50)`. Same quiescence model — the closed-phase conversation runs until no agent has more to say. - Agents grew `pending_alarm_claims()`: each agent checks its own graph for un-alarmed mutations and produces AlarmClaims directly. The driver loop calls this every iteration, so alarms emit and propagate in the same loop as regular emissions and gossip — no separate "alarm phase." - `Mothership.emit_alarms_from_detectors()` and the explicit `run_gossip_round()` step are no longer needed by callers; both are subsumed by the async loop. `run_gossip_round()` stays as a helper but tests no longer call it externally. What changed in the agent interface: - `CrisisAgent.next_turn(turn, received_claims)` becomes `try_emit()` — no arguments. Agents in an async network don't see a global tick. They decide based on their own internal state. - `CrisisAgent.observe(claim)` is the new optional callback the closed-phase loop uses to feed context into agents that care (overridden by LiveClaudeAgent to populate its prompt buffer). - `pending_alarm_claims()` is idempotent: an internal `_already_alarmed` set tracks claims this agent has emitted, so the loop calls it every step without flooding the network with duplicate alarms. What changed in the dataclass schema: - `AlarmClaim.detected_at_turn` -> `emitted_at_step`. The word "turn" implies a global clock; "step" is a per-agent sequence number used only for log ordering — local, not networked. - `ClosedPhaseEntry.turn` and `CrisisPhaseEntry.turn` -> `step`. Same rename, same reasoning. - `Scenario.closed_phase_turns` and `Scenario.crisis_phase_turns` are gone. The scenario no longer prescribes how many turns; it just provides agents and lets the async loop run them out. What changed in the CLI: - Phase 3 reports "drove to quiescence in N step(s)" with a breakdown of regular emissions / gossip transfers / alarm emissions, instead of "ran N turns". - `QuiescenceReport` (new dataclass) carries the run statistics back from `run_until_quiescent`/`run_closed_phase` — steps taken, emissions made, gossip transfers, alarm claims emitted, plus whether termination was via quiescence or max-step cap. New regression tests (`test_async_quiescence.py`): - `test_run_until_quiescent_terminates`: the loop must exit. - `test_two_runs_produce_identical_final_state`: determinism check — if anything in the loop depended on real wall time, this would fail. - `test_max_steps_bound_caps_runtime`: setting max_steps=1 exits immediately and `QuiescenceReport.reached_quiescence` reflects reality. - `test_no_turn_argument_exposed_to_agents`: introspects `CrisisAgent.try_emit` signature; fails if anyone re-adds a `turn` parameter. - `test_no_turn_field_on_alarmclaim`: introspects the dataclass fields; fails if `detected_at_turn` reappears. - `test_alarms_propagate_through_async_loop_alone`: the loop alone (no manual emit_alarms / run_gossip_round) ratifies an alarm. - `test_quiescence_report_counts_match_logs`: sanity check that the report's emission count equals the crisis log length. Suite: 163 -> 170 tests, all green in 0.79s. Behavioral end-state is identical to the previous (synchronous) version: same fact-check scenario, same byzantine equivocation, same proof JSON shape, same three signers, same quorum-met outcome. The difference is structural: the protocol now matches the paper's async shape, and a future port to actual TCP gossip + concurrent agents needs no change to this engine. CrisisViz: still untouched. The `crisis_data.json` pipeline that drives the visualizer is orthogonal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 20:06:56 +00:00
# One async run, no clock — alarms emit and propagate inside the loop
report = m.run_until_quiescent()
assert report.reached_quiescence
assert report.alarm_claims_emitted >= 3
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
threshold = quorum_for(m.boundary.size())
ratified_sets = [
m.ratified_alarms_from(name)
for name in ("agent_alpha", "agent_beta", "agent_gamma")
]
assert ratified_sets[0] == ratified_sets[1] == ratified_sets[2]
assert len(ratified_sets[0]) == 1
r = ratified_sets[0][0]
assert r.statement_id == "s03"
assert r.quorum_threshold == threshold
assert r.signer_count >= threshold
def test_proof_round_trips_through_json(self, tmp_path):
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
s = build_fact_check_scenario()
m = Mothership()
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
for a in s.honest_agents:
m.add_agent(a)
crisis_agents: drop the wall-clock, drive asynchronously to quiescence The previous driver imposed a synchronous turn-counted clock that the Crisis paper explicitly forbids — Crisis is supposed to work in asynchronous P2P networks, with any synchronicity being virtual and derived inside the consensus algorithm from the DAG structure, not imposed externally by a coordinator. This commit removes the wall clock. What changed in the engine: - `Mothership.run_crisis_phase(num_turns, gossip_rounds_per_turn)` is replaced by `run_until_quiescent(max_steps=200)`. The loop interleaves three concerns on each iteration — emissions, gossip, and alarm emissions — until none make progress. Termination is by quiescence, not by a fixed turn count. `max_steps` is a safety bound (loop-iteration cap), not an exposed clock. - `Mothership.run_closed_phase(num_turns)` becomes `run_closed_phase(max_steps=50)`. Same quiescence model — the closed-phase conversation runs until no agent has more to say. - Agents grew `pending_alarm_claims()`: each agent checks its own graph for un-alarmed mutations and produces AlarmClaims directly. The driver loop calls this every iteration, so alarms emit and propagate in the same loop as regular emissions and gossip — no separate "alarm phase." - `Mothership.emit_alarms_from_detectors()` and the explicit `run_gossip_round()` step are no longer needed by callers; both are subsumed by the async loop. `run_gossip_round()` stays as a helper but tests no longer call it externally. What changed in the agent interface: - `CrisisAgent.next_turn(turn, received_claims)` becomes `try_emit()` — no arguments. Agents in an async network don't see a global tick. They decide based on their own internal state. - `CrisisAgent.observe(claim)` is the new optional callback the closed-phase loop uses to feed context into agents that care (overridden by LiveClaudeAgent to populate its prompt buffer). - `pending_alarm_claims()` is idempotent: an internal `_already_alarmed` set tracks claims this agent has emitted, so the loop calls it every step without flooding the network with duplicate alarms. What changed in the dataclass schema: - `AlarmClaim.detected_at_turn` -> `emitted_at_step`. The word "turn" implies a global clock; "step" is a per-agent sequence number used only for log ordering — local, not networked. - `ClosedPhaseEntry.turn` and `CrisisPhaseEntry.turn` -> `step`. Same rename, same reasoning. - `Scenario.closed_phase_turns` and `Scenario.crisis_phase_turns` are gone. The scenario no longer prescribes how many turns; it just provides agents and lets the async loop run them out. What changed in the CLI: - Phase 3 reports "drove to quiescence in N step(s)" with a breakdown of regular emissions / gossip transfers / alarm emissions, instead of "ran N turns". - `QuiescenceReport` (new dataclass) carries the run statistics back from `run_until_quiescent`/`run_closed_phase` — steps taken, emissions made, gossip transfers, alarm claims emitted, plus whether termination was via quiescence or max-step cap. New regression tests (`test_async_quiescence.py`): - `test_run_until_quiescent_terminates`: the loop must exit. - `test_two_runs_produce_identical_final_state`: determinism check — if anything in the loop depended on real wall time, this would fail. - `test_max_steps_bound_caps_runtime`: setting max_steps=1 exits immediately and `QuiescenceReport.reached_quiescence` reflects reality. - `test_no_turn_argument_exposed_to_agents`: introspects `CrisisAgent.try_emit` signature; fails if anyone re-adds a `turn` parameter. - `test_no_turn_field_on_alarmclaim`: introspects the dataclass fields; fails if `detected_at_turn` reappears. - `test_alarms_propagate_through_async_loop_alone`: the loop alone (no manual emit_alarms / run_gossip_round) ratifies an alarm. - `test_quiescence_report_counts_match_logs`: sanity check that the report's emission count equals the crisis log length. Suite: 163 -> 170 tests, all green in 0.79s. Behavioral end-state is identical to the previous (synchronous) version: same fact-check scenario, same byzantine equivocation, same proof JSON shape, same three signers, same quorum-met outcome. The difference is structural: the protocol now matches the paper's async shape, and a future port to actual TCP gossip + concurrent agents needs no change to this engine. CrisisViz: still untouched. The `crisis_data.json` pipeline that drives the visualizer is orthogonal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 20:06:56 +00:00
m.run_closed_phase()
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
m.open_boundary(s.byzantine_joiner)
crisis_agents: drop the wall-clock, drive asynchronously to quiescence The previous driver imposed a synchronous turn-counted clock that the Crisis paper explicitly forbids — Crisis is supposed to work in asynchronous P2P networks, with any synchronicity being virtual and derived inside the consensus algorithm from the DAG structure, not imposed externally by a coordinator. This commit removes the wall clock. What changed in the engine: - `Mothership.run_crisis_phase(num_turns, gossip_rounds_per_turn)` is replaced by `run_until_quiescent(max_steps=200)`. The loop interleaves three concerns on each iteration — emissions, gossip, and alarm emissions — until none make progress. Termination is by quiescence, not by a fixed turn count. `max_steps` is a safety bound (loop-iteration cap), not an exposed clock. - `Mothership.run_closed_phase(num_turns)` becomes `run_closed_phase(max_steps=50)`. Same quiescence model — the closed-phase conversation runs until no agent has more to say. - Agents grew `pending_alarm_claims()`: each agent checks its own graph for un-alarmed mutations and produces AlarmClaims directly. The driver loop calls this every iteration, so alarms emit and propagate in the same loop as regular emissions and gossip — no separate "alarm phase." - `Mothership.emit_alarms_from_detectors()` and the explicit `run_gossip_round()` step are no longer needed by callers; both are subsumed by the async loop. `run_gossip_round()` stays as a helper but tests no longer call it externally. What changed in the agent interface: - `CrisisAgent.next_turn(turn, received_claims)` becomes `try_emit()` — no arguments. Agents in an async network don't see a global tick. They decide based on their own internal state. - `CrisisAgent.observe(claim)` is the new optional callback the closed-phase loop uses to feed context into agents that care (overridden by LiveClaudeAgent to populate its prompt buffer). - `pending_alarm_claims()` is idempotent: an internal `_already_alarmed` set tracks claims this agent has emitted, so the loop calls it every step without flooding the network with duplicate alarms. What changed in the dataclass schema: - `AlarmClaim.detected_at_turn` -> `emitted_at_step`. The word "turn" implies a global clock; "step" is a per-agent sequence number used only for log ordering — local, not networked. - `ClosedPhaseEntry.turn` and `CrisisPhaseEntry.turn` -> `step`. Same rename, same reasoning. - `Scenario.closed_phase_turns` and `Scenario.crisis_phase_turns` are gone. The scenario no longer prescribes how many turns; it just provides agents and lets the async loop run them out. What changed in the CLI: - Phase 3 reports "drove to quiescence in N step(s)" with a breakdown of regular emissions / gossip transfers / alarm emissions, instead of "ran N turns". - `QuiescenceReport` (new dataclass) carries the run statistics back from `run_until_quiescent`/`run_closed_phase` — steps taken, emissions made, gossip transfers, alarm claims emitted, plus whether termination was via quiescence or max-step cap. New regression tests (`test_async_quiescence.py`): - `test_run_until_quiescent_terminates`: the loop must exit. - `test_two_runs_produce_identical_final_state`: determinism check — if anything in the loop depended on real wall time, this would fail. - `test_max_steps_bound_caps_runtime`: setting max_steps=1 exits immediately and `QuiescenceReport.reached_quiescence` reflects reality. - `test_no_turn_argument_exposed_to_agents`: introspects `CrisisAgent.try_emit` signature; fails if anyone re-adds a `turn` parameter. - `test_no_turn_field_on_alarmclaim`: introspects the dataclass fields; fails if `detected_at_turn` reappears. - `test_alarms_propagate_through_async_loop_alone`: the loop alone (no manual emit_alarms / run_gossip_round) ratifies an alarm. - `test_quiescence_report_counts_match_logs`: sanity check that the report's emission count equals the crisis log length. Suite: 163 -> 170 tests, all green in 0.79s. Behavioral end-state is identical to the previous (synchronous) version: same fact-check scenario, same byzantine equivocation, same proof JSON shape, same three signers, same quorum-met outcome. The difference is structural: the protocol now matches the paper's async shape, and a future port to actual TCP gossip + concurrent agents needs no change to this engine. CrisisViz: still untouched. The `crisis_data.json` pipeline that drives the visualizer is orthogonal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 20:06:56 +00:00
m.run_until_quiescent()
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
r = m.ratified_alarms_from("agent_alpha")[0]
proof = build_proof(r)
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
out = tmp_path / "proof.json"
out.write_text(proof.to_json())
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()
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
# The five named phases appear
for phase in ("Phase 1", "Phase 2", "Phase 3",
"Phase 4", "Phase 5", "Phase 6"):
assert phase in captured.out
# The chokepoint-free marker prints
assert "no chokepoint" in captured.out
# Exactly one proof file written
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
proofs = list(tmp_path.glob("proof_*.json"))
assert len(proofs) == 1
obj = json.loads(proofs[0].read_text())
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
assert obj["statement_id"] == "s03"
assert len(obj["signer_process_id_hexes"]) >= 3
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
def test_cli_verify_passes_on_valid_proof(self, tmp_path, capsys):
cli_main(["demo", "--scenario", "fact_check", "--out-dir", str(tmp_path)])
proof_path = next(tmp_path.glob("proof_*.json"))
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
capsys.readouterr()
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
exit_code = cli_main(["verify", str(proof_path)])
assert exit_code == 0
out = capsys.readouterr().out
Decentralize crisis_agents: agents own graphs, detect locally, vote by quorum The previous design routed every Crisis message through a `Mothership` that also held every agent's LamportGraph, ran the byzantine scan from a privileged vantage, and built proofs from its own view. That made the mothership a chokepoint — exactly what a BFT layer is supposed to remove. This commit redistributes responsibility along the lines you'd expect from a real open protocol: Each `CrisisAgent` now owns: - its own `LamportGraph` (the agent's view of the network) - `emit_claim(claim) → Message`: wraps a Claim into a fully-valid Crisis Message built from the agent's OWN graph state, with chain link + cross references + mined PoW nonce - `receive(message)`: extends my graph if integrity holds; idempotent - `gossip_to(peer) → int`: shares everything I have with peer until quiescence (Algorithm 4 in the paper, in-process flavor) - `detect_mutations() → list[LocalAlarm]`: scans MY graph for same-id spacelike vertex pairs via the existing `LamportGraph.find_mutations`, filtered by application-layer `statement_id` so cross-detector AlarmClaims canonicalize The `Mothership` shrinks to coordinator-only: - bootstrap (register honest agents; trigger boundary open with a joiner) - clock (call each agent's `next_turn()` per turn) - first-hop routing (sender's emission → declared target subset) - all-pairs gossip rounds between turns - emit_alarms_from_detectors(): poll each agent for its LocalAlarms, wrap any returned alarms into AlarmClaim payloads, broadcast them as Crisis Messages over the gossip layer Gone (regression-tested in `test_no_chokepoint.py`): - `Mothership._graphs`, `Mothership.all_graphs()`, `Mothership.graph_of()` - `alarm.scan_for_mutations(mothership)` - any path where the mothership reads an agent's internal state New voting layer (`crisis_agents/vote.py`): - `AlarmClaim`: a Crisis-payload dataclass discriminated by `kind="alarm"`. Wraps the accused process_id, statement_id, witness_digests, and detection turn. Round-trips through JSON same as Claim. - `quorum_for(n) = ceil(2n/3)`: classic BFT threshold. - `tally_alarms(graph, threshold)`: groups AlarmClaim vertices by (accused, statement_id, witness_pair), counts unique signer process_ids, ratifies groups meeting the threshold. Deterministic ordering so two equal graphs produce equal `RatifiedAlarm` lists. - `RatifiedAlarm`: the network-level consensus on byzantine behavior. Multi-signer proofs (`crisis_agents/proof.py`): - schema_version bumped 1 → 2. - ProofDocument now embeds every signer's process_id_hex and the quorum threshold that was met. Self-consistency check enforces distinct signers, witness pairs, and signer count ≥ threshold. Byzantine scenario rewrite: - `MockByzantineAgent` now takes an `intro_claim` for its first turn (a benign broadcast). The intro is technically necessary: the agent's two contradictory variants both chain to the intro vertex, so they can propagate through gossip — without it, the second variant would fail the chain constraint in any graph already holding the first. - `fact_check` scenario: closed phase still has 3 honest agents emitting 6 claims each into the closed log; Crisis phase grew to 2 turns (intro + equivocation) so the byzantine can establish its same-id anchor before equivocating. End-to-end CLI output reframed around six phases: 1. closed team (no Crisis) 2. boundary opens 3. emission + gossip 4. decentralized detection (each agent reports its own findings) 5. alarms emitted + gossiped + ratified by quorum 6. proof emission Tests (51 fresh + 5 carried over for boundary): - `test_mothership.py`: per-agent graph ownership, broadcast vs. targeted delivery semantics, gossip propagation, regression guards against the removed centralization attributes. - `test_alarm.py`: every honest agent independently detects the same mutation; the byzantine doesn't detect itself; witness pairs are canonical across detectors. - `test_vote.py`: AlarmClaim round-trip, quorum formulas, tally determinism, mothership convenience method matches direct tallying. - `test_proof.py`: build_proof from RatifiedAlarm; multi-signer JSON round-trip; tampered-witness/below-quorum/duplicate-signer rejection. - `test_no_chokepoint.py` (the centerpiece): after the full lifecycle, every honest agent's ratified-alarm set is byte-identical. A single byzantine accuser alone cannot ratify. Forbidden attributes don't exist on Mothership. Full suite: 163 tests, all green in 0.80s. CrisisViz: untouched by this refactor. The `crisis_data.json` pipeline the visualizer consumes is produced by the orthogonal `crisis.demo.Simulation`, which this commit doesn't touch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 19:55:49 +00:00
assert "self-consistent: True" in out
Add crisis_agents — Crisis as a coordination layer for AI agent teams 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>
2026-05-14 14:38:11 +00:00
def test_cli_unknown_scenario(self, capsys):
exit_code = cli_main(["demo", "--scenario", "nonexistent"])
assert exit_code == 2