SouveraineOS umbrella: docs, dumps, handoffs, cross-repo STATE
This commit is contained in:
commit
dc4fd14bbd
13 changed files with 2360 additions and 0 deletions
88
STATE.md
Normal file
88
STATE.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# SouveraineOS — cross-repo state
|
||||
|
||||
Souveraine is an OS, an agent substrate, and the name of an agent.
|
||||
Ani + Aster = Annie (primary consciousness + subconscious = composite).
|
||||
Hard constraint everywhere: no GNOME or KDE library dependencies.
|
||||
|
||||
This repo is the umbrella: cross-repo docs, decisions, session dumps, and
|
||||
this state file. Component repos keep their own repo-local PAF/ docs; what
|
||||
crosses repo boundaries lives here. Update this file when component state
|
||||
moves; it is the resume anchor.
|
||||
|
||||
Last updated: 2026-07-16.
|
||||
|
||||
## Component repos
|
||||
|
||||
### souveraine (`~/Projects/souveraine`)
|
||||
Branch `experimental/session-trust-phase1` @ `04a8390`.
|
||||
- Session Trust Phase 1 is live on phone + laptop under `qs -c souveraine`:
|
||||
sessionctl arbiter, first-party lock/nav (pill retired), SessionEvents
|
||||
logind ingress + delay inhibitor, StepUpAuth, break-glass, hash-chained
|
||||
audit. Freshly green — the last five commits of Jul 14 were runtime fixes;
|
||||
see `docs/DUMP-session-trust-2026-07-14.md`.
|
||||
- `souveraine-secrets`: org.freedesktop.secrets daemon behind the `secrets`
|
||||
feature, rooted in `SeedId` (no gnome-keyring/KWallet). Committed, not yet
|
||||
build-verified. Intended consumers: Chatty/libcmatrix via libsecret, and
|
||||
keyring-core.
|
||||
- Design docs for the shell live HERE in `docs/`, not in the repo tree
|
||||
(moved Jul 15; deletion committed `0f50a3e`).
|
||||
|
||||
### Pixel3Arch (`~/Projects/Pixel3Arch`)
|
||||
`main` @ `862202a`. Repo == phone for deployed config as of 2026-07-16.
|
||||
- Mic: hexagonrpcd now serves the ADSP audio PD (verified live — fluence/ECNS
|
||||
opens succeed at boot). Open blocker is the SLIM IRQ-read NACK loop
|
||||
(~1090 hits/boot); state in `PAF/audio.md`. Capture route moved to
|
||||
MultiMedia2.
|
||||
- MMS: works end-to-end via the `netsvcs` APN (`PAF/mms.md`). Open: mmsd-tng
|
||||
has session-autostart via hyprland.lua but no systemd unit; Chatty can't
|
||||
read the MMSC (daemon doesn't expose org.ofono.mms.ModemManager).
|
||||
- USB: usb-signaller 0.3.1 built + staged; the RUNNING phone still uses
|
||||
usb-gadget-net (cutover severs the SSH link — do it with console fallback
|
||||
or at reprovision). Old gadget archived in `archive/usb-gadget-net/`.
|
||||
- Untracked by design: `pkgs/pulseaudio-q6/` (working clone; delta preserved
|
||||
in `pkgs/pulseaudio-q6-overlay/`), `pkgs/*/pkg/` intermediates.
|
||||
- Undecided: two full kernel worktrees `kernel/linux-blueline-mic-order{,-g56}`
|
||||
(~GBs, mic experiments, each has STATUS.md) — push branches to Gitea or drop.
|
||||
|
||||
### Chatty (`~/Projects/Chatty`) + libcmatrix (`~/Projects/libcmatrix`)
|
||||
Chatty branch `identity-layer` @ `1b94cf9`; libcmatrix branch `sas-fixes`
|
||||
@ `0eb5f81`.
|
||||
- Built and live-verified on the phone: history v6 (persons tables),
|
||||
ChattyPerson + identity API, Known/Unknown re-keyed (Matrix rooms no
|
||||
longer auto-Known), E.164 normalize, four SAS verification fixes (emoji
|
||||
verify completes).
|
||||
- Design + build order: `PAF-chatty-identity-layer.md` in the Chatty repo.
|
||||
EDS was investigated and REJECTED (GNOME stack). Contacts backend is
|
||||
undecided pending qtpim work — see below.
|
||||
- Phone runs hand-installed chatty/libchatty/libcmatrix binaries over
|
||||
pacman-owned files (stock backups: `chatty.orig-pacman`,
|
||||
`libcmatrix.so.0u.orig-pacman`). PKGBUILDs owed.
|
||||
|
||||
### qtpim / contacts (`~/Projects/qtpim`, phone `~/Projects/qtpim`)
|
||||
Full state: `docs/qtpim-contacts-handoff.md`.
|
||||
- qtpim dev (`d03b5f6`) built and installed on the phone at /usr
|
||||
(6.12-versioned libs on Qt 6.11.1, version check overridden; untracked —
|
||||
PKGBUILD owed). QML `QtContacts` module loads.
|
||||
- Only the in-memory backend exists. qtcontacts-sqlite port stalled: it
|
||||
targets Sailfish's qtpim fork (`IgnoreAccessConstraints`). Three paths in
|
||||
the handoff; direction not chosen.
|
||||
|
||||
## Decisions on record
|
||||
- No GNOME/KDE library deps. EDS/libebook rejected for contacts.
|
||||
- qtpim (upstream dev branch) is the contacts library direction.
|
||||
- Fido MMS rides the `netsvcs` APN, patched into the carrier DB overlay.
|
||||
- ETHOS-style guarantees are not runtime knobs; identity is a precondition
|
||||
(SeedId::load never generates).
|
||||
- Contacts store under qtpim: undecided.
|
||||
|
||||
## docs/ map
|
||||
- `SESSION-AUTHORITY-DOCTRINE.md` — session/power/lock authority model (§5–8
|
||||
verified implemented on phone, 2026-07-15).
|
||||
- `SESSION-TRUST-ARCHITECTURE.md`, `TRUST-BOUNDARY-MATRIX.md`,
|
||||
`INTERFACE-ARCHITECTURE.md`, `HOW-IT-WORKS.md`, `REFERENCE-EXTRACTION.md`,
|
||||
`SETTINGS-APP-PLAN.md`, `README.md` — shell design set (moved from
|
||||
souveraine repo).
|
||||
- `DUMP-*.md` — per-session bootstrap dumps; write one per working session
|
||||
that changes cross-repo state.
|
||||
- `qtpim-contacts-handoff.md`, `session-trust-map.html` — moved from
|
||||
~/Projects and ~/ on 2026-07-16.
|
||||
168
docs/DUMP-power-idle-lock-2026-07-15.md
Normal file
168
docs/DUMP-power-idle-lock-2026-07-15.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Souveraine power/idle/lock — session dump (2026-07-15)
|
||||
|
||||
Scope: laptop + Pixel 3, both running `qs -c souveraine`. Not a design doc —
|
||||
next-session bootstrap so today's findings aren't re-derived from scratch.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Fixed three real bugs today (laptop lockscreen unclickable, phone stale
|
||||
suspend-key config, phone's redundant sleep lock path, a runaway
|
||||
`hypridle`→`polkitd` crash-loop). Confirmed the Android power-button/idle
|
||||
model this project is measuring itself against, and confirmed
|
||||
`SESSION-AUTHORITY-DOCTRINE.md` §5–8's design (logind delay-inhibitor +
|
||||
`ext-idle-notify-v1`) is **actually implemented and running** on the phone
|
||||
today, not just designed — verified live via `systemd-inhibit --list` (5
|
||||
inhibitors: ModemManager, NetworkManager, RealtimeKit, UPower, Souveraine
|
||||
Shell, all delay-mode). Two real open threads carried forward below.
|
||||
|
||||
---
|
||||
|
||||
## Fixed today
|
||||
|
||||
1. **Laptop right-sidebar unclickable** — root cause was `Config.qml`
|
||||
missing `sidebar.width`/`widthExtended` (dropped when Souveraine forked
|
||||
ii's `Config.qml`), collapsing the `PanelWindow` to 1px. Also restored
|
||||
`lock.dpmsTimeout`, `lock.unlockHook`, top-level `autoIdleInhibit`, and
|
||||
`Idle.qml`'s `autoIdleInhibit` property (ii-stock `IdleInhibitorToggle.qml`
|
||||
reads it; Souveraine's rewritten `Idle.qml` never defined it). All are the
|
||||
same failure class: phone-fork dropped a key ii-stock consumer code
|
||||
expects. A `source: null` in Souveraine's PulseAudio `Audio.qml` (correct
|
||||
for the Pixel 3's absent mic capture) was a related but separate crash in
|
||||
`QuickSliders.qml` — fixed with a real stub object, not a null-guard patch.
|
||||
|
||||
2. **Phone: stale `HandleSuspendKey=ignore`** in
|
||||
`rootfs-overlay/etc/systemd/logind.conf.d/50-phone-powerkey.conf` — its
|
||||
justification (DSI hold-rails DPMS hazard) is fixed per `ROADMAP.md`
|
||||
2026-07-14. Removed; `HandlePowerKey=ignore` kept (unrelated, still-valid:
|
||||
modem state doesn't survive poweroff). Deployed to device; needs
|
||||
`systemctl restart systemd-logind` (or reboot) to take effect — **not yet
|
||||
tested against a real suspend-key cycle.**
|
||||
|
||||
3. **Phone `hypridle.conf`** — repo copy was stale relative to the device;
|
||||
synced from live device (it already used the IPC lock call, no
|
||||
`before_sleep_cmd`, ahead of what either repo checkout showed).
|
||||
|
||||
4. **Laptop runaway `hypridle` → `polkitd` crash-loop (this evening).** This
|
||||
morning's `hypridle.conf` edit (stripped `|| hyprlock` fallback) was
|
||||
correct on disk, but the *running* `hypridle` process (started 08:24)
|
||||
never reloaded — hypridle reads its config once at launch. Something kept
|
||||
firing `lock_cmd` in a tight loop all day, so every trigger executed the
|
||||
**stale in-memory** `loginctl lock-session; pidof qs quickshell ||
|
||||
hyprlock` — dozens of processes/sec, each a real `loginctl` D-Bus round
|
||||
trip. This hammered `polkit_system_bus_name_get_user_sync`, crashing
|
||||
`polkitd` with SIGTRAP every ~30s for 2+ hours (634+ restarts) — same
|
||||
crash site/shape as CVE-2021-3560 (client disconnects from D-Bus mid
|
||||
identity-resolution), though that CVE is old and should be patched in
|
||||
polkit 127-3, so this may be an unpatched regression of the same class,
|
||||
not a reopened old bug. Load average hit 32+; dbus-broker/polkitd/qs were
|
||||
the CPU offenders reported. **Fix applied:** killed stale hypridle (988)
|
||||
+ all runaway children, relaunched clean (now on the fixed config, no
|
||||
lock triggered by the fix itself). Confirmed stable (polkitd held 50s+,
|
||||
load average dropping). **Not yet root-caused: what was actually firing
|
||||
`lock_cmd` repeatedly in the first place** — an idle-timeout misfire, or
|
||||
something calling it directly. Needs checking before calling this closed;
|
||||
could recur.
|
||||
|
||||
---
|
||||
|
||||
## Android reference model (for calibrating what's a real gap vs. already-designed)
|
||||
|
||||
Source-verified this session (AOSP docs, kernel.org wakeup-sources,
|
||||
`source.android.com/docs/core/power/platform_mgmt`):
|
||||
|
||||
- **Suspend is opportunistic, not commanded.** Kernel autosuspends whenever
|
||||
zero wakelocks/wakeup-sources are held (`/sys/power/autosleep`). Nothing
|
||||
decides "sleep now" — sleep is the default state; holding a
|
||||
`PARTIAL_WAKE_LOCK` is what postpones it.
|
||||
- **Radio/modem during suspend:** a RIL interrupt is itself a registered
|
||||
wakeup source — it wakes the CPU, the modem driver holds a wakelock long
|
||||
enough to hand off to userspace, then releases; kernel re-suspends within
|
||||
seconds. "Asleep" never means "unreachable," it means "reachable only by a
|
||||
small allowlisted set of interrupt sources."
|
||||
- **Doze** (two-stage: Light on screen-off, Full on screen-off + stationary)
|
||||
is a *policy* layered on top of suspend, not the suspend mechanism itself —
|
||||
throttles what apps may do, not whether the kernel can sleep.
|
||||
- **Pocket/proximity:** Android uses the proximity sensor to keep the screen
|
||||
*off* while obstructed and to suppress accidental touch input during a
|
||||
call or in-pocket — proximity-cleared is a *display* signal, never treated
|
||||
as an authentication/unlock signal on its own.
|
||||
|
||||
Mapping onto this stack: `logind.Inhibit()` (delay-mode, fd-held) is the
|
||||
wakelock equivalent; `ext-idle-notify-v1` is the PowerManagerService-idle
|
||||
equivalent. **Confirmed live and running** on the phone tonight — this is
|
||||
not a gap, it's implemented per `SESSION-AUTHORITY-DOCTRINE.md` §5–8.
|
||||
|
||||
---
|
||||
|
||||
## Open threads for next session
|
||||
|
||||
### 1. `InhibitDelayMaxSec` — collective 5s budget, unverified against a real cycle
|
||||
|
||||
`/etc/systemd/logind.conf` has it commented out (systemd default = 5s), and
|
||||
**five subsystems share that one window**, not 5s each: ModemManager,
|
||||
NetworkManager, RealtimeKit, UPower, Souveraine Shell (all confirmed live via
|
||||
`systemd-inhibit --list` on the phone tonight). If ModemManager's device
|
||||
reset genuinely needs more than its slice, systemd suspends anyway — the
|
||||
inhibitor is advisory-with-a-timeout, not a hard block. This is the leading,
|
||||
still-unverified hypothesis for "modem in a bad state after resume."
|
||||
|
||||
**Next step:** pull the journal across a real `PrepareForSleep(true)` →
|
||||
`(false)` pair on the phone and measure how long ModemManager's reset
|
||||
actually takes vs. the 5s budget. If it's close or over, that's the fix
|
||||
target (raise `InhibitDelayMaxSec`, or reduce what's sharing the window).
|
||||
|
||||
### 2. Pocket/proximity wake bypasses the lock screen — real, observed, security-relevant
|
||||
|
||||
Casey: phone pulled from pocket has woken on its own **and skipped the lock
|
||||
screen** — observed twice. This is not Android's model (proximity-cleared
|
||||
is a display-on signal there, never treated as unlock) — something on this
|
||||
stack is currently treating proximity-cleared (or the wake it triggers) as
|
||||
equivalent to authenticating. This is the exact failure mode
|
||||
`SESSION-AUTHORITY-DOCTRINE.md` §9 already names in the abstract ("sensor
|
||||
readings are evidence, not fact... an adversarial sensor drives that gate at
|
||||
a moment of its choosing") — it just hasn't been checked against what's
|
||||
actually wired on the phone today. Likely candidates to check first: does
|
||||
the lift-to-wake/proximity-out path in the lock surface call anything that
|
||||
actually *unlocks* (vs. just wakes the display), and is `screenLockSecure`
|
||||
being read correctly on that path or is something checking `screenLocked`
|
||||
(the request, not the compositor ack) per the doctrine's own §on this
|
||||
distinction.
|
||||
|
||||
**Next step:** trace the actual QML/service path proximity-clear takes on
|
||||
wake and confirm whether it's touching lock state at all, or whether
|
||||
something downstream is treating "display woke" as "session unlocked."
|
||||
|
||||
### 3. The physical power button itself — still not mapped end-to-end
|
||||
|
||||
We fixed the *logind* layer today (`HandleSuspendKey`/`HandlePowerKey` in
|
||||
`logind.conf.d`), but what the button actually drives compositor-side —
|
||||
screen-off vs. lock vs. suspend, and whether that's the same code path
|
||||
proximity-wake hits — hasn't been traced. This is the original question that
|
||||
opened tonight's investigation and is still open.
|
||||
|
||||
### 4. Why did `lock_cmd` fire in a tight loop on the laptop today?
|
||||
|
||||
Root cause of the crash-loop (stale hypridle config) is fixed, but *what was
|
||||
triggering repeated lock calls in the first place* is not identified. Could
|
||||
be an idle-timeout misconfiguration, could be something calling `loginctl
|
||||
lock-session` directly. Worth a quick check next session so it doesn't
|
||||
silently recur now that hypridle is back on a clean config.
|
||||
|
||||
---
|
||||
|
||||
## Also shipped today, unrelated to power/idle/lock
|
||||
|
||||
`souveraine-secrets` — a new `[[bin]]` in the souveraine crate: a Secret
|
||||
Service D-Bus daemon (`org.freedesktop.secrets`) rooted in the existing
|
||||
`SeedId` Ed25519 identity instead of gnome-keyring/KWallet, to fix Chatty's
|
||||
Matrix cross-signing key not persisting across restarts. Loads the existing
|
||||
seed only (`SeedId::load`, added as a pure addition to `seed.rs` — no
|
||||
generation path, refuses to start if no seed exists, per explicit ETHOS
|
||||
direction this session: identity creation stays deliberate/guarded, never
|
||||
silent). DH-1024/AES-128-CBC OpenSession transport implemented against the
|
||||
actual Secret Service spec + libsecret's `secret-session.c` (not memory).
|
||||
Compiles clean, unit tests pass (DH round-trip, AES round-trip, existing
|
||||
seed.rs tests untouched). **Not yet tested against a real D-Bus bus or
|
||||
Chatty** — that's the next step, on the phone.
|
||||
193
docs/DUMP-session-trust-2026-07-14.md
Normal file
193
docs/DUMP-session-trust-2026-07-14.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Souveraine `surfaces/quickshell` — session-trust work dump (2026-07-14)
|
||||
|
||||
Branch: `experimental/session-trust-phase1`
|
||||
Tip: `6532572` — all committed except `Audio.qml` (M) and `volumeMixer/` (untracked).
|
||||
Scope: the QuickShell shell that runs on **both the Pixel 3 (blueline) and the laptop**.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR — what landed today
|
||||
|
||||
Two things shipped in one day:
|
||||
|
||||
1. **Shell restructure — "retire the pill."** The separate pill nav shell is gone.
|
||||
The shell now runs as one config (`qs -c souveraine`), with an owned first-party
|
||||
lock/nav/session layer on top of the borrowed `ii` shell. Three guarded IPC
|
||||
surfaces for the agent (`dock.*`, `shell.*`, `apps.*`), and a real IPC bug fixed
|
||||
(returns were silently dropping their payload — see below).
|
||||
|
||||
2. **Session Trust Phase 1** — the security model. Step-up auth, lock-time response
|
||||
redaction, break-glass override, sleep-before-lock coordination, and a
|
||||
hash-chained audit trail. This is phase 1 of the SouveraineOS homogenization
|
||||
(one shell, laptop + phone).
|
||||
|
||||
**Caveat for whoever this goes to:** the feature stabilized *right up to the last
|
||||
commit*. Five consecutive QML-runtime fixes close the day, and the very last one
|
||||
(`6532572`, 22:26) fixed a `ReferenceError` that was **crashing the lock-redaction
|
||||
path itself**. So: freshly-green, on an `experimental/` branch, not yet
|
||||
battle-tested. Verification checklist at the bottom is not all ticked.
|
||||
|
||||
---
|
||||
|
||||
## The core security model (from SESSION-TRUST-ARCHITECTURE.md + TRUST-BOUNDARY-MATRIX.md)
|
||||
|
||||
### Authorities we consume, never replace
|
||||
| Question | Authority | Our role |
|
||||
|---|---|---|
|
||||
| Can this machine suspend/hibernate/poweroff/reboot? | logind `Can*` | query + expose + execute matching verb |
|
||||
| Has the compositor secured the display? | `WlSessionLock.secure` | mirror as `screenLockSecure`; never infer from a button |
|
||||
| Does the shell want its lock shown? | `GlobalStates.screenLocked` | maintain request state, persist across crash |
|
||||
| Has the user authenticated? | PAM + lock's `LockContext` | start PAM; only it releases `WlSessionLock` |
|
||||
| Why is idle sleep suppressed? | active idle mechanism | reason/cookie registry; report only applied mechanisms |
|
||||
|
||||
**The central invariant:** `screenLocked` (shell's *request*) and `screenLockSecure`
|
||||
(compositor's *acknowledgement*) are kept distinct. A failed/delayed Wayland lock
|
||||
shows as `lockRequested: true, locked: false` — never mistaken for success.
|
||||
Personal-data disclosure gates on `screenLockSecure`, not on a button press.
|
||||
|
||||
### Capability tiers (one policy system for lock screen, media, and agent)
|
||||
| Tier | Requirement | Examples |
|
||||
|---|---|---|
|
||||
| `ambient` | none | clock, weather, timers, transport controls, non-personal answers |
|
||||
| `personal` | `!screenLocked && !screenLockSecure` (live check, not cached) | messages, calendar, memories, fleet state, history |
|
||||
| `stepUp` | recent successful reauth (TTL window) | sending, pushes, deletion, payments, physical access |
|
||||
|
||||
On lock, in-flight personal/step-up agent output is withheld/redacted; only ambient remains.
|
||||
|
||||
### Trust boundary matrix — enforcement status
|
||||
- **enforced:** lock/unlock, suspend/hibernate/poweroff/reboot (capability-probed),
|
||||
idle+sleep inhibitors (reason required), all read-only projections, step-up PAM
|
||||
conversation, personal-context send gate (`LockContentPolicy`), in-flight response
|
||||
redaction on lock, media controls/metadata on lock, sleep transition
|
||||
(delay inhibitor + `WlSessionLock.secure`), audit trail.
|
||||
- **`session.unlock()` is refused over IPC by design** — the lock is the credential
|
||||
gate; no IPC caller routes around the PIN pad.
|
||||
- **gaps (not yet gated):** agent delete/push and physical-access ops —
|
||||
`StepUpAuth` is built but the per-op `minTier` metadata isn't wired yet.
|
||||
- Matrix is **local-only**: all callers are same-machine over the quickshell IPC
|
||||
socket. If a network-reachable caller is ever added, every row needs re-evaluation.
|
||||
|
||||
---
|
||||
|
||||
## Commit-by-commit (chronological, full messages)
|
||||
|
||||
### `c78c551` 15:03 — quickshell: run as qs -c souveraine + dock/shell/apps method surfaces
|
||||
Own config name composed by deploy.sh from our files + pristine-ii dir borrows; ii
|
||||
tree stays untouched. One SouveraineFamily, two modes gated on `souveraine.phone`.
|
||||
Three guarded IPC surfaces for the agent (via Souveraine's harness):
|
||||
`dock.manifest/pin/unpin/restack`, `shell.surfaces/state`, `apps.list/get/find/categories`.
|
||||
Refusals are result shapes, not errors. Settings split into per-domain pages.
|
||||
|
||||
### `cecde3b` 18:31 — quickshell: session arbiter + fix IPC returns silently dropping payloads
|
||||
**The IPC bug is the important half.** Quickshell marshals exactly five types
|
||||
(string/int/bool/double/color) and maps a `var` return to VOID, discarding the
|
||||
payload with no error. `dock.*`, `shell.*`, `apps.*` were all `: var` → registered
|
||||
as `(): void` and returned nothing. The `{ok, reason}` contract had **never once
|
||||
reached a caller.** All now return JSON as a string.
|
||||
Session: upstream fires `systemctl X || loginctl X` detached and throws the exit
|
||||
code away — fine on a desktop, not on a phone where the shell *is* the session
|
||||
manager. Now: probe once, run verbs through a Process that logs exit codes, refuse
|
||||
honestly (swapless phone → hibernate says so instead of no-opping). Inhibits carry a
|
||||
mandatory reason + cookie; `state()` lists who holds the machine awake and why.
|
||||
`unlock()` refused by design. Named `sessionctl` (ii owns `session`).
|
||||
|
||||
### `e31c3aa` 20:00 — quickshell: first-party lock/nav/session layer, retire the pill
|
||||
Session arbiter probes logind `Can*` over **busctl** (carries polkit tier
|
||||
yes/challenge/na) — swapless phone reports hibernate `na` and refuses. busctl parsed
|
||||
with **awk, not a sed regex** (the sed version returned nothing on the phone, left
|
||||
every capability "unknown" — invisible on laptop where timing masked it).
|
||||
Lock trust: `screenLocked` distinct from `screenLockSecure`; cards gate on secure.
|
||||
`LockContentPolicy` centralizes ambient/personal/step-up tiers. New first-party
|
||||
`modules/souveraine/`: LockMediaCard, LockSurfaceHost, SystemGestureRail. Pill
|
||||
retired: `pill/shell.qml` + PillConfig gone → NavigationConfig + gesture rail.
|
||||
Hyprland starts `qs -c souveraine` directly.
|
||||
**Verified on phone:** session.* reports challenge/na, hibernate+unlock refuse,
|
||||
inhibit round-trips with its reason.
|
||||
|
||||
### `4d5690b` 20:17 — quickshell: Idle & sleep settings page
|
||||
Live idle-stage readout (polled over session IPC), native-coordinator toggle,
|
||||
dim/lock timers (greyed while coordinator off). New `Audio.qml` service.
|
||||
|
||||
### `a8fd510` 20:48 — session trust phase 1: logind ingress, sleep inhibitor, step-up auth
|
||||
The three highest-priority gaps. New: `SessionEvents.qml` (logind PrepareForSleep +
|
||||
session Lock via gdbus monitor; delay-mode sleep inhibitor held from startup,
|
||||
released only after `WlSessionLock.secure`), `StepUpAuth.qml` (PAM step-up, short-
|
||||
lived in-memory grants per action family), `TRUST-BOUNDARY-MATRIX.md`. Modified:
|
||||
IdleCoordinator (+Suspending/Asleep/Waking), Session.qml (sleep inhibitor kind,
|
||||
stepUp/sleepInhibitor in IPC projection, boot-time IPC audit), Config (lock.stepUp),
|
||||
LockConfig, deploy.sh.
|
||||
|
||||
### `61bdc53` 21:13 — session trust: step-up send gating + lock-time response redaction
|
||||
`Souveraine.send()` returns `'step-up'` when auth required; `Ai.sendUserMessage()`
|
||||
triggers `StepUpAuth.requestAuth('send')`, retries on success. Lock-time redaction:
|
||||
`Ai.qml` watches `screenLocked`; on lock mid-stream, replaces displayed content with
|
||||
`[content hidden until unlock]`, preserves `rawContent` for post-unlock.
|
||||
|
||||
### `377b829` 21:24 — break-glass: one-time emergency grant with journaling
|
||||
`StepUpAuth.breakGlass(family, reason)`: mandatory reason (logged prominently),
|
||||
60s TTL (not 5min), one-time (consumed on use), can't be issued while locked,
|
||||
cleared on lock (fail-closed), tracked separately for audit. `send()` checks
|
||||
break-glass before blocking on step-up; it bypasses PAM — user declared an emergency.
|
||||
|
||||
### `ae9ef41` 21:31 — hash-chained audit trail for session transitions
|
||||
`SessionAudit.qml`: tamper-evident JSONL of all state changes (lock/unlock,
|
||||
secure/insecure, idle transitions, PrepareForSleep, inhibitor lifecycle, step-up
|
||||
success/failure, break-glass issued/consumed/expired, grant expiry/revocation).
|
||||
Each entry carries md5 of the previous; sequence numbers detect gaps; chain validated
|
||||
on startup. Uses `Qt.md5()` (no SHA256 in QML) — **advisory tamper-evidence, not
|
||||
crypto.** Log: `~/.local/share/souveraine/session-audit.jsonl`.
|
||||
|
||||
### `ffe4157` 21:39 — settings: show sleep inhibitor state, update stage names
|
||||
IdleConfig shows full stage names (incl. Suspending/Asleep/Waking), sleep inhibitor
|
||||
held/released, stepUp grant TTL. **hypridle.conf changes are NOT in repo** (apply to
|
||||
`~/.config/hypr/`): lock_cmd → `loginctl lock-session`; before_sleep_cmd REMOVED;
|
||||
inhibit_sleep REMOVED; suspend_cmd prefers loginctl (SessionEvents owns these now).
|
||||
|
||||
### The closing five fixes (21:58 → 22:26) — it didn't run clean
|
||||
- `10c206c` `Component.onCompleted` doesn't fire on QML Singletons → `Timer{interval:0}` in Session/SessionAudit/SessionEvents
|
||||
- `bc08149` missing `pragma Singleton` on SessionAudit
|
||||
- `e17f02b` missing `QtQuick` import (Timer) in Session
|
||||
- `13cded0` Timer unavailable in a functions module → use Process
|
||||
- `6532572` **Ai.qml missing `import qs` — GlobalStates ReferenceError was crashing lock redaction** (the security-critical path was down until this)
|
||||
|
||||
---
|
||||
|
||||
## Key mechanisms (how the important bits actually work)
|
||||
|
||||
**Sleep-before-lock (SessionEvents.qml):** one `systemd-inhibit --what=sleep
|
||||
--mode=delay sleep infinity` held from startup. On `PrepareForSleep(true)`: if
|
||||
already `screenLockSecure`, release immediately; else `Session.lock()` and wait for
|
||||
`WlSessionLock.secure`, then release. Reacquire on `PrepareForSleep(false)`. Fail
|
||||
policy: **fail-closed** — if not secure before logind's `InhibitDelayMaxUSec`
|
||||
(~5s) deadline, leave a diagnostic and don't pretend it locked. Lock persists across
|
||||
suspend/resume; only the human PIN pad unlocks. External `loginctl lock-session`
|
||||
caught via a per-session-path gdbus monitor (echo-guarded against our own lock).
|
||||
|
||||
**Step-up (StepUpAuth.qml):** action families send/delete/payment/physical/admin.
|
||||
Grant = `{granted, expires}`, 5min default TTL (`Config.lock.stepUp.grantTtlMs`).
|
||||
One PAM conversation at a time. Backend: prefers `souveraine-pam-auth` helper
|
||||
(password via env `SOUVERAINE_STEPUP_PASSWORD`, not argv), falls back to `pkcheck`
|
||||
(polkit) at runtime. **The `/etc/pam.d/souveraine-stepup` file is NOT shipped** —
|
||||
root-owned, install/audit separately. Grants cleared on lock, session end, PAM
|
||||
failure, expiry (30s coarse timer). Re-request within window does NOT refresh clock.
|
||||
|
||||
**Audit chain (SessionAudit.qml):** append-only JSONL, `md5(prev)` chained, seq
|
||||
numbers for gap detection, validated on startup. Explicitly advisory, not crypto —
|
||||
production should swap for sha256sum/dedicated binary.
|
||||
|
||||
---
|
||||
|
||||
## Verification status (from SESSION-TRUST-ARCHITECTURE.md)
|
||||
- [x] `session.capabilities()` matches logind `Can*` — verified phone
|
||||
- [ ] `session.lock()` reaches `locked:true` only after `WlSessionLock.secure` — needs timing on Pixel
|
||||
- [ ] `loginctl lock-session` and suspend take same Wayland lock path
|
||||
- [x] step-up grants can't survive lock/timeout/restart/family-change (StepUpAuth revokes on lock, 30s expiry)
|
||||
- [ ] lock-during-personal-output hides it; lock-during-ambient-timer leaves timer — **redaction path only stopped crashing at last commit; re-verify**
|
||||
- [ ] **delay budget (~5s) timed on Pixel 3** — required before this is a security claim, not yet done
|
||||
|
||||
## Open items for next session
|
||||
- Wire per-op `minTier` metadata so agent delete/push/physical actually hit stepUp (built, not connected)
|
||||
- Time the suspend delay budget on the Pixel 3
|
||||
- Ship/audit `/etc/pam.d/souveraine-stepup`; confirm one polkit agent owns the session
|
||||
- Verify native Wayland idle-notify on the Pixel compositor before flipping `nativeCoordinatorEnabled`
|
||||
- Uncommitted: `Audio.qml`, untracked `volumeMixer/`
|
||||
55
docs/HOW-IT-WORKS.md
Normal file
55
docs/HOW-IT-WORKS.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# How the phone shell works
|
||||
|
||||
Three surfaces: **bar** (top), **dock** (bottom, above the navigation rail),
|
||||
and Souveraine's integrated **navigation rail** (bottom edge).
|
||||
Edit here → `./deploy.sh --phone` → restart. Deployed via symlink; live edits = git-tracked.
|
||||
|
||||
## Navigation rail (`modules/souveraine/navigation/SystemGestureRail.qml`)
|
||||
|
||||
- **Always visible.** `WlrLayer.Overlay` + `ExclusionMode.Ignore` + `margins.bottom:0`. Survives fullscreen, dock, OSK. Never lower the layer.
|
||||
- **Owned by Souveraine.** It is loaded by `SouveraineFamily` on phone mode;
|
||||
there is no second Quickshell configuration, external IPC hop, or Hyprland
|
||||
layer rule to keep in sync.
|
||||
- Gestures (MouseArea — pointer handlers don't get touch here):
|
||||
- **double-tap** → toggles the active app's named `fullscreen` mode (whole
|
||||
display, no border/gaps). Targets `Hyprland.activeToplevel.address` — not
|
||||
hyprctl's focus (the tap focuses the shell).
|
||||
- **swipe up** → reveals the dock.
|
||||
- **swipe down** → dismisses the dock.
|
||||
- **No keyboard on the rail.** OSK = 3-finger hyprgrass swipe only.
|
||||
|
||||
## Dock (`modules/ii/dock/Dock.qml`)
|
||||
|
||||
- **On `WlrLayer.Overlay`** so the navigation rail can reveal it over fullscreen apps.
|
||||
- **Hidden = layer unmounted** (`visible:false`), not just tucked — else it paints over fullscreen.
|
||||
- **Visibility** (`computeDockState`, first match wins):
|
||||
1. fullscreen app on focused monitor → **Hidden** (unless rail-revealed)
|
||||
2. pinned → **Pinned** (only state that reserves exclusive zone)
|
||||
3. preview-hover → **Shown**
|
||||
4. empty desktop / no focused app → **Shown**
|
||||
5. else (normal app focused) → **Hidden**
|
||||
- **Reserves the navigation rail height** at the bottom visually *and in its
|
||||
layer-shell input mask*, so the dock cannot intercept rail touches.
|
||||
- **Bar height is content-driven**: the window sizes itself to the button row (64px buttons) + row margin + rail height; `Config.options.dock.height` is only a floor. Don't tune the config height to "fix" icon clipping.
|
||||
- **App list width is capped** to the screen (`DockApps.maxWidth`); past that the icon row scrolls horizontally by touch (flick is only enabled when overflowing, so drag-to-combine keeps working when everything fits).
|
||||
- **DockStack renders with the same content block as DockAppButton** (icon + half-reserved dot strip, centered as one unit). Keep them structurally identical or they drift apart on the bar.
|
||||
- Depends on **`GlobalStates.dockRevealed`** — lives in the surface tree (`GlobalStates.qml`, stock ii + patch) and the deploy manifest. If ii updates its GlobalStates, re-diff and re-apply the patch.
|
||||
|
||||
## souveraine-settings (planned, not built)
|
||||
|
||||
A future settings app for the phone shell. Planned sections:
|
||||
|
||||
- **Dock** — pinned apps, height floor, monochrome icons, ignored-app regexes (today: hand-edited in `~/.config/illogical-impulse/config.json`).
|
||||
- **Stacks** — create/rename/reorder app stacks and their members (today: `dock.stacks` strings in config.json, or drag-to-combine on the dock).
|
||||
|
||||
The dock already reserves its entry points: the long-press menu's "App settings…"
|
||||
and the `dockSettings` IPC target (`openApp(appId)` / `open()`) in `Dock.qml`.
|
||||
Both are STUBS — they log and pulse the dock, nothing opens. That's deliberate:
|
||||
the IPC name stays stable so the settings app can take it over without touching
|
||||
the dock.
|
||||
|
||||
## Fullscreen API (Hyprland 0.55, Lua dispatch)
|
||||
|
||||
- Dispatch: `hl.dsp.window.fullscreen({ window="address:0x…", mode="fullscreen", action="toggle" })`.
|
||||
- **`fullscreen`** = whole display, no borders or gaps. ← the navigation rail uses this.
|
||||
- **`maximized`** = keeps the normal workspace layout margins.
|
||||
182
docs/INTERFACE-ARCHITECTURE.md
Normal file
182
docs/INTERFACE-ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# Phone shell — interface architecture
|
||||
|
||||
The phone has four primary interfaces the user actually touches: the **pill**,
|
||||
the **dock**, the **keyboard**, and **touch/gesture**. This doc names what each
|
||||
one is *meant to be*, what it is *today*, and the single seam where a hardcode
|
||||
should become a small framework so the surface can grow without rewrites.
|
||||
|
||||
Principle: **clean beats feature-rich.** Each surface has one job and one place
|
||||
its behaviour is tuned. A new feature should be a new entry in an existing
|
||||
table, not a new hardcoded branch. Where a value is tuned in more than one
|
||||
place today, that is the bug — not the value.
|
||||
|
||||
Truth hierarchy: this doc describes intent; `HOW-IT-WORKS.md` describes the
|
||||
current wiring; the code is the reality. When they disagree, fix the code or
|
||||
fix the doc in the same change — never leave a third story.
|
||||
|
||||
---
|
||||
|
||||
## 1. Pill — the always-on gesture bar
|
||||
|
||||
**Meant to be:** the one surface that is *always* reachable, on any app, in any
|
||||
state. A thin, dumb input strip at the bottom edge that recognises a small
|
||||
fixed vocabulary of gestures and forwards them — it owns no app state and makes
|
||||
no decisions beyond "which gesture was this."
|
||||
|
||||
**Today (`pill/shell.qml`):** `WlrLayer.Overlay`, `ExclusionMode.Ignore`,
|
||||
`margins.bottom:0`. Gesture arbitration is one MouseArea (double-tap, swipe up,
|
||||
swipe down) → `dock` IPC. This part is clean and well-reasoned.
|
||||
|
||||
**The seam / the bug:** the pill's z-order guarantee is *documented but not
|
||||
enforced*. `pill/shell.qml:23-27` sets namespace `quickshell:pill` and points
|
||||
at "the `order` layerrule in hyprland.lua" — **that layerrule does not exist**
|
||||
(hyprland.lua only has a commented-out generic `^my-overlay$` example). So
|
||||
after an `ii` restart the pill can be buried under the dock and stop taking
|
||||
touch — the exact failure the comment says is solved. Fix: add the real
|
||||
`order` layerrule for `quickshell:pill`, or drop the promise from the code and
|
||||
docs. Do not add more pill gestures until stacking is enforced — an always-on
|
||||
surface that silently dies is worse than a plain one.
|
||||
|
||||
**Growth rule:** new pill gestures are new cases in the one MouseArea, each
|
||||
forwarding to a *named* IPC target — never doing app logic inline.
|
||||
|
||||
---
|
||||
|
||||
## 2. Dock — the app switcher and stack surface
|
||||
|
||||
**Meant to be:** the surface that shows and switches running/pinned apps and
|
||||
lets the user group them into stacks. It owns app-presentation state; it does
|
||||
not own gesture recognition (the pill and hyprgrass feed it via IPC).
|
||||
|
||||
**Today:** interaction layer (`Dock.qml`, `DockAppButton.qml`, `DockStack.qml`)
|
||||
is strong — drag-to-combine, fan-out arc, visibility state machine
|
||||
(`computeDockState`). The weak layer is **state/config**:
|
||||
|
||||
- Stacks are a hand-parsed string `"stackId|appId,appId"` (`TaskbarApps.qml:25`).
|
||||
A `|` or `,` in a name corrupts it silently; parsed/encoded in ~6 places.
|
||||
→ move to **one JSON blob per entry** (array-of-strings the adapter tolerates,
|
||||
but each string is JSON): kills the delimiter-corruption class, keeps the
|
||||
adapter happy. This is the agreed direction.
|
||||
- Stack ids are count-based (`nextStackName` = `"Stack " + (count+1)`,
|
||||
`:87`) → collide after a delete, and `id` is the lookup key. Two stacks can
|
||||
share an id and cross-contaminate members. → stable uid, display name
|
||||
separate from id.
|
||||
- Combining into a new stack suppresses the target's standalone pin at *render*
|
||||
time (`apps` getter) but never removes it from `pinnedApps` in config
|
||||
(`:99-108`) → the pin lingers; unstack works by luck. → remove-on-combine so
|
||||
config matches what's shown.
|
||||
- Tap-cycle `lastFocused` index (`DockAppButton :196`) is never reconciled when
|
||||
windows open/close and is also written by hover (`:180`) → tapping can focus
|
||||
the wrong window. → clamp/reconcile against the live toplevel model.
|
||||
- Magic numbers live in the components: `pillStripHeight 32` (`Dock.qml:161`),
|
||||
dwell `500` (twice: `DockAppButton:133`, `DockStack:116`), double-tap `350`,
|
||||
arc geometry (`DockStack:33-36`). → lift into `Config.options.dock` next to
|
||||
the `height`/`monochromeIcons`/`hoverRegionHeight` that already live there.
|
||||
- `reorderStackMember` (`TaskbarApps:113`) is dead — superseded by
|
||||
`setStackOrder`. → remove, or wire the menu reorder it was meant for.
|
||||
|
||||
**The seam:** `Config.options.dock` already exists — the framework anchor is
|
||||
already there. "More structure" here means *finishing* that namespace (all
|
||||
tuning knobs + a real stack schema under it), not inventing a new one.
|
||||
|
||||
**Growth rule:** dock feel/config knobs go under `Config.options.dock`; stack
|
||||
members are structured entries, not delimited strings. Rename + ungroup are
|
||||
table-stakes and should exist before any fancier stack feature.
|
||||
|
||||
---
|
||||
|
||||
## 3. Keyboard (OSK) — squeekboard
|
||||
|
||||
**Meant to be:** an on-screen keyboard that appears on text focus and can be
|
||||
summoned by one deliberate gesture, driven through **one** IPC target so its
|
||||
visibility and any focus-grab shield always move together.
|
||||
|
||||
**Today:** squeekboard, launched from `hyprland.lua`, auto-shows via
|
||||
input-method-v2, toggled by 3-finger swipe-up routed through ii's `osk` IPC.
|
||||
The *routing* is right (single `osk` target).
|
||||
|
||||
**The seam / the drift:** the comments and the retained
|
||||
`quickshell-ii-patches/OnScreenKeyboard.qml` still describe **wvkbd**
|
||||
(retired 2026-07-10) and its focus-grab shield — the `osk` IPC now drives
|
||||
squeekboard, but the code comments and the history-only patch file still tell
|
||||
the wvkbd story. Same doc-vs-reality drift as the pill layerrule. → update the
|
||||
comments to squeekboard reality; confirm whether `OnScreenKeyboard.qml`'s
|
||||
GlobalStates.oskOpen plumbing is still the thing `osk toggle` hits, or vestigial.
|
||||
|
||||
**Growth rule:** one `osk` IPC target owns show/hide; layout/theme is
|
||||
squeekboard config, addressable later from souveraine-settings. No second path
|
||||
to toggle the keyboard.
|
||||
|
||||
---
|
||||
|
||||
## 4. Touch / gesture — the input spine
|
||||
|
||||
**Meant to be:** one routing layer that maps a physical touch gesture to a
|
||||
named interface action. Today this is the messiest surface because it is spread
|
||||
across four dispatch styles.
|
||||
|
||||
**Today (`hyprland.lua`):**
|
||||
- `hl.gesture{fingers=3,horizontal}` → workspace (Hyprland-native)
|
||||
- `hl.plugin.hyprgrass.bind{...}` → close / terminal / edge-swipes →
|
||||
`qs -c ii ipc call <target>`
|
||||
- the pill's own MouseArea → `dock` IPC
|
||||
- `touchdevice.output = "DSI-1"` binds touch to the panel for rotation
|
||||
|
||||
Two input sources (pill swipe-up, hyprgrass edge-swipe-up) both drive the dock;
|
||||
no single arbiter. Gesture → action bindings are inline `exec_cmd` strings
|
||||
scattered through the file.
|
||||
|
||||
**The seam:** this is the memory note "don't hardcode bindings blindly; need a
|
||||
routing system to avoid overlap." The clean form is **one gesture table** —
|
||||
edge/finger/direction → named IPC target — read top-to-bottom so overlaps are
|
||||
visible in one place. Every binding already ends in a `qs ... ipc call <target>`;
|
||||
the framework is just making that table explicit and single-sourced instead of
|
||||
sprinkled through `hyprland.lua`.
|
||||
|
||||
**Growth rule:** a new gesture is a new row in the gesture table pointing at a
|
||||
named IPC target. Two rows may not target the same action from different edges
|
||||
without being adjacent in the table (so overlap is obvious).
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting: the one recurring defect
|
||||
|
||||
Three of four surfaces have the **same** flaw — *a documented guarantee the
|
||||
code doesn't enforce*: the pill layerrule (doc says pinned, code doesn't pin),
|
||||
the keyboard (comments say wvkbd, code runs squeekboard), the dock stack pin
|
||||
(render says suppressed, config says still pinned). Cleaning these up is mostly
|
||||
making the code match its own stated contract, then giving each surface exactly
|
||||
one place it's tuned.
|
||||
|
||||
## Status (2026-07-13)
|
||||
|
||||
1. ~~Pill layerrule~~ DONE — `pill-above-dock` rule added to hyprland.lua
|
||||
(`order = 1` on `^quickshell:pill$`). VERIFY ON GLASS: `order`
|
||||
passthrough in the Lua layer_rule wrapper is untested on this build.
|
||||
2. ~~Gesture table~~ DONE — hyprgrass binds are one `touch_gestures` table
|
||||
in hyprland.lua; the two other gesture sources (hl.gesture workspace
|
||||
swipe, the pill's MouseArea) are listed in its header so overlap stays
|
||||
visible in one read.
|
||||
3. ~~Dock state layer~~ DONE — JSON-per-entry stacks (legacy pipe entries
|
||||
still parse, rewritten on next write), minted `stack-N` ids that never
|
||||
reuse after delete, name separate from id (+ `renameStack()`, no UI
|
||||
yet), pin actually removed from config on combine, tap-cycle starts
|
||||
from the truly focused window, dead `reorderStackMember` removed,
|
||||
`dragDwellMs`/`pillStripHeight` lifted into `Config.options.dock`.
|
||||
4. ~~Keyboard~~ DONE — hyprland.lua comments tell the squeekboard story;
|
||||
`OnScreenKeyboard.qml` (the "osk" IPC owner) moved into this surface +
|
||||
deploy manifest. Remaining hand-deployed patches (BarContent, Network,
|
||||
QuickSliders, switchwall.sh) noted in overlays/QUICKSHELL-MOVED.md.
|
||||
|
||||
## Open decision — two-stage swipe-up was removed, not lost by accident?
|
||||
|
||||
Souveraine commit `6e21c50` ("resume agent conversations from server")
|
||||
reworked the dock state machine: the Peek state, `dockRevealPulse`, and the
|
||||
two-stage swipeUp (reveal → escalate to overview) all went away in favor of
|
||||
a flat reveal/hide contract — under a commit title that says nothing about
|
||||
the dock. Pixel3Arch CLAUDE.md still records two-stage as SHIPPED
|
||||
(2026-07-11). The replacement comment is articulate, so this reads as a
|
||||
deliberate redesign, but it needs Casey's confirmation: keep the flat
|
||||
contract (overview = the dock's apps button), or restore the escalation.
|
||||
Comments in hyprland.lua/HOW-IT-WORKS now describe the flat behavior that
|
||||
actually runs.
|
||||
98
docs/README.md
Normal file
98
docs/README.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Souveraine quickshell surface
|
||||
|
||||
The desktop shell as a Souveraine surface — the primary visual frontend for
|
||||
SouveraineOS, with the TUI remaining the dive-in instrument.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `services/Souveraine.qml` — the substrate singleton. The ONE server
|
||||
connection every shell module hangs off: agent inventory, conversation
|
||||
lifecycle, the SSE turn stream (raw events re-emitted via
|
||||
`streamEvent(var)`), the backchannel (`cancelTurn()` / `interject(text)`),
|
||||
and the desktop sensorium — every send carries ambient context (active
|
||||
window, open apps, cursor position) so she perceives the room she is being
|
||||
spoken to in. Device sensors (SouveraineOS positional data from the Pixel
|
||||
3 kernel path) extend `collectAmbient()`.
|
||||
- `services/Ai.qml` — ii-compat adapter. Keeps the API the illogical-impulse
|
||||
sidebar expects; owns no transport. Shapes wire events into the message
|
||||
objects the existing chat UI renders.
|
||||
- `modules/` (coming) — presence (portrait PNGs from memfs, posture state
|
||||
machine), cockpit (subconscious pane), agents (master–detail manager),
|
||||
settings, schedules. Each subscribes to the Souveraine singleton.
|
||||
|
||||
## What changes
|
||||
|
||||
- "Models" in the sidebar are **Souveraine agents** (`GET /v1/agents`).
|
||||
Picking one starts a conversation with that agent — memory, sensors,
|
||||
subconscious and all.
|
||||
- Messages stream over the server's SSE endpoint
|
||||
(`POST /v1/conversations/:id/messages`), authenticated with the agent's
|
||||
bearer token from `~/.souveraine/server/agents/<id>/api_token`.
|
||||
- Subconscious **surfacings**, **reflection**, and **archivist** pressure
|
||||
render in the chat as interface notes (dedicated widgets later).
|
||||
- Reasoning and sensor activity render inside collapsible `<think>` blocks.
|
||||
- Keys/providers/temperature are owned by `souveraine.toml` — the sidebar's
|
||||
`/key` and `/temp` commands now just point there. The keyring path is dead.
|
||||
- Token pressure is fetched after each turn from
|
||||
`GET /v1/conversations/:id/tokens`.
|
||||
|
||||
## Portability (KDE / non-Hyprland)
|
||||
|
||||
`Souveraine.qml` itself is compositor-agnostic: quickshell runs on any
|
||||
wlroots-ish Wayland compositor and KWin; window sensing uses the
|
||||
foreign-toplevel protocol (KWin implements it); the cursor read tries
|
||||
`hyprctl`, then `kdotool`, then degrades to nothing — ambient never blocks
|
||||
a send. Server autostart is desktop-neutral (systemd user unit, nohup
|
||||
fallback), so opening any surface summons her.
|
||||
|
||||
What is NOT portable yet is the chrome: the chat UI is illogical-impulse's
|
||||
sidebar. The path for "I run KDE, can I use this?" is a standalone
|
||||
quickshell config (own ShellRoot + a window hosting the chat/presence
|
||||
modules) that ships `Souveraine.qml` unchanged — planned once the modules
|
||||
stop being ii-embedded. Same service, same mappings, different shell.
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
./deploy.sh # backs up upstream Ai.qml, symlinks ours in
|
||||
./deploy.sh -u # restore upstream
|
||||
```
|
||||
|
||||
Requires the server: `souveraine server` (default http://127.0.0.1:8484,
|
||||
override with `ai.souveraineUrl` in the ii config).
|
||||
|
||||
## Wire contract
|
||||
|
||||
The server's SSE layer is a full mirror of `BackendEvent` (see
|
||||
`src/api/models.rs::StreamEvent` — exhaustive `From` impls both ways, so a
|
||||
new engine event is a compile error at the seam, not a silent skip). The
|
||||
surface consumes the personification channel: subconscious tokens buffer and
|
||||
flush as one bubble when the N+1 pass ends (`subconscious_pass`), halts land
|
||||
as body signals, interstitials render by register (cenno = quiet aside,
|
||||
her_voice = gutter passage), `primary_complete` releases the input while the
|
||||
stream stays open for the subconscious, and `context_pressure` drives the
|
||||
live token counter. `atmosphere`/`outfit`/`itinerary` are logged, awaiting
|
||||
their shell-chrome layer.
|
||||
|
||||
Server-side, the backchannel and verbs exist for every surface:
|
||||
`POST /v1/conversations/:id/cancel` (interrupt, `*[raised hand]*`
|
||||
semantics), `.../interject` (mid-turn notes, queued between turns),
|
||||
`GET .../messages` (transcript backfill), `POST .../fork` (`/btw`
|
||||
side-quests). `SendMessageRequest.ambient` injects the sensorium note.
|
||||
RemoteBackend rides all of it, so TUI remote mode gained cancel/interject/
|
||||
fork/resume in the same stroke.
|
||||
|
||||
## Not yet wired
|
||||
|
||||
- Sidebar UI hooks for cancel (Esc) and interject (type-while-busy) — the
|
||||
service functions exist, the ii chat input doesn't call them yet
|
||||
- Conversation resume in the sidebar (server verb exists; surface always
|
||||
starts fresh)
|
||||
- Atmosphere/outfit/itinerary driving actual shell chrome (events arrive;
|
||||
modules pending)
|
||||
- File/image attachments (server has an image path; surface doesn't use it yet)
|
||||
- Regenerate (Souveraine conversations are forward-only by doctrine)
|
||||
- "Blank LLM mode" — a memoryless passthrough agent for throwaway questions;
|
||||
needs a server-side agent flavor first
|
||||
- Dedicated widgets for surfacing/subconscious bubbles instead of interface
|
||||
notes
|
||||
176
docs/REFERENCE-EXTRACTION.md
Normal file
176
docs/REFERENCE-EXTRACTION.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# Lock, idle, and wallpaper reference extraction
|
||||
|
||||
This is a design extraction, not a dependency list. Souveraine should retain
|
||||
one session authority across desktop, laptop, and phone; form factor changes
|
||||
the chosen layout and cards, never the meaning of lock, idle, or auth.
|
||||
|
||||
Migration rule: ii is a temporary compatibility substrate, not a source of
|
||||
new architecture. New behavior belongs in `surfaces/quickshell` under a
|
||||
Souveraine-owned module or service. Borrowed code is replaced deliberately,
|
||||
with the replacement's contract documented before the borrowed file leaves.
|
||||
|
||||
## What the active shell has today
|
||||
|
||||
The phone lock has a credential keypad, fingerprint attempt, battery state,
|
||||
and guarded power actions. It does **not** yet have lockscreen media,
|
||||
notifications, timers, weather, calls, agent output policy, a staged idle
|
||||
coordinator, or a real sleep-inhibitor backend. `Idle.qml` currently controls
|
||||
hypridle because the tested invisible Wayland idle inhibitor is not honoured
|
||||
on the Pixel 3 compositor build.
|
||||
|
||||
That is a sound small base. It is not yet a phone-grade lock surface.
|
||||
|
||||
## Ideas to extract
|
||||
|
||||
| Source | Keep | Do not inherit |
|
||||
| --- | --- | --- |
|
||||
| Sailfish/Lipstick lockscreen | A lock screen as layered/pannable content: glance surface, credential surface, and event surface. Wake/display-off are separate from lock. | Its device-specific Lipstick and DeviceLock plumbing; source provenance must be audited before copying. |
|
||||
| Glacier Home | A compact MPRIS card with capability-gated previous/play/next controls; media is loaded only if a player exists. Clock, media, keypad, and notifications are separate components. | Its older Nemo/Amber APIs and visual code verbatim unless copyright notices travel with it. |
|
||||
| DankMaterialShell | Independent idle monitors for dim/DPMS/lock/suspend, explicit re-arming, AC-vs-battery thresholds, and a wake monitor after screen-off. | Treating its shell state as authority or blindly importing its settings model. |
|
||||
| ii/iNiR | MPRIS card mechanics, downloaded artwork, palette adaptation, and image "least busy region" placement. ii already calculates content placement against wallpaper pixels. | The current one-file wallpaper assumption and arbitrary widget dimensions. |
|
||||
| Ambxst | Screen capture at lock, short-lived credential handling, failure lockout feedback, and listening to logind Lock/PrepareForSleep events. | Its AGPL code, polling `axctl` layer, and its separate lockscreen authority. Concepts only. |
|
||||
|
||||
DankMaterialShell and iNiR are MIT; Glacier carries MIT/BSD notices. Ambxst is
|
||||
AGPL and Vast is GPL. Do not copy from the latter two into this repository
|
||||
without intentionally accepting their licence obligations. Sailfish files in
|
||||
the local research extraction need per-file provenance review before reuse.
|
||||
|
||||
## One lock surface, composable cards
|
||||
|
||||
The lock surface should be a host with slots rather than a new shell for each
|
||||
device:
|
||||
|
||||
```
|
||||
WlSessionLockSurface
|
||||
└─ LockSurfaceHost
|
||||
├─ glance slot clock · date · battery · weather
|
||||
├─ continuity slot timer · ongoing call · navigation · media transport
|
||||
├─ event slot notification summaries / previews
|
||||
├─ credential slot PIN/password/fingerprint PAM conversation
|
||||
└─ ambient-agent slot explicitly safe question/answer cards
|
||||
```
|
||||
|
||||
The host chooses a layout profile:
|
||||
|
||||
| Profile | Layout |
|
||||
| --- | --- |
|
||||
| phone portrait | One vertical glance surface; a swipe/press brings up credentials; continuity cards occupy the lower third. |
|
||||
| laptop | Center credentials with a side or lower card rail; multi-monitor cards remain local to their screen. |
|
||||
| desktop | Center credentials and a sparse, large-screen card rail; no phone gesture assumptions. |
|
||||
|
||||
Each card declares a tier and a lock-surface visibility policy. This prevents
|
||||
an attractive card from silently becoming a data leak.
|
||||
|
||||
| Tier | Suitable cards | Rule |
|
||||
| --- | --- | --- |
|
||||
| ambient | clock, battery, weather, timer, transport buttons, an ongoing-call *indicator* | no user data beyond the explicitly permitted setting |
|
||||
| personal | track title/art, notification body, calendar title, agent conversation, call identity | hidden until unlock unless the user opts that field into ambient |
|
||||
| step-up | send/reply/delete/purchase/unlock a physical thing | never performed by a card tap alone; needs a recent step-up grant |
|
||||
|
||||
An ambient agent is not a diluted personal agent. It has a separate tool and
|
||||
memory allowlist, cannot read conversation history or sensors, and its output
|
||||
is labelled ambient. If the lock starts while a personal response streams, the
|
||||
host immediately withdraws it.
|
||||
|
||||
## Idle is a transition graph, not a timer
|
||||
|
||||
The useful DMS extraction is the vocabulary and re-arm discipline. The actual
|
||||
authority remains Wayland idle protocols plus logind:
|
||||
|
||||
```
|
||||
active ─idle timeout→ dimmed ─timeout→ lock-requested
|
||||
│
|
||||
WlSessionLock.secure
|
||||
↓
|
||||
lock-secure ─timeout→ display-off
|
||||
│ │
|
||||
PrepareForSleep(true) ─────────────────────────────────────┘
|
||||
↓
|
||||
suspending → asleep → waking → active
|
||||
```
|
||||
|
||||
Input can cancel dimming and display-off before lock. It cannot cancel an
|
||||
already secure lock; credentials do that. Every transition has an event source
|
||||
and an observable result. `screenLocked` is the request; `screenLockSecure` is
|
||||
the compositor acknowledgement.
|
||||
|
||||
The implementation sequence is:
|
||||
|
||||
1. Add one `IdleCoordinator` that owns the staged state and form-factor
|
||||
thresholds. It consumes native `IdleMonitor` events when verified on the
|
||||
target; the Pixel can retain the hypridle adapter behind that interface.
|
||||
2. Add a logind event watcher for this session's `Lock` and the manager's
|
||||
`PrepareForSleep` signal.
|
||||
3. Hold a delay-mode sleep inhibitor while the shell is running. Release it
|
||||
only after the Wayland lock is secure; re-acquire it on wake. Measure the
|
||||
actual logind delay budget on the Pixel 3.
|
||||
4. Make `idle`, `sleep`, `logout`, and `user-switch` explicit inhibitor kinds.
|
||||
Only expose a kind after it owns a real backend. `idle` is the sole
|
||||
implemented kind today.
|
||||
|
||||
## Wallpaper should be an asset, not a path
|
||||
|
||||
The current background path gives every output the same image and asks crop to
|
||||
solve incompatible aspect ratios. A downloaded landscape image consequently
|
||||
loses its subject on the phone. Preserve originals, but track a portable asset
|
||||
record:
|
||||
|
||||
```
|
||||
id, originalPath, origin, licence, palette,
|
||||
focalPoint(x,y), safeRegions[],
|
||||
variants: { landscape, portrait, square },
|
||||
previewPath, generatedAt
|
||||
```
|
||||
|
||||
Selection order for a screen is:
|
||||
|
||||
1. Use an explicit per-device assignment when the user set one.
|
||||
2. Select a sufficiently close aspect-ratio variant.
|
||||
3. Crop a derived local variant around `focalPoint` while preserving a
|
||||
`safeRegion`; never overwrite the original.
|
||||
4. Fall back to a blurred/palette background with the image contained, rather
|
||||
than crop the only meaningful part away.
|
||||
|
||||
ii's existing least-busy-region analysis is useful twice: place a clock/card
|
||||
where it does not cover the subject, and propose an initial safe region.
|
||||
However, it must run against the **actual output crop** and card dimensions,
|
||||
not a fixed 300×300 desktop widget. A manual focal-point editor is the escape
|
||||
hatch; an analyser may propose, never silently decide.
|
||||
|
||||
For the lock surface, prefer a frozen screencopy or an already-derived local
|
||||
variant over downloading or decoding a remote wallpaper at lock time. Locking
|
||||
must remain instant and work offline.
|
||||
|
||||
## Proposed Souveraine map
|
||||
|
||||
```
|
||||
modules/common/functions/Session.qml lifecycle verbs + IPC projection
|
||||
services/SessionEvents.qml logind Lock / PrepareForSleep ingress
|
||||
services/IdleCoordinator.qml transition graph + target adapters
|
||||
services/LockContentPolicy.qml card tier / field visibility decisions
|
||||
modules/ii/lock/LockSurfaceHost.qml shared card host
|
||||
modules/ii/lock/cards/ Clock · Media · Notifications · Continuity · AmbientAgent
|
||||
services/StepUpAuth.qml independent PamContext + expiring grants
|
||||
services/WallpaperAssets.qml asset records, variants, safe regions
|
||||
```
|
||||
|
||||
None of these are separate session managers. They are consumers of the single
|
||||
`session` lifecycle and its state projection.
|
||||
|
||||
## Build order
|
||||
|
||||
1. **Truth before visuals:** logind event ingress, delay inhibitor, and
|
||||
`IdleCoordinator` with a visible transition log. [done — SessionEvents.qml, delay inhibitor in SessionEvents, IdleCoordinator extended with sleep states]
|
||||
2. **First lock cards:** clock/date, battery, transport-only MPRIS. Add the
|
||||
metadata privacy switch before title/artist/artwork.
|
||||
3. **Continuity:** timers, ongoing call, navigation, and notification summary
|
||||
cards with per-field redaction.
|
||||
4. **Trust:** ambient-agent allowlist, lock-time response redaction, then
|
||||
`StepUpAuth` and action-family grants. [partially done — StepUpAuth.qml built, minTier wiring and ambient-agent allowlist remain]
|
||||
5. **Wallpaper assets:** per-device assignments, focal-point editor, derived
|
||||
portrait/landscape variants, and crop-aware card placement.
|
||||
|
||||
The first useful vertical slice is therefore not a huge lockscreen redesign:
|
||||
one shared `LockSurfaceHost`, a transport-only media card, a real staged idle
|
||||
state projection, and a wallpaper asset descriptor. It proves the architecture
|
||||
on both shapes without committing personal data to the lock surface.
|
||||
307
docs/SESSION-AUTHORITY-DOCTRINE.md
Normal file
307
docs/SESSION-AUTHORITY-DOCTRINE.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
# Session authority — doctrine
|
||||
|
||||
The concepts behind the session/trust work, locked down so they stop living in
|
||||
chat logs. This is *why*; `SESSION-TRUST-ARCHITECTURE.md` is the contract and
|
||||
`TRUST-BOUNDARY-MATRIX.md` is the enforcement status. Where this and the code
|
||||
disagree, the code is right — fix one or the other in the same change.
|
||||
|
||||
## 1. Why Linux has no session manager
|
||||
|
||||
Unix's security boundary is the UID. Everything running as you *is* you — your
|
||||
shell, your editor, a binary you curl-piped last Tuesday. There was never a
|
||||
concept of "the session" as an authority separate from "whatever runs as this
|
||||
user," because nothing needed gating from the user's own processes. X11 made it
|
||||
explicit: any authenticated client could do anything, and session management
|
||||
(XSMP) was a save-file, not a boundary.
|
||||
|
||||
What did need gating was narrower: things affecting *other people on the box*.
|
||||
Suspend, poweroff, VT-switch, killing another user's session. That is the entire
|
||||
reason logind's inhibitors and polkit actions cluster there and nowhere else.
|
||||
ConsoleKit (2007) → logind (2011) is the closest thing to a session manager
|
||||
Linux grew, and its job is deliberately small: who is logged in on which seat,
|
||||
and arbitration of the few operations touching shared hardware.
|
||||
|
||||
polkit and xdg-desktop-portal look like a permission broker but exist for a
|
||||
different reason: Flatpak. Sandboxed apps have zero ambient access — the first
|
||||
things on Linux that aren't automatically you. Portals are the mediated door.
|
||||
This is why the stack feels redundant and inconsistent: two overlapping trust
|
||||
models bolted together, unified by convention and a shared bus. Unsandboxed apps
|
||||
(still most of the desktop) ignore both and call `system()`.
|
||||
|
||||
Wayland quietly pulled session-manager authority into the compositor without
|
||||
naming it that — `ext-session-lock-v1` and `ext-idle-notify-v1` exist because
|
||||
Wayland's founding move was "the compositor is the sole trusted actor; anything
|
||||
privileged goes through a protocol it can refuse."
|
||||
|
||||
The phone did not invent the need for a session manager. It removed the ability
|
||||
to shrug it off — one screen, one battery, no fallback device, and an
|
||||
expectation since Android/iOS that a misbehaving app cannot hold a wake lock
|
||||
forever or walk past the lock screen.
|
||||
|
||||
**So this is not a redundant permissions layer.** It is the thing classical
|
||||
Linux never had: one authority treating compositor state (lock, idle) and system
|
||||
state (suspend, inhibit) as facets of the same session, instead of three
|
||||
subsystems queried separately and reconciled by hand.
|
||||
|
||||
## 2. Capability tiers — one policy system, not three features
|
||||
|
||||
The lock screen, media, and the agent are not three subsystems. They are one
|
||||
policy system with tiers. Tier is **metadata on the guarded methods that already
|
||||
exist** (`{minTier: 'stepUp'}`), checked at the point that currently checks
|
||||
`state().locked`. It is not a fourth subsystem.
|
||||
|
||||
| Tier | Requirement | Examples |
|
||||
| --- | --- | --- |
|
||||
| `ambient` | none | clock, weather, timers, MPRIS transport, general Q&A that touches neither memory nor sensorium |
|
||||
| `personal` | session genuinely unlocked (`!screenLocked && !screenLockSecure`, live check) | messages, calendar, memories, fleet state, conversation history |
|
||||
| `stepUp` | fresh re-auth, even when already unlocked | sending, pushes, deletes, payments, physical access — **and reading personal content on the lock surface** |
|
||||
|
||||
### stepUp is a reveal mechanism, not only an action gate
|
||||
|
||||
The tier table read naively implies personal content is binary: hidden until the
|
||||
session unlocks. That is wrong for the sole-user phone. Notification bodies,
|
||||
message content, and agent output stay hidden at ambient — but a fingerprint
|
||||
touch on the lock surface should reveal them **in place, without unlocking the
|
||||
session**.
|
||||
|
||||
That means action families include `read`, not just mutation verbs. On a phone
|
||||
you are the only user of, `read` is the family that matters most.
|
||||
|
||||
Mechanically this needs no new machinery: a `PamContext` against
|
||||
`souveraine-stepup` accepting fingerprint mints a short-lived grant, and
|
||||
`LockContentPolicy.allowsOnLock()` gains a third condition — not just
|
||||
`ambient || promotedAmbient`, but `|| hasFreshGrant('read')`.
|
||||
|
||||
Gated on the fingerprint sensor coming online (not yet true on blueline).
|
||||
|
||||
### Media metadata is a toggle, not a fixed answer
|
||||
|
||||
`nowPlaying` metadata is ambient by default, configurable to personal. Android
|
||||
and iOS both punt this to the user because it genuinely varies by person. Wire
|
||||
it into the tier system; do not special-case it.
|
||||
|
||||
### Two policy questions, deliberately unanswered
|
||||
|
||||
- **Freshness window.** Prompting per tagged action is sudo asking per line of a
|
||||
script. Needs a trust window (last-success timestamp vs expiry). The number is
|
||||
a deliberate policy setting, not an implementation accident.
|
||||
- **In-flight output on lock.** If lock fires mid-turn, personal/step-up output
|
||||
is withheld or redacted; only ambient remains. Same rule as notification
|
||||
content — not a separate rule for agent output.
|
||||
|
||||
## 3. Step-up is decoupled from the session lock
|
||||
|
||||
`PamContext` is a standalone connection type, not baked into `WlSessionLock`.
|
||||
Point a second, narrower context at a custom service
|
||||
(`/etc/pam.d/souveraine-stepup`, accepting password or fingerprint in any
|
||||
order), run that one conversation, gate that one call.
|
||||
|
||||
Step-up never unlocks the session. It never accepts a boolean from an agent as
|
||||
proof. The grant is short-lived, in-memory, bound to an action family, and
|
||||
cleared on lock, session end, PAM failure, and expiry.
|
||||
|
||||
## 4. Never hold state the protocol owns
|
||||
|
||||
The rule that keeps this from becoming a second source of truth:
|
||||
|
||||
- `locked` comes from `LockedHint` / `Lock()` / `Unlock()` — never a hand-tracked
|
||||
bool. A shadow copy can disagree with logind, and anything asking logind
|
||||
directly (loginctl, another client, you at 2am) sees something different than
|
||||
the shell shows.
|
||||
- Idle goes through `ext-idle-notify-v1` so other apps' inhibitors are visible to
|
||||
us and ours to them. Hand-rolled idle detection is blind to both.
|
||||
- Suspend goes through logind's `Suspend()`, never `/sys/power/state` — that is
|
||||
where decades of S3-vs-s2idle and firmware-quirk handling live.
|
||||
- Auth goes through PAM. It is the one surface everything on the system already
|
||||
agrees on (sudo, sshd, fprintd, login). Reinventing it is the one move that
|
||||
would genuinely be redesigning a Linux fundamental.
|
||||
|
||||
Being a *client* of logind and the Wayland protocols is the same role
|
||||
gnome-session, ksmserver, and swayidle+swaylock have always had. That is a
|
||||
well-trodden category, not a hack on the OS. It stops being that the moment we
|
||||
keep parallel state.
|
||||
|
||||
## 5. Idle belongs to the compositor; logind owns only sleep and shutdown
|
||||
|
||||
`ext-idle-notify-v1` (`IdleMonitor`) and `idle-inhibit-unstable-v1`
|
||||
(`IdleInhibitor`) are native Wayland protocol bindings — no external process, no
|
||||
polling. Idle staging becomes several `IdleMonitor` instances at different
|
||||
timeouts, declared in config and legible to the agent, instead of exec
|
||||
directives parsed out of hypridle.conf.
|
||||
|
||||
`IdleMonitor.respectIdleInhibitors` (default true) already factors active
|
||||
inhibitors into `isIdle`.
|
||||
|
||||
logind's `Inhibit()` is therefore needed only for **sleep and shutdown** — the
|
||||
categories genuinely outside compositor authority. That is a far smaller surface
|
||||
than "wrap all of logind."
|
||||
|
||||
Caveat before removing the hypridle unit: both protocols must actually be bound
|
||||
on the build. Verify separately on x86 and SDM845 — do not assume parity.
|
||||
|
||||
## 6. The inhibit vocabulary is portal-shaped, not logind-shaped
|
||||
|
||||
logind's `what` is a fixed set: `shutdown`, `sleep`, `idle`, `handle-*-key`,
|
||||
`handle-lid-switch`. **There is no `logout` or `user-switch` in logind** —
|
||||
nothing in the kernel or systemd cares whether you are mid-logout.
|
||||
|
||||
The four-flag bitmask (1=Logout, 2=UserSwitch, 4=Suspend, 8=Idle) is
|
||||
`org.freedesktop.portal.Inhibit` — the GNOME session-manager convention, later
|
||||
standardized as the portal interface. It is what browsers actually call when a
|
||||
video call wants to stop the screen locking. Build the agent-facing surface on
|
||||
that semantic (third-party software already speaks it), translate per flag:
|
||||
|
||||
| Flag | Underlying mechanism |
|
||||
| --- | --- |
|
||||
| IDLE | toggle `IdleInhibitor.enabled` — no process, no fd |
|
||||
| SUSPEND | fd-holding `systemd-inhibit` process (§7) |
|
||||
| LOGOUT / USER_SWITCH | pure shell policy — our own entry points check the registry; nothing to inhibit at OS level |
|
||||
|
||||
Only expose a kind once it owns a real backend. Accepting a kind we cannot
|
||||
enforce is a lie in a result shape.
|
||||
|
||||
Eventually this sets up registering as a real `org.freedesktop.portal.Inhibit`
|
||||
backend so browsers' "prevent lock during this call" lands in the same registry
|
||||
instead of silently doing nothing. Separate design pass; not folded in now.
|
||||
|
||||
## 7. The fd problem
|
||||
|
||||
No generic D-Bus binding in QML, and `Inhibit()`'s entire contract is "hold this
|
||||
fd open; close it to release." A `Process` that runs `loginctl` and exits cannot
|
||||
hold it — the fd dies with the process.
|
||||
|
||||
The pattern everything else uses (systemd-inhibit, swayidle): spawn a long-lived
|
||||
wrapper that takes the lock and blocks.
|
||||
|
||||
```
|
||||
systemd-inhibit --what=sleep --who=souveraine --why="lock before suspend" \
|
||||
--mode=delay sleep infinity
|
||||
```
|
||||
|
||||
The lock is held exactly as long as the QML `Process` object lives.
|
||||
`uninhibit(cookie)` is "kill this Process." The cookie is a locally-generated
|
||||
UUID mapped to the Process; systemd owns the fd, we never see it.
|
||||
|
||||
If process-per-inhibit proves fragile (e.g. merging N reasons into one D-Bus
|
||||
inhibitor), the next step up is a small long-lived Rust helper owning the fd,
|
||||
taking commands over stdin/socket — the same shape as RedFlag's privileged
|
||||
helper. Reach for it only when the simple model breaks; it is one dependency
|
||||
more.
|
||||
|
||||
## 8. The race that matters — lock before suspend
|
||||
|
||||
systemd's own inhibitor docs name this scenario: "a screen lock tool wants to
|
||||
bring up the screen lock right before suspend, and delay the suspend until
|
||||
that's complete."
|
||||
|
||||
1. Take a standing delay-mode sleep inhibitor at shell startup.
|
||||
2. Watch `PrepareForSleep` via a persistent `gdbus monitor --system` process,
|
||||
parsed line-by-line — **not** polling. Polling latency eats the delay budget
|
||||
directly.
|
||||
3. On `PrepareForSleep(true)`: drive the lock, wait for `WlSessionLock.secure`,
|
||||
release the delay inhibitor only then.
|
||||
4. On `PrepareForSleep(false)`: immediately re-acquire — the old inhibitor is
|
||||
dead and there is a window between wake and next sleep where nothing holds
|
||||
the line.
|
||||
|
||||
Budget is `InhibitDelayMaxUSec` (commonly 5s). **Fail closed:** if the lock is
|
||||
not secure before the deadline, leave a diagnostic event and do not pretend the
|
||||
session locked. Must be timed on the Pixel 3 before it is a security claim —
|
||||
SDM845 lock-surface creation under load is not laptop timing.
|
||||
|
||||
## 9. Sensor readings are evidence, not fact
|
||||
|
||||
The tier system asks "does the user need to authenticate to see this." It never
|
||||
asks "can the system trust this without verification." Those are different
|
||||
questions and the gap between them is a real vector.
|
||||
|
||||
Clock time is genuinely ambient — it comes from a kernel authority. Sensor
|
||||
readings are **observations**: they arrive from hardware through a userspace
|
||||
daemon (`iio-sensor-proxy`) over the system bus. A compromised proxy or a
|
||||
spoofed signal feeds the shell arbitrary orientation, light, and proximity — and
|
||||
the shell acts on it without question.
|
||||
|
||||
This is not a data leak. It is a trust gate opened or closed by a liar. The lock
|
||||
surface decides when to show the credential gate based on lift-to-wake and
|
||||
proximity-out; an adversarial sensor drives that gate at a moment of its
|
||||
choosing. The sensor does not need to beat PAM — it only has to lie to the thing
|
||||
that decides whether PAM is ever asked.
|
||||
|
||||
This is the same shape as `screenLocked` vs `screenLockSecure`: the *reading* is
|
||||
not the *reality*, exactly as the lock *request* is not the compositor's
|
||||
*acknowledgement*. And it is the same shape RedFlag already answered — verify the
|
||||
key, not the messenger; keep the trust root off the box the attacker owns.
|
||||
|
||||
**Consequence for the sensor service:** `souveraine-surface-003` currently scopes
|
||||
`SensorService` as a thin bridge that "exposes what the hardware reports,
|
||||
interpretation belongs to consumers." That wires an unverified input straight
|
||||
into a trust gate. Unreconciled — the open question is whether the service
|
||||
publishes raw readings or observations carrying:
|
||||
|
||||
- provenance (which bus name, which hardware path)
|
||||
- cross-sensor verification (accelerometer says face-down, light sensor says
|
||||
bright — one of them is lying)
|
||||
- confidence (hardware noise is real; smoothing can mask injection)
|
||||
|
||||
The tier vocabulary likely needs a row the current matrix has no cell for —
|
||||
working name `observed`: *this came from a sensor; treat it as evidence, not
|
||||
fact.*
|
||||
|
||||
## 10. Binary compliance — the policy layer is advisory until it isn't
|
||||
|
||||
Everything above is policy: the shell asks, decides, refuses. Policy in
|
||||
userspace is advisory against anything that does not ask. A binary that talks to
|
||||
`net.hadess.SensorProxy` directly, opens the MPRIS bus, or reads the
|
||||
notification store never touches `LockContentPolicy` — it simply acts. Same hole
|
||||
as §9: a gate only gates what routes through it.
|
||||
|
||||
RedFlag already solved this shape and it points inward without modification:
|
||||
capability tokens over a resolved closure, signed by an authority that lives off
|
||||
the box, verified by a small privileged network-less executor, with kernel
|
||||
enforcement (eBPF / WDAC / ESF) ensuring the executor is the only permitted
|
||||
caller. Applied here: binaries carry an attestation, the session authority is
|
||||
the signer, and things that bypass the tier checks do not run.
|
||||
|
||||
RedFlag's load-bearing constraint #5 transfers directly: **kernel stops are
|
||||
defense-in-depth, not a prerequisite.** The policy layer means something before
|
||||
eBPF lands; eBPF raises the cost of bypass. Partial deployment still moves the
|
||||
device out of the soft-target category.
|
||||
|
||||
This is a reaching plan. It is written down because the unification is the point,
|
||||
not the individual pieces.
|
||||
|
||||
## 11. Why unification is paramount
|
||||
|
||||
Four faces of one system:
|
||||
|
||||
| Face | Role | Where it lives today |
|
||||
| --- | --- | --- |
|
||||
| Capability tiers | the policy | `LockContentPolicy`, `StepUpAuth` |
|
||||
| Capability gate | the enforcement | RedFlag's `helper/` — not yet pointed inward |
|
||||
| Seed identity | the signer | `src/core/identity/` (Ed25519, machine key + agent key) |
|
||||
| memfs git DAG | the audit trail | `src/core/memory/`, every write a commit |
|
||||
|
||||
Today these are separate bodies of work that share an author's instincts. The
|
||||
thing that makes SouveraineOS an OS rather than a shell is that **the session
|
||||
authority and the binary authority are the same authority.**
|
||||
|
||||
## 12. Watch list
|
||||
|
||||
- **One polkit agent only.** Two agents racing the same authentication request is
|
||||
a documented failure mode, not a hypothetical. Audit what the base install
|
||||
pulls in.
|
||||
- **Stray legacy daemons.** A leftover `gnome-settings-daemon` /
|
||||
`*-power-manager` fragment watching `PrepareForSleep` or holding its own
|
||||
inhibitor produces double-locks and races that look like our bug but are two
|
||||
authorities colliding. Audit `systemctl --user list-units` and the session bus
|
||||
on both machines before trusting the suspend race.
|
||||
- **Scope portals deliberately.** `xdg-desktop-portal-wlr` already does
|
||||
screenshot/screencast well. Run it underneath; take over only the
|
||||
lock/inhibit-adjacent portal, later, on purpose.
|
||||
- **Verify protocol availability per target.** x86 and SDM845 separately. Both
|
||||
`IdleMonitor` and `IdleInhibitor` report unavailable on some builds.
|
||||
- **SDDM-vs-greetd asymmetry is debt — deferred, not accepted.** Laptop on SDDM
|
||||
and phone on greetd is the current state, not the target. The intent is one
|
||||
system across the board; two greeters is a seam that contradicts it. Not
|
||||
today's work, but do not close this as "two legitimate greeters for two form
|
||||
factors" — that framing is wrong and would let the seam calcify.
|
||||
104
docs/SESSION-TRUST-ARCHITECTURE.md
Normal file
104
docs/SESSION-TRUST-ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Session and interaction trust model
|
||||
|
||||
This is the contract for the Souveraine shell on laptop and phone. It records
|
||||
the boundary between orchestration we own and the Linux/Wayland authorities we
|
||||
must consume rather than replace.
|
||||
|
||||
## Authorities and projections
|
||||
|
||||
| Question | Authority | Souveraine's role |
|
||||
| --- | --- | --- |
|
||||
| Can this machine suspend, hibernate, power off, or reboot? | logind `Can*` methods | Query and expose the answer; execute the matching logind/systemd verb. |
|
||||
| Has the compositor secured the display? | `WlSessionLock.secure` | Mirror it as `screenLockSecure`; never infer it from a button press. |
|
||||
| Does the shell want its lock surface shown? | `GlobalStates.screenLocked` | Maintain this request state and persist it across a shell crash. |
|
||||
| Has the user authenticated? | PAM and the lock's `LockContext` | Start a PAM conversation; only it may release `WlSessionLock`. |
|
||||
| Why is automatic idle sleep suppressed? | The active idle mechanism | Keep a reason/cookie registry, but report only mechanisms that are actually applied. |
|
||||
|
||||
`screenLocked` and `screenLockSecure` must stay distinct. The first closes
|
||||
ordinary shell surfaces immediately. The second is the only proof suitable for
|
||||
personal-data disclosure. A failed or delayed Wayland lock is therefore
|
||||
visible as `lockRequested: true, locked: false`, not mistaken for success.
|
||||
|
||||
## Session IPC
|
||||
|
||||
All structured IPC results are JSON strings. Quickshell IPC only marshals
|
||||
primitive QML types, so `: var` silently becomes `void` and loses its payload.
|
||||
|
||||
`session.lock()` means “the Wayland lock was requested”; it does not claim
|
||||
the compositor is secure yet. `session.state()` exposes the subsequent
|
||||
`locked` acknowledgement and the most recent asynchronous power-action result.
|
||||
Power verbs return `status: "started"` when their command has started; a later
|
||||
exit is available in `lastAction`. Refusals always log and return a reason.
|
||||
|
||||
The current public inhibitor surface implements `idle` and `sleep`. It is
|
||||
intentionally wrong to accept `shutdown`, `logout`, or `user-switch` until each
|
||||
owns a real mechanism. The eventual contract is:
|
||||
|
||||
| Kind | Backend | Status |
|
||||
| --- | --- | --- |
|
||||
| `idle` | Native Wayland idle inhibitor once verified on both targets; until then the tested hypridle control path. | Implemented. |
|
||||
| `sleep` | A live logind/systemd inhibitor FD or `systemd-inhibit` process, held for the cookie lifetime. | Implemented via SessionEvents delay-mode inhibitor. |
|
||||
| `logout`, `user-switch` | Shell policy checks at those operations, not fake logind inhibitors. | Not yet implemented. |
|
||||
|
||||
Every inhibitor must have a non-empty human reason, a cookie, and a visible
|
||||
holder in `state()`. Releasing an unknown cookie is a refusal.
|
||||
|
||||
## Lock and suspend sequencing
|
||||
|
||||
The next lifecycle change is not another cached state machine. It is a logind
|
||||
event ingress, implemented in `services/SessionEvents.qml`:
|
||||
|
||||
1. [done] Subscribe to the current logind session's `Lock` signal and request the
|
||||
Wayland lock on receipt. This makes external `loginctl lock-session` calls
|
||||
meaningful without assuming logind implements a screen locker.
|
||||
2. [done] Hold one delay-mode sleep inhibitor from shell startup.
|
||||
3. [done] On logind `PrepareForSleep(true)`, request the Wayland lock and wait for
|
||||
`WlSessionLock.secure`; release the delay inhibitor only then.
|
||||
4. [done] Reacquire it after `PrepareForSleep(false)`.
|
||||
|
||||
The delay budget is logind's `InhibitDelayMaxUSec` (commonly five seconds), so
|
||||
this must be timed on the Pixel 3 before it becomes a security claim. The
|
||||
failure policy is fail closed: if the lock is not secure before the deadline,
|
||||
leave a diagnostic event and do not pretend the session was locked.
|
||||
|
||||
## Interaction capability tiers
|
||||
|
||||
The lock screen, media controls, and agent are one policy system, not three.
|
||||
Each operation and each response is labelled with a minimum tier:
|
||||
|
||||
| Tier | Requirement | Examples |
|
||||
| --- | --- | --- |
|
||||
| `ambient` | None | clock, weather, timers, transport controls, non-personal answers |
|
||||
| `personal` | No lock is requested and the compositor is not secure (`!screenLocked && !screenLockSecure`) | messages, calendar, memories, fleet state, conversation history |
|
||||
| `stepUp` | Recent successful reauthentication | sending, pushes, deletion, payments, physical access |
|
||||
|
||||
Media metadata is `ambient` by default but must be configurable to `personal`.
|
||||
When the lock occurs, in-flight personal or step-up agent output is withheld or
|
||||
redacted; only ambient output remains visible.
|
||||
|
||||
Step-up authentication is a separate `PamContext`, using a dedicated PAM
|
||||
service such as `souveraine-stepup`. It never unlocks the session and it never
|
||||
accepts a boolean from an agent as proof. A successful result mints a
|
||||
short-lived, in-memory grant bound to the local action family. The grant must
|
||||
be cleared on lock, session end, PAM failure, and expiry. The exact freshness
|
||||
window is a deliberate policy setting, not an implementation accident.
|
||||
|
||||
Implemented in `services/StepUpAuth.qml`. The PAM service file
|
||||
(`/etc/pam.d/souveraine-stepup`) is NOT shipped by the shell — it is
|
||||
root-owned system configuration and must be installed/audited separately.
|
||||
Before enabling it, verify that one polkit agent owns the session and that
|
||||
no legacy desktop power manager is competing for idle or sleep policy.
|
||||
|
||||
The trust boundary matrix is documented in `TRUST-BOUNDARY-MATRIX.md`.
|
||||
|
||||
## Required verification
|
||||
|
||||
- `session.capabilities()` matches logind `Can*` on laptop and phone.
|
||||
- `session.lock()` reaches `locked: true` only after `WlSessionLock.secure`.
|
||||
- `loginctl lock-session` and suspend both take the same Wayland lock path.
|
||||
- Unsupported inhibitors refuse; each supported cookie visibly changes its
|
||||
mechanism and is released exactly once.
|
||||
- A lock during personal agent output hides it; a lock during an ambient timer
|
||||
leaves only the timer visible.
|
||||
- Step-up grants cannot survive lock, timeout, shell restart, or action-family
|
||||
changes. [done — StepUpAuth revokes on lock, expiry timer runs every 30s]
|
||||
180
docs/SETTINGS-APP-PLAN.md
Normal file
180
docs/SETTINGS-APP-PLAN.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Souveraine shell — plan (2026-07-13)
|
||||
|
||||
## What this is now
|
||||
|
||||
`qs -c souveraine` — our own quickshell config, composed by `deploy.sh`:
|
||||
our files (shell.qml, SouveraineFamily, services/, modules/settings/) are
|
||||
symlinked from this repo; untouched ii directories are borrowed as whole-dir
|
||||
symlinks; directories where we override any file are composed file-by-file.
|
||||
ii's tree stays pristine — no overlay symlinks, no `.upstream` backups. We
|
||||
step on end-4's toes nowhere; we borrow deliberately and vendor per-directory
|
||||
only when a reason forces it.
|
||||
|
||||
`SouveraineFamily.qml` is the convergence scoreboard: one family, both modes
|
||||
(desktop + phone). Panels that differ by form factor get an
|
||||
`extraCondition: Config.options.souveraine.phone` gate; a panel is
|
||||
"homogenized" when its gate is deleted. No `panelFamily` switching — the
|
||||
config loads SouveraineFamily, period.
|
||||
|
||||
Live on laptop + phone as of 2026-07-13. Hyprland flipped: laptop via the
|
||||
`qsConfig` var, phone via sed across its 4 hardcoded `qs -c ii` sites
|
||||
(backup: `hyprland.lua.bak-souveraine-config`).
|
||||
|
||||
### The Settings app
|
||||
|
||||
`souveraine/settings-phone.qml` — purpose-built mobile shell (list → push →
|
||||
back, 68px finger rows, fullscreen, no titlebar), launched by
|
||||
`settings-launch.sh` (imports compositor env from the user manager) via
|
||||
`souveraine-settings.desktop` in the phone's app grid. Page registry shape
|
||||
`[{name, icon, component}]` is identical to desktop settings.qml, so pages
|
||||
are interchangeable. Pages edit `Config.options` (JsonAdapter → config.json,
|
||||
hot-apply + persist).
|
||||
|
||||
Current pages: Device (souveraine.phone flag + future per-device overrides),
|
||||
Lock screen, Dock, Pill, Keyboard, About (ii's stock page verbatim).
|
||||
|
||||
## Doctrine — "tie back down"
|
||||
|
||||
Two tiers, nothing app-private:
|
||||
|
||||
1. **Shell settings** → `Config.options.*` → config.json. Idempotent,
|
||||
survives redeploy, diffable. Zero new persistence.
|
||||
2. **System settings** → the owning daemon, live: NetworkManager, ModemManager
|
||||
(mmcli -J), bluez, UPower, brightnessctl. The app is a *view* over system
|
||||
state — kill it and the system is still the truth. No shadow copies, no
|
||||
sync jobs. Privileged writes via polkit (`pkexec` — setuid fixed
|
||||
2026-07-13, Pixel3Arch gap #7), zero sudo.
|
||||
|
||||
## Upstream drift — the decision (2026-07-13)
|
||||
|
||||
Our overrides were forked against the **phone's vintage** of ii. The laptop
|
||||
runs a **newer** ii. Drift is large and one-directional. The question is not
|
||||
"rebase everything" — it's "which upstream additions do we actually want,
|
||||
and which are desktop features we're deliberately not running on a
|
||||
phone-first shell."
|
||||
|
||||
**Laptop-upstream additions we do NOT have:**
|
||||
- `background.widgets.{visualizer,stats,systemResources}` — desktop widgets
|
||||
(the pctrade-style widget system landed upstream). Phone doesn't use these.
|
||||
BUT: borrowed `Background.qml` reads `Config.options.background.widgets.*`
|
||||
unconditionally → TypeError spam on laptop. **Fix: backport the keys into
|
||||
our Config.qml (small surgical add), don't pull the widget machinery.**
|
||||
- `claudeUsage` — Claude Pro/Max subscription gauge in the bar. Want later.
|
||||
- `fileSearch` — indexed file search in the launcher. Want later.
|
||||
- `sidebar.width`/`widthExtended`, AiChat `fontSize` live-binding. Want.
|
||||
- `dpmsTimeout`, `unlockHook`, `autoIdleInhibit`, `termBgTone` — small knobs.
|
||||
- GlobalStates super-press timing (`superPressTime`,
|
||||
`shouldSuppressSuperReleaseSearch`) — press-and-hold-super suppresses
|
||||
search. Conflicts with our `dockRevealPulse` patch (both touch the
|
||||
super-release block). **Rebase needed eventually; not breaking anything
|
||||
today.**
|
||||
- Ai.qml: OpenAI Responses API strategy, `thoughtSignature`, FileView chat
|
||||
save. Our Ai.qml is heavily customized (no hardcoded models, Bifrost
|
||||
server, agent/resume) — **rebase here is the dangerous one; do carefully
|
||||
or not at all.**
|
||||
|
||||
**What we have that upstream DROPPED:**
|
||||
- `Config.options.dock.stacks` (fan-out stacks, "stackId|appId,appId") —
|
||||
upstream removed it. Our `DockStack.qml` arc feature is ours alone now,
|
||||
not just forked. Keep.
|
||||
|
||||
### Drift action items (priority order)
|
||||
1. **Backport `background.widgets.*` keys into our Config.qml** — kills the
|
||||
laptop TypeError spam. Small, surgical, no widget machinery pulled in.
|
||||
2. GlobalStates.qml — rebase `dockRevealPulse` onto upstream's super-press
|
||||
rewrite. Both touch the same block.
|
||||
3. Ai.qml — decide: carry our customizations forward on the old base (safe,
|
||||
current path) or rebase onto upstream's Responses-API Ai.qml (risky, gets
|
||||
us thoughtSignature + FileView). Defer until we want one of those.
|
||||
4. SidebarLeft/Right/Overview — dedupe the OSK flee-fix (15 lines triplicated)
|
||||
into one place after any rebase.
|
||||
5. Don't rebase dock/osk/TaskbarApps unless a specific breakage forces it.
|
||||
|
||||
## Pages still to build (deferred, "in time")
|
||||
|
||||
- **Display** — brightness (Brightness service), screen toggle
|
||||
(`blueline-screen-toggle`), idle/suspend timeouts. hypridle.conf is
|
||||
user-owned: regenerate whole from template, don't sed.
|
||||
- **Network** — WiFi via existing Network service; **Cellular** needs a new
|
||||
`services/Cellular.qml` wrapping `mmcli -J` (data toggle, APN, signal,
|
||||
operator, SIM PIN). The real new work.
|
||||
- **Bluetooth** — BluetoothStatus exists, but org.bluez activation fails on
|
||||
the phone (journal, every boot). Fix the service before building UI on it.
|
||||
- **App health check** — surfaced as a wanted page; not built.
|
||||
- **Dock pins/stacks editor** — list editor with rename; consumes the staged
|
||||
`renameStack`/`stackName` plumbing in TaskbarApps/DockStack.
|
||||
|
||||
## Other open items
|
||||
- Phone cold-boot verification (autostart line flipped, unproven until next boot).
|
||||
- `souveraine.phone` defaults false; Device page exposes the toggle but
|
||||
deploy.sh --phone doesn't flip it. Tying it down on phone deploy would
|
||||
satisfy idempotency doctrine.
|
||||
- Services rename to be more "souveraine-esq" — deferred.
|
||||
- Licensing sweep — see ~/Projects/MAKE_PROPER_LICENSES_FOR_ALL_PROJECTS.md.
|
||||
|
||||
## Ecosystem — the framework basics (2026-07-13)
|
||||
|
||||
Reference studied: `PostMarketOS-Blueline/references/phosh` (full source
|
||||
checkout). Phosh's shell is built on three primitives we can learn from
|
||||
without copying its C: `PhoshLayerSurface` (every visible thing subclasses
|
||||
it), the zwlr_layer_shell_v1 layer it sits on (BACKGROUND/TOP/OVERLAY), and
|
||||
`zphoc_stacked_layer_surface_v1` — phoc's extension for ordering
|
||||
layer-surfaces above/below each other *within* a layer (bare wlr can't).
|
||||
Plus a `PhoshState` bitmask (NONE / MODAL_SYSTEM_PROMPT / BLANKED / LOCKED /
|
||||
SETTINGS / OVERVIEW) that the shell reads to gate layer visibility —
|
||||
`use_top_layer = !phosh_shell_get_locked()` is the canonical line. State
|
||||
drives layer visibility, not the reverse.
|
||||
|
||||
Note: `PhoshDockedManager` is NOT "the dock" in our sense — it's *hardware*
|
||||
docking (phone → external display: disables OSK, stops auto-maximize, flips
|
||||
`is-phone`). It's the system-level analog of our `souveraine.phone` flag:
|
||||
the phone-becomes-desktop transition. They rhyme; they aren't the same
|
||||
feature.
|
||||
|
||||
### What we're actually building (three pieces, compose incrementally)
|
||||
|
||||
1. **Layer registry + ShellState** — declare each surface's layer and
|
||||
stack-order explicitly, driven by a state bitmask. GlobalStates is
|
||||
*almost* this already (it has the state bits) but doesn't own the
|
||||
layer/stack declarations. Quickshell gives us layer-shell via
|
||||
`PanelWindow` anchors + exclusive zones — same protocol as Phosh, QML
|
||||
binding not C. Steal the model, not the code.
|
||||
|
||||
2. **Live manifest per surface** — the dock (then each major surface)
|
||||
projects its current state as a queryable structure: pinned apps,
|
||||
stacks, positions, visibility, mode. This is a *service*, not UI —
|
||||
`dock.manifest` → `{pinned, stacks, hidden, mode}`. The dock already
|
||||
holds this state internally; the manifest is the projection for
|
||||
external consumers.
|
||||
|
||||
3. **Method abstraction for the agent** — the agent does NOT get a new
|
||||
toolcall integration. Souveraine is already a fully-fleshed harness
|
||||
with its own integration state; we use THAT. What lives on the dock
|
||||
side is a small method surface (`dock.pin(appId)`, `dock.restack(...)`,
|
||||
`dock.reveal()`) with **state checks baked in** — validate inputs,
|
||||
refuse mutation when the dock is in a state that forbids it (locked,
|
||||
mid-drag), return real results. The agent calls a guarded method, not
|
||||
a footgun; it genuinely can't mess up the dock through the abstraction.
|
||||
|
||||
### Where the lessons live — Souveraine School
|
||||
|
||||
The *teaching* of these methods — what they are, when to use them, the
|
||||
lessons — is NOT inline in the dock or the agent. It lives in the
|
||||
Souveraine School, taught to agent and human in proper time. The dock's
|
||||
job is to expose the abstraction; the School's job is to teach it. Two
|
||||
separate concerns; do not conflate.
|
||||
|
||||
### First artifact
|
||||
|
||||
The **dock manifest** is the smallest concrete first step: the dock is
|
||||
built, working, and already has internal state (pinned apps, stacks,
|
||||
reveal/pulse). Projecting it into a queryable manifest + guarded method
|
||||
surface proves the pattern. The layer registry generalizes it after.
|
||||
|
||||
### Reference material on disk
|
||||
- Phosh source: `~/Projects/PostMarketOS-Blueline/references/phosh` (full
|
||||
C/meson checkout, read-only reference — do not edit).
|
||||
- end4-pC fork (pctrade): `~/Projects/end4-pC` — studied for the
|
||||
panelFamilies + background-widget patterns; not copied.
|
||||
- Pure Maps (Kirigami/QML nav) — referenced in
|
||||
`Pixel3Arch/docs/car-and-dock-references.md`; not yet pulled as source.
|
||||
64
docs/TRUST-BOUNDARY-MATRIX.md
Normal file
64
docs/TRUST-BOUNDARY-MATRIX.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Trust boundary matrix
|
||||
|
||||
Every session operation classified by caller type, required auth gate, and
|
||||
current enforcement status. This is the reference for what is gated and what
|
||||
is not — if an operation is not in this matrix, it is not gated.
|
||||
|
||||
## Legend
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| Operation | The IPC verb or internal action |
|
||||
| Caller | Who can invoke it |
|
||||
| Min tier | Minimum capability tier required |
|
||||
| Gate | What enforces the tier |
|
||||
| Status | `enforced`, `partial`, `ungated` |
|
||||
|
||||
## Operations
|
||||
|
||||
| Operation | Caller | Min tier | Gate | Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `session.lock()` | IPC / UI / logind signal | ambient | None (always allowed) | enforced |
|
||||
| `session.unlock()` | Credential gate only | stepUp | LockContext PIN/PAM | enforced — IPC refuses |
|
||||
| `session.suspend()` | IPC / UI | ambient | capability probe | enforced |
|
||||
| `session.hibernate()` | IPC / UI | ambient | capability probe | enforced |
|
||||
| `session.poweroff()` | IPC / UI | ambient | capability probe + polkit | enforced |
|
||||
| `session.reboot()` | IPC / UI | ambient | capability probe + polkit | enforced |
|
||||
| `session.logout()` | IPC / UI | ambient | None | enforced |
|
||||
| `session.inhibit("idle")` | IPC / internal | ambient | reason required | enforced |
|
||||
| `session.inhibit("sleep")` | IPC / internal | ambient | reason required | enforced |
|
||||
| `session.state()` | IPC / agent | ambient | None (read-only projection) | enforced |
|
||||
| `session.caps()` | IPC / agent | ambient | None (read-only projection) | enforced |
|
||||
| `StepUpAuth.requestAuth()` | UI / agent | ambient | PAM conversation | enforced |
|
||||
| `StepUpAuth.isGranted()` | UI / agent | ambient | None (read-only check) | enforced |
|
||||
| `StepUpAuth.revokeGrant()` | UI / internal | ambient | None | enforced |
|
||||
| Agent conversation | Souveraine IPC | ambient | Server auth token | enforced |
|
||||
| Agent send with personal context | Souveraine IPC | personal | !screenLocked && !screenLockSecure | enforced via LockContentPolicy |
|
||||
| Agent output on lock surface | Lock surface | ambient | LockContentPolicy.allowsOnLock() | enforced |
|
||||
| In-flight agent response on lock | Lock surface | personal | Ai.qml redacts on screenLocked | enforced |
|
||||
| Media metadata on lock surface | Lock surface | ambient (if opted) | LockContentPolicy.mediaMetadataAmbient | enforced |
|
||||
| Media transport controls | Lock surface | ambient | LockContentPolicy.mediaControlsVisible | enforced |
|
||||
| Lock screen power actions | Lock surface | ambient | Config.lock.security.requirePasswordToPower | enforced |
|
||||
| Idle state transition | IdleCoordinator | ambient | nativeEnabled config | enforced |
|
||||
| Sleep/suspend transition | SessionEvents | ambient | delay inhibitor + WlSessionLock.secure | enforced |
|
||||
| Session audit trail | SessionAudit | ambient | append-only JSONL with hash chain | enforced |
|
||||
|
||||
## Not yet gated (gaps)
|
||||
|
||||
| Operation | Caller | Required tier | Gap |
|
||||
| --- | --- | --- | --- |
|
||||
| Agent delete/push operations | Souveraine IPC | stepUp | StepUpAuth built but minTier metadata not wired |
|
||||
| Agent physical access | Souveraine IPC | stepUp | StepUpAuth built but minTier metadata not wired |
|
||||
| Break-glass override | Emergency | scoped grant | Done (StepUpAuth.breakGlass) |
|
||||
| Boot-time IPC audit | Shell startup | ambient | Done (log in Session.qml) |
|
||||
|
||||
## Notes
|
||||
|
||||
- The matrix is local-only: all callers are on the same machine, reachable
|
||||
only over quickshell's IPC socket by the user who owns the session.
|
||||
- If a network-reachable caller is ever added, every row needs re-evaluation.
|
||||
- The `personal` tier gate is `!screenLocked && !screenLockSecure` — the
|
||||
session must not be locked and the compositor must not have secured the
|
||||
lock surface. This is a live check, not a cached bool.
|
||||
- The `stepUp` tier requires a recent (TTL-window) StepUpAuth grant for the
|
||||
relevant action family.
|
||||
170
docs/qtpim-contacts-handoff.md
Normal file
170
docs/qtpim-contacts-handoff.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# qtpim contacts — state as of 2026-07-15
|
||||
|
||||
Casey (he/him). Pixel 3, mainline Arch. Ani + Aster = Annie.
|
||||
Souveraine = an OS, an agent substrate, and an agent.
|
||||
|
||||
## Constraints (non-negotiable, learned the hard way this session)
|
||||
|
||||
- **No GNOME or KDE library dependencies.** This rules out EDS/libebook
|
||||
(Evolution stack) and Akonadi/KPeople. EDS was investigated at length and
|
||||
**rejected**. Do not resurrect it.
|
||||
- The only thing EDS ever offered was "Chatty already reads it." Chatty is
|
||||
Casey's fork — teach Chatty to read the new store instead. The EDS book on
|
||||
the phone is **empty (0 contacts)**. There is no sunk value there.
|
||||
- Cite `file:line` or say "not in the code / undecided." Never state a guess
|
||||
as established fact.
|
||||
|
||||
## What is actually built and installed on the phone
|
||||
|
||||
**qtpim dev — DONE, working.**
|
||||
|
||||
- Source: `~/Projects/qtpim`, branch `dev` (the only branch), HEAD `d03b5f6`.
|
||||
Actively maintained upstream by Axel Spoerl (Qt Company), commits through
|
||||
July 2026.
|
||||
- Built native aarch64, 268/268 targets clean.
|
||||
- Installed to `/usr` (NOT `/usr/local` — Qt6's prefix is `/usr/lib`; a first
|
||||
attempt landed in `/usr/local` and was removed; only one copy exists now).
|
||||
- Artifacts: `libQt6Contacts.so.6.12.0`, `libQt6Versit.so.6.12.0` (vCard),
|
||||
`libQt6VersitOrganizer`, QML modules `QtContacts` + `QtOrganizer`,
|
||||
`Qt6Contacts.pc` (6.12.0).
|
||||
- QML plugin links clean: `libdeclarative_contactsplugin.so` resolves
|
||||
`libQt6Contacts.so.6` with no missing symbols.
|
||||
|
||||
Reconfigure line (idempotent):
|
||||
|
||||
```sh
|
||||
cd ~/Projects/qtpim/_build
|
||||
/usr/lib/qt6/bin/qt-cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DQT_NO_PACKAGE_VERSION_CHECK=TRUE \
|
||||
-DQT_NO_PACKAGE_VERSION_INCOMPATIBLE_WARNING=TRUE \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_STAGING_PREFIX=/usr
|
||||
ninja && sudo ninja install && sudo ldconfig
|
||||
```
|
||||
|
||||
### Caveats on the qtpim install
|
||||
|
||||
- qtpim dev's `CMakeLists.txt:14` does `find_package(Qt6 6.12.0)`. The phone
|
||||
has Qt **6.11.1**. `QT_NO_PACKAGE_VERSION_CHECK=TRUE` forces past that gate.
|
||||
It compiles and links clean, so the gate is bureaucratic, not a real API
|
||||
break — but libs are versioned `6.12.0` against a 6.11.1 Qt. Works; not
|
||||
blessed. Clean alternative: `qt6-base-git` (AUR) for a real 6.12 dev Qt,
|
||||
then no override needed. Big native build on a Pixel 3.
|
||||
- `/usr/lib/qt6/bin/qt-configure-module` is **broken on Arch** — it looks for
|
||||
`qt-cmake-private` at `/usr/lib/qt6/lib/qt6/`, actual location is
|
||||
`/usr/lib/qt6/`. Use `qt-cmake` directly (above).
|
||||
- This is a manual `ninja install`, not a package. A Qt update can stomp it.
|
||||
No PKGBUILD tracks it. (Casey's standing rule: untracked hand-installed
|
||||
binaries are a known gap — worth a PKGBUILD eventually.)
|
||||
|
||||
## The actual blocker: there is no persistent backend
|
||||
|
||||
Only backend installed is `libqtcontacts_memory.so` — **RAM only, gone on
|
||||
restart**. qtpim upstream ships only `memory` and `serviceactionmanager`.
|
||||
|
||||
So Chatty cannot swap to qtpim yet. There is nothing persistent to swap *to*.
|
||||
The store must exist first.
|
||||
|
||||
## qtcontacts-sqlite attempt — BLOCKED, decision needed
|
||||
|
||||
`~/Projects/qtcontacts-sqlite`, cloned from `sailfishos/qtcontacts-sqlite`,
|
||||
HEAD `2939cf2`. Active upstream (May 2026). qmake `.pro`, Qt5-era, RPM
|
||||
packaging.
|
||||
|
||||
**Patched so far** (all reversible; `.orig` files sit next to each):
|
||||
|
||||
- `config.pri` → `config.pri.orig`: `PKGCONFIG += Qt5Contacts` → `Qt6Contacts`,
|
||||
`c++11` → `c++17`, db path `qtcontacts-sqlite-qt5/` → `-qt6/`.
|
||||
- `src/engine/contactstransientstore.cpp` → `.orig`: `QMutex::Recursive` →
|
||||
`QRecursiveMutex` (Qt6 split the class), added `#include <QRecursiveMutex>`,
|
||||
`QMutexLocker` → `QMutexLocker<QRecursiveMutex>` at 3 sites.
|
||||
- `src/engine/contactsengine.h` + `.cpp` → `.orig`: dropped `const` from
|
||||
`collection()` / `collections()` — Qt6's `QContactManagerEngine`
|
||||
(`/usr/include/qt6/QtContacts/qcontactmanagerengine.h:77-78`) declares them
|
||||
non-const.
|
||||
|
||||
Those fixes **worked** — those errors are gone. `qmake6` configures, exit 0.
|
||||
|
||||
**Remaining errors, two kinds:**
|
||||
|
||||
Mechanical Qt5→Qt6 drift (known fixes, tedious):
|
||||
- `contactsengine.cpp:948,950,951,953,958` — `QMutexLocker` needs template args
|
||||
(`JobThread::MutexUnlocker` / `m_locker`)
|
||||
- `contactsengine.cpp:1244,1273` — deleted `QMutexLocker` copy ctor
|
||||
- `contactsengine.cpp:1297` — `qRegisterMetaTypeStreamOperators` removed in Qt6
|
||||
|
||||
**The real problem — `QContact::IgnoreAccessConstraints`:**
|
||||
|
||||
Verified by grep:
|
||||
- NOT in `/usr/include/qt6/QtContacts/` (upstream Qt6 headers)
|
||||
- NOT in `~/Projects/qtpim/src/contacts/*.h` (upstream dev source)
|
||||
- **IS** in `sailfishos/qtpim` fork at `src/contacts/qcontact.h:59`
|
||||
|
||||
So qtcontacts-sqlite is **not** a Qt5 engine needing a port. It is written
|
||||
against *Sailfish's forked qtpim*, which carries API upstream never had.
|
||||
Call sites: `contactreader.cpp:764,1997`, `contactwriter.cpp:3473,3512,3558,
|
||||
3954,3967,3976,3984`, `contactsengine.cpp` — 8+ places.
|
||||
|
||||
Stubbing it out means silently changing access-constraint semantics in a
|
||||
contacts store. Not a safe sed.
|
||||
|
||||
## Three paths — Casey's call, not to be picked unilaterally
|
||||
|
||||
1. **Finish the mechanical fixes, then invent semantics for
|
||||
`IgnoreAccessConstraints` against upstream.** Cheap up front, quiet
|
||||
wrongness later. Not recommended.
|
||||
2. **Build Sailfish's qtpim fork instead of upstream dev.** qtcontacts-sqlite
|
||||
then compiles as designed. But Casey asked for *dev* specifically, and the
|
||||
Sailfish fork was last touched 2021. Trades dev for fork debt.
|
||||
3. **Write a sqlite engine against upstream dev.** Template is the in-tree
|
||||
`memory` backend: `~/Projects/qtpim/src/plugins/contacts/memory/`,
|
||||
**1,070 lines**, complete working reference. Only **2 pure virtuals** are
|
||||
mandatory (`managerName()`, `managerVersion()`); the other 33 virtuals have
|
||||
working defaults — implement save/remove/fetch, inherit the rest. More work
|
||||
up front, no fork debt, it's Casey's.
|
||||
|
||||
## Why qtpim over EDS (settled, don't relitigate)
|
||||
|
||||
| | qtpim | EDS |
|
||||
|---|---|---|
|
||||
| Shape | in-process library + plugin engine | D-Bus daemon, mandatory |
|
||||
| D-Bus in source | **zero** (verified by grep) | the entire architecture |
|
||||
| Backend | yours, swappable | Evolution's model |
|
||||
| QML | native module in-tree | none |
|
||||
| vCard | in-tree (`src/versit`) | yes |
|
||||
| Must implement | 2 methods | n/a — you adapt to it |
|
||||
|
||||
Precedent: UBports ships this exact stack today —
|
||||
`gitlab.com/ubports/development/core/lomiri-addressbook-app` (active
|
||||
2026-06-23) builds against `qtpim5-dev` + `qml-module-qtcontacts`. They pin a
|
||||
git snapshot (`qtpim5-dev (>= 5.0~git20171109~0bd985b)`) — that's what
|
||||
vendoring an unreleased module looks like. They also have a separate
|
||||
`lomiri-address-book-service` because *multiple* apps share one store. Chatty
|
||||
is a second reader, so that concern is real — but solve it when there are two
|
||||
readers; "both open the same sqlite store" may be enough. Don't pre-build it.
|
||||
|
||||
Note UBports/Sailfish use `qtcontacts-sqlite` (their own DB), **not** EDS.
|
||||
Nobody in that lineage uses EDS.
|
||||
|
||||
## Chatty state (unchanged this session — no code touched)
|
||||
|
||||
`~/Projects/Chatty`, design doc `PAF-chatty-identity-layer.md`.
|
||||
|
||||
- Steps 1–3 **built and working**: `persons` / `person_addresses` /
|
||||
`person_link_exceptions` tables at `HISTORY_VERSION 6`; `ChattyPerson` +
|
||||
`chatty_identity_*` API; `chatty_chat_get_priority_section()` re-keyed onto
|
||||
Person. Matrix rooms no longer auto-bucket as Known — the original bug.
|
||||
- `chatty-identity.c` currently forwards to Chatty's **private SQL tables**.
|
||||
That's the seam that would repoint at a qtpim store.
|
||||
- Binding seam to be aware of: Chatty is C/GObject, qtpim is C++/Qt. Not a
|
||||
blocker, not free.
|
||||
- The design doc still contains an **EDS pivot section — that is dead**.
|
||||
Superseded by this file. Steps 1–3 in it are still valid.
|
||||
- Still missing: any favoriting UI at all. That gap is what started this whole
|
||||
investigation ("what is the methodology to favorite someone?").
|
||||
|
||||
## Open question that was never answered
|
||||
|
||||
What are contacts *for* in Souveraine? An address book, or people Annie knows
|
||||
(stable referent across transports + git-backed memory)? That answer probably
|
||||
determines whether the store is a contacts DB at all. This was asked and never
|
||||
resolved — resolve it before writing an engine.
|
||||
575
docs/session-trust-map.html
Normal file
575
docs/session-trust-map.html
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
<title>Session Trust Architecture — Souveraine / Pixel 3</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f7f5f0;
|
||||
--bg-raised: #ffffff;
|
||||
--ink: #1c1f24;
|
||||
--ink-dim: #52585f;
|
||||
--ink-faint: #8a8f96;
|
||||
--line: #ddd8cd;
|
||||
--line-strong: #b8b2a4;
|
||||
--accent: #a8722f;
|
||||
--enforced: #3a7a8c;
|
||||
--gap: #b3453f;
|
||||
--partial: #b8862f;
|
||||
--done: #4b7c52;
|
||||
--code-bg: #eeeae0;
|
||||
--shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #14171b;
|
||||
--bg-raised: #1b1f24;
|
||||
--ink: #e8e6e1;
|
||||
--ink-dim: #a3a9ae;
|
||||
--ink-faint: #6b7178;
|
||||
--line: #2b2f35;
|
||||
--line-strong: #3a3f46;
|
||||
--accent: #d99a4f;
|
||||
--enforced: #6fb0c4;
|
||||
--gap: #d97a74;
|
||||
--partial: #d9a94f;
|
||||
--done: #7fb586;
|
||||
--code-bg: #20242a;
|
||||
--shadow: 0 1px 2px rgba(0,0,0,0.2), 0 4px 16px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #14171b;
|
||||
--bg-raised: #1b1f24;
|
||||
--ink: #e8e6e1;
|
||||
--ink-dim: #a3a9ae;
|
||||
--ink-faint: #6b7178;
|
||||
--line: #2b2f35;
|
||||
--line-strong: #3a3f46;
|
||||
--accent: #d99a4f;
|
||||
--enforced: #6fb0c4;
|
||||
--gap: #d97a74;
|
||||
--partial: #d9a94f;
|
||||
--done: #7fb586;
|
||||
--code-bg: #20242a;
|
||||
--shadow: 0 1px 2px rgba(0,0,0,0.2), 0 4px 16px rgba(0,0,0,0.25);
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 1.75rem 5rem;
|
||||
}
|
||||
|
||||
code, .mono, .path {
|
||||
font-family: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.path {
|
||||
background: var(--code-bg);
|
||||
border-radius: 4px;
|
||||
padding: 0.05em 0.4em;
|
||||
color: var(--ink-dim);
|
||||
}
|
||||
|
||||
header.top {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.9rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5rem;
|
||||
letter-spacing: -0.01em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.dek {
|
||||
color: var(--ink-dim);
|
||||
font-size: 1rem;
|
||||
max-width: 62ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
.meta-row b { color: var(--ink-dim); font-weight: 600; }
|
||||
|
||||
section { margin-bottom: 3rem; }
|
||||
|
||||
h2 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
h2 .num {
|
||||
color: var(--ink-faint);
|
||||
font-weight: 400;
|
||||
font-size: 0.85rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.callout {
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.callout p:first-child { margin-top: 0; }
|
||||
.callout p:last-child { margin-bottom: 0; }
|
||||
|
||||
/* summary strip */
|
||||
.strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--line);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.strip > div {
|
||||
background: var(--bg-raised);
|
||||
padding: 1.1rem 1.3rem;
|
||||
}
|
||||
.strip .label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--ink-faint);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.strip .value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.strip .value.enforced { color: var(--enforced); }
|
||||
.strip .value.gap { color: var(--gap); }
|
||||
.strip .value.partial { color: var(--partial); }
|
||||
|
||||
/* diagram container */
|
||||
.diagram-frame {
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.diagram-frame .mermaid { min-width: 900px; }
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 1.4rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink-dim);
|
||||
}
|
||||
.legend span.dot {
|
||||
display: inline-block;
|
||||
width: 9px; height: 9px;
|
||||
border-radius: 2px;
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
/* status table */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
.table-scroll { overflow-x: auto; border-radius: 8px; box-shadow: var(--shadow); }
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--ink-faint);
|
||||
font-weight: 600;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||
|
||||
.status-pill {
|
||||
display: inline-block;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
padding: 0.1rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-pill.enforced { background: color-mix(in srgb, var(--enforced) 18%, transparent); color: var(--enforced); }
|
||||
.status-pill.gap { background: color-mix(in srgb, var(--gap) 18%, transparent); color: var(--gap); }
|
||||
.status-pill.partial { background: color-mix(in srgb, var(--partial) 20%, transparent); color: var(--partial); }
|
||||
.status-pill.done { background: color-mix(in srgb, var(--done) 18%, transparent); color: var(--done); }
|
||||
|
||||
/* gaps grid */
|
||||
.gaps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.gap-card {
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 1.1rem 1.25rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.gap-card h3 {
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.gap-card p {
|
||||
margin: 0.4rem 0;
|
||||
color: var(--ink-dim);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.gap-card .sev {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
display: inline-block;
|
||||
background: var(--gap);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.decision-block {
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem 1.4rem;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.decision-block h3 { margin: 0 0 0.5rem; font-size: 1rem; }
|
||||
.decision-block .q { color: var(--ink-dim); font-size: 0.88rem; margin-bottom: 0.7rem; }
|
||||
.decision-block .options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 0.8rem;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
.opt {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 0.7rem 0.85rem;
|
||||
font-size: 0.83rem;
|
||||
}
|
||||
.opt .opt-title { font-weight: 600; margin-bottom: 0.3rem; }
|
||||
.opt .opt-title.rec::after {
|
||||
content: "recommended";
|
||||
font-weight: 500;
|
||||
font-size: 0.68rem;
|
||||
color: var(--done);
|
||||
margin-left: 0.5em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--line);
|
||||
font-size: 0.78rem;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
a { color: var(--enforced); }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
h1 { font-size: 1.5rem; }
|
||||
.wrap { padding: 1.5rem 1rem 3rem; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<header class="top">
|
||||
<div class="eyebrow">Architecture map · verified against source, 2026-07-15</div>
|
||||
<h1>Session Trust: Lockscreen, Idle, and the Session Manager</h1>
|
||||
<p class="dek">How SouveraineOS's QuickShell layer consumes logind, PAM, and Wayland session-lock across the laptop and the Pixel 3 — what's built, what's designed but unverified, and where the real gaps sit.</p>
|
||||
<div class="meta-row">
|
||||
<span><b>Primary source</b> <span class="path">souveraine/surfaces/quickshell/</span></span>
|
||||
<span><b>Branch</b> <span class="path">experimental/session-trust-phase1</span> @ <span class="path">6532572</span></span>
|
||||
<span><b>Deploys to</b> laptop + Pixel 3 blueline, identical config</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="strip">
|
||||
<div><div class="label">Core invariant</div><div class="value">screenLocked ≠ screenLockSecure</div></div>
|
||||
<div><div class="label">Rows enforced</div><div class="value enforced">19 of 23</div></div>
|
||||
<div><div class="label">Rows gapped</div><div class="value gap">2 wiring gaps</div></div>
|
||||
<div><div class="label">Unverified claims</div><div class="value partial">3 (see §4)</div></div>
|
||||
<div><div class="label">Phone-only landmine</div><div class="value gap">logind.conf power-key hack</div></div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2><span class="num">01</span>Why this exists</h2>
|
||||
<div class="callout">
|
||||
<p>SouveraineOS is not a new lockscreen — it's an orchestration layer that <em>consumes</em> logind, PAM, and Wayland's session-lock protocol rather than replacing them. The design rule that keeps this safe: <b>always route state through the protocol, never invent a shadow copy.</b> Two form factors (x86 laptop, ARM Pixel 3) run the exact same QuickShell config via <span class="path">deploy.sh --phone</span>.</p>
|
||||
<p>The reason a map is worth building now: this is genuinely two different codebases doing overlapping-sounding things. <span class="path">souveraine/src/core/session/</span> (Rust) is Ani's <em>conversation</em> session — turns, SSE streams, subagents. <span class="path">souveraine/surfaces/quickshell/</span> (QML) is the Linux <em>login session</em> — lock, suspend, idle. They share a name and nothing else. All of what follows is the QML side.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="num">02</span>The architecture</h2>
|
||||
<div class="diagram-frame">
|
||||
<pre class="mermaid">
|
||||
flowchart TB
|
||||
subgraph AUTH["Linux/Wayland authorities — consumed, never replaced"]
|
||||
direction LR
|
||||
logind["logind\nCan*, PrepareForSleep,\nSession.Lock signal"]
|
||||
wlock["WlSessionLock\n.secure (compositor ack)"]
|
||||
pam["PAM\nlogin + step-up conversation"]
|
||||
polkit["polkit\none agent per session"]
|
||||
end
|
||||
|
||||
subgraph INGRESS["services/SessionEvents.qml — logind ears"]
|
||||
direction TB
|
||||
resolver["session path resolver\nloginctl show-session"]
|
||||
sleepmon["gdbus monitor: PrepareForSleep"]
|
||||
lockmon["gdbus monitor: Session.Lock\n(echo-guarded vs our own lock)"]
|
||||
delayinh["delay-mode sleep inhibitor\nsystemd-inhibit --mode=delay\nheld from shell startup"]
|
||||
end
|
||||
|
||||
subgraph CORE["Core state — GlobalStates.qml"]
|
||||
direction LR
|
||||
reqstate["screenLocked\n(shell's REQUEST)"]
|
||||
secstate["screenLockSecure\n(compositor's ACK)"]
|
||||
end
|
||||
|
||||
subgraph ARBITER["modules/common/functions/Session.qml — the arbiter"]
|
||||
direction TB
|
||||
caps["capability probe\nbusctl Can* via awk\n(not sed — broke on phone)"]
|
||||
verbs["verbs: lock/suspend/hibernate/\npoweroff/reboot/logout\nrun via Process, exit code logged"]
|
||||
inhibitreg["inhibit(what, reason) → cookie\nstate() lists every holder"]
|
||||
end
|
||||
|
||||
subgraph IDLE["services/Idle.qml + IdleCoordinator.qml"]
|
||||
direction TB
|
||||
idleinh["IdleInhibitor\n(idle-inhibit-unstable-v1)"]
|
||||
idlestage["stage machine:\nDim → Lock → Suspending → Asleep → Waking"]
|
||||
end
|
||||
|
||||
subgraph TIERS["Capability tiers — one policy for lock, media, agent"]
|
||||
direction TB
|
||||
ambient["ambient — no auth\nclock, weather, MPRIS transport"]
|
||||
personal["personal — !screenLocked && !screenLockSecure\nmemory, calendar, fleet state, history"]
|
||||
stepup["stepUp — fresh PAM re-auth\nsend, delete, push, payment, physical"]
|
||||
end
|
||||
|
||||
subgraph STEPUP["services/StepUpAuth.qml"]
|
||||
direction TB
|
||||
grant["grant = {family, expires}\n5min TTL, 30s coarse expiry timer"]
|
||||
breakglass["breakGlass()\n60s TTL, one-time, logged"]
|
||||
end
|
||||
|
||||
subgraph AUDIT["services/SessionAudit.qml"]
|
||||
audlog["append-only JSONL\nQt.md5(prev) chain\n~/.local/share/souveraine/session-audit.jsonl\n(advisory, NOT crypto)"]
|
||||
end
|
||||
|
||||
subgraph AGENT["Agent surface — Ai.qml / Souveraine.qml"]
|
||||
direction TB
|
||||
redact["in-flight response redaction\non screenLocked mid-stream"]
|
||||
sendgate["Souveraine.send() returns 'step-up'\nwhen family lacks a grant"]
|
||||
end
|
||||
|
||||
logind -- "Session.Lock signal" --> lockmon
|
||||
logind -- "PrepareForSleep(true/false)" --> sleepmon
|
||||
lockmon -- "not an echo of our own lock" --> reqstate
|
||||
sleepmon --> delayinh
|
||||
delayinh -- "release only after" --> secstate
|
||||
wlock -- "WlSessionLock.secure fires" --> secstate
|
||||
reqstate -.->|"never conflated with"| secstate
|
||||
|
||||
caps -->|"probed once at startup"| verbs
|
||||
verbs --> logind
|
||||
verbs --> polkit
|
||||
reqstate --> ARBITER
|
||||
ARBITER --> CORE
|
||||
|
||||
idlestage --> reqstate
|
||||
idleinh --> inhibitreg
|
||||
|
||||
secstate ==> personal
|
||||
reqstate ==> personal
|
||||
pam --> stepup
|
||||
stepup --> grant
|
||||
grant --> stepup
|
||||
|
||||
personal --> sendgate
|
||||
stepup --> sendgate
|
||||
reqstate --> redact
|
||||
grant -. "revoked on lock" .-> reqstate
|
||||
|
||||
CORE --> AUDIT
|
||||
STEPUP --> AUDIT
|
||||
INGRESS --> AUDIT
|
||||
|
||||
classDef authority fill:#5b8fb0,stroke:#3a6b82,color:#fff
|
||||
classDef core fill:#d99a4f,stroke:#a8722f,color:#1c1f24
|
||||
classDef gapNode fill:#c15c5c,stroke:#8f3b36,color:#fff
|
||||
classDef done fill:#7fb586,stroke:#4b7c52,color:#1c1f24
|
||||
|
||||
class logind,wlock,pam,polkit authority
|
||||
class reqstate,secstate core
|
||||
class sendgate,grant done
|
||||
</pre>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span><span class="dot" style="background:#5b8fb0"></span>External Linux authority (query/consume only)</span>
|
||||
<span><span class="dot" style="background:#d99a4f"></span>Core dual-state invariant</span>
|
||||
<span><span class="dot" style="background:#7fb586"></span>Enforced & verified</span>
|
||||
<span><span class="dot" style="background:#c15c5c"></span>Gap or unverified</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="num">03</span>The one invariant that governs everything</h2>
|
||||
<div class="callout">
|
||||
<p><code>GlobalStates.screenLocked</code> is the shell <b>asking</b> for the lock surface. <code>GlobalStates.screenLockSecure</code> mirrors <code>WlSessionLock.secure</code> — the compositor's <b>proof</b> that it actually happened. A failed or delayed Wayland lock is visible as <code>lockRequested: true, locked: false</code>. Every personal-data gate in the system reads <code>secure</code>, never <code>locked</code>. This is stated three separate times across the source docs (<span class="path">SESSION-TRUST-ARCHITECTURE.md</span>, <span class="path">TRUST-BOUNDARY-MATRIX.md</span>, the <span class="path">2026-07-14 DUMP</span>) — it's the load-bearing decision the whole model hangs from.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="num">04</span>Status matrix</h2>
|
||||
<p style="color:var(--ink-dim); font-size:0.85rem; margin-top:-0.5rem;">Sourced directly from <span class="path">TRUST-BOUNDARY-MATRIX.md</span> and the phase-1 dump's own verification checklist — not re-derived.</p>
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Operation</th><th>Min tier</th><th>Gate</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>session.lock()</td><td>ambient</td><td>always allowed</td><td><span class="status-pill enforced">enforced</span></td></tr>
|
||||
<tr><td>session.unlock()</td><td>stepUp</td><td>LockContext PIN/PAM only — IPC refuses by design</td><td><span class="status-pill enforced">enforced</span></td></tr>
|
||||
<tr><td>session.suspend/hibernate/poweroff/reboot()</td><td>ambient</td><td>capability probe (+ polkit for power/reboot)</td><td><span class="status-pill enforced">enforced</span></td></tr>
|
||||
<tr><td>session.inhibit("idle" / "sleep")</td><td>ambient</td><td>reason required, cookie tracked</td><td><span class="status-pill enforced">enforced</span></td></tr>
|
||||
<tr><td>Agent send w/ personal context</td><td>personal</td><td>!screenLocked && !screenLockSecure (live check)</td><td><span class="status-pill enforced">enforced</span></td></tr>
|
||||
<tr><td>In-flight agent response on lock</td><td>personal</td><td>Ai.qml redacts on screenLocked</td><td><span class="status-pill partial">re-verify</span> — redaction path only stopped crashing at the last commit of the session</td></tr>
|
||||
<tr><td>Sleep/suspend transition</td><td>ambient</td><td>delay inhibitor + WlSessionLock.secure</td><td><span class="status-pill partial">unverified</span> — delay budget (~5s) not yet timed on the Pixel 3</td></tr>
|
||||
<tr><td>lock() reaches locked:true only after secure</td><td>—</td><td>—</td><td><span class="status-pill partial">unverified</span> — needs timing pass</td></tr>
|
||||
<tr><td>Session audit trail</td><td>ambient</td><td>append-only JSONL, md5 hash chain</td><td><span class="status-pill enforced">enforced</span> <i>(advisory tamper-evidence only, not crypto)</i></td></tr>
|
||||
<tr><td>Agent delete / push operations</td><td>stepUp</td><td>StepUpAuth exists but per-op minTier metadata isn't wired</td><td><span class="status-pill gap">gap</span></td></tr>
|
||||
<tr><td>Agent physical-access operations</td><td>stepUp</td><td>same as above</td><td><span class="status-pill gap">gap</span></td></tr>
|
||||
<tr><td>Break-glass override</td><td>scoped</td><td>StepUpAuth.breakGlass — mandatory reason, 60s TTL, one-time</td><td><span class="status-pill done">done</span></td></tr>
|
||||
<tr><td>Native Wayland idle-notify on Pixel compositor</td><td>—</td><td>—</td><td><span class="status-pill partial">unverified</span> — hypridle is the stopgap until confirmed on both targets</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="num">05</span>Fundamental gaps — the actual decision points</h2>
|
||||
<div class="gaps-grid">
|
||||
<div class="gap-card">
|
||||
<h3><span class="sev"></span>Phone power-button hack predates and bypasses the new ingress</h3>
|
||||
<p>The Pixel 3's power button was fixed by setting <span class="path">HandlePowerKey=ignore</span> in <span class="path">/etc/systemd/logind.conf</span> (2026-07-06) plus a Hyprland no-op bind — because logind's default poweroff was killing modem state. That fix <b>lives only on the phone</b>, is not in any repo, and predates <span class="path">SessionEvents.qml</span>. Once the hold-rails kernel patch lands and DPMS/suspend is safe, the button needs rebinding to screen-off/suspend — <b>through the new logind ingress</b>, not another local hack layered on top.</p>
|
||||
</div>
|
||||
<div class="gap-card">
|
||||
<h3><span class="sev"></span>Two things are named "session" and share nothing</h3>
|
||||
<p><span class="path">souveraine/src/core/session/</span> (Rust, Ani's conversation/turn lifecycle) and <span class="path">souveraine/surfaces/quickshell/modules/common/functions/Session.qml</span> (Linux login session — lock/suspend/idle) are unrelated. This cost real time in this scan alone; worth a naming pass if confusion recurs (e.g. rename the QML one to <span class="path">LinuxSession</span> or similar internally — it already deploys as IPC target <span class="path">sessionctl</span> to avoid colliding with ii's own <span class="path">session</span>).</p>
|
||||
</div>
|
||||
<div class="gap-card">
|
||||
<h3><span class="sev"></span>stepUp tier is built but not load-bearing yet</h3>
|
||||
<p><span class="path">StepUpAuth.qml</span> is fully implemented — grants, TTL, break-glass, revocation on lock — but the per-operation <span class="path">minTier</span> metadata that would actually route agent delete/push/physical-access calls through it isn't wired. Right now those operations are ungated in practice even though the machinery to gate them exists.</p>
|
||||
</div>
|
||||
<div class="gap-card">
|
||||
<h3><span class="sev"></span>PAM step-up backend is polkit today, not PAM</h3>
|
||||
<p><span class="path">/etc/pam.d/souveraine-stepup</span> is intentionally not shipped (root-owned system config). Until it's installed and audited, <span class="path">StepUpAuth</span> falls through to <span class="path">pkcheck</span>/polkit at runtime. That's a materially different trust boundary than a dedicated PAM service — worth deciding explicitly whether polkit-as-stepup is acceptable long-term or just a bootstrap.</p>
|
||||
</div>
|
||||
<div class="gap-card">
|
||||
<h3><span class="sev"></span>Audit chain is advisory, not tamper-proof</h3>
|
||||
<p><span class="path">SessionAudit.qml</span> uses <span class="path">Qt.md5()</span> because QML has no SHA-256 primitive. The docs are explicit that this is "advisory tamper-evidence, not crypto" — fine for debugging "why did it lock," not sufficient if this audit trail is ever meant to survive an adversarial actor with local access.</p>
|
||||
</div>
|
||||
<div class="gap-card">
|
||||
<h3><span class="sev"></span>Trust matrix is scoped local-only, by design and by necessity</h3>
|
||||
<p>Every gated operation assumes a same-machine caller over the QuickShell IPC socket, owned by the single user. This is explicitly flagged in the source: "if a network-reachable caller is ever added, every row needs re-evaluation." Any federation/multi-machine work (per <span class="path">federation-fork-vision</span>) that lets a remote agent reach into session state would invalidate this entire matrix and needs its own pass, not an incremental patch.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="num">06</span>What's actually deployed vs. designed</h2>
|
||||
<div class="callout">
|
||||
<p>The live config at <span class="path">~/.config/quickshell/ii</span> (deployed, currently running) has a <b>simpler</b> GlobalStates/Idle than what's described above — no <code>screenLockSecure</code>, no session IPC surface, no SessionEvents.qml. The richer model lives in <span class="path">souveraine/surfaces/quickshell/</span> and is composed into a separate <span class="path">~/.config/quickshell/souveraine</span> config by <span class="path">deploy.sh</span>, which borrows untouched ii directories as whole-dir symlinks and overlays only the files Souveraine changes — ii's own tree stays pristine. Session-trust phase 1 landed same-day (2026-07-14) on <span class="path">experimental/session-trust-phase1</span>; verification is partial (see §4). Before treating any "enforced" row above as a hard security guarantee, confirm which config is actually running with <code>qs -c souveraine</code> vs <code>qs -c ii</code>.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2><span class="num">07</span>File index</h2>
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<thead><tr><th>Concern</th><th>File</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Design contract</td><td><span class="path">souveraine/surfaces/quickshell/SESSION-TRUST-ARCHITECTURE.md</span></td></tr>
|
||||
<tr><td>Per-operation gating matrix</td><td><span class="path">souveraine/surfaces/quickshell/TRUST-BOUNDARY-MATRIX.md</span></td></tr>
|
||||
<tr><td>Same-day work log + verification checklist</td><td><span class="path">souveraine/surfaces/quickshell/DUMP-session-trust-2026-07-14.md</span></td></tr>
|
||||
<tr><td>logind PrepareForSleep + Session.Lock ingress</td><td><span class="path">services/SessionEvents.qml</span></td></tr>
|
||||
<tr><td>Step-up PAM/polkit grants</td><td><span class="path">services/StepUpAuth.qml</span></td></tr>
|
||||
<tr><td>Hash-chained audit trail</td><td><span class="path">services/SessionAudit.qml</span></td></tr>
|
||||
<tr><td>Idle stage machine</td><td><span class="path">services/IdleCoordinator.qml</span>, <span class="path">services/Idle.qml</span></td></tr>
|
||||
<tr><td>Capability probe + verbs (the arbiter)</td><td><span class="path">modules/common/functions/Session.qml</span></td></tr>
|
||||
<tr><td>Dual lock-state singleton</td><td><span class="path">GlobalStates.qml</span> (screenLocked / screenLockSecure)</td></tr>
|
||||
<tr><td>Settings UI for idle/lock/dim timers</td><td><span class="path">modules/settings/LockConfig.qml</span>, <span class="path">IdleConfig.qml</span></td></tr>
|
||||
<tr><td>Deploy/compose script (ii borrow + overlay)</td><td><span class="path">surfaces/quickshell/deploy.sh</span></td></tr>
|
||||
<tr><td>Phone-only power-button fix (not in any repo)</td><td><span class="path">/etc/systemd/logind.conf</span> on-device, see <span class="path">Pixel3Arch/docs/phone-shell-ux.md</span> §"Power button fix"</td></tr>
|
||||
<tr><td>Unrelated: Ani's conversation/turn session (Rust)</td><td><span class="path">souveraine/src/core/session/mod.rs</span> — not part of this map</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Built from direct source reads on 2026-07-15: SESSION-TRUST-ARCHITECTURE.md, TRUST-BOUNDARY-MATRIX.md, DUMP-session-trust-2026-07-14.md, SessionEvents.qml, StepUpAuth.qml, Session.qml (both the QML arbiter and confirmation it differs from the Rust core/session), GlobalStates.qml, Idle.qml, and Pixel3Arch/docs/phone-shell-ux.md. No claim above goes beyond what those files state.
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
Loading…
Reference in a new issue