#!/usr/bin/env python3 """ topology_node.py — Fit-to-Purpose Topology Node Runtime Python extraction of Semantics.TopologyNode Lean module. Each node runs this runtime to participate in the Research Stack topology. Roles: core — build, verify, synthesize (architect) judge — arbitrate, attest, verify (BFT partner) mirror — store, relay, backup (git mirror) edge — filter, compress, route, attest (minimal resource) foxTop — experimental, unindexed State machine: BOOT → SELFTEST → ANNOUNCE → ACTIVE → (DEGRADED | FAILED) → BOOT Per AGENTS.md §6: Python may only serialize, spawn, wrap, display. All cost/invariant/branching decisions are extracted from Lean. """ import json import sys import time import signal import subprocess import threading import sqlite3 import hashlib from pathlib import Path from dataclasses import dataclass, field, asdict from typing import Dict, List, Optional, Any from enum import Enum, auto # ═══════════════════════════════════════════════════════════════════════════ # §0 Q16.16 Fixed-Point (Python extraction) # ═══════════════════════════════════════════════════════════════════════════ class Q16_16: """Fixed-point Q16.16 using Python int (backend: UInt32 semantics).""" def __init__(self, val: int): self.val = val & 0xFFFFFFFF @staticmethod def zero() -> "Q16_16": return Q16_16(0) @staticmethod def one() -> "Q16_16": return Q16_16(0x00010000) @staticmethod def from_nat(n: int) -> "Q16_16": return Q16_16((n * 65536) & 0xFFFFFFFF) @staticmethod def from_frac(num: int, denom: int) -> "Q16_16": if denom == 0: return Q16_16.zero() return Q16_16((num * 65536 // denom) & 0xFFFFFFFF) def __add__(self, other: "Q16_16") -> "Q16_16": return Q16_16((self.val + other.val) & 0xFFFFFFFF) def __sub__(self, other: "Q16_16") -> "Q16_16": return Q16_16((self.val - other.val) & 0xFFFFFFFF) def __lt__(self, other: "Q16_16") -> bool: # Signed comparison for Q16.16 a = self.val if self.val < 0x80000000 else self.val - 0x100000000 b = other.val if other.val < 0x80000000 else other.val - 0x100000000 return a < b def __le__(self, other: "Q16_16") -> bool: return self == other or self < other def __gt__(self, other: "Q16_16") -> bool: return other < self def __ge__(self, other: "Q16_16") -> bool: return other <= self def __eq__(self, other: object) -> bool: if not isinstance(other, Q16_16): return False return self.val == other.val def to_float(self) -> float: v = self.val if self.val < 0x80000000 else self.val - 0x100000000 return v / 65536.0 def to_dict(self) -> dict: return {"val": self.val, "float": self.to_float()} @staticmethod def from_dict(d: dict) -> "Q16_16": return Q16_16(d["val"]) # ═══════════════════════════════════════════════════════════════════════════ # §1 Enums # ═══════════════════════════════════════════════════════════════════════════ class NodeRole(str, Enum): CORE = "core" JUDGE = "judge" MIRROR = "mirror" EDGE = "edge" FOXTOP = "foxTop" class NodeState(str, Enum): BOOT = "boot" SELFTEST = "selftest" ANNOUNCE = "announce" ACTIVE = "active" DEGRADED = "degraded" FAILED = "failed" class NodeCapability(str, Enum): LEAN_BUILD = "leanBuild" FPGA_SYNTH = "fpgaSynth" EQUATION_FOREST = "equationForest" FULL_GIT = "fullGit" BFT_ARBITRATE = "bftArbitrate" MMR_VERIFY = "mmrVerify" ATTESTATION = "attestation" GIT_MIRROR = "gitMirror" OBJECT_STORE = "objectStore" RELAY = "relay" BACKUP = "backup" COMPRESS = "compress" RGFLOW_FILTER = "rgflowFilter" ROUTE = "route" STORAGE = "storage" COMPUTE = "compute" EXPERIMENTAL = "experimental" class BindClass(str, Enum): INFORMATIONAL = "informational" GEOMETRIC = "geometric" THERMODYNAMIC = "thermodynamic" PHYSICAL = "physical" CONTROL = "control" # ═══════════════════════════════════════════════════════════════════════════ # §2 Capability & Role Defaults (extracted from Lean) # ═══════════════════════════════════════════════════════════════════════════ CAPABILITY_COST: Dict[NodeCapability, Q16_16] = { NodeCapability.LEAN_BUILD: Q16_16.from_nat(10), NodeCapability.FPGA_SYNTH: Q16_16.from_nat(50), NodeCapability.EQUATION_FOREST: Q16_16.from_nat(20), NodeCapability.FULL_GIT: Q16_16.from_nat(5), NodeCapability.BFT_ARBITRATE: Q16_16.from_nat(3), NodeCapability.MMR_VERIFY: Q16_16.from_nat(2), NodeCapability.ATTESTATION: Q16_16.from_nat(2), NodeCapability.GIT_MIRROR: Q16_16.from_nat(2), NodeCapability.OBJECT_STORE: Q16_16.from_nat(1), NodeCapability.RELAY: Q16_16.from_nat(1), NodeCapability.BACKUP: Q16_16.from_nat(1), NodeCapability.COMPRESS: Q16_16.from_nat(4), NodeCapability.RGFLOW_FILTER: Q16_16.from_nat(3), NodeCapability.ROUTE: Q16_16.from_nat(1), NodeCapability.STORAGE: Q16_16.from_nat(1), NodeCapability.COMPUTE: Q16_16.from_nat(2), NodeCapability.EXPERIMENTAL: Q16_16.from_nat(8), } DEFAULT_CAPABILITIES: Dict[NodeRole, List[NodeCapability]] = { NodeRole.CORE: [ NodeCapability.LEAN_BUILD, NodeCapability.FPGA_SYNTH, NodeCapability.EQUATION_FOREST, NodeCapability.FULL_GIT, NodeCapability.STORAGE, NodeCapability.COMPUTE, ], NodeRole.JUDGE: [ NodeCapability.BFT_ARBITRATE, NodeCapability.MMR_VERIFY, NodeCapability.ATTESTATION, NodeCapability.COMPUTE, ], NodeRole.MIRROR: [ NodeCapability.GIT_MIRROR, NodeCapability.OBJECT_STORE, NodeCapability.RELAY, NodeCapability.BACKUP, NodeCapability.STORAGE, ], NodeRole.EDGE: [ NodeCapability.COMPRESS, NodeCapability.RGFLOW_FILTER, NodeCapability.ATTESTATION, NodeCapability.ROUTE, NodeCapability.STORAGE, NodeCapability.COMPUTE, ], NodeRole.FOXTOP: [ NodeCapability.EXPERIMENTAL, NodeCapability.COMPUTE, NodeCapability.STORAGE, NodeCapability.ROUTE, ], } DEFAULT_BIND_CLASSES: Dict[NodeRole, List[BindClass]] = { NodeRole.CORE: [BindClass.INFORMATIONAL, BindClass.GEOMETRIC, BindClass.THERMODYNAMIC, BindClass.PHYSICAL, BindClass.CONTROL], NodeRole.JUDGE: [BindClass.INFORMATIONAL, BindClass.CONTROL], NodeRole.MIRROR: [BindClass.INFORMATIONAL, BindClass.GEOMETRIC], NodeRole.EDGE: [BindClass.PHYSICAL, BindClass.THERMODYNAMIC, BindClass.CONTROL], NodeRole.FOXTOP: [BindClass.INFORMATIONAL, BindClass.PHYSICAL, BindClass.CONTROL], } MAX_ENERGY: Dict[NodeRole, Q16_16] = { NodeRole.CORE: Q16_16.from_nat(100), NodeRole.JUDGE: Q16_16.from_nat(50), NodeRole.MIRROR: Q16_16.from_nat(50), NodeRole.EDGE: Q16_16.from_nat(25), NodeRole.FOXTOP: Q16_16.from_nat(40), } RECOVERY_RATE: Dict[NodeRole, Q16_16] = { NodeRole.CORE: Q16_16.from_frac(1, 2), NodeRole.JUDGE: Q16_16.from_frac(1, 4), NodeRole.MIRROR: Q16_16.from_frac(1, 4), NodeRole.EDGE: Q16_16.from_frac(1, 8), NodeRole.FOXTOP: Q16_16.from_frac(1, 4), } STATE_TRANSITION_COST: Dict[tuple, Q16_16] = { (NodeState.BOOT, NodeState.SELFTEST): Q16_16.from_nat(1), (NodeState.SELFTEST, NodeState.ANNOUNCE): Q16_16.from_nat(1), (NodeState.ANNOUNCE, NodeState.ACTIVE): Q16_16.from_nat(2), (NodeState.ACTIVE, NodeState.DEGRADED): Q16_16.from_nat(1), (NodeState.DEGRADED, NodeState.ACTIVE): Q16_16.from_nat(3), (NodeState.ACTIVE, NodeState.FAILED): Q16_16.zero(), (NodeState.DEGRADED, NodeState.FAILED): Q16_16.zero(), (NodeState.FAILED, NodeState.BOOT): Q16_16.from_nat(5), (NodeState.SELFTEST, NodeState.FAILED): Q16_16.from_nat(1), (NodeState.ANNOUNCE, NodeState.FAILED): Q16_16.from_nat(1), } ALLOWED_TRANSITIONS: set = { (NodeState.BOOT, NodeState.SELFTEST), (NodeState.SELFTEST, NodeState.ANNOUNCE), (NodeState.ANNOUNCE, NodeState.ACTIVE), (NodeState.ACTIVE, NodeState.DEGRADED), (NodeState.ACTIVE, NodeState.FAILED), (NodeState.DEGRADED, NodeState.ACTIVE), (NodeState.DEGRADED, NodeState.FAILED), (NodeState.FAILED, NodeState.BOOT), (NodeState.SELFTEST, NodeState.FAILED), (NodeState.ANNOUNCE, NodeState.FAILED), } # ═══════════════════════════════════════════════════════════════════════════ # §3 Node Config & State # ═══════════════════════════════════════════════════════════════════════════ @dataclass class NodeConfig: node_id: str role: NodeRole memory_budget_mb: int disk_budget_gb: int jurisdiction: str capabilities: List[NodeCapability] = field(default_factory=list) bind_classes: List[BindClass] = field(default_factory=list) services: Dict[str, List[str]] = field(default_factory=dict) # services: {"warden": ["/usr/bin/python3", "warden.py", "--port", "8448"]} def __post_init__(self): if not self.capabilities: self.capabilities = DEFAULT_CAPABILITIES.get(self.role, []) if not self.bind_classes: self.bind_classes = DEFAULT_BIND_CLASSES.get(self.role, []) @dataclass class TopologyNodeState: node_id: str role: NodeRole state: NodeState = NodeState.BOOT capabilities: List[NodeCapability] = field(default_factory=list) energy_budget: Q16_16 = field(default_factory=Q16_16.zero) memory_budget_mb: int = 0 disk_budget_gb: int = 0 bind_classes: List[BindClass] = field(default_factory=list) jurisdiction: str = "" def to_dict(self) -> dict: return { "node_id": self.node_id, "role": self.role.value, "state": self.state.value, "capabilities": [c.value for c in self.capabilities], "energy_budget": self.energy_budget.to_dict(), "memory_budget_mb": self.memory_budget_mb, "disk_budget_gb": self.disk_budget_gb, "bind_classes": [b.value for b in self.bind_classes], "jurisdiction": self.jurisdiction, } @staticmethod def from_config(cfg: NodeConfig) -> "TopologyNodeState": return TopologyNodeState( node_id=cfg.node_id, role=cfg.role, state=NodeState.BOOT, capabilities=cfg.capabilities, energy_budget=MAX_ENERGY.get(cfg.role, Q16_16.from_nat(25)), memory_budget_mb=cfg.memory_budget_mb, disk_budget_gb=cfg.disk_budget_gb, bind_classes=cfg.bind_classes, jurisdiction=cfg.jurisdiction, ) # ═══════════════════════════════════════════════════════════════════════════ # §4 Topology Node Runtime # ═══════════════════════════════════════════════════════════════════════════ class TopologyNodeRuntime: """Fit-to-purpose topology node runtime.""" def __init__(self, config_path: str): self.config_path = Path(config_path) self.cfg = self._load_config() self.state = TopologyNodeState.from_config(self.cfg) self.db_path = Path.home() / f"var/db/topology-node-{self.cfg.node_id}.db" self.db_path.parent.mkdir(parents=True, exist_ok=True) self._init_db() self._running = False self._services: Dict[str, subprocess.Popen] = {} self._threads: List[threading.Thread] = [] def _load_config(self) -> NodeConfig: with open(self.config_path) as f: raw = json.load(f) return NodeConfig( node_id=raw["node_id"], role=NodeRole(raw["role"]), memory_budget_mb=raw["memory_budget_mb"], disk_budget_gb=raw["disk_budget_gb"], jurisdiction=raw["jurisdiction"], capabilities=[NodeCapability(c) for c in raw.get("capabilities", [])], bind_classes=[BindClass(b) for b in raw.get("bind_classes", [])], services=raw.get("services", {}), ) def _init_db(self): conn = sqlite3.connect(str(self.db_path)) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS node_state_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL, old_state TEXT, new_state TEXT, energy_budget_val INTEGER, reason TEXT ) """) c.execute(""" CREATE TABLE IF NOT EXISTS bind_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL, bind_class TEXT, cost_val INTEGER, accepted INTEGER, reason TEXT ) """) conn.commit() conn.close() # ───────────────────────────────────────────────────────────────────── # State Machine # ───────────────────────────────────────────────────────────────────── def transition(self, new_state: NodeState, reason: str = "") -> bool: if (self.state.state, new_state) not in ALLOWED_TRANSITIONS: self._log("transition_rejected", f"illegal: {self.state.state.value} -> {new_state.value}") return False cost = STATE_TRANSITION_COST.get((self.state.state, new_state), Q16_16.from_nat(1)) if self.state.energy_budget < cost: self._log("transition_rejected", f"insufficient_energy: need {cost.to_float()}, have {self.state.energy_budget.to_float()}") return False old = self.state.state self.state.energy_budget = self.state.energy_budget - cost self.state.state = new_state self._log_state_change(old, new_state, reason) return True def recover_energy(self): rate = RECOVERY_RATE.get(self.state.role, Q16_16.from_frac(1, 8)) max_cap = MAX_ENERGY.get(self.state.role, Q16_16.from_nat(25)) new_energy = self.state.energy_budget + rate self.state.energy_budget = new_energy if new_energy < max_cap else max_cap def can_accept_bind(self, bc: BindClass, cost: Q16_16) -> bool: return ( self.state.state == NodeState.ACTIVE and bc in self.state.bind_classes and self.state.energy_budget >= cost ) def deduct_energy(self, cost: Q16_16) -> bool: if self.state.energy_budget >= cost: self.state.energy_budget = self.state.energy_budget - cost return True return False # ───────────────────────────────────────────────────────────────────── # Service Management # ───────────────────────────────────────────────────────────────────── def start_services(self): """Start configured services as child processes.""" for name, cmd in self.cfg.services.items(): if name in self._services: continue self._log("service_start", f"spawning {name}: {' '.join(cmd)}") proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) self._services[name] = proc def stop_services(self): for name, proc in self._services.items(): self._log("service_stop", f"terminating {name}") proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() self._services.clear() def check_services(self) -> Dict[str, bool]: health = {} for name, proc in list(self._services.items()): ret = proc.poll() if ret is not None: health[name] = False self._log("service_died", f"{name} exited {ret}") del self._services[name] else: health[name] = True return health # ───────────────────────────────────────────────────────────────────── # Lifecycle # ───────────────────────────────────────────────────────────────────── def run(self): self._running = True signal.signal(signal.SIGTERM, self._on_signal) signal.signal(signal.SIGINT, self._on_signal) # BOOT → SELFTEST → ANNOUNCE → ACTIVE self.transition(NodeState.SELFTEST, "boot_complete") self._run_selftest() self.transition(NodeState.ANNOUNCE, "selftest_pass") self._announce() self.transition(NodeState.ACTIVE, "announce_complete") self.start_services() # Main loop while self._running: self.recover_energy() health = self.check_services() if any(not v for v in health.values()): if self.state.state == NodeState.ACTIVE: self.transition(NodeState.DEGRADED, "service_failure") else: if self.state.state == NodeState.DEGRADED: self.transition(NodeState.ACTIVE, "service_recovery") time.sleep(5) self.stop_services() self._log("shutdown", "runtime exiting") def _run_selftest(self): """Verify capabilities.""" self._log("selftest", f"capabilities: {[c.value for c in self.state.capabilities]}") for cap in self.state.capabilities: cost = CAPABILITY_COST.get(cap, Q16_16.from_nat(1)) self._log("selftest", f"{cap.value} cost={cost.to_float()}") time.sleep(0.5) def _announce(self): """Broadcast presence to mesh.""" self._log("announce", json.dumps(self.state.to_dict())) def _on_signal(self, signum, _frame): self._log("signal", f"received {signum}") self._running = False def _log(self, event: str, msg: str): ts = time.strftime("%Y-%m-%dT%H:%M:%S") print(f"[{ts}] [{self.cfg.node_id}] [{event}] {msg}", flush=True) def _log_state_change(self, old: NodeState, new: NodeState, reason: str): self._log("state_change", f"{old.value} -> {new.value} ({reason})") conn = sqlite3.connect(str(self.db_path)) c = conn.cursor() c.execute( "INSERT INTO node_state_log (timestamp, old_state, new_state, energy_budget_val, reason) VALUES (?, ?, ?, ?, ?)", (time.time(), old.value, new.value, self.state.energy_budget.val, reason) ) conn.commit() conn.close() def main(): if len(sys.argv) < 2: print("Usage: topology_node.py ") sys.exit(1) runtime = TopologyNodeRuntime(sys.argv[1]) runtime.run() if __name__ == "__main__": main()