Files
turnstone/docs/governance.md
T
Patrick Buckley be165c1971 feat: MCP resource and prompt discovery with read_resource tool (#44)
* feat: MCP resource and prompt discovery with read_resource tool

Extends MCPClientManager with resource and prompt discovery alongside
existing tool support. Resources and prompts are discovered on connect,
cached per-server with copy-on-write rebuilds, and refreshed via push
notifications, periodic polling, or manual /mcp refresh.

New read_resource built-in tool reads MCP resources by URI. Requires
user approval (same as MCP tool calls) since resources are served by
external MCP servers. Resource catalog injected into system message
with XML delimiters. Error messages sanitized to prevent leaking
server internals to the model.

Prompt discovery stores prefixed names (mcp__server__prompt) and
exposes get_prompt_sync() for future use_prompt tool (Chunk D).

/mcp command now shows tools, resources, and prompts. Docs and
diagrams updated.

* feat: MCP prompt governance sync with origin tracking and readonly guards

Migration 009 adds origin, mcp_server, and readonly columns to
prompt_templates. MCP prompts discovered by MCPClientManager are
automatically synced into the governance table as read-only templates
with origin="mcp".

Sync engine handles: create on connect, update on prompt refresh,
delete when prompts are removed from server. Manual templates take
precedence on name collision (MCP prompt skipped with warning).

Admin API returns 403 on update/delete of readonly templates. Console
UI shows MCP origin badge and disables edit/delete buttons. Storage
backends gain get_prompt_template_by_name, list_prompt_templates_by_origin,
and delete_prompt_templates_by_server methods.

Also addresses PR #44 review feedback: concurrent.futures.TimeoutError
handling in sync dispatch, XML-escape resource catalog descriptions,
resource template entries excluded from _resource_map, URI collision
warnings, needs_periodic capability-aware computation, malformed JSON
primary key fallback for read_resource.

* feat: use_prompt tool, prompt catalog, and PR review hardening

New use_prompt built-in tool invokes MCP prompt templates by name,
expanding them into messages. Requires user approval (external MCP
servers). Prompt catalog injected into system message with XML
delimiters (up to 30 prompts, HTML-escaped).

Prompt listener registered in session for catalog rebuild on changes.

Addresses PR #44 review feedback:
- _init_system_messages() now uses copy-on-write (build locally,
  assign atomically) so background thread callbacks never see
  partial system messages
- sync_prompts_to_storage() serialized behind _sync_lock to prevent
  races between set_storage() (main thread) and MCP background thread
- shutdown() clears listener lists to release callback references

Docs and diagrams updated for 18 built-in tools.

* feat: granular tool policies for MCP resources, prompts, and tools

Policy evaluation now uses approval_label (falling back to func_name)
for fnmatch pattern matching, enabling fine-grained per-URI and
per-server policies:
- read_resource: mcp_resource__{normalized_uri}
- use_prompt: mcp__{server}__{prompt} (prefixed name)
- MCP tools: mcp__{server}__{tool} (was static "mcp_tool")

URI normalization resolves .. path segments to prevent traversal
bypasses in policy matching. Resource templates filtered from system
message catalog (not directly readable). use_prompt arguments
validated as dict with string coercion.

TypeScript SDK PromptTemplateInfo gains origin, mcp_server, readonly
fields. Governance docs updated with MCP policy patterns.

* feat: MCP visibility in server and console UIs

Server health endpoint includes mcp.servers, mcp.resources, mcp.prompts
counts. Server UI status bar shows magenta MCP indicator with tooltip.
Console cluster status bar shows MCP metrics with magenta LED dot.
Console node detail view shows per-node MCP summary. Console collector
aggregates MCP counts across nodes in overview.

Uses var(--magenta) design token with new --magenta-glow for theme
adaptation. ARIA roles on MCP status elements. Tooltips on console
MCP metric labels. Node MCP summary hidden on mobile (< 700px).

New diagram: 20-mcp-architecture.puml covering full MCP lifecycle
(connection, discovery, refresh, governance sync, policy, UI).

* fix: McpStatus in health schema, count properties, catalog name fidelity

Adds McpStatus model to HealthResponse (Python + TypeScript SDKs) so
typed clients see the mcp field from /health.

Addresses Copilot review feedback:
- resource_count/prompt_count properties avoid list allocation on
  /health and /metrics polls
- get_tools/resources/prompts return shallow-copied dicts to prevent
  callers from mutating internal cache
- Prompt names and arg names in system message catalog are NOT
  HTML-escaped (model must use exact strings in use_prompt calls);
  only descriptions are escaped

* fix: OpenAPI spec McpStatus + diagram approval column accuracy

Adds McpStatus schema and optional mcp field to HealthResponse in
openapi-server.json, matching the Python schema and TypeScript types.

Fixes tool pipeline diagram: math, web_fetch, web_search correctly
shown as auto-approve (not "Yes" for approval).
2026-03-12 14:49:58 -07:00

7.5 KiB

Governance

Turnstone governance provides role-based access control (RBAC), tool execution policies, prompt templates, 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) — 15 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.templates, 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 15 valid permissions.

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

Prompt Templates

Reusable system message templates with variable substitution:

  • Variables: {{variable_name}} placeholders in content
  • Categories: general, engineering, support, custom, mcp
  • Default flag: is_default=true templates intended for new workstreams
  • Storage: prompt_templates table with JSON variables array
  • MCP sync: MCP server prompts are auto-synced into prompt_templates with origin="mcp", mcp_server set, and readonly=True. Manual templates take precedence on name collision. Admin UI shows origin badge and disables edit/delete for MCP-sourced templates. See docs/tools.md MCP Prompts section

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, tool call count, model, ws_id
  • Querying: GET /v1/api/admin/usage with group_by (day/hour/model/user) and time range filtering
  • 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, 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 templates
usage_events Per-request token/tool 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
Prompt Templates 4 (CRUD) admin.templates
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

5 new tabs added to the admin panel (10 total):

  • Roles — CRUD roles, permission checkbox grid, user role assignment modal
  • Policies — CRUD tool policies with colored action badges (green/red/amber)
  • Templates — CRUD prompt templates with wide modal, textarea editor
  • 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.

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 a 15-item 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