# 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, wants_channel, version }` @ the assigned device | confirms the assignment, starts the deadline clock — **and moves the channels** (below) | | 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) @ start index | one page of the tree, one `ChildEntry` per record in the reply's tail; a short page is the end | | 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 | **`hello` is also the establishment plane for driver-layer channels** ([communication.md](../os-development/communication.md) "Establishment: two planes, one namespace"). A provider's hello carries its serving endpoint UP as the call's capability (the xHCI bus its transfer endpoint, usb-storage its block endpoint — several processes provide those contracts, so neither is ever a registry name). A hello with `wants_channel` set is answered with a capability DOWN: for a `.device`-role driver, the channel of its device's *reporter* (a class driver reaching its own controller); for a `.consumer` (not a spawned driver at all — fat looking for its volume), the channel of the driver *bound to* the target device. No channel in an acked reply means the provider is mid-restart and its re-report is on the way — retryable, never a verdict. 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. Delegation is BUILT (docs/os-development/device-authority.md "As built"): the device arrives WITH the spawn — `system_spawn`'s sixth argument moves the claim before the child's first instruction — and the id still rides argv for the driver to name itself by. A given device is a loan that returns to the manager on the holder's death. 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, **reaping the class drivers bound to its children**. Its children describe protocol state (xHCI slot ids, transfer rings) that died with the process; keeping the nodes would be keeping a lie — and the class drivers hold channels into the dead process they cannot observe dying (an HID driver blocks on reports that simply never come). Each is killed and its entry cleared, so the matcher can respawn a fresh generation when the re-report arrives; their hellos fetch the successor's channel. Watchers receive `child_removed` — the input service losing, then regaining, a keyboard is the *honest* description of what happened. 3. **The device is already back** because the loan returned at death — the manager re-delegates the same controller with the respawn, and the kernel confines it afresh. 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.