extract music player deps - souveraine-player is its own crate now
This commit is contained in:
parent
dfd494c5f1
commit
8d661d2352
7 changed files with 1083 additions and 66 deletions
|
|
@ -99,6 +99,8 @@
|
|||
| `souveraine-subconscious-naming.md` | 2 | pending | Clarify naming: Ani's subconscious = Aster, Souveraine agents' subconscious = Souveraine-subconscious. |
|
||||
| `quickshell-surface-package.md` | delivery | **in progress** | Split core from QuickShell surface package; safe preview/adopt installer, no automatic ii overwrite. |
|
||||
| `device-specific-quickshell-profiles.md` | delivery | scoped | Phone and laptop share the agent channel but have distinct QuickShell bodies, manifests, and packages. |
|
||||
| `souveraine-settings-ipc-profiles.md` | design | scoped | One typed, audited settings authority for UI + agent IPC, exact-diff step-up proposals, and atomic named profiles. |
|
||||
| `souveraine-native-audio-privacy.md` | delivery | **in progress** | Original AGPL PulseAudio controls and truthful privacy surfaces; PipeWire retained only for portal video. |
|
||||
| `power-indication.md` | delivery | scoped | Charging state, honest full detection (state not onBattery — kills "Charging 1% forever"), a PowerService singleton, and charge-aware idle policy (sleep-inhibit while charging, wake-on-charge). |
|
||||
| `footer-cleanup.md` | 3.0 | **landed** | Removed S-Ret indicator, scroll hint, posture label. Footer: Esc menu · Tab · tool hint · conv ID · ctx%. |
|
||||
| `ui-styling-cleanup.md` | 3.0 | pending | UI styling polish for cockpit/subconscious/thinking boxes — sentences run together, needs formatting and visual cleanup. |
|
||||
|
|
|
|||
46
docs/tasks/souveraine-lock-primitives-session-prompt.md
Normal file
46
docs/tasks/souveraine-lock-primitives-session-prompt.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Next session — Souveraine-owned AGPL lock primitives
|
||||
|
||||
> Paste this into the next session as the opening message. It is orders, not
|
||||
> a draft. Related task doc: `docs/tasks/souveraine-lock-primitives.md`.
|
||||
|
||||
---
|
||||
|
||||
We're building the Souveraine-owned lock surface. Read
|
||||
`docs/tasks/souveraine-lock-primitives.md` first — it's the full spec:
|
||||
dependency set, the primitive tiers, the build order, done-when criteria.
|
||||
|
||||
## Non-negotiables (do not re-litigate)
|
||||
|
||||
1. **SouveraineOS, not ii OS.** Cut the cord from ii by **rewriting**, never
|
||||
vendoring. ii files inherit GPLv3; we will not ship ii-derived files.
|
||||
Every primitive is original Souveraine AGPL, Casey as copyright holder.
|
||||
2. **Build the end goal, not v1/v2.** No borrowed-widget bridges. The lock
|
||||
may not compile or may miss a subcomponent while the owned primitives
|
||||
are built — that's preferable to a single compromise. Full AGPL lock
|
||||
dep graph is the target.
|
||||
3. **ii is behavioral reference only.** Read how ii's widget behaves (on the
|
||||
phone at `~/.config/quickshell/ii/modules/common/widgets/`, `ssh pixel`);
|
||||
do not copy. Write fresh.
|
||||
4. **No subagents for this.** Write the code in the main conversation.
|
||||
5. **No mid-session paperwork** beyond the task doc that already exists.
|
||||
Don't write PLAN.md / handoffs / memory unless asked.
|
||||
6. **Don't run build/test loops as the model if you're Opus.** Verify once
|
||||
with the real build.
|
||||
|
||||
## Start here
|
||||
|
||||
Tier 0 (`Appearance` / `ColorUtils` / `Translation`) — nothing compiles
|
||||
without them, and `Appearance` is where the Souveraine lock palette gets
|
||||
defined (retire the scattered `#d9ffffff` etc. literals). Confirm the
|
||||
target path (`modules/souveraine/widgets/` unless Casey says otherwise),
|
||||
then Tier 0 → Tier 1 (`StyledText`, `MaterialSymbol`, `MaterialShape`,
|
||||
`ErrorShakeAnimation`) → Tier 2 (`RippleButton`) → Tier 3 (`AmbientCard`,
|
||||
`PinDots`, `LockKeypad`, `LockUtilityRow`, `LockReveal`) → rewire
|
||||
`TouchLockSurface.qml` + the two cards → manifest lines → deploy.
|
||||
|
||||
Behavior reference on the phone: ii widgets at
|
||||
`~/.config/quickshell/ii/modules/common/widgets/`; Phosh lockscreen at
|
||||
`~/build/phosh/src/lockscreen.c` + `ui/lockscreen.ui` (swipe-capture model,
|
||||
shake midpoint-crossing termination — implement in QML, do not port).
|
||||
|
||||
Go.
|
||||
191
docs/tasks/souveraine-lock-primitives.md
Normal file
191
docs/tasks/souveraine-lock-primitives.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
task_id: souveraine-lock-primitives-001
|
||||
title: "Souveraine-owned AGPL lock primitives — cut the cord from ii by rewriting"
|
||||
status: pending
|
||||
priority: high
|
||||
phase: build
|
||||
created: 2026-07-18
|
||||
references: docs/tasks/qs-souveraine-composer.md, docs/DECISIONS.md, ~/Projects/MAKE_PROPER_LICENSES_FOR_ALL_PROJECTS.md, surfaces/quickshell/modules/ii/lock/TouchLockSurface.qml, surfaces/quickshell/modules/souveraine/lock/
|
||||
---
|
||||
|
||||
# Souveraine-owned AGPL lock primitives
|
||||
|
||||
## The end goal (build THIS, not an increment)
|
||||
|
||||
A fully Souveraine-owned, fully AGPL lock surface. Zero ii-derived files in
|
||||
the lock dependency graph. Every primitive the lock uses is original
|
||||
Souveraine-authored QML, licensed AGPLv3, designed to behave how *we* want
|
||||
— not a copy of ii.
|
||||
|
||||
There is no v1/v2 here. We do not vendor ii files. We do not borrow ii
|
||||
widgets as a bridge. We accept that the lock may not compile, or may be
|
||||
missing a subcomponent, while the owned primitives are built. That is
|
||||
preferable to a single borrowed-widget compromise. See the cord-cut rule in
|
||||
`docs/DECISIONS.md` ("Shell = own config") and
|
||||
`docs/tasks/qs-souveraine-composer.md`.
|
||||
|
||||
## Why rewrite, not vendor
|
||||
|
||||
Vendoring an ii file inherits GPLv3 — we can never make it ours by
|
||||
relabeling. The licensing master doc governs the boundary: original
|
||||
Souveraine files get AGPL headers + Casey as copyright holder; genuinely
|
||||
inherited GPLv3 files keep their headers. Because we choose to author
|
||||
rather than inherit, every lock primitive will be original AGPL, and the
|
||||
lock surface conveys cleanly under our license. ii is behavioral reference
|
||||
only — read how it behaves, write our own implementation. Do not copy.
|
||||
|
||||
## What the lock surface currently depends on
|
||||
|
||||
Direct external symbols referenced by the lock files (verified by grep,
|
||||
2026-07-18):
|
||||
|
||||
| Symbol | Used by | What it is |
|
||||
|--------|---------|------------|
|
||||
| `RippleButton` | `TouchLockSurface.qml` (KeypadButton, UtilityButton inline components) | Pressable button with material-ripple feedback. Transitive ii dep: `MaterialShape`. |
|
||||
| `StyledText` | `TouchLockSurface.qml` (dots hint, keypad glyphs, hint labels) | Themed text bound to `Appearance` colors/fonts. |
|
||||
| `MaterialSymbol` | `TouchLockSurface.qml` (icons: backspace, arrow, power, reboot) | Material Symbols icon glyph. |
|
||||
| `ErrorShakeAnimation` | `TouchLockSurface.qml` (wrong-PIN shake on `dotsArea`) | Frame-driven horizontal shake animation. |
|
||||
| `Appearance` (singleton) | `TouchLockSurface.qml` (15×), `LockScreen.qml` (1×). The host + cards use raw literals instead — evidence for the palette work below. | Colors (`colOnLayer1`, `colError`, `colSubtext`, `colOnSurfaceVariant`, `colOnPrimary`) + font pixel sizes. |
|
||||
| `ColorUtils` | `TouchLockSurface.qml` | `transparentize()`. |
|
||||
| `Translation` | `TouchLockSurface.qml` | `tr()` i18n. |
|
||||
| `MaterialShape` | transitive (used by ii's RippleButton) | Material-style rounded/shape background. First-class rewrite target since our RippleButton needs it. |
|
||||
|
||||
Everything else in the lock files is QtQuick/QtQuick.Layouts/Quickshell
|
||||
(primitives we keep) or Souveraine-owned already (`GlobalStates`,
|
||||
`LockContext`, `Config`, `LockContentPolicy`, `Ai`, `Battery`,
|
||||
`LockSurfaceHost`, `LockMediaCard`, `LockAgentCard`).
|
||||
|
||||
## What to build (the primitive set)
|
||||
|
||||
All under a Souveraine-owned path — propose
|
||||
`surfaces/quickshell/modules/souveraine/widgets/` — each file original AGPL,
|
||||
Casey as copyright holder, designed from the lock's needs with ii as
|
||||
behavioral reference only.
|
||||
|
||||
### Tier 0 — theme singletons (these are not optional; everything below keys off them)
|
||||
|
||||
- **`Appearance`** — colors + font scale. This is the single source of truth
|
||||
every styled component reads. Decide the Souveraine lock palette here
|
||||
(replacing the scattered literals `#d9ffffff`, `#e6ffffff`, `#1c1b20`,
|
||||
`#b3181b20`, `#22000000`, `#33ffffff`, `#55ffffff` currently hand-rolled
|
||||
across the three lock cards). One palette, named tokens.
|
||||
- **`ColorUtils`** — at minimum `transparentize(color, amount)`.
|
||||
- **`Translation`** — `tr()` passthrough to whatever i18n we standardize on.
|
||||
|
||||
### Tier 1 — visual primitives
|
||||
|
||||
- **`StyledText`** — text bound to Appearance tokens; the type every label
|
||||
uses.
|
||||
- **`MaterialSymbol`** — icon glyph renderer.
|
||||
- **`MaterialShape`** — material background shape (rounded rect minimum;
|
||||
designed so `RippleButton` can compose it).
|
||||
- **`ErrorShakeAnimation`** — horizontal shake with the *midpoint-crossing
|
||||
termination* rule (stop when the oscillation would cross the rest point,
|
||||
not on a fixed timer — Phosh's `shake_entry` model, frame-clock driven).
|
||||
|
||||
### Tier 2 — composite primitives (compose Tier 1)
|
||||
|
||||
- **`RippleButton`** — the pressable button. Ripple feedback baked in from
|
||||
day one (this is one of the "minor details" Casey wants to tweak — build
|
||||
it ours, tunable). Properties the lock uses today: `contentItem`,
|
||||
`colBackground`, `buttonRadius`, `toggled`, `enabled`, signals
|
||||
`clicked`/`pressAndHold`.
|
||||
|
||||
### Tier 3 — lock-surface components (built on the owned primitives)
|
||||
|
||||
These replace the inline blocks in `TouchLockSurface.qml`:
|
||||
|
||||
- **`AmbientCard`** — the shared bordered rounded-rect card both
|
||||
`LockMediaCard` and `LockAgentCard` currently hand-roll with different
|
||||
radii/alphas. One primitive, one palette.
|
||||
- **`PinDots`** — the entered-PIN dots row + empty-state hint, with
|
||||
`ErrorShakeAnimation` wired in.
|
||||
- **`LockKeypad`** — the 3×3 + 0/backspace/confirm grid, built from our
|
||||
`RippleButton`.
|
||||
- **`LockUtilityRow`** — power / battery / reboot, from our `RippleButton`.
|
||||
- **`LockReveal`** — the glance↔PIN stage controller. Owns the reveal
|
||||
intent (`reveal()`, `retreat()`) so every input path (swipe, tap-hint,
|
||||
hardware key, future double-tap-clock) funnels through one transition
|
||||
instead of each re-implementing threshold logic. Replaces the inline
|
||||
`pressY`/threshold/retreat-timer block in `TouchLockSurface.qml`.
|
||||
|
||||
### The result
|
||||
|
||||
`TouchLockSurface.qml` shrinks to ~layout + intent wiring (target ~60–80
|
||||
lines), built entirely on Souveraine-owned AGPL QML. The lock dep graph has
|
||||
zero ii-derived files.
|
||||
|
||||
## How to build it (approach, not steps)
|
||||
|
||||
1. **Tier 0 first.** `Appearance`/`ColorUtils`/`Translation` — nothing else
|
||||
compiles without them. Define the Souveraine lock palette here as named
|
||||
tokens; retire the scattered literals.
|
||||
2. **Tier 1 next**, in dependency order: `StyledText` (needs Appearance),
|
||||
`MaterialSymbol`, `MaterialShape`, then `ErrorShakeAnimation`
|
||||
(standalone).
|
||||
3. **Tier 2:** `RippleButton` (composes `MaterialShape`).
|
||||
4. **Tier 3:** `AmbientCard`, `PinDots`, `LockKeypad`, `LockUtilityRow`,
|
||||
`LockReveal` — built on Tiers 0–2.
|
||||
5. **Rewire `TouchLockSurface`** and the two cards to consume the owned
|
||||
primitives, removing every ii import.
|
||||
|
||||
Each file lands with an AGPLv3 header and Casey as copyright holder.
|
||||
Behavioral reference from ii is fine (read how ii's `RippleButton` feels,
|
||||
what properties it exposes); copying is not.
|
||||
|
||||
## ii reference access (behavioral, read-only)
|
||||
|
||||
ii's widget sources are NOT in this repo. They live on the phone at
|
||||
`/home/casey/.config/quickshell/ii/modules/common/widgets/` (RippleButton,
|
||||
StyledText, MaterialSymbol, MaterialShape, ErrorShakeAnimation, etc.) and
|
||||
on the laptop at `~/.config/quickshell/ii/`. SSH: `ssh pixel`. Read them to
|
||||
understand behavior and the property contract the lock code expects — do
|
||||
not copy. Write fresh AGPL implementations.
|
||||
|
||||
Phosh's lockscreen source is also on the phone at `~/build/phosh/src/`
|
||||
(`lockscreen.c`, `ui/lockscreen.ui`) — useful behavioral reference for the
|
||||
swipe-capture model (deck/carousel, rounded-progress page gating) and the
|
||||
shake midpoint-crossing termination. Do not port; implement in QML.
|
||||
|
||||
## Manifest + composer wiring
|
||||
|
||||
Every new Souveraine-owned file needs a line in
|
||||
`surfaces/quickshell/deploy.sh` `MANIFEST` (the composer only ships files
|
||||
in the manifest — a missing line means "is not a type" at load). As files
|
||||
are written, add manifest lines pointing them into the `souveraine/`
|
||||
composed config. Borrowed ii widget dirs stay borrowed for the rest of the
|
||||
shell until their own cord-cut; only the lock's deps are cut here.
|
||||
|
||||
## License headers
|
||||
|
||||
Per `~/Projects/MAKE_PROPER_LICENSES_FOR_ALL_PROJECTS.md`:
|
||||
|
||||
- Every file written here: AGPLv3 header, Casey Tunturi as copyright holder.
|
||||
- Do NOT put inherited GPLv3 headers on these — they are original work.
|
||||
- Mixed tree is honest: the lock dep graph is all-original AGPL after this
|
||||
task; other parts of the shell may still carry inherited GPLv3 until their
|
||||
own rewrite.
|
||||
|
||||
## Non-goals / out of scope for this task
|
||||
|
||||
- Rewriting non-lock widgets (dock, sidebar, overview, etc.) — separate
|
||||
cord-cuts, separate tasks.
|
||||
- Changing lock *behavior* beyond what the primitive rewrite naturally
|
||||
enables. Feature additions (clock double-tap, new gesture paths, peek
|
||||
mode) are follow-on work on top of the owned primitives, not part of
|
||||
getting the substrate owned.
|
||||
- Touching sessiond (Rust) — it is already Souveraine-owned and unaffected.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Tier 0–2 primitives exist as original AGPL files under
|
||||
`modules/souveraine/widgets/` (or agreed path), each with AGPL header.
|
||||
- [ ] Tier 3 lock components exist, built on the owned primitives.
|
||||
- [ ] `TouchLockSurface.qml` + both lock cards import only Souveraine-owned
|
||||
+ Qt/Quickshell modules — zero `qs.modules.common.widgets`
|
||||
ii-borrowed imports in the lock graph.
|
||||
- [ ] The lock surface dep graph contains no ii-derived files (grep
|
||||
verifiable).
|
||||
- [ ] Manifest lines added; `deploy.sh --phone` ships and the lock runs.
|
||||
- [ ] Cord-cut ledger updated (this file's "Done when", or a dedicated
|
||||
ledger) recording which borrowed dirs remain.
|
||||
252
docs/tasks/souveraine-native-audio-privacy.md
Normal file
252
docs/tasks/souveraine-native-audio-privacy.md
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
---
|
||||
task_id: souveraine-native-audio-001
|
||||
title: "Souveraine native audio and privacy surfaces — PulseAudio for sound, PipeWire for portals"
|
||||
status: in_progress
|
||||
priority: high
|
||||
phase: delivery
|
||||
created: 2026-07-18
|
||||
references:
|
||||
- surfaces/quickshell/services/Audio.qml
|
||||
- surfaces/quickshell/modules/ii/sidebarRight/volumeMixer/VolumeDialogContent.qml
|
||||
- surfaces/quickshell/modules/ii/bar/UtilButtons.qml
|
||||
- surfaces/quickshell/deploy.sh
|
||||
- /home/casey/Projects/Pixel3Arch/rootfs-overlay/etc/pulse/default.pa.d/90-blueline-alsa.pa
|
||||
- /home/casey/Projects/Pixel3Arch/pkgs/pipewire-abi-provider/PKGBUILD
|
||||
- docs/tasks/souveraine-settings-ipc-profiles.md
|
||||
- docs/tasks/souveraine-lock-primitives.md
|
||||
---
|
||||
|
||||
# Souveraine native audio and privacy surfaces
|
||||
|
||||
## End goal
|
||||
|
||||
Souveraine owns the complete audio control and privacy presentation for phone
|
||||
and laptop. Native PulseAudio is the only audio server. PipeWire exists only as
|
||||
the video transport required by screen-cast/camera portals; `pipewire-pulse` is
|
||||
never started and never owns an audio device.
|
||||
|
||||
The user and agents see one truthful audio model:
|
||||
|
||||
- the real default output and input;
|
||||
- output/input volume and mute;
|
||||
- which applications hold playback or recording streams;
|
||||
- a visible, persistent microphone/camera/screen-capture privacy indicator;
|
||||
- device switching where more than one real endpoint exists;
|
||||
- structured IPC methods guarded by the settings/capability authority;
|
||||
- no ii or PipeWire audio assumptions in the active dependency graph.
|
||||
|
||||
This is an original AGPL Souveraine implementation. The current files under
|
||||
`modules/ii/` are functional scaffolding and behavioral evidence, not the
|
||||
destination and not files to relabel.
|
||||
|
||||
## Ground truth as of 2026-07-18
|
||||
|
||||
The important PulseAudio foundation is already present:
|
||||
|
||||
- `services/Audio.qml` is PulseAudio-backed and mirrors both the default sink
|
||||
and the default source through `pactl`.
|
||||
- It has real source volume/mute setters, `toggleMicMute()`, `sourceReady`,
|
||||
`inputDevices`, and a first `micActive` signal based on Pulse source outputs.
|
||||
- `VolumeDialogContent.qml` presents a real input page and binds the slider to
|
||||
`Audio.source` rather than a fake microphone object.
|
||||
- `UtilButtons.qml` now calls `Audio.toggleMicMute()` and reads the real source
|
||||
mute state.
|
||||
- Pixel3Arch autoloads the handset capture PCM as the named Pulse source
|
||||
`blueline_mic`, makes it the default source, and uses UCM to establish the
|
||||
handset route before PulseAudio starts.
|
||||
|
||||
Do not reopen those solved questions or restore PipeWire audio. The remaining
|
||||
work is ownership, completeness, event accuracy, privacy presentation, IPC,
|
||||
and portal coexistence.
|
||||
|
||||
## The architectural boundary
|
||||
|
||||
| Function | Authority |
|
||||
|---|---|
|
||||
| Speaker/headphone playback | native PulseAudio |
|
||||
| Handset/external microphone capture | native PulseAudio |
|
||||
| Per-app audio streams | native PulseAudio sink-input/source-output model |
|
||||
| Audio device selection | native PulseAudio defaults and profiles |
|
||||
| Screen-cast frames | PipeWire portal transport |
|
||||
| Camera frames | PipeWire portal transport where the portal requires it |
|
||||
| Audio for screen recording | explicit PulseAudio capture chosen by the recorder; never `pipewire-pulse` |
|
||||
|
||||
PipeWire's presence must not imply it is an audio server. Packaging and user
|
||||
units enforce the boundary: no Pulse compatibility daemon, no ALSA device
|
||||
acquisition by PipeWire, and no competing session policy for sound.
|
||||
|
||||
## Owned service model
|
||||
|
||||
Replace the scaffolding with an original `SouveraineAudio.qml` service (final
|
||||
name may remain `Audio` for call-site stability). It projects:
|
||||
|
||||
```text
|
||||
serverReady
|
||||
defaultSink / defaultSource
|
||||
sinks[] / sources[]
|
||||
playbackStreams[] / recordingStreams[]
|
||||
outputActive / microphoneActive
|
||||
lastError / generation
|
||||
```
|
||||
|
||||
Each device and stream has a stable, typed shape: Pulse name/index,
|
||||
description, availability, volume, mute, monitor/physical classification,
|
||||
application identity, media role, corked/running state, and owning PID when
|
||||
Pulse exposes it. Do not infer a microphone from `hw:0,1` naming alone;
|
||||
`blueline_mic` is the board's canonical source name, while laptops may publish
|
||||
different source names.
|
||||
|
||||
### Event model
|
||||
|
||||
The current 1.5-second `pactl` snapshot is acceptable diagnostic scaffolding
|
||||
but not the final privacy mechanism. The owned service keeps one long-lived
|
||||
`pactl subscribe` observer, debounces server events, then refreshes a complete
|
||||
machine-readable snapshot. Prefer `pactl --format=json` when supported and a
|
||||
small owned parser/fallback when it is not; do not parse localized display
|
||||
labels.
|
||||
|
||||
Loss/restart of PulseAudio increments `generation`, clears stale objects, and
|
||||
repopulates defaults. Sink readiness and source readiness remain independent:
|
||||
a missing microphone must never disable speaker controls.
|
||||
|
||||
Every setter returns a structured result or emits an error with the affected
|
||||
object. The UI does not optimistically lie when `pactl` fails.
|
||||
|
||||
## Privacy truth
|
||||
|
||||
Build one owned `PrivacyService` that consumes actual subsystem activity, not
|
||||
UI toggles:
|
||||
|
||||
- `microphoneActive`: at least one Pulse source-output exists; expose the
|
||||
application identities and whether streams are corked.
|
||||
- `cameraActive`: portal/camera session ownership, not merely device presence.
|
||||
- `screenCaptureActive`: active portal screencast or Souveraine recorder
|
||||
session.
|
||||
|
||||
The prior borrowed Privacy model must not be reused: converting an array with
|
||||
`.map(...)` to a boolean is always truthy and is not evidence of access. The
|
||||
owned implementation uses explicit collection predicates and retains the
|
||||
participants needed for disclosure.
|
||||
|
||||
Privacy indication is not configurable away. Appearance may vary, but active
|
||||
capture always has a persistent visible indicator on the unlocked shell and a
|
||||
safe ambient indication on the lock surface. Opening it shows which
|
||||
application is accessing what and offers a stop/revoke action where the
|
||||
underlying subsystem supports one. Unknown clients are displayed as unknown,
|
||||
not hidden.
|
||||
|
||||
Muting the microphone and stopping access are distinct operations:
|
||||
|
||||
- mute changes the default source's audio value;
|
||||
- stop/revoke terminates or denies a recording stream/session;
|
||||
- the indicator remains visible while a client still holds the stream, even if
|
||||
the source is muted.
|
||||
|
||||
This distinction prevents a muted-but-still-recording process from becoming
|
||||
invisible.
|
||||
|
||||
## Owned surfaces
|
||||
|
||||
Create original Souveraine AGPL components under Souveraine paths:
|
||||
|
||||
- `services/SouveraineAudio.qml` — authoritative Pulse model and methods.
|
||||
- `services/PrivacyService.qml` — cross-subsystem access truth.
|
||||
- `modules/souveraine/audio/VolumeSurface.qml` — output/input tabs, real
|
||||
endpoint state, and per-app streams.
|
||||
- `modules/souveraine/audio/DeviceRow.qml` and `StreamRow.qml` — reusable owned
|
||||
presentation primitives.
|
||||
- `modules/souveraine/privacy/PrivacyIndicator.qml` — persistent compact state.
|
||||
- `modules/souveraine/privacy/PrivacyDetails.qml` — identities and stop/revoke
|
||||
actions.
|
||||
- phone and laptop hosts that place the same semantic components according to
|
||||
their own form-factor contract.
|
||||
|
||||
Rewire active bars/sidebars to these components, then remove the corresponding
|
||||
`modules/ii` overrides and borrowed imports from the active graph. Do not copy
|
||||
or vendor ii widgets while doing so; use the Souveraine-owned primitive layer.
|
||||
|
||||
## IPC and agent reach
|
||||
|
||||
Expose a JSON-over-string `audio` IPC surface because Quickshell drops `var`
|
||||
returns:
|
||||
|
||||
```text
|
||||
audio.state()
|
||||
audio.devices(direction)
|
||||
audio.streams(direction)
|
||||
audio.setVolume(direction, value, reason)
|
||||
audio.setMuted(direction, muted, reason)
|
||||
audio.setDefault(direction, pulseName, reason)
|
||||
audio.stopStream(streamId, reason)
|
||||
```
|
||||
|
||||
Read-only state is ambient. Ordinary volume/mute changes are direct low-risk
|
||||
settings/actions and audited. Changing a default endpoint is direct while
|
||||
unlocked unless the settings schema says otherwise. Stopping another
|
||||
application's recording stream is a physical/privacy action: an agent may
|
||||
propose it, and human surfaces may act according to the settings authority's
|
||||
policy. IPC never invents a second authorization model.
|
||||
|
||||
All methods validate direction, ranges, endpoint existence, Pulse generation,
|
||||
and current lock/security state. They return `{ok:false, reason}` rather than
|
||||
silently no-oping.
|
||||
|
||||
## Portal-only PipeWire
|
||||
|
||||
The current Pixel3Arch `pipewire-abi-provider` conflicts with the PipeWire
|
||||
daemon and WirePlumber. That satisfies ABI consumers but makes real portal
|
||||
screen sharing impossible. Split the packaging policy:
|
||||
|
||||
1. retain native PulseAudio and its ALSA ownership;
|
||||
2. install a real PipeWire daemon plus the minimal session/portal policy needed
|
||||
for video nodes;
|
||||
3. explicitly mask/omit `pipewire-pulse`;
|
||||
4. prevent WirePlumber/PipeWire from claiming ALSA audio devices, or use a
|
||||
portal-video-only configuration if the selected session manager supports
|
||||
it;
|
||||
5. verify `xdg-desktop-portal` and the compositor portal can establish a
|
||||
screencast without changing Pulse sinks/sources.
|
||||
|
||||
This packaging repair belongs in Pixel3Arch and must be tested as a system
|
||||
contract. Souveraine surfaces consume its portal state; they do not launch an
|
||||
ad-hoc competing PipeWire audio stack.
|
||||
|
||||
## Verification matrix
|
||||
|
||||
Verify on both phone and laptop:
|
||||
|
||||
1. Pulse starts with the intended default sink/source and no `pipewire-pulse`
|
||||
socket or daemon.
|
||||
2. Source/sink volume and mute changes round-trip through Pulse and survive a
|
||||
service refresh.
|
||||
3. Starting and stopping recording changes the privacy indicator promptly and
|
||||
names the client.
|
||||
4. Muting during recording leaves the access indicator visible.
|
||||
5. A Pulse restart clears stale devices/streams and recovers without restarting
|
||||
Quickshell.
|
||||
6. A missing source leaves output controls fully functional.
|
||||
7. Screen sharing works through the portal-only PipeWire path while Pulse
|
||||
remains the sole audio server.
|
||||
8. Lock/unlock transitions never expose private application metadata on the
|
||||
lock surface, but active capture remains visibly indicated.
|
||||
9. IPC calls return structured success/refusal and cannot bypass the settings
|
||||
or step-up authority.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Active audio/privacy services and surfaces are original Souveraine AGPL
|
||||
files with no ii widget dependency in their graph.
|
||||
- [ ] Native PulseAudio is the sole audio authority on phone and laptop.
|
||||
- [ ] Real sinks, sources, playback streams, and recording streams are modeled
|
||||
eventfully and recover across daemon restart.
|
||||
- [ ] A persistent truthful privacy indicator identifies active capture clients
|
||||
and distinguishes mute from revocation.
|
||||
- [ ] Owned phone and laptop presentations share semantics without sharing
|
||||
inappropriate layout.
|
||||
- [ ] Agent IPC covers discovery and guarded actions through the common
|
||||
settings/capability policy.
|
||||
- [ ] Portal video works with a real PipeWire transport while `pipewire-pulse`
|
||||
remains absent.
|
||||
- [ ] Pixel 3 microphone capture is verified non-zero through the named
|
||||
`blueline_mic` Pulse source; no raw-ALSA-only success is accepted as the
|
||||
product result.
|
||||
258
docs/tasks/souveraine-settings-ipc-profiles.md
Normal file
258
docs/tasks/souveraine-settings-ipc-profiles.md
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
---
|
||||
task_id: souveraine-settings-authority-001
|
||||
title: "Souveraine settings authority — complete IPC, security policy, and named profiles"
|
||||
status: scoped
|
||||
priority: high
|
||||
phase: design
|
||||
created: 2026-07-18
|
||||
references:
|
||||
- surfaces/quickshell/modules/common/Config.qml
|
||||
- surfaces/quickshell/modules/settings/
|
||||
- surfaces/quickshell/services/StepUpAuth.qml
|
||||
- surfaces/quickshell/modules/common/functions/Session.qml
|
||||
- docs/tasks/souveraine-shell-ecosystem.md
|
||||
- docs/tasks/device-specific-quickshell-profiles.md
|
||||
- docs/tasks/power-indication.md
|
||||
- docs/DECISIONS.md
|
||||
---
|
||||
|
||||
# Souveraine settings authority
|
||||
|
||||
## End goal
|
||||
|
||||
Every supported Souveraine setting is discoverable and operable through one
|
||||
Souveraine-owned settings authority. The settings application, shell surfaces,
|
||||
profiles, the local human, and agents all use the same schema and mutation
|
||||
path. There are no UI-only settings, hidden ad-hoc writes to `Config.options`,
|
||||
or profiles that bypass the security policy.
|
||||
|
||||
This is not a loose `settings.set(any.path, any.value)` escape hatch. It is a
|
||||
typed, audited capability surface:
|
||||
|
||||
- low-risk settings can be adjusted directly by a human or agent;
|
||||
- sensitive settings can be proposed by an agent but only committed by a
|
||||
local human after seeing the exact diff and satisfying step-up;
|
||||
- secret-bearing values are never returned over shell IPC;
|
||||
- a profile is an atomic settings transaction, not a second configuration
|
||||
mechanism;
|
||||
- every refusal returns a structured reason instead of silently doing nothing.
|
||||
|
||||
Build this authority as original Souveraine AGPL code. Existing ii-derived
|
||||
configuration is input to inventory, not an architecture to preserve.
|
||||
|
||||
## Why a schema is load-bearing
|
||||
|
||||
`Config.qml` currently defines values and persistence, while individual
|
||||
settings pages know labels and presentation. That is not enough for an agent
|
||||
or a profile system: neither can safely infer type, range, risk, device scope,
|
||||
or whether a value is a credential from an arbitrary QML property path.
|
||||
|
||||
Create a Souveraine-owned settings schema. Every supported key has one record:
|
||||
|
||||
```text
|
||||
path lock.security.allowPowerFromLock
|
||||
type bool
|
||||
default false
|
||||
scope common | phone | laptop | device
|
||||
risk ambient | personal | physical | admin | secret
|
||||
mutability direct | propose-only | human-only | read-only
|
||||
profileEligible true
|
||||
requires [capabilities or companion settings]
|
||||
conflicts [mutually exclusive settings]
|
||||
validation enum/range/pattern/custom validator
|
||||
description human- and agent-readable intent
|
||||
```
|
||||
|
||||
The registry is authoritative. A config key without schema metadata is treated
|
||||
as internal and is not exposed over IPC. CI fails when a user-facing setting is
|
||||
added to a settings page without a schema record and IPC reachability test.
|
||||
|
||||
## One settings service, one mutation path
|
||||
|
||||
Create an original `SettingsService.qml` (or equivalent owned service) that
|
||||
owns schema lookup, validation, authorization, persistence, audit, profiles,
|
||||
and change notification. Settings pages stop assigning directly to
|
||||
`Config.options.*`; they call this service just like agents do.
|
||||
|
||||
The service exposes in-process methods for QML and an IPC adapter. Quickshell
|
||||
IPC only reliably transports declared primitive types, so structured payloads
|
||||
and results cross as JSON strings, following the proven `apps` and `dock`
|
||||
surfaces.
|
||||
|
||||
Proposed IPC target: `settings`.
|
||||
|
||||
```text
|
||||
settings.schema() -> all non-secret metadata
|
||||
settings.describe(path) -> metadata + effective value
|
||||
settings.get(path) -> effective value (redacted if secret)
|
||||
settings.list(prefix) -> settings under a namespace
|
||||
settings.set(path, jsonValue, reason) -> direct write or structured refusal
|
||||
settings.propose(path, jsonValue, reason) -> immutable pending change
|
||||
settings.pending() -> pending proposals, no secret values
|
||||
settings.history(limit) -> audit history, redacted
|
||||
settings.profileList() -> available profiles
|
||||
settings.profileDescribe(name) -> metadata + expanded diff
|
||||
settings.profilePropose(name, reason) -> pending atomic profile transaction
|
||||
```
|
||||
|
||||
Do not expose `approve`, `authenticate`, or `mintGrant` over IPC. Approval is a
|
||||
local Souveraine surface action. An agent can formulate a change and explain
|
||||
why; it cannot click its own approval dialog or turn a proposal into authority.
|
||||
|
||||
## Authorization policy
|
||||
|
||||
The settings authority evaluates each key's schema risk. The caller never
|
||||
supplies or self-asserts its risk class.
|
||||
|
||||
| Risk | Read | Agent mutation | Human mutation |
|
||||
|---|---|---|---|
|
||||
| `ambient` | allowed | direct, validated, audited | direct |
|
||||
| `personal` | value may be redacted while locked | direct or proposal according to key metadata | direct when unlocked |
|
||||
| `physical` | allowed unless value itself is sensitive | proposal only | local review + `StepUpAuth("physical")` |
|
||||
| `admin` | metadata allowed; value policy per key | proposal only | local review + `StepUpAuth("admin")` |
|
||||
| `secret` | never return the value | never accept plaintext through shell IPC | local credential surface only |
|
||||
|
||||
`lock.security.allowPowerFromLock`, `requirePasswordToPower`, step-up enablement
|
||||
and TTL, suspend/hibernate policy, charge thresholds, and any setting that
|
||||
widens an agent's own authority are never direct agent writes. They are
|
||||
proposal-only even if a broad step-up grant happens to be live. This prevents
|
||||
an unrelated process from racing the IPC socket during a grant window.
|
||||
|
||||
Approval binds to the immutable proposal digest: path(s), old value(s), new
|
||||
value(s), reason, requester, device, and expiry. The local approval surface
|
||||
shows the complete diff before PAM. Authentication approves that exact digest,
|
||||
not a reusable boolean and not a caller-provided token. Locking the session,
|
||||
changing any old value, or reaching expiry invalidates the proposal.
|
||||
|
||||
## Audit and recovery
|
||||
|
||||
Every attempted mutation records:
|
||||
|
||||
- timestamp and device/profile;
|
||||
- caller identity when available (`human`, `agent:<id>`, system component);
|
||||
- path and redacted old/new values;
|
||||
- reason;
|
||||
- outcome (`applied`, `proposed`, `refused`, `expired`, `rolled-back`);
|
||||
- authorization path (direct policy, physical step-up, admin step-up).
|
||||
|
||||
Successful transactions retain an inverse diff so the settings UI can undo a
|
||||
change. Audit is append-only and bounded/rotated; secret material is never
|
||||
written. A failed multi-key transaction rolls back fully and reports the key
|
||||
that failed.
|
||||
|
||||
## Profiles are policy transactions
|
||||
|
||||
Profiles such as the working-name pair “paranoid” and “open” are named,
|
||||
versioned bundles of schema paths and values. Names remain product language to
|
||||
decide later; do not bake those working names into API semantics.
|
||||
|
||||
A profile is not a parallel config file that overwrites `Config.qml`. Applying
|
||||
one expands it against the current effective configuration, validates every
|
||||
key, computes one visible diff, evaluates the highest risk in that diff, and
|
||||
commits atomically through `SettingsService`.
|
||||
|
||||
Profile model:
|
||||
|
||||
```text
|
||||
id / displayName / description / version
|
||||
deviceScopes: common, phone, laptop, optional explicit device IDs
|
||||
values: schema path -> typed value
|
||||
inherits: optional single base profile (cycle rejected)
|
||||
source: built-in | user | managed
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. Applying a profile never bypasses per-key authorization. If one key is
|
||||
`physical` or `admin`, the whole transaction becomes a reviewed proposal.
|
||||
2. Unknown, removed, or device-incompatible keys refuse the transaction; they
|
||||
are not silently ignored.
|
||||
3. Profiles contain no secrets. Credential references are separate and remain
|
||||
local.
|
||||
4. The preview names values that will change, values already satisfied, and
|
||||
unsupported capabilities before authentication.
|
||||
5. A profile applies atomically and records one audit transaction plus its
|
||||
expanded per-key diff.
|
||||
6. The active-profile label is descriptive state, not magical inheritance. A
|
||||
manual edit may mark the profile `modified`; it must not be overwritten by a
|
||||
background reapply.
|
||||
7. Agent-created profiles are proposals until a human saves them. Agents may
|
||||
suggest and preview profiles freely.
|
||||
|
||||
## Device profiles are not security profiles
|
||||
|
||||
Do not conflate `phone`/`laptop` surface profiles with a security posture.
|
||||
|
||||
- A **device profile** chooses form factor, available surfaces, dimensions,
|
||||
hardware capabilities, and package manifest.
|
||||
- A **settings profile** changes policy and preferences on a supported device.
|
||||
|
||||
Settings schema records declare their device scope, and profile expansion
|
||||
refuses incompatible keys. A laptop security profile cannot accidentally turn
|
||||
on a phone-only lock keypad or power-button gesture.
|
||||
|
||||
## Human surfaces
|
||||
|
||||
The Souveraine settings application consumes the same schema and service:
|
||||
|
||||
- categories and controls are projected from metadata where practical;
|
||||
- bespoke pages remain valid for rich interactions, but their writes still go
|
||||
through `SettingsService`;
|
||||
- every setting shows whether agents may change it directly, propose it, or
|
||||
never access it;
|
||||
- pending agent proposals have a first-class review surface;
|
||||
- profile preview is a real diff, with risk and required authentication shown;
|
||||
- undo/history is visible rather than hidden in logs.
|
||||
|
||||
The lock surface never hosts general settings approval. A physical/admin change
|
||||
waits until the user is securely unlocked, except for a deliberately designed
|
||||
emergency action whose policy explicitly says otherwise.
|
||||
|
||||
## Implementation shape
|
||||
|
||||
Original AGPL components, names adjustable during build:
|
||||
|
||||
- `services/SettingsSchema.qml` — metadata and validation definitions.
|
||||
- `services/SettingsService.qml` — effective values, transactions, auth policy,
|
||||
persistence, proposals, history, and profiles.
|
||||
- `modules/souveraine/settings/SettingsScope.qml` — `IpcHandler` adapter only;
|
||||
JSON-string transport, no policy duplication.
|
||||
- `modules/souveraine/settings/ProposalReview.qml` — local diff + step-up.
|
||||
- `modules/souveraine/settings/ProfileManager.qml` — list, edit, preview, apply,
|
||||
and modified-state presentation.
|
||||
- a profile store under Souveraine-owned user state, written atomically.
|
||||
|
||||
The existing `Config.qml` `JsonAdapter` may remain the persistence adapter
|
||||
while the migration is underway, but once `SettingsService` owns a key no UI
|
||||
or service may write that key around it. The end state has one authority, not a
|
||||
permanent split brain.
|
||||
|
||||
## Migration rule
|
||||
|
||||
Inventory all current user-facing Config keys first and classify every one.
|
||||
Move whole namespaces through the authority, not individual toggle patches.
|
||||
Security-sensitive lock/session/power settings are the first namespace because
|
||||
the new `allowPowerFromLock` toggle demonstrates why a direct property write is
|
||||
not enough.
|
||||
|
||||
The migration is complete only when every visible settings control is backed
|
||||
by schema and uses the authority. Do not call a generic unvalidated setter a
|
||||
finished IPC implementation.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Every user-facing setting has schema metadata, device scope, validation,
|
||||
risk, and mutability.
|
||||
- [ ] Every settings UI write and agent mutation uses one SettingsService
|
||||
transaction path.
|
||||
- [ ] `settings` IPC offers discovery, read, direct low-risk set, proposal,
|
||||
history, and profile preview/proposal with JSON-string results.
|
||||
- [ ] Secret values never cross IPC, logs, profile files, or audit history.
|
||||
- [ ] Security-widening changes require local review bound to the exact diff;
|
||||
an agent cannot approve its own proposal.
|
||||
- [ ] Profiles validate and apply atomically and cannot bypass a key's policy.
|
||||
- [ ] Phone/laptop device profiles remain distinct from settings/security
|
||||
profiles.
|
||||
- [ ] Every change is audited and successful transactions can be undone.
|
||||
- [ ] CI catches user-facing Config keys or settings controls missing schema and
|
||||
IPC coverage.
|
||||
Loading…
Reference in a new issue