# 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](diagrams/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 ` 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 ` 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](console.md) for the full tab list and [docs/settings.md](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`