# 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](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](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](process-lifecycle.md), signals over IPC and the stable
`runtime.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](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](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 (the vfs-protocol pattern): extern-struct
messages, a version in the handshake, reserved fields everywhere. The manager is a
well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is
talking; the same endpoint receives its children's exit notifications — one loop,
one world.

| Direction | Message | Purpose |
|---|---|---|
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock |
| bus → manager | `child_added { parent, identity, resources }` | one node the bus discovered |
| bus → manager | `child_removed { id }` | unplug, or the bus lost it |
| app → manager | `enumerate` | snapshot of the tree (read-only) |
| app → manager | `subscribe` | receive published add/remove events |

`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](process-lifecycle.md)'s vocabulary, not this protocol.

Assignment stays argv (`usb-xhci-bus <device id>`) 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.

## 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](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](process-lifecycle.md) (claim cleanup on death, exit reasons,
published exit events, signals + `runtime.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](discovery.md); the kernel seeds
   only the host bridge and the acpi-tables node. 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 stays code until the third bus.** `driverFor`/`pciDriverFor` are
  honest at two bus types; the third triggers the manifest (a driver declares what
  it binds: a PCI class triple, a USB class triple, an ACPI `_HID`).
