# mgf-hrb-usb — the USB Worker Design

> **What this is.** The concrete per-bus worker (Part 7) for
> `hw:/usb/**`. The complete design [Part 7] names the worker; this
> document *designs* it: the URN translation, the substrate matrix, the
> composite-device per-interface model, the identity-continuity
> comparator, the seam ops mapped to USB on Linux, the typed-ops
> vocabulary, and the survivability specialisations.
>
> **Why now.** PCI is the only worker exercised in code. Until a
> *second* concrete worker is designed, the claim that the kabi seam
> is "concrete enough to be a kernel spec" is unfalsifiable. USB is the
> exemplar second worker — and lessons from the working `mgf-usb`
> Python library (v0.2.5, alongside this project at the repo root) port
> directly into HRB shape.
>
> **Reads alongside.**
> [`mgf-hrb-complete-design.md`](mgf-hrb-complete-design.md) Part 2
> (model), Part 6 (kabi seam), Part 7 (workers); the
> [`survivability review`](mgf-hrb-survivability-review.md) for the
> deadline/quarantine application.
>
> **Encoded lessons.** This doc deliberately ports mgf-usb's
> hard-earned answers: the three-substrate fallback chain, the
> per-interface composite model, identity continuity across DFU-style
> re-enumeration, the typed control verbs (`reset`/`bind_driver`/
> `unbind_driver`/`set_authorized`/`set_autosuspend`/`set_port_power`),
> the udev-rule recipe. The Linux USB stack hasn't gotten less
> idiosyncratic since `mgf-usb` shipped against it; the answers earned
> there are the answers here.

---

## §1. The worker's responsibility, drawn sharply

The USB worker owns one bus subtree: `hw:/usb/**`. Its responsibility
is *exactly* what every other bus worker's is (per Part 7.1):

- enumerate its subtree truthfully and at known fidelity (G2);
- arbitrate ownership of devices inside it via a lease engine (Part 3);
- enforce capabilities on the grant path (Part 4);
- expose typed ops + a raw channel per claimed resource (G1);
- emit events for hotplug/state/ownership change (Part 2.9);
- survive: deadlines, quarantine, revocation-below-the-fd (Part 12).

It refuses what every other worker refuses: it does not touch hardware
outside its subtree; it does not implement policy; it does not parse
client bytes (the facade does that and forwards typed requests).

**Language:** Zig, all of it (below the socket). **Process model:**
one process — the unprivileged USB worker — talking to one **privileged
USB helper process** (`mgf-hrb-privileged-core-hardening.md`) that
holds the elevated `usbfs`/`/dev/bus/usb` access.

---

## §2. The URN model for USB

### 2.1 The tree

USB is a *forest* of trees rooted at host controllers (xHCI / EHCI /
OHCI / UHCI / dwc3 …). Each tree's leaves are *interfaces*, not
devices — because composite USB devices are the rule, not the
exception, and the unit of arbitration is what kernel drivers bind to,
which is the interface.

```
hw:/usb/
├─ bus1/                  (host controller — xHCI)
│  └─ port-2/             (root-hub port 2)
│     └─ port-2.3/        (downstream hub port 3)
│        └─ dev/          (USB device — composite, e.g. headset)
│           ├─ if0        (Claimable: usb_interface, class=Audio)
│           ├─ if1        (Claimable: usb_interface, class=Audio)
│           └─ if3        (Claimable: usb_interface, class=HID)
└─ bus2/                  (host controller — dwc3)
   └─ port-1/
      └─ dev/
         └─ if0           (Claimable: usb_interface, class=Mass Storage)
```

### 2.2 The URN grammar

```
hw:/usb/bus<N>/port-<chain>/[dev/]if<K>
```

- **`bus<N>`** — host-controller index (kernel-assigned, stable across
  the lifetime of one boot; **not** stable across reboots).
- **`port-<chain>`** — root-port-to-leaf hub-traversal chain, joined
  by `.` (e.g. `port-2.3.1`). This is *the* canonical stable
  within-session identifier (Part 2.4 §topological). It survives bus
  rescans and is the one thing every backend agrees on (cross-backend
  consistency proven in `mgf-usb/docs/design/ARCHITECTURE.md` §3.5).
- **`dev`** — an opaque segment present so the parent device is a
  navigable URN. Carries no identifier the consumer parses; consumers
  walk parent/child via topology, never by parsing the URN.
- **`if<K>`** — interface number (`bInterfaceNumber`). The leaf and
  the unit of `claim`. Alternate settings are *not* in the URN — they
  are a property of the typed op (see §6).

### 2.3 The stability ladder, applied

For every USB resource, the descriptor's `identity_class`
(Part 2.4) is recorded honestly:

| URN rung | What it means for USB | When |
|---|---|---|
| **topological** | `(bus, port_chain, interface)` | Default. Stable across rescans; defeated by a cable move or a hub power blip. |
| **intrinsic** | `(vid, pid, serial_number, interface)` | Available when the device exposes `iSerialNumber` *and* the serial wasn't dropped in a mode transition. The strongest within-vendor identity. |
| **synthetic-pinned** | admin-assigned alias | For passthrough plans against devices that omit `iSerialNumber` — the operator pins by hub port. |

The descriptor reports *which rung* is in use for *this* URN so a
consumer knows whether "the same URN tomorrow" means "the same hub
port" or "the same physical device." Honesty about identity
stability is itself a fidelity guarantee (D-010).

> **Gotcha (real, learned the hard way).** The kernel's USB
> *devnum* (the `<bbb>/<ddd>` in `/dev/bus/usb/<bbb>/<ddd>`) **rotates
> on every replug**. It does not belong in the URN. It is recorded as
> an *attribute* with fidelity `live` and an explicit "ephemeral"
> note, because some operations (the `USBDEVFS_RESET` ioctl, libvirt
> `<address device='X'/>`) need it — but no consumer ever stores it.

### 2.4 Composite devices — interfaces first, not devices

The single most consequential modelling decision for USB, ported from
[`mgf-usb` D-013](../../docs/design/DECISIONS.md):

> **`enumerate` emits one `Resource` per interface, not per device.**
> The device is a *parent* node in the topology tree; it carries no
> `Family` and is not itself claimable.

The USB spec permits — and standard devices ship — composite devices
whose `bDeviceClass` byte is `0x00` (PER_INTERFACE), meaning "see
interface descriptors for class." A device-level claim is, for those
devices, *definitionally wrong*: two distinct kernel drivers can bind
to two distinct interfaces of the same device (a USB headset has an
Audio driver on its audio interfaces and an HID driver on its volume
controls; a multifunction printer has Printer + Imaging + Mass Storage
each with its own driver). Claiming "the device" is meaningless under
that model.

This is also why **a claim's blast radius is one interface, not one
device**: revoking one interface from a tenant cannot disturb the
co-existing drivers on its sibling interfaces. (The same survivability
property the IOMMU-group story tries to provide for PCI; on USB it's
native because the granularity matches reality.)

### 2.5 Open question on identity stability — Q-USB-1

When the parent device's `vid:pid` flips in a mode transition (the
iFig DFU sequence: `0x05ac:0x12ab` → `0x05ac:0x1281` → `0x05ac:0x1227`),
does the `if<K>` URN change? It must — the parent's identity-class
changed; the new device-mode exposes a different interface set.
**Decision (carried in §4):** the URN tracks the *current*
enumeration; identity continuity across the transition is provided by
the runtime comparator (§4), not by URN stability. See `MODEL-01` in
`FEEDBACK.md`.

---

## §3. The substrate matrix

Linux exposes USB through three substrates with overlapping but
non-identical capabilities. The USB worker keeps the choice explicit
in its `BackendCapabilities` report so `mgf-hrb doctor` can render the
truth.

| Substrate | Enumerate | Watch (events) | Acquire | Reset | Raw channel | Bind/unbind | Set authorized |
|---|---|---|---|---|---|---|---|
| **sysfs read** (`/sys/bus/usb/devices/...`) | ✓ all descriptor fields, fidelity `live` | netlink uevent (separate fd) | n/a | n/a | n/a | ✓ via `drivers/<d>/{bind,unbind}` write | ✓ via `authorized` attribute |
| **usbfs ioctl** (`/dev/bus/usb/<bbb>/<ddd>` + USBDEVFS_*) | partial (descriptor walking) | n/a | ✓ `USBDEVFS_CLAIMINTERFACE` per `bInterfaceNumber` | ✓ `USBDEVFS_RESET` | ✓ the fd itself (SCM_RIGHTS-able) | n/a (kernel drivers do this) | n/a |
| **netlink** (kobject uevent socket) | n/a | ✓ add/remove/change events (DP-09 cancellable) | n/a | n/a | n/a | n/a | n/a |

The mapping is:

```
KernelBackend.enumerate      → sysfs read (raw syscalls, no libudev/libusb)
KernelBackend.query_topology → sysfs walk
KernelBackend.watch          → netlink uevent socket → typed events
KernelBackend.acquire        → usbfs CLAIMINTERFACE   (privileged helper)
KernelBackend.release        → usbfs RELEASEINTERFACE (privileged helper)
KernelBackend.reset          → usbfs RESET            (privileged helper)
KernelBackend.map_region     → Unsupported (USB has no MMIO mapping)
KernelBackend.raw_channel    → the usbfs fd, dup'd, SCM_RIGHTS-sent (G4)
KernelBackend.enforce_cap    → null on Linux (userspace mgf-hrb-cap enforces)
```

The same "Level 2: direct syscalls, no convenience libraries" rule the
PCI backend (`linux_backend.zig`) follows: **no `libusb`, no
`libudev`**. Raw syscalls only. Linux specifics stay contained in
`mgf-hrb-linux/usb_*`.

> **Why no libusb.** Same reasoning as `mgf-usb/docs/design/DECISIONS.md`
> D-002 inverted: there, mgf-usb wanted to *survive* on hosts where
> only one substrate is present. Here, mgf-hrb is the privileged broker
> on a known platform — it doesn't need fallbacks, but it does need
> the *fewest* moving parts in the privileged path. A direct syscall
> path against the kernel ABI is auditable; a `libusb` path is not.

### 3.1 Capability declaration

The Linux USB backend implements `BackendCapabilities` so the worker
can answer "what can I do on this substrate?" before any work begins:

```zig
const UsbBackendCaps = struct {
    enumerate: bool,               // always true if /sys/bus/usb/devices readable
    watch_native_change: bool,     // true if netlink kobject_uevent reachable
    acquire: bool,                 // requires /dev/bus/usb/* writable
    reset: bool,                   // requires /dev/bus/usb/* + USBDEVFS_RESET available
    bind_unbind: bool,             // requires /sys/bus/usb/drivers/*/bind writable
    set_authorized: bool,          // requires `authorized` sysfs attr writable
    set_port_power: bool,          // requires `disable` sysfs attr writable on the hub
    set_autosuspend: bool,         // requires power/control writable
    declared_acquire_latency_ms: u32,   // p50 for CLAIMINTERFACE on this host
    declared_watch_latency_ms: u32,     // p99 for netlink event delivery
};
```

The latency numbers are *declared* (per-host calibration), not
measured per-call — same pattern as
`mgf-usb/docs/design/ARCHITECTURE.md` §3.4. This becomes the answer to
"why is my acquire slow on this kernel?" without requiring a
profiler.

### 3.2 Operating modes

| Mode | Trigger | What works | What doesn't |
|---|---|---|---|
| **full** | `/sys/bus/usb/devices` + `/dev/bus/usb/*` both readable+writable | every op above | nothing |
| **read-only** | sysfs readable, usbfs not writable (no udev rule, no root) | enumerate / topology / watch / observe-only claim | acquire / reset / raw / bind / authorized / port-power → `Unsupported{reason=usbfs_not_writable, suggested_fix=...}` |
| **degraded — no IOMMU** | sysfs OK, no IOMMU groups | enumerate, observe, raw-via-usbfs (no DMA escape — usbfs operates above the IOMMU boundary, doesn't need it for the USB case) | DMA-capable PCI-side workers, but USB-side is *not* IOMMU-gated (USB I/O goes through the usbfs file, not direct DMA) — declared mode is informational |

The mode is reported via `BackendCapabilities` and surfaced by
`mgf-hrb doctor`. Operators see "your USB worker is in **read-only**
mode because `/dev/bus/usb` requires the `plugdev` group; fix: install
`/etc/udev/rules.d/70-mgf-hrb.rules` and re-login."

---

## §4. Identity continuity — the runtime comparator

### 4.1 The problem (the iFig DFU case, restated for HRB)

A USB device drops off the bus and re-appears with:

- a different `pid` (mode change: Recovery → DFU),
- possibly a vanished `serial_number` (some firmwares drop
  `iSerialNumber` in DFU),
- the same `(bus, port_chain)` (the hub port didn't change),
- a new kernel devnum.

The URN's `if<K>` may renumber because the new device-mode exposes a
different interface set. A consumer mid-flow (a passthrough plan, a
recovery tool watching for re-emergence, a `wait_for_urn`) needs to
answer **"is this the same physical unit I started with?"** without
relying on the URN alone.

### 4.2 `Snapshot` — the captured identity

A model-layer type, backend-free, survives the device disappearing
from the bus:

```zig
pub const Snapshot = struct {
    urn:             Urn,
    identity_class:  enum { topological, intrinsic, synthetic_pinned },
    vid:             u16,
    pid:             u16,
    serial_number:   ?[]const u8,         // null = device omits iSerialNumber
    bus:             u8,
    port_chain:      [8]u8,               // up to 8 hubs deep, zero-padded
    port_depth:      u4,
    captured_at:     u64,                 // monotonic; for the debounce window
};
```

Captured from any `Resource` via a pure function — no allocations, no
backend touch.

### 4.3 `same_likely(snapshot, candidate)` — the comparator

The algorithm ports directly from
`mgf-usb/docs/design/IDENTITY_HEURISTIC.md` — per-dimension scoring,
weighted, returning a confidence tier:

```zig
pub const Confidence = enum { same, likely_same, ambiguous, different };

pub fn sameLikely(
    snapshot: *const Snapshot,
    candidate: *const Resource,
    opts: ComparatorOpts,
) Confidence { ... }

pub const ComparatorOpts = struct {
    tolerate_mode_change:    bool = true,   // PID-flip on stable VID + port allowed
    tolerate_port_change:    bool = false,  // cable move alone is suspicious by default
    tolerate_missing_serial: bool = true,   // DFU-style serial vanish allowed
    port_debounce_ms:        u32  = 1000,   // mode-change window
};
```

Score table (the same weights `mgf-usb` validated against the iFig
restore loop):

| Dimension | Weight | Trigger |
|---|---|---|
| `vid_match` | +3 | snapshot.vid == candidate.vid (mismatch → short-circuit DIFFERENT) |
| `serial_match` | +6 | both present and equal (mismatch → short-circuit DIFFERENT) |
| `serial_asymmetric` | +2 / −4 | one side has, the other doesn't — `+2` if tolerate, `−4` strict |
| `port_match` | +4 | bus + port_chain identical |
| `port_mismatch` | 0 / −6 | port differs — `0` if tolerate_port_change, `−6` otherwise |
| `mode_change_explained` | +2 | vid stable, pid differs, port matches, within debounce |

| Score | Tier |
|---|---|
| ≥ 12 | `SAME` (vid + serial + port = 13; Normal→Recovery with serial intact = 15) |
| 6..11 | `LIKELY_SAME` (iFig Recovery→DFU: vid + port + mode-change, serial vanished = 11) |
| 1..5 | `AMBIGUOUS` |
| ≤ 0 | `DIFFERENT` |

### 4.4 Where it lives

The *algorithm* lives in `mgf-hrb-model` (so every bus that needs it
can use it). The *weights and the `ComparatorOpts` defaults* live in
each per-bus worker — USB's defaults match the table above; PCI's
defaults differ (PCI BDF is far more stable, so port-mismatch is more
suspicious there). This is the same mechanism/meaning split (D-014)
applied to identity heuristics: the *math* is mechanism (Zig, in
`mgf-hrb-model`); the *defaults* are per-bus meaning (each worker, by
convention).

### 4.5 What the comparator does NOT promise

Same non-goals as
`mgf-usb/docs/design/IDENTITY_HEURISTIC.md` §6 — repeated for
discoverability:

- **Not a cryptographic identity proof.** It will say "likely same" for
  a malicious device deliberately mimicking another. That's a
  capability question, not an identity question.
- **Not deterministic across firmware revisions.** A device that
  changes its iSerialNumber across a firmware update will register as
  `DIFFERENT` — correctly, by the heuristic; possibly wrong by the
  operator's intent. The consumer owns that policy.
- **Not portable across hosts.** `(bus, port_chain)` is a host
  property. Cross-host identity is a different problem (synthetic-pinned
  aliases solve it).

---

## §5. The seam ops, mapped concretely

This is the *contract* the USB worker satisfies. Each op below maps
to either a sysfs path or a usbfs ioctl, with the survivability
constraints from the kabi survivability comment block applied.

### 5.1 `enumerate(scope, sink)`

**Implementation.** Raw `openat` + `getdents64` on
`/sys/bus/usb/devices`. For each entry, parse `<bus>-<port_chain>`
into a topology node, then walk that device's interface descriptors
from `bInterfaceNumber*` sysfs attributes. Emit one `Resource` per
interface matching `scope`.

**Fidelity tagging.** Per-field: `vid`/`pid`/`bcdDevice`/`bDeviceClass`
are `live` (read just now); per-interface
`bInterfaceClass`/`bInterfaceSubClass`/`bInterfaceProtocol` are `live`;
`iSerialNumber` is `live` *if present in sysfs*, else `unknown` (we
refuse to open the device to read it — that would stun kernel-bound
devices, per `mgf-usb` D-016 / PERF-01); `driver` is `live`-from-sysfs
or `unknown` if no driver is bound.

**Deadline.** Bounded by a per-call deadline (D-029). A blocked sysfs
read on a wedged USB controller times out, the worker is killed and
respawned, the controller's subtree is quarantined.

**Survivability.** No `enumerate` ever opens a device. **Read-only by
construction** — even on the "full" mode of the substrate.

### 5.2 `query_topology(sink)`

**Implementation.** `enumerate("hw:/usb/**", sink)` plus the
device-level parent nodes (not just interfaces). The facade can fan
out across worker subtrees and merge.

### 5.3 `watch(scope, sink) -> WatchHandle`

**Implementation.** Open a kobject-uevent netlink socket; for each
incoming `add`/`remove`/`change` event whose `DEVPATH` is under
`/devices/...usb...`, parse to a typed `Event` (Part 2.9) — typed with
the *interface*'s URN, not the device's.

**Native `change`.** The kernel's USB driver emits a `change` action
for driver rebind, authorisation flips, and descriptor refreshes.
These pass through as `state_changed` events with the affected
interface as the URN.

**Gap-honesty.** The netlink buffer is bounded (kernel-side); under
flood the kernel can drop. The watch handler detects the drop (via
`recvmsg` returning `ENOBUFS`), emits an explicit `overrun` event with
the affected scope, and re-enumerates the affected subtree. **Never
silently lose** (G2 + Part 5.3.3's discipline).

### 5.4 `acquire(urn, mode) -> AcquireToken`

**Implementation.** The unprivileged worker dispatches the typed
request to the privileged helper, which:

1. Resolves `urn` to `(bus, devnum)` and `interface_number`.
2. Opens `/dev/bus/usb/<bbb>/<ddd>` (`open(O_RDWR | O_CLOEXEC)`).
3. Issues `USBDEVFS_CLAIMINTERFACE` with the interface number.
4. On any failure, `errdefer`-unwinds — the fd is closed; nothing is
   committed (D-012, Part 3.4).
5. On success, returns the fd (held only by the helper) and a token id.

**`mode`.** `exclusive` is the natural USB shape — kernel
USBDEVFS_CLAIMINTERFACE is exclusive against any kernel driver bound
to that interface (the helper does an implicit `USBDEVFS_DISCONNECT`
first, deterministic kernel-side). `shared_rw` is `Unsupported` for
USB (the kernel ABI has no shared claim). `observe` doesn't acquire
hardware (Part 3.3 — observers never conflict).

**Deadline.** Bounded. `USBDEVFS_CLAIMINTERFACE` against a
hung-kernel-driver device can in principle block; the helper enforces
the deadline.

**Survivability post-condition.** After successful acquire, the helper
*verifies* the kernel driver is detached by re-reading the interface's
`driver` sysfs attribute (must be absent). If the verification fails,
the acquire is rolled back and the interface is `Quarantined`
(D-009 / Part 12 §2.3 — never trust kernel success without
verification where verification is cheap).

### 5.5 `release(token)`

**Implementation.** The helper issues `USBDEVFS_RELEASEINTERFACE`,
then `USBDEVFS_RESET` (the safe-reset on release, Part 6.3), then
closes the fd. Re-attaches the original host driver via
`USBDEVFS_CONNECT`.

**Total + idempotent.** Releasing an already-released token is a
no-op (the helper maintains the token table). Release must succeed;
on the unlikely RELEASEINTERFACE failure, the helper escalates to a
hub-port disable (set_port_power off→on, §5.10 — the device is
forcibly re-enumerated) and the interface is left `Quarantined` until
the operator inspects.

### 5.6 `reset(urn, level)`

**Implementation.** Two levels for USB:

- **`function_level`** → `USBDEVFS_RESET` on the device. Re-enumerates
  the whole composite device (all interfaces). The right primitive
  for "this interface is stuck."
- **`bus_level`** → set_port_power off, wait 1s, set_port_power on, on
  the upstream hub. The strongest reset available short of physical
  unplug. Used when `function_level` fails verification.

**Reset-escalation ladder** (Part 12 §2.5): the worker tries
`function_level` first, verifies (re-read descriptors, compare against
pre-reset snapshot), escalates to `bus_level` on verification failure,
and quarantines if both fail. Each rung is deadline-bounded.

**Survivability — the device-lie guard (D6).** USBDEVFS_RESET returns
`0` (success) even on firmware that didn't actually reset (some
devices ignore the request). Verification: a successful reset
re-enumerates and produces a `change` event on netlink; if no event
arrives within `2 * declared_acquire_latency_ms`, the worker treats
the reset as unverified and escalates.

### 5.7 `map_region(token, region)`

**Implementation.** `Unsupported{reason=usb_has_no_mmio}`. USB devices
don't expose MMIO regions — all I/O is through usbfs URBs. Honestly
unsupported (G2 + the substrate matrix). Documented so a future
audit-pass doesn't waste time looking for the missing implementation.

### 5.8 `raw_channel(token) -> RawHandle`

**Implementation.** Dup the helper's open `/dev/bus/usb/<bbb>/<ddd>`
fd. SCM_RIGHTS-send it to the worker; the worker SCM_RIGHTS-relays it
to the client. The client now holds a real, usable usbfs fd —
USBDEVFS_BULK / USBDEVFS_CONTROL / USBDEVFS_SUBMITURB all work against
it directly. **The broker is absent from the use path** (G4).

**Revocation below the fd.** When the lease ends (release / death /
preempt), the helper closes its copy. *But the client's copy still
works* — that's the G4 dividend and the survivability gap. The fix is
to revoke *below the fd*: the helper does `USBDEVFS_RELEASEINTERFACE`
(the kernel-level binding is gone) and then `USBDEVFS_DISCONNECT`
(re-binds the host driver if any) — at which point the client's
outstanding URBs against that fd error out with `ENODEV`. The fd is a
straw; pulling the kernel's interface binding pulls what the straw
drinks from. (Part 12 §3.4 made concrete for USB.)

### 5.9 The typed-ops table — `usb_interface`'s `OpsTable`

Per Part 2.6, every `kind` declares typed ops or the raw channel. For
`usb_interface`:

| Typed op | Maps to | Privilege | Notes |
|---|---|---|---|
| `read_descriptor(type)` | sysfs read or USBDEVFS_CONTROL | none / lease | descriptor walks; read-only |
| `set_alt_setting(alt: u8)` | USBDEVFS_SETINTERFACE | lease (`claim_exclusive`) | the alternate-setting selector |
| `clear_halt(endpoint: u8)` | USBDEVFS_CLEAR_HALT | lease | endpoint stall recovery |
| `bulk_transfer(ep, buf, deadline)` | USBDEVFS_BULK | lease | the *common-case* I/O; typed by direction (in / out) |
| `control_transfer(req, ...)` | USBDEVFS_CONTROL | lease | the *other* common-case I/O |

The rest of the USB surface — unusual descriptor types, exotic
quirks, USB 3 streams — reaches via the **raw channel** (§5.8). This
is the design's promise made operational for USB: every legitimate
USB operation is either a typed op above or reachable via raw — never
"unreachable" (G1 / D-007).

### 5.10 The control verbs — operator-visible state changes

Ported from `mgf-usb`'s `mgf.usb.control.*` (v0.2.5). These are
*device-state* manipulations, not I/O — they go on the parent device
URN, not the interface URN. They are typed ops on the *device* node
when one is declared (USB's device-as-a-resource model deviation: the
device itself isn't `Family.claimable` because claim happens at
interface granularity, but it *does* have state that can be set, so
it's a `Resource` with no claim verb — equivalent to `observe`-only
with a writable surface guarded by `claim_exclusive` on at least one
of its interfaces).

| Verb | URN | Mechanism | Privilege gate |
|---|---|---|---|
| `set_authorized(value: bool)` | device | sysfs `authorized` write | needs cap on any interface of the device |
| `set_autosuspend(mode: Auto / On)` | device | sysfs `power/control` | needs cap on any interface |
| `set_port_power(value: bool)` | parent hub's port | sysfs `disable` on the hub port | needs cap on the hub |
| `bind_driver(driver, interface)` | interface | sysfs `drivers/<d>/bind` | needs `claim_exclusive` on the target interface |
| `unbind_driver(interface)` | interface | sysfs `drivers/<d>/unbind` | needs `claim_exclusive` on the target interface |

Same constraints as
`mgf-usb/src/mgf/usb/control/operations.py`: per-interface bind/unbind
(D-013), keyword-only `value=` (`set_authorized(value=True)` reads
clearly; bare `set_authorized(True)` reads as a riddle), permission
errors carry a `suggested_fix` (the udev rule, the `plugdev` group).

---

## §6. The structured permission error

When a `usb_interface` claim or a control verb fails because the
substrate isn't writable (no udev rule, no `plugdev` membership), the
kabi op returns:

```zig
Err.NotPermitted, with side-channel structured payload:
   PermissionContext {
       operation:      []const u8,   // "acquire" | "set_authorized" | ...
       urn:            Urn,
       suggested_fix:  Fix,           // canned, machine-matchable
   }

const Fix = enum {
    install_udev_rule,
    add_to_plugdev,
    re_run_as_root_less_preferred,
    no_iommu_dma_capable_blocked,
    none,
};
```

The fix vocabulary is *closed* (an enum, not a string), so `mgf-hrb
doctor` can match on identity and render the operator-facing prose for
the active locale / paste-buffer / GUI surface. The canned messages
mirror `mgf-usb`'s vocabulary
([`exceptions.py` constants `FIX_UDEV_RULE`, etc.](../../src/mgf/usb/exceptions.py)).

This is the kabi shape of `mgf-usb`'s `DeviceAccessDeniedError`
(D-015 / API-05) — the "carry the fix with the error" pattern. It is
*not* a violation of the kernel-seam dependency law (no Linux noun
crosses upward) because `Fix` is an abstract enum; the operator-facing
*rendering* of each fix lives in `mgf-hrb-tools` (Python), above the
socket.

---

## §7. The udev rule, shipped + checked

`mgf-hrb` will face the same 80%-of-issues bring-up shape as
`mgf-usb`: "device visible but the broker can't claim." Same
mitigation:

**`packaging/70-mgf-hrb.rules`** (shipped with the project):

```
# Grant USBFS access to the 'mgf-hrb' group so the broker can claim
# interfaces without root. The privileged helper drops to this group.
SUBSYSTEM=="usb", MODE="0660", GROUP="mgf-hrb"
# Per-port hub controls (uhubctl pattern).
SUBSYSTEM=="usb", DRIVERS=="hub", MODE="0660", GROUP="mgf-hrb"
```

**`mgf-hrb doctor --check-udev`** verifies (using the robust
signals, not by parsing `udevadm test` output — mgf-usb's D-024
refinement):

- the rule file is installed at `/etc/udev/rules.d/70-mgf-hrb.rules`,
- the `mgf-hrb` group exists,
- the privileged helper runs in that group,
- a known USB interface is readable+writable through the rule.

Each check fails with the same structured `Fix` enum from §6 so the
operator sees `Fix: install_udev_rule` rendered as a copy-pastable
shell command.

---

## §8. Survivability specialisations

Stage-by-stage from
[`mgf-hrb-survivability-review.md`](mgf-hrb-survivability-review.md),
applied to USB:

### S1 — Upward crash-safety

The worker doesn't parse client bytes (the facade does). USB-side
inputs are typed at the kabi layer. The trust gate already protects
the worker.

### S2 — Downward fault-containment (the main USB hardening)

| Pathology (Part 12 §2) | USB application |
|---|---|
| D3 indefinite block | every USBDEVFS_* ioctl runs under the per-bus-worker deadline; a wedged USB controller costs one worker, which respawns; the controller's subtree is `Quarantined` until verified |
| D4 slow-but-eventual | `USBDEVFS_RESET` can take seconds on some devices — `declared_acquire_latency_ms` × 4 is the deadline; longer is `Timeout` |
| D6 success-then-lie | `USBDEVFS_RESET` verified by re-enumeration on netlink (§5.6) |
| D7 spurious interrupts | USB device flooding interrupts can DoS the host — the worker watches `/proc/interrupts` for runaway rates on granted devices and `revoke`s + `Quarantine`s the abuser (Part 12 §2.7) |
| D8 hotplug during acquire | the acquire path is atomic (D-012) — if the device disappears during CLAIMINTERFACE, the helper sees `ENODEV`, the acquire fails, no lease is committed |
| D11 reset-escalation | function_level → bus_level (port-power cycle on the hub) → `Quarantined` (§5.6) |

### S3 — Tenant isolation

Per-interface granularity means a tenant claiming `if0` of a composite
device cannot reach `if1`. The `enumerate` is **capability-filtered**
(Part 12 §3.6) — a tenant sees only resources its capability scope
matches.

### S4 — Self-fault containment

The USB worker is one process. A panic in its descriptor parser
crashes only that worker; the facade respawns it; the worker
re-derives from sysfs + connections (D-033). The privileged helper is
a separate process — a parser exploit cannot reach it (D-031).

### S5 — Bounded everything

- max devices per worker (the bus has a hard upper bound: 127 per
  controller; the worker caps at 128 to leave room for the hub)
- max URBs in flight per lease (per-tenant)
- max events per netlink burst before back-pressure
- max acquire/release rate per tenant (control-path cold)

Hit a bound → `Err.Overloaded` typed return, not unbounded growth.

---

## §9. Build phasing for `mgf-hrb-usb`

Mirrors the design's overall phasing (Part 9.3) at the worker scale.

**Phase U1 — model + kabi conformance.** Add `Kind.usb_interface` to
`mgf-hrb-model`. Add the `Snapshot` + `sameLikely` to the model. Run
the kabi conformance suite against a USB-flavoured fake backend
(in-memory composite devices, scripted state). *Exit: conformance
green against the USB-fake.*

**Phase U2 — real read-side.** `LinuxBackend.usb` enumerate + watch
against `/sys/bus/usb/devices` + netlink, on real hardware.
`enumerate` returns honest fidelity tags; netlink delivers `add` /
`remove` / `change`. No claims yet. *Exit: a USB scan reporter
(`mgf-hrb-tools usb-scan`) prints the live tree, fidelity-tagged.*

**Phase U3 — acquire + control.** The privileged USB helper.
`CLAIMINTERFACE`, `RELEASEINTERFACE`, `RESET`, the typed control verbs
(`set_authorized` etc.) all with deadlines + verification. Raw channel
via SCM_RIGHTS. *Exit: claim → raw → ioctl-from-Python → release →
verify safe on a real USB device.*

**Phase U4 — survivability.** The reset-escalation ladder. Netlink
overrun handling. Interrupt-rate watchdog. The `Quarantined` state
transitions all exercised against a stub-firmware fake (a USB device
emulator that can be told to lie / hang / flood). *Exit: the
S-conformance USB suite green.*

Each phase ends with something testable; the conformance suite grows
with each phase.

---

## §10. The two consumers we already know

**vm-vmanager-app** (USB passthrough orchestration). Uses the
[`claim-a-device-for-vfio-passthrough.md`](../recipes/claim-a-device-for-vfio-passthrough.md)
pattern: claim an interface (or the whole composite device via
"claim all its interfaces" — a policy decision in `mgf-hrb-policy`,
not a kabi op), pass to the VM, watch for release, detach.

**figrecovery** (iFig DFU flow). Uses the
[`track-a-device-across-a-mode-change.md`](../recipes/track-a-device-across-a-mode-change.md)
pattern: snapshot, trigger DFU, wait for re-enumeration, confirm
identity via `sameLikely`, claim the new interface, raw-channel the
firmware payload, release.

Both patterns are concrete enough today that the worker design above
is sufficient to satisfy them. When real consumers integrate, friction
returns as `USB-NN` papers in [`FEEDBACK.md`](../FEEDBACK.md).

---

## §11. What this worker deliberately doesn't do

Honesty about boundaries (same fidelity discipline as the design):

- **USB-C alt modes / PD / cable identity** — `/sys/class/typec/*` is
  a separate subsystem. Out of scope; a future `mgf-hrb-typec` worker
  could cover it. (Same boundary `mgf-usb` drew in DOC-01.)
- **USB device drivers** — `mgf-hrb-usb` claims interfaces; it does
  not implement device protocols (HID, Audio, Mass Storage, etc.).
  Those are the consumer's problem. (Same boundary `mgf-usb` drew in
  its non-goals.)
- **`USBDEVFS_HOTPLUG`** — `libusb`'s polling-hotplug API. Linux's
  native event source is netlink uevent; we use that. No second
  source.

---

## §12. SOLID report (per the per-Part convention)

- **SRP.** This worker has one reason to change: how Linux exposes
  USB. It does not parse client bytes (facade), does not arbitrate
  ownership (core), does not enforce capability math (cap). The
  privileged helper has one reason to change: how privileged USB ops
  are executed.
- **OCP.** A new typed op (USB 3 streams, USBDEVFS_DROP_PRIVILEGES)
  adds to the `OpsTable` registry — no kabi seam change, no facade
  change, no core change.
- **LSP.** The USB backend satisfies the kabi behavioral contract
  (D-027) including the survivability constraints. Verified by the
  same conformance suite as the PCI backend and the fake.
- **ISP.** The privileged helper sees only typed ops, not bytes (Part
  11). The worker's interface to the facade is the worker
  interface — narrow.
- **DIP.** The worker depends on `mgf-hrb-kabi` (abstract) and
  `mgf-hrb-cap` (abstract). It contains the Linux-USB concrete; the
  rest of the system never sees it.
- **Consciously declined:** **libusb / libudev**. We could DIP-abstract
  behind a "USB substrate" trait — but on the future kernel, USB will
  speak the same 9-op kabi seam, so adding a sub-abstraction here
  would be flexibility nobody needs. The trait is the kabi seam
  already.

---

## §13. What the USB worker commits us to

- **Per-interface as the unit of resource** — composite devices are
  the rule, and arbitration must match the granularity reality.
- **Three substrates, declared not assumed** — sysfs / usbfs / netlink,
  with `BackendCapabilities` reporting which are usable on this host.
- **The runtime identity comparator** — `(Snapshot, Resource) →
  Confidence`, in the model, weighted by the worker.
- **Typed ops vocabulary** — the operator-visible state changes
  (reset, bind/unbind, authorized, port-power, autosuspend) match the
  shipped `mgf-usb` v0.2.5 surface.
- **Survivability specialised for USB** — deadlines on every USBDEVFS
  ioctl, reset-escalation through bus-power-cycle, the netlink overrun
  story, the interrupt-rate watchdog.
- **Structured permission errors with `suggested_fix`** — the udev
  rule recipe pre-shipped, `doctor --check-udev` to verify.

When `mgf-hrb-usb` is in code, the kabi seam will have been pressure-tested
against *two* concrete buses (PCI + USB) rather than one — and the
conformance suite gains a USB-flavoured fake that future workers
can borrow from.
