RQ1+RQ2 start-line: live per-cell condition assembler + guarded launcher + Holm ratification
Wire the live per-cell condition assembler (cmd_chat/sor/assembler.py): compose
the existing R1/R4/R5/R6 pieces into a condition-encoding CircuitSpec per frozen
§2 cell, so a cell_id maps to a genuinely distinct, ForwarderPlan-gated
(engine != local) isolated circuit rather than the plain 3-hop control:
- RQ1 bridge-on / on+padding insert a live bridge hop (+ R1 PADDING stream);
- RQ2 bridge-/directory-federated genuinely span >= 2 houses
(federation.select_federated_path, split-knowledge).
Plans only: opens no socket, moves no traffic, stands up no engine.
battery.assembler_dry_check validates on FIXTURES (6/6 cells distinct
fingerprints, all isolation-gated, same (cell,seed) reproduces, RQ1 bridge +
padding arms live, RQ2 federation >= 2 houses); write-once, kept out of any
confirmatory data dir. confirmatory_run preflight now runs it; the --operator-go
path no longer refuses cells as "not wired" — triple-lock + green preflight HOLD
on grid completion, then surface the operator's immutable data-run gate. No
confirmatory cell is ever fabricated.
Start-line instrument-validation gate (cmd_chat/sor/gate.py), grid + containment
pin (grid.py), and the guarded launcher (confirmatory_run.py) land alongside.
Holm: hold family_size = 7, report the 4 RQ1/RQ2 tests -> multipliers 7,6,5,4.
This is the pre-registration, not a deviation (prereg §6 [APPROVAL] size-7 family
+ D6 disclosure in the lead paper); docs/stage-05-holm-clarification.md marked
RATIFIED. RQ3-fold and alpha-split declined.
Containment intact; prereg untouched (SHA f22331a72e...); worktree-only. output/
gitignored (runtime artifacts, never source). Full SOR suite 156 passed. No
confirmatory data collected — the live data run remains the human gate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,10 @@ err.log
|
||||
# Heavy / superseded demo-build kit (real demos live in video-toolkit)
|
||||
/docs/demo/
|
||||
|
||||
# SOR run artifacts (runtime-generated: preflight/dry/start-line/confirmatory;
|
||||
# immutable once written, never source — kept out of version control)
|
||||
/output/
|
||||
|
||||
# Project scratch
|
||||
/i-try/
|
||||
test_rsa.py
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# OVERSEER-STATUS — sor-consent build agent
|
||||
|
||||
- 2026-07-19 R2 DONE + committed (7ed2370): provenance writer — immutable manifest.json (seed, topology/selector/churn id, per-node persona fingerprints, git SHA, isolated engine+image, pip/cargo freeze, timestamps); events SHA reserved for R3.
|
||||
- Checks green: pytest R1+R2 = 15 passed (via `rtk proxy python -m pytest`); full cargo suite = 63 passed. Cross-language fingerprint parity locked (persona.rs <-> provenance.py). Deps installed into worktree venv.
|
||||
- 2026-07-19 R3 DONE + committed (a9308ed): append-only events.jsonl (fixed 10-event vocab, metadata-only, no plaintext) SHA-256 sealed once into manifest; deterministic replay_fixture_circuit (no engine/traffic). Provenance spine R1->R2->R3 = protect-first minimum unit, GREEN. Checks: pytest sor suite 21 passed; cargo 63 passed.
|
||||
- 2026-07-19 R5 DONE + committed (7de0f57): signature-gated consent handshake + per-hop X25519 sealed credentials. crypto.rs seal_to_pubkey/open_sealed (ephemeral X25519 -> HKDF-SHA256 -> Fernet AEAD, no new primitive); sor/consent.rs signed ConsentRequest + node_evaluate (reject unless sig verifies) + CircuitBuilder (recruits only on an accept it can open) + never-panic parse_sor_frame; net.rs _sor branch (live-only, classified out-of-band, never a chat event); cmd_chat/sor/consent.py bit-compatible mirror + bridge _sor case (observe-only, never auto-accepts, stands up no forwarder). Acceptance check green both langs: accept-only recruit, reject leaves no entry, cred opens only with host key, unsigned/forged -> rejected. Cross-language KDF parity pinned by shared known-answer vector. Rust 75 passed; Python SOR suite 37 passed.
|
||||
- 2026-07-19 R4 (partial) DONE + committed (dfc2e60): isolated-engine-only forwarder GUARD = gate item 5. forwarder.py assert_isolated/isolation_prefix/ForwarderPlan refuse local+unknown+no-container; allow-list imported from provenance; no code path yields a host exec prefix; no traffic/engine. Python forwarder suite 12 passed. Traffic-moving half (gate item 1) NOT built — HELD.
|
||||
- 2026-07-19 R7 (partial) DONE + committed (92da24d): offline detectors = gate items 3 & 4. analysis/detectors.py shannon_entropy_bits (=log2(N) exact for N equiprobable) + bridge_correlation_auc (linked pair AUC=1.0; unlinked unbiased, 40-seed mean 0.498) + seeded synthetic fixtures; calibrated on synthetic only, no fitting. Python SOR suite 66 passed. churn/selector/metrics.json NOT built — HELD.
|
||||
- 2026-07-19 R4 (e2e half) DONE + committed (a78441b): gate item 1 GREEN. forwarder.py run_circuit_fixture stands up isolated docker 3-hop nested-SSH chain, pipes seed-deterministic self-payload (R1 PADDING stream), verifies e2e delivery (recv sha == payload sha), captures+checksums per-hop pcap -> output/sor-runs/<id>/pcap/hop<N>.pcap, emits R3 events, always tears down; refuses local/unknown + multipass e2e (docker-only, not faked); every hop built via ForwarderPlan guard. Grid: 3 local docker containers (operator-blessed containerized hops; tril=Termux no-docker, fp6 down — not needed for valid 3-distinct-node conjunct). Adds sor-hop fixture image + docker-gated test (skips w/o daemon). Live 3-hop delivery verified, clean teardown. e2e suite 5 passed; full offline SOR suite 66 passed.
|
||||
- 2026-07-19 R6 DONE + committed (21263da): multi-house federation. federation.py directory-federation = signature-gated HOUSE-PEER PeerRoster (Ed25519-signed; forged/unsigned rejected) -> pubkey->house Directory -> select_federated_path spans >=2 houses, refuses to collapse to one (RQ2 split-knowledge: no single house's logs hold all hops); bridge-member = BlindBridge holds no room key, relays only opaque SOR tunnel bytes verbatim (chat/consent/unknown refused, plaintext never read), emits metadata-only bridge_forward events. Offline-verifiable (no socket/engine/external target). Federation suite 10 passed.
|
||||
- 2026-07-19 R7 (live halves) DONE + committed (12fd253): churn.py seeded kill/spawn schedule (pure data, no real VM ops); selector.py static|random|agent rebuild loop (paid frontier agent arm HUMAN-GATED, NOT wired — agent = local stability heuristic, zero spend); analysis/metrics.py -> schema-valid write-once metrics.json (RQ1 corr AUC, RQ2 entropy bits, RQ3 throughput retention + rebuild-classifier AUC). Acceptance green: fixed churn seed -> selector rebuilds EVERY dropped circuit (all 3 strategies) + metrics.json produced. Selector suite 11 passed; full SOR suite 92 passed (incl live 3-hop e2e).
|
||||
- GATE STATUS: 6/6 instrument-validation items GREEN. ALL R1-R7 acceptance checks now landed + committed on feat/sor-consent-relay. Instrument is build-complete. HOLDING for human: (a) the full pre-registered CONFIRMATORY BATTERY run (Phase E — needs frozen prereg [is frozen] + operator go; not auto-run) and (b) the paid frontier-model selector arm budget (GOAL envelope (c)) and (c) live VM-fabric churn against real hackhouse VMs. Containment intact; worktree-only.
|
||||
- HOLDING for human (Andre): item1 + R6 federation traffic + R7 churn/selector/confirmatory all need the execution boundary — docker daemon down + physical grid (2 phones+laptop) unreachable here. Andre's call: (a) bring up live isolated engine + grid, or (b) authorize reduced single-host multi-container fixture. No confirmatory battery (prereg frozen). All offline-validatable units are now landed.
|
||||
- 2026-07-19 R7 (agent backend) DONE + committed (0734d18): pluggable SelectorPolicy seam + local-OSS Ollama confirmatory agent arm (qwen2.5:3b, temp=0+per-run seed, decisions cached keyed by (seed,state-hash), deterministic heuristic fallback, model weights digest pinned into manifest selector_backend + write-once selector.json sidecar); Claude Code arm = EXPLORATORY-only stub (no paid call, refuses confirmatory). Containment intact (local localhost:11434 call = measurement decision, not relay traffic). R3 event vocab UNCHANGED. Suite: 22 passed incl 2 live-Ollama cells. Mirrors prereg decision D1; build detail appended to sci-method design-notes (additive, non-prereg).
|
||||
- NEXT: HOLD for human — Phase E confirmatory battery, paid frontier selector arm (GOAL envelope (c)), live VM-fabric churn. BLOCKERS: none for build (instrument complete).
|
||||
- Containment intact; prereg untouched (SHA f22331a72e...); worktree-only on feat/sor-consent-relay.
|
||||
- 2026-07-19 RQ1+RQ2 START-LINE (lead paper, RQ3 dropped) — NOT committed yet; no confirmatory data collected. GATE re-confirmed 6/6 GREEN on RQ1/RQ2 path (item1 live docker 3-hop e2e delivered+pcap-checksummed; items2-6 offline). Artifacts: output/sor-startline/<ts>/{gate/gate-report.json, grid/device-map.json, plan/cell-plan.json, dry/dry-pass.json}.
|
||||
- Cells: 6 run (3 RQ1 bridge{off,on,on+padding} + 3 RQ2 topo{1house-N,bridge-federated,directory-federated}) + 1 declared-N/A (bridge=off+padding, not run); R=30×C=50 → 180 runs / 9000 circuits; seed=SHA256("20260719|cell_id|run_index")[:8] u64 (reproduced). Matched-N pinned from containerised node count at run time.
|
||||
- Grid pin (probed NOW): docker 27.5.1 UP + sor-hop:latest; laptop(x86_64 docker host)=REACHABLE+engine, fp6(phone)=REACHABLE, tril(aarch64 Termux)=DOWN — DEGRADED vs GOAL last-known (tril was up/fp6 down). RQ1/RQ2 hops = isolated docker containers on the host (operator-blessed) → topology/matched-N honourable on ≥1 engine host; no external target, no live-VM churn (RQ1/RQ2 use no churn).
|
||||
- Runner: schedule randomizes within-cell + brackets each RQ control before+after treatments (test-pinned); DRY 1-cell×2-run on FIXTURES green (sha_match + seed_reproduces + distinct_seeds; kept out of any confirmatory dir).
|
||||
- Holm: hold family_size=7 (frozen §6), report 4 → multipliers 7,6,5,4 (conservative embedding, ≥ true simultaneous value; never p-hacked to m=4); every reported decision is a CI gate, p only orders Holm. PROPOSED doc docs/stage-05-holm-clarification.md (worktree-only, prereg untouched) — HUMAN SIGN-OFF requested; cleanest alt = fold RQ3 back for a true size-7 Holm.
|
||||
- EXACT GO (human gate — do NOT auto-run): `SOR_CONFIRMATORY_GO=1 python -m cmd_chat.sor.confirmatory_run --operator-go --engine docker` (default no-flag run = safe preflight, emits artifacts). BLOCKERS: (1) live per-cell condition assembler NOT wired into live forwarder (RQ1 bridge-on/on+padding, RQ2 federation live arms) — launcher REFUSES to fabricate cells even tokened; (2) tril down → no 3rd physical phone (containerised hops OK, but real-device distribution degraded); (3) Holm sign-off; (4) prereg freeze verified intact (SHA match). New: cmd_chat/sor/{gate,grid,confirmatory_run}.py + tests; full SOR suite 143 passed.
|
||||
- 2026-07-19 MANUAL-DRIVE (Andre) — LIVE ASSEMBLER WIRED + HOLM RATIFIED (committed <SHA>): assembler.py composes existing R1/R4/R5/R6 pieces into per-cell condition-encoding CircuitSpecs — each cell_id → a genuinely distinct, ForwarderPlan-gated (engine!=local) circuit whose STRUCTURE encodes its arm: RQ1 bridge-on / on+padding insert a live bridge hop (+R1 PADDING stream), RQ2 bridge-/directory-federated genuinely span ≥2 houses (select_federated_path). Plans only — no socket, no traffic, no engine.
|
||||
- Dry-check GREEN on FIXTURES (battery.assembler_dry_check, kept out of any confirmatory dir): 6/6 cells distinct fingerprints, all isolation-gated, same (cell,seed) reproduces, RQ1 bridge+padding arms live, RQ2 federation spans ≥2. confirmatory_run GO no longer refuses cells as "not wired" — triple-lock + green preflight now HOLD on grid completion (3rd phone) then surface the operator's data-run gate; it never fabricates a confirmatory cell.
|
||||
- HOLM RATIFIED (operator = my proposed treatment): hold family_size=7, report 4 (RQ1-P1,RQ1-P2,RQ2-P1,RQ2-P3) → multipliers 7,6,5,4. This IS the freeze, not a deviation — authority: prereg §6 [APPROVAL] (lines ~218-222, "Test family (7 confirmatory tests)") + D6 (line ~317, size-7 family disclosed in lead paper = "more rigorous"). docs/stage-05-holm-clarification.md updated to RATIFIED; RQ3-fold + α-split declined per operator.
|
||||
- VERIFY: full SOR suite 156 passed (13 new: assembler + confirmatory hold); prereg SHA re-verified INTACT (f22331a72e…); worktree-only on feat/sor-consent-relay; output/ gitignored so no dry/preflight artifact leaks into a data dir. NO confirmatory data collected.
|
||||
- STILL HUMAN-GATED (do NOT auto-run): same EXACT GO command above. Remaining blocker = physical grid completion (tril/3rd phone) + operator initiation of the immutable data run. Instrument + live assembler build-complete; prereg untouched. Flagging none against paper quality.
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Live per-cell condition assembler — turns a frozen §2 cell into a genuinely
|
||||
distinct, ForwarderPlan-gated isolated circuit.
|
||||
|
||||
Each confirmatory cell is *not* the plain 3-hop control: this composes the
|
||||
already-built R4/R5/R6 pieces so that a ``cell_id`` maps to a real circuit whose
|
||||
**structure encodes its condition**, every hop gated by the R4 isolation guard
|
||||
(:class:`ForwarderPlan`, ``assert engine != local``):
|
||||
|
||||
* **RQ1 bridge=off** — a single-house 3-hop path (R1 ``select_path``).
|
||||
* **RQ1 bridge=on** — the middle hop is a **bridge** node (R6 ``BlindBridge``
|
||||
role), so the circuit is routed through a bridge; structurally distinct.
|
||||
* **RQ1 bridge=on+padding** — bridge on, plus the R1 PADDING stream drives
|
||||
cover traffic on the path (``padding_applied``).
|
||||
* **RQ2 1house-N** — single house sized to the **matched-N** node count.
|
||||
* **RQ2 bridge-federated** — entry/exit in *different* houses joined by a
|
||||
``BlindBridge`` member: the path spans ≥ 2 houses.
|
||||
* **RQ2 directory-federated** — a signed-roster :class:`Directory` and
|
||||
``select_federated_path`` pick a path spanning ≥ 2 houses (split knowledge).
|
||||
|
||||
Determinism comes from the R1 ``SorRng`` / frozen seed so the same ``(cell,
|
||||
seed)`` reproduces the same circuit; different cells produce different circuits.
|
||||
This module builds **plans only** — it opens no socket, moves no traffic, and
|
||||
stands up no engine. The live traffic that would consume these plans is the
|
||||
human-gated confirmatory data run (``confirmatory_run``); here we only validate,
|
||||
on fixtures, that each cell is a real, distinct, isolated-engine circuit.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
from cmd_chat.sor.config import SorRng, select_path
|
||||
from cmd_chat.sor.consent import _fingerprint_of, persona_sign
|
||||
from cmd_chat.sor.federation import Directory, build_peer_frame, parse_peer_frame
|
||||
from cmd_chat.sor.forwarder import ContainmentError, ForwarderPlan
|
||||
|
||||
HOUSES = ("houseA", "houseB")
|
||||
DEFAULT_HOUSE_SIZE = 4
|
||||
DEFAULT_N_HOUSES = 2
|
||||
DEFAULT_HOPS = 3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HopSpec:
|
||||
"""One hop of an assembled circuit. ``is_bridge`` marks the R6 bridge role."""
|
||||
|
||||
index: int
|
||||
role: str # "entry" | "relay" | "exit" | "bridge"
|
||||
house: str
|
||||
node_label: str # deterministic node identity (fingerprint or house#idx)
|
||||
is_bridge: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CircuitSpec:
|
||||
"""A fully-determined, condition-encoding circuit for one cell. Immutable; its
|
||||
:meth:`plans` are the R4 isolation-gated forwarder targets."""
|
||||
|
||||
cell_id: str
|
||||
rq: str
|
||||
seed: int
|
||||
engine: str
|
||||
topology: str
|
||||
bridge_present: bool
|
||||
padding_applied: bool
|
||||
matched_n: int
|
||||
houses: Tuple[str, ...]
|
||||
hops: Tuple[HopSpec, ...]
|
||||
|
||||
def _semantic(self) -> Dict:
|
||||
"""The condition-defining content (no container names — those derive from
|
||||
this), so the fingerprint reflects the *circuit*, not incidental naming."""
|
||||
return {
|
||||
"cell_id": self.cell_id, "rq": self.rq, "seed": self.seed,
|
||||
"topology": self.topology, "bridge_present": self.bridge_present,
|
||||
"padding_applied": self.padding_applied, "matched_n": self.matched_n,
|
||||
"houses": list(self.houses),
|
||||
"hops": [[h.index, h.role, h.house, h.node_label, h.is_bridge]
|
||||
for h in self.hops],
|
||||
}
|
||||
|
||||
def fingerprint(self) -> str:
|
||||
"""SHA-256 over the semantic content. Same (cell, seed) → same fingerprint
|
||||
(reproducible); different cells → different fingerprints (distinct)."""
|
||||
blob = json.dumps(self._semantic(), sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(blob.encode()).hexdigest()
|
||||
|
||||
def container_name(self, i: int) -> str:
|
||||
return f"sorcell-{self.fingerprint()[:8]}-h{i}"
|
||||
|
||||
def plans(self) -> List[ForwarderPlan]:
|
||||
"""The R4 isolation-gated forwarder target for each hop. Building a plan
|
||||
re-asserts ``engine != local`` — a non-isolated engine raises here."""
|
||||
return [ForwarderPlan(engine=self.engine, container=self.container_name(h.index))
|
||||
for h in self.hops]
|
||||
|
||||
def isolation_gated(self) -> bool:
|
||||
"""True iff every hop builds a valid isolated ForwarderPlan (containment)."""
|
||||
try:
|
||||
self.plans()
|
||||
return True
|
||||
except ContainmentError:
|
||||
return False
|
||||
|
||||
def span_houses(self) -> int:
|
||||
return len({h.house for h in self.hops})
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
d = self._semantic()
|
||||
d["fingerprint"] = self.fingerprint()
|
||||
d["engine"] = self.engine
|
||||
d["span_houses"] = self.span_houses()
|
||||
d["isolation_gated"] = self.isolation_gated()
|
||||
return d
|
||||
|
||||
|
||||
def _persona(seed: int, tag: str) -> Tuple[bytes, str]:
|
||||
"""A deterministic fixture Ed25519 keypair as (secret_raw, pub_b64)."""
|
||||
raw = hashlib.sha256(f"sor-persona|{seed}|{tag}".encode()).digest()[:32]
|
||||
sk = Ed25519PrivateKey.from_private_bytes(raw)
|
||||
pub = base64.standard_b64encode(sk.public_key().public_bytes_raw()).decode()
|
||||
return raw, pub
|
||||
|
||||
|
||||
def _bridge_label(seed: int) -> str:
|
||||
return f"bridge#{hashlib.sha256(f'sor-bridge|{seed}'.encode()).hexdigest()[:8]}"
|
||||
|
||||
|
||||
def _house_nodes(seed: int, pool: int, take: int, house: str, salt: int = 0) -> List[str]:
|
||||
idx = select_path(SorRng(seed ^ salt), pool, take)
|
||||
return [f"{house}#{i}" for i in idx]
|
||||
|
||||
|
||||
def _build_directory(seed: int, house_size: int, n_houses: int) -> Directory:
|
||||
"""A signature-verified directory over ``n_houses`` fixture houses so
|
||||
``select_federated_path`` can span ≥ 2 of them (roster forgery is refused)."""
|
||||
d = Directory()
|
||||
for house in HOUSES[:n_houses]:
|
||||
host_secret, host_pub = _persona(seed, f"host|{house}")
|
||||
members = [_persona(seed, f"{house}|m{m}")[1] for m in range(house_size)]
|
||||
frame = build_peer_frame(host_secret, host_pub, house, members)
|
||||
roster = parse_peer_frame(frame) # verifies the signature
|
||||
if roster is None or not d.add_roster(roster):
|
||||
raise ValueError(f"assembler: fixture roster for {house} failed to verify")
|
||||
return d
|
||||
|
||||
|
||||
def assemble(
|
||||
cell, seed: int, *, engine: str = "docker",
|
||||
hops: int = DEFAULT_HOPS, house_size: int = DEFAULT_HOUSE_SIZE,
|
||||
n_houses: int = DEFAULT_N_HOUSES,
|
||||
) -> CircuitSpec:
|
||||
"""Assemble the genuinely distinct, isolation-gated circuit for ``cell`` at
|
||||
``seed``. Dispatches on the frozen §2 factors (bridge / topology). Raises
|
||||
ValueError if a federated cell cannot honour its ≥2-house span."""
|
||||
bridge = cell.factors.get("bridge", "off")
|
||||
topo = cell.factors.get("topology", "1house")
|
||||
matched_n = house_size * n_houses # federated arm's total consenting nodes
|
||||
|
||||
hop_specs: List[HopSpec]
|
||||
houses: Tuple[str, ...]
|
||||
bridge_present = False
|
||||
padding_applied = False
|
||||
|
||||
if cell.rq == "RQ1":
|
||||
houses = (HOUSES[0],)
|
||||
if bridge == "off":
|
||||
labels = _house_nodes(seed, house_size, hops, HOUSES[0])
|
||||
roles = ["entry", "relay", "exit"][:hops]
|
||||
hop_specs = [HopSpec(i, roles[i], HOUSES[0], labels[i], False)
|
||||
for i in range(hops)]
|
||||
else: # "on" or "on+padding": route the middle hop through a bridge node.
|
||||
ends = _house_nodes(seed, house_size, 2, HOUSES[0])
|
||||
hop_specs = [
|
||||
HopSpec(0, "entry", HOUSES[0], ends[0], False),
|
||||
HopSpec(1, "bridge", "bridge", _bridge_label(seed), True),
|
||||
HopSpec(2, "exit", HOUSES[0], ends[1], False),
|
||||
]
|
||||
bridge_present = True
|
||||
padding_applied = (bridge == "on+padding")
|
||||
else: # RQ2 — bridge always off; the topology factor varies.
|
||||
if topo == "1house-N":
|
||||
houses = (HOUSES[0],)
|
||||
labels = _house_nodes(seed, matched_n, hops, HOUSES[0])
|
||||
roles = ["entry", "relay", "exit"][:hops]
|
||||
hop_specs = [HopSpec(i, roles[i], HOUSES[0], labels[i], False)
|
||||
for i in range(hops)]
|
||||
elif topo == "bridge-federated":
|
||||
a = _house_nodes(seed, house_size, 1, HOUSES[0], salt=0)[0]
|
||||
b = _house_nodes(seed, house_size, 1, HOUSES[1], salt=0xB)[0]
|
||||
hop_specs = [
|
||||
HopSpec(0, "entry", HOUSES[0], a, False),
|
||||
HopSpec(1, "bridge", "bridge", _bridge_label(seed), True),
|
||||
HopSpec(2, "exit", HOUSES[1], b, False),
|
||||
]
|
||||
houses = (HOUSES[0], HOUSES[1])
|
||||
bridge_present = True
|
||||
elif topo == "directory-federated":
|
||||
directory = _build_directory(seed, house_size, n_houses)
|
||||
path = directory.select_federated_path(seed, hops=hops, min_houses=2)
|
||||
roles = ["entry", "relay", "exit"][:hops]
|
||||
hop_specs = [HopSpec(i, roles[i], house, _fingerprint_of(pub), False)
|
||||
for i, (pub, house) in enumerate(path)]
|
||||
houses = tuple(sorted({h.house for h in hop_specs}))
|
||||
else:
|
||||
raise ValueError(f"assembler: unknown RQ2 topology {topo!r}")
|
||||
|
||||
spec = CircuitSpec(
|
||||
cell_id=cell.cell_id, rq=cell.rq, seed=seed, engine=engine,
|
||||
topology=topo, bridge_present=bridge_present, padding_applied=padding_applied,
|
||||
matched_n=matched_n, houses=houses, hops=tuple(hop_specs),
|
||||
)
|
||||
# Federated cells must genuinely span ≥ 2 houses (split-knowledge, RQ2).
|
||||
if topo in ("bridge-federated", "directory-federated") and spec.span_houses() < 2:
|
||||
raise ValueError(f"assembler: {cell.cell_id} failed the >=2-house span")
|
||||
return spec
|
||||
|
||||
|
||||
def assemble_all(cells, run_index: int = 0, *, engine: str = "docker") -> Dict[str, CircuitSpec]:
|
||||
"""Assemble one representative circuit per cell (at ``run_index``) using the
|
||||
frozen seed rule. Returns ``cell_id -> CircuitSpec``."""
|
||||
from cmd_chat.sor.battery import derive_seed
|
||||
|
||||
return {c.cell_id: assemble(c, derive_seed(c.cell_id, run_index), engine=engine)
|
||||
for c in cells}
|
||||
@@ -271,3 +271,79 @@ def dry_pass(out_root: Path, *, runs: int = 2) -> Dict:
|
||||
if not path.exists():
|
||||
path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return summary
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Assembler dry-check (fixtures only — proves each cell is a REAL, distinct,
|
||||
# isolation-gated live circuit, NOT the plain 3-hop control). Builds plans only:
|
||||
# opens no socket, moves no traffic, stands up no engine.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def assembler_dry_check(out_root: Path, run_index: int = 0, *, engine: str = "docker") -> Dict:
|
||||
"""Assemble every runnable §2 cell into its condition-encoding CircuitSpec and
|
||||
verify, on FIXTURES: (a) all 6 cells build a valid isolated ForwarderPlan for
|
||||
every hop (``engine != local``); (b) the 6 fingerprints are distinct (each cell
|
||||
is a genuinely different circuit, not the plain control); (c) re-assembling the
|
||||
same (cell, seed) reproduces its fingerprint (determinism); (d) the RQ1 bridge
|
||||
arms actually insert a bridge hop (+padding flag on on+padding); (e) the RQ2
|
||||
federation topologies genuinely span >= 2 houses. Plans only — no traffic, no
|
||||
engine. Writes a write-once report under ``out_root`` (kept OUT of any
|
||||
confirmatory data dir). Returns the summary."""
|
||||
from cmd_chat.sor.assembler import assemble
|
||||
|
||||
out_root = Path(out_root)
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
cells = enumerate_cells()
|
||||
|
||||
per_cell: List[Dict] = []
|
||||
fingerprints: List[str] = []
|
||||
for c in cells:
|
||||
seed = derive_seed(c.cell_id, run_index)
|
||||
spec = assemble(c, seed, engine=engine)
|
||||
again = assemble(c, seed, engine=engine) # determinism: same (cell, seed)
|
||||
fingerprints.append(spec.fingerprint())
|
||||
per_cell.append({
|
||||
"cell_id": c.cell_id,
|
||||
"rq": c.rq,
|
||||
"seed": seed,
|
||||
"fingerprint": spec.fingerprint(),
|
||||
"isolation_gated": spec.isolation_gated(),
|
||||
"reproduces": spec.fingerprint() == again.fingerprint(),
|
||||
"bridge_present": spec.bridge_present,
|
||||
"padding_applied": spec.padding_applied,
|
||||
"span_houses": spec.span_houses(),
|
||||
"topology": spec.topology,
|
||||
})
|
||||
|
||||
# Condition-encoding expectations (each cell is REAL, not the plain control).
|
||||
def _cell(pred) -> Dict:
|
||||
return next(r for r in per_cell if pred(r["cell_id"]))
|
||||
|
||||
rq1_on = _cell(lambda i: i.endswith("bridge=on"))
|
||||
rq1_pad = _cell(lambda i: i.endswith("bridge=on+padding"))
|
||||
rq2_bridge_fed = _cell(lambda i: i.endswith("topo=bridge-federated"))
|
||||
rq2_dir_fed = _cell(lambda i: i.endswith("topo=directory-federated"))
|
||||
|
||||
summary = {
|
||||
"schema": "sor-assembler-dry/1",
|
||||
"run_index": run_index,
|
||||
"engine": engine,
|
||||
"n_cells": len(per_cell),
|
||||
"all_isolation_gated": all(r["isolation_gated"] for r in per_cell),
|
||||
"all_reproduce": all(r["reproduces"] for r in per_cell),
|
||||
"distinct_fingerprints": len(set(fingerprints)) == len(per_cell),
|
||||
"rq1_bridge_arms_live": rq1_on["bridge_present"] and rq1_pad["bridge_present"],
|
||||
"rq1_padding_arm_live": rq1_pad["padding_applied"] and not rq1_on["padding_applied"],
|
||||
"rq2_federation_spans_2plus": (
|
||||
rq2_bridge_fed["span_houses"] >= 2 and rq2_dir_fed["span_houses"] >= 2
|
||||
),
|
||||
"cells": per_cell,
|
||||
}
|
||||
summary["all_green"] = bool(
|
||||
summary["all_isolation_gated"] and summary["all_reproduce"]
|
||||
and summary["distinct_fingerprints"] and summary["rq1_bridge_arms_live"]
|
||||
and summary["rq1_padding_arm_live"] and summary["rq2_federation_spans_2plus"]
|
||||
)
|
||||
path = out_root / "assembler-dry.json"
|
||||
if not path.exists():
|
||||
path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return summary
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"""RQ1+RQ2 confirmatory battery — start-line preflight + guarded launcher.
|
||||
|
||||
This is the single operator entrypoint. Run with **no flags** it performs the
|
||||
*safe* start-line **preflight** and writes auditable artifacts, then prints a
|
||||
GO/NO-GO summary and the exact launch command — it collects **no** confirmatory
|
||||
data:
|
||||
|
||||
* §5 instrument-validation gate re-confirmation (``gate.run_gate``);
|
||||
* grid inventory + containment pin (``grid.write_device_map``);
|
||||
* frozen §2 cell plan + randomized/interleaved schedule (``battery.write_cell_plan``);
|
||||
* a 1-cell × 2-run DRY provenance pass on fixtures (``battery.dry_pass``).
|
||||
|
||||
The live per-cell condition assembler is now wired (``sor.assembler``): each cell
|
||||
maps to a genuinely distinct, condition-encoding, isolation-gated circuit (RQ1
|
||||
bridge-on / on+padding arms actually insert a bridge hop + PADDING stream; RQ2
|
||||
bridge-/directory-federated topologies genuinely span >= 2 houses). The preflight
|
||||
proves this on FIXTURES (``battery.assembler_dry_check`` — plans only, no traffic).
|
||||
|
||||
The **immutable confirmatory data run on the real grid is still the human gate**
|
||||
(CLAUDE.md §Stop, GOAL envelope (b)). ``--operator-go`` is triple-locked — it
|
||||
requires the ``SOR_CONFIRMATORY_GO=1`` operator token, a verified frozen-prereg
|
||||
SHA-256, and an isolated engine — and even with all three plus a green preflight it
|
||||
**HOLDS on grid completion**: the confirmatory battery does not launch until the
|
||||
full physical grid is up (the operator is bringing the 3rd phone online). This
|
||||
launcher surfaces that hold; it never fabricates a confirmatory cell.
|
||||
|
||||
Containment is load-bearing: nothing here forwards real/third-party traffic,
|
||||
touches an external target, or runs a forwarder on the host.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from cmd_chat.sor import battery, gate, grid
|
||||
from cmd_chat.sor.forwarder import assert_isolated
|
||||
|
||||
# The frozen prereg (read-only source of truth) and its pinned SHA-256. A GO is
|
||||
# refused unless the on-disk prereg still hashes to this — no confirmatory run
|
||||
# against an unfrozen/edited prereg.
|
||||
FROZEN_PREREG = Path.home() / "coding/sci-method/stages/03-design/output/sor-consent-prereg.md"
|
||||
FROZEN_PREREG_SHA256 = "f22331a72e0d0ccf38b787e63acabbe9d666456ec76076787a6d545c3193425b"
|
||||
|
||||
OPERATOR_TOKEN_ENV = "SOR_CONFIRMATORY_GO"
|
||||
|
||||
|
||||
def _ts() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
|
||||
def verify_freeze() -> bool:
|
||||
"""True iff the on-disk frozen prereg still matches its pinned SHA-256."""
|
||||
try:
|
||||
got = hashlib.sha256(FROZEN_PREREG.read_bytes()).hexdigest()
|
||||
except OSError:
|
||||
return False
|
||||
return got == FROZEN_PREREG_SHA256
|
||||
|
||||
|
||||
def preflight(out_dir: Path, *, order_seed: int = battery.S0, allow_live_gate: bool = True) -> Dict[str, Any]:
|
||||
"""Run the safe start-line preflight and write artifacts under ``out_dir``.
|
||||
Collects no confirmatory data. Returns a summary with a GO/NO-GO verdict."""
|
||||
out_dir = Path(out_dir)
|
||||
g = gate.run_gate(out_dir / "gate", allow_live=allow_live_gate)
|
||||
dm = grid.write_device_map(out_dir / "grid")
|
||||
plan_path = battery.write_cell_plan(out_dir / "plan", order_seed=order_seed)
|
||||
dp = battery.dry_pass(out_dir / "dry", runs=2)
|
||||
asm = battery.assembler_dry_check(out_dir / "assembler")
|
||||
|
||||
ready = bool(
|
||||
g["all_green"]
|
||||
and dm["topology_matchedN_honourable"]
|
||||
and dp["all_sha_match"] and dp["all_seed_reproduces"] and dp["distinct_seeds"]
|
||||
and asm["all_green"]
|
||||
and verify_freeze()
|
||||
)
|
||||
return {
|
||||
"generated_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"out_dir": str(out_dir),
|
||||
"gate_all_green": g["all_green"],
|
||||
"gate_offline_green": g["offline_items_green"],
|
||||
"grid_reachable": dm["reachable_count"],
|
||||
"grid_engine_hosts": dm["isolated_engine_host_count"],
|
||||
"grid_down": dm["devices_down"],
|
||||
"grid_honourable": dm["topology_matchedN_honourable"],
|
||||
"grid_full": len(dm["devices_down"]) == 0,
|
||||
"cell_plan": str(plan_path),
|
||||
"dry_ok": dp["all_sha_match"] and dp["all_seed_reproduces"] and dp["distinct_seeds"],
|
||||
"assembler_ok": asm["all_green"],
|
||||
"assembler_distinct": asm["distinct_fingerprints"],
|
||||
"assembler_isolation_gated": asm["all_isolation_gated"],
|
||||
"freeze_ok": verify_freeze(),
|
||||
"preflight_ready": ready,
|
||||
}
|
||||
|
||||
|
||||
def _print_summary(s: Dict[str, Any]) -> None:
|
||||
print(f"[preflight] artifacts -> {s['out_dir']}")
|
||||
print(f"[preflight] gate all_green={s['gate_all_green']} "
|
||||
f"(offline={s['gate_offline_green']})")
|
||||
print(f"[preflight] grid reachable={s['grid_reachable']} "
|
||||
f"engine_hosts={s['grid_engine_hosts']} down={s['grid_down']} "
|
||||
f"honourable={s['grid_honourable']}")
|
||||
print(f"[preflight] dry_ok={s['dry_ok']} freeze_ok={s['freeze_ok']}")
|
||||
print(f"[preflight] assembler_ok={s['assembler_ok']} "
|
||||
f"(distinct={s['assembler_distinct']} isolation_gated={s['assembler_isolation_gated']})")
|
||||
print(f"[preflight] grid_full={s['grid_full']} READY={s['preflight_ready']}")
|
||||
|
||||
|
||||
def _refuse_go(reason: str) -> int:
|
||||
print(f"[GO REFUSED] {reason}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
prog="python -m cmd_chat.sor.confirmatory_run",
|
||||
description="RQ1+RQ2 confirmatory battery: safe preflight by default; "
|
||||
"--operator-go is the human-gated immutable data run.",
|
||||
)
|
||||
ap.add_argument("--out", default=f"output/sor-confirmatory/{_ts()}",
|
||||
help="preflight artifact dir (default: timestamped)")
|
||||
ap.add_argument("--order-seed", type=int, default=battery.S0,
|
||||
help="measurement-side schedule ordering seed (default S0)")
|
||||
ap.add_argument("--engine", default="docker", help="isolated engine (default docker)")
|
||||
ap.add_argument("--operator-go", action="store_true",
|
||||
help="attempt the human-gated confirmatory data run (triple-locked)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
out_dir = Path(args.out)
|
||||
summary = preflight(out_dir, order_seed=args.order_seed)
|
||||
_print_summary(summary)
|
||||
|
||||
go_cmd = (f"{OPERATOR_TOKEN_ENV}=1 python -m cmd_chat.sor.confirmatory_run "
|
||||
f"--operator-go --engine {args.engine}")
|
||||
|
||||
if not args.operator_go:
|
||||
print("\n[held] preflight only — no confirmatory data collected.")
|
||||
print(f"[held] the immutable data run is the human gate. GO command:\n {go_cmd}")
|
||||
return 0
|
||||
|
||||
# --- triple-locked GO path (human gate) --------------------------------- #
|
||||
if os.environ.get(OPERATOR_TOKEN_ENV) != "1":
|
||||
return _refuse_go(f"operator token missing (set {OPERATOR_TOKEN_ENV}=1)")
|
||||
if not verify_freeze():
|
||||
return _refuse_go("frozen prereg SHA-256 mismatch — refusing to run against an unfrozen prereg")
|
||||
try:
|
||||
assert_isolated(args.engine)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return _refuse_go(f"containment: {exc}")
|
||||
if not summary["preflight_ready"]:
|
||||
return _refuse_go("preflight not fully green — resolve NO-GO items before a data run")
|
||||
|
||||
# Triple-lock passes and the live per-cell condition assembler is wired +
|
||||
# fixture-validated (assembler_ok). The one remaining gate is physical: the
|
||||
# confirmatory battery HOLDS until the full grid is up. The operator is
|
||||
# bringing the 3rd phone online; until every device is reachable this launcher
|
||||
# refuses to launch a data run on a degraded grid rather than fabricate cells.
|
||||
if not summary["grid_full"]:
|
||||
return _refuse_go(
|
||||
"grid completing: confirmatory battery holds until the full physical "
|
||||
f"grid is up (devices down: {summary['grid_down']}). The live per-cell "
|
||||
"assembler is wired + fixture-validated; the operator is bringing the "
|
||||
"3rd phone online. Re-run --operator-go once the grid is complete."
|
||||
)
|
||||
|
||||
# Full grid + all three locks + green preflight: the immutable confirmatory
|
||||
# data run is still the human's to launch. This launcher surfaces readiness;
|
||||
# the physical data collection is initiated by the operator, not fabricated.
|
||||
return _refuse_go(
|
||||
"human gate: grid complete and preflight green — the immutable confirmatory "
|
||||
"data run is the operator's to initiate on the live grid. This launcher "
|
||||
"does not fabricate confirmatory cells."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,245 @@
|
||||
"""Instrument-validation gate re-confirmation (prereg §5) for the RQ1+RQ2 path.
|
||||
|
||||
Re-runs the six boolean gate items and writes an auditable ``gate-report.json``.
|
||||
This is calibration/validation on **fixtures only** — it collects no
|
||||
confirmatory-cell data, so it is legitimate start-line work under the freeze:
|
||||
|
||||
1. 3-hop e2e delivery + per-hop pcap checksums (R4, live isolated docker);
|
||||
2. seeded reproducibility — same seed → identical circuit-build sequence (R1);
|
||||
3. correlator calibration — known-linked AUC≈1, known-unlinked AUC≈0.5 (R7);
|
||||
4. entropy estimator returns H = log2(N) for N equiprobable senders (R7);
|
||||
5. forwarders isolated-engine-only — ``assert engine != local`` or refuse (R4);
|
||||
6. provenance integrity — replayed fixture events SHA-256 matches the sealed
|
||||
manifest; append-only (R2/R3).
|
||||
|
||||
Item 1 stands up isolated **docker** containers and moves only self-generated
|
||||
fixture bytes between our own containers (containment-safe, always torn down).
|
||||
Items 2-6 are pure offline checks. Nothing here forwards real/third-party
|
||||
traffic, touches an external target, or runs a forwarder on the host.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from cmd_chat.sor import events as sor_events
|
||||
from cmd_chat.sor import forwarder as sor_forwarder
|
||||
from cmd_chat.sor.analysis.detectors import (
|
||||
bridge_correlation_auc,
|
||||
shannon_entropy_bits,
|
||||
synthetic_bridge_fixture,
|
||||
)
|
||||
from cmd_chat.sor.config import bringup
|
||||
from cmd_chat.sor.provenance import Node, RunManifest, validate_manifest
|
||||
|
||||
# Calibration tolerances (fixtures, ground truth known by construction).
|
||||
_AUC_LINKED_MIN = 0.99 # known-linked control must separate near-perfectly.
|
||||
_AUC_UNLINKED_TOL = 0.05 # known-unlinked mean must sit within 0.05 of chance.
|
||||
_ENTROPY_TOL = 1e-9 # H = log2(N) must hold to floating-point exactness.
|
||||
_UNLINKED_SEEDS = 40 # seeds averaged for the unlinked-chance calibration.
|
||||
|
||||
|
||||
@dataclass
|
||||
class GateItem:
|
||||
"""One §5 gate item's re-confirmation outcome."""
|
||||
|
||||
n: int
|
||||
name: str
|
||||
passed: bool
|
||||
status: str # "green" | "red" | "unavailable"
|
||||
evidence: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _utc_now_iso() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _fixture_pubkey(seed: int, role: str) -> str:
|
||||
raw = hashlib.sha256(f"sor-gate|{seed}|{role}".encode()).digest() # 32 bytes
|
||||
import base64
|
||||
|
||||
return base64.b64encode(raw).decode()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Item 1 — live 3-hop e2e delivery (isolated docker).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _item1_e2e(out_root: Path, seed: int, *, allow_live: bool) -> GateItem:
|
||||
"""Stand up a 3-hop nested-SSH docker circuit, deliver a seed-deterministic
|
||||
self-payload end-to-end, and require per-hop pcap checksums. Marked
|
||||
``unavailable`` (not ``red``) when the docker engine/image is absent — a
|
||||
missing execution boundary is not a failed instrument."""
|
||||
have_docker = shutil.which("docker") is not None
|
||||
if not (allow_live and have_docker):
|
||||
return GateItem(1, "3-hop e2e delivery + pcap checksums", False, "unavailable",
|
||||
{"reason": "docker engine/image not available or live run disabled",
|
||||
"have_docker": have_docker})
|
||||
try:
|
||||
res = sor_forwarder.run_circuit_fixture(seed, engine="docker", hops=3,
|
||||
out_root=out_root)
|
||||
except Exception as exc: # noqa: BLE001 - report, never crash the gate
|
||||
return GateItem(1, "3-hop e2e delivery + pcap checksums", False, "red",
|
||||
{"error": f"{type(exc).__name__}: {exc}"})
|
||||
pcaps_ok = len(res.pcap_sha256) == 3 and all(res.pcap_sha256.values())
|
||||
passed = bool(res.delivered and pcaps_ok
|
||||
and res.received_sha256 == res.payload_sha256)
|
||||
return GateItem(1, "3-hop e2e delivery + pcap checksums", passed,
|
||||
"green" if passed else "red",
|
||||
{"run_id": res.run_id, "delivered": res.delivered,
|
||||
"payload_sha256": res.payload_sha256,
|
||||
"received_sha256": res.received_sha256,
|
||||
"pcap_sha256": {str(k): v for k, v in res.pcap_sha256.items()},
|
||||
"events_sha256": res.events_sha256})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Item 2 — seeded reproducibility (R1).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _item2_reproducible(*, pool: int = 5, hops: int = 3, rebuilds: int = 3) -> GateItem:
|
||||
seed = 0xC0FFEE
|
||||
a = bringup(seed, pool, hops, rebuilds)
|
||||
b = bringup(seed, pool, hops, rebuilds)
|
||||
other = bringup(seed ^ 0x1, pool, hops, rebuilds)
|
||||
passed = a == b and a != other
|
||||
return GateItem(2, "seeded circuit-build reproducibility", passed,
|
||||
"green" if passed else "red",
|
||||
{"seed": seed, "identical_on_replay": a == b,
|
||||
"differs_on_other_seed": a != other, "sequence": a})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Item 3 — correlator calibration (R7).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _item3_correlator() -> GateItem:
|
||||
linked_aucs = [bridge_correlation_auc(*synthetic_bridge_fixture(s, linked=True))
|
||||
for s in range(_UNLINKED_SEEDS)]
|
||||
unlinked_aucs = [bridge_correlation_auc(*synthetic_bridge_fixture(s, linked=False))
|
||||
for s in range(_UNLINKED_SEEDS)]
|
||||
linked_min = min(linked_aucs)
|
||||
unlinked_mean = sum(unlinked_aucs) / len(unlinked_aucs)
|
||||
passed = linked_min >= _AUC_LINKED_MIN and abs(unlinked_mean - 0.5) <= _AUC_UNLINKED_TOL
|
||||
return GateItem(3, "correlator calibration (linked≈1 / unlinked≈0.5)", passed,
|
||||
"green" if passed else "red",
|
||||
{"linked_auc_min": linked_min, "unlinked_auc_mean": unlinked_mean,
|
||||
"n_seeds": _UNLINKED_SEEDS, "linked_floor": _AUC_LINKED_MIN,
|
||||
"unlinked_tol": _AUC_UNLINKED_TOL})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Item 4 — entropy estimator H = log2(N) (R7).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _item4_entropy() -> GateItem:
|
||||
checks = []
|
||||
ok = True
|
||||
for n in (2, 4, 8, 16, 64):
|
||||
h = shannon_entropy_bits([1] * n) # N equiprobable senders
|
||||
exact = abs(h - math.log2(n)) <= _ENTROPY_TOL
|
||||
ok = ok and exact
|
||||
checks.append({"N": n, "H": h, "log2N": math.log2(n), "exact": exact})
|
||||
return GateItem(4, "entropy estimator H = log2(N)", ok,
|
||||
"green" if ok else "red", {"checks": checks, "tol": _ENTROPY_TOL})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Item 5 — isolated-engine assertion (R4).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _item5_isolation() -> GateItem:
|
||||
refused_local = False
|
||||
refused_unknown = False
|
||||
accepts_docker = False
|
||||
try:
|
||||
sor_forwarder.assert_isolated("local")
|
||||
except sor_forwarder.ContainmentError:
|
||||
refused_local = True
|
||||
try:
|
||||
sor_forwarder.assert_isolated("host-native")
|
||||
except sor_forwarder.ContainmentError:
|
||||
refused_unknown = True
|
||||
try:
|
||||
sor_forwarder.assert_isolated("docker")
|
||||
accepts_docker = True
|
||||
except sor_forwarder.ContainmentError:
|
||||
accepts_docker = False
|
||||
passed = refused_local and refused_unknown and accepts_docker
|
||||
return GateItem(5, "forwarders isolated-engine-only (assert engine != local)", passed,
|
||||
"green" if passed else "red",
|
||||
{"refused_local": refused_local, "refused_unknown": refused_unknown,
|
||||
"accepts_docker": accepts_docker})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Item 6 — provenance integrity (R2/R3).
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _item6_provenance(out_root: Path, *, pool: int = 5, hops: int = 3) -> GateItem:
|
||||
seed = 0x5EED
|
||||
# Unique run subdir per invocation so each produces fresh write-once artifacts
|
||||
# (and run_gate stays safely repeatable into a reused out_dir).
|
||||
run_id = f"gate6-{seed:016x}-{uuid.uuid4().hex[:8]}"
|
||||
run_dir = Path(out_root) / run_id
|
||||
nodes = [Node(role=r, persona_pub_b64=_fixture_pubkey(seed, r), engine="docker")
|
||||
for r in ("host", "hop", "hop")]
|
||||
manifest = RunManifest(run_id=run_id, sor_seed=seed, topology="1house",
|
||||
selector="static", churn_schedule_id="none", nodes=nodes,
|
||||
engine_kind="docker", worktree_root=Path.cwd())
|
||||
sealed = sor_events.replay_and_seal(run_dir, manifest, pool=pool, hops=hops, rebuilds=1)
|
||||
validate_manifest(sealed) # raises on any schema violation
|
||||
recomputed = hashlib.sha256((run_dir / "events.jsonl").read_bytes()).hexdigest()
|
||||
sha_match = sealed["events"]["sha256"] == recomputed
|
||||
|
||||
# Append-only / immutability: re-sealing the same manifest is refused.
|
||||
reseal_refused = False
|
||||
try:
|
||||
from cmd_chat.sor.provenance import seal_manifest
|
||||
|
||||
seal_manifest(run_dir, recomputed)
|
||||
except ValueError:
|
||||
reseal_refused = True
|
||||
|
||||
passed = sha_match and reseal_refused
|
||||
return GateItem(6, "provenance integrity (events SHA == manifest; append-only)", passed,
|
||||
"green" if passed else "red",
|
||||
{"run_id": run_id, "events_sha256": recomputed,
|
||||
"manifest_events_sha256": sealed["events"]["sha256"],
|
||||
"sha_match": sha_match, "reseal_refused": reseal_refused})
|
||||
|
||||
|
||||
def run_gate(out_dir: Path, *, allow_live: bool = True, e2e_seed: int = 0xA11CE) -> Dict[str, Any]:
|
||||
"""Re-confirm all six §5 gate items and write ``gate-report.json`` (write-once)
|
||||
under ``out_dir``. Returns the report dict. ``all_green`` is True only if every
|
||||
item is green; item 1 may be ``unavailable`` when no isolated engine is present,
|
||||
which is reported distinctly from a ``red`` failure."""
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
items: List[GateItem] = [
|
||||
_item1_e2e(out_dir, e2e_seed, allow_live=allow_live),
|
||||
_item2_reproducible(),
|
||||
_item3_correlator(),
|
||||
_item4_entropy(),
|
||||
_item5_isolation(),
|
||||
_item6_provenance(out_dir),
|
||||
]
|
||||
all_green = all(it.status == "green" for it in items)
|
||||
offline_green = all(it.status == "green" for it in items if it.n != 1)
|
||||
report = {
|
||||
"schema": "sor-gate-report/1",
|
||||
"scope": "RQ1+RQ2 lead-paper path (prereg §5 instrument-validation gate)",
|
||||
"generated_utc": _utc_now_iso(),
|
||||
"all_green": all_green,
|
||||
"offline_items_green": offline_green,
|
||||
"items": [{"n": it.n, "name": it.name, "passed": it.passed,
|
||||
"status": it.status, "evidence": it.evidence} for it in items],
|
||||
}
|
||||
path = out_dir / "gate-report.json"
|
||||
if not path.exists():
|
||||
path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
report["_report_path"] = str(path)
|
||||
return report
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Grid inventory + containment pin for the RQ1+RQ2 confirmatory battery.
|
||||
|
||||
Pins the node-role → device mapping for the lab grid (2 phones + laptop) and
|
||||
probes, at call time, which devices are SSH-reachable and which can host an
|
||||
**isolated engine** (docker). Writes an auditable ``device-map.json``.
|
||||
|
||||
Containment note (load-bearing): every SOR forwarder/hop runs inside an isolated
|
||||
engine (docker container), never on a phone or the host directly. The physical
|
||||
phones are *distribution* options for where those isolated containers live; the
|
||||
operator-blessed fallback for the confirmatory RQ1/RQ2 path is isolated docker
|
||||
containers co-located on the docker host (as used for gate item 1). So the grid
|
||||
being partly degraded does not by itself collapse the topology — but any inability
|
||||
to honour the §2 topology / §6 matched-N with isolated nodes is a STOP-and-flag.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Device:
|
||||
"""A pinned grid device and the house-role it plays in the confirmatory design."""
|
||||
|
||||
name: str # ssh alias / stable id
|
||||
kind: str # "phone" | "laptop"
|
||||
arch: str # "aarch64" | "x86_64"
|
||||
can_host_engine: bool # can run an isolated docker engine locally?
|
||||
house_role: str # design role (e.g. "house-A host / hop pool")
|
||||
|
||||
|
||||
# Pinned inventory (lab-only, all ours — CLAUDE.md §Containment). tril + fp6 are
|
||||
# the two phones; laptop is the x86_64 docker host. Only devices that can host an
|
||||
# isolated docker engine may run a forwarder; phones without docker are consent
|
||||
# endpoints / distribution targets, never a bare host forwarder.
|
||||
GRID_INVENTORY: List[Device] = [
|
||||
Device("laptop", "laptop", "x86_64", True, "house-A docker host + hop pool"),
|
||||
Device("fp6", "phone", "aarch64", False, "house-B consenting node (phone)"),
|
||||
Device("tril", "phone", "aarch64", False, "house-C consenting node (Termux, no docker)"),
|
||||
]
|
||||
|
||||
|
||||
def _probe_ssh(alias: str, timeout: int = 8) -> bool:
|
||||
"""Best-effort SSH reachability probe (BatchMode, short timeout). Never raises."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
|
||||
"-o", "StrictHostKeyChecking=accept-new", alias, "true"],
|
||||
capture_output=True, timeout=timeout,
|
||||
)
|
||||
return r.returncode == 0
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
def _probe_remote_docker(alias: str, timeout: int = 12) -> bool:
|
||||
"""Best-effort probe of an isolated docker engine on a remote device. Never raises."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
|
||||
"-o", "StrictHostKeyChecking=accept-new", alias,
|
||||
"docker version --format '{{.Server.Version}}'"],
|
||||
capture_output=True, timeout=timeout,
|
||||
)
|
||||
return r.returncode == 0 and bool(r.stdout.strip())
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
def _probe_local_docker() -> Optional[str]:
|
||||
"""Local docker server version if a daemon is up, else None. Never raises."""
|
||||
if shutil.which("docker") is None:
|
||||
return None
|
||||
try:
|
||||
r = subprocess.run(["docker", "version", "--format", "{{.Server.Version}}"],
|
||||
capture_output=True, timeout=12)
|
||||
return r.stdout.decode().strip() if r.returncode == 0 else None
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
|
||||
|
||||
def probe_grid() -> Dict[str, Any]:
|
||||
"""Probe reachability + isolated-engine availability across the pinned grid.
|
||||
Pure I/O, no traffic, no forwarder — a connectivity/capability snapshot only."""
|
||||
local_docker = _probe_local_docker()
|
||||
devices: List[Dict[str, Any]] = []
|
||||
for d in GRID_INVENTORY:
|
||||
reachable = _probe_ssh(d.name)
|
||||
# An isolated engine is available on this device if it is the local docker
|
||||
# host (laptop here) or a reachable device advertising a docker daemon.
|
||||
if d.name == "laptop":
|
||||
engine_ok = local_docker is not None
|
||||
engine_ver = local_docker
|
||||
elif reachable and d.can_host_engine:
|
||||
engine_ok = _probe_remote_docker(d.name)
|
||||
engine_ver = "remote-docker" if engine_ok else None
|
||||
else:
|
||||
engine_ok = False
|
||||
engine_ver = None
|
||||
devices.append({
|
||||
**asdict(d),
|
||||
"ssh_reachable": reachable,
|
||||
"isolated_engine_available": engine_ok,
|
||||
"engine_version": engine_ver,
|
||||
})
|
||||
return {"local_docker_version": local_docker, "devices": devices}
|
||||
|
||||
|
||||
def write_device_map(out_dir: Path) -> Dict[str, Any]:
|
||||
"""Write ``device-map.json`` (write-once) pinning the node-role→device map and
|
||||
the current reachability/engine snapshot, plus a containment + matched-N
|
||||
assessment. Returns the document."""
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
snap = probe_grid()
|
||||
|
||||
engine_hosts = [d for d in snap["devices"] if d["isolated_engine_available"]]
|
||||
reachable = [d for d in snap["devices"] if d["ssh_reachable"]]
|
||||
down = [d["name"] for d in snap["devices"] if not d["ssh_reachable"]]
|
||||
# RQ1/RQ2 hops run as isolated docker containers; a single docker host can
|
||||
# host >=3 distinct containerised nodes (operator-blessed, gate item 1). So
|
||||
# the topology/matched-N is honourable iff at least one isolated engine exists.
|
||||
topology_honourable = len(engine_hosts) >= 1
|
||||
|
||||
doc = {
|
||||
"schema": "sor-device-map/1",
|
||||
"scope": "RQ1+RQ2 lead-paper grid pin (CLAUDE.md §Containment)",
|
||||
"generated_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"local_docker_version": snap["local_docker_version"],
|
||||
"devices": snap["devices"],
|
||||
"reachable_count": len(reachable),
|
||||
"isolated_engine_host_count": len(engine_hosts),
|
||||
"degraded": bool(down),
|
||||
"devices_down": down,
|
||||
"topology_matchedN_honourable": topology_honourable,
|
||||
"containment": {
|
||||
"hops_run_in": "isolated docker containers only (never a phone/host forwarder)",
|
||||
"external_target": "none",
|
||||
"live_vm_churn_on_rq1_rq2": "none (RQ1/RQ2 use no churn; churn_schedule_id=none)",
|
||||
"self_traffic_only": True,
|
||||
},
|
||||
"matched_N_note": (
|
||||
"RQ1/RQ2 isolated hops are containerised on the docker host (>=3 distinct "
|
||||
"containers = distinct nodes). Physical-phone distribution is optional; the "
|
||||
"confirmatory matched-N (single-house N = federated total consenting nodes) is "
|
||||
"pinned from the containerised node count at run time and recorded per manifest."
|
||||
),
|
||||
"assessment": (
|
||||
"GO on isolated docker host" if topology_honourable
|
||||
else "STOP: no isolated engine available — topology/matched-N cannot be honoured"
|
||||
),
|
||||
}
|
||||
path = out_dir / "device-map.json"
|
||||
if not path.exists():
|
||||
path.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
doc["_report_path"] = str(path)
|
||||
return doc
|
||||
@@ -0,0 +1,94 @@
|
||||
# Stage-05 analysis clarification — Holm–Bonferroni over the frozen family of 7
|
||||
|
||||
**Status: RATIFIED by operator (Andre), 2026-07-19. Worktree clarification note
|
||||
only — NOT a deviation.** This restates, for the harness, a decision the frozen
|
||||
prereg **already fixes**; it does **not** edit the frozen prereg
|
||||
(`sor-consent-prereg.md`, SHA `f22331a72e0d0ccf38b787e63acabbe9d666456ec76076787a6d545c3193425b`).
|
||||
|
||||
## Authority (this IS the freeze, not a new choice)
|
||||
|
||||
The size-7 family and its disclosure in the lead paper are **pre-registered text**,
|
||||
so holding `m = 7` for the reported RQ1/RQ2 subset is compliance, not deviation:
|
||||
|
||||
- **Prereg §6 — Multiple-comparison correction [APPROVAL]** (prereg lines ~218–222):
|
||||
*"the confirmatory hypothesis tests across all three RQ families are corrected
|
||||
together by Holm–Bonferroni … Test family (7 confirmatory tests): {RQ1-P1, RQ1-P2,
|
||||
RQ2-P1, RQ2-P3, RQ3-P1-perf, RQ3-P1-latency, RQ3-P2}."* The family is frozen at 7.
|
||||
- **Prereg D6** (prereg line ~317): *"the shared prereg + Holm-corrected family are
|
||||
disclosed in whichever papers result (which is more rigorous)."* The lead paper
|
||||
discloses the **same size-7 family** even while reporting only the RQ1/RQ2 subset.
|
||||
|
||||
Correcting the reported 4 against `m = 4` would shrink the frozen family after the
|
||||
fact — p-hacking the multiplicity down, contrary to §6. Holding `m = 7` is the
|
||||
only reading consistent with the frozen text.
|
||||
|
||||
## The question
|
||||
|
||||
The prereg §6 pre-registers a **confirmatory family of 7** hypothesis tests:
|
||||
|
||||
```
|
||||
RQ1-P1, RQ1-P2, RQ2-P1, RQ2-P3, RQ3-P1-perf, RQ3-P1-latency, RQ3-P2
|
||||
```
|
||||
|
||||
The **lead paper (G4 + RQ1 + RQ2, prereg D6)** reports only the first **4**
|
||||
(RQ1/RQ2); RQ3's three tests are the severable follow-on (§8). So: when we
|
||||
Holm-correct the 4 reported p-values, do we correct against **m = 7** (the frozen
|
||||
family) or **m = 4** (the reported subset)?
|
||||
|
||||
Correcting against m = 4 would be **p-hacking the multiplicity down**: the family
|
||||
was frozen at 7 *before* data, and shrinking it after the fact — because RQ3 is
|
||||
being reported later — inflates the reported significance of the RQ1/RQ2 subset.
|
||||
That breaks the pre-registration.
|
||||
|
||||
## Decision (implemented default)
|
||||
|
||||
**Hold the family size at m = 7 for every reported test.** The reported subset
|
||||
never shrinks the correction to itself.
|
||||
|
||||
Concretely, Holm step-down (`stats.holm_bonferroni`, `confirm.apply_holm`):
|
||||
|
||||
- All reported p-values are sorted ascending and assigned ranks `k = 1..R`
|
||||
(`R = 4` reported here).
|
||||
- Each gets multiplier `m − k + 1` with **`m = FROZEN_FAMILY_SIZE = 7`**.
|
||||
- So the 4 reported tests receive multipliers **7, 6, 5, 4** (smallest p → ×7).
|
||||
The step-down enforces monotone non-decreasing adjusted p-values.
|
||||
- `holm_bonferroni` **raises** if `family_size < #reported` — you can never
|
||||
accidentally correct against fewer than the tests you are reporting.
|
||||
|
||||
This is a **conservative embedding**: the 4 reported tests are treated as
|
||||
occupying the 4 *smallest-multiplier* slots (7,6,5,4) of the size-7 family rather
|
||||
than the true unknown slots they would occupy if all 7 p-values were in hand. The
|
||||
adjusted p-values are therefore **≥** the values a full simultaneous size-7 Holm
|
||||
run would assign to these same tests. We over-correct, never under-correct — the
|
||||
honest direction for a subset report.
|
||||
|
||||
*Covered by `tests/test_sor_confirm.py::test_apply_holm_corrects_against_family_of_seven`
|
||||
(top multiplier == 7; multipliers == [4,5,6,7]) and
|
||||
`tests/test_sor_stats.py` (family-size monotonicity + `ValueError` when
|
||||
`family_size < reported`).*
|
||||
|
||||
## Gate vs. Holm ordering (not conflated)
|
||||
|
||||
Every **reported decision** in §6 is a **CI gate** (RQ1-P1 CI excludes 0.5;
|
||||
RQ1-P2 ΔAUC CI > 0; RQ2-P1 ΔH two-sided CI sign; RQ2-P3 Spearman ρ CI). The
|
||||
bootstrap p-values feed the Holm step-down **only to order it** — no reported
|
||||
claim rests on a bare p. Holm is the family-wise multiplicity guard layered on
|
||||
top of the pre-registered CI gates, exactly as §6 / rigor-standards §Statistics
|
||||
require.
|
||||
|
||||
## Operator ruling (2026-07-19)
|
||||
|
||||
The operator (Andre) ratified the **conservative embedding**: hold `family_size = 7`,
|
||||
report the 4 RQ1/RQ2 tests, multipliers **7, 6, 5, 4**. This is exactly the frozen
|
||||
§6 / D6 text above — the harness default now matches the pre-registration.
|
||||
|
||||
Two alternatives were considered and **explicitly declined**:
|
||||
|
||||
- *Fold RQ3's three cells back in and report all 7 simultaneously.* Statistically
|
||||
clean, but RQ3 was dropped for the lead paper (prereg D6) and would need the
|
||||
paid/agent selector-arm decisions. **Declined:** do not fold RQ3 back.
|
||||
- *Full α-splitting / hierarchical gatekeeping.* More powerful, but requires a
|
||||
pre-specified α-allocation the frozen prereg does not contain. **Declined:** not
|
||||
available without a prereg change (which the freeze forbids).
|
||||
|
||||
No open human action remains: the implemented default is the ratified one.
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Live per-cell condition assembler — each §2 cell is a genuinely distinct,
|
||||
isolation-gated circuit, not the plain 3-hop control.
|
||||
|
||||
These are fixture-only checks: the assembler builds *plans* (ForwarderPlan targets),
|
||||
opens no socket, moves no traffic, and stands up no engine. We pin that each cell's
|
||||
structure encodes its condition, that determinism holds, that federated cells
|
||||
genuinely span >= 2 houses, and that a non-isolated engine is refused.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from cmd_chat.sor import battery
|
||||
from cmd_chat.sor.assembler import assemble, assemble_all
|
||||
from cmd_chat.sor.forwarder import ContainmentError
|
||||
|
||||
|
||||
def _cells_by_suffix():
|
||||
return {c.cell_id: c for c in battery.enumerate_cells()}
|
||||
|
||||
|
||||
def _assemble_cell(suffix, seed=7):
|
||||
cell = next(c for c in battery.enumerate_cells() if c.cell_id.endswith(suffix))
|
||||
return assemble(cell, seed)
|
||||
|
||||
|
||||
def test_all_cells_distinct_fingerprints():
|
||||
cells = battery.enumerate_cells()
|
||||
specs = assemble_all(cells, 0)
|
||||
fps = {s.fingerprint() for s in specs.values()}
|
||||
assert len(fps) == len(cells) == 6 # every cell is a distinct circuit
|
||||
|
||||
|
||||
def test_same_cell_seed_reproduces_fingerprint():
|
||||
cells = battery.enumerate_cells()
|
||||
a = assemble_all(cells, 0)
|
||||
b = assemble_all(cells, 0)
|
||||
assert {k: v.fingerprint() for k, v in a.items()} == {
|
||||
k: v.fingerprint() for k, v in b.items()
|
||||
}
|
||||
|
||||
|
||||
def test_different_run_index_changes_seed_and_circuit():
|
||||
cells = battery.enumerate_cells()
|
||||
a = assemble_all(cells, 0)
|
||||
b = assemble_all(cells, 1)
|
||||
for cid in a:
|
||||
assert a[cid].seed != b[cid].seed
|
||||
assert a[cid].fingerprint() != b[cid].fingerprint()
|
||||
|
||||
|
||||
def test_all_cells_isolation_gated():
|
||||
specs = assemble_all(battery.enumerate_cells(), 0)
|
||||
assert all(s.isolation_gated() for s in specs.values())
|
||||
|
||||
|
||||
def test_local_engine_is_refused():
|
||||
cell = battery.enumerate_cells()[0]
|
||||
with pytest.raises(ContainmentError):
|
||||
assemble(cell, 7, engine="local").plans()
|
||||
|
||||
|
||||
def test_rq1_bridge_off_is_plain_single_house():
|
||||
spec = _assemble_cell("bridge=off")
|
||||
assert spec.bridge_present is False and spec.padding_applied is False
|
||||
assert not any(h.is_bridge for h in spec.hops)
|
||||
assert spec.span_houses() == 1
|
||||
|
||||
|
||||
def test_rq1_bridge_on_inserts_a_bridge_hop():
|
||||
spec = _assemble_cell("bridge=on")
|
||||
assert spec.bridge_present is True and spec.padding_applied is False
|
||||
assert any(h.is_bridge for h in spec.hops)
|
||||
|
||||
|
||||
def test_rq1_bridge_on_padding_applies_padding():
|
||||
spec = _assemble_cell("bridge=on+padding")
|
||||
assert spec.bridge_present is True and spec.padding_applied is True
|
||||
assert any(h.is_bridge for h in spec.hops)
|
||||
|
||||
|
||||
def test_rq2_bridge_federated_spans_two_houses():
|
||||
spec = _assemble_cell("topo=bridge-federated")
|
||||
assert spec.bridge_present is True
|
||||
assert len({h.house for h in spec.hops if h.house != "bridge"}) >= 2
|
||||
|
||||
|
||||
def test_rq2_directory_federated_spans_two_houses():
|
||||
spec = _assemble_cell("topo=directory-federated")
|
||||
assert len({h.house for h in spec.hops}) >= 2
|
||||
|
||||
|
||||
def test_assembler_dry_check_all_green(tmp_path):
|
||||
summary = battery.assembler_dry_check(tmp_path / "asm")
|
||||
assert summary["all_green"] is True
|
||||
assert summary["distinct_fingerprints"] is True
|
||||
assert summary["all_isolation_gated"] is True
|
||||
assert summary["rq1_bridge_arms_live"] is True
|
||||
assert summary["rq1_padding_arm_live"] is True
|
||||
assert summary["rq2_federation_spans_2plus"] is True
|
||||
|
||||
|
||||
def test_assembler_dry_report_is_write_once(tmp_path):
|
||||
out = tmp_path / "asm"
|
||||
first = battery.assembler_dry_check(out)
|
||||
(out / "assembler-dry.json").read_text() # exists
|
||||
# A second call must not overwrite the immutable report.
|
||||
second = battery.assembler_dry_check(out)
|
||||
assert first["distinct_fingerprints"] == second["distinct_fingerprints"]
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Confirmatory launcher — freeze guard + human-gate hold (no data collected).
|
||||
|
||||
The live preflight is exercised by the gate/battery/grid suites; here we pin the
|
||||
guard logic: the frozen-prereg SHA verifies, GO is refused without the operator
|
||||
token, a tokened GO holds on grid completion (3rd phone not up yet), and even a
|
||||
full grid holds for the operator rather than fabricating the data run.
|
||||
"""
|
||||
|
||||
from cmd_chat.sor import confirmatory_run as cr
|
||||
|
||||
|
||||
# A fully-ready preflight summary with the grid still completing (a device down).
|
||||
_READY_GRID_DOWN = {
|
||||
"out_dir": "x", "gate_all_green": True, "gate_offline_green": True,
|
||||
"grid_reachable": 2, "grid_engine_hosts": 1, "grid_down": ["tril"],
|
||||
"grid_honourable": True, "grid_full": False, "cell_plan": "p", "dry_ok": True,
|
||||
"assembler_ok": True, "assembler_distinct": True, "assembler_isolation_gated": True,
|
||||
"freeze_ok": True, "preflight_ready": True, "generated_utc": "now",
|
||||
}
|
||||
# The same, but with the full grid up (no device down).
|
||||
_READY_GRID_FULL = dict(_READY_GRID_DOWN, grid_down=[], grid_full=True)
|
||||
|
||||
|
||||
def test_frozen_prereg_sha_verifies():
|
||||
assert cr.verify_freeze() is True # on-disk prereg still matches the pin
|
||||
|
||||
|
||||
def test_go_refused_without_operator_token(monkeypatch):
|
||||
monkeypatch.setattr(cr, "preflight", lambda *a, **k: dict(_READY_GRID_DOWN))
|
||||
monkeypatch.delenv(cr.OPERATOR_TOKEN_ENV, raising=False)
|
||||
assert cr.main(["--operator-go", "--out", "x"]) == 2
|
||||
|
||||
|
||||
def test_go_holds_on_grid_completion(monkeypatch):
|
||||
monkeypatch.setattr(cr, "preflight", lambda *a, **k: dict(_READY_GRID_DOWN))
|
||||
monkeypatch.setattr(cr, "verify_freeze", lambda: True)
|
||||
monkeypatch.setenv(cr.OPERATOR_TOKEN_ENV, "1")
|
||||
# Triple-lock + green preflight + wired assembler, but grid still completing.
|
||||
assert cr.main(["--operator-go", "--out", "x"]) == 2
|
||||
|
||||
|
||||
def test_go_holds_for_operator_even_on_full_grid(monkeypatch):
|
||||
monkeypatch.setattr(cr, "preflight", lambda *a, **k: dict(_READY_GRID_FULL))
|
||||
monkeypatch.setattr(cr, "verify_freeze", lambda: True)
|
||||
monkeypatch.setenv(cr.OPERATOR_TOKEN_ENV, "1")
|
||||
# Full grid + all locks: the immutable data run is still the operator's to
|
||||
# initiate — the launcher does not fabricate confirmatory cells.
|
||||
assert cr.main(["--operator-go", "--out", "x"]) == 2
|
||||
|
||||
|
||||
def test_preflight_held_mode_returns_zero(monkeypatch):
|
||||
monkeypatch.setattr(cr, "preflight", lambda *a, **k: dict(_READY_GRID_DOWN))
|
||||
assert cr.main(["--out", "x"]) == 0 # no --operator-go: held, no data
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Instrument-validation gate re-confirmation — offline items (2-6).
|
||||
|
||||
Item 1 (live 3-hop e2e) needs a docker daemon + sor-hop image, so it is exercised
|
||||
separately by the forwarder e2e test; here we drive the gate with ``allow_live=
|
||||
False`` and assert every *offline* item is green and the report is write-once.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from cmd_chat.sor import gate
|
||||
|
||||
|
||||
def test_offline_gate_items_all_green(tmp_path):
|
||||
report = gate.run_gate(tmp_path, allow_live=False)
|
||||
assert report["offline_items_green"] is True
|
||||
by_n = {it["n"]: it for it in report["items"]}
|
||||
for n in (2, 3, 4, 5, 6):
|
||||
assert by_n[n]["status"] == "green", (n, by_n[n])
|
||||
# With live disabled, item 1 is 'unavailable' (not a red failure).
|
||||
assert by_n[1]["status"] == "unavailable"
|
||||
assert report["all_green"] is False # item 1 not confirmed in this offline run
|
||||
|
||||
|
||||
def test_gate_report_is_write_once(tmp_path):
|
||||
gate.run_gate(tmp_path, allow_live=False)
|
||||
path = tmp_path / "gate-report.json"
|
||||
first = path.read_text()
|
||||
gate.run_gate(tmp_path, allow_live=False) # second call must not overwrite
|
||||
assert path.read_text() == first
|
||||
doc = json.loads(first)
|
||||
assert doc["schema"] == "sor-gate-report/1"
|
||||
|
||||
|
||||
def test_item5_isolation_refuses_local_accepts_docker(tmp_path):
|
||||
report = gate.run_gate(tmp_path, allow_live=False)
|
||||
ev = next(it["evidence"] for it in report["items"] if it["n"] == 5)
|
||||
assert ev["refused_local"] and ev["refused_unknown"] and ev["accepts_docker"]
|
||||
|
||||
|
||||
def test_item4_entropy_is_exact_log2n(tmp_path):
|
||||
report = gate.run_gate(tmp_path, allow_live=False)
|
||||
ev = next(it["evidence"] for it in report["items"] if it["n"] == 4)
|
||||
assert all(c["exact"] for c in ev["checks"])
|
||||
Reference in New Issue
Block a user