Files
turnstone/docs/governance.md
T
Patrick Buckley 7a06f5e8bc refactor(session): make ModelLane the provider boundary (#979) (#989)
* refactor(session): make ModelLane the provider boundary (#979)

## Summary

This closes the model-lane ownership gap left by #832: `ChatSession` no longer stores raw provider/client handles. `ResolvedModelBinding` now carries the provider, client, model, capabilities, registry generation, and backend-auth configuration as one coherent snapshot.

- Atomically rebind existing sessions after model-registry changes while pinning each in-flight send, fallback, judge, output guard, task agent, title, compaction, perception, and voice operation to its initiating principal and binding.
- Fence UI publication, canonical trajectory folds, durable writes, streams, retries, child scopes, and judge work by generation. Stop can hand off to a successor without accepting late state; cancelled tools retain typed effect receipts, and concurrent approval batches resolve by exact cycle or call.
- Make create, fork, open, close, and delete race-safe with hidden `creating` reservations, incarnation-aware state tails, and an ACL-rechecked transaction that clones checkpoint-bounded history, configuration, project/persona state, and attachment references.
- Extend REST/OpenAPI and Python/TypeScript SDK contracts for create/fork inputs, routed-create metadata, live-workstream probes, targeted approvals, and structured cancellation results.
- Update architecture, storage, authentication, judge, channel, console, API, and SDK documentation, including regenerated architecture diagrams and OpenAPI artifacts.

## Validation

- SQLite suite: 11,188 passed, 9 skipped, 10 deselected
- PostgreSQL suite: 11,195 passed, 2 skipped, 10 deselected
- Live backend: 3 passed
- SSE recovery: 6 passed; browser recovery harness passed all scenarios
- Ruff: clean; 595 files correctly formatted
- mypy: 243 source files clean
- TypeScript: typecheck/build and 35 tests passed
- OpenAPI artifacts fresh; all 14 changed diagrams reproduce byte-for-byte
- `git diff --check` and Git LFS integrity clean

Closes #979.

* fix(deps): update nanoid for GHSA-2v37-7h3g-55p8

Refresh the transitive lock entry admitted by PostCSS so the TypeScript security gate no longer resolves the vulnerable custom-generator implementation.

Validation:
- npm ci
- npm audit --audit-level=moderate: 0 vulnerabilities
- TypeScript typecheck and build
- TypeScript tests: 35 passed

* fix(test): assert canonical model registry URLs

Replace prefix checks with exact canonical base URL assertions so the tests do not model incomplete URL validation.

Validation: tests/test_model_registry.py (185 passed); Ruff check/format; mypy.
2026-08-08 16:13:35 -07:00

13 KiB
Raw Blame History

Governance

Turnstone governance provides role-based access control (RBAC), tool execution policies, skills, usage tracking, and audit logging for the admin console.

Architecture

See diagram: 19-governance-architecture.puml.

RBAC (Roles & Permissions)

The permission model has two layers:

  1. Scopes (legacy) — read, write, approve. Checked by AuthMiddleware on every request based on URL path classification.
  2. Permissions (granular) — named permission strings checked per-endpoint by require_permission().

Built-in roles (seeded by migration 008 and extended by later feature migrations):

Role Permissions
admin Admin-default baseline: ordinary admin, lifecycle, tool-approval, coordinator, project, and persona capabilities. Explicit opt-in capabilities such as model.skills.write remain ungranted.
operator read, write, workstreams.create, workstreams.close, conversation.modify
viewer read

Custom roles can be created with any subset of the valid permissions. Built-in role permission overrides can grant or revoke individual capabilities, so the admin console is authoritative for the effective set on a deployment. The persona.create / persona.read / persona.write family gates persona administration; migration 063 seeds all three onto builtin-admin, and any role can be granted them through the standard role and permission-override editors.

Auth flow:

  1. User logs in (password or API token) → _load_user_permissions() aggregates permissions from all assigned roles
  2. _permissions_to_scopes() derives legacy scopes (any admin.*approve)
  3. JWT created with both scopes and permissions claims
  4. Middleware checks scope → handler checks permission via require_permission()

Tool Policies

Admin-defined rules that control tool execution:

  • Pattern matching: Glob syntax via fnmatch (e.g., bash*, file_write, *)
  • Actions: allow (auto-approve), deny (block), ask (normal approval flow)
  • Priority: Higher priority evaluated first, first match wins
  • Enforcement: evaluate_tool_policies_batch() called in WebUI.approve_tools() before the auto_approve check
  • MCP granular policies: MCP resources and prompts are evaluated using their approval_label for fine-grained control:
    • Resource reads: mcp_resource__{uri} (e.g., mcp_resource__file:///docs/* to allow, mcp_resource__* to deny all)
    • Prompt invocations: mcp__{server}__{prompt} (e.g., mcp__trusted__* to allow, mcp__* to require approval for all)
    • Built-in tools continue to use func_name for backward compatibility

Skills

Admin-curated system message skills injected at workstream startup. Skills also include session configuration (model, temperature, auto-approve, token budget, etc.) since workstream templates were merged into the skills system in v0.8.0.

  • Runtime behavior: Skills are loaded once at session creation and injected into the system message before user instructions. Skills set the baseline; instructions customize per-workstream behavior.
  • Default skills: All is_default=true skills auto-apply to new workstreams, concatenated in alphabetical order by name. Use name prefixes (e.g. 01-safety, 02-style) to control ordering.
  • Explicit selection: --skill <name> CLI flag, skill field on POST /v1/api/workstreams/new, console launcher dropdown, scheduled task config, and channel adapter config. An explicit skill replaces defaults.
  • Variables: Three built-in placeholders resolved at load time: {{model}} (active model name), {{ws_id}} (workstream ID), {{node_id}} (server node ID). Unrecognized placeholders are kept as-is.
  • Runtime switching: /skill <name> to switch, /skill clear to revert to defaults, /skill to show current. Persisted across resume.
  • Model-driven loading: The skill built-in tool lets the model discover and activate skills mid-conversation. search action finds skills by query (auto-approved); load action activates by name (requires user approval since it changes session behavior). Main session only.
  • Categories: general, engineering, support, custom, mcp
  • Content limit: 32 KB per skill (enforced on create/update)
  • Storage: prompt_templates table (stores skills) with JSON variables array. Migration 010 adds template column to scheduled_tasks.
  • MCP sync: MCP server prompts auto-sync into the prompt_templates table with origin="mcp", mcp_server set, and readonly=True. Manual skills take precedence on name collision. MCP-synced content updates reset is_default to prevent compromised servers from injecting defaults. Admin UI shows origin badge and disables edit/delete for MCP-sourced skills.
  • Spec fields: Skills support the full Agent Skills standard frontmatter: name, description, license, compatibility, metadata (author, version), allowed-tools. The license and compatibility fields are preserved on import and editable in the admin UI. See https://agentskills.io/specification.
  • Security scanning: Skills are automatically scanned at creation and update time. The scanner evaluates four risk axes: content risk (command execution, data exfiltration), supply chain risk (pipe-to-shell, transitive installs), vulnerability risk (prompt injection, insecure credentials), and declared capability risk (from allowed-tools in SKILL.md). Results populate the risk_level (safe/low/medium/high/critical) and scan_report (JSON breakdown) columns. These fields are system-managed and cannot be overwritten via the admin API.
  • Discovery: External skills can be discovered and installed from registries:
    • GET /v1/api/admin/skills/discover?q=... — search the skills.sh registry (or a custom registry via skills.discovery_url setting)
    • POST /v1/api/admin/skills/install — install from skills.sh or GitHub. Fetches the SKILL.md file, parses YAML frontmatter, creates a skill with origin="source" and readonly=True, stores bundled resources.
    • Admin UI: Skills tab has "Installed" / "Discover" pill toggle. Discovery view has search bar, result cards, and "Import from GitHub" modal.
    • SDK: discover_skills(q) and install_skill(source, skill_id=..., url=...) on both Python and TypeScript console clients.
  • Runtime config on installed skills: Installed (readonly) skills can have their runtime configuration edited — model, temperature, reasoning effort, token budget, max tokens, agent max turns, auto-approve, allowed tools, and enabled flag. The server restricts updates to these fields only via _SKILL_RUNTIME_CONFIG_FIELDS filtering; spec/content fields (name, description, tags, license, compatibility, content, activation) remain immutable. The admin UI shows "Save Config" instead of "Save" for these skills. Audit action: skill.update.config.
  • Admin UI: Create/Edit skill modals use a two-column spec manifest layout (left: Identity / Manifest / Deployment; right: Skill Content editor with monospace font). Runtime Config is a collapsible 3-column grid below. License uses an SPDX identifier dropdown (MIT, Apache-2.0, GPL-3.0, etc.). Installed skills show a cyan origin badge with source URL, spec fields are disabled, and all collapsible sections auto-expand in view mode.

Usage Tracking

Per-LLM-request token and tool call metrics:

  • Recording: on_status() in WebUI records a usage_event after each LLM response with prompt/completion tokens, cache tokens, tool call count, model, ws_id
  • Prompt caching: Anthropic automatic caching (cache_control: ephemeral) and OpenAI caching are enabled by default. Pre-5.6 GPT-5 models request prompt_cache_retention: 24h; GPT-5.6 uses prompt_cache_options: {"ttl": "30m"}. GPT-5.6 cache writes use the provider's 1.25× input-token rate. cache_creation_tokens and cache_read_tokens are tracked per request in usage_events and surfaced in the Usage admin tab
  • Querying: GET /v1/api/admin/usage with group_by (day/hour/model/user) and time range filtering — includes cache token aggregates
  • Prometheus: turnstone_tokens_total{type="cache_creation|cache_read"} counters on /metrics
  • Pruning: prune_usage_events(retention_days=90) and prune_audit_events(retention_days=365) run automatically via the console scheduler's periodic cleanup cycle

Audit Logging

Append-only trail of admin actions:

  • Recording: record_audit() helper called from all admin mutation handlers
  • Events captured: user.create, user.delete, token.create, token.revoke, channel.link, channel.unlink, role.create, role.update, role.delete, role.assign, role.unassign, policy.create, policy.update, policy.delete, template.create, template.update, template.delete, skill.create, skill.update, skill.delete, org.update
  • Querying: GET /v1/api/admin/audit with action/user/time filters + pagination

Database Schema

Migration 008 adds 7 tables:

Table Purpose
orgs Organizations (single default org for now)
roles Named permission bundles (3 builtin + custom)
user_roles User-to-role assignments (composite PK)
tool_policies Per-tool approve/deny/ask rules
prompt_templates Reusable system message skills
usage_events Per-request token/tool/cache metrics
audit_events Admin action log

Also adds org_id column to users table.

API Endpoints

All under /v1/api/admin/ (requires approve scope + granular permission).

Group Endpoints Permission
Users / Tokens / Channels 9 (CRUD) admin.users
Roles 7 (CRUD + assignment) admin.roles / admin.users
Orgs 3 (list, get, update) admin.orgs
Tool Policies 4 (CRUD) admin.policies
Skills 4 (CRUD) admin.skills
Personas 4 (list, create, get, edit/archive) persona.read / persona.create / persona.write
Schedules 6 (CRUD + runs) admin.schedules
Watches 3 (list, create, cancel) admin.watches
Usage 1 (aggregated query) admin.usage
Audit 1 (paginated, filtered) admin.audit

Full OpenAPI spec at /openapi.json and Swagger UI at /docs.

Admin Console UI

Governance-related tabs within the 18-tab admin panel:

  • Roles — CRUD roles, permission checkbox grid, user role assignment modal
  • Policies — CRUD tool policies with colored action badges (green/red/amber)
  • Prompts — Prompt-policy editor (heuristics for admin guardrails)
  • Skills — CRUD skills with wide modal, textarea editor; Discover pill for installing from skills.sh / GitHub; per-row scan badges (safe/low/med/high/critical)
  • Judge — Intent validation configuration and verdict history
  • Usage — Summary readouts + CSS bar chart, time range + group-by selectors
  • Audit — Filterable log with relative timestamps, load-more pagination

Tabs are permission-gated: hidden if the user lacks the required permission. See docs/console.md for the full tab list and docs/settings.md for the Settings tab that edits live ConfigStore values.

SDK

Both Python and TypeScript console SDKs expose governance methods:

Python (TurnstoneConsole / AsyncTurnstoneConsole):

  • list_roles(), create_role(), update_role(), delete_role()
  • list_user_roles(), assign_role(), unassign_role()
  • list_orgs(), get_org(), update_org()
  • list_policies(), create_policy(), update_policy(), delete_policy()
  • list_templates(), create_template(), update_template(), delete_template()
  • get_usage(since, group_by=...), get_audit(action=..., limit=...)

TypeScript (TurnstoneConsole):

  • Same methods with camelCase naming and typed interfaces

Security Considerations

  • Privilege escalation prevented: admin_assign_role blocks self-assignment and requires caller to hold a superset of the target role's permissions
  • Permission validation: Role create/update validates permissions against the permission allowlist (_VALID_PERMISSIONS)
  • Self-deletion blocked: admin_delete_user rejects attempts to delete your own account (matching the self-assignment guard on role endpoints)
  • Field allowlists: Storage update_* methods filter fields against allowlists (_ROLE_MUTABLE, _POLICY_MUTABLE, etc.) — handler bugs cannot overwrite role_id, builtin, created, or other protected columns
  • Bootstrap safety: handle_auth_setup fails and rolls back if admin role assignment fails, preventing locked-out first user
  • API token RBAC: _authenticate_api_token loads permissions from user's roles, ensuring API tokens are subject to RBAC enforcement
  • Policy evaluation is fail-open: If storage is unavailable, tool policies degrade to the existing approval flow (not auto-approve)
  • Audit IP resolution: _audit_context() prefers X-Forwarded-For for client IP when behind a reverse proxy, falling back to request.client.host