Files
turnstone/docs/governance.md
T
Patrick Buckley b450b9ad20 fix(providers): align GPT-5.6 with the GA API surface
- every 5.6 tier accepts effort "max" and reasoning.mode
  "standard"/"pro" (GA docs: pro is a request mode on any GPT-5.6
  model) -- drop the Sol-only gating
- GPT-5.6 deprecates prompt_cache_retention; send
  prompt_cache_options={"ttl": "30m"} (its only supported lifetime)
  and keep the 24h retention policy for pre-5.6 models
- never inject commercial cache params into local lanes: dropped from
  the Chat Completions lane (which serves only openai-compatible and
  google) and gated off the compat-pinned Responses lane -- a gpt-5*
  served-model name is not an OpenAI account
- account cache writes: usage *_tokens_details.cache_write_tokens
  flows into cache_creation_tokens (5.6 bills writes at 1.25x the
  uncached input rate)
- drop non-string verbosity/reasoning_mode overrides with a warning
  instead of raising on unhashable capability-JSON values
- keep ModelCapabilities' public positional prefix stable by appending
  the verbosity/pro fields at the tail; pin it with a constructor test
- openai floor 2.44 -> 2.45, the first release with the typed
  prompt_cache_options kwarg
2026-07-10 15:59:40 -07:00

13 KiB
Raw Permalink 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):

Role Permissions
admin read, write, approve, admin.users, admin.roles, admin.orgs, admin.policies, admin.skills, admin.audit, admin.usage, admin.schedules, admin.watches, tools.approve, workstreams.create, workstreams.close
operator read, write, workstreams.create, workstreams.close
viewer read

Custom roles can be created with any subset of the valid permissions. 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 creation modal 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