//! server — a minimal broker server (Part 7/8 boundary). Listens on a Unix
//! socket, parses the line-JSON protocol through the trust gate, and drives
//! the facade. This is the Zig side of the no-FFI boundary: it speaks BYTES
//! to Python, sharing no library, no memory — only the socket.
//!
//! Deliberately tiny and synchronous (one client at a time) for the skeleton;
//! the real facade (Part 7) multiplexes via the rt event loop. The point here
//! is to PROVE the Python↔Zig socket boundary works end-to-end with real
//! processes.
//!
//! TRUST-GATE DISCIPLINE (S1 / Part 12 §3.2). Every byte from the client
//! passes through `request.parse()` and becomes a typed `Request` before any
//! dispatch logic touches it. NO code below the gate ever reads from
//! `std.json.Value` — the gate has already validated op, fields, types,
//! lengths, and integer ranges. A malformed line becomes `Err.BadRequest`
//! and a typed `{"ok":false,"err":"BadRequest"}` response; the broker stays
//! up.

const std = @import("std");
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const cap = @import("mgf_hrb_cap");
const core_mod = @import("mgf_hrb_core");
const Core = core_mod.Core;
const facademod = @import("mgf_hrb_facade");
const FakeBackend = @import("mgf_hrb_fake_backend").FakeBackend;
const LinuxBackend = @import("mgf_hrb_linux_backend").LinuxBackend;
const LinuxUsbBackend = @import("mgf_hrb_linux_usb").LinuxUsbBackend;
const fdpass = @import("mgf_hrb_fdpass");
const subscription = @import("mgf_hrb_subscription");
const shm = @import("mgf_hrb_shm");
const irq_monitor = @import("mgf_hrb_irq_monitor");
const linux_irq = @import("mgf_hrb_linux_irq");
const lbound = @import("mgf_hrb_linux_bound");
const restart_marker = @import("mgf_hrb_restart_marker");
const driver_registry = @import("mgf_hrb_driver_registry");
const audit = @import("mgf_hrb_audit");
const UsbBackendViaIpc = @import("mgf_hrb_usb_backend_via_ipc").UsbBackendViaIpc;
const admission = @import("mgf_hrb_admission");
const dispatcher = @import("mgf_hrb_dispatch");
const event_pump = @import("mgf_hrb_event_pump");
const build_options = @import("build_options");

const Urn = model.Urn;
const Err = model.Err;
const Deadline = kabi.Deadline;
const linux = std.os.linux;

/// Cheap env-var presence check via /proc/self/environ.
/// Returns true iff `name` appears (with any value, including
/// empty). Zig 0.16 hides the std.process.Environ singleton
/// behind an Allocator-needing API; this raw-syscall path
/// matches the broker's "no allocator on the boot path" style.
fn envSet(name: []const u8) bool {
    const fd_rc = linux.open(
        "/proc/self/environ",
        .{ .ACCMODE = .RDONLY },
        0,
    );
    if (linux.errno(fd_rc) != .SUCCESS) return false;
    const fd: i32 = @intCast(fd_rc);
    defer _ = linux.close(fd);
    var buf: [4096]u8 = undefined;
    const n_rc = linux.read(fd, &buf, buf.len);
    if (linux.errno(n_rc) != .SUCCESS) return false;
    const n: usize = n_rc;
    // Walk NUL-separated entries; check if any starts with `name=`.
    var i: usize = 0;
    while (i < n) {
        const end = std.mem.indexOfScalarPos(u8, buf[0..n], i, 0) orelse n;
        const entry = buf[i..end];
        if (entry.len > name.len and
            std.mem.startsWith(u8, entry, name) and
            entry[name.len] == '=')
            return true;
        i = end + 1;
    }
    return false;
}

/// Read an unsigned-integer env var via /proc/self/environ, returning
/// `fallback` when it's absent or unparseable. Same raw-syscall,
/// no-allocator style as `envSet`. Used for the operator-tunable
/// slowloris deadline (R-T1-9C).
fn envU64(name: []const u8, fallback: u64) u64 {
    const fd_rc = linux.open("/proc/self/environ", .{ .ACCMODE = .RDONLY }, 0);
    if (linux.errno(fd_rc) != .SUCCESS) return fallback;
    const fd: i32 = @intCast(fd_rc);
    defer _ = linux.close(fd);
    // /proc/self/environ is a proc file: a single read can return a
    // partial slice, and a full process environment routinely exceeds a
    // few KiB. Loop-read into a generous buffer so a var set late in the
    // environment block (env dicts are unordered) is still found.
    var buf: [16384]u8 = undefined;
    var n: usize = 0;
    while (n < buf.len) {
        const r_rc = linux.read(fd, buf[n..].ptr, buf.len - n);
        if (linux.errno(r_rc) != .SUCCESS) return fallback;
        const r: usize = r_rc;
        if (r == 0) break;
        n += r;
    }
    var i: usize = 0;
    while (i < n) {
        const end = std.mem.indexOfScalarPos(u8, buf[0..n], i, 0) orelse n;
        const entry = buf[i..end];
        if (entry.len > name.len and
            std.mem.startsWith(u8, entry, name) and
            entry[name.len] == '=')
        {
            const val = entry[name.len + 1 ..];
            return std.fmt.parseInt(u64, val, 10) catch fallback;
        }
        i = end + 1;
    }
    return fallback;
}

/// Read a string env var via /proc/self/environ into `out`, returning the
/// slice (or null if absent / empty / longer than `out`). Same raw-syscall,
/// no-allocator style as `envU64`. Used for the socket-path override
/// (`MGF_HRB_SOCK`) that lets the test suite give each pytest-xdist worker
/// its own broker socket — and lets operators relocate the socket.
fn envStr(name: []const u8, out: []u8) ?[:0]const u8 {
    const fd_rc = linux.open("/proc/self/environ", .{ .ACCMODE = .RDONLY }, 0);
    if (linux.errno(fd_rc) != .SUCCESS) return null;
    const fd: i32 = @intCast(fd_rc);
    defer _ = linux.close(fd);
    var buf: [16384]u8 = undefined;
    var n: usize = 0;
    while (n < buf.len) {
        const r_rc = linux.read(fd, buf[n..].ptr, buf.len - n);
        if (linux.errno(r_rc) != .SUCCESS) return null;
        const r: usize = r_rc;
        if (r == 0) break;
        n += r;
    }
    var i: usize = 0;
    while (i < n) {
        const end = std.mem.indexOfScalarPos(u8, buf[0..n], i, 0) orelse n;
        const entry = buf[i..end];
        if (entry.len > name.len and
            std.mem.startsWith(u8, entry, name) and
            entry[name.len] == '=')
        {
            const val = entry[name.len + 1 ..];
            if (val.len == 0 or val.len + 1 > out.len) return null;
            @memcpy(out[0..val.len], val);
            out[val.len] = 0; // NUL-terminate for syscalls that need [*:0]
            return out[0..val.len :0];
        }
        i = end + 1;
    }
    return null;
}

pub fn main() !void {
    const alloc = std.heap.page_allocator;

    // Part 13 §5.4.2 — initialise the subscription registry. One
    // process-global registry; per-peer cap_id keys the partition.
    // page_allocator backs both Subscription structs + their arenas.
    subscription.init(alloc);

    // Part 5.3 — install the broker-wide shm region pool (the data-plane
    // channels open_shm_channel hands out). One process-global pool;
    // per-uid private regions, S5-bounded.
    try shm.initGlobal(alloc);
    // D7 (Tier-2) — interrupt-rate containment monitor + the real
    // /proc/interrupts counter source (PCI; USB is unattributable → null).
    try irq_monitor.initGlobal(alloc, irq_monitor.DEFAULT_MAX_IRQ_PER_SEC);
    irq_monitor.setCounterSource(linux_irq.counterSource());

    // Tier-1 #9 — install admission-control config. The app shell owns env
    // parsing (envU64); `wire/admission` stays env-free + testable. Absent
    // an env var, the compiled defaults stand. The contract suite tunes
    // these to observe sheds deterministically.
    admission.configure(.{
        .rl_burst_tokens = envU64("MGF_HRB_RL_BURST", admission.RL_DEFAULT_BURST_TOKENS),
        .rl_refill_per_sec = envU64("MGF_HRB_RL_REFILL_PER_SEC", admission.RL_DEFAULT_REFILL_PER_SEC),
        .slowloris_deadline_ms = envU64("MGF_HRB_SLOWLORIS_MS", admission.SLOWLORIS_DEADLINE_MS),
    });

    // Part 13 §5.2.3 — initialise the curated ioctl-interface
    // registry. JSON files are @embedFile'd into the binary at
    // compile time; init() parses them once and caches the result
    // for the lifetime of the process. Refresh-on-SIGHUP (per
    // spec) is a follow-up — current sequence is "regenerate JSON
    // → zig build → restart broker".
    driver_registry.init(alloc);

    // Part 13 §5.6.1 — initialise the broker's audit ring. Starts
    // the uptime clock + zeros the counters. The audit push path
    // in `handle` records every wire op (including parse failures);
    // `audit_tail` / `state_snapshot` ops read from it.
    audit.init();

    // Ignore SIGPIPE — a write() to a peer that closed before we
    // finished sending raises SIGPIPE by default, which would kill
    // the broker. This is a routine occurrence (e.g. `mgf-hrb
    // doctor`'s `check_socket` connects-then-closes immediately to
    // probe reachability; the broker's subsequent hello write hits
    // the closed peer). Per Part 12 §3 survivability: a misbehaving
    // peer must not be able to kill the broker. EPIPE on write
    // surfaces as an error return we already silently discard
    // (the peer is gone; nothing to report).
    var sa = std.mem.zeroes(linux.Sigaction);
    sa.handler.handler = linux.SIG.IGN;
    _ = linux.sigaction(linux.SIG.PIPE, &sa, null);

    // Fixed socket path for the skeleton (the test uses the same path). Avoids
    // the churning argv API; the real broker reads this from config (Part 8.4).
    // Future: argv override for per-worker pytest-xdist isolation (deferred
    // alongside the xdist enablement itself).
    // Socket path: `MGF_HRB_SOCK` override (lets each pytest-xdist worker
    // bind its own socket so the contract suite parallelises without
    // collision; also lets operators relocate it), else the default.
    var sock_buf: [108]u8 = undefined; // sockaddr_un.path capacity
    const sock_path = envStr("MGF_HRB_SOCK", &sock_buf) orelse "/tmp/mgf-hrb.sock";

    // Build the live constellation: one worker per bus subtree, each with
    // its own Core + real Linux backend. The PCI and USB backends are
    // genuine substrate readers (sysfs + USBDEVFS); no seeded fakes.
    //
    // The e2e test path (tests/e2e/test_e2e.py) reaches for fake-seeded
    // PCI URNs and so will skip / be hardware-gated on hosts without
    // matching real devices. The fake-PCI demo path lives in the
    // FakeBackend conformance suite (hrb-code/src/conformance.zig) where
    // it belongs — the broker should reflect what hardware actually
    // exposes, not a sprinkle of demo data.
    var ct = cap.CapTable.init(alloc);

    var pci_backend = LinuxBackend.init(alloc);
    var pci_core = Core.initWithCaps(alloc, pci_backend.backend(), &ct);

    var usb_backend = LinuxUsbBackend.init(alloc);
    defer usb_backend.deinit();
    // Tier-0 subprocess refactor — env-var opt-in. When
    // MGF_HRB_USB_USE_HELPER is set (any value), the broker
    // routes the 6 privileged USB ops through the
    // `mgf-hrb-usb-priv` helper subprocess. Reads stay in-
    // process via the same LinuxUsbBackend. Default off — the
    // existing direct-mode keeps working until the operator
    // opts in.
    const use_helper = envSet("MGF_HRB_USB_USE_HELPER");
    var usb_ipc = UsbBackendViaIpc.init(alloc, &usb_backend);
    defer if (use_helper) usb_ipc.deinit();
    const usb_kernel_backend: kabi.KernelBackend = if (use_helper)
        usb_ipc.backend()
    else
        usb_backend.backend();
    if (use_helper) {
        std.debug.print(
            "broker: USB worker uses helper subprocess (MGF_HRB_USB_USE_HELPER set)\n",
            .{},
        );
    }
    var usb_core = Core.initWithCaps(alloc, usb_kernel_backend, &ct);

    // `hw:/test/` worker — a FakeBackend with two seeded synthetic devices
    // for the Python↔Zig e2e test (`tests/e2e/test_e2e.py`). Kept in the
    // skeleton broker because the e2e test depends on URNs that don't
    // change across hosts; production builds can drop this worker once a
    // separate e2e-broker target exists. Claiming a real PCI function via
    // VFIO is too invasive for an automated test — the synthetic test
    // worker keeps the round-trip honestly exercisable.
    var test_backend = FakeBackend.init(alloc);
    defer test_backend.deinit();
    // Enable fd-passing so the e2e test can SCM_RIGHTS-receive a fake raw
    // channel against test/dev0 — the broker dup()s a memfd and sends it.
    // Lets the round-trip exercise the kernel ABI without claiming real
    // hardware (which would detach a kernel driver from a real device).
    test_backend.pass_fd_for_raw = true;
    try test_backend.add("hw:/test/dev0", .claimable, .pcie_function);
    try test_backend.add("hw:/test/dev1", .claimable, .pcie_function);
    // G6/G2 (review wave 2 — Cluster L regression pins, 2026-05-27):
    // adversarial-attr fixture. Seeds a synthetic URN whose inspect
    // attrs deliberately contain characters that need JSON-escaping
    // (`"`, `\`, control chars) AND a 200-char "serial" — exercises
    // the json_escape.zig + the inspect-buffer's 256-byte capacity
    // through the wire so `Client.inspect` survives both.
    //
    // Production-safe: the URN lives under the `hw:/test/` worker
    // (which is already the synthetic-fixture scope; real
    // deployments drop this worker entirely). No risk to real
    // hardware enumeration.
    const _g6_edge_serial: []const u8 = "F4GS9PWWJCM7-" ++ "X" ** 200;
    const _g6_edge_attrs = [_]kabi.Attr{
        // Quote + backslash: pre-G2 (commit e9ea7e9) these crashed
        // json.loads on the client side. The json_escape.zig fix
        // ships them as `\"` and `\\`.
        .{ .key = "json_dquote", .value = "has\"a-quote" },
        .{ .key = "json_bslash", .value = "has\\a-slash" },
        // Long value to exercise the inspect-emit buffer.
        .{ .key = "long_serial", .value = _g6_edge_serial },
    };
    try test_backend.addWithAttrs(
        "hw:/test/edge_attrs",
        .claimable,
        .pcie_function,
        &_g6_edge_attrs,
    );
    var test_core = Core.initWithCaps(alloc, test_backend.backend(), &ct);

    var workers = [_]facademod.Worker{
        .{ .scope_prefix = "hw:/pci/", .core = &pci_core, .backend_name = "mgf-hrb-linux" },
        .{ .scope_prefix = "hw:/usb/", .core = &usb_core, .backend_name = "mgf-hrb-usb" },
        .{ .scope_prefix = "hw:/test/", .core = &test_core, .backend_name = "mgf-hrb-fake" },
    };
    var facade = facademod.Facade.initWithCaps(&workers, &ct);

    // --- listen on a Unix socket (raw syscalls; Linux specifics in this layer) ---
    // SOCK.CLOEXEC: the listen fd must NOT leak into the privileged
    // helper subprocess (`mgf-hrb-usb-priv`) or any other fork+exec
    // descendant — that would let a compromised helper accept new
    // broker clients with the broker's bound cap.
    const fd: i32 = @intCast(linux.socket(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0));
    defer _ = linux.close(fd);
    var addr = std.mem.zeroes(linux.sockaddr.un);
    addr.family = linux.AF.UNIX;
    @memcpy(addr.path[0..sock_path.len], sock_path);
    _ = linux.unlink(sock_path); // best-effort cleanup
    const bind_rc = linux.bind(fd, @ptrCast(&addr), @sizeOf(linux.sockaddr.un));
    if (linux.errno(bind_rc) != .SUCCESS) {
        // I2 (review wave 2) — startup-time bind failure used to
        // print just "bind failed" with no errno, no path, no uid.
        // Operators had no way to tell EACCES (wrong uid for that
        // path) vs EADDRINUSE (stale broker / another process) vs
        // ENOENT (parent dir missing). This is the SINGLE MOST
        // consequential failure at startup; loud + complete here
        // saves a lot of forensic work.
        std.debug.print(
            "broker: bind({s}) failed errno={s} uid={d} — " ++
                "broker cannot start. Check: socket-dir perms? stale broker? parent dir exists?\n",
            .{ sock_path, @tagName(linux.errno(bind_rc)), linux.getuid() },
        );
        return;
    }
    // Listen backlog: the depth of the kernel's pending-accept queue. The old
    // value (8) was tight for a multi-client broker — a burst of concurrent
    // connects (e.g. an SDK opening its main + event-stream pair, or a client
    // reconnecting under contention) faster than the single accept loop drains
    // them overflows the queue and the kernel returns ECONNREFUSED to the
    // connector (a spurious, retry-worthy failure, not back-pressure). 128 is
    // generous headroom while still bounded; the per-uid admission cap (S5) is
    // the real concurrency limit, applied AFTER accept.
    _ = linux.listen(fd, 128);
    std.debug.print("mgf-hrb broker listening on {s}\n", .{sock_path});

    // R-T1-8 (a) — self-restart detection. A marker beside the socket records
    // "an instance is running"; finding one a prior instance left (a crash / kill
    // — NOT a host reboot, which clears tmpfs) means we restarted, so re-derive
    // truth from the hardware (§4.3, "re-derive don't persist"). The auto-reconcile
    // uses the STRICT bound source — it quarantines ONLY confident vfio-pci orphans,
    // never disrupting idle / USB devices. A clean exit removes the marker.
    var marker_buf: [256]u8 = undefined;
    const marker_path = std.fmt.bufPrintZ(&marker_buf, "{s}.restart", .{sock_path}) catch sock_path;
    const restarted = restart_marker.checkAndMark(marker_path);
    defer restart_marker.clear(marker_path);
    // The accept loop below never returns, so the `defer` above only fires in
    // tests — in production the broker stops on a signal. Install SIGTERM +
    // SIGINT handlers that remove the marker before exiting, so an operator
    // stop (systemd `stop`, `kill`, Ctrl-C) is recorded as a CLEAN exit and the
    // next start is cold. A crash / SIGKILL can't run a handler → marker stays
    // → correctly detected as a restart.
    restart_marker.installCleanupHandler(marker_path);
    if (restarted) {
        const r = facade.reconcileAfterRestart("hw:/**", lbound.boundSourceStrict(), kabi.Deadline.fromNow(5000));
        std.debug.print(
            "mgf-hrb: RESTART detected (prior instance left a marker) — re-derived hardware: " ++
                "{d} bound device(s) scanned, {d} orphan(s) quarantined for recovery\n",
            .{ r.scanned, r.quarantined },
        );
    } else {
        std.debug.print("mgf-hrb: cold start (no prior-instance marker)\n", .{});
    }

    // M-B B2 — the in-broker event pump. Spawned HERE, beside the accept
    // loop, because this is where the constellation exists: one thread
    // that polls every worker backend's watch fd and forwards what it
    // drains to `subscription.pushFanout`.
    //
    // Ordering: after `reconcileAfterRestart` and before the accept loop.
    // After the reconcile because that sweep quarantines orphans through
    // the core, whose B1 sink already announces each one — starting the
    // pump first would add a concurrent writer to the same fan-out during
    // a startup scan that has no subscribers yet anyway. Before the accept
    // loop because a client that connects must never find a broker that
    // accepts subscriptions but has nothing feeding them.
    var pump = event_pump.Pump{};
    wireEventPump(&pump, &workers);
    pump.start() catch |e| {
        // The broker is still useful without it (lease events still flow
        // from the core's own sink), so this is loud, not fatal.
        std.debug.print(
            "broker: event pump failed to start ({s}) — HARDWARE events will not " ++
                "reach subscribers this run; lease events are unaffected\n",
            .{@errorName(e)},
        );
    };
    // The accept loop below never returns, so this fires only on the
    // loop's error exit (and in tests); production stops on a signal and
    // the process teardown collects the thread. Registered anyway so the
    // shutdown path is honest about owning it (DP-13).
    defer pump.stop();

    // Thread-per-client accept loop (Part 7.1 multi-client
    // constellation). The serial-accept predecessor would block any
    // second connection from the same process until the first one
    // closed — that broke `monitor_fd`'s natural shape of "open a
    // dedicated event-stream connection alongside the main one"
    // (FEEDBACK SDK-01 implementation surface). With one thread per
    // accepted client, the broker can handle many concurrent
    // connections; the Core + CapTable are protected by their own
    // mutexes so the shared facade stays safe.
    //
    // Concurrency cap: B2 — without an upper bound, a hostile (or
    // buggy) caller could keep opening connections until the kernel
    // refuses to spawn another thread, taking the broker down. We
    // gate spawn behind a process-wide atomic counter. New
    // connections past the cap are politely closed with a TooBusy
    // response so SDKs can back off rather than seeing connection-
    // refused at the kernel layer.
    while (true) {
        // SOCK.CLOEXEC on the accepted client fd: same rationale as
        // the listen socket — the cfd must not leak into any helper
        // subprocess this thread later spawns (the privileged USB
        // helper is forked from the client thread on first claim).
        const cfd_rc = linux.accept4(fd, null, null, linux.SOCK.CLOEXEC);
        if (linux.errno(cfd_rc) != .SUCCESS) break;
        const cfd: i32 = @intCast(cfd_rc);

        // B2 — concurrent-connection cap. Atomic CAS keeps the
        // counter accurate under multiple concurrent accepts (only
        // one accept loop in current code, but defensive against
        // future multi-accept). When at cap, write a polite
        // TooBusy and close — the SDK sees a structured error
        // instead of a hung connection.
        if (!admission.aggregateAcquire()) {
            _ = linux.write(cfd, admission.TOO_BUSY_LINE.ptr, admission.TOO_BUSY_LINE.len);
            _ = linux.close(cfd);
            continue;
        }

        // Spawn a thread to handle this client. The thread takes
        // ownership of `cfd` (closes it on exit). Lifetime of the
        // captured `&facade`, `&ct`, `alloc` references
        // is `main()` — they outlive every client thread.
        const t = std.Thread.spawn(.{}, handleClient, .{
            cfd, &facade, &ct, alloc,
        }) catch |e| {
            // Spawn failed: close the socket and continue the accept
            // loop. The kernel-side accept queue still has us listening.
            //
            // I2 (review wave 2) — the original print had no cfd, no
            // error name, no active-clients count. An operator
            // chasing "the broker keeps dropping connections" needs
            // exactly those three fields: which connection, why
            // (ENOMEM / EAGAIN — kernel thread cap, vs other), and
            // whether we were near MAX_ACTIVE_CLIENTS.
            const live_after = admission.aggregateRelease();
            _ = linux.close(cfd);
            std.debug.print(
                "broker: thread spawn failed cfd={d} err={s} " ++
                    "active_clients={d}/{d} — closing connection, accept loop continues\n",
                .{ cfd, @errorName(e), live_after, admission.MAX_ACTIVE_CLIENTS },
            );
            continue;
        };
        // We don't join client threads — the broker process exit
        // collects them. `detach` releases the kernel resources
        // immediately on thread exit; no zombie accumulation.
        t.detach();
    }
}

/// M-B B2 — register one pump source per worker backend that HAS a
/// buffered event source, and skip the rest.
///
/// "Has one" is read straight off the seam: a backend whose vtable slot
/// `drain_events` is null has nothing to drain (PCI's LinuxBackend and
/// the fake test backend today), so it is skipped without a syscall. For
/// the rest we call `watch()` once to obtain the readiness fd —
/// `WatchHandle.id` IS the fd (`linux_usb.zig`'s netlink socket) — and
/// hand the pump `{fd, ctx, drain-fn}`. The sink passed to `watch` is a
/// no-op by the seam's own contract: watch OPENS the source, drain
/// DELIVERS from it.
///
/// A watch that fails (no netlink permission, for instance) skips that
/// worker loudly. The broker keeps running: it simply has no hardware
/// events for that bus, which is exactly what was true before the watch
/// was attempted, and saying so beats pretending.
fn wireEventPump(pump: *event_pump.Pump, workers: []facademod.Worker) void {
    const NoopSink = struct {
        fn push(_: *anyopaque, _: model.Event) void {}
    };
    for (workers) |*w| {
        const be = w.core.backend;
        const drain = be.vtable.drain_events orelse continue; // no event source
        var scope_buf: [event_pump.MAX_SCOPE_BYTES]u8 = undefined;
        const scope = std.fmt.bufPrint(&scope_buf, "{s}**", .{w.scope_prefix}) catch continue;

        var sink_ctx: u8 = 0;
        const sink = kabi.EventSink{ .ctx = &sink_ctx, .push = NoopSink.push };
        const handle = be.watch(scope, &sink, kabi.Deadline.fromNow(5_000)) catch |e| {
            std.debug.print(
                "broker: event pump could not open watch on {s} ({s}) — no hardware " ++
                    "events from that bus this run\n",
                .{ scope, @errorName(e) },
            );
            continue;
        };
        pump.addSource(@intCast(handle.id), be.ctx, drain, scope) catch |e| {
            std.debug.print(
                "broker: event pump refused source {s} ({s})\n",
                .{ scope, @errorName(e) },
            );
            continue;
        };
        std.debug.print("broker: event pump watching {s} (fd={d})\n", .{ scope, handle.id });
    }
}

/// B1 — per-client request buffer ceiling. The largest legitimate
/// single request (subscribe with a full predicate AST) fits
/// comfortably in a few KiB; 64 KiB is room for ~16× headroom.
/// Past this, the client is either buggy or hostile — we close
/// with a typed BadRequest rather than letting `pending` grow
/// without bound.
const MAX_PENDING_BYTES: usize = 64 * 1024;

/// G1 (review wave 2) — the typed "rejected at the cap layer"
/// wire response. Hoisted to a named constant so:
///   1. The length is `.len` (not a magic 36, which used to drift
///      past the literal's actual 34 bytes and leak 2 stack bytes
///      onto the wire on every peer-cred / cap-bind rejection).
///   2. Every reject site refers to the same byte sequence, so a
///      future edit (e.g. adding a `"detail":"..."` field) can't
///      desynchronise two write sites that need to stay in lock-
///      step.
/// The trailing `\n` is the line-framing terminator; SDK
/// `_recv_line` reads up to and including it.
const NOT_PERMITTED_LINE: []const u8 = "{\"ok\":false,\"err\":\"NotPermitted\"}\n";

/// D-038 — the broker-assigned connection identity, and the source of
/// every lease's `holder`.
///
/// Monotonic, and deliberately NOT the cfd. The cfd is the right key
/// for subscriptions and shm regions (both reaped on this connection's
/// teardown) but it is the WRONG key for ownership: the kernel recycles
/// descriptor numbers, so a cfd-keyed holder admits an ABA hazard where
/// a new connection inherits a dead one's leases. Reclamation ordering
/// happens to make that unreachable today (see the teardown defer in
/// `handleClient`), which is exactly why it must not be relied on — a
/// future defer reorder would turn it into a silent cross-connection
/// ownership bug. A counter costs one atomic add and removes the
/// question permanently.
///
/// Starts at 1: `request.zig` validates client-supplied ids as >= 1,
/// and 0 stays available as "no holder" for anything that needs it.
var next_conn_id: std.atomic.Value(u64) = std.atomic.Value(u64).init(1);

/// The authority every accepted connection is minted (D-039).
///
/// `grant` (R-CAP-ATTEN-WIRE) lets a connection mint attenuated sub-caps
/// from its own via the `attenuate` op — delegating a NARROWER view to a
/// sub-component. Safe to give every connection: attenuation only ever
/// shrinks authority, so a sub-cap can never escalate beyond its parent.
/// `detach_driver` carries the atomic disconnect-then-claim path
/// (USB-worker §5.10).
const CONN_RIGHTS = cap.RightSet.of(&.{
    .observe,
    .claim_exclusive,
    .raw,
    .detach_driver,
    .audit_read,
    .steal,
    .grant,
});

/// Handle one accepted client end-to-end: peer-cred read, cap bind,
/// hello handshake, then a request/response loop until the client
/// disconnects. Runs in its own thread (Part 7.1) — must be self-
/// contained (own stack buffers, no shared mutable state past
/// `facade` + `ct` which carry their own mutexes).
fn handleClient(
    cfd: i32,
    facade: *facademod.Facade,
    ct: *cap.CapTable,
    alloc: std.mem.Allocator,
) void {
    defer _ = linux.close(cfd);
    // Part 13 §5.6.2 — count this connection in the broker's
    // active_connections counter that `state_snapshot` reports.
    // Defer the decrement so it runs regardless of how the
    // function exits.
    audit.incActiveConnections();
    defer audit.decActiveConnections();
    // B2 — release our slot in the process-wide concurrent-client
    // cap so the accept loop can admit another connection.
    defer _ = admission.aggregateRelease();
    // Part 13 §5.4.2 — reap every subscription this connection opened
    // when it closes. Keyed by the cfd (connection-unique while
    // live); the spec is "client disconnect releases all its
    // subscriptions" and this is where that happens.
    //
    // ORDERING — load-bearing, same LIFO reasoning as the `close(cfd)`
    // defer above (RC-B2-01, delta court). `subscription.hidden_slots` is
    // sized to `admission.MAX_ACTIVE_CLIENTS` on the argument that a
    // hidden subscription is one-per-connection, so the two counts must
    // never disagree. Zig runs defers LIFO, so registering this one LAST
    // makes it run FIRST — before `aggregateRelease` frees our connection
    // slot. Registered earlier (as it was), the release ran first and left
    // a window in which the accept loop could admit a replacement while
    // our hidden slot was still linked: 129 hidden subs wanted from a
    // 128-entry table. Nothing observed it, because the window is a few
    // instructions and the table has to be full — which is exactly why it
    // must hold by construction rather than by the order two unrelated
    // defers happen to be written in. Do not move this above the
    // admission release.
    defer subscription.reapPeer(@intCast(cfd));

    // Part 12 §3.3 Tier-0 #2 — connection-bound capabilities.
    // SO_PEERCRED on this cfd; the connection's own capability is minted
    // from it below, bound at birth (D-039).
    //
    // G1 (2026-05-26 review wave 2) — the previous shape of this code
    // passed a HARD-CODED LENGTH 36 to `linux.write` for the literal
    // `{"ok":false,"err":"NotPermitted"}\n`. That literal is 34 bytes,
    // not 36, so every peer-cred rejection or cap-bind rejection
    // leaked TWO BYTES OF STACK MEMORY onto the wire after the JSON.
    // The fix uses `.len` so future edits to the literal cannot drift
    // again. The constant is pulled out to a top-level `const NOT_PERMITTED_LINE`
    // so both call sites refer to the same definition (cluster L3 of
    // the docs-drift audit also noted this pattern).
    const peer = cap.peerCredOf(cfd) catch |e| {
        _ = linux.write(cfd, NOT_PERMITTED_LINE.ptr, NOT_PERMITTED_LINE.len);
        // I-cluster (logging): without context, an operator at 3am sees
        // a client mysteriously disconnect. Include the cfd, the errno
        // family, and the configured socket so they can correlate with
        // their connection-side logs.
        std.debug.print(
            "broker: peerCredOf cfd={d} failed: {s} — client rejected (NotPermitted)\n",
            .{ cfd, @errorName(e) },
        );
        return;
    };
    // D-039 / CAP-01 — THIS CONNECTION'S capability, minted here and
    // bound to its peer at birth.
    //
    // What this replaces: the broker used to mint ONE root capability at
    // startup and have every connection call `bindTo` on it. `bound_to`
    // is set once and was never cleared, so the FIRST process to connect
    // owned the broker's only capability for the broker's lifetime and
    // every later process — including the same program restarted, since
    // the bind is on {uid, pid} — was refused at the handshake with
    // NotPermitted. Multi-operator was not unimplemented but impossible.
    //
    // Binding at mint rather than after it removes the shared mutable
    // `bound_to` entirely: there is no contention left to arbitrate, so
    // the rejection path that used to live here has no reason to exist.
    // It is also strictly stronger than the old scheme — two connections
    // from the SAME {uid, pid} now get distinct capabilities, where
    // before they shared one and the binding could not tell them apart.
    const conn_cap = ct.mint(.{
        .scope = Urn.from("hw:/**"),
        .rights = CONN_RIGHTS,
        .bound_to = peer,
    }) catch |e| {
        _ = linux.write(cfd, NOT_PERMITTED_LINE.ptr, NOT_PERMITTED_LINE.len);
        std.debug.print(
            "broker: cap mint failed for peer uid={d} pid={d}: {s} — " ++
                "client rejected (NotPermitted)\n",
            .{ peer.uid, peer.pid, @errorName(e) },
        );
        return;
    };
    // The other half of CAP-01: without this the table would grow by one
    // entry per connection forever, and `CapTable.get` is a linear scan,
    // so the leak would degrade every authorization check as the broker
    // runs. `revoke` is transitive, so any sub-caps this connection
    // minted via `attenuate` die with it — authority must not outlive
    // the connection it derived from.
    defer _ = ct.revoke(conn_cap);

    // S5 / Tier-1 #9 (R-T1-9B) — per-tenant connection cap. Checked
    // AFTER the peer-cred read (we need the uid) and BEFORE the hello,
    // so an over-cap connection receives a TooBusy line instead of a
    // working cap_id. The release defer is registered only on a
    // successful acquire, so the reject path doesn't decrement.
    if (!admission.uidConnAcquire(peer.uid)) {
        _ = linux.write(cfd, admission.TOO_BUSY_LINE.ptr, admission.TOO_BUSY_LINE.len);
        std.debug.print(
            "broker: per-uid connection cap hit uid={d} (MAX_CONN_PER_UID={d}) — " ++
                "rejected with TooBusy; tenant should back off\n",
            .{ peer.uid, admission.MAX_CONN_PER_UID },
        );
        return;
    }
    defer admission.uidConnRelease(peer.uid);
    // Part 5.3 — a connection's shm data-plane regions never outlive it
    // (mirrors subscription.reapPeer above, keyed on the same per-connection
    // cfd): zero + munmap + close every region THIS CONNECTION created. Keyed
    // on the cfd, NOT peer.uid — else a second same-uid connection (e.g. a
    // dedicated event-stream alongside the main one) disconnecting would zero
    // this connection's still-live regions (the "bad magic 0x0" flake).
    defer _ = shm.globalPool().releaseAllForConn(@intCast(cfd));

    // D-038 / G3 — this connection's ownership identity, and the
    // reclamation that makes D-011's "ghost ownership is structurally
    // impossible" true of the running broker rather than only of the
    // test suite.
    //
    // Every lease claimed over this connection is held by `conn_id`
    // (dispatch ignores the client-declared `holder`/`caller`), so one
    // `holderDied` on the way out releases exactly this connection's
    // leases and nothing else. Before this, `Facade.holderDied` had no
    // production caller at all: a client that crashed holding an
    // exclusive lease stranded the device until the broker restarted.
    //
    // ORDERING — load-bearing. Zig runs defers LIFO, and
    // `defer close(cfd)` is registered first in this function, so it
    // runs LAST. Registering the reclamation here means it runs BEFORE
    // the descriptor is closed, and therefore before the kernel can
    // reissue that number to a new connection. Do not move this above
    // the close defer.
    const conn_id: u64 = next_conn_id.fetchAdd(1, .monotonic);
    defer _ = facade.holderDied(conn_id);

    // Tier-0 #2 layer-1 hello handshake.
    //
    // `client_id` (D-038) tells the client the identity the broker
    // assigned it, so it can recognise its OWN rows in `list_leases`
    // and distinguish them from other operators' — the thing a
    // multi-operator console needs in order to say "yours" rather than
    // just listing holders. Additive: the SDK reads `cap_id` by name
    // and ignores unknown keys.
    // M-B B2 — `version` is the REPO's version, read from pyproject.toml at
    // build time (`build.zig`'s `projectVersion`). It used to be the
    // literal "0.1.0" and had been wrong since 0.1.1: nine releases of a
    // broker telling every connecting client the wrong number. Verified
    // before the change that no consumer parses it — the SDK stashes it in
    // a log field, hrb-app renders it — so this is a correction, not a
    // wire break.
    var hello_buf: [192]u8 = undefined;
    const hello = std.fmt.bufPrint(
        &hello_buf,
        "{{\"ok\":true,\"hello\":\"mgf-hrb\",\"cap_id\":{d},\"client_id\":{d},\"version\":\"{s}\"}}\n",
        .{ conn_cap, conn_id, build_options.broker_version },
    ) catch return;
    _ = linux.write(cfd, hello.ptr, hello.len);

    var rbuf: [4096]u8 = undefined;
    var pending: std.ArrayListUnmanaged(u8) = .empty;
    defer pending.deinit(alloc);

    // R-T1-9C — slowloris guard. SO_RCVTIMEO makes read() return EAGAIN
    // after RECV_POLL_MS so the loop can enforce a progress deadline
    // even when the client sends nothing. `partial_since_ms` marks when
    // the current INCOMPLETE request line first arrived (0 = no partial
    // in flight); a line that doesn't COMPLETE within
    // SLOWLORIS_DEADLINE_MS of its first byte drops the connection.
    // Time-since-FIRST-byte (not since-last-byte) means trickling one
    // byte at a time can't reset the deadline.
    // Poll interval is derived from the deadline (see admission.recvPollMs).
    cap.setRecvTimeoutMs(cfd, admission.recvPollMs());
    var partial_since_ms: u64 = 0;

    while (true) {
        const n = linux.read(cfd, &rbuf, rbuf.len);
        const e = linux.errno(n);
        if (e == .SUCCESS) {
            if (n == 0) return; // EOF — client gone
            // B1 — bound the per-client buffer. Without this cap, a
            // single client streaming bytes without ever sending a
            // `\n` would grow `pending` until the broker OOMs. 64 KiB
            // comfortably fits the largest legitimate single request
            // (subscribe with full predicate); past that the client
            // is either buggy or hostile. We surface a typed error
            // and close the connection rather than silently dropping.
            if (pending.items.len + @as(usize, @intCast(n)) > MAX_PENDING_BYTES) {
                const err = "{\"ok\":false,\"err\":\"BadRequest\",\"detail\":\"request line exceeds 64 KiB\"}\n";
                _ = linux.write(cfd, err.ptr, err.len);
                return;
            }
            pending.appendSlice(alloc, rbuf[0..n]) catch return;

            // process complete lines
            while (std.mem.indexOfScalar(u8, pending.items, '\n')) |nl| {
                const line = pending.items[0..nl];
                // Response buffer sized for the chattiest op (`dump_all` —
                // every URN in scope serialised in one response). 64 KiB
                // comfortably fits ~200 inspected URNs at ~300 bytes each;
                // beyond that, `dump_all` truncates honestly. Previously
                // 16 KiB (commit 271bdd4, raised for `list` against full
                // hardware) — Part 13 §5.5.3 bumps to 64 KiB for the new
                // bulk-introspection surface.
                var out: [65536]u8 = undefined;
                const result = dispatcher.handle(facade, peer, @intCast(cfd), conn_id, line, &out);
                if (result.fd_to_send >= 0) {
                    fdpass.sendFdWithPayload(cfd, result.resp, result.fd_to_send) catch {};
                    _ = linux.close(result.fd_to_send);
                } else {
                    _ = linux.write(cfd, result.resp.ptr, result.resp.len);
                }
                const rest = pending.items[nl + 1 ..];
                std.mem.copyForwards(u8, pending.items[0..rest.len], rest);
                pending.shrinkRetainingCapacity(rest.len);
            }
        } else if (e == .AGAIN) {
            // Read deadline tick — no new bytes. Fall through to the
            // slowloris check; if no partial is stalled we just loop.
        } else {
            return; // real socket error — client gone
        }

        // R-T1-9C slowloris progress deadline — runs after a read OR a
        // timeout tick. A non-empty `pending` is an in-flight partial
        // request; it must COMPLETE (hit a `\n`, draining `pending`)
        // within SLOWLORIS_DEADLINE_MS of its first byte.
        //
        // M-B B2 — WHY A LONG-POLLING CONNECTION IS SAFE HERE.
        //
        // `drain_events` with `wait_ms` blocks inside `dispatcher.handle`,
        // above, for up to 30 s. Nothing in this loop can kill it, and the
        // reason is structural rather than a matter of tuning:
        //
        //   * This is ONE `while` body. While dispatch runs, no `read` is
        //     in flight, so `SO_RCVTIMEO` cannot fire against a connection
        //     that is merely waiting for an event.
        //   * The deadline below is armed ONLY by a non-empty `pending` —
        //     an INCOMPLETE request line. A line that reached dispatch was
        //     already consumed from `pending` by the inner loop, so a
        //     connection idling in a long poll has nothing armed.
        //
        // Verified, not assumed: with MGF_HRB_SLOWLORIS_MS=300 (a 10× tighter
        // deadline than the wait) a 3 000 ms long poll answered at 3 001 ms
        // and the connection stayed usable.
        //
        // One nuance, recorded so the next reader does not have to
        // rediscover it: a client that PIPELINES a partial next line behind
        // a long-polling request leaves that partial in `pending` across the
        // wait, so its deadline is measured from before the wait and can be
        // spent by it. Strict request/response consumers — which is every
        // consumer the SDK produces — never have a partial in flight while a
        // request is outstanding, so none can hit this. Closing it would mean
        // moving the deadline's clock past dispatch; deliberately not done
        // here, where the change would be invisible to every test.
        if (pending.items.len > 0) {
            const now = kabi.nowMs();
            if (partial_since_ms == 0) {
                partial_since_ms = now;
            } else if (now - partial_since_ms > admission.slowlorisDeadlineMs()) {
                const err = "{\"ok\":false,\"err\":\"Timeout\",\"detail\":\"request line not completed within deadline\"}\n";
                _ = linux.write(cfd, err.ptr, err.len);
                std.debug.print(
                    "broker: slowloris guard dropped cfd={d} uid={d} — partial " ++
                        "request stalled > {d}ms (S5 R-T1-9C)\n",
                    .{ cfd, peer.uid, admission.slowlorisDeadlineMs() },
                );
                return;
            }
        } else {
            partial_since_ms = 0;
        }
    }
}
