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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,17 @@ jobs:
echo "Using kernel: $KERNEL_ELF"
QOS_KERNEL="$KERNEL_ELF" make ci-smoke-society-gate

# Society of societies (epic #178): two kernels, each running its OWN
# complete agent society (agentdemo token), coupled AND exchanging
# qseed-salted society aggregates over the FSYP wire — host-verified
# cross-appearance. Separate gate so the demo-free society timing above
# stays untouched; single-sourced timeout in the Makefile.
- name: Society of societies (epic #178)
run: |
KERNEL_ELF=$(find build/ -name "kernel.elf32" | head -1)
echo "Using kernel: $KERNEL_ELF"
QOS_KERNEL="$KERNEL_ELF" make ci-smoke-society-agents-gate

# N-way society (epic #139): three kernels mean-field-couple on a shared
# mcast L2. Separate single-sourced timeout; the gate FAILS LOUD if host
# multicast is unavailable (never a silent green — a skipped coupling gate
Expand Down
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,15 @@ ci-smoke: kernel
grep -E "AGENT|AGENTSUB" /tmp/qemu-boot.log 2>/dev/null || true; \
echo ""; echo "=== Smoke Test FAILED ==="; exit 1; \
fi
@# Society-of-societies handoff (epic #178): the orchestrator computed its
@# society's qseed-salted aggregate and handed it to fieldsyncd (which
@# broadcasts it to coupled peers; single-VM boots just hold it). Inside
@# the DEMO OK conjunction — a dead handoff cannot ship green.
@if ! grep -q "AGENT: aggregate a" /tmp/qemu-boot.log 2>/dev/null; then \
echo "ERROR: society aggregate not handed off (AGENT: aggregate a...)"; \
grep -E "AGENT" /tmp/qemu-boot.log 2>/dev/null || true; \
echo ""; echo "=== Smoke Test FAILED ==="; exit 1; \
fi
@if grep -qE "AGENT BROKEN|AGENTD: DEMO BROKEN|AGENTSUB BROKEN" /tmp/qemu-boot.log 2>/dev/null; then \
echo "ERROR: agent demo reported a BROKEN step"; \
grep -E "AGENT BROKEN|AGENTD: DEMO BROKEN|AGENTSUB BROKEN" /tmp/qemu-boot.log 2>/dev/null || true; \
Expand Down Expand Up @@ -1970,6 +1979,20 @@ ci-smoke-society-gate:
@echo "=== QuantumOS Agent Society Test (epic #131) ==="
timeout -k 5 $(SOCIETY_GATE_TIMEOUT) python3 scripts/test_qos_society.py

# Society of societies (epic #178): TWO kernels, each running its OWN complete
# agent society (agentdemo token), coupled AND exchanging qseed-salted society
# aggregates over the FSYP wire — host-verified cross-appearance. A separate
# gate from ci-smoke-society so the demo-free society timing stays untouched
# (design-review finding); generous timeout because the agentdemo work rides
# the same boots (the #171 lesson). Single-sourced like the others.
SOCIETY_AGENTS_GATE_TIMEOUT ?= 420s

ci-smoke-society-agents: kernel ci-smoke-society-agents-gate

ci-smoke-society-agents-gate:
@echo "=== QuantumOS Society-of-Societies Test (epic #178, two societies) ==="
timeout -k 5 $(SOCIETY_AGENTS_GATE_TIMEOUT) python3 scripts/test_qos_society_agents.py

# N-way society (epic #139): THREE kernels mean-field-couple on a shared mcast
# L2. A separate gate/timeout from the 2-node one (a 3-body field converges
# slower and the mcast transport differs) — the proven ci-smoke-society stays
Expand Down
23 changes: 23 additions & 0 deletions docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,29 @@ again relocate the format-unchanged authority ledger's home — existing
disks keep their durable ledger across the upgrade, and only the field
section honestly cold-starts at the superblock geometry guard.

**Society of societies (epic #178).** In a coupled boot each kernel runs its
OWN complete agent society, and the two societies **exchange results**: after
the division-of-labor verification, the orchestrator computes its society's
AGGREGATE (FNV-1a over the three specialist results, salted with FNV-1a over
the boot qseed's bytes so two members' aggregates provably differ), prints it
inside the `DEMO OK` conjunction, and hands it to `fieldsyncd` over an IPC
pair minted only in `agentdemo` boots (agentd discovers the pid via the
uncapped `SYSINFO_PS` text — the qtop pattern). `fieldsyncd` broadcasts it to
every configured peer as a fixed 16-byte **FSYP** frame riding the same ~1 Hz
cadence and peer-source validation as the phase frames — a **continuous
idempotent resend**, so a peer that boots later or loses datagrams still
converges — and prints each DISTINCT received value once:
`FIELDSYNC: peer aggregate a<hex>`. Received aggregates are **never imprinted
into the field**: the design review rejected field-content replication
(unauthenticated wire data must not become recallable or persistable field
content); the printed line is verified HOST-side by the
`test_qos_society_agents.py` gate, which recomputes both expected values from
the compile-time scheme + each boot's qseed and asserts cross-appearance on
both consoles (`ci-smoke-society-agents`). The same change fixed a latent
`fieldsyncd` demux bug: the receive drain treated any datagram under 260
bytes as WOULDBLOCK and stalled, so a short frame both vanished and ended the
drain — dispatch is now magic-first with per-type size checks.

`user/quantumd.c` is the essence of `kannaka-quantum`, and — unlike the two
above — a **kernel-embedded service**, not a `/bin` program. `SYS_QRAND` and
`SYS_QSEED` are capability-gated (a capless `/bin` caller gets EPERM by
Expand Down
24 changes: 24 additions & 0 deletions kernel/src/syscall.c
Original file line number Diff line number Diff line change
Expand Up @@ -2451,6 +2451,11 @@ void user_httpd_init(void) {
boot_log("epic98: httpd (ring 3) serving the status page on :8080");
}

/* fieldsyncd's pid, remembered for the agent demo (epic #178): agentd hands
* its society's aggregate to fieldsyncd over an IPC pair minted in
* user_agent_demo_init (agentdemo boots only). */
static uint32_t g_fieldsyncd_pid;

/* Bring up fieldsyncd — the UDP field-coupling bridge (epic #97). It holds
* the network cap (grant_net, for SYS_UDP) and an IPC send-cap to ghostd
* (with a reply cap back), so it can pull phase snapshots from the local
Expand Down Expand Up @@ -2483,6 +2488,7 @@ void user_fieldsync_demo_init(uint32_t ghostd_pid) {
cap_create(fs_pid, CAP_RESOURCE_IPC, ghostd_pid, CAP_READ | CAP_WRITE, 0, &cap);
cap_create(ghostd_pid, CAP_RESOURCE_IPC, fs_pid, CAP_READ | CAP_WRITE, 0, &cap);
service_monitor(sid, true);
g_fieldsyncd_pid = fs_pid;
boot_log("epic97: fieldsyncd (ring 3) bridges ghostd's field to a UDP peer");
}

Expand Down Expand Up @@ -2874,6 +2880,13 @@ void user_agent_demo_init(void) {
.field_region = 3,
.field_region_span = 4,
.grant_field_delegable = 1,
/* SYS_QSEED (epic #178): the society's published aggregate is salted
* with the boot identity (qseed) so two coupled VMs' aggregates
* provably DIFFER and the host can verify cross-appearance. Manifest
* budget: quantum 1 + spawn 1 + FIELD:3-6 4 + QPU 1 = 7 of 8 rows —
* ONE row of headroom left; a 9th would hit the fail-closed guard
* (epic #177) and be dropped loudly. Budget before adding grants. */
.grant_quantum_pool = 1,
};

uint32_t ag_sid = 0, ag_pid = 0;
Expand All @@ -2888,6 +2901,17 @@ void user_agent_demo_init(void) {
return;
}

/* Society-of-societies wiring (epic #178): agentd hands its aggregate to
* fieldsyncd over this pair (agentd discovers the pid via SYSINFO_PS, the
* qtop pattern); fieldsyncd broadcasts it to configured peers as an FSYP
* frame and prints received peer aggregates for host-side verification.
* Minted ONLY in agentdemo boots — every other boot is unchanged. */
if (g_fieldsyncd_pid != 0) {
uint32_t cap = CAP_ID_INVALID;
cap_create(ag_pid, CAP_RESOURCE_IPC, g_fieldsyncd_pid, CAP_READ | CAP_WRITE, 0, &cap);
cap_create(g_fieldsyncd_pid, CAP_RESOURCE_IPC, ag_pid, CAP_READ | CAP_WRITE, 0, &cap);
}

boot_log(
"agentd: agent-native society demo (orchestrator spawns + delegates to its own society)");
}
12 changes: 8 additions & 4 deletions scripts/qos_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -1324,10 +1324,13 @@ def is_running(self):
return (self.a is not None and self.a.is_running()
and self.b is not None and self.b.is_running())

def boot(self, qseed_a, qseed_b, expect_a=_UNSET, expect_b=_UNSET, timeout=45):
def boot(self, qseed_a, qseed_b, expect_a=_UNSET, expect_b=_UNSET, timeout=45,
extra_tokens=""):
"""Boot two coupled members. qseed_a != qseed_b is REQUIRED (identical
qseeds are a duplicate attested identity and a vacuous instant 'sync').
expect_a/expect_b verify against a DIFFERENT qseed (negative admission)."""
expect_a/expect_b verify against a DIFFERENT qseed (negative admission).
extra_tokens: additional cmdline tokens appended to BOTH members (e.g.
"agentdemo" for the society-of-societies gate, epic #178)."""
with self._lock:
if self.a is not None or self.b is not None:
raise QosError("society already booted — shut it down first")
Expand All @@ -1352,14 +1355,15 @@ def boot(self, qseed_a, qseed_b, expect_a=_UNSET, expect_b=_UNSET, timeout=45):
try:
# Directed udp= crossing must be EXACT: A localaddr=port_a sends to
# port_b; B mirrors. Get it backwards and phases flow one way only.
extra = f" {extra_tokens.strip()}" if extra_tokens.strip() else ""
self.a.boot(qseed=qseed_a, expect_qseed=expect_a, timeout=timeout,
quiet=False, arm_signals=False, mac=self._MAC_A,
append_extra=f"ip={self._NET_A} peer={self._NET_B}",
append_extra=f"ip={self._NET_A} peer={self._NET_B}{extra}",
netdev=("socket,id=n0,"
f"udp=127.0.0.1:{port_b},localaddr=127.0.0.1:{port_a}"))
self.b.boot(qseed=qseed_b, expect_qseed=expect_b, timeout=timeout,
quiet=False, arm_signals=False, mac=self._MAC_B,
append_extra=f"ip={self._NET_B} peer={self._NET_A}",
append_extra=f"ip={self._NET_B} peer={self._NET_A}{extra}",
netdev=("socket,id=n0,"
f"udp=127.0.0.1:{port_a},localaddr=127.0.0.1:{port_b}"))
# Both coupling NICs must come up — fieldsyncd logs this even under
Expand Down
139 changes: 139 additions & 0 deletions scripts/test_qos_society_agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""QuantumOS society-of-societies gate (epic #178). Stdlib-only, like the other
society gates: drives QosSociety directly and never imports `mcp`, so it runs in
the integration CI job with no pip.

Boots TWO attested QuantumOS VMs with BOTH the `agentdemo` token AND the
coupling wire, and proves, anti-vacuously:
1. each VM runs its OWN complete agent society locally: spawn-assembled
specialists, content consensus, division of labor, and the qseed-salted
AGGREGATE handoff (the full AGENTD: DEMO OK conjunction, per VM);
2. the two societies EXCHANGE results: each VM's COM1 shows
"FIELDSYNC: peer aggregate a<hex>" carrying the OTHER VM's aggregate —
the host recomputes both expected values from the compile-time phrase +
workspace digits + each boot's qseed, so a forged/looped-back local value
cannot satisfy the assertion (the two aggregates provably DIFFER because
the qseeds differ, which boot() already enforces);
3. the underlying fields also synchronize (the existing society property) —
with a GENEROUS budget, because the agentdemo work (a QPU job, four
spawns, field ops) rides the same boots (the #171 timing lesson);
4. shutdown reaps BOTH QEMU processes.

This gate is NEW rather than an extension of test_qos_society.py so the
existing society gates keep their demo-free timing (design-review finding).
"""
import os
import re
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from qos_bridge import QosSociety # noqa: E402

QSEED_A = "1111111111111111"
QSEED_B = "8888888888888888"

# Mirrors user/agentd.c exactly: the aggregate is FNV-1a over the three
# specialist result strings ("w%08x" of the phrase||digit digest), salted with
# hi32^lo32 of the boot qseed.
PHRASE = b"agentd end to end field phrase"
FNV_PRIME = 16777619
FNV_BASIS = 2166136261
M32 = 0xFFFFFFFF


def _fnv1a(data, h=FNV_BASIS):
for b in data:
h = ((h ^ b) * FNV_PRIME) & M32
return h


def expected_aggregate(qseed_hex):
agg = FNV_BASIS
for ws in (4, 5, 6):
rd = _fnv1a(PHRASE)
rd = ((rd ^ (ord("0") + ws)) * FNV_PRIME) & M32
for b in ("w%08x" % rd).encode():
agg = ((agg ^ b) * FNV_PRIME) & M32
# Identity salt mirrors agentd: FNV-1a over the qseed's 8 LE bytes (a
# plain hi32^lo32 fold is ZERO for palindromic qseeds like the CI pair).
q = int(qseed_hex, 16)
return agg ^ _fnv1a(q.to_bytes(8, "little"))


def _fail(msg):
print(f"FAIL: {msg}")
raise SystemExit(1)


def _await(pred, deadline, what):
while time.time() < deadline:
if pred():
return
time.sleep(0.5)
_fail(f"timeout awaiting {what}")


def main():
kernel = os.environ.get("QOS_KERNEL") or None
soc = QosSociety(kernel=kernel)
exp_a = expected_aggregate(QSEED_A)
exp_b = expected_aggregate(QSEED_B)
if exp_a == exp_b:
_fail("expected aggregates identical — qseed salt dead, gate vacuous")
own_re = re.compile(r"AGENT: aggregate a([0-9a-f]{8}) handed to fieldsyncd")
peer_re = re.compile(r"FIELDSYNC: peer aggregate a([0-9a-f]{8})")
try:
soc.boot(QSEED_A, QSEED_B, timeout=60, extra_tokens="agentdemo")
print("OK: two attested members booted with agentdemo + coupling")

# 1. Each VM completes its OWN society end to end (DEMO OK is a
# conjunction over qpu+field+spawn+society+division+aggregate).
deadline = time.time() + 90.0
_await(lambda: "AGENTD: DEMO OK" in soc.a._log_text()
and "AGENTD: DEMO OK" in soc.b._log_text(),
deadline, "AGENTD: DEMO OK on both members")
for vm, name, exp in ((soc.a, "A", exp_a), (soc.b, "B", exp_b)):
text = vm._log_text()
if "AGENT: division of labor 3/3" not in text:
_fail(f"member {name}: no division-of-labor proof")
m = own_re.search(text)
if not m:
_fail(f"member {name}: no aggregate handoff line")
if int(m.group(1), 16) != exp:
_fail(f"member {name}: own aggregate {m.group(1)} != expected "
f"{exp:08x} — recompute drifted from agentd")
print(f"OK: both societies complete; aggregates a{exp_a:08x} / a{exp_b:08x} verified")

# 2. EXCHANGE: each member's console must show the OTHER's aggregate,
# delivered as an FSYP frame from a validated peer source.
deadline = time.time() + 60.0
_await(lambda: any(int(v, 16) == exp_b
for v in peer_re.findall(soc.a._log_text())),
deadline, "member A receiving B's aggregate")
_await(lambda: any(int(v, 16) == exp_a
for v in peer_re.findall(soc.b._log_text())),
deadline, "member B receiving A's aggregate")
# Distinctness (anti-vacuous): a looped-back/forged LOCAL value can
# never satisfy the cross assertion above, but assert it explicitly.
for vm, name, own in ((soc.a, "A", exp_a), (soc.b, "B", exp_b)):
for v in peer_re.findall(vm._log_text()):
if int(v, 16) == own:
_fail(f"member {name} printed its OWN aggregate as a peer's "
"— self-frame leaked through peer validation")
print("OK: societies exchanged results — A holds B's aggregate and vice versa")

# 3. The fields still synchronize under the demo load (generous budget).
deadline = time.time() + 120.0
_await(lambda: (lambda s: s["a"]["synchronized"] and s["b"]["synchronized"])
(soc.status()), deadline, "cross-VM field sync (R_x)")
print("OK: both fields synchronized under agentdemo load")
finally:
soc.shutdown()
if soc.a is not None or soc.b is not None:
_fail("shutdown did not clear the society's handles")
print("=== SOCIETY-OF-SOCIETIES gate PASSED — two societies, one field, "
"results exchanged ===")


if __name__ == "__main__":
main()
62 changes: 62 additions & 0 deletions user/agentd.c
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,68 @@ static int do_delegate(void) {
}
printf("AGENT: division of labor %d/%d - every specialist published in its own region\n",
AGENT_SUBS, AGENT_SUBS);

/* SOCIETY OF SOCIETIES (epic #178): hand this society's AGGREGATE to
* fieldsyncd, which broadcasts it to coupled peer kernels (and prints any
* peer society's aggregate it receives, for host-side verification). The
* aggregate is fnv1a over the three specialist results, salted with the
* boot identity (qseed) so two VMs' aggregates provably differ — the host
* knows both qseeds and recomputes both expected values. Inside
* do_delegate so DEMO OK stays a conjunction over this step too. */
unsigned int agg = 2166136261u;
for (int i = 0; i < AGENT_SUBS; i++) {
unsigned ws = 4u + (unsigned)i;
unsigned int rd = fnv1a(AGENT_PHRASE, AGENT_PHRASE_LEN);
rd = (rd ^ (unsigned int)('0' + ws)) * 16777619u;
char rs[12];
snprintf(rs, sizeof(rs), "w%08x", rd);
for (unsigned n = 0; n < 9; n++) {
agg = (agg ^ (unsigned char)rs[n]) * 16777619u;
}
}
/* Identity salt: FNV-1a over the qseed's 8 LE bytes — NOT hi32^lo32,
* which collapses to ZERO for any qseed with equal halves (both CI
* society qseeds are palindromic; the gate's vacuity self-check caught
* exactly that). */
unsigned long long qs = (unsigned long long)qseed_value();
unsigned char qb[8];
for (unsigned i = 0; i < 8; i++) {
qb[i] = (unsigned char)(qs >> (8u * i));
}
agg ^= fnv1a(qb, 8);

/* fieldsyncd's pid comes from the uncapped SYSINFO_PS text (the qtop
* pattern): one "PS: <pid> <name> STATE" line per live process. */
static char ps[2048];
long pn = sysinfo(SYSINFO_PS, ps, sizeof(ps) - 1);
ps[(pn > 0) ? pn : 0] = '\0';
long fs_pid = 0;
for (long o = 0; ps[o]; o++) {
if ((o == 0 || ps[o - 1] == '\n') && ps[o] == 'P' && ps[o + 1] == 'S' && ps[o + 2] == ':' &&
ps[o + 3] == ' ') {
long p = 0, k = o + 4;
while (ps[k] >= '0' && ps[k] <= '9') {
p = p * 10 + (ps[k] - '0');
k++;
}
if (ps[k] == ' ' && ps[k + 1] == 'f' && ps[k + 2] == 'i' && ps[k + 3] == 'e' &&
ps[k + 4] == 'l' && ps[k + 5] == 'd' && ps[k + 6] == 's' && ps[k + 7] == 'y') {
fs_pid = p;
break;
}
}
}
if (fs_pid == 0) {
write_str("AGENT BROKEN aggregate: fieldsyncd not in PS");
return 0;
}
char am[12];
snprintf(am, sizeof(am), "A%08x", agg);
if (send_to(fs_pid, am, 9) != 0) {
printf("AGENT BROKEN aggregate send_to(%ld)\n", fs_pid);
return 0;
}
printf("AGENT: aggregate a%08x handed to fieldsyncd (qseed-salted)\n", agg);
return 1;
}

Expand Down
Loading
Loading