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:
leetcrypt
2026-07-19 20:49:12 -07:00
parent 72a6c7b519
commit 6d61c53152
11 changed files with 1228 additions and 0 deletions
+231
View File
@@ -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}
+76
View File
@@ -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
+184
View File
@@ -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())
+245
View File
@@ -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
+163
View File
@@ -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