Watch
1
0
Fork
You've already forked souveraine
0
Commit graph souveraine/surfaces/quickshell/services
Author SHA1 Message Date
Fimeg
0e7dec53d1 the app scales into multitasking; pill has two destinations 2026-08-05 16:34:51 -04:00
Fimeg
277a0d2b32 dock lives on zone one; short swipe multitasks, big swipe goes home 2026-08-05 16:17:47 -04:00
Fimeg
c3f998f17a move is a mode: grab, drag, done 2026-08-05 15:35:40 -04:00
Fimeg
b0688bc30b split chooser, home reaches the compositor, idle-inhibit comment was backwards 2026-08-05 14:52:48 -04:00
Fimeg
bf8bc013ef three-finger tap opens a window sheet, not the overview 2026-08-05 14:12:22 -04:00
Fimeg
3aea8398a9 shell: intent wins the settle window, and a swap is a request for a keyboard
Two gaps in the owner-change handling, both found on device.

The push at adopt time can lose a race. osk-switch sleeps 0.3s before
its deliberate SetVisible(true), and stevia self-shows once it has
finished coming up - both land after the shell has spoken. Measured:
adopt+hide 07:37:51, Visible=true 07:37:52, leaving oskOpen=false with a
keyboard on the screen. Re-assert when the settle window closes so the
last speaker is not the winner.

And the oskSwap gesture never recorded that a swap means the user wants
a keyboard, so osk-switch's re-show and the shell's stale false were
racing with nothing to arbitrate them. Declare the intent first.
2026-08-05 07:41:02 -04:00
Fimeg
870d88210b shell: wait longer for shell_ready than sessiond takes to answer it
`shell_ready` blocks in the daemon for up to 5s waiting for its
lock-session thread to drop its Wayland connection, because the
compositor refuses a second locker while the first is alive
(server.rs `wait_timeout_while`). The shell's timer was 3s, so it gave
up on a handshake sessiond was still answering, assumed locked, and
asked for a lock that had not been released — straight into TASK-48's
"Tried to show lockscreen surfaces without active lock".

Under Hyprland the release usually lands in milliseconds and the race is
won. Measured against viewtop on blueline today it loses every time: the
shell crash-looped every 11 seconds and the session never came up.
Same latent bug, a compositor that exposes it.

7s, so the daemon's own deadline expires first. Its answer for that case
is a refusal (`lock session did not release in time`), and hearing a real
refusal beats inventing a verdict — "assuming locked" is the shell
holding state the authority owns.

The 5s retry timer only arms after this one fires, so nothing overlaps.
2026-08-02 11:15:44 -04:00
Fimeg
1c90d7f76a settings: the device state machine gets a surface
TASK-08(f)/TASK-19: state, panel, evidence, confidence, per-source health, the
sensors_degraded flag and the recent decision trail were legible only through
forensic.jsonl. DeviceEvidence gains a read path (polled only while watched);
the Device page renders it. Readout only — the confidence gates are still
computed and never branched on, so controls over them would lie.
2026-07-31 13:11:43 -04:00
Fimeg
e4e659483b fix: keep the conversation when the agent list is re-polled
selectAgent cleared conversationId even when the agent hadn't changed, and the
60s inventory poll re-selects the persisted agent forever — so any message sent
a minute after the last one opened a fresh conversation. 27 of them yesterday,
four messages each. An empty curl body no longer counts as "no conversations".
2026-07-31 12:50:24 -04:00
Fimeg
0ef239515f surface: per-agent TTS voice + turn clock + reply-handler fix
Speech resolves voice per active agent (agents[].voice_id from the list),
with the system voice as fallback — two agents on a host need not match.
Souveraine.qml carries voice_id into the agents map and adds a turn clock
(turnStartedAt/turnElapsedMs) + a 60s agent-inventory refresh. Ai.qml
declares pendingFilePath and imports qs.modules.common.functions so
StringUtils.ttsClean no longer aborts the stream-finished handler.
2026-07-30 09:18:56 -04:00
Fimeg
5a643d7059 Selection: fix untappable chip and the chip that never cleared
Two bugs from the first on-device run.

The card sized itself from a layout it also centered, which resolved to zero, so
mask claimed a zero-size input region: the chip painted but no tap landed. Card
now sizes from the layout's implicit size and the background is a plain Item
child, not layout-managed.

wl-paste --watch replays the existing selection on start, so every watcher
restart (every unlock) resurrected a stale selection that could never be cleared
because the primary buffer never changed again. The first emission after a start
now only sets the baseline, and a dismissed selection is remembered so a re-read
cannot revive it.
2026-07-29 18:05:49 -04:00
Fimeg
808f06358e Selection: import qs for GlobalStates
Deployed and verified on blueline: watcher starts, a selection settles with an
anchor, and the lock gate kills the watcher.
2026-07-29 17:54:39 -04:00
Fimeg
0bebc779c6 Selection menu surface: chip that expands to actions (TASK-18)
Overlay layer following DialHost, keyboardFocus None throughout, and an input
mask limited to the card so taps elsewhere reach the app underneath. Anchors to
the pointer hint above the touch point, clamped on screen.

Chip shows a character count, never a preview: the surface floats over the app
that owns the selection and the content may be a password, so it stays ambient
and discloses nothing. Read Aloud is live via Speech; agent and reference
actions render with the reason they cannot act yet.

Adds DeviceEvidence, reporting input to the state machine's existing
Request::Input with an intent label, so a new input surface is not another
isolated actor per DEVICE-STATE-MACHINE 1. Opt-in: nothing loads or watches
until Config.options.selection.enable.
2026-07-29 17:47:20 -04:00
Fimeg
147999a918 Selection service: compositor-wide primary selection for TASK-18
Verified on blueline that Hyprland advertises primary-selection plus both
data-control managers, so wl-paste --primary --watch sees every selection with
no per-app hooks. Settles TASK-18's selection-detection question as
compositor-level; viewtop is not needed.

The protocol gives no selection rectangle, so anchor is the pointer position at
selection time. Opt-in by default: the watcher observes every highlight on the
device, including passwords, so it is killed while locked and selection text is
never persisted, logged, or trailed.
2026-07-29 17:25:34 -04:00
Fimeg
45fbbea400 shell: "already registered" is a transient, not a verdict
A scene reload re-runs SessiondBridge while the outgoing connection is still
open, so the reload's shell_ready races its own predecessor's EOF and sessiond
refuses it — correctly, since the lease is genuinely held. The bug was the
shell then giving up for good: it read the refusal as "another shell owns the
lease, we are not the authority", cleared nothing, and left registerRetry
idle. A second later that EOF landed, shell_alive dropped, and sessiond spent
the rest of the session believing there was no shell — while a live one sat on
a connected socket.

Observed 2026-07-29: refused 09:14:35, predecessor closed 09:16:36, and
`{"op":"status"}` reported shell_alive=false for 90 minutes afterwards.

Now the refusal sets needsRegistration and lets the 5s retry run. If the lease
really is another live shell's, each retry is refused again for free — and we
take it the moment that shell dies. Verified on hardware: retrying (1) ->
registered -> locked_ack sent, sessiond back to phase=released.
2026-07-29 10:43:07 -04:00
Fimeg
de64480ba2 gestures: osk-switch is packaged now, not a /usr/local stray 2026-07-29 09:14:22 -04:00
Fimeg
5965c64781 shell: recover the phone-only edits into the repo
~/souveraine-surfaces/quickshell on the phone is not a git repo and was the
only copy of five files. Brought back verbatim:

- Gestures.qml, SystemGestureRail.qml: TASK-38 osk-swap detent (DUMP §5, §6)
- OnScreenKeyboard.qml: showOsk asks the bus instead of pgrep+sleep 1
- DockAppButton.qml, DockStack.qml: suffix-tolerant AppSearch.resolveEntry

resolveEntry itself had been added to the phone's live ~/.config/quickshell/ii
tree, which deploy.sh rsyncs from ii-base — the next deploy would have deleted
it and left the two dock callers referring to nothing. It lands in ii-base here.
2026-07-29 08:27:27 -04:00
Fimeg
68b6025226 shell: fail closed when the session authority cannot be reached
Every failure path in shellReady answered cb(false) — no lock owed — so a
socket that was not up, a duplicate handshake, or a 3s timeout all concluded
the session was open. The timeout logged 'proceeding without sessiond' and left
the phone unlocked at exactly the moment the authority was unreachable, while
the daemon side of the same contract retakes the lock on heartbeat EOF
'whether or not the session was locked at the time. Fail closed.'

Not knowing the session is locked is not knowing it is not. All of those now
assume locked. Refusals still answer false only for 'already registered', which
means another shell holds the lease and this process is not the authority.

A timed-out handshake is also retried instead of abandoned: a shell that merely
started while sessiond was restarting stayed unregistered for its whole life,
so sessiond saw no heartbeat and raised its fallback surface over ours.

SessionEvents likewise retries session-path resolution rather than disabling
lock-signal monitoring for the life of the process, and logs the gap as an
error while it is open.
2026-07-27 21:38:18 -04:00
Fimeg
deda4a7b6b shell: send locked_ack on re-registration when the surface is already secure
onScreenLockSecureChanged is an edge. After a sessiond restart our lock surface
is already secure, so the edge is in the past and the ack owed by the new
handoff was never sent — sessiond logged 'shell never confirmed its lock after
handoff' and retook the lock 15s later.
2026-07-27 21:30:38 -04:00
Fimeg
4ed8f3dd74 shell: resolve logind session via User.Display, re-register after sessiond restart
SessionEvents asked loginctl for -p ObjectPath, which is not a property — it
returned empty on every boot, so external lock signals were never monitored and
the shell had no unlock ingress. Resolve the graphical session the way
lockhint.rs does; not GetSession(auto) (this shell is a session-less user unit)
and not the /session/auto alias (PropertiesChanged only fires on the concrete
path).

SessiondBridge read root.registered to decide whether to re-register on
reconnect, but the disconnect branch had already cleared it, so the shell never
re-registered after a sessiond restart. Latch it.
2026-07-27 21:21:26 -04:00
Fimeg
002a1a7b08 gestures: consumer + routing table, wired to the dial 2026-07-26 21:01:11 -04:00
Fimeg
bef6e6a653 dial: radial picker, haptic detents, dial IPC 2026-07-26 20:36:07 -04:00
Fimeg
2b69957f18 lock: surface charge rate, and stop counting down while plugged in
UPower's ChargeType (fork, read off the charger) reaches the lock line as
fast/slow charging. Discharging with the cable in is the charger resting
between top-ups, not a drain — it now reads Charged/Plugged in.
2026-07-26 16:56:39 -04:00
Fimeg
4f113ad62d shell: the dim is a grace before the lock, not a second absolute timer 2026-07-26 15:09:17 -04:00
Fimeg
24ce6d382b shell: execute authority directives; Settings reads sessiond
SessiondBridge: handle pushed {"directive":"lock"} on the heartbeat line and
raise the lock surface. Unknown directives are console.error — a silent drop
leaves the daemon blanking unlocked when its budget expires.

SessiondPolicy: new singleton, reads/writes DeviceStatePolicy over its own
short-lived connection (not the bridge socket — that EOF is shell-death
detection).

IdleConfig: new "Lock screen (device authority)" section bound to it. SetPolicy
had zero callers, so the old spinboxes moved a JSON file the daemon never read.
Says so in the error colour when sessiond is unreachable.

New file needs a deploy.sh manifest line or the services qmldir fails whole.
2026-07-25 18:53:19 -04:00
Fimeg
15a32796ba sessiond: make lock-before-blank an invariant, not a timer coincidence
LOCK-DPMS-LESSONS §1 is "Ordering: lock, then off". It held only because
hypridle's 300s lock listener fired before its own 600s screen-off listener —
two numbers in a config file, not a guarantee. Anything that skipped the lock
(idle inhibitor, native coordinator off, dead shell) still met the blank, and
the panel went dark on an unlocked session with nothing said about it.

Every path to a dark panel now routes through request_blank():

- locked  -> Blank, unchanged, no added round trip
- unlocked -> Action::Lock, blank withheld, pending_blank deadline armed
- ack lands -> Blank
- budget expires -> Blank anyway + error-security in the forensic trail

The panel fails open because a lit unlocked phone in a pocket is worse than a
dark one (§1); the *claim* fails closed because doctrine §8 forbids pretending
the session locked. Input inside the ack window cancels the blank outright.

Action::Lock does not go through the executor table — the lock is not a shell
command. While a live shell owns steady state the directive goes down the
heartbeat connection and the shell raises its rich surface; with no shell alive
sessiond raises its own. Same relationship DEVICE-STATE-MACHINE §6 gives the
DPMS executor: the authority decides, the surface executes. A directive that
cannot be delivered takes the lock here rather than timing out into a dark
unlocked screen, and is recorded either way.

Policy gains lock_ack_budget (2s, refuses 0) and unlocked_blank_after (None by
default — the shell's IdleCoordinator owns the unlocked idle timer through
ext-idle-notify per doctrine §5, and a second one here would recreate the
competing-owner disease). Both are on the GetPolicy/SetPolicy seam so Settings
is a view over the owning daemon, per TASK-19.

Unknown directives are loud on the shell side: an older shell silently
dropping one would leave the daemon waiting out its budget and blanking
unlocked.

38 tests pass, 5 new covering the ordering.
2026-07-25 18:38:32 -04:00
Fimeg
b4b30b124d Unified device state machine + security hardening
sessiond:
- device_state.rs: 8-state unified device state machine with legal
  transition table, sensor evidence model (proximity/accel/light/touch),
  confidence scoring, cross-sensor disagreement detection, and forensic
  logging with full state snapshots at every decision point.
- protocol.rs: extended with DeviceState, SensorInput, and ForensicLog
  IPC requests. SensorSource/SensorValue types for proximity, accel,
  light, touch.
- server.rs: DeviceStateMachine integrated into Daemon struct. Handlers
  for DeviceState (full state query), SensorInput (sensor evidence +
  Observed transitions), ForensicLog (recent entries query).
- lock.rs: Failed attempt counter on PIN surface (advisory, PAM owns
  lockout policy). Rendered as red digit glyphs below PIN dots.
- draw.rs: Attempt counter rendering + test.
- auth.rs: PAM config docs reference.
- mod.rs: device_state module added.

shell:
- IdleCoordinator.qml: Legal transition table with runtime enforcement.
  setState() refuses illegal transitions with warning. returnActive()
  explicitly only allows Dimmed/Waking.
- GlobalStates.qml: Write authority comments on every property (// WRITER:).
- SessionAudit.qml: SHA-256 replaces MD5 for hash chain. Forensic event
  wiring (device-state-transition, device-error, sensor-input, wake-event).
  logDeviceError/logSensorInput/logWakeEvent functions for QML callers.

Design doc: SouveraineOS/docs/DEVICE-STATE-MACHINE.md (separate repo).

Tests: 22 passing (was 7). Full lifecycle test exercises Active → Dimmed →
Locked → Observed → DozeLight → DozeDeep → Suspending → Asleep → Locked
with 23 forensic entries.
2026-07-24 16:29:04 -04:00
Fimeg
0b13e2d347 fix: speech prefetch flag, agent identity on restore, workspace selector
Speech: prefetch() now sets synthProc._prefetch=true before voiceLookup
so the background synth doesn't auto-play. speak() skips voiceLookup
when the endpoint+voice are already resolved (faster repeat taps).

Ai: addMessage defaults model to Souveraine.currentAgentId so restored
messages (/resume, new conversations) show the agent icon/name instead
of a generic placeholder.

Session: create_with_messages_and_timestamps preserves persisted
created_at/updated_at through load_persisted so /resume sorts by
actual activity time, not server restart time.

TTS: ttsClean strips unclosed think blocks, fixes regex dotAll flag
for older Qt/V8 (phone), adds pronunciation map.

SubconsciousTicker: tap psychology icon to expand inline mini-log,
long-press to open the event panel overlay.

New: workspace-selector.sh + .desktop — wofi dmenu grid for quick
workspace 1-10 jump from the app drawer. Installed to phone via
deploy.sh symlink.
2026-07-23 16:47:00 -04:00
Fimeg
19fe6b1d0e shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00
Fimeg
132d8134f2 dock: add drag-to-reorder for pinned apps alongside existing combine-into-stack
Dragging a pinned app now shows an insertion gap indicator when hovering
over other pinned apps. Quick horizontal slide + release = reorder the
pinned apps array. Dwell (500ms) on a target still = combine into stack
(existing behavior). Both gestures share the same drag start and ghost.

TaskbarApps: reorderPinned() splice-moves within Config.options.dock.pinnedApps.
DockApps: shared dragSourceIndex/dragInsertIndex state, delegate passes
  parent.index as modelIndex to DockAppButton.
DockAppButton: insertion indicator (2px primary-color line), DropArea
  onEntered computes gap position based on drag direction, release handler
  commits reorder when no dwell fired.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 21:11:19 -04:00
Fimeg
9c27504009 quickshell: converge live trees without root replacement 2026-07-21 19:11:51 -04:00
Fimeg
b0a1304ba1 quickshell: pin shared ii base and phone overlay 2026-07-21 18:55:02 -04:00
Fimeg
cd2150542e settings: wallpaper download + purity toggle, repo-owned
Adds a Download-random-wallpaper button and a 3-way content filter
(SFW/Sketchy/NSFW) to our Wallpaper settings page, backed by a new
WallpaperDownload service and its own download script under
surfaces/quickshell/scripts/wallpaper/ — not ii's scripts/colors tree,
which is going away. Only the 'wallpapers apply' IPC target still comes
from ii and moves with it when the base is vendored.

Downloads are named by wallhaven id so the library accumulates instead of
overwriting a single wallhaven_wallpaper.<ext>. Purity is read from and
written to background.wallhaven.purity in config.json (not in the upstream
Config.options schema). Anchors the root scripts/ gitignore to / so shipped
surface scripts are tracked.
2026-07-21 17:30:32 -04:00
Fimeg
db6d732e32 shell: regenerate qmldir manifests from the composed tree
The repo qmldirs listed only repo-owned types; composed dirs also carry
ii-borrowed types (ReloadPopup, PanelLoader, Bar, ...) which a qmldir
present in the dir must list or they stop resolving — broke the phone
shell on deploy. Regenerated every deployed qmldir from the composed
config on-device (capitalized .qml -> type line, pragma Singleton ->
singleton line) and pulled back. Shell verified active, zero load
errors.
2026-07-21 13:36:39 -04:00
Fimeg
acc1161e71 shell: Speech service + sidebar Speak button (Read Aloud seam)
Speech.qml singleton: speak(text) reads the who->voice mapping live
from souveraine /v1/config (VoiceConfig tts_url + voice_id), POSTs
VibeVoice /audio/speech ({input, voice, model} -> mp3, same contract
as core/voice/client.rs) and plays via mpv/ffplay. The shell picks no
voice of its own. speech.tts.enable is the kill switch (button hidden
when off); speech.tts.endpoint overrides the mapped URL. Sidebar gains
a volume_up/stop toggle beside send that reads the last visible
assistant reply, skipping lock-redacted content.
2026-07-21 13:28:47 -04:00
Fimeg
271e4f2cc2 shell: add missing qmldir manifests + rail OSK layer fix
19 dirs had .qml but no qmldir -> quickshell config load failed
(File not found @ shell.qml[-1:-1]). Corrected singleton decls in
3 existing qmldir. SystemGestureRail: pill->WlrLayer.Top, oskLift
200, drop oskOpen hide.
2026-07-21 13:01:58 -04:00
Fimeg
ad5be0a661 crash reporter: surface crashes.log, failed units, coredumps through the notify pipe 2026-07-21 08:40:00 -04:00
Fimeg
90077bb1ea lock: notifications card + NotifyEvents fan-out seam, battery fraction fix 2026-07-21 08:17:54 -04:00
Fimeg
7a052d77b0 task docs: lock primitives, settings authority, native audio privacy 2026-07-19 22:13:40 -04:00
Fimeg
dfd494c5f1 quickshell: complete native PulseAudio source support 2026-07-18 15:05:17 -04:00
Fimeg
9254333fcc sessiond bridge: use connectionStateChanged, fix lock card imports
The shell refused to load on the phone — onSocketConnected/
onSocketDisconnected handler-slots aren't reliably attachable across
quickshell builds, so the whole singleton cascade aborted at the bridge.
Switched to the real connectionStateChanged signal, branching on
sock.connected. LockAgentCard needs 'import qs' for the GlobalStates
singleton (root module), not just qs.services.
2026-07-16 21:32:43 -04:00
Fimeg
e10efbbd56 lock surface: restore redacted chat on unlock + announce replies
Redacted messages stayed '[content hidden until unlock]' forever — nothing
restored them on unlock. Redaction now records the message and unlock
restores content from rawContent. Turns finishing while locked are also
redacted and surfaced. New LockAgentCard shows a one-line preview of
replies that landed during the lock window on the lock glance, like a
notification; body stays hidden until unlock.

sessiond auth: getpwuid_r buffer is u8 with a c_char cast so it compiles
on aarch64 (c_char = u8 there) and x86 (c_char = i8).
2026-07-16 21:16:28 -04:00
Fimeg
856c6e5576 sessiond: session authority daemon — lock-before-shell, lock-past-shell-death
ext-session-lock client that locks at session start, hands the lock to the
shell over a runtime socket without an unlocked instant (abandon + restore),
and retakes it the moment the shell heartbeat drops. Spartan PIN fallback
surface, PAM via hand-rolled libpam FFI, machined-style sync server.
Shell side: SessiondBridge singleton + LockScreen initIfReady routes the
startup lock decision through the handshake.
2026-07-16 20:32:46 -04:00
Fimeg
b18fa331e0 idle: wire the dim stage and native-coordinator hardening
Dimmed now actually lowers the backlight (brightnessctl save/restore)
and Active restores it - previously dimRequested had no consumer, so
auto-dim never existed on this stack. Keep System Awake is checked
inside the monitor handlers rather than bound to enabled: recreating
the ext-idle-notify object during lock teardown is a fatal Wayland
protocol error on the Pixel compositor. Defaults now mirror the
hypridle timers (dim 120s, lock 300s).
2026-07-16 17:44:01 -04:00
Fimeg
72b01457d2 quickshell: restore ii-stock config keys the fork dropped
Config.qml regained sidebar.width/widthExtended (their absence collapsed
the right sidebar PanelWindow to 1px — 'unclickable' on the laptop), plus
lock.dpmsTimeout, lock.unlockHook, and autoIdleInhibit with its Idle.qml
property. Audio.qml's absent capture source is now a zero-volume stub so
stock consumers stop dereferencing null, and the pactl status parse runs
under LC_ALL=C. volumeMixer dialog content comes along for the sidebar.
Findings logged in SouveraineOS/docs/DUMP-power-idle-lock-2026-07-15.md.
2026-07-16 08:06:11 -04:00
Fimeg
6532572b8d fix: Ai.qml missing 'import qs' — GlobalStates ReferenceError crashed lock redaction
The lock-time redaction Connections block referenced GlobalStates but
Ai.qml never imported the root qs module, so every screenLocked change
threw ReferenceError inside the handler — taking the lock surface down.
2026-07-14 22:26:19 -04:00
Fimeg
bc0814939e fix: add missing pragma Singleton to SessionAudit.qml 2026-07-14 22:01:45 -04:00
Fimeg
10c206c09c fix: Component.onCompleted doesn't work on QML Singletons
Replaced with Timer { interval: 0; running: true } in:
- Session.qml (boot IPC audit)
- SessionAudit.qml (init log)
- SessionEvents.qml (init log)
2026-07-14 21:58:02 -04:00
Fimeg
ae9ef41bb1 hash-chained audit trail for session transitions
SessionAudit.qml — tamper-evident JSONL log of all session state changes:
- Lock/unlock, secure/insecure transitions
- Idle state machine transitions (active/dimmed/lock/sleep/wake)
- PrepareForSleep, external lock signals, inhibitor lifecycle
- Step-up auth success/failure, break-glass issued/consumed/expired
- Grant expiry and revocation

Hash chain: each entry includes md5 of previous entry. Sequence numbers
detect gaps. Chain is validated on startup from the last entry.

Uses Qt.md5() (not SHA256 — not available in QML). Advisory tamper-
evidence, not cryptographic security. Log at:
~/.local/share/souveraine/session-audit.jsonl
2026-07-14 21:31:23 -04:00
Fimeg
377b82999d break-glass: one-time emergency grant with journaling
StepUpAuth.breakGlass(actionFamily, reason):
- Requires a non-empty reason (logged prominently)
- 60-second TTL (not the normal 5 minutes)
- One-time: consumed on use via isBreakGlass()
- Cannot be issued while session is locked
- Cleared on lock (fail-closed)
- Tracked separately from normal grants for audit

Souveraine.send() now checks break-glass before blocking on step-up.
Break-glass bypasses normal PAM auth — the user declared an emergency.
2026-07-14 21:24:57 -04:00