"""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