1# Sigil appliance configuration v123`sigil appliance config` is the local, machine-readable settings interface for4an unprivileged appliance controller such as the Decky plugin. It exposes a5small typed projection instead of accepting arbitrary TOML edits.67Every command requires the owner-only host configuration and explicit JSON8output:910```bash11sigil appliance config show --config ~/.config/sigil-spark/host.toml --json12sigil appliance config validate --config ~/.config/sigil-spark/host.toml --json <request.json13sigil appliance config set --config ~/.config/sigil-spark/host.toml --json <request.json14sigil appliance config commit --config ~/.config/sigil-spark/host.toml \15 --transaction <id> --expected-instance <id> --json16sigil appliance config rollback --config ~/.config/sigil-spark/host.toml \17 --transaction <id> --json18```1920The configuration file must be owned by the effective user and not writable by21group or other users. Its immediate directory must also be owned by that user,22must retain owner write and search permission, must not be a symlink, and must23not be writable by group or other users. A mode-`0755` configuration directory24is supported; mode `0700` remains the recommended appliance layout because it25also keeps filenames private. The daemon, `config check`, and every managed26write enforce this same integrity boundary before doing work.2728`set` and `commit` use the service's XDG runtime root to share its global29lifecycle lock and to bind baseline and candidate evidence to one exact runtime30directory. When `XDG_RUNTIME_DIR` is unavailable (for example, from a recovery31shell), pass the same owner-only mode-`0700` root to both commands with32`--runtime-dir /run/user/<uid>`; Sigil appends `sigil-spark`. An explicit root33takes precedence over the environment and must be absolute, owned by the34effective user, and not a symlink. Without either source, `set` and `commit`35fail closed. `rollback` needs no runtime evidence and can use only the durable36state lock when XDG runtime state is unavailable.3738The transaction journal binds `set` to the canonical runtime child with a39non-secret SHA-256 path identifier. `commit` must resolve to that same child40before transaction recovery or mutation, so copying otherwise-valid stopped41status evidence into a different runtime namespace cannot authorize commit.42Legacy journals without this identifier remain rollback-capable but cannot be43committed. Do not downgrade Sigil while a configuration transaction is pending;44older binaries may not understand journals written by this version.4546`show` returns the exact-byte SHA-256 revision, editable settings, and a pending47transaction summary. The editable v1 settings are:4849- Native dimensions or an explicit even width and height.50- Frame rate.51- CBR bitrate or CQP quantizer for a configured Gamescope PipeWire source.5253Source selection, codec, capture backend, executable and device paths,54PipeWire selectors, audio/input configuration, identity, and state paths are55not editable through this contract. Operator comments and all non-managed TOML56fields survive an update.5758Enrollment capacity is authorization state, not a host configuration field.59Authorization v2 durably supports at most 32 enrolled viewers, while this60implementation independently bounds concurrent native-MoQ v2 viewers with the61strict `max_viewers` host setting. The setting defaults to `3`, accepts only62`1` through `8`, and does not change the durable enrollment ceiling. It is a63direct TOML setting rather than part of the appliance config v1 editable64projection, so changing it follows the same reviewed host-configuration and65service-restart process as other non-managed fields.6667Multi-viewer operation is available only through `sigil/control/2` and the68authenticated native-MoQ generation. Every admitted viewer gets an independent69control connection, consume-once media attachment, feedback claim, and bounded70delivery state while sharing one video source, video encoder, optional Opus71source/encoder, signed broadcast, and keyframe coordinator. `control/1` and72grouped-v3 remain strictly one-client compatibility modes. An active legacy73generation rejects v2 admission, and any live v2 generation rejects legacy74admission, including while old sidecars are draining.7576Concurrent capacity and durable enrollment are intentionally separate:7778| Bound | Default | Hard limit | Scope |79|---|---:|---:|---|80| `max_viewers` | 3 | 8 | Simultaneously connected native-MoQ v2 viewers |81| `MAX_ENROLLED_VIEWERS` | fixed | 32 | Durable authorization-v2 viewer records |8283Slot-0 focus uses one bounded holder-approved handoff proposal, not a FIFO.84When the slot is free, the first eligible request wins. While occupied, one85requester may wait up to 15 seconds for the holder to approve, deny, or release86toward it; additional requests receive a busy response. A grant must activate87an input-v2 sidecar within 10 seconds or Sigil revokes it. Every transfer first88publishes a no-inject neutralizing state, resets all virtual input devices, and89only then publishes a successor focus generation.9091An operator may set `focus_owner = "viewer-<16 lowercase hex digits>"` to the92stable opaque handle shown by enrollment administration. That exact enrolled93viewer may request controller-confirmed preemption. The setting does not grant94view or input capability, does not allow another viewer to preempt, and does95not create an administrative or arbitrary preemption path. Like96`max_viewers`, it is a reviewed direct TOML setting and requires service97restart; omission disables owner preemption.9899Requests are strict JSON and contain an optimistic-concurrency revision:100101```json102{103 "schema_version": 1,104 "expected_revision": "sha256:<64 lowercase hexadecimal digits>",105 "settings": {106 "resolution": { "mode": "native" },107 "framerate": 60,108 "rate_control": { "mode": "cbr", "bitrate_kbps": 12000 }109 }110}111```112113For a synthetic source, `rate_control` must be `null`. A fixed resolution uses114`{"mode":"fixed","width":1280,"height":800}`; CQP uses115`{"mode":"cqp","quantizer":24}`. `validate` performs no writes. A no-op116`set` returns `changed: false`, no transaction ID, and does not require a117restart.118119## Controller sequence120121Only a stopped Sigil can be changed. The controller performs:1221231. Stop `sigil-host.service` and wait until it is inactive.1242. Call `set`. Sigil atomically installs the candidate and returns a transaction125 ID plus candidate revision. If needed, pass the service's runtime root with126 `--runtime-dir` and retain that exact value for `commit`.1273. Start the service and wait for appliance status to report a new instance in128 `ready` state with the candidate revision.1294. Stop the service cleanly and retain that exact instance ID.1305. Call `commit` with the transaction and instance IDs. If startup or shutdown131 fails, stop the service, call `rollback`, and restart the prior config.132133Commit succeeds only when the bounded runtime document is fresh, belongs to the134current host, reports the exact candidate revision and a daemon instance newer135than the one present at `set`, reached `ready` at least once, stopped cleanly,136and contains no runtime error. A running daemon holds the same lifecycle locks,137so set, commit, rollback, enrollment reset, and service execution cannot race.138Configured service startup takes those locks before its definitive config reload139and transaction recovery. An interrupted prepared install is therefore140completed before capture or networking starts; startup can never launch bytes141that were read before a concurrent `set`.142143## Durability and recovery144145Config files and requests are bounded to 64 KiB and 16 KiB respectively.146Revisions hash the exact bytes read from the same opened file descriptor used147for parsing. Writes and recovery artifacts are owner-only, no-follow, atomic,148and directory-synced.149150The journal records `prepared`, `pending_validation`, `committing`, or151`rolling_back`. Recovery deterministically finishes an interrupted candidate152install, commit cleanup, or byte-exact rollback. Live bytes that match neither153the journaled base nor candidate fail closed instead of being overwritten.154Only one transaction may be pending.155156Management failures write one JSON object to stderr with `schema_version: 1`157and one closed error code: `unsupported_schema`, `invalid_request`,158`validation_failed`, `revision_conflict`, `lifecycle_busy`,159`transaction_busy`, `transaction_pending`, `transaction_not_found`,160`transaction_conflict`, `health_not_proven`, or `unsafe_storage`. They never161include paths, configuration contents, or free-form internal errors.