Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion docs/adr/0014-field-societies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
103 changes: 103 additions & 0 deletions scripts/test_qos_society4.py
Original file line number Diff line number Diff line change
@@ -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())
Loading