Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/SESSION-TRUST-ARCHITECTURE.md
Fimeg e31c3aaf62 quickshell: first-party lock/nav/session layer, retire the pill
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.
2026-07-14 20:00:57 -04:00

5.5 KiB

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 only. It is intentionally wrong to accept sleep, shutdown, logout, or user-switch until each owns a real mechanism. The eventual contract is:

Kind Backend
idle Native Wayland idle inhibitor once verified on both targets; until then the tested hypridle control path.
sleep A live logind/systemd inhibitor FD or systemd-inhibit process, held for the cookie lifetime.
logout, user-switch Shell policy checks at those operations, not fake logind inhibitors.

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:

  1. 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. Hold one delay-mode sleep inhibitor from shell startup.
  3. On logind PrepareForSleep(true), request the Wayland lock and wait for WlSessionLock.secure; release the delay inhibitor only then.
  4. 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.

Do not ship that PAM service from the user 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.

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.