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