6.1 KiB
TASK 74 — LibrePods: an admitted accessory, not a second shell
Status: shell projection visually exercised on the laptop through an
isolated development fixture, 2026-08-13; no daemon/device proof yet. Repos:
Fimeg/librepods (AirPods protocol), Fimeg/souveraine (session authority +
Quickshell surface), SouveraineOS (this contract).
The decision
LibrePods owns the Bluetooth/AACP protocol and the per-headset state. It is an application producer, not a new authority component and never a second shell. The attractive AirPods opening surface belongs in Souveraine's Quickshell tree.
LibrePods protocol producer
│ admitted accessory event
▼
sessiond — identity, policy, audit, presentation admission
│ one permitted presentation event
▼
Quickshell — QML projection only
No LibrePods process writes GlobalStates.qml or calls Quickshell IPC. Those
paths have no caller identity today; doing so would recreate the ungated
producer problem demonstrated in TASK-41.
The existing ii-base/DeviceConnectNotification.qml is evidence, not the
implementation: it is a generic four-second toast and the current phone
shell.qml does not instantiate it. The AirPods card is a new, deliberate
surface with its own owner and lock-tier policy.
What is real today
Fimeg/librepodsis a private Gitea mirror onlinux/rust, with GitHub retained as upstream and Gitea Actions as its build path.- Its Rust branch opens BlueZ L2CAP/AACP, reads battery and proximity keys, and
has an initial disconnect/reconnect cleanup commit (
a629229). - The LE monitor reads encrypted advertising payloads, but no source or packet fixture proves a distinct case-open transition. “Bluetooth connected” is not an acceptable substitute for the opening animation trigger.
souveraine-sessiondalready owns the user-tier Unix socket, device state trail, and shell-facing subscription path. It currently has no accessory event/admission verb.- The Souveraine shell has an
AirPodsSurfaceand anAccessoryPresentationholder. On 2026-08-13, an isolatedqs -pfixture supplied one synthetic presentation to that same QML and produced a mappedsouveraine:airpodsoverlay on the laptop. That proves the QML projection, placement and animation only: the fixture had nosessiondconnection or authority claim.SessiondBridgeaccepts only the futureaccessory_presentationdirective; no LibrePods path can trigger it today. - The laptop's composed QML tree was missing the newly manifested module and
singleton, so reloads failed before the shell could load. That composition
was repaired on 2026-08-13.
AirPodsSurfacenow keeps Hyprland's focused output when available and falls back to the one announced screen under Membrane. This is source/config repair, not daemon or device proof.
The contract to build
1. Protocol truth — LibrePods
LibrePods produces a bounded AccessoryObservation; it does not decide what
the screen does. The first useful fields are a stable local accessory id,
connection state, left/right/case charge, charging state, and an evidence tag
for the observed AirPods transition.
Capture and redact real packet fixtures before naming an observation
case_opened. The fixture test proves parser behaviour without an AirPods
pairing session. Keys, IRKs and decrypted advertising payloads are secrets and
never appear in logs, fixtures, the audit trail, or the QML payload.
2. Admission — sessiond
Add an explicit, narrow presentation request to sessiond rather than a general-purpose shell call. It must:
- identify the producer and ultimately require TASK-41's capability grant;
- reject an unadmitted event with
not_permittedand record that refusal; - record an admitted event in the existing forensic trail;
- classify the requested payload as ambient or personal before it reaches the shell;
- never treat an accessory event as human input, a wake request, an unlock factor, or a device-state transition.
Until TASK-41 lands, any same-uid development route is explicitly an unattested diagnostic path, not a deployed authority claim.
3. Projection — Quickshell QML
The shell subscribes to sessiond's admitted event stream and owns the one
AirPodsSurface state. It renders the case-open card when the display is
already active; an accessory opening does not wake the panel or compete with
lock/power policy.
While screenLockSecure is true, default to an ambient announcement only.
Whether device name and individual battery levels are ambient is a policy call
to make explicitly before exposing them on the lock surface.
The card is allowed to be gorgeous: the case opens, buds settle, charge arrives in sequence, and the card melts away into the ordinary Bluetooth controls. Its animation is a projection of an admitted event, never the event source.
Order of work
- Let LibrePods CI establish the current Rust lifecycle patch; add fixture tests for the specific advertisement transition only after capture.
- Define and test sessiond's accessory admission verb and forensic event, tied to TASK-41's producer grant design.
- Wire the visually exercised
AirPodsSurfaceto the tested sessiond admission path once sessiond emits the admitted event. - Package the producer and surface through the normal Gitea path; test on actual AirPods, first unlocked and then locked.
Acceptance
- One captured, redacted fixture proves the animation trigger.
- LibrePods cannot directly open or mutate any shell surface.
- An unadmitted producer is refused and the refusal is visible in the trail.
- One admitted event renders one QML card; reconnects cannot duplicate it.
- The card never wakes, unlocks, or exposes a Personal-class field while locked without an explicit policy decision.
- Gitea builds the relevant code; the final behaviour is exercised on real AirPods and marked separately from CI proof.
Connects to
TASK-08 (device state), TASK-41 (attested producers), TASK-19 (settings and
surface policy), TASK-25 (packaged delivery),
docs/DEVICE-STATE-MACHINE.md, and
docs/souveraine-components/README.md.