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

9.5 KiB

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.