Desktop settings contract

Status: accepted authority contract, version 0.4.0 (0.2.0 added the summary topic and read view, decision; 0.3.0 added snapshot schema 2, decision; 0.4.0 adds key bindings, decision). Full desktop consumer integration remains in development. No frozen ABP wire bytes change.

settingsd serves one explicitly initialised profile in the initial slice. settings provides shared headless types, resolver, consumer and snapshot reducer; design::DesignReadProjection is render data that cannot construct an accepted design or fork compiler apply lineage. Consumers are the desktop applications and compd's Quoin shell.

Served verbs

All trusted mesh operators can invoke these verbs. Requests carry JSON bodies; responses preserve ordinary ABP rc conventions: 0 for handled results and 10 for refusal, invalid input or unknown verbs. Domain status is structured in the body.

VerbInput and result
settings.describeNo mutation; version, fields, ranges, defaults, limits and implemented/deferred capabilities
settings.get{binding:{instance,profile}, view?, schema?}; view full (default): complete accepted snapshot under snapshot, in schema 1 (default) or 2 (each distinct design projection once under designs, contexts naming it by digest; other schemas refused with unsupported_schema); summary: the profile's Summary (schema, binding, incarnation, revision, design revision, source digest, appearance names, custom_source, keymaps when any are set, terminal when any value is set, and email when any value is set; never projections or a source) under summary; both with publication/recovery status
settings.appearance.get{binding:{instance,profile}, schema?}; read-only appearance projection (mode, contrast, sRGB accent) tagged with incarnation and revisions; the reply also includes the atomic snapshot (in schema 1 or 2, as for settings.get) for shared Consumer validation; wrong_target on a foreign binding
settings.validateFenced apply-shaped body; resolve/compile candidate without a write or receipt; valid with current base identity or diagnostics/conflict
settings.applyFenced batch described below; durable changed/unchanged receipt
settings.resetApply-shaped body with empty changes and explicit reset paths; same transaction semantics
settings.statusBinding plus optional operation_id; accepted/published revision and retained receipt or unknown_operation

The profile identifier and instance use 1–64 ASCII letters, digits, dash or underscore. Operation IDs use the same character set, up to 128 bytes. Revisions are canonical decimal u64 strings, including on strict-data boundaries; numeric, overflowing, signed and leading-zero representations are refused. Panel thickness accepts a whole numeric value such as 48 or 48.0, within its declared range; Mix data numbers use floating representation. Fractional values, numeric strings and overflow are refused before acceptance.

{
  "binding": {"instance": "example", "profile": "default"},
  "expected_incarnation": "authority-readback-uuid",
  "expected_revision": "1",
  "operation_id": "example-batch-1",
  "changes": {
    "appearance.mode": "light",
    "ui.text_scale": 1.1,
    "shell.panels.bottom.thickness": 48
  },
  "reset": []
}

An optional request_digest is checked against BLAKE3 of the canonical serde JSON representation of the typed request, with request_digest set to null. Object maps are sorted; reset-list order remains part of the request. Use the shared request digest API rather than hashing arbitrary JSON text. Reusing an operation ID with different input is refused. Receipt lookup/deduplication precedes expected revision conflict, after target/incarnation checks. An old retained operation can retrieve its original receipt after subsequent edits. Retry the identical request to recover its receipt. Updating a fence changes the digest and requires a new operation ID. Validation checks the present fence; another writer may still win before a later apply.

Retain at most 128 receipts, inside the accepted document. A no-op writes its receipt durably without increasing desktop revision or publishing a new snapshot. Missing/evicted IDs return unknown_operation: opaque IDs have no expiry order, and the initial contract cannot prove expired_operation. Resolve ambiguity by readback and a newly fenced operation; exactly-once is not promised beyond retention. Restores establish a new incarnation and clear foreign-history receipts. Status includes publication_pending and recovering separately from durable acceptance; a changed result can remain unpublished after broker failure.

Initial data and resolution

The schema covers appearance scheme/style/mode/contrast, window decorations (csd or ssd) and caption side (right or left) plus embedded or complete strict-data design source; UI density/text scale/reduced motion; named panels with edge/mode/thickness; ordered shell pages; and whole app override records with scheme/mode/contrast/text scale. Describe supplies exact ranges. Unknown fields/scopes are refused. Field reset restores a package default; removing an app override restores profile inheritance. Per-field authored provenance beyond these initial concrete profile values is later work. The package source is pinned in accepted storage at profile creation. Null appearance.source selects that pinned source, including after binary upgrades; it does not silently adopt the upgraded binary's embedded defaults. Adopting a new package source requires an explicit mutation with a new revision. The accepted effective digest also fences compiler interpretation drift. A changed interpretation requires explicit migration rather than changing the same accepted identity on reload.

The accepted record carries a format number (absent in format 1; now 2). On open the record's seal is verified as stored, in its own format, so tampering is still refused. An older record is then upgraded by one rule: everything authored is kept, the effective interpretation is resolved again from the authored desktop and the pinned source (derived data is never trusted across formats), both digests are sealed at the current format, and the backup and then the primary are replaced atomically. A second open changes nothing. An authored field added later is omitted at its default, so older records still verify; a change to the effective interpretation bumps the format.

Validate the whole candidate and all advertised app contexts before acceptance. Compilation checks the source's claimed contexts. App overlays affect their context alone; profile high contrast takes precedence. A custom source is at most 256 KiB. Encoded requests are at most 384 KiB. Inline snapshots are at most 960 KiB, reserving 64 KiB for ABP/broker envelope overhead under the current 1 MiB retained limit. Larger settings are refused until native immutable artifact delivery is implemented. Required fonts/assets and runtime live capability are not yet advertised.

Snapshot schema 1 includes binding, incarnation, desktop revision, resolved-design generation, source digest, complete desktop and effective per-app/desktop design projections. The resolved-design generation is owned by the authority and survives restart; it is not a broker sequence or a reconstituted compiler apply handle. Clients must confirm an incarnation change through fresh bound readback before installing it. Work tickets fence superseded asynchronous completion. Conflicting payloads at the same incarnation/revision trigger a fresh read; if the bound authority confirms the contradiction, report it and preserve the installed values rather than enter an endless readback loop. Fresh decoded topic deliveries use Reducer.observe. Captured tickets are for asynchronous completions; a new delivery can advance the state while an older bound read is pending, and that read cannot roll the revision back.

The settings library API is now 0.5.0; authority verbs are 0.3.0. Snapshot schema 1 is the default; schema 2 is its compact wire form, decoded into the same in-memory snapshot. The shared native consumer follows the compact topic and reads schema 2; consumers on their own library copy stay on schema 1 until ported. The shared consumer performs subscribe-before-get over the host's existing supervised Bus connection through its optional native executor. It owns no transport/task/incoming receiver. Hosts feed connection generations, deliveries and explicit queue loss, execute at most one current action, and cancel superseded work. Bootstrap buffers one latest candidate; loss coalesces one later full read. A bounded history suppresses repeated rejected confirmation candidates. Transport recovery has one pending backoff from 250 ms to 30 seconds; success/disconnect cancels it. Protocol/target/schema refusal is visible and quiescent until a new event or explicit refresh. Every native call is bounded to one second; the host must also cap the combined initial bootstrap to one second and show a labelled fallback while recovery continues. Use the same initial deadline with native::execute_until for subscribe and read. Recovery exposes an absolute deadline, preserved during malformed-event storms; arrivals cannot restart or bypass it. Canonical app/shell constructors choose app:<id> or desktop contexts. Captured read tickets and baselines distinguish real same-incarnation rollback from a valid read racing a newer publication. Cancel old executor futures when their current work ticket changes.

One context-specific change plan separates paint, text, layout, resources, motion and optional shell geometry. Source/provenance/revision-only changes advance evidence without rendering work. Received accepted data is distinct from staged and applied data. Prepare all resources, then on the renderer event loop check the stage ticket immediately before synchronous activation and acknowledgement, without an intervening await. Old connection, consumer and stage completions cannot claim application. Hosts must fence the actual swap as well as its report. Identical render inputs need no swap; application never proves presentation. Consumer::evidence() reports binding, context, connection generation, confirmation, presentation kind, accepted (current) and UI-acknowledged (applied) identities, plus normal and fallback faults. Each identity carries incarnation, canonical string revision, design revision and source digest. Absent identities are null. Losing confirmation demotes installed current data to LastGood; an installed fallback is never an authority fence. The shell exposes this same evidence at shell.props.get path settings, including root and dotted reads such as settings.applied.revision, behind its existing caller and connection checks. Evidence does not assert a broker participant record or frame presentation. settings_cache supplies the shared host's persistence identity, cache fault and fallback diagnostics. The persistence identity is historical and must match current applied evidence to describe that capture. Quoin uses the shared worker's service-owned persistent cache while its existing Bus supervisor starts; terminal refusal preserves offline resources, and only explicit initial rejection permits a configured scene service override. compd.truth.slots.<id>.decided independently reads the geometry owner's decided size, or null when absent, without falling back to committed client geometry. The native desktop geometry gate compares these readbacks with real client configures and captured pixels. DTO field additions require explicit classification in the shared comparison; source/provenance exclusions are deliberate rather than wildcard omissions. Shared fallback preparation tries validated retained bootstrap data, then local cache, then current embedded defaults. The host supplies readiness checks for every reference used by its context and shell capability on its worker. A fenced completion produces the same immutable UI stage; it never seeds the authority reducer or mutation fences. Installed data has explicit Current, Retained, Cached, LastGood and Embedded labels. Identical fresh authority data can promote cache/embedded evidence without a redundant swap.

Optional settings/cache stores applied authority/retained generations in an existing isolated absolute cache directory. Its versioned JSON envelope binds instance/profile, context, shell capability, interpretation and content digest. Bounded directory-relative regular-file reads refuse symlinks in every path component and cannot block on FIFOs. Before reuse, complete projections must match current compilation of their authored inputs; unavailable old pinned package sources and compiler drift are refused visibly. Exact JSON float round trips preserve these checks. Digests detect corruption, not file origin; valid cache data still supplies no authority evidence. A stable locked serial writer fences superseded applied-generation saves, permits retry of the latest failed attempt and reports ambiguous post-rename failure. All I/O belongs off the UI loop. Cache failure preserves usable applied data. Real font/asset loading, artifact preparation and GUI fallback/first-map timing remain pending.

Shell panel preferences

Quoin prepares appearance and the global panel policy together, then installs both on the compositor loop before acknowledging the settings revision. Each record's explicit edge selects one of four panels on every current and future output; the record ID is provenance, independent of a connector or page name. dock, overlay and hidden map to runtime docked, pinned and hidden modes. The default profile owns the bottom panel in dock mode, requesting 40 logical pixels. An edge without pages draws nothing and reserves no work area.

Thickness is requested logical pixels. Runtime edge ranges, the active page's minimum and the output/opposite-edge budget fit the request. A bottom page with a 52-pixel minimum therefore presents 52 pixels for the default 40-pixel request. Panel rows include a nullable settings object with the record ID, requested_px, fitted_px, presented_px, reserved_px and constraint names. Output shrinkage fits the panel; regrowth recovers the request. Page selection and registration use the existing carousel and panel model.

Settings owns the mode and thickness of each declared edge. Conflicting legacy panel writes refuse SETTINGS_MANAGED before changing interaction state; exact current mode or settled-size requests are no-ops. Transient show/hide and page selection remain available. Persistent toggles cannot undock managed panels. For managed edges Toggle changes transient visibility only while hidden; docked or pinned modes treat it as a successful no-op. Corner-menu mode entries are disabled while managed; activating a disabled selection is an inert action. Removing a profile record restores that edge's captured local mode and settled thickness. Saving a page selection keeps those local values, so profile preferences do not leak into the local state file. Thickness or appearance updates retain interaction holders; an actual transition to hidden retires invalid keyboard/menu holders.

Duplicate records targeting one edge and non-empty shell.page_order lack a runtime identity mapping and fail whole shell preparation. The authority's schema can accept them, but Quoin reports the preparation fault and retains the last good appearance and panel policy. Correct them through a new fenced settings mutation. This is consumer capability validation, not a rollback of the accepted authority revision. Empty page ordering preserves local order.

Requested maximised windows keep their owning output and follow only changes to their desired content rectangle; removal chooses an available output while retaining the original unmaximise geometry. Scene reservations are reconciled after settings activation and before frame work, including without the comp Bus port. An unchanged target does not emit another maximise configure while the client is slow to acknowledge or replace its buffer. The compositor's decided slot remains authoritative during these transitions; older legally acknowledged buffers do not undo a newer target. Existing aspect-preserving fit and inverse input mapping remain in use.

Fullscreen continues to use the whole output. Its compositor record, pending protocol intent and committed state fence work-area reconfiguration through delayed entry/exit commits. After fullscreen releases ownership, the next normal dispatch restores the latest maximised content rectangle, even if the work-area map has not changed again. Window geometry changes schedule redraw and retarget the human Wayland pointer, respecting grabs. This does not by itself prove stationary hover or scene bounds before/after rendering.

The protocol fixtures exercise actual xdg configure/ack/buffer commits using the production geometry executor. Their headless output has no renderer; native Bus delivery fixtures and protocol fixtures do not establish their composed rendered settings-to-frame behaviour. Persistent tiling, new maximise intent with no outputs, native window/input/frame evidence and VT latency acceptance remain outstanding. Current text scale changes the title font, while decoration extents continue to come from the selected style's fixed height and border.

Key bindings

The profile's keymaps holds the person's key bindings: per application id, per action id, the chords chosen. An empty list unbinds the action; an action not listed keeps the application's own default, which ships with the application. The field is omitted while empty, so profiles written before it keep their seals, and it does not affect the resolved design.

  • Apply: keymaps.<app> takes the whole map (null drops every change for that app); keymaps.<app>.<action> takes a list of chords (null returns the action to its default). Action ids are dotted: everything after the app id is the action id.
  • Chords: Ctrl, Alt and Shift, each at most once and in any order, then one key: a name of ASCII letters and digits (ArrowLeft, F6, PageDown, B, 1) or =, -, /. settingsd checks this grammar; the application checks that it knows the key.
  • Limits: 16 applications, 256 actions each, 4 distinct chords per action, 32 bytes per chord, 64 bytes per action id.
  • Followers: the summary carries keymaps, and each application applies its own entry when the revision changes (toolkit::Registry::rebind).

Terminal

The profile's terminal holds the term's preferences. Each default is the term's behaviour before the group existed, except padding (8 px instead of 1 px) and the focus ring: accent at strength 0.25 gives a quiet tint from the current theme accent. neutral remains available; accent at strength 0.5 matches the earlier half-accent ring. Each value is omitted at its default, so older profiles keep their seals. It does not affect the resolved design. Terminal colours are not a setting: they follow the session theme.

FieldType and rangeDefault
terminal.font.familystring or null, 1–128 bytes, no control charactersnull: appearance.mono_family
terminal.font.sizenumber 6–48the design's UI type size
terminal.font.weightinteger 100–900, step 100400
terminal.paddinginteger 1, 2, 4, 8 or 16 (px)8
terminal.scrollback.linesinteger 0–10000001000
terminal.scroll.speedinteger 10–200 (rows per wheel line, scaled by speed/40; touchpad travel in cell heights at that scale)40
terminal.border.widthinteger 1–4 (px)1
terminal.cursor.shapeblock, underline or barunderline
terminal.cursor.blinkbooleanfalse
terminal.focus.ringneutral or accentaccent
terminal.focus.strengthnumber 0–1, used with accent0.25
terminal.mouse.copy_on_selectbooleanfalse
terminal.env.term[a-z0-9-]{1,64}; new tabs and panesxterm-256color

Apply and reset take each path, or terminal for the whole group. The summary carries terminal when any value is set.

Email

The profile's email holds the email app's JMAP accounts and mail options. It is not secret and cannot hold a secret. The snapshot and summary are retained on the Bus for any client to read, so an account has no password field and every record in the group refuses unknown keys: an account carrying password is refused with validation_failed, and nothing is written. Passwords live only in the email app's private secrets file, set through its own write-only verb. The group is omitted at its defaults, so older profiles keep their seals and the store format stays 3. It does not affect the resolved design.

FieldType and rangeDefault
email.accountslist of accounts, at most 64, ids unique; replaced whole, in tree order[]
email.default_accountan id in email.accounts, or nullnull: the first enabled account
email.mail.page_sizeinteger 10–500100
email.mail.mark_read_on_openbooleantrue
email.mail.confirm_deletebooleantrue
email.mail.quote_on_replyboolean: whether a plain Reply quotes the originaltrue
email.mail.reply_position"above" or "below": where the reply text goes in a quoted reply"above"

Each account is a record of exactly these keys:

KeyType and rangeDefault
id[a-z0-9-]{1,64}, stable; the writer mints acct- and 12 hex digitsrequired
namethe tree label: 1–128 bytes, no control characters, no surrounding spacerequired
urlthe JMAP base: https://host[:port][/path], the host a DNS name, IPv4 or bracketed IPv6 address; no userinfo (user:secret@, so no password rides in a URL), query, fragment, backslash, trailing slash or space; at most 2048 bytesrequired
emailthe login and default From: one @ with text on both sides, no space; at most 254 bytesrequired
ca_pathan absolute path to a PEM bundle, or null for the system rootsnull
enabledbooleantrue
signatureplain text appended to new drafts: at most 4096 characters, no control characters but newline and tab""

A refused account is named by position: email.accounts[2].url. The default account must name an account, so removing it from the list needs email.default_account changed or reset in the same batch. Apply and reset take each path, or email for the whole group (null resets it too); email.mail alone is not a path. The summary carries email when any value is set; settings::email::from_snapshot and from_summary read it.

Topics and storage

Publish retained settingsd.desktop.changed.<profile> (the snapshot in schema 1), then retained settingsd.desktop.compact.<profile> (schema 2), then retained settingsd.desktop.summary.<profile> (its Summary), all owned by the registered settingsd service. The unscoped base names are reserved too. A revision is published once all three topics are; a failure retries all three. All clients may subscribe; ordinary publishers/clearers cannot replace its canonical state. Noded strips inner routing headers and stamps broker_service independently of caller input. The publisher repopulates retained state at startup and reconnect, even without a settings mutation. Generic props-prefix ownership is deliberately not used: embedding a profile in that prefix changes its matched service owner. Failed publication starts a single pending retry job, with exponential backoff from 250 ms capped at 30 seconds. Success removes the job and every timer. Reconnection or a new revision restarts the backoff. There is no idle heartbeat; publication_pending remains visible throughout a failed pending job.

Following from an app. An app on a plain Bus connection (no owner stamps) subscribes to the profile's summary topic, treats a delivery only as a hint, and reads the look with settings.get's summary view, never the complete snapshot: settings::follow::Follower keeps one read in flight (hints during it fold into one more), ignores an answer older than one already seen (same incarnation, lower or equal revision), adopts the instance from wrong_target once (never the profile), and reports settingsd missing so the app keeps its theme file. toolkit::session_look turns the profile's appearance into the app's session theme; an appearance naming its own design package (source) or a name the app does not know keeps the theme file's.

Default root: resolved MixOS Etc directory / settings/<profile>/. Explicit test roots are supported. One retained writer.lock inode provides a process flock; it is never removed on normal exit. The root must be an owned directory not writable by other users. desktop.conf.mix holds schema, binding, incarnation, revisions, authored desktop, pinned package source, receipts and a verified BLAKE3 content digest and effective interpretation digest. These digests detect corruption and interpretation drift; they are not an authentication signature. A synced desktop.previous.conf.mix is written before replacing the accepted generation. Files are private, never evaluated as Mix source. Reads, lock acquisition, replacements and syncs are relative to the same held directory descriptor. Replacing/detaching the profile directory or writer-lock inode fences the existing writer. Newly created directory entries are synced in their parents before initial acceptance.

Replacement uses exclusive temporary create, write, file sync, directory-relative rename and directory sync. Pre-rename errors leave accepted state unchanged. Post-rename sync errors report outcome_unknown and fence further mutations until recovery; they cannot be reported as a known uncommitted failure. Corrupt input is preserved before a valid backup is restored with a new incarnation. Newer schemas or mismatched target bindings are not overwritten by a fallback. Corruption evidence is hard-linked and synced before the atomic replacement; recovery never removes the primary before its replacement is ready. A missing established primary fails visibly even with a backup; automatic recovery applies only to a present corrupt primary. I/O errors, strict shape changes and intact documents requiring a changed compiler interpretation fail without an automatic backup rollback.

settingsd init --instance example explicitly creates a profile and refuses existing accepted/backup data. settingsd serve --instance example never materialises defaults for a missing established store. Plain settingsd seed --instance example validates an existing profile without rewriting valid state; it refuses a wholly missing directory. First installation explicitly uses settingsd seed --allow-create --instance example as the service account. A retained writer.lock is establishment evidence: even --allow-create refuses a missing established primary without a backup. Existing-store corruption uses the same validated, evidence-preserving recovery as serve. The unit validates with plain seed before serving, binds the instance to the machine hostname, and the session target wants/upholds it. Settings readiness never blocks application or compositor startup. The image provisions the authority account's writable MixOS settings parent and seeds once explicitly. Automatic unit startup never uses --allow-create; missing mounts/directories fail visibly rather than resetting preferences. Isolated native image adoption still needs runtime evidence.

Validation and remaining scope

Machine fixtures: authority.spec.mix. Rust tests cover precision, patches, projection round-trip, writer locking, changed/no-op receipts, lost replies, conflicts, digest reuse, eviction, backup restore and future schemas. tests/settings/authority_test.mix runs real ABP publication, unrelated operator mutation, forged publish/clear refusal, authority restart and cold broker restart against isolated noded instances. Broker loss keeps the authority alive and its writer exclusive; the shared consumer reconnects through the same client and the authority repopulates the empty retained broker without a semantic edit. The broker-restart fixture also starts a headless cold consumer from persisted cache while offline, then promotes it to current with no redundant stage after fresh ABP readback. Resource readiness is a fixture inventory, not GUI proof. Another native fixture keeps an authority registered but deliberately withholds read replies, exercising the combined bootstrap deadline and expired work. OS process lifecycle here belongs to the test fixture; application calls and observations remain native ABP.

This slice does not claim GUI propagation, renderer acknowledgements, presentation, GUI offline-fallback/first-map timing, candidate import/watch, immutable artifacts, full field provenance, named-profile management, compatibility, scheduled policy, preview, routed observation or replication. Every deferred feature must extend the same authority and shared contracts, with appropriate contract versions and migration. Native VT/image presentation and latency gates remain outstanding.

This page as Markdown · This page describes the current main branch of MixOS.