library: the last three protocols speak the envelope

These were the awkward ones. Each began with an operation packed into a
single byte — two of them with a version wedged in beside it — so there was
no wrapping them: the layouts had to be rebuilt. The device manager's own
enumerate and subscribe become the reserved verbs that mean the same thing
everywhere, its replies lose three status structs the envelope already
carries, and a device id becomes the packet's target. Power drops the
version it repeated on every request, because describe is the handshake,
and stops claiming a 64-byte ceiling it never needed for calls. USB moves a
control transfer's data to the packet tail in both directions, which makes
the status length the transferred length and retires a field that had been
saying the same thing twice.

The danger in this one was not the protocols but their readers. Init
recognised a power button by two bytes at the head of a message, the ACPI
service dispatched on the first byte, the xHCI driver read its operation
with a raw integer load, and the HID drivers reinterpreted a report
wholesale — none of which would have failed to compile once the layouts
moved. They would simply have stopped: no shutdown on the power button, no
reports from the keyboard. Every one of them now reads through the
generated types, and the shutdown gate that answers only a subscriber is
the same code it was.

Two sizes were decided by measuring rather than assuming. The child-added
message is both a request and the event broadcast to subscribers, and
alignment rounds it to 48 bytes, which puts its packet exactly on the
64-byte push floor — a test pins that, because a field added carelessly
would now overflow it. The interrupt report gives up eight bytes of inline
room to make space for the header; the two drivers that produce reports
send eight and four.

Suite 110/110.
This commit is contained in:
Daniel Samson
2026-08-01 07:20:37 +01:00
parent d2dfbcabf8
commit 2719b93530
27 changed files with 1058 additions and 685 deletions
@@ -64,19 +64,38 @@ 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
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 | Message | Purpose |
| Direction | Packet | Purpose |
|---|---|---|
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock |
| bus → manager | `child_added { parent, bus_address, identity, device_id, hid }` | one node the bus discovered |
| 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` | snapshot of the tree (read-only) |
| app → manager | `subscribe` | receive published add/remove events |
| 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 |
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
+25 -14
View File
@@ -28,28 +28,39 @@ unchanged.
## The protocol
The `power-protocol` module ([library/protocol/power/power-protocol.zig](../../library/protocol/power/power-protocol.zig))
follows the vfs-protocol pattern — extern-struct messages, a version, reserved
fields. Three operations:
is defined through the [envelope](protocol-namespace.md), so every packet begins
with the folded `Header`. `Header.target` is unused in both directions: the
provider is the only object either side addresses.
| Direction | Operation | Purpose |
| Direction | Packet | Purpose |
|---|---|---|
| subscriber → service | `subscribe` | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
| init → service | `shutdown` | orderly shutdown's last step: enter S5 (soft off) |
| service → subscriber | `event` | a published `EventMessage`, delivered as a buffered message (never sent *to* the service) |
| subscriber → service | `subscribe` (reserved verb 2) | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
| init → service | `shutdown` (verb 16) | orderly shutdown's last step: enter S5 (soft off) |
| service → subscriber | one event per kind | a published `Notice`, `ipc_send`t as a buffered packet (never sent *to* the service) |
`subscribe` is not one of this protocol's own verbs: a synchronous call whose
attached capability is the subscriber's endpoint is exactly what the envelope's
reserved `subscribe` means everywhere, so power adopts it wholesale. And no
packet carries a version — the reserved `describe` verb is the version handshake,
asked once at connect time rather than out of every packet's budget.
Events are published, not polled: like the input service, the service holds
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
message, so a slow or dead subscriber can never wedge the source. The event
packet, so a slow or dead subscriber can never wedge the source. **The kind is
the packet's operation** — one declared event per named kind, exactly as the
input service delivers one per device class — so a subscriber reads *what
happened* out of the header rather than out of a tag inside the payload. The
vocabulary is hardware-neutral:
- `power_button` — the button was pressed (a fixed ACPI event on x86).
- `lid`, `ac`, `battery` — the named GPE-driven events.
- `notify` — a device notification that maps to none of the above; its `code`
(the ACPI `Notify` argument) and the notifying device's `hid` say which device
and what happened.
- `power_button` (event 16) — the button was pressed (a fixed ACPI event on x86).
- `lid` (17), `ac` (18), `battery` (19) — the named GPE-driven events.
- `notify` (20) — a device notification that maps to none of the above; its
`code` (the ACPI `Notify` argument) and the notifying device's `hid` say which
device and what happened.
An `EventMessage` carries the `event` tag plus `code` and an 8-byte `hid`, so a
generic `notify` is fully described without a second round trip.
The payload every one of them carries is a `Notice`: `code` plus an 8-byte `hid`,
so a generic `notify` is fully described without a second round trip, and the
four named kinds leave both fields zero because the verb already said it all.
**`shutdown` is authority, not information.** It is the only operation that
*does* something irreversible, so it is gated: the contract is that only init
+29 -3
View File
@@ -36,10 +36,10 @@ plain `main` checkout always tells the truth about where the work is.**
| | |
|---|---|
| Working on | **P4b** — device-manager, power, usb-transfer onto `Define` |
| Working on | **P4c** — harness subscriber lift + badge-scoped client ids |
| Branch carrying it | `feat/security-group-3` (pushed to origin) |
| On `main` | Phase 0, PM, H1, P1, P2, P3 — groups 1 and 2 merged |
| Awaiting merge | P4a — lands with the group 3 merge |
| Awaiting merge | P4a, P4b — land with the group 3 merge |
| Suite | 110 cases, all passing |
| Last updated | 2026-08-01 |
@@ -66,7 +66,7 @@ group boundary.
bind attestation and every refusal it makes untouched (suite 109/109)
- [x] **merge** group 2 → main, push
- [x] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input; display's one overloaded request split per-operation and its field abuse ended, scanout's bogus 64-byte maximum deleted, directory EOF re-spelled as a nameless entry, input moved onto the service harness; new `protocol-conformance` case asks every reachable provider for `describe` and requires `-ENOSYS` for an undefined verb; suite 110/110)
- [ ] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer)
- [x] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer; every leading operation byte folded into the header, and with it the `device_id`/`device_token` that followed it — `Header.target` now carries the device in all three. device-manager's own `enumerate`/`subscribe` became the reserved verbs and its three `{status, reserved}` reply structs the envelope's `Status`; `ChildAdded` is one struct under two numbers, a call and an event, landing exactly on the 64-byte push floor. power's kinds became one declared event each, the input protocol's shape, so init reads *what happened* from the header; usb-transfer's control data stage moved to the packet tail in both directions, which made `Status.len` the transferred length and `actual_length` redundant. The two silent-breakage sites — init's byte-offset power parse and acpi's `message[0]` dispatch — are gone, the shutdown badge gate unchanged; three more rows in the conformance table. Suite 110/110)
- [ ] **P4c** — harness subscriber lift + badge-scoped per-client integers
- [ ] **merge** group 3 → main, push
- [ ] **H2** — SMEP on every core
@@ -413,6 +413,32 @@ re-layouts and raw-offset readers):
USB storage/HID all exercise these wires); the conformance case now covers
three more providers. Suite 110.
*Landed. Three judgment calls the plan left open, recorded because a reader of
the wire formats will want them:*
- *`ChildAdded` is 48 bytes, not the 44 the "60 B" estimate assumed: three `u64`s
give the struct eight-byte alignment, so 41 bytes of content round up whatever
order the fields sit in. The packet is therefore **exactly** 64 — on the push
floor, not under it — which `Define` accepts and the module pins in a test. The
fields are ordered small-tail-last deliberately, so the slack the rounding pays
for is where the small ones live.*
- *power's events are declared **per kind** (`power_button`, `lid`, `ac`,
`battery`, `notify`), not one `event` with the kind in the payload. That is the
shape P4a gave input — "the class is the header's operation, so a subscriber
reads the kind from the packet rather than from a tag inside the payload" — and
it is what makes `Header.operation` carry information here at all. It also kept
every call site's spelling: `Protocol.Event` is re-exported as the protocol's
own `Event`, with the members it always had.*
- *the conformance case still checks two providers, and the three new rows report
as unbound. All three P4b contracts arrive with the device manager — it is the
first, it spawns the discovery service that binds the second, and the xHCI
driver that binds the third — so booting one means booting the driver tree, and
the fixture takes **one snapshot** of `/protocol`: a scenario whose bound set
depends on how far that tree got would make the case's own summary line a boot
race. The rows still earn their place — a future scenario that binds one gets it
checked with no edit here, and `/test/*` already holds the `open` grant for
`device-manager`.*
## P4c — harness subscriber lift + badge scoping
- `library/kernel/service.zig` grows the subscriber table, exit-