Files
tai/settings.md
T
retoor fa4e638a5c Vault-backed settings catalog with three-function API (1.41.0)
- SETTINGS_CATALOG: 28 env-mapped settings with type, default,
  required/nullable, strict options, preferred (reserved), encrypted
  flags, per-key bool word sets; routing split into
  CONFIG_SETTING_FIELDS (Config build) and SETTING_EXPORT_KEYS
- Store.get_or_create_settings / get_all_settings /
  upsert_settings: global+profile scopes, atomic strict validation,
  canonical forms, created/updated timestamps, env/db/default source
  reporting; encrypted values sealed at rest, rotation-aware,
  plaintext-boot refusal covers the table
- boot_settings in boot() and default_runner: Config second pass
  (env wins, CLI --no-opencode wins) plus env export for peripheral
  and applied keys; applied keys printed at boot
- settings.md contract with full catalog and exclusion rationale;
  16 SettingsTests incl. catalog self-check and doc drift guard
2026-10-18 14:15:35 +02:00

155 lines
9.5 KiB
Markdown

# settings.md
Vault-backed settings: one catalog, three functions, env always wins.
Every `TAI_*` knob that behaves like a setting lives in the `settings`
table and is readable and overridable through
`get_or_create_settings` / `upsert_settings` / `get_all_settings`.
`SettingsTests.test_settings_doc_names_every_key` fails when this file
drifts from the code.
## Precedence
For every catalog key, the effective value resolves in this order:
1. **env**: a non-empty `${ENV_VAR}` (empty counts as unset, matching
every existing reader)
2. **db, own scope**: the profile row when one is stored
3. **db, global scope**: the shared fallback row
4. **catalog default**
Two run-scoped inputs outrank even env: CLI `--theme` beats the theme
setting, and CLI `--no-opencode` beats the stored opencode flag. A
stored model pin counts as explicit, so per-directory model defaults
do not override it.
Boot applies vault settings in `boot()` (and in `default_runner` for
workers) through `boot_settings`: `Config` fields first, then the
peripheral keys are exported into `os.environ` so the existing
readers, which all run later, pick them up unchanged. Applied keys are
printed (`settings from vault: ...`) and also exported for nested
processes. A second boot pass is idempotent: exported env wins, so
nothing is applied twice.
## Row shape
Every function returns the same row dicts:
| Field | Meaning |
|---|---|
| `key`, `env`, `desc` | catalog identity and one-line docs |
| `type` | `string`, `bool`, or `float` |
| `default` | python value used when nothing is set (may be null) |
| `required` | null and empty are rejected; a required key must always resolve |
| `nullable` | whether null (unset) is a legal stored value |
| `options` | strict list the value must be one of, or null |
| `preferred` | suggestions that still allow free input, or null (reserved: no v1 entry uses it) |
| `encrypted` | sealed at rest with the vault key when true |
| `stored` | decrypted canonical string in the vault, or null |
| `value` | effective typed value after precedence |
| `source` | where `value` came from: `env`, `db`, or `default` |
| `scope`, `created`, `updated` | owning scope and ISO UTC timestamps |
Canonical stored forms: bool as `1`/`0`, float as `str(float)`,
strings raw. Env bools accept `1/true/yes/on` and `0/false/no/off`
(case-insensitive, surrounding whitespace ignored); anything else
falls back to the default, exactly like the readers. Env floats use
plain decimals (`12`, `2.5`); anything else falls back to the default.
Three bool keys extend the env word sets (see catalog): those legacy
spellings stay readable from env but are never written by upsert.
## API
- `get_or_create_settings(keys=None, scope="global")`: ensure rows
exist (all 28 when `keys` is None, a subset or single key otherwise)
and return their merged rows. Unknown keys and bad scopes raise
`ValueError`, as does a required key that resolves to nothing.
- `get_all_settings(scope="global")`: the same merge, read-only:
creates no rows.
- `upsert_settings(items, scope="global")`: validate a
`{key: value}` mapping and store it atomically; anything invalid
raises `ValueError` and writes nothing. `None` clears a nullable
key back to null. Returns fresh rows for the written keys.
Scopes are `global` or a profile name (`PROFILE_RE`). A profile
literally named `global` shares the global scope.
## Encryption
Keys marked encrypted are sealed with `seal.lock` on write and opened
on read; plaintext vaults store them as-is, same as vault secrets. At
rest they are indistinguishable from other sealed blobs (`tai1$...`).
Passphrase rotation re-seals every sealed settings value and leaves
plaintext values alone. Booting a vault that holds sealed settings
without the passphrase refuses to start, like the other sealed
tables. Encrypted settings are config, not vault secrets: they are
not covered by secret redaction, exactly like their env equivalents.
## Catalog
28 keys. Types, defaults, and flags below are asserted by
`SettingsTests.test_catalog_self_check`, so this table cannot drift
silently.
| Key | Env | Type | Default | Req | Null | Options | Enc | What it does |
|---|---|---|---|---|---|---|---|---|
| `model` | `TAI_MODEL` | string | `muse` | yes | no | - | no | Default model id. A stored pin counts as explicit. |
| `backends` | `TAI_BACKENDS` | string | null | no | yes | - | yes | Extra OpenAI-compatible backends: `;`-separated `label\|base\|key[\|model][\|profile]`; a `$VAR` key reads another env var. |
| `openrouter_key` | `OPENROUTER_API_KEY` | string | null | no | yes | - | yes | OpenRouter key. Most free models work without one. |
| `openrouter_models` | `TAI_OPENROUTER_MODELS` | string | null | no | yes | - | no | Comma-separated model ids. Null means live discovery. |
| `openrouter_enabled` | `TAI_OPENROUTER` | bool | true | no | no | - | no | Offer the OpenRouter backend. |
| `opencode_enabled` | `TAI_OPENCODE` | bool | true | no | no | - | no | Offer the opencode backend. CLI `--no-opencode` wins. |
| `opencode_models` | `TAI_OPENCODE_MODELS` | string | null | no | yes | - | no | Comma-separated model ids. Null means live discovery. |
| `devplace_key` | `DEVPLACE_API_KEY` | string | null | no | yes | - | yes | DevPlace key. Null disables the fallback backend. |
| `devplace_base` | `TAI_DEVPLACE_BASE` | string | `https://devplace.net/openai/v1` | no | no | - | no | DevPlace API base URL. Trailing slashes stripped. |
| `devplace_models` | `TAI_DEVPLACE_MODELS` | string | null | no | yes | - | no | Comma-separated model ids. Null means live discovery. |
| `pollinations_enabled` | `TAI_POLLINATIONS` | bool | true | no | no | - | no | Offer the Pollinations backend. |
| `pollinations_models` | `TAI_POLLINATIONS_MODELS` | string | null | no | yes | - | no | Comma-separated model ids. Null means live discovery. |
| `zen_enabled` | `TAI_ZEN` | bool | false | no | no | - | no | Opt in to the opencode.ai zen backend. |
| `zen_models` | `TAI_ZEN_MODELS` | string | null | no | yes | - | no | Comma-separated model ids. Null means live discovery. |
| `retry_seconds` | `TAI_RETRY_SECONDS` | float | 0.0 | no | no | - | no | Seconds to retry all backends on total failure. 0 means forever. |
| `voice` | `TAI_VOICE` | string | `en-US-EmmaMultilingualNeural` | no | no | - | no | Edge TTS voice name for speak. |
| `theme` | `TAI_THEME` | string | `dark` | no | no | dark, light, nord, gruvbox, ansi | no | TUI theme, strict. CLI `--theme` wins. |
| `gemini_plan` | `TAI_GEMINI_PLAN` | bool | false | no | no | - | no | Try the Gemini plan approval variant first. Env also accepts `plan`. |
| `codex_sandbox` | `TAI_CODEX_SANDBOX` | bool | false | no | no | - | no | Re-enable Codex sandbox flags. Env also accepts `read-only`. |
| `box_caps` | `TAI_BOX_CAPS` | bool | true | no | no | - | no | Apply caps to old sandbox boxes. False skips. Env honors only `0` as false. |
| `muse_bin` | `MUSE_BIN` | string | null | no | yes | - | no | muse CLI path override. Null means PATH lookup. |
| `opencode_bin` | `OPENCODE_BIN` | string | null | no | yes | - | no | opencode CLI path override. Null means PATH lookup. |
| `grok_bin` | `GROK_BIN` | string | null | no | yes | - | no | grok CLI path override (then grokcli). Null means PATH lookup. |
| `claude_bin` | `CLAUDE_BIN` | string | null | no | yes | - | no | claude CLI path override. Null means PATH lookup. |
| `codex_bin` | `CODEX_BIN` | string | null | no | yes | - | no | codex CLI path override. Null means PATH lookup. |
| `gemini_bin` | `GEMINI_BIN` | string | null | no | yes | - | no | gemini CLI path override. Null means PATH lookup. |
| `opencode_client_name` | `OPENCODE_CLIENT_NAME` | string | null | no | yes | - | no | `x-opencode-client` header override. Null means built-in. |
| `opencode_client_ua` | `OPENCODE_CLIENT_USER_AGENT` | string | null | no | yes | - | no | User-Agent override for zen. Null means built-in. |
## Deliberately not cataloged
| Var | Why it stays out |
|---|---|
| `TAI_HOME` | locates the vault; cannot come from inside it |
| `TAI_PASSPHRASE` | unlocks the vault; cannot come from inside it |
| `TELEGRAM_BOT_TOKEN` | owned by `telegram.env` plus the service unit; move it only with a migration |
| `TAI_SESSION` | per-process derived context, resolved around store boot |
| `TAI_TUI_SOCK` | per-process socket rendezvous path |
| `TAI_TUI_VERIFY` | dev-only pipeline verification switch |
| `GRR_URL` | owned by `grr.json` (env wins there too) |
| `CAP_SESSION_CONFIG` | wire passthrough for one speak turn, not a setting |
| `TAI_PLEXER_*` | runtime plexer state, not settings |
| `TERM`, `NO_COLOR`, `SHELL`, `TMUX*`, `EDITOR`, `VISUAL`, `HISTFILE`, `USER`, `TZ`, `VIRTUAL_ENV`, `XDG_*`, `COLORFGBG`, `TERMINFO` | foreign standards tai reads but does not own |
Internal tuning constants (`WORKER_STEPS`, `CAP_*`, `BOX_*`, scheduler
intervals) are code, not settings: promoting one means a catalog
entry, a wiring decision, and a test, not a silent centrally-stored
override. `mplex.toml`, CAP fleet files, and per-directory backend
pins (`dir_defaults`) keep their own homes for the same reason.
## Maintenance
To add a setting: append a `setting_spec` to `SETTINGS_CATALOG`, route
it through `CONFIG_SETTING_FIELDS` (read at `Config` build) or
`SETTING_EXPORT_KEYS` (read later at call sites), extend the catalog
table above, and extend `SettingsTests`. The catalog self-check
asserts every default validates, every options list holds its
default, env names are unique, and the two routing tuples partition
the catalog exactly. The theme row is pinned against `TUI_THEMES` so
a new theme breaks the test until the options list follows.