diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f2220c..5a3f56e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -828,6 +828,12 @@ jobs: echo "Using kernel: $KERNEL_ELF" QOS_KERNEL="$KERNEL_ELF" make ci-smoke-keyauth-n-gate + - name: N=4 society — the documented configuration ceiling (societies epic) + run: | + KERNEL_ELF=$(find build/ -name "kernel.elf32" | head -1) + echo "Using kernel: $KERNEL_ELF" + QOS_KERNEL="$KERNEL_ELF" make ci-smoke-society4-gate + integration: name: Integration Tests runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index d4698f8..5a54fe7 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ OBJECTS = $(KERNEL_SOURCES:$(KERNEL_DIR)/src/%.c=$(BUILD_DIR)/%.o) \ -include $(OBJECTS:.o=.d) # Targets -.PHONY: all clean kernel run debug dump test test-list test-coverage ci-smoke ci-smoke-mem256 ci-smoke-mem512 ci-smoke-sched ci-smoke-disk ci-smoke-disk-upgrade ci-smoke-net ci-smoke-http ci-smoke-httpd ci-smoke-quiet ci-smoke-resonant ci-smoke-qseed ci-smoke-swarm ci-smoke-mcp ci-smoke-mcp-gate ci-smoke-qsubmit ci-smoke-society ci-smoke-society-gate ci-smoke-society-agents ci-smoke-society-agents-gate ci-smoke-society3 ci-smoke-society3-gate ci-smoke-iso ci-smoke-kbd ci-smoke-noserial ci-smoke-screen swarm-pingpong +.PHONY: all clean kernel run debug dump test test-list test-coverage ci-smoke ci-smoke-mem256 ci-smoke-mem512 ci-smoke-sched ci-smoke-disk ci-smoke-disk-upgrade ci-smoke-net ci-smoke-http ci-smoke-httpd ci-smoke-quiet ci-smoke-resonant ci-smoke-qseed ci-smoke-swarm ci-smoke-mcp ci-smoke-mcp-gate ci-smoke-qsubmit ci-smoke-society ci-smoke-society-gate ci-smoke-society-agents ci-smoke-society-agents-gate ci-smoke-society3 ci-smoke-society3-gate ci-smoke-society4 ci-smoke-society4-gate ci-smoke-iso ci-smoke-kbd ci-smoke-noserial ci-smoke-screen swarm-pingpong all: kernel @@ -2335,6 +2335,26 @@ ci-smoke-society3-gate: @echo "=== QuantumOS N-Way Society Test (epic #139, three kernels) ===" timeout -k 5 $(SOCIETY3_GATE_TIMEOUT) python3 scripts/test_qos_society3.py +# N=4 society — the DOCUMENTED configuration ceiling (societies epic increment 2): +# FOUR kernels on a shared mcast L2, the max the host bridge addresses +# (_NET/_MAC are 4 entries) with each node's peer set at N-1=3 against a +# 4-slot ghostd table. Proves the full 4-cycle mesh (12 directed frame +# observations, per-IP in boot_n), four distinct attested identities, and +# min-pairwise R_x >= 0.80 from divergent (< 0.50) per-node starts, then a clean +# 4-way reap. Deliberately does NOT assert slot-full/eviction (unreachable at +# 3/4 slots, and unobservable). Larger timeout than society3: 4 QEMU-TCG VMs +# oversubscribe the runner's cores and dilate convergence wall-clock. FAILS LOUD +# if host multicast is unavailable. Revert-confirm: drop one member's qseed to a +# duplicate -> boot_n's distinct-seed guard reddens; shrink the roster to 3 -> +# this is society3, not the ceiling. See ADR-0014 for the true N=5 constant wall. +SOCIETY4_GATE_TIMEOUT ?= 600s + +ci-smoke-society4: kernel ci-smoke-society4-gate + +ci-smoke-society4-gate: + @echo "=== QuantumOS N=4 Society Test (societies epic, the documented ceiling) ===" + timeout -k 5 $(SOCIETY4_GATE_TIMEOUT) python3 scripts/test_qos_society4.py + # N-way authenticated swarm plane (epic #139 + ADR-0019): THREE kernels on a # shared mcast L2, and the host-admitted group session key gating the FIELD wire # across N>2. The gate does NOT assert on R_x/SYNCHRONIZED (a keyless node diff --git a/docs/adr/0014-field-societies.md b/docs/adr/0014-field-societies.md index 0ee58e5..587bb85 100644 --- a/docs/adr/0014-field-societies.md +++ b/docs/adr/0014-field-societies.md @@ -74,7 +74,26 @@ over COM2 and admits it to the L2. problem). - Watchdog-reborn `fieldsyncd`/peer IPC caps are not re-minted on restart (user/fieldsyncd.c:333-339) — a standing reliability gap that ADR-0019 turns into - a key-distribution outage and must fix in-phase. + a key-distribution outage. **Closed (2026-07-15):** ADR-0023 re-mints peer IPC + caps declaratively on every start, and swarm_svc re-forwards the cached group + key on a `fieldsyncd` pid change — a rebirth is now a re-admission, not an outage. + +> **Update (2026-07-15) — the N=4 configuration is now proven, and the ceiling +> stated precisely.** The societies epic adds `ci-smoke-society4-gate` +> (`scripts/test_qos_society4.py`): four attested kernels on a shared mcast L2, +> the full 4-cycle mesh (12 directed frame observations), four distinct +> identities, min-pairwise R_x ≥ 0.80 from divergent per-node starts, clean reap. +> The gate deliberately does **not** assert slot-full/eviction behavior: at N=4 +> each node has N-1 = 3 peers against a 4-slot ghostd table (3/4 occupied), so +> the slot-full and eviction branches are structurally unreachable and have no +> console observable — an "assert no eviction" would be vacuous. The binding +> ceilings, precisely: the host bridge's `_NET`/`_MAC` arrays are 4 entries (the +> wall this gate sits at); the `GHOST_MAX_PEERS`/`MAX_PEERS` = 4 constants bound +> *peers per node* to 4 and would actually admit **N = 5** (a 5th node means 4 +> peers each) — lifting past 5 is where the two lockstep constants plus the +> per-peer arrays must all grow together. The N-way authenticated wire (the group +> key gating field coupling at N > 2) landed alongside as `ci-smoke-keyauth-n` +> (ADR-0019 update). ## Evidence - Shipped in: PR #116 — static IP + ARP responder (raw-L2 prerequisite, epic #97) diff --git a/scripts/test_qos_society4.py b/scripts/test_qos_society4.py new file mode 100644 index 0000000..b42f591 --- /dev/null +++ b/scripts/test_qos_society4.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""QuantumOS N=4 society gate — the documented configuration ceiling (societies +epic, increment 2). Stdlib-only: drives QosSociety directly (the path the +qos_society_* MCP tools delegate to) and never imports `mcp`, so it runs in CI +with no pip. + +Boots FOUR attested QuantumOS VMs into ONE mean-field society on a shared mcast +L2 — the maximum the host bridge addresses (`_NET`/`_MAC` are 4 entries, +qos_bridge.py) and the same count `GHOST_MAX_PEERS`/`MAX_PEERS` bound each node's +peer set to (N-1 = 3 peers). Proves, anti-vacuously: + 1. all four members attest with their OWN distinct qseeds (four identities); + 2. each member receives phase frames from ALL THREE of its peers — 12 directed + frame observations, asserted per-IP inside boot_n; a 3-of-4 masquerade + cannot satisfy the full mesh; + 3. every field synchronizes to MIN-pairwise R_x >= 0.80 (min, not mean, so a + partial 3-of-4 lock cannot pass) AND each started DIVERGENT (a sub-0.50 + sample) — four divergent seeds mean-field converging is not a pairwise lock; + 4. shutdown reaps all four QEMU processes. + +Scope, stated honestly (per the epic's design review): N=4 puts THREE peers in +each node's four-slot ghostd table, so the slot table is at 3/4 — the slot-full +and eviction paths are structurally unreachable here and are NOT asserted (there +is no such thing as "no eviction" to observe). What this gate proves is the +DOCUMENTED N=4 configuration: full-mesh coupling, distinct identities, divergent +convergence, clean teardown. The true ceilings are the host `_NET`/`_MAC` arrays +(this N=4) and, one past them, the `GHOST_MAX_PEERS`/`MAX_PEERS`=4 constants +(which bound peers-per-node and would admit N=5) — see ADR-0014. + +Exit 0 on success. A SIGTERM (timeout) or any exception still reaps every VM via +the society's single atexit + signal reaper. +""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from qos_bridge import QosSociety, QosError # noqa: E402 + +# Four well-separated qseeds — distinct so each field STARTS divergent (the +# non-vacuous R_x-climb premise) and each attests a distinct identity. +QSEEDS = ["1111111111111111", "5555555555555555", + "aaaaaaaaaaaaaaaa", "ffffffffffffffff"] + + +def _fail(msg): + print(f"FAIL: {msg}") + raise SystemExit(1) + + +def _kernel(): + return os.environ.get("QOS_KERNEL") or None + + +def main(): + soc = QosSociety(kernel=_kernel()) + try: + # 1-2. Boot 4 members; boot_n's mcast reachability precheck asserts the + # full per-IP 4-cycle (each of the 4 nodes heard all 3 peers = 12 + # directed observations) or FAILS LOUD. Precheck deadline scales with N. + st = soc.boot_n(QSEEDS, timeout=60) + members = st["members"] + if len(members) != 4: + _fail(f"expected 4 members, got {len(members)}") + seeds = [m["identity"]["qseed"].upper() for m in members] + if not all(m["verified"] for m in members): + _fail(f"all four members must attest: {st}") + if len(set(seeds)) != 4: + _fail(f"four DISTINCT attested identities expected: {seeds}") + print(f"OK: four attested members booted, distinct qseeds {seeds}; " + f"each received frames from all three peers (12 directed observations)") + + # 3. All four synchronize on MIN-pairwise R_x, each from a divergent + # start. r_x_min is per-NODE (min over that node's live pairs, over time), + # so the divergence assert is per-node, not per-pair (the log carries no + # per-slot R_x). Timeout is generous: 4 QEMU-TCG VMs oversubscribe the + # runner's cores, dilating guest-tick (hence convergence) wall-clock. + final = soc.await_sync_n(threshold=0.80, timeout=240) + for i, node in enumerate(final["members"]): + if not node["synchronized"] or (node["r_x"] or 0) < 0.80: + _fail(f"member {i} did not synchronize (min-pairwise): {node}") + if node["r_x_min"] is None or node["r_x_min"] >= 0.50: + _fail(f"member {i} never started divergent — gate vacuous: {node}") + rxs = [(m["r_x"], m["r_x_min"]) for m in final["members"]] + print(f"OK: all four fields mean-field synchronized — (r_x, min) {rxs} " + f"— non-vacuous (each started < 0.50); four divergent seeds locking " + f"on min-pairwise proves a real 4-node mean field, not a lucky pair") + finally: + soc.shutdown() + if soc.members: + _fail("society did not clear its member handles on shutdown") + print("OK: society shutdown reaped all four members") + + if "mcp" in sys.modules: + _fail("the `mcp` package leaked into the stdlib-only test") + print("OK: no `mcp` import (integration CI stays pip-free)") + print("=== SOCIETY4 gate PASSED — four attested kernels, one mean field " + "(the documented ceiling) ===") + return 0 + + +if __name__ == "__main__": + sys.exit(main())