Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/40-inference-tiering-sensors.md
Fimeg 6a86b510b5 correct the graphical-session.target claim
Four docs recorded 'nothing starts it' as settled fact and concluded don't use
the target, where the conclusion available was light the target. viewtop's
session launcher starts it now.
2026-08-15 10:55:25 -04:00

8.9 KiB
Raw Permalink Blame History

TASK 40 — Inference tiering for the sensor surfaces

Status: open, gating. Raised 2026-07-27 (Casey, explicitly: the "based on what they might infer" lens). Size: decisions first, then small code. Repos: souveraine (sensord, sessiond, dial QML), Pixel3Arch (blueline-edge-sense). Gates the grip producer's raw stream (TASK-13 shipped without it) and TASK-31's dial IPC.

The lens, in its own words

SECURITY-AUDIT.md P1 — inference-first tiering:

Permission classes are defined by what a capable agent could infer from a stream, not by what the field literally is. A contact count, an app-launch timing log, an ambient noise level, a raw accelerometer reading — each is "just data" per-field, and each is an inference substrate for a sufficiently capable model.

AOSP retrofitted the 200Hz sensor cap and HIGH_SAMPLING_RATE_SENSORS after realizing motion sensors infer keystrokes, gait, identity — it was a gap, then a patch, not first-class design. We model inference as first-class.

Application: every tier assignment must be justified against the inference lens. "Looks innocent" is not a defense.

Three things landed or moved in the last day that this has never been applied to. Two are about to start emitting for the first time, which is exactly the moment P1 says to do the work — before the stream flows, not after.


1. The sensor stream is about to exist for the first time

souveraine-sensord has been enabled and dead from every boot (its unit hung off graphical-session.target, which nothing on this device started — fixed 2026-07-27; the target itself is started by souveraine-session-viewtop as of TASK-76, so hanging off it is now the correct idiom). The forensic trail proves the consequence: 2093 wake, 138 transition, 121 decision, 29 error, and not one sensor entry. So the device state machine — and through doctrine §13, the agent — has never had sensor evidence at all.

Turning it on is therefore not "restoring" a stream. It is opening one.

What the current shape gets right, apparently by accident

sensord reports edges, not levels (de3a332, "sensord: report edges, not every sample"):

  • light → {"changing": bool}, never lux
  • accel → {"moving": bool}, derived from iio-sensor-proxy orientation changing, not from raw acceleration
  • proximity → near/far, debounced

That is close to the minimum an inference-first design would have chosen. The agent learns that something changed, not the waveform. Accel in particular is orientation-only, which is the difference between "the phone was picked up" and the 200 Hz stream AOSP had to cap.

This should be written down as a property to preserve, not left as a side-effect of an unrelated commit. Nothing currently stops a future change from passing lux through "because the machine needs it".

Which is exactly what auto-brightness proposes to do

DEVICE-STATE-MACHINE.md §12 (auto-brightness) requires lux to enter as sensor_input so brightness can leave as an Action. That converts light from a boolean edge into a continuous level, and P1 says that is a tier change requiring justification, not an implementation detail.

Ambient light over time is the sleeper in this set. Individually it looks like the most innocent value on the device; as a series with timestamps it carries sleep and wake times, indoor/outdoor transitions, commute shape, and whether the house is occupied. Combined with proximity and orientation edges it is a timeline of a day — P1's "intersection attack" line, verbatim.

The decision this task owes: lux may be needed by the machine without being readable by the agent. Those are different consumers and the code does not currently distinguish them — sensor_evidence is on the snapshot and doctrine §13 makes snapshots agent-readable. Splitting "what tick() computes on" from "what describe/snapshot exposes" is the concrete work.


2. Grip is the sharpest case on the device

elmyra_raw is six strain gauges at 100 Hz (libssc patched to take the fastest advertised rate, TASK-13). Against P1 this is not a near-miss of the AOSP precedent — it is the precedent:

  • Keystrokes. Every tap flexes the chassis. A 100 Hz six-channel strain array on the frame is a typing side-channel, which is the literal thing the HIGH_SAMPLING_RATE_SENSORS permission was retrofitted for.
  • Identity. Where and how hard a person holds a phone is a biometric. Google's own factory calibration (edgesense_roller_cal) exists because the gauges differ per unit and per grip — that sensitivity is the feature and the leak.
  • Health. Hand tremor is in that band.

TASK-13 has been, correctly, a bring-up task. It contains zero tier analysis, and its own producer is the leaky shape: blueline-edge-sense opens elmyra_raw and decodes six floats per sample into AP userspace at 100 Hz, which is the highest-inference stream we have ever run.

The architecture that makes it work is also the architecture that fixes it

This is the part worth not missing. TASK-36 established that squeeze cannot be configured from the AP at all: the AFE control surface (ashConfigureElmyraFilters) is reachable only from inside a CHRE nanoapp, and elmyra.so is that nanoapp. It consumes the raw gauges on the SLPI and emits a gesture.

So the correct implementation is also the private one:

raw stream what crosses to the AP
blueline-edge-sense (today, diagnostic) 100 Hz × 6 channels, in AP userspace everything
elmyra.so nanoapp (TASK-36) stays on the SLPI "a squeeze happened"

That is the tiering argument made in hardware. The rule this task should set: the raw elmyra_raw stream is a bring-up instrument, not a product surface. blueline-edge-sense keeps existing as a diagnostic, gated and not running by default; the shipping path is the nanoapp's gesture event.

Doing it the other way — shipping the raw reader because it works sooner — would be P1's "gap, then a patch" repeated knowingly.


3. The dial enumerates capabilities, and enumeration is not gated

TASK-31 already says the dial "does not appear over a locked screen with anything personal on it". That governs painting. It does not govern the IPC, and the IPC is the agent-facing surface.

modules/souveraine/dial/DialHost.qml today:

IpcHandler {
    target: "dial"
    function open(): void  { scope.dialOpen = true; ... }
    function close(): void { ... }
    function toggle(): void { ... }
    function entries(): string { /* label, icon, enabled, reason for every entry */ }
}

None of the four checks lock state, and GlobalShortcut { name: "dialToggle" } does not either. Two distinct problems:

  • entries() is reconnaissance without exercise. P3 says capability use is visible, not just the grant — but enumeration is neither a grant nor a use, so it is invisible. The returned reason strings are designed to be explanatory ("no focused window to close"), which makes the refusal itself an information channel: it answers "is an app open right now" to any caller, at any lock state, leaving no trace.
  • The contents are the leak, not the ring. Today's entries are generic (lock, screenshot, kill window, health, agent, keyboard). TASK-31's whole design is that entries come from the verb tables (TASK-30) — at which point the dial's contents become "every mutating verb currently permitted", which is precisely a capability map, and a per-contact or per-app entry would carry personal data into the ring.

The decision this task owes: entries() must answer at the caller's tier, not the device's — an ambient caller sees ambient verbs. That has to be settled before TASK-30 feeds the dial, because afterwards it is a filter retrofitted onto a list, which is the shape P1 warns about.


Acceptance

  • The edges-not-levels property of sensord is documented as a requirement with a rationale, so a later change has to argue with it.
  • Lux reaches the state machine for auto-brightness without widening what the agent can read, or the widening is explicitly justified and recorded.
  • elmyra_raw is designated a bring-up instrument; the shipping squeeze path is the nanoapp gesture (TASK-36), and blueline-edge-sense does not run by default.
  • dial entries() and dial open() answer at the caller's tier, and the refusal reason strings are checked for what they disclose.
  • Each of these is justified in writing against P1, per its own "Application" clause — "looks innocent" is not a defense.

Connects to

SECURITY-AUDIT.md P1/P2/P3, TRUST-BOUNDARY-MATRIX.md, SESSION-AUTHORITY-DOCTRINE.md §9 (evidence not fact) and §13 (agent-readable context), DEVICE-STATE-MACHINE.md §4 and §12, TASK-13 (grip), TASK-36 (the nanoapp that makes the tiering physical), TASK-30 (verb tables — the thing that will feed the dial), TASK-31 (the dial).