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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -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}}
|
||||||
Reference in New Issue
Block a user