diff --git a/README.md b/README.md
index d679d727..db6ec718 100644
--- a/README.md
+++ b/README.md
@@ -147,7 +147,7 @@ Turnstone includes a built-in governance layer for enterprise deployments — ma
- **Usage tracking** — per-request token and tool metrics, aggregation by day / model / user, automatic 90-day pruning
- **Audit logging** — append-only event trail for all admin mutations, IP-aware, 365-day retention
-All governance features are managed through the console admin panel (10 tabs) and the full REST API. See [docs/governance.md](docs/governance.md) for setup and configuration.
+All governance features are managed through the console admin panel (13 tabs) and the full REST API. Runtime settings (model, tools, rate limiting, health, judge, memory) are configurable via the admin Settings tab — no config file edits or restarts needed for most changes. See [docs/governance.md](docs/governance.md) for setup and [docs/settings.md](docs/settings.md) for the settings reference.
### Intent Validation (LLM Judge)
diff --git a/docs/architecture.md b/docs/architecture.md
index 88a72183..09c30eea 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1018,8 +1018,8 @@ Three hierarchical scopes control endpoint access:
- **Console** is the auth management hub — it hosts the admin endpoints for
creating users, issuing API tokens, and managing channel mappings. User
records and token hashes live in the shared storage backend. The console
- dashboard includes an **admin panel** (Users and Tokens tabs) for managing
- credentials through the browser.
+ dashboard includes an **admin panel** (13 tabs) for managing
+ credentials, governance, and runtime settings through the browser.
- **Server** is a JWT validator only — it validates tokens on each request but
never creates users or tokens. Both processes share the same `jwt_secret`
(via `TURNSTONE_JWT_SECRET` env var or `[auth].jwt_secret` config).
@@ -1393,7 +1393,8 @@ enforcement tracks consumption in `session.send()` with 80% warning and
100% approval gate via the `__budget_override__` synthetic tool name.
The console admin panel adds 6 governance tabs (Roles, Policies, Templates,
-WS Templates, Usage, Audit) for a total of 11 tabs, all permission-gated.
+WS Templates, Usage, Audit), a Memories tab, and a Settings tab (form-based
+editor for all ConfigStore settings) for a total of 13 tabs, all permission-gated.
Both Python and TypeScript SDKs expose governance methods on the console
client.
diff --git a/docs/console.md b/docs/console.md
index c326ff3c..fc48fe09 100644
--- a/docs/console.md
+++ b/docs/console.md
@@ -421,8 +421,9 @@ The browser maintains a local `clusterState` object that mirrors the cluster sna
Accessed via the "admin" button in the header (visible when authenticated
with `approve` scope). Provides user, API token, channel link, and workstream
-template management with 11 tabs (see also [Governance](governance.md) for
-the Roles, Policies, Templates, WS Templates, Usage, and Audit tabs):
+template management with 13 tabs (see also [Governance](governance.md) for
+the Roles, Policies, Templates, WS Templates, Usage, and Audit tabs, and
+[Settings](settings.md) for the database-backed configuration editor):
**Users tab:**
diff --git a/docs/governance.md b/docs/governance.md
index 209d9dd6..8e0a9ff3 100644
--- a/docs/governance.md
+++ b/docs/governance.md
@@ -99,7 +99,7 @@ Workstream templates are behavioral profiles applied at workstream creation —
**Admin API:** 7 endpoints under `/v1/api/admin/ws-templates` (list, create, get, update, delete, version history) plus a read-only summary at `/v1/api/ws-templates`. Permission: `admin.ws_templates`.
-**Console UI:** "WS Templates" tab (11th admin tab) with CRUD table, create/edit modals (name, description, system prompt source toggle, model, auto-approve, per-tool auto-approve, temperature, reasoning effort, max tokens, agent max turns, token budget, enabled), and version history modal. "Profile" dropdown on workstream creation modal. "WS Template" dropdown on scheduler create/edit modals.
+**Console UI:** "WS Templates" tab with CRUD table, create/edit modals (name, description, system prompt source toggle, model, auto-approve, per-tool auto-approve, temperature, reasoning effort, max tokens, agent max turns, token budget, enabled), and version history modal. "Profile" dropdown on workstream creation modal. "WS Template" dropdown on scheduler create/edit modals.
**Token budget enforcement:** Tracked in `session.send()`. At 80% consumption, emits an info message. At 100%, the next turn requires explicit approval via the `__budget_override__` synthetic tool name (reuses existing approval UI — inline in browser, Discord buttons, bridge auto-approve). The synthetic name can be targeted by tool policies (e.g. `__budget_override__` → `allow` for admins).
diff --git a/tests/test_model_registry.py b/tests/test_model_registry.py
index 4c3b5806..9f764228 100644
--- a/tests/test_model_registry.py
+++ b/tests/test_model_registry.py
@@ -28,7 +28,7 @@ class TestModelConfig:
)
assert cfg.alias == "local"
assert cfg.model == "qwen3-32b"
- assert cfg.context_window == 131072 # default
+ assert cfg.context_window == 32768 # default
def test_custom_context_window(self) -> None:
cfg = ModelConfig(
diff --git a/turnstone/bootstrap.py b/turnstone/bootstrap.py
index e257cddf..87e19550 100644
--- a/turnstone/bootstrap.py
+++ b/turnstone/bootstrap.py
@@ -122,6 +122,15 @@ This is a one-time endpoint that only works when zero users exist.
Subsequent governance setup (roles, policies, templates) uses the console admin API \
with the JWT returned from setup.
+## Runtime Settings (ConfigStore)
+After the stack is running, ~40 runtime settings (model, temperature, max_tokens, \
+reasoning_effort, tool timeout, rate limiting, health probes, judge config, memory \
+config, etc.) are configurable via the admin Settings tab in the console — no \
+config.toml edits or restarts needed for most changes. These settings are stored in \
+the database and apply cluster-wide. The `.env` file only needs bootstrap-critical \
+settings (database, Redis, auth, ports, API keys). Tell users they can fine-tune \
+model and behavioral settings after deployment through the admin panel.
+
## Built-in Roles
- **Admin** (`builtin-admin`): Full access — read, write, approve, all admin.* permissions
- **Operator** (`builtin-operator`): read, write, workstreams.create, workstreams.close
diff --git a/turnstone/cli.py b/turnstone/cli.py
index b101f98d..efa71bd5 100644
--- a/turnstone/cli.py
+++ b/turnstone/cli.py
@@ -799,8 +799,8 @@ def main() -> None:
parser.add_argument(
"--context-window",
type=int,
- default=131072,
- help="Context window size in tokens (default: 131072)",
+ default=0,
+ help="Context window size in tokens (0 = auto-detect from model)",
)
parser.add_argument(
"--compact-max-tokens",
@@ -986,10 +986,12 @@ def main() -> None:
else:
model, detected_ctx = detect_model(client, provider=provider_name)
- # Use detected context window when the user hasn't explicitly set one
+ # Use detected context window, fall back to CLI override or 32768
context_window = args.context_window
- if detected_ctx and context_window == 131072: # default unchanged
+ if detected_ctx and not context_window: # 0 = auto-detect
context_window = detected_ctx
+ elif not context_window:
+ context_window = 32768
# Build model registry (reads [models.*] sections from config.toml)
from turnstone.core.model_registry import load_model_registry
diff --git a/turnstone/console/server.py b/turnstone/console/server.py
index 413eb481..f48f61e7 100644
--- a/turnstone/console/server.py
+++ b/turnstone/console/server.py
@@ -2834,6 +2834,8 @@ async def admin_settings_schema(request: Request) -> JSONResponse:
"max_value": defn.max_value,
"choices": defn.choices,
"restart_required": defn.restart_required,
+ "help": defn.help,
+ "reference_url": defn.reference_url,
}
)
diff --git a/turnstone/console/static/admin.js b/turnstone/console/static/admin.js
index 2fcfa03f..e224551e 100644
--- a/turnstone/console/static/admin.js
+++ b/turnstone/console/static/admin.js
@@ -1611,6 +1611,13 @@ function _removeTrap(handler) {
// Global Escape key for admin modals
document.addEventListener("keydown", function (e) {
if (e.key !== "Escape") return;
+ // Close any open settings help popover first
+ var openHelp = document.querySelector('.settings-help-popover[style=""]');
+ if (openHelp) {
+ e.preventDefault();
+ _closeAllSettingsHelp();
+ return;
+ }
var cu = document.getElementById("create-user-overlay");
if (cu && cu.style.display !== "none") {
e.preventDefault();
@@ -1789,13 +1796,554 @@ function _confirmCallback() {
}
// ---------------------------------------------------------------------------
-// Settings (stub — full implementation is a separate project)
+// Settings — form-based editor grouped by section
// ---------------------------------------------------------------------------
+var _settingsOriginal = {}; // original values for dirty detection
+
+// Section display order
+var _settingsSectionOrder = [
+ "model",
+ "session",
+ "tools",
+ "server",
+ "mcp",
+ "ratelimit",
+ "health",
+ "judge",
+ "memory",
+];
+
+function _settingsSectionLabel(section) {
+ var labels = {
+ model: "Model",
+ session: "Session",
+ tools: "Tools",
+ server: "Server",
+ mcp: "MCP",
+ ratelimit: "Rate Limiting",
+ health: "Health",
+ judge: "Judge",
+ memory: "Memory",
+ };
+ return labels[section] || section;
+}
+
function loadSettings() {
var el = document.getElementById("admin-settings-content");
- if (el)
- el.innerHTML = '
Settings coming soon
';
+ if (!el) return;
+
+ Promise.all([
+ authFetch("/v1/api/admin/settings").then(function (r) {
+ if (!r.ok) throw new Error("Failed to load settings");
+ return r.json();
+ }),
+ authFetch("/v1/api/admin/settings/schema").then(function (r) {
+ if (!r.ok) throw new Error("Failed to load schema");
+ return r.json();
+ }),
+ ])
+ .then(function (results) {
+ var valuesArr = results[0].settings || [];
+ var schemaArr = results[1].schema || [];
+
+ // Build schema lookup
+ var schemaMap = {};
+ for (var i = 0; i < schemaArr.length; i++) {
+ schemaMap[schemaArr[i].key] = schemaArr[i];
+ }
+
+ // Merge values + schema
+ var merged = {};
+ for (var j = 0; j < valuesArr.length; j++) {
+ var v = valuesArr[j];
+ var s = schemaMap[v.key] || {};
+ merged[v.key] = {
+ key: v.key,
+ value: v.value,
+ source: v.source,
+ type: v.type || s.type || "str",
+ default_value: s.default !== undefined ? s.default : "",
+ description: v.description || s.description || "",
+ section: v.section || s.section || "",
+ is_secret: v.is_secret || false,
+ min_value: s.min_value,
+ max_value: s.max_value,
+ choices: s.choices || null,
+ restart_required: v.restart_required || false,
+ changed_by: v.changed_by || "",
+ updated: v.updated || "",
+ help: s.help || "",
+ reference_url: s.reference_url || "",
+ };
+ }
+
+ _settingsOriginal = {};
+
+ // Group by section
+ var grouped = {};
+ var keys = Object.keys(merged);
+ for (var k = 0; k < keys.length; k++) {
+ var item = merged[keys[k]];
+ var sec = item.section || "other";
+ if (!grouped[sec]) grouped[sec] = [];
+ grouped[sec].push(item);
+ }
+
+ _renderSettings(el, grouped);
+ })
+ .catch(function (err) {
+ el.innerHTML =
+ '
";
+ });
+}
+
+function _renderSettings(container, grouped) {
+ var html = "";
+
+ for (var i = 0; i < _settingsSectionOrder.length; i++) {
+ var sec = _settingsSectionOrder[i];
+ var items = grouped[sec];
+ if (!items || items.length === 0) continue;
+
+ html +=
+ '
';
+ html +=
+ '
';
+ html += "" + _settingsSectionLabel(sec) + "";
+ html += "
";
+ html +=
+ '
';
+
+ for (var j = 0; j < items.length; j++) {
+ html += _renderSettingRow(items[j]);
+ }
+
+ html += "
";
+ }
+
+ // Render any sections not in the explicit order
+ var allSections = Object.keys(grouped);
+ for (var s = 0; s < allSections.length; s++) {
+ if (_settingsSectionOrder.indexOf(allSections[s]) === -1) {
+ var extra = grouped[allSections[s]];
+ html +=
+ '
';
+ html +=
+ '
';
+ html += "" + _settingsSectionLabel(allSections[s]) + "";
+ html += "
";
+ html +=
+ '
';
+ for (var x = 0; x < extra.length; x++) {
+ html += _renderSettingRow(extra[x]);
+ }
+ html += "
";
+ }
+ }
+
+ container.innerHTML = html;
+
+ // Store original values for dirty detection
+ var inputs = container.querySelectorAll("[data-setting-key]");
+ for (var n = 0; n < inputs.length; n++) {
+ var inp = inputs[n];
+ var key = inp.getAttribute("data-setting-key");
+ if (inp.type === "checkbox") {
+ _settingsOriginal[key] = inp.checked;
+ } else {
+ _settingsOriginal[key] = inp.value;
+ }
+ }
+}
+
+function _renderSettingRow(item) {
+ var shortKey =
+ item.key.indexOf(".") !== -1
+ ? item.key.substring(item.key.indexOf(".") + 1)
+ : item.key;
+ var escapedKey = escapeHtml(item.key);
+ var escapedShort = escapeHtml(shortKey);
+ var escapedDesc = escapeHtml(item.description);
+
+ var html = '
';
+
+ // Label column
+ html += '
';
+ html += '
';
+ html += escapeHtml(shortKey);
+ if (item.help) {
+ html +=
+ ' ';
+ }
+ html += "
";
+ if (item.description) {
+ html += '
' + escapedDesc + "
";
+ }
+ if (item.help) {
+ html += '
';
+ html +=
+ '' + escapeHtml(item.help) + "";
+ if (item.reference_url) {
+ html +=
+ ' learn more';
+ }
+ html += "