Watch
1
0
Fork
You've already forked SouveraineOS
0

blueline: consolidate wifi bring-up, adopt cellular/MMS path, task audit

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.
This commit is contained in:
Fimeg 2026-07-20 22:21:15 -04:00
commit 7ec2479f83
12 changed files with 533 additions and 9 deletions

View file

@ -0,0 +1,35 @@
# blueline cellular data + MMS path
Fido's LTE bearer (`ltemobile.apn`, modem netdev `qmapmux0.0`) is
IPv6-only with NAT64. IPv4 exists on the phone only through 464XLAT:
`blueline-clat.service` runs `blueline-clat.sh`, which derives a CLAT
IPv6 address from the bearer's current /64 and runs `clatd` (PLAT prefix
via RFC 7050 DNS64 discovery against the carrier's resolvers). The CLAT
installs a v4 default at metric 2048, so WiFi (metric 600) stays
preferred for ordinary traffic.
## The MMS routing trap
MMS is carrier-internal: the MMSC proxy (`205.151.11.13:80`) answers only
from inside Fido's network, i.e. only via the CLAT. With WiFi up, the
plain routing table sends it out `wlan0` and MMS silently dies. The clat
script pins `205.151.11.13/32 dev clat` after clatd brings the device up.
If Fido ever changes the proxy (it is set in
`~/.mms/modemmanager/mms` `CarrierMMSProxy`), update both places.
## mmsd-tng config contract (`~/.mms/modemmanager/mms`)
- `MMS_APN` must equal the APN of the *connected* bearer —
`ltemobile.apn`. (It shipped as `netsvcs`, which matches nothing;
mmsd-tng then never considers the bearer usable. Fixed 2026-07-20.)
- `CarrierMMSC=http://mms.fido.ca`, `CarrierMMSProxy=205.151.11.13:80`.
- mmsdtng runs in the user session; chatty is the D-Bus consumer
(`org.ofono.mms`).
## Known-good state (2026-07-20)
`mmcli -m any`: Fido LTE attached, bearer `ipv4v6` requested, v6-only
granted. `curl` to the proxy over the pinned clat route: TCP connect OK
from `192.0.0.1`. IPv6 has no WiFi default (LAN is v4-only), so all v6
traffic rides the bearer even on WiFi — metered-data caveat, accepted
for now.

View file

@ -0,0 +1,11 @@
[Unit]
Description=464XLAT CLAT daemon for the cellular bearer
After=NetworkManager.service
[Service]
ExecStart=/usr/bin/blueline-clat.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,58 @@
#!/bin/sh
# 464XLAT launcher: derive the CLAT IPv6 address from the cellular bearer's
# current /64 (changes per attach, so it cannot live in a static config),
# then run clatd against it. PLAT prefix is discovered via DNS64 (RFC 7050).
DEV="${CLAT_WAN_DEV:-qmapmux0.0}"
ADDR=""
i=0
while [ $i -lt 30 ]; do
ADDR="$(ip -6 addr show dev "$DEV" scope global 2>/dev/null \
| awk '/inet6/ { print $2; exit }' | cut -d/ -f1)"
[ -n "$ADDR" ] && break
i=$((i + 1))
sleep 2
done
if [ -z "$ADDR" ]; then
echo "no global IPv6 on $DEV after 60s, giving up" >&2
exit 1
fi
CLAT_V6="$(python3 -c "
import ipaddress, sys
net = ipaddress.IPv6Interface('$ADDR/64').network
print(net[0xc1a7])
")" || exit 1
# RFC 7050 PLAT discovery must query the CARRIER's DNS64 servers; the system
# resolver may prefer WiFi DNS (no DNS64) and discovery would fail.
DNS64="$(nmcli -g IP6.DNS device show "$DEV" 2>/dev/null \
| sed -e 's/ | /,/g' -e 's/\\//g')"
echo "bearer $DEV prefix holds $ADDR; CLAT address $CLAT_V6; DNS64 $DNS64"
# MMS must ride the CLAT: the Fido MMS proxy (205.151.11.13, from
# ~/.mms/modemmanager/mms CarrierMMSProxy) is only reachable from inside the
# carrier network, and while WiFi is up its default route (metric 600) beats
# the CLAT default (2048) — so without this pin, MMS send/receive dies the
# moment WiFi connects. Wait for clatd to create the device, then install a
# host route (replace = idempotent; re-runs on every service restart).
(
i=0
while [ $i -lt 30 ]; do
if ip link show clat >/dev/null 2>&1; then
ip route replace 205.151.11.13/32 dev clat
exit 0
fi
sleep 1
i=$((i + 1))
done
echo "clat device never appeared; MMS proxy route not installed" >&2
) &
# v4-conncheck off: WiFi may hold a v4 default route now but drop later;
# the CLAT must exist whenever the bearer does. Route metrics keep WiFi
# preferred (clat route is metric 2048).
exec /usr/bin/clatd clat-v6-addr="$CLAT_V6" plat-dev="$DEV" \
v4-conncheck-enable=0 ${DNS64:+dns64-servers="$DNS64"}

View file

@ -3,8 +3,17 @@ Description=WCN3990 wifi bring-up after DSP firmware services
# See blueline-wifi-defer.conf: ath10k_snoc is blacklisted from udev
# autoload because probing before rmtfs + tqftpserv serve the DSP firmware
# crashes the wifi firmware and wedges the driver for the whole boot.
After=rmtfs.service tqftpserv.service
# Ordering is the union of every dependency proven to matter:
# - rmtfs + tqftpserv: board data / wlanmdsp.mbn (2026-07-20 boot crashed
# with only slpi/adsp/rmtfs ordering — tqftpserv was the missing one)
# - slpi + adsp: TrustZone MSA arbitration during island boot (the retired
# blueline-wifi-load.service's finding, kept here)
# This unit supersedes blueline-wifi-load.service (same job, no retry,
# incomplete ordering — disabled 2026-07-20). blueline-wifi-msa-release
# stays: it owns the shutdown unload that keeps warm reboots clean.
After=blueline-slpi.service blueline-adsp.service rmtfs.service tqftpserv.service
Wants=rmtfs.service tqftpserv.service
Before=NetworkManager.service
[Service]
Type=oneshot

View file

@ -0,0 +1,19 @@
[Unit]
Description=Release WCN3990 WLAN firmware state before shutdown
# Warm reboot with ath10k_snoc loaded leaves the WLAN MSA assigned in TZ;
# the next boot's wlfw handshake then fails forever ("host capability
# request rejected: 90", later "failed to assign msa map permissions: -22")
# and only a cold power-off clears it. ath10k's remove path reassigns the
# MSA back to HLOS, so unloading the module at shutdown/reboot prevents it.
# (Adopted into the repo 2026-07-20 — this unit predates wifi-bringup and
# was only ever installed on the phone.)
After=NetworkManager.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/true
ExecStop=/usr/bin/modprobe -r ath10k_snoc
[Install]
WantedBy=multi-user.target

View file

@ -27,8 +27,18 @@ wake that races the lock can land input on an unlocked surface.
else calls `hyprctl dispatch dpms` — when hypridle still had a raw dpms
call after the toggle became authority, double-tap wake raced `on-resume`
and the touch controller calibrated against a half-ramped panel
(Pixel3Arch `73d0cd0`). Any new wake source (power button, dt2w, proximity
someday) goes through the toggle, or it will reintroduce the race.
(Pixel3Arch `73d0cd0`). Any new wake source (power button, dt2w, proximity)
goes through the toggle, or it will reintroduce the race.
Proximity landed 2026-07-20 (Pixel3Arch): `blueline-screen-toggle` itself
gates DT2W wake on `ProximityNear`, and a new `blueline-proximity-lock`
user service (same `monitor-sensor` watch pattern as `blueline-autorotate`)
blanks an already-on locked screen the moment proximity goes near, via
`blueline-screen-toggle off` — no second DPMS authority introduced. Gated
on `session state`'s `.locked` from the souveraine IPC; unlocked screens
are untouched. `iio-sensor-proxy` is now always-on rather than start/stop
on demand (the old wake-lockup hazard tied to it is confirmed gone — see
Pixel3Arch `PAF/slpi.md`).
## 3. The FTS touch controller has opinions

View file

@ -1,6 +1,8 @@
# TASK 01 — Favorites: wife's name on her number
**Status:** blocked on TASK-06 (qtpim build → contacts store).
**Status:** blocked on a contacts-store decision. qtpim retired
2026-07-20 (see archive/06) — the store needs a non-Qt-PIM design
(small owned store the Person layer reads directly) before this lands.
**Size:** small, once the contacts store exists.
## Goal

View file

@ -0,0 +1,202 @@
# 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:
1. **Unlock.** `session.unlock()` is already gated on `LockContext 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.
2. **Step-up / in-place reveal.** The doctrine's open slot: `PamContext`
against `souveraine-stepup` "accepting fingerprint mints a short-lived
grant," and `LockContentPolicy.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, exposes
`com.gundulabs.Gaze` on D-Bus. Stateful: `claim/release`,
`verify_start/stop`, `enroll_start/stop`, signals `face_status` /
`verify_status` carrying `CaptureStatus` (TooDark / etc.) and
`VerifyResult` (Match / NoMatch).
- `pam-gaze` — thin C-ABI PAM shim (`pam_sm_authenticate`).
- `pam-gaze-core` — talks to the daemon over zbus, returns
`Match / NoMatch / Unavailable`. The shim is dumb on purpose.
- **Tiered strictness** — 5 levels (`low/medium/high/maximum/custom`) swap
detector models (`det_500m.onnx` vs `det_10g.onnx`) and recognizer
(`w600k_mbf` vs `w600k_r50`) + liveness threshold. The model-swap-by-tier
idea maps cleanly onto our `ambient/personal/stepUp` tiers in reverse:
*higher trust decision → stricter pipeline*, not a single global knob.
- **Presence gates**`abort_if_ssh` and `abort_if_lid_closed` refuse 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 — `PamContext` is a standalone
connection type (not baked into `WlSessionLock`); 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 tiers `ambient / personal /
stepUp`. `personal` gate is `!screenLocked && !screenLockSecure`. `stepUp`
requires 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/lib` tree.
## What to build (full plan, not V1/V2)
One owned factor, end to end. No phased feature-stripping — design the
final shape, then implement.
1. **In-tree face daemon (the `gazed` equivalent), SouveraineOS-native.**
- Owns the camera, runs SCRFD/ArcFace/MiniFASNet, stores embeddings.
- NOT a D-Bus `com.gundulabs.Gaze` clone. Exposed over the existing
Souveraine IPC surface (same bus agents and sessiond use), with a
`verify_*` / `enroll_*` / status-signal shape derived from Gaze's
`CaptureStatus` + `VerifyResult` — because that enum pair is
well-thought-out and the `Unavailable` vs `NoMatch` distinction 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).
2. **PAM factor, not a PAM silo.** A face mint flows through the *existing*
`PamContext``souveraine-stepup` and the lock's `LockContext`, not a
new `pam-gaze` service with its own policy. Add face to the factor list
`/etc/pam.d/souveraine-stepup` already 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.
3. **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 `stepUp` runs 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.
4. **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 yields `Unavailable`, never `Match`. No unlock, no
reveal. This matches the suspend-before-lock fail-closed invariant.
5. **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/gaze` parallel tree — biometric data lives where secrets
live.
6. **Audit.** Face success/failure/unavailable events hit the existing
`SessionAudit` append-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.
7. **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` /
lock `LockContext` and 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 `gazed` process, 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 to `LockContentPolicy`. 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 trail` rows).
- 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`, commit `c9c8ffb`, 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.

View file

@ -0,0 +1,161 @@
# TASK 13 — Active Edge (squeeze) as a sensor input
**Status:** open. Substrate present, bring-up unit not yet drafted. The
sensor front-end config and the Android boot recipe both exist in-tree as
reference; nothing here is invented.
## Goal
Active Edge — the Pixel 3 squeeze gesture — becomes a usable input on
blueline: squeeze fires an event the shell (and, via surfacing, the agent)
can consume. Symmetric with the other sensor inputs already working over
SSC/IIO (accel, proximity, light). It is a sensor, not a keyboard key and
not a GPIO button — it must come up through the SSC/SLPI → libssc path
that already serves the other sensors.
## Context — the substrate already exists
This is the decisive point: the squeeze is **not** a missing kernel input
driver. It is a sensor exposed over the Snapdragon Sensor Core (SSC), and
the entire transport is already booted and serving sensors on blueline:
- SLPI remoteproc (remoteproc2) comes up via `blueline-slpi.service`.
- `blueline-hexagonrpcd-sdsp.service` serves the FastRPC / HexagonFS doorbell.
- `blueline-sns-registry-stage.service` regenerates the SSC sensor registry
from durable source under `/usr/share/qcom/sensors-src` (the LineageOS
`etc/sensors/` tree) via `sscregistrygen`, filtered by `-p PLAT -s SOC
-v PV`.
- `90-iio-sensor-proxy-ssc.rules` already exposes ssc-accel / ssc-proximity
/ ssc-light. Same IIO bus a squeeze client would read.
So the heavy lifting is done. What's missing is the **squeeze-specific**
pieces: the power rail on the sensor, and a userspace client for the
`sns_touch_gesture` SUID.
## Context — the squeeze front-end and power path
Two reference files in the tree pin the exact contract:
- `~/Projects/Pixel3Arch/blobs/los-vendor/etc/sensors/registry/b1_touch_gesture_0.json`
— the squeeze sensor config: `controller_id 1, bus_instance 5,
slave_address 73 (0x49), irq_num 125, owner sns_touch_gesture,
hw_platform [OEM], soc_id [321], platform_version
[0x20000:0xFFFF0000]`. This matches blueline (device 0x20028), so the
sensor is **not** excluded by the platform-version filter — the registry
already admits it once the power rail is up.
- `~/Projects/PostMarketOS-Blueline/android-reference/vendor-stack-20260621/vendor/bin/init.edge_sense.sh`
— LineageOS's boot recipe: it raises **PM8998 GPIO 2** (the edge-sense
power rail) via `/sys/class/gpio/export`, sets direction out, drives
high. The comment names the chain explicitly: Elmyra sensor HAL → SLPI
driver, powered through PM8998. The script's own TODO notes the rail
should ideally be gated on use by the SLPI driver, which "doesn't have
direct access to the PM8998 GPIOs" — so userspace powers it on boot.
The squeeze and the microphone are fully independent: PM8998 GPIO 2 + I²C
sensor on SLPI vs. WCD9340 DMIC + MICBIAS over LPASS/SLIMbus. No shared
GPIO, I²C bus, registry, DSP process, or clock. Active Edge bring-up must
not be folded into the audio boot path — it's a separate variable.
## Update 2026-07-20 — step 1 is a kernel patch, not a systemd unit
Verified live on the phone and against kernel source: the vendor script's
whole mechanism is dead on this kernel. `/sys/class/gpio` does not exist
(`find /sys/class/gpio` — nothing; mainline moved to the `gpiod`
character-device ABI, no legacy sysfs export path). A pure rootfs-overlay
unit cannot replicate `init.edge_sense.sh` as originally scoped.
Checked the actual devicetree, both source and live (`blueline-live.dts`
in PostMarketOS-Blueline's `dt-work/`, decompiled from the running boot):
`pm8998_gpios` (`gpio@c000` under `pmic@1`, `compatible = "qcom,pm8998-gpio",
"qcom,spmi-gpio"`) is real and live — `sdm845-google-common.dtsi:716`
already uses it for `gpio6` (volume-up). **GPIO 2 (the squeeze rail) has
no node at all** — not disabled, never added, on either the kernel source
tree (`kernel/linux-blueline-work/arch/arm64/boot/dts/qcom/sdm845-google-common.dtsi`)
or the live decompile. This is a real gap, not a config/boot-state issue.
GitHub search turned up `LineageOS/android_kernel_motorola_msm8998:arch/arm/boot/dts/qcom/msm8998-loki.dtsi`
(different SoC family, same PMIC generation) with a `gpio@c100 { /*
PM8998_GPIO2 */ }` node — confirms GPIO2 addresses at register offset
`0xc100` (one 0x100 slot per pin from `gpio@c000`'s base) and that this
is a normal per-pin pinctrl subnode. That file uses the legacy CAF
`qcom,mode`/`qcom,vin-sel`/`qcom,src-sel` binding style, which is NOT what
blueline's mainline-derived tree uses — `vol-up-active-state` (gpio6) uses
the modern generic pinconf shape (`function`, `input-enable`,
`bias-pull-up`, `qcom,drive-strength`). The GPIO2 node must match that
shape, not the CAF one — the Motorola file is useful only for confirming
the register-offset/binding-family, not as copy-paste source.
1. **Devicetree patch (the actual missing piece).** Add a `pm8998_gpios`
subnode for GPIO 2 to `sdm845-google-common.dtsi` (or a Pixel3Arch
kernel overlay), mirroring `vol-up-active-state`'s binding style:
`function = "normal"`, output-enable (not input-enable — this rail
needs to be driven, not read), no pull needed if driven push-pull.
Whether this should be a plain pinctrl state some other node
references, or a `gpio-hog` (drives itself high automatically at
kernel boot, zero userspace component, matching what the always-on
rail actually needs) is the open design choice — `gpio-hog` is
probably right since nothing needs to read this pin, only assert it.
Requires a kernel rebuild + deploy cycle on archdev, not a rootfs-only
change. If `gpio-hog` doesn't fit the binding, a small `libgpiod`
(`gpioset`) oneshot systemd unit is the userspace fallback — targeting
`pm8998_gpios` line 2 via the modern char-device API, not
`/sys/class/gpio`.
2. **Confirm the SUID is served.** Once the rail is live and SLPI is up,
verify `sns_touch_gesture` appears as a served SUID (libssc client
enumerate) and produces a gesture event on squeeze. The registry JSON
already admits it; the question is purely whether the SLPI image
serves the SUID and the powered sensor answers. First decisive test
requires the phone booted live.
3. **Userspace consumer.** Wire the gesture into the existing IIO/sensor
path the other SSC sensors already use (the
`90-iio-sensor-proxy-ssc.rules` surface), or a small libssc client if
the SUID isn't auto-exposed as IIO. Either way the squeeze becomes a
normal shell-consumable input event — not a special case.
4. **Shell binding + agent surfacing.** The squeeze is an input with a
natural home in the keyboard/routing system (see Keyboard System
memory): it must register through that system, not as a hardcoded
binding, so it doesn't collide. A squeeze can surface the glance
(TASK-02), trigger the assistant, or whatever the routing policy maps
it to.
## Acceptance
- Cold boot: `blueline-edge-sense.service` brings the rail up, the unit
goes active, and `sns_touch_gesture` is enumerable as a served SUID.
- A physical squeeze fires a gesture event visible to the userspace
consumer.
- The event reaches the shell through the keyboard/routing system as a
normal input, bound by policy (not hardcoded).
- Bring-up is independent of the audio/mic boot — proven by the two
sharing no GPIO, bus, or DSP path.
## Do not
- **Do not fold this into the WCD9340 mic work.** Separate variable,
separate substrate. The mic kernel build does not touch PM8998 GPIO 2,
SLPI sensor registry, or `sns_touch_gesture`.
- **Do not write a kernel input driver for squeeze.** It's a sensor over
SSC; the transport is already up. A kernel GPIO-key would be the wrong
layer and would duplicate the rail handling.
- **Do not hardcode the squeeze binding.** Route it through the
keyboard/routing system like any other input.
- **Do not run the vendor `init.edge_sense.sh` by hand as the steady
state.** It's a reference for what the systemd unit must do; the unit
replaces it.
## Connects to
- `init.edge_sense.sh` (PM8998 GPIO 2 power recipe — the unit's spec).
- `b1_touch_gesture_0.json` (sensor front-end; already in registry).
- `blueline-slpi.service`, `blueline-hexagonrpcd-sdsp.service`,
`blueline-sns-registry-stage.service` (substrate already booted).
- `90-iio-sensor-proxy-ssc.rules` (the IIO surface the other SSC sensors
use).
- Keyboard System memory (routing — squeeze must register as an input,
not a hardcoded binding).
- TASK-02 lockscreen glance (one plausible policy target for a squeeze).
- GPT Sol gap prompt (sent 2026-07-18) covers the same gap from the
external side; this task is the local in-tree half.

View file

@ -12,17 +12,20 @@ plus open threads from the 2026-07-17 session.
| # | Task | Status | Blocks / blocked-by |
|---|------|--------|---------------------|
| 1 | [Favorites — wife's name on her number](01-favorites-contacts.md) | blocked | needs TASK-09 (qtpim build) |
| 1 | [Favorites — wife's name on her number](01-favorites-contacts.md) | blocked | needs a contacts-store decision (qtpim retired — see archive/06) |
| 2 | [Lockscreen-as-Rust-system: glance + swipe](02-lockscreen-rust-system.md) | in progress | stopgap shipped 2026-07-17 |
| 3 | [Boot timing: fade splash → lock, no extra fixes](03-boot-timing-splash-to-lock.md) | open | pairs with TASK-11 |
| 3 | [Boot timing: fade splash → lock, no extra fixes](03-boot-timing-splash-to-lock.md) | open | paired with TASK-11 (now archived) |
| 4 | [Notification server (org.freedesktop.Notifications)](04-notification-server.md) | open | — |
| 5 | [Crash reporter surfacing](05-crash-reporter.md) | half-landed | pairs with TASK-04 |
| 6 | [qtpim aarch64 build → contacts store](06-qtpim-build-contacts.md) | staged | archdev binfmt wall |
| 7 | [WCD9340 mic: SLIM TX channel/port contract](07-mic-wcd9340-slim-tx.md) | diagnosed | frontier; pairs with kernel series |
| 8 | [Device state manager / power profiles](08-device-state-manager.md) | open | blocked-by TASK-09 suspend item |
| 9 | [Suspend-resume FTS calibration race](09-suspend-resume-fts.md) | interim-fix | kernel-side; cold-boot only |
| 10 | [Boot chain: pmOS init → mkinitcpio](10-boot-chain-mkinitcpio.md) | open | flash-caution |
| 11 | [souveraine-secrets on the phone](11-souveraine-secrets.md) | built, blocked | seed restore decision |
| 12 | [Face auth as a capability factor (Gaze reference)](12-gaze-reference-face-auth.md) | open | per-target: phone needs front-cam bring-up |
| 13 | [Active Edge (squeeze) as a sensor input](13-active-edge-squeeze.md) | open | substrate up; bring-up unit not drafted |
Archived (see `archive/`): 06 qtpim build — retired 2026-07-20, qtpim is
no longer the contacts path; 10 boot chain — done, we ship our own
mkinitcpio; 11 souveraine-secrets — built and committed 2026-07-20.
## Conventions

View file

@ -59,3 +59,12 @@ A contacts query returns from the phone. TASK-01 unblocked.
## Connects to
TASK-01 (favorites), handoff doc item 9 (crossbuild policy).
---
**RETIRED 2026-07-20.** qtpim is dead as the contacts path — old,
unmaintained, and the archdev binfmt wall made its cross-build a money
pit. The contacts store needs a fresh decision (likely a small owned
store the Person layer reads directly — EBook/sqlite class, no Qt PIM
dependency). TASK-01 (favorites) re-pointed at that decision; nothing
else depended on this build.

View file

@ -41,3 +41,8 @@ to the splash. Slot verified before flash; rollback slot intact.
TASK-03 (boot timing — adjacent; the splash fade is post-initramfs).
Pixel3Arch boot tooling.
---
**ARCHIVED 2026-07-20** — marked done in the index ("we ship our own
mkinitcpio"); moved out of the open queue during the task audit.