wifi: bringup unit now carries the union ordering (slpi/adsp for TZ MSA, rmtfs+tqftpserv for firmware serving — the 2026-07-20 boot proved the old load unit's ordering incomplete) and supersedes blueline-wifi-load; msa-release adopted into the repo (shutdown unload that keeps warm reboots clean). cellular: adopt blueline-clat service+script. clat script now pins a host route to the Fido MMS proxy via the CLAT — with wifi up the wlan default beat the clat default and MMS died off-carrier. mmsd config contract documented (MMS_APN must match the bearer APN ltemobile.apn; shipped as netsvcs which matches nothing). tasks: retire 06 qtpim (no longer the contacts path), archive 10 (done), index catches up on 11 (archived); 01 re-pointed at a contacts-store decision.
10 KiB
TASK 12 — Face auth as a capability factor (Gaze is the reference, not the dep)
Status: open. Reference cloned to ~/Projects/gaze (GunduLabs/gaze @
c9c8ffb, MIT, v0.2.6, 2026-07-18). This doc scopes the SouveraineOS-owned
face-auth factor modeled on it. We own the source — anything touching the
permission/capability gating is in-tree, not vendored.
Goal
A first-class face-auth factor that plugs into the existing capability
tier model as a step-up / unlock credential, symmetric with fingerprint and
PIN. Gaze proves the pipeline is real and fully on-device; we build the
SouveraineOS-native equivalent against the primitives we already have
(PamContext, StepUpAuth, LockContext, LockContentPolicy), not a
wrapper around gazed.
Two placements, one mechanism:
- Unlock.
session.unlock()is already gated onLockContext PIN/PAM(TRUST-BOUNDARY-MATRIX row). Face becomes a PAM factor behind that same gate — a face mint unlocks, exactly as a fingerprint mint would. - Step-up / in-place reveal. The doctrine's open slot:
PamContextagainstsouveraine-stepup"accepting fingerprint mints a short-lived grant," andLockContentPolicy.allowsOnLock()gains its third condition (|| hasFreshGrant('read')). Face is that same mint for a second factor type — reveal message/notification bodies on the lock surface without unlocking the session. (SESSION-AUTHORITY-DOCTRINE §2, currently "gated on the fingerprint sensor coming online (not yet true on blueline)." Face is unblocked by any working front camera; it does not wait on the FP driver.)
Face auth is not a new trust mechanism. It is one more factor behind the two existing gates. The doctrinal guarantees are unchanged: step-up never unlocks the session, never accepts an agent-supplied boolean, the grant is short-lived / in-memory / action-family-bound.
Context — what Gaze does (the reference)
Cloned at ~/Projects/gaze. Architecture worth copying, decisions worth
re-making:
- Pipeline (all on-device, no network):
Camera → SCRFD detect → align → ArcFace embed → match → MiniFASNet-V2 liveness. Optional IR camera for high-security spoof resistance. - Process split (the part to copy structurally):
gazed— system daemon, owns the camera and embeddings, exposescom.gundulabs.Gazeon D-Bus. Stateful:claim/release,verify_start/stop,enroll_start/stop, signalsface_status/verify_statuscarryingCaptureStatus(TooDark / etc.) andVerifyResult(Match / NoMatch).pam-gaze— thin C-ABI PAM shim (pam_sm_authenticate).pam-gaze-core— talks to the daemon over zbus, returnsMatch / NoMatch / Unavailable. The shim is dumb on purpose.
- Tiered strictness — 5 levels (
low/medium/high/maximum/custom) swap detector models (det_500m.onnxvsdet_10g.onnx) and recognizer (w600k_mbfvsw600k_r50) + liveness threshold. The model-swap-by-tier idea maps cleanly onto ourambient/personal/stepUptiers in reverse: higher trust decision → stricter pipeline, not a single global knob. - Presence gates —
abort_if_sshandabort_if_lid_closedrefuse to fire when no user is physically present. This is the right instinct and aligns with our fail-closed lock policy: no camera / too dark / SSH session →Unavailable→ don't unlock, stay secure. - Storage (reference, not to copy verbatim):
/var/lib/gaze/users, config/etc/gaze/config.toml. We will not inherit this layout — see "What to build."
Files of record in the reference:
gaze-core/src/{dbus.rs,config.rs,face.rs,detect.rs,camera.rs},
pam-gaze/src/lib.rs:113 (pam_sm_authenticate),
pam-gaze-core/src/lib.rs (D-Bus auth flow, AuthOutcome).
Context — what SouveraineOS already has
SESSION-AUTHORITY-DOCTRINE.md§2-3 —PamContextis a standalone connection type (not baked intoWlSessionLock); step-up points a second PAM context at/etc/pam.d/souveraine-stepup, accepts password or fingerprint in any order, mints a short-lived grant bound to an action family. Face slots in as a third accepted factor on the same context.SESSION-TRUST-ARCHITECTURE.md— capability tiersambient / personal / stepUp.personalgate is!screenLocked && !screenLockSecure.stepUprequires a recent grant.TRUST-BOUNDARY-MATRIX.md—session.unlock()→ stepUp,LockContext PIN/PAM;StepUpAuth.requestAuth()→ PAM conversation. Both are the insertion points for a face factor.02-lockscreen-rust-system.md— the glance/lock surface where an in-place reveal (face mint →hasFreshGrant('read')) would show notification/message bodies without unlocking.11-souveraine-secrets.md— secrets store; face embeddings are a biometric secret and belong in the same trust/at-rest discipline, not a parallel/var/libtree.
What to build (full plan, not V1/V2)
One owned factor, end to end. No phased feature-stripping — design the final shape, then implement.
-
In-tree face daemon (the
gazedequivalent), SouveraineOS-native.- Owns the camera, runs SCRFD/ArcFace/MiniFASNet, stores embeddings.
- NOT a D-Bus
com.gundulabs.Gazeclone. Exposed over the existing Souveraine IPC surface (same bus agents and sessiond use), with averify_*/enroll_*/ status-signal shape derived from Gaze'sCaptureStatus+VerifyResult— because that enum pair is well-thought-out and theUnavailablevsNoMatchdistinction is load-bearing for fail-closed behavior. - Camera/tiling/liveness run as their own service the PAM path can reach; keep the PAM-facing shim dumb (Gaze's split is right).
-
PAM factor, not a PAM silo. A face mint flows through the existing
PamContext→souveraine-stepupand the lock'sLockContext, not a newpam-gazeservice with its own policy. Add face to the factor list/etc/pam.d/souveraine-stepupalready accepts. The unlock path (session.unlock()) and the reveal path (StepUpAuth→LockContentPolicy.allowsOnLock()third condition) both consume it identically — one factor, two effects, by virtue of which context it mints into. This is the doctrine's design; we're populating it, not extending it. -
Tiered pipeline strictness. Reuse Gaze's model-swap pattern but map it onto our capability tiers at the decision side: a grant that will unlock the session or satisfy
stepUpruns the strict pipeline (det_10g+ IR-if-available + tight liveness); an ambient read-mint can run the fast pipeline. The mapping is policy; the mechanism is two model sets behind the same daemon. -
Presence / fail-closed gates (copy Gaze's instinct, wire to our state). Refuse to capture when:
- session is SSH/remote (our session authority already knows this),
- lid closed / no front camera available (device-state manager, TASK-08),
- liveness fails or capture is
TooDark. Every one of these yieldsUnavailable, neverMatch. No unlock, no reveal. This matches the suspend-before-lock fail-closed invariant.
-
Enrollment + storage. Enrollment UI in the shell settings app (SETTINGS-APP-PLAN). Embeddings stored under the souveraine-secrets discipline (TASK-11), at-rest protected, never logged, never transmitted. No
/var/lib/gazeparallel tree — biometric data lives where secrets live. -
Audit. Face success/failure/unavailable events hit the existing
SessionAuditappend-only JSONL with hash chain (TRUST-BOUNDARY-MATRIX row "Session audit trail"). A face unlock is a security-relevant event and must be in the same trail as PIN/PAM unlocks — not a separate log. -
Models. Ship the ONNX models Gaze uses (SCRFD detector, ArcFace recognizer, MiniFASNet liveness) as packaged assets, same provenance discipline as any other binary blob in the rootfs. Pin versions; do not fetch at runtime.
Acceptance
- Cold boot to unlock: a face match mints through
souveraine-stepup/ lockLockContextand unlocks the session, indistinguishable at the capability layer from a PIN or fingerprint unlock. Same audit row shape. - On the lock surface (post-TASK-02 glance): a face mint reveals message /
notification bodies in place via
hasFreshGrant('read')without unlocking the session. Lock is still requested; compositor still secure. - SSH session, lid closed, camera gone, too dark, or liveness fail →
Unavailable→ no unlock, no reveal, no audit "success." Fall through to PIN silently. - Enrollment, model swap by tier, and embedding storage live entirely
in-tree under the secrets discipline. No
gazedprocess, no/var/lib/gaze, no GunduLabs D-Bus name on the bus.
Do not
- Do not vendor or wrap
gazed. It's a reference. We own the source for anything in the permission-gating path. The PAM shim, the daemon, the policy — all in-tree. - Do not make face a new trust mechanism. No new tier, no new grant
type. It is a factor behind
PamContext/StepUpAuth, exactly as fingerprint is framed in the doctrine. The "two factors" framing is explicitly rejected in SESSION-AUTHORITY-DOCTRINE — don't reintroduce it. - Do not gate personal content on face alone. The reveal is
hasFreshGrant('read'); whether that grant came from face, fingerprint, or PIN is irrelevant toLockContentPolicy. Keep the policy factor-agnostic. - Do not special-case face in the audit trail. Same JSONL, same hash chain, factor recorded as a field — not a parallel log.
Connects to
- SESSION-AUTHORITY-DOCTRINE §2-3 (the slot this fills — "gated on the fingerprint sensor coming online"; face is unblocked by any camera).
- TRUST-BOUNDARY-MATRIX (
session.unlock,StepUpAuth.requestAuth,Session audit trailrows). - TASK-02 lockscreen glance (in-place reveal surface).
- TASK-08 device-state manager (lid-closed / camera-present gate).
- TASK-11 souveraine-secrets (embedding at-rest).
- SETTINGS-APP-PLAN (enrollment UI).
- Blocker, per-target: laptop front camera is straightforward; Pixel 3 (blueline) front-camera bring-up is not yet solved — face auth is moot on the phone until a userspace camera device exists for the daemon to grab. Not a reason to scope down; a reason to land laptop-first and have the phone light up when the camera does.
Reference provenance
- Upstream:
https://github.com/GunduLabs/gaze, commitc9c8ffb, MIT. - Local clone:
~/Projects/gaze(reference only; not a submodule, not a build input). - Cite file:line when borrowing a design decision (not code) from it.