- 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
155 lines
9.5 KiB
Markdown
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.
|