LockedHint was never set on this device, so sessiond's `locked` (which
comes from logind per doctrine §4) was permanently false, request_blank()
timed out LOCK_ACK_BUDGET on every blank, and the panel went dark on a
session nobody could confirm was locked. LOCK-DPMS-LESSONS §1 held in the
code and not on the glass — every blank recorded blank-without-lock.
Three faults, measured 2026-08-02:
- Session.qml wrote the hint through /session/auto, which is the caller's
own session. The shell is not in the session that owns the seat:
viewtop in logind 66 (seat0/tty1), qs in 70. Written to one session,
read from another.
- lockhint.rs resolved via User.Display, which an ssh login is enough to
point at a seatless remote session — and which then went stale when
that session ended. Both sides now ask seat0 for its active session.
- The report was dropped outright. hasLoginctl starts false and only
flips when a Process probe returns; secure=true landed 30s earlier, so
the one edge that mattered hit the guard and was never retried, because
the shell locks once at boot and never changes again. Fourth
edge-vs-level bug after locked_ack, ChargeRate and bootBloomActive.
Verified on hardware: LockedHint now goes yes ~2s after a power tap.
Session.qml and Lock.qml both registered target 'session'; quickshell keeps
whichever loads first and drops the other. Lock.qml's handler is a strict
superset, so the duplicate goes.
loginctl manages sessions/users/seats only — `loginctl poweroff` exits 1
"Unknown command verb". Preferring it silently broke poweroff/reboot/
suspend/hibernate from every shell surface. Use systemctl; fall back to
loginctl only on elogind systems where it does carry power verbs. The
logind D-Bus capability probe (CanPowerOff etc.) is unchanged — logind
owns policy, systemctl carries out the action.
Session.lock() notified logind unconditionally; hypridle's lock_cmd fires
on logind's Lock signal, so every lock echoed back through logind ->
hypridle -> lock() forever. Observed: ~450 lock requests/min for 7 minutes,
re-locking the screen seconds after each unlock. Notify only on the
unlocked->locked edge.
sessiond gave up waiting for the compositor lock ack after one dispatch —
at boot Hyprland defers the ack while the splash holds DRM, so the initial
lock silently failed (journal: 'never acknowledged', 0.7s in) and boot ran
on the legacy lock path. Wait up to 60s for locked/finished.
Phase 1 of the SouveraineOS homogenization — the three highest-priority
gaps from the session trust architecture:
New files:
- services/SessionEvents.qml — logind event ingress (PrepareForSleep,
session Lock signal) via gdbus monitor; delay-mode sleep inhibitor
held from startup, released only after WlSessionLock.secure
- services/StepUpAuth.qml — step-up PAM authentication with short-lived
in-memory grants per action family; revokes on lock/timeout/failure
- TRUST-BOUNDARY-MATRIX.md — every session operation classified by
caller type, auth gate, and enforcement status
Modified:
- IdleCoordinator: extended with Suspending/Asleep/Waking states for
the sleep lifecycle; wires to SessionEvents when present
- Session.qml: added sleep inhibitor kind, stepUp/sleepInhibitor
state in IPC projection, boot-time IPC audit log
- Config.qml: added lock.stepUp config section (enabled, grantTtlMs)
- LockConfig.qml: added step-up authentication settings section
- deploy.sh: added SessionEvents and StepUpAuth to manifest
- SESSION-TRUST-ARCHITECTURE.md: marked phase 1 items as done
- REFERENCE-EXTRACTION.md: marked build order items as done
Grows Souveraine's own surfaces on top of the borrowed ii shell and drops
the separate pill shell in favor of one integrated navigation rail.
Session arbiter (functions/Session.qml): probe logind's Can* methods over
busctl instead of guessing from installed binaries -- the answer carries the
polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na
and refuses honestly rather than firing a verb that no-ops. Verbs run through
a Process that logs exit codes and tracks lastAction; refusals log too. The
busctl output is parsed with awk, not a sed regex buried under four escaping
layers -- the sed version returned nothing on the phone and left every
capability stuck at "unknown" (invisible on the laptop, where timing masked
it). Every structured result is JSON-over-string; quickshell maps a var
return to void.
Lock trust: screenLocked (the shell's lock request) is now distinct from
screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement,
mirrored from LockScreen). Cards that disclose personal data gate on secure,
not on a button press. LockContentPolicy centralizes the ambient/personal/
step-up tiers so no card grows its own private rule.
New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost,
SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one
staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off
until the native Wayland idle-notify is verified on the Pixel compositor;
hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for
phone-vs-laptop display shapes.
Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig
and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary
shell, no qsConfig flip.
Verified on the phone: session.* reports challenge/na correctly, hibernate
and unlock refuse, inhibit round-trips with its reason.
Fork ii's Session singleton and add a sessionctl.* surface.
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 -- src/io/ipc.cpp, "void and var get mixed by qml
engine". dock.*, shell.* and apps.* were all declared `: var`, so they
registered as `(): void` and returned nothing at all. The {ok, reason}
contract has never once reached a caller. All of them now return JSON as a
string, which is what actually crosses the socket.
Session: upstream fires `systemctl X || loginctl X` detached and throws the
exit code away. Fine on a desktop with someone at the keyboard, not fine on
a phone where the shell is the session manager and a verb that silently does
nothing leaves you believing the machine is suspending when it isn't. So:
probe loginctl/systemctl/hibernate once instead of assuming, run verbs
through a Process that logs the exit code, and refuse honestly when the
machine can't do the thing (the phone has no swap -- hibernate now says so
instead of no-opping). Every upstream verb keeps its name and call sites.
Inhibits carry a mandatory reason and get a cookie; state() lists who is
holding the machine awake and why. "Why didn't it sleep" is now answerable.
unlock() is refused by design -- the lock is the credential gate, so no IPC
caller routes around the PIN pad.
Named sessionctl, not session: ii's SessionScreen already owns "session",
and quickshell drops duplicate targets silently rather than erroring.
Idle: drop the 2>/dev/null and run hypridle through a Process, so a unit
that fails to come back is a log line instead of a flat battery.
Verified on the laptop: inhibit stops hypridle, uninhibit brings it back.