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).
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=Trueneed 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:
- Note the values from
config.tomlfor sections that overlap with ConfigStore - Use
PUT /v1/api/admin/settings/{key}or the console Settings tab to set each value - Remove the migrated sections from
config.toml - 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.