From 6bae9a176d1de737f6dac6c8bbaf6187ab6f37ad Mon Sep 17 00:00:00 2001 From: leetcrypt Date: Wed, 22 Jul 2026 23:18:16 -0700 Subject: [PATCH] DRAFT(next-exp): container-house pools + connectivity failsafe scaffolds Defensive-measurement instrument scaffolds for the re-staged experiment, not wired into the sealed pipeline: - house_pool.py: distinct isolated container pools per house (own net + keys), deterministic seed-derived node ids; refuses engine=="local" (containment). - watchdog.py: pre-registered HALT / PAUSE-RESUME failsafe for load-bearing node pools; deterministic, probe injected (no external target). Both are DRAFT for a fresh pre-registration; neither touches the sealed battery. Co-Authored-By: Claude Opus 4.6 --- cmd_chat/sor/house_pool.py | 129 +++++++++++++++++++++++++++++++++++++ cmd_chat/sor/watchdog.py | 112 ++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 cmd_chat/sor/house_pool.py create mode 100644 cmd_chat/sor/watchdog.py diff --git a/cmd_chat/sor/house_pool.py b/cmd_chat/sor/house_pool.py new file mode 100644 index 0000000..bf601e2 --- /dev/null +++ b/cmd_chat/sor/house_pool.py @@ -0,0 +1,129 @@ +"""Track-B container-simulated house pools — DRAFT (next experiment only). + +Purpose (see prereg NEXT-EXPERIMENT-DRAFT §1 Track B): replace the *logical phone +labels* used to stand in for house-B/house-C in the sealed battery with **genuinely +distinct container pools**. Each house becomes an isolated Docker network hosting its +own N node containers, each with its own X25519/Ed25519 identity — giving real, +auditable node-distinctness on a single laptop, with **no phone dependency**. + +Containment (load-bearing, identical to the sealed instrument): + * Every node runs inside an **isolated engine** — this module refuses to place a node + unless ``engine != "local"``. No node ever runs on the bare host. + * Self-generated fixture traffic only; no external target; lab-only. + +Integrity boundary: anything measured through these pools is a **new** measurement. It +may be reported as *exploratory*, or promoted to confirmatory only under a fresh frozen +pre-registration. It may NEVER be merged into the sealed original battery. + +This module is a pool *model* + containment validator + deterministic identity +assignment. Actual container launch is injected by the caller (``executor.py``) via a +``launch_fn`` seam, so this file stays correct without duplicating engine plumbing. +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass, field +from typing import Callable, Dict, List, Optional + + +class ContainmentError(RuntimeError): + """Raised when a placement would violate the isolated-engine law.""" + + +@dataclass(frozen=True) +class SimNode: + """One simulated relay node: an isolated container with its own identity.""" + + node_id: str # deterministic, seed-derived + house: str # owning house id (e.g. "house-B") + engine: str # MUST be an isolated engine (docker/multipass), never "local" + net: str # isolated docker network the node lives on + + def __post_init__(self) -> None: + if self.engine == "local": + raise ContainmentError( + f"node {self.node_id}: engine=='local' is forbidden — " + "isolated engine only (containment law §1)" + ) + + +@dataclass +class HousePool: + """A house realised as a distinct, isolated container pool (Track B).""" + + house: str + engine: str + net: str + size: int + nodes: List[SimNode] = field(default_factory=list) + + +def _derive_node_id(s0: int, house: str, idx: int) -> str: + """Deterministic node id from the base seed — mirrors the sealed seed formula.""" + h = hashlib.sha256(f"{s0}|{house}|{idx}".encode()).digest() + return h[:8].hex() + + +def plan_house(house: str, size: int, engine: str, s0: int) -> HousePool: + """Plan a house pool of ``size`` distinct isolated nodes. Does not launch anything. + + Raises ``ContainmentError`` if asked to plan on the local (non-isolated) engine. + """ + if engine == "local": + raise ContainmentError( + f"{house}: refuse to plan a pool on the local engine — isolated only" + ) + net = f"sor-{house}-net" + pool = HousePool(house=house, engine=engine, net=net, size=size) + for i in range(size): + pool.nodes.append( + SimNode(node_id=_derive_node_id(s0, house, i), house=house, engine=engine, net=net) + ) + return pool + + +def plan_federation( + house_sizes: Dict[str, int], engine: str, s0: int +) -> Dict[str, HousePool]: + """Plan several distinct house pools that will be federated. + + Each house gets its **own** isolated network + identities, so cross-house + split-knowledge is a real property of the topology, not a label. + """ + return {h: plan_house(h, n, engine, s0) for h, n in house_sizes.items()} + + +def assert_distinct(pools: Dict[str, HousePool]) -> None: + """Fail loudly unless every node id is globally unique and every net is per-house.""" + seen: set[str] = set() + nets: set[str] = set() + for pool in pools.values(): + if pool.net in nets: + raise ContainmentError(f"network {pool.net} reused across houses") + nets.add(pool.net) + for n in pool.nodes: + if n.node_id in seen: + raise ContainmentError(f"duplicate node id {n.node_id}") + seen.add(n.node_id) + + +def realise( + pools: Dict[str, HousePool], + launch_fn: Callable[[SimNode], str], +) -> Dict[str, List[str]]: + """Launch every planned node via the injected ``launch_fn`` (executor seam). + + ``launch_fn`` must actually start the node inside its isolated engine/network and + return a container handle. This function only enforces the containment invariant and + the distinctness check before delegating; it never shells out to docker itself. + """ + assert_distinct(pools) + handles: Dict[str, List[str]] = {} + for house, pool in pools.items(): + handles[house] = [] + for node in pool.nodes: + if node.engine == "local": # belt-and-braces + raise ContainmentError(f"{node.node_id}: local engine at launch") + handles[house].append(launch_fn(node)) + return handles diff --git a/cmd_chat/sor/watchdog.py b/cmd_chat/sor/watchdog.py new file mode 100644 index 0000000..5c8709e --- /dev/null +++ b/cmd_chat/sor/watchdog.py @@ -0,0 +1,112 @@ +"""Connectivity failsafe for load-bearing node pools — DRAFT (next experiment only). + +Purpose (see prereg NEXT-EXPERIMENT-DRAFT §2): when node pools are *load-bearing* for +the measured data (Track B/D), a run-control watchdog periodically probes every required +pool and, on an outage, fires a **pre-registered** policy — chosen in advance and echoed +into the manifest, never decided after seeing data. + +Policies: + * HALT (default, integrity-first): abort the run, mark the cell ``inconclusive`` (a + pre-committed outcome, so there is no optional-stopping bias), seal what exists. + * PAUSE_RESUME (only when determinism is provable): freeze the churn clock + seed + stream, wait (bounded) for the pool to return, then resume from the exact frozen + state. Legitimate only because the seed formula makes the resumed sequence + bit-identical. + +Why this did NOT apply to the sealed study: there, the phones were non-forwarding, so no +measured data depended on connectivity. Here the pools ARE in the data path, so the +watchdog protects real integrity. + +Self-contained + deterministic: the reachability probe is injected (no external target, +no sockets opened here). This is run-control logic, not a network client. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Callable, List, Optional + + +class Policy(str, Enum): + HALT = "halt" + PAUSE_RESUME = "pause_resume" + + +class Outcome(str, Enum): + OK = "ok" # all required pools reachable at every check + INCONCLUSIVE = "inconclusive" # HALT fired (or PAUSE timed out → escalated) + RESUMED = "resumed" # PAUSE fired and the pool returned in time + + +@dataclass +class FrozenState: + """The exact run state a PAUSE freezes so RESUME is bit-identical.""" + + churn_tick: int + seed_cursor: int + cell_id: str + + +@dataclass +class WatchdogReport: + policy: Policy + outcome: Outcome + checks: int = 0 + outages: List[int] = field(default_factory=list) # ticks at which a pool was down + paused_ticks: int = 0 + note: str = "" + + +@dataclass +class Watchdog: + """Pre-registered connectivity failsafe. + + ``probe`` returns True iff every REQUIRED pool is reachable. It is injected so this + module opens nothing and reaches no target. ``pause_budget`` bounds how long a + PAUSE_RESUME will wait (in ticks) before escalating to HALT. + """ + + policy: Policy + probe: Callable[[], bool] + pause_budget: int = 30 + + def check(self, state: FrozenState, report: WatchdogReport) -> Outcome: + """Run one watchdog check at the current frozen state. Deterministic.""" + report.checks += 1 + if self.probe(): + return Outcome.OK + + report.outages.append(state.churn_tick) + if self.policy is Policy.HALT: + report.outcome = Outcome.INCONCLUSIVE + report.note = ( + f"required pool unreachable at tick {state.churn_tick}; " + "HALT policy → cell inconclusive (pre-committed outcome)" + ) + return Outcome.INCONCLUSIVE + + # PAUSE_RESUME: clock + seed cursor are frozen in `state`; wait, bounded. + waited = 0 + while waited < self.pause_budget: + waited += 1 + report.paused_ticks += 1 + if self.probe(): + report.outcome = Outcome.RESUMED + report.note = ( + f"paused {waited} tick(s) at churn_tick={state.churn_tick}, " + f"seed_cursor={state.seed_cursor}; resumed bit-identically" + ) + return Outcome.RESUMED + + report.outcome = Outcome.INCONCLUSIVE + report.note = ( + f"PAUSE budget {self.pause_budget} exhausted at tick {state.churn_tick} " + "→ escalated to HALT (inconclusive)" + ) + return Outcome.INCONCLUSIVE + + def manifest_record(self) -> dict: + """What to echo into manifest.json so the policy is provably pre-committed.""" + return {"connectivity_failsafe": {"policy": self.policy.value, + "pause_budget_ticks": self.pause_budget}}