258 lines
12 KiB
Markdown
258 lines
12 KiB
Markdown
---
|
|
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.
|