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:
@@ -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
|
||||
Reference in New Issue
Block a user