# The device manager **Status: the protocol and supervision are built** (M18.1, 2026-07-13): `hello` with its deadline, supervised spawn, restart with backoff, and the crash-loop cap are in — usb-xhci-bus is the first conforming driver, and the `driver-restart` scenario proves fault → backoff → re-claim → cap end to end. Tree reports are built too (M18.2, 2026-07-13): the xHCI driver scans its root-hub ports and reports each connected device (`child_added`); the manager mirrors them and prunes a dead reporter's children, and the `usb-report` scenario proves report → prune → respawn → re-report. The application surface is built (M18.3, 2026-07-13): `enumerate` and `subscribe` over IPC, with `device-list` as the first client — the manager is now the one answer to "what devices exist" for applications. The primitives underneath are real ([process-management.md](../os-development/process-management.md): spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first per-device driver spawn works (the device manager matches the xHCI controller by PCI class and spawns `usb-xhci-bus` with the device id as argv[1]). This document designs the rest: the device manager as **the tree, the matcher, and the supervisor** — the policy process that turns [resilience.md](../os-development/resilience.md)'s restart goal into practice for drivers. How processes stop, reload, and report their deaths is deliberately **not** in this document: that is the universal lifecycle every danos process speaks — [process-lifecycle.md](../os-development/process-lifecycle.md), signals over IPC and the stable `process` interface. The device manager is that design's first serious customer, not its owner. Its own protocol contains nothing lifecycle-shaped; a driver is stopped, health-checked, and buried exactly like any other process. ## The tree: structure in the manager, authority in the kernel The device tree is two things fused: *information* (what exists, how it nests) and *authority* (a descriptor is a licence to map physical memory). They separate: - The **kernel keeps the capability system** — device, I/O-port, and interrupt claims, resource containment on `device_register`, the `mmio_map`/`irq_bind`/`msi_bind` gates — and **cleans all of it up when a process dies** (settled; it is increment 1 of [process-lifecycle.md](../os-development/process-lifecycle.md)). The three invariants in [driver-model.md](driver-model.md) stay exactly where they are. A device manager that could mint MMIO mappings by its own say-so would be a second kernel, and a buggy one would un-earn everything the microkernel bought. - The **device manager owns the tree as data** — identity, topology, naming, driver matching, hotplug events, and being the one process everything else asks about devices. Firmware discovery seeds it (today via the kernel's snapshot); **bus drivers grow it** by reporting what they see; applications query and watch it. `device_enumerate` fades to a manager-internal (then deleted) seam. Long-term, discovery itself leaves the kernel — but not *into* the manager. PCI enumeration is a **pci-bus driver**: the manager spawns it against the host bridge (already a device with the ECAM window as a resource), it scans, it reports functions like any bus reports children. ACPI becomes an **acpi service** that interprets the tables and reports the namespace. The manager only orchestrates and merges. Moving AML interpretation out of ring 0 is its own project on its own track; nothing here depends on when it lands. (It landed: [discovery.md](../os-development/discovery.md), M19–M20.) `device_register` is **idempotent on exact match**: a re-registration with an identical (parent, class, identity, resources) tuple returns the existing id instead of appending a duplicate. The kernel table has no unregister, so without this a restarted registering bus would re-report its children as fresh nodes on every respawn. Idempotence is what makes restart-and-re-report sound for *every* reporting bus — pci-bus, the acpi service, a future fdt service — not just one, and it is why supervision (below) can prune a dead bus's subtree and trust the restarted instance to rebuild exactly the same ids. ## The protocol A `device-manager-protocol` module, defined through the [envelope](../os-development/protocol-namespace.md): every packet — request, reply, and pushed event alike — begins with the folded `Header`, and **the device id is `Header.target`**, the manager's object addressing. The contract is bound at `/protocol/device-manager`; the kernel-stamped badge tells the manager who is talking; the same endpoint receives its children's exit notifications — one loop, one world. | Direction | Packet | Purpose | |---|---|---| | driver → manager | `hello { role, version }` @ the assigned device | confirms the argv assignment, starts the deadline clock | | bus → manager | `child_added { parent, bus_address, identity, bus, vendor, device, subsystem, hid }` @ the registered device id | one node the bus discovered | | bus → manager | `child_removed { parent, bus_address }` | unplug, or the bus lost it | | app → manager | `enumerate` (reserved verb 1) | snapshot of the tree: one `ChildEntry` per record in the reply's tail | | app → manager | `subscribe` (reserved verb 2) | receive published add/remove events; the subscriber's endpoint rides as the call's capability | | manager → app | `child_added` / `child_removed` events | the same two structs, pushed rather than called | The watcher table behind those last two rows is the **service harness's** (`service.Subscribers`, shared with input and power), not the manager's: it answers `subscribe`/`unsubscribe`, frames each event once for the fan-out, and sweeps a watcher on its exit notification — where the manager previously had no sweep for watchers at all. Its own supervised-driver exits are a different thing and unchanged, except that a driver's death now arrives twice (the manager is both its supervisor and a subscriber to published exits), so the manager retires a dead driver's process id as it handles the first and the second finds nothing to act on. Two of what used to be the manager's own operations are the envelope's **reserved** verbs, which mean the same thing at every provider in the system, so this protocol numbers only three of its own (`hello` = 16, `child_added` = 17, `child_removed` = 18) and its two events in their own space (`child_added` = 16, `child_removed` = 17). No reply carries a status field: that is the `Status` every reply begins with. `child_added` is the one struct that travels both ways — a bus *calls* it, the manager *pushes* it — which is why the operation and event numbering spaces are separate: one encoding, both directions, told apart by which way the packet went. Folding the operation byte and the device id out of it is also what makes it fit: a pushed event is 64 bytes at most, header included, and this one lands exactly on that floor. `child_removed` is the single message whose target stays 0, because it is addressed by the composite (parent, bus address) and no single `u64` carries a pair. `hello` is the one deadline the manager enforces itself: spawned and silent past the deadline means wrong binary, wrong protocol version, or wedged before main — apply the stop sequence and the restart policy. Everything else lifecycle-shaped (terminate, the common `ping` liveness call, exit reasons) arrives through [process-lifecycle.md](../os-development/process-lifecycle.md)'s vocabulary, not this protocol. Assignment stays argv (`usb-xhci-bus `) for now — simple, and it works. The step after `hello` exists is delegation: the manager claims (or is granted) the devices and passes the claim to the driver over IPC (the M13 capability-transfer mechanism), replacing first-come-first-served `device_claim` with policy. Identity in `child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the xHCI match already uses); USB children carry the (class, subclass, protocol) triple from usb-ids.zig — each bus's native language, decoded by the shared ids modules. (Since the registry landed, `child_added` also carries a `bus` discriminator and the numeric `vendor`/`device`/`subsystem` ids the finer match levels need — see [/etc/devices.csv](devices-csv.md).) ## Supervision and restart Every driver is spawned with the manager's exit endpoint (`spawnSupervised` — built). On a death notification: 1. **Read the reason** ([process-lifecycle.md](../os-development/process-lifecycle.md) increment 2). Clean exit → it meant to; don't restart. Fault or missed `hello` deadline → restart with **backoff**, and a crash-loop cap (three fast deaths → mark failed, stop respawning, log loudly; a later `reload` to the manager can retry). 2. **Prune the subtree** the dead bus driver reported. Its children describe protocol state (xHCI slot ids, transfer rings) that died with the process; keeping the nodes would be keeping a lie. Watchers receive `child_removed` — the input service losing, then regaining, a keyboard is the *honest* description of what happened. The restarted instance rediscovers and re-reports. 3. **The claim is already free** because the kernel released it at death — the restarted instance claims the same controller and comes up. Who supervises the supervisor: **init** (PID 1), which already supervises the services it starts. If the manager dies, drivers keep running (they hold their claims; the kernel doesn't care who their supervisor was — though their exit notifications now dangle harmlessly). The restarted manager re-learns the world: kernel snapshot, then a re-`hello` round — drivers answer a broadcast or are stopped and respawned. Full state handoff is deliberately not attempted. ## Thin drivers, class protocols The [driver-model.md](driver-model.md) three-shape split, restated as processes: - A **bus driver** (usb-xhci-bus) owns its controller — claim, MMIO, IRQ/MSI, DMA rings — and offers a *transfer* protocol ("submit a control transfer to device N", built from the usb-abi request constructors) plus tree reports to the manager. - A **class driver** (usb-hid, usb-storage) owns nothing: it is matched to a reported child by its identity triple, speaks the bus's transfer protocol downward and its service's protocol upward — HID reports to the input service, blocks to the block service. It works unchanged over any controller. - **Services** (input, display, block) aggregate class drivers and face applications. Each arrow is a protocol module. The manager routes none of the data plane — it introduces the parties (matching), supervises them (lifecycle), and gets out of the way. ## Increments Increments 1–4 are the lifecycle prerequisites and live in [process-lifecycle.md](../os-development/process-lifecycle.md) (claim cleanup on death, exit reasons, published exit events, signals + `process`). On top of those: 5. **device-manager-protocol**: `hello`, supervised spawn with restart policy; usb-xhci-bus becomes the first conforming driver. 6. **Tree reports**: `child_added`/`child_removed`; the manager mirrors; xHCI reports the mouse and keyboard QEMU already hangs off it. 7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats to a manager-internal seam. 8. **Discovery migration** — DONE (M19–M20, 2026-07-13): enumeration moved to ring 3 as swappable per-firmware discoverers — the pci-bus driver (M19) then the acpi service (M20), see [discovery.md](../os-development/discovery.md); of the enumerable devices, the kernel seeds only the host bridge and the acpi-tables node (the non-enumerable platform nodes — processors, interrupt controllers, the HPET, the loader's framebuffer — stay kernel-seeded too). Matching moved with it: `child_added` grew a `device_id` (the kernel-registered id, `no_device` for unregistered leaves like USB ports) and a firmware `hid`, and the manager now matches drivers from those **reports** rather than its boot-time snapshot. The PCI arm flipped in M19.3, the ACPI arm (ps2-bus matched from `_HID`) in M20.3 — each in a single phase so no device is ever matched from both sources at once. The acpi service reports only the non-PCI `_HID` devices, since pci-bus already reports PCI functions (M20.2). ## Settled questions (2026-07-12) - **Stateful buses**: pruning the subtree on bus-driver death is right for USB. A future storage bus with in-flight writes wants drain-before-terminate — which is exactly the `deadline_ms` parameter `stop()` already has; a per-driver deadline is one value in the manager's policy table when such a bus arrives. No design change. - **Manager death**: drivers survive the manager; the restarted manager re-learns the world (above). Checkpointing driver state with the manager is deferred until something demonstrates the need. - **Matching is a registry, not code (resolved 2026-07-26).** `driverFor`/ `pciDriverFor` were honest at two bus types; the third (USB) was matched in code too, and then the switch tables started to hurt — they keyed PCI matches on the class triple alone, so a virtio-gpu could only be matched as a generic display function and the driver had to re-confirm its `1AF4:1050` identity from config space after being spawned. The manifest the earlier note anticipated landed as a human-readable registry: **[/etc/devices.csv](devices-csv.md)**, parsed by the pure `device-registry` module and read by the manager at boot. A row binds a driver to a device by any of base / subclass / prog-IF / vendor / device / subsystem / `_HID`, most-specific match winning; it is authoritative (no compiled-in fallback — an unmatched device is logged, never guessed). `pciDriverForIdentity`, `hidDriverFor`, and `usbDriverForIdentity` are gone.