Files
turnstone/docs/settings.md
T
Patrick Buckley a917bf2690 docs: apply Copilot review feedback on PR #367
All eight suggestions verified against source before applying:

- docs/settings.md — ConfigStore key names are `model.plan_alias` /
  `model.task_alias` (not `plan_model` / `task_model`); updated in
  both the overview list and the plan/task overrides table.
- docs/security.md — `src` claim values now reflect what actually
  gets minted: `password`, `database` (from API-token exchange),
  `oidc`, plus service origins `console`, `cli`, `channel`.
- docs/sdk.md — `upload_attachment(ws_id, filename, data, *,
  mime_type=...)` matches the real SDK signature; `bytes`-returning
  helper is `get_attachment_content` (not `download_attachment`);
  code example reordered so it doesn't collide on `filename=` kwarg.
- docs/architecture.md — "prior `plan` tool call" → "prior
  `plan_agent` tool call" so wording stays consistent with the
  renamed tool.
- docs/tools.md — `plan_agent` `primary_key` is `goal`, not
  `prompt`, in both the primary-key table and the summary table
  (matches the JSON schema in turnstone/tools/plan_agent.json).
2026-04-16 16:01:24 -07:00

12 KiB

System Settings

See also: Settings Architecture diagram

The system settings feature provides database-backed configuration for server nodes. Settings are stored in the system_settings table and managed through the admin API or console Settings tab. This replaces config.toml for non-bootstrap settings on server entry points, while the CLI continues to read config.toml directly.

Overview

Settings follow a typed registry pattern: every storable setting has a SettingDef entry in settings_registry.py with type, default, description, validation constraints, and a restart_required flag. Unknown keys are rejected at the API boundary.

At runtime, ConfigStore loads all settings from storage into an in-memory cache. Reads are lock-free dict lookups on an immutable snapshot. Writes acquire a lock, persist to storage, and swap the cache atomically.


Precedence

Settings resolution differs between entry points:

Entry point Chain
Server (turnstone-server) CLI flag > ConfigStore > registry default
CLI (turnstone) CLI flag > config.toml > argparse default

The server's apply_config() ignores config.toml sections that overlap with ConfigStore. A startup warning is logged for each overlapping key, directing users to the admin Settings API.


Per-Model Sampling Overrides

The global model.temperature, model.max_tokens, and model.reasoning_effort settings serve as cluster-wide defaults. Individual models can override these via per-model settings in the model_definitions table (admin Models tab).

Resolution order for sampling parameters:

Priority Source
1 (highest) Per-model override (set in Models tab)
2 Global default (set in Settings tab)
3 Registry default (code)

When a per-model override is NULL (empty in the UI), the global default is used. Switching models via /model <alias> re-resolves sampling parameters from the new model's overrides or global defaults.

Removed settings: model.name and model.context_window have been removed from ConfigStore. Model names and context windows are now configured per-model in the Models tab. A startup warning is logged if these keys appear in config.toml.

Plan / task agent overrides

plan_agent and task_agent sub-sessions resolve independently from the conversation model so operators can pick a cheaper/faster model for autonomous loops:

Setting Purpose
model.plan_alias Alias used for plan_agent sub-sessions. Falls back to [model].plan_model in config.toml, then [model].agent_model, then the session's active model.
model.task_alias Alias used for task_agent sub-sessions. Same fallback chain as plan_alias.
model.plan_effort Reasoning effort for plan_agent (none / minimal / low / medium / high / xhigh / max). Defaults to high.
model.task_effort Reasoning effort for task_agent. Empty string means "inherit from the session".

All four are live-editable from the Settings tab and take effect on the next sub-agent invocation — no restart required.


Bootstrap vs ConfigStore

Bootstrap settings are required before storage is available (database connection, Redis, auth secrets, server bind address). These stay in config.toml and environment variables.

Category Section Where
API credentials [api] config.toml / env
Database [database] config.toml / env
Auth [auth] config.toml / env
Console bind [console] config.toml / env

ConfigStore settings are loaded from the database after storage initialization:

Section Settings
model default_alias, temperature, max_tokens, reasoning_effort, plan_alias, task_alias, plan_effort, task_effort
session instructions, retention_days, compact_max_tokens, auto_compact_pct
tools timeout, truncation, agent_max_turns, skip_permissions, search, search_threshold, search_max_results
server workstream_idle_timeout, max_workstreams
cluster node_fan_out_limit, mcp_max_servers
mcp config_path, refresh_interval, registry_url
ratelimit enabled, requests_per_second, burst, trusted_proxies
health backend_probe_interval, backend_probe_timeout, circuit_breaker_threshold, circuit_breaker_cooldown
judge enabled, model, provider, base_url, api_key, confidence_threshold, max_context_ratio, timeout, read_only_tools, output_guard, redact_secrets, cancel_on_approval
interface close_tab_action, theme
skills discovery_url
memory relevance_k, fetch_limit, max_content, nudge_cooldown, nudges

Settings are addressed by dotted key (e.g. memory.relevance_k). Each has a declared type (int, float, str, bool), optional min_value/max_value range, optional choices list, and an is_secret flag.


Storage

The system_settings table (migration 015) stores settings as JSON-encoded values with a composite primary key of (key, node_id):

Column Type Description
key text Dotted setting key (e.g. model.temperature)
value text JSON-encoded value
node_id text Node ID for per-node overrides (empty string = global)
is_secret int 1 if the setting contains secrets
changed_by text Username of last editor
created text ISO timestamp
updated text ISO timestamp

Per-node overrides layer on top of global settings. When ConfigStore loads, it fetches global settings first, then overlays per-node values.


Admin API

Four endpoints on the console server, all requiring the admin.settings permission.

GET /v1/api/admin/settings

List all settings with their effective values, defaults, and metadata.

Response: 200

{
  "settings": [
    {
      "key": "model.temperature",
      "value": 0.7,
      "source": "storage",
      "type": "float",
      "description": "Sampling temperature",
      "section": "model",
      "is_secret": false,
      "node_id": "",
      "changed_by": "admin",
      "updated": "2026-03-14T10:00:00",
      "restart_required": false
    }
  ]
}

GET /v1/api/admin/settings/schema

Return the full registry catalog (all defined settings with metadata). Useful for building dynamic admin UIs.

Response: 200

{
  "schema": [
    {
      "key": "model.temperature",
      "type": "float",
      "default": 0.5,
      "description": "Sampling temperature",
      "section": "model",
      "is_secret": false,
      "min_value": 0.0,
      "max_value": 2.0,
      "choices": null,
      "restart_required": false
    }
  ]
}

PUT /v1/api/admin/settings/{key}

Update a setting. The value is validated against the registry (type coercion, range, choices). Secret settings (is_secret=true) cannot be written via the API -- they must be configured via config.toml or environment variables.

Path parameters:

Parameter Type Description
key string Dotted setting key (e.g. model.temperature)

Request body:

{
  "value": 0.7,
  "node_id": ""
}
Field Type Required Default Description
value any yes -- New value (type-coerced against registry)
node_id string no "" Node ID for per-node override

Response (success): 200

{
  "key": "model.temperature",
  "value": 0.7,
  "source": "storage",
  "type": "float",
  "description": "Sampling temperature",
  "section": "model",
  "is_secret": false,
  "node_id": "",
  "changed_by": "admin",
  "updated": "",
  "restart_required": false
}

Errors:

Status Condition
400 Unknown key, invalid value, type mismatch, out of range
403 Secret setting (must use config.toml or env)

DELETE /v1/api/admin/settings/{key}

Reset a setting to its registry default by removing it from storage.

Path parameters:

Parameter Type Description
key string Dotted setting key

Query parameters:

Parameter Type Required Default Description
node_id string no "" Node ID (empty = global)

Response (success): 200

{"status": "ok", "key": "model.temperature", "default": 0.5}

Response (not found): 404

{"error": "Setting 'model.temperature' has no stored value"}

Secret Settings

Settings with is_secret=True (currently only judge.api_key) are blocked from the write API with a 403 response. This prevents accidental exposure through the admin UI or audit logs. Secret settings must be configured via config.toml or environment variables.

The list endpoint masks secret values: stored secrets appear as "***" rather than their actual value.


Hot Reload

ConfigStore caches all settings in memory for fast, lock-free reads. To refresh the cache after external changes (e.g. direct database edits or cluster-wide propagation):

POST /v1/api/_internal/config-reload

This triggers ConfigStore.reload(), which re-reads all settings from storage and atomically swaps the cache. The version counter increments on every reload.

Behavior after reload:

  • New workstreams pick up updated values immediately (via session_factory)
  • Existing sessions keep their frozen configuration (settings are captured at workstream creation time, not read on every turn)
  • Settings marked restart_required=True need a server restart to take effect

Migration from config.toml

On startup, warn_migrated_settings() scans config.toml for keys that are now managed by ConfigStore. Each overlap produces a warning:

WARNING config.toml [model] temperature is now managed via Settings API —
this value will be ignored. Use the admin Settings tab or
PUT /v1/api/admin/settings/model.temperature to configure.

To migrate:

  1. Note the values from config.toml for sections that overlap with ConfigStore
  2. Use PUT /v1/api/admin/settings/{key} or the console Settings tab to set each value
  3. Remove the migrated sections from config.toml
  4. Restart the server to verify no warnings

SDK

Python

from turnstone.sdk import TurnstoneConsole

with TurnstoneConsole("http://localhost:9090", token="tok_xxx") as admin:
    # List all settings with effective values
    result = admin.list_settings()
    for s in result["settings"]:
        print(f"{s['key']} = {s['value']} (source: {s['source']})")

    # Get the schema catalog
    schema = admin.get_settings_schema()

    # Update a setting
    admin.update_setting("model.temperature", value=0.7)

    # Update with per-node override
    admin.update_setting("model.temperature", value=0.3, node_id="node-2")

    # Reset to default
    admin.delete_setting("model.temperature")

TypeScript

import { TurnstoneConsole } from "@turnstone/sdk";

const admin = new TurnstoneConsole({
  baseUrl: "http://localhost:9090",
  token: "tok_xxx",
});

// List all settings
const result = await admin.listSettings();
for (const s of result.settings) {
  console.log(`${s.key} = ${s.value} (source: ${s.source})`);
}

// Get schema catalog
const schema = await admin.getSettingsSchema();

// Update a setting
await admin.updateSetting("model.temperature", { value: 0.7 });

// Reset to default
await admin.deleteSetting("model.temperature");

Architecture

See Settings Architecture diagram for the full data flow covering server startup, admin API writes, hot reload, and settings precedence.