Watch
1
0
Fork
You've already forked souveraine
0

extract music player deps - souveraine-player is its own crate now

This commit is contained in:
Fimeg 2026-07-19 22:11:33 -04:00
commit 8d661d2352
7 changed files with 1083 additions and 66 deletions

399
Cargo.lock generated
View file

@ -613,7 +613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
dependencies = [
"atk-sys",
"glib",
"glib 0.18.5",
"libc",
]
@ -623,10 +623,10 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
dependencies = [
"glib-sys",
"gobject-sys",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -653,6 +653,12 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atomic_refcell"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4"
[[package]]
name = "atty"
version = "0.2.14"
@ -1195,7 +1201,7 @@ checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
dependencies = [
"bitflags 2.11.1",
"cairo-sys-rs",
"glib",
"glib 0.18.5",
"libc",
"once_cell",
"thiserror 1.0.69",
@ -1207,9 +1213,9 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
dependencies = [
"glib-sys",
"glib-sys 0.18.1",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -1349,7 +1355,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
"smallvec",
"target-lexicon",
"target-lexicon 0.12.16",
]
[[package]]
name = "cfg-expr"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
dependencies = [
"smallvec",
"target-lexicon 0.13.5",
]
[[package]]
@ -2533,7 +2549,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.9",
]
[[package]]
@ -3355,7 +3371,7 @@ dependencies = [
"gdk-pixbuf",
"gdk-sys",
"gio",
"glib",
"glib 0.18.5",
"libc",
"pango",
]
@ -3368,7 +3384,7 @@ checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
dependencies = [
"gdk-pixbuf-sys",
"gio",
"glib",
"glib 0.18.5",
"libc",
"once_cell",
]
@ -3379,11 +3395,11 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
dependencies = [
"gio-sys",
"glib-sys",
"gobject-sys",
"gio-sys 0.18.1",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -3394,13 +3410,13 @@ checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gio-sys 0.18.1",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"pango-sys",
"pkg-config",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -3410,11 +3426,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
dependencies = [
"gdk-sys",
"glib-sys",
"gobject-sys",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"pkg-config",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -3426,7 +3442,7 @@ dependencies = [
"gdk",
"gdkx11-sys",
"gio",
"glib",
"glib 0.18.5",
"libc",
"x11",
]
@ -3438,9 +3454,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
dependencies = [
"gdk-sys",
"glib-sys",
"glib-sys 0.18.1",
"libc",
"system-deps",
"system-deps 6.2.2",
"x11",
]
@ -3580,8 +3596,8 @@ dependencies = [
"futures-core",
"futures-io",
"futures-util",
"gio-sys",
"glib",
"gio-sys 0.18.1",
"glib 0.18.5",
"libc",
"once_cell",
"pin-project-lite",
@ -3595,13 +3611,26 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
dependencies = [
"glib-sys",
"gobject-sys",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"system-deps",
"system-deps 6.2.2",
"winapi",
]
[[package]]
name = "gio-sys"
version = "0.22.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5"
dependencies = [
"glib-sys 0.22.8",
"gobject-sys 0.22.6",
"libc",
"system-deps 7.0.8",
"windows-sys 0.61.2",
]
[[package]]
name = "git2"
version = "0.19.0"
@ -3638,10 +3667,10 @@ dependencies = [
"futures-executor",
"futures-task",
"futures-util",
"gio-sys",
"glib-macros",
"glib-sys",
"gobject-sys",
"gio-sys 0.18.1",
"glib-macros 0.18.5",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"memchr",
"once_cell",
@ -3649,6 +3678,27 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "glib"
version = "0.22.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100"
dependencies = [
"bitflags 2.11.1",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys 0.22.8",
"glib-macros 0.22.6",
"glib-sys 0.22.8",
"gobject-sys 0.22.6",
"libc",
"memchr",
"smallvec",
]
[[package]]
name = "glib-macros"
version = "0.18.5"
@ -3663,6 +3713,18 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "glib-macros"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "glib-sys"
version = "0.18.1"
@ -3670,7 +3732,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
dependencies = [
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
name = "glib-sys"
version = "0.22.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233"
dependencies = [
"libc",
"system-deps 7.0.8",
]
[[package]]
@ -3718,9 +3790,20 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
dependencies = [
"glib-sys",
"glib-sys 0.18.1",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
name = "gobject-sys"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
dependencies = [
"glib-sys 0.22.8",
"libc",
"system-deps 7.0.8",
]
[[package]]
@ -3751,7 +3834,7 @@ dependencies = [
"log",
"presser",
"thiserror 1.0.69",
"windows 0.54.0",
"windows 0.58.0",
]
[[package]]
@ -3786,6 +3869,99 @@ dependencies = [
"xxhash-rust",
]
[[package]]
name = "gstreamer"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4527e1b9bae8d29ce137bde5b8eec8ae8f78f13ad00fc6e70cbe227d6ad027"
dependencies = [
"cfg-if",
"futures-channel",
"futures-core",
"futures-util",
"glib 0.22.8",
"gstreamer-sys",
"itertools 0.15.0",
"kstring",
"libc",
"muldiv",
"num-integer",
"num-rational",
"option-operations",
"pastey 0.2.2",
"pin-project-lite",
"smallvec",
"thiserror 2.0.18",
]
[[package]]
name = "gstreamer-app"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97f8ae9238c2352398dcc084de28df3f7099af216ac6c160b52318d23f25c010"
dependencies = [
"futures-core",
"futures-sink",
"glib 0.22.8",
"gstreamer",
"gstreamer-app-sys",
"gstreamer-base",
"libc",
]
[[package]]
name = "gstreamer-app-sys"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a74a8211e5d7df2f45b612c284ddf56b92bdf4e879e8ed72e7c46dd0842e158"
dependencies = [
"glib-sys 0.22.8",
"gstreamer-base-sys",
"gstreamer-sys",
"libc",
"system-deps 7.0.8",
]
[[package]]
name = "gstreamer-base"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91c94a4d3047d05dd6e1f6d91c74f61f56384c7ea1c9d0c1051572eeeb0138d"
dependencies = [
"atomic_refcell",
"cfg-if",
"glib 0.22.8",
"gstreamer",
"gstreamer-base-sys",
"libc",
]
[[package]]
name = "gstreamer-base-sys"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fbbc623dc066908ba10c43d629c21096508dea04796592a206c4edd864e37"
dependencies = [
"glib-sys 0.22.8",
"gobject-sys 0.22.6",
"gstreamer-sys",
"libc",
"system-deps 7.0.8",
]
[[package]]
name = "gstreamer-sys"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533fa8d28fc830eafccbcfcfddb390563ea5d3a351af2c3aab99e197e5f5b1ba"
dependencies = [
"cfg-if",
"glib-sys 0.22.8",
"gobject-sys 0.22.6",
"libc",
"system-deps 7.0.8",
]
[[package]]
name = "gtk"
version = "0.18.2"
@ -3799,7 +3975,7 @@ dependencies = [
"gdk",
"gdk-pixbuf",
"gio",
"glib",
"glib 0.18.5",
"gtk-sys",
"gtk3-macros",
"libc",
@ -3817,12 +3993,12 @@ dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gio-sys 0.18.1",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"pango-sys",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -4580,6 +4756,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
@ -4593,7 +4778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
dependencies = [
"bitflags 1.3.2",
"glib",
"glib 0.18.5",
"javascriptcore-rs-sys",
]
@ -4603,10 +4788,10 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
dependencies = [
"glib-sys",
"gobject-sys",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -4849,6 +5034,15 @@ dependencies = [
"libc",
]
[[package]]
name = "kstring"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e"
dependencies = [
"static_assertions",
]
[[package]]
name = "lab"
version = "0.11.0"
@ -4900,7 +5094,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
dependencies = [
"glib",
"glib 0.18.5",
"gtk",
"gtk-sys",
"libappindicator-sys",
@ -5108,6 +5302,32 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lofty"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec4feeff6c7d75093278133a06e827d7af6d2bfe20b0f331f9d10338a5ec7ca"
dependencies = [
"byteorder",
"data-encoding",
"flate2",
"lofty_attr",
"log",
"ogg_pager",
"paste",
]
[[package]]
name = "lofty_attr"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "log"
version = "0.4.29"
@ -5565,6 +5785,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "muldiv"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"
[[package]]
name = "murmurhash32"
version = "0.3.1"
@ -6371,6 +6597,15 @@ dependencies = [
"cc",
]
[[package]]
name = "ogg_pager"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216"
dependencies = [
"byteorder",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@ -6466,6 +6701,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "option-operations"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca39cf52b03268400c16eeb9b56382ea3c3353409309b63f5c8f0b1faf42754"
dependencies = [
"pastey 0.2.2",
]
[[package]]
name = "orbclient"
version = "0.3.55"
@ -6590,7 +6834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
dependencies = [
"gio",
"glib",
"glib 0.18.5",
"libc",
"once_cell",
"pango-sys",
@ -6602,10 +6846,10 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
dependencies = [
"glib-sys",
"gobject-sys",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -9040,7 +9284,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
dependencies = [
"futures-channel",
"gio",
"glib",
"glib 0.18.5",
"libc",
"soup3-sys",
]
@ -9051,11 +9295,11 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
dependencies = [
"gio-sys",
"glib-sys",
"gobject-sys",
"gio-sys 0.18.1",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"libc",
"system-deps",
"system-deps 6.2.2",
]
[[package]]
@ -9087,7 +9331,10 @@ dependencies = [
"figlet-rs",
"futures",
"git2",
"glib 0.22.8",
"glob",
"gstreamer",
"gstreamer-app",
"hex",
"hkdf",
"hound",
@ -9095,6 +9342,7 @@ dependencies = [
"keyring",
"keyring-core",
"libc",
"lofty",
"matrix-sdk",
"notify",
"num-bigint",
@ -9670,13 +9918,26 @@ version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [
"cfg-expr",
"cfg-expr 0.15.8",
"heck 0.5.0",
"pkg-config",
"toml 0.8.2",
"version-compare",
]
[[package]]
name = "system-deps"
version = "7.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
dependencies = [
"cfg-expr 0.20.8",
"heck 0.5.0",
"pkg-config",
"toml 1.1.2+spec-1.1.0",
"version-compare",
]
[[package]]
name = "tantivy"
version = "0.26.1"
@ -9888,6 +10149,12 @@ version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "target-lexicon"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tauri"
version = "2.11.1"
@ -11876,10 +12143,10 @@ dependencies = [
"gdk",
"gdk-sys",
"gio",
"gio-sys",
"glib",
"glib-sys",
"gobject-sys",
"gio-sys 0.18.1",
"glib 0.18.5",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"gtk",
"gtk-sys",
"javascriptcore-rs",
@ -11898,15 +12165,15 @@ dependencies = [
"bitflags 1.3.2",
"cairo-sys-rs",
"gdk-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gio-sys 0.18.1",
"glib-sys 0.18.1",
"gobject-sys 0.18.0",
"gtk-sys",
"javascriptcore-rs-sys",
"libc",
"pkg-config",
"soup3-sys",
"system-deps",
"system-deps 6.2.2",
]
[[package]]

View file

@ -33,6 +33,7 @@ git2 = { version = "0.19", default-features = false }
# Database (server mode)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "migrate", "chrono", "json"] }
# Concurrent collections (server sessions)
dashmap = "5"

View file

@ -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. |

View 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.

View 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 ~6080
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 02.
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 02 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.

View 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.

View 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.