Security & Governance¶
Precis¶
This document is the canonical reference for how Thinklio keeps data safe, controls what agents can do, and manages the credentials agents need to work with external systems. It consolidates the April 2026 security model, secrets vault specification, governance policy framework, and MCP credential and permission model into one layered reference.
Thinklio enforces security in depth across four layers: network, application, database, and cache. Tenant isolation is structural rather than policy-only: code bugs should not expose one account's data to another. Every authentication attempt, every authorisation decision, and every data access is logged. Agent-to-agent delegation can only narrow permissions, never widen them.
Authentication is delegated to Clerk (the Thinklio default, replacing the earlier Supabase Auth design). Clerk handles email, OAuth, magic links, and session lifecycle; the Convex backend validates Clerk tokens via a first-class integration. The three external API surfaces (Channel API, Platform API, Integration API) each have their own authentication mechanism but resolve to the same platform identity and authorisation model.
Authorisation is evaluated at four levels: platform (can this user access this resource?), agent (can this agent perform this action?), assignment (is this action further restricted for this context?), and step (can this specific step execute right now given budget, rate limits, delegation depth, and approval gates?). The evaluation order is: account policies, assignment restrictions, trust level, budget, rate limit, delegation depth and cycle checks, approval gate. Fail-closed behaviour applies: if the policy engine cannot evaluate, the decision is deny.
The governance policy framework provides eight categories of declarative, data-driven rules: action permissions, cost limits, data boundaries, temporal constraints, delegation constraints, content policies, audit requirements, and approval gates. Policies layer as account, team, and user, with the most restrictive wins rule. Every policy carries scoping fields (agent, channel, tool, user) so a rule can target exactly the right context. Industry template packs for healthcare, finance, and legal layer atop a safe default baseline and can be applied during onboarding.
Credentials are stored and injected through three connected mechanisms. Platform secrets (LLM keys, internal service tokens) live in infrastructure secret management and are managed by Thinklio ops. User- and account-facing secrets live in the Secrets Vault, which is a first-class product feature: users store passwords, API keys, TOTP seeds, and configuration blocks with fine-grained sharing, access logs, and agent integration. Agents can use stored credentials only with per-secret, per-session user approval; values are never stored in message history, reasoning traces, or knowledge extraction. MCP servers receive credentials per-request as HTTP headers (never stored on the server); Thinklio handles OAuth refresh cycles and surfaces user-friendly errors when credentials are missing or invalid.
MCP tool permissions enforce a core principle: in any channel, an agent's effective tool set is the intersection of all human participants' tool permissions. This prevents privilege escalation via shared channels. Tool access is granted at three levels (organisation, team, user) with most-restrictive-wins resolution and a max-of-teams rule for multi-team users. In group chats, tools downgrade to the lowest common permission level; if a participant has no access, the tool becomes invisible to the agent. Agents in channels have four visibility states: active, limited, unavailable, and needs-setup. Delegation chains preserve the intersection rule at every level. Tool calls use the credentials of the user who triggered the request for user-scoped integrations; account-scoped credentials use the shared token, and access control is handled by Thinklio's permission layer rather than the external system.
The remainder of this document is structured in four parts. Part A covers the platform security model (threat model, authentication, authorisation, data isolation, input validation, rate limits, logging, incident response, compliance). Part B is the governance policy framework (evaluation model, eight policy categories, formal TypeScript rule types, supporting tables, default templates, and industry packs). Part C covers credential management (the full Secrets Vault specification plus MCP credential injection and OAuth handling). Part D covers MCP tool permissions (grants, effective resolution, channel downgrade, delegation chains, and the MCP metadata endpoint). For data ownership and scoping see 04-data-model.md. For the harness that invokes policies see 03-agent-architecture.md. For Convex helpers used in enforcement (customQuery, customMutation, RLS, triggers) see 11-convex-reference.md.
Table of contents¶
- Part A: Security model
- 1. Security principles
- 2. Threat model
- 3. Authentication
- 4. Authorisation
- 5. Data isolation
- 6. Input validation
- 7. Rate limiting
- 8. Security logging and monitoring
- 9. Incident response
- 10. Compliance
- Part B: Governance policy framework
- 1. Purpose
- 2. Policy evaluation model
- 3. Policy categories
- 4. Rule type definitions
- 5. Supporting data structures
- 6. Governance UI concepts
- 7. Policy evaluation performance
- 8. What this framework does not cover
- Part C: Credential management
- 1. Overview and credential scopes
- 2. Secrets Vault specification
- 3. MCP credential injection
- Part D: MCP tool permissions
- 1. Core principle
- 2. Permission grants
- 3. Resolving a user's effective tool set
- 4. Channel tool resolution
- 5. Agent visibility states
- 6. Delegation chains
- 7. Whose credentials are used
- 8. MCP server metadata endpoint
- 9. Integration lifecycle
- 10. Summary
- References
- Revision history
Part A: Security model¶
1. Security principles¶
- Defence in depth. Security is enforced at multiple layers: network, application, database, and cache. No single layer's failure compromises the system.
- Least privilege. Every entity (user, agent, service) has the minimum permissions needed to function.
- Fail closed. If a security check cannot be performed (service down, timeout), the action is denied.
- Audit everything. Every authentication, authorisation decision, and data access is logged.
- Isolation is structural. Tenant data isolation is enforced by architecture, not just policy. A code bug should not expose one tenant's data to another.
- Delegation does not escalate. Agent-to-agent delegation can only narrow permissions, never widen them. A delegate agent operates under the intersection of its own permissions and the invoking context's restrictions.
2. Threat model¶
2.1 Assets¶
| Asset | Sensitivity | Examples |
|---|---|---|
| User conversations | High | Message content, personal context |
| Knowledge facts | High | Extracted information about users, teams, accounts |
| Agent configuration | Medium | System prompts, tool access, policies |
| Credentials | Critical | API keys, auth tokens, database passwords |
| Operational data | Medium | Usage metrics, cost data, system configuration |
| Audit logs | High | Security events, access records |
| Job data | High | Dispatch payloads, context bundles, subjob results |
| Delegation chains | Medium | Agent composition structure, invocation contracts |
2.2 Threat actors¶
| Actor | Capability | Motivation |
|---|---|---|
| External attacker | Network access, public endpoints | Data theft, service disruption |
| Malicious user | Authenticated access, legitimate account | Privilege escalation, data access |
| Compromised agent | Agent-level permissions | Data exfiltration via tool abuse |
| Malicious delegate agent | Delegated permissions | Exploiting delegation chain to access data or tools beyond intended scope |
| Insider (compromised admin) | Administrative access | Data theft, sabotage |
| Supply chain | Compromised dependencies/images | Backdoor, cryptomining |
| Malicious external tool | Integration API registration | Data exfiltration via tool responses, prompt injection via tool output |
2.3 Attack vectors¶
- Authentication bypass: gaining access without valid credentials
- Privilege escalation: accessing resources beyond assigned permissions
- Cross-tenant data access: reading another account/team/user's data
- Prompt injection: manipulating agent behaviour through crafted inputs
- Tool abuse: using agent tool access for unintended purposes
- Delegation escalation: using agent composition to bypass permission restrictions
- Resource exhaustion: overwhelming the system to cause denial of service
- Credential theft: stealing API keys, tokens, or database credentials
- Data exfiltration: extracting sensitive data through legitimate channels
- External tool injection: dynamically registered tools returning malicious content to influence agent reasoning
3. Authentication¶
3.1 Clerk integration (Thinklio default)¶
User authentication is handled by Clerk. Clerk manages email/password, magic link, and OAuth (Google, GitHub, etc.) authentication flows, and provides organisation and role primitives that map directly to Thinklio accounts and RBAC. The Convex backend never handles raw credentials: all credential management is delegated to Clerk.
Clerk tokens are validated inside Convex query, mutation, and action functions via the first-class Clerk integration (see 11-convex-reference.md section 8). Identity is resolved from the token; organisation membership, roles, and permissions are resolved from Convex tables (account_user, team_member, agent_assignment) keyed by the Clerk user ID, not from token claims.
3.2 External authentication: three API surfaces¶
Users and systems authenticate through the gateway and HTTP action endpoints. The three API surfaces have different authentication mechanisms, all resolving to the same platform identity and authorisation model.
Channel API: authenticates as a user.
- The external system provides credentials that map to a platform user identity
- All interactions are governed by that user's permissions, team memberships, and account policies
- Supported methods: Clerk session token (JWT), API key scoped to a user context
Platform API: authenticates as an account or service account.
- API keys scoped to specific accounts, teams, or agents
- Operations governed by the role associated with the API key (admin, member, etc.)
- Supported methods: API key with account/team scope
Integration API: authenticates as an external system.
- Registration requires account approval
- Execution credentials scoped to specific tool definitions
- Thinklio-to-external calls carry authentication configured at registration time
- External-to-Thinklio calls authenticate via API key with tool-specific scoping
3.3 JWT structure¶
Clerk produces a standard JWT. The relevant claims for Convex authorisation are:
{
"sub": "user_2abc123...",
"email": "user@example.com",
"org_id": "org_2def456...",
"org_role": "admin",
"org_permissions": ["accounts:manage", "agents:create"],
"aud": "convex",
"exp": 1710000000,
"iat": 1709913600
}
Account membership and detailed roles are resolved from the account_user table after token validation. The Clerk org_id maps one-to-one to a Thinklio account.id.
3.4 Channel authentication¶
Each channel adapter handles authentication in its native way:
- Telegram: user ID verification against registered users
- Channel API: bearer token (Clerk JWT or API key)
- Web chat: Clerk session token from the web app
Channel authentication maps to platform identity. A Telegram user ID is linked to a platform user account.
3.5 Inter-service authentication¶
During the Convex-first phase, server functions run inside Convex and share trust via the managed runtime. Internal functions (see 11-convex-reference.md section 4) are only callable from other Convex functions, not from clients. For external calls out (to HTTP endpoints, MCP servers, LLM providers), the action function authenticates with service tokens stored in platform secrets (section C.1) or per-account credentials resolved from the vault (section C.2).
3.6 Session management¶
- Clerk session tokens with configurable expiry
- Refresh token rotation handled by Clerk
- Session state maintained by Clerk; Convex queries are reactive and re-run when auth changes
- Explicit logout invalidates the Clerk session
- Concurrent session limits configurable per account via Clerk
4. Authorisation¶
4.1 Permission model¶
Authorisation is evaluated at four levels:
1. Platform level: can this user/system access this resource at all?
User → AccountUser (role) → Account
User → TeamMember (role) → Team
User → AgentAssignment → Agent
API Key → Scoped account/team/agent access
2. Agent level: can this agent perform this action?
Agent → AgentTool (permission) → Tool
Agent → capability_level → action type
Agent → Account.policies → governance rules
3. Assignment level: is this action further restricted for this context?
4. Step level: can this specific step execute right now?
Step → PolicyEngine → trust level check
Step → AssignmentRestrictions → tool restriction check
Step → BudgetEnforcer → cost check
Step → RateLimiter → rate check
Step → DelegationRules → depth and cycle check
4.2 Role-based access control¶
| Role | Scope | Capabilities |
|---|---|---|
| Platform admin | System-wide | System configuration, user management |
| Account owner | Account | Full access including billing, deletion, ownership transfer, API key management |
| Account admin | Account | Manage teams, members, agents, settings (not billing/delete) |
| Account editor | Account | Create/edit agents, knowledge, use all features |
| Account viewer | Account | Read-only access, can interact with assigned agents |
| Team admin | Team | Team agent configuration, member management |
| Team editor | Team | Create/edit team agents, contribute to team knowledge |
| Team viewer | Team | View team agent interactions, no interaction |
| Agent admin | Agent | Configure agent settings, tools, knowledge |
| Agent user | Agent | Interact with the agent |
Clerk organisation roles map to the account-level roles. Team- and agent-level roles are stored in Thinklio tables and resolved after Clerk token validation.
4.3 Policy engine¶
The policy engine evaluates every tool execution, delegation, and sensitive operation. Policies are defined at account, team, and user levels and enforced by the harness. The full policy framework is Part B of this document.
Policy evaluation order:
- Account policies: explicit allow/deny rules, delegation limits
- Assignment restrictions: per-assignment tool narrowing
- Trust level check: does the user's trust level meet the tool's requirements?
- Budget check: is there sufficient budget for this operation?
- Rate limit check: has the user/agent exceeded rate limits?
- Delegation checks: depth limit, cycle detection (for agent-type tools)
- Approval check: does this action require explicit approval?
Policy decisions:
allow: proceed with executiondeny: block with reason loggedrequire_approval: pause and request human approval
Fail-closed behaviour: If the policy engine cannot evaluate (service unavailable, timeout), the decision is deny.
4.4 Trust levels¶
Trust is graduated and governs tool access:
| Trust Level | Description | Example Tools |
|---|---|---|
read |
Read-only operations | Search web, get weather, read documents |
low_risk_write |
Write operations with limited impact | Create todo, send notification, update note |
high_risk_write |
Write operations with significant impact | Send email, modify database, execute payment |
Users are assigned trust levels per agent assignment. Agents are configured with tool trust requirements. The policy engine compares user trust against tool requirements.
The governance framework (Part B, section 3) extends this with a five-level permission scheme (deny, read, draft, confirm, autonomous) that applies to specific actions rather than coarse tool categories.
4.5 Per-assignment tool restrictions¶
Each agent assignment can include tool_restrictions that narrow the agent's configured tool permissions for that specific context. This applies to both regular tools and agent-as-tool delegations.
Key principles:
- Restrictions can only narrow, never widen. An assignment cannot grant tool access the agent doesn't already have.
- An account policy cannot grant permissions that the assignment has removed.
- For agent-as-tool delegations, assignment restrictions can limit what actions the delegate is asked to perform.
- The same agent assigned to different teams can have different effective tool permissions via different assignment restrictions.
4.6 Delegation security¶
Depth limits. The account policy max_delegation_depth (default: 3) limits how many levels of agent-to-agent delegation are permitted. The policy engine denies act steps that would exceed this depth.
Cycle detection: configuration time. When an agent-as-tool is registered in Agent Studio, the system checks the delegation graph for cycles. Circular configurations (A → B → C → A) are rejected.
Cycle detection: runtime. Each delegation carries a delegation chain (list of agent IDs in the current call stack). If a delegation would invoke an agent already in the chain, the policy engine denies it. This catches cases that configuration-time checks might miss (e.g., if the delegation graph was modified after configuration).
Permission narrowing through delegation. When Agent A delegates to Agent B, Agent B operates under:
- Its own configured tool permissions (maximum capability)
- Narrowed by the assignment restrictions for the context Agent A is operating in
- Narrowed by account policies
The delegate never gains permissions the invoking agent doesn't have in its current context.
Context isolation. The delegate does not receive the invoking agent's full context, knowledge layers, or conversation history. It receives the invocation payload (conforming to the parameter schema) and assembles its own context from its own knowledge layers scoped to its own assignment.
5. Data isolation¶
5.1 Database level¶
Application-layer authorisation (implemented with convex-helpers row-level security; see 11-convex-reference.md section 9) enforces that queries only return data the current user is authorised to see:
- Knowledge facts: scope-based access (user sees their own, team members see team's, account members see account's)
- Interactions: users see only their own interactions
- Jobs: visible to users who created the originating interaction or are registered observers
- Events: scoped to the user's agent assignments
- Configuration: account admins see account configuration; members see only what they need
RLS is applied as a custom function wrapper around every query and mutation:
// convex/lib/accountAuth.ts
export const accountQuery = customQuery(query, {
args: { accountId: v.id("account") },
input: async (ctx, { accountId }) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthenticated");
const membership = await ctx.db
.query("account_user")
.withIndex("by_account_user", q =>
q.eq("accountId", accountId).eq("userId", identity.subject))
.unique();
if (!membership) throw new Error("Not a member of this account");
return { ctx: { ...ctx, accountId, membership }, args: {} };
},
});
Every application-facing query and mutation uses accountQuery or accountMutation rather than the raw query/mutation primitives.
5.2 Note visibility model¶
Note visibility follows a layered model:
- Notes with
visibility = 'private'are visible only to the creator - Notes with
visibility = 'team'are visible to team members - Notes with
visibility = 'account'are visible to all account members - Granular sharing via
note_sharerecords can extend visibility to specific users or teams - The
note_mentionsystem tracks @ mentions within note content, triggering notifications to mentioned entities
5.3 Application level¶
Every server function that accesses data includes context validation:
export const getKnowledge = accountQuery({
args: { agentId: v.id("agent"), userId: v.string() },
handler: async (ctx, args) => {
// Verify the requesting user has access to this agent
const hasAccess = await ctx.db.query("agent_assignment")
.withIndex("by_agent_user",
q => q.eq("agentId", args.agentId).eq("userId", args.userId))
.unique();
if (!hasAccess) throw new Error("Unauthorised");
// proceed with scoped query
},
});
5.4 Cache level¶
Cache keys include scope identifiers. A cache key for user knowledge is thinklio:knowledge:agent:{agent_id}:user:{user_id}. There is no cache key that could return cross-tenant data. Active job cache keys include the job ID; job data is only accessible to authorised observers.
5.5 Network level¶
- Convex server functions run in Convex-managed infrastructure
- No custom service-to-service network to expose
- Gateway and HTTP actions are the only public-facing interfaces
- SSL/TLS for all external connections
- External service calls (LLM providers, MCP servers, third-party APIs) go through Convex actions with validated credentials
6. Input validation¶
All user input is validated before processing:
- Message content: length limits, encoding validation
- Commands: parameter validation against defined schemas
- API requests: JSON schema validation on all endpoints across all three API surfaces
- File uploads: type checking, size limits, malware scanning (future)
- Tool parameters: validated against tool parameter schemas
- Delegation invocation payloads: validated against the agent-as-tool's parameter schema
- Dynamically registered tool definitions: schema validation, endpoint reachability check
Convex server functions validate arguments via v validators on every function definition. The convex-helpers Zod integration provides richer validation where needed.
6.1 Prompt injection defence¶
User input is never directly concatenated into system prompts. Mitigation strategies:
- Clear delimiters between system instructions and user content
- Input sanitisation removing known injection patterns
- Output monitoring for anomalous responses
- Trust boundaries: user content is always treated as untrusted data
- Rate limiting to prevent brute-force injection attempts
- External tool output is treated as untrusted data in the agent's context (same as user input)
6.2 External tool security¶
Dynamically registered external tools (via Integration API) introduce additional attack surface:
- Tool registration requires account approval before the tool becomes available
- Tool responses are treated as untrusted data (cannot override system prompts or policies)
- Tool endpoints are validated at registration time
- Health monitoring detects unresponsive or misbehaving tools
- Circuit breakers prevent cascading failures from unhealthy external tools
- Rate limiting applies per tool per agent
7. Rate limiting¶
Rate limits are enforced at multiple levels. The implementation uses the Convex Rate Limiter component (see 11-convex-reference.md Part B) for transactional, sharded token bucket and fixed window enforcement.
| Level | Scope | Default Limit | Enforcement point |
|---|---|---|---|
| Gateway | Per IP | 100 req/min | Gateway edge |
| User message | Per user | 20 msg/min | Gateway |
| Tool execution | Per agent per tool | 10 exec/min | Harness |
| LLM calls | Per agent | 30 calls/min | Agent service |
| Channel API | Per API key | 60 req/min | Gateway |
| Platform API | Per API key | 60 req/min | Gateway |
| Integration API | Per tool registration | 30 exec/min | Gateway |
| Job dispatch | Per agent | 10 jobs/min | Workflow dispatcher |
Per-feature rate limits (for example, the per-user limits on secret reveal/copy in Part C) are defined in their own sections.
8. Security logging and monitoring¶
8.1 Security events¶
All security-relevant events are logged:
- Authentication attempts (success and failure) across all three API surfaces
- Authorisation decisions (especially denials)
- Policy engine evaluations
- Delegation depth and cycle check results
- Rate limit hits
- Input validation failures
- External tool registration and approval decisions
- Unusual patterns (rapid command sequences, anomalous tool usage, unexpected delegation chains)
8.2 Alerting¶
Alerts trigger on:
- Repeated authentication failures from same source
- Policy denial rate exceeding threshold
- Rate limit hits exceeding normal levels
- Service health check failures
- Unusual tool execution patterns
- Delegation depth limit hits (may indicate misconfiguration)
- External tool health failures
9. Incident response¶
- Security events aggregated in audit log
- Alert escalation to admin notification channel
- Agent kill switch for immediate containment (cancels pending jobs)
- Audit trail preserved for investigation
- Post-incident review process
10. Compliance¶
10.1 GDPR readiness¶
- Right to access: users can export all their data
- Right to erasure: users can request deletion; user data hard-deleted, contributions anonymised, job observer registrations removed
- Data portability: export in standard formats
- Consent: explicit consent for data processing
- Data minimisation: only necessary data collected
- Retention: configurable retention periods with automatic archival
10.2 Data residency¶
Initial Thinklio deployment runs on Convex's managed cloud. Data residency requirements (EU-only for some customers, AU-only for others) can be met either by Convex region selection or by the self-hosted deployment path (see 11-convex-reference.md section 12).
Part B: Governance policy framework¶
Part B defines the governance policy framework: how organisations declare the rules that shape agent behaviour, where those rules are evaluated during an agent turn, and how account, team, and user layers compose. Every tool call, every delegation, and every external interaction passes through this framework before execution. The policies themselves are declarative data (not code), scoped precisely (agent, channel, tool, user), and merged with a most-restrictive-wins rule.
1. Purpose¶
Part B defines the governance policy categories, evaluation model, and supporting data structures for controlling agent behaviour across Thinklio. Governance is the mechanism by which organisations maintain control over what agents can do, what data they can access, how much they can spend, and when they need human approval.
The governance layer sits between agent intent and agent action. Every tool call, every delegation, every external interaction passes through policy evaluation before execution. The policies themselves are declarative (data, not code) and follow the layering model from 03-agent-architecture.md: account policies set the ceiling, team policies tighten within those bounds, user preferences tighten further. No layer can loosen a restriction set above it.
2. Policy evaluation model¶
2.1 When policies are evaluated¶
Policy evaluation happens at four points during an agent turn:
-
Before context assembly (contextHandler). Content policies and data boundary rules are injected into the system prompt so the LLM is aware of constraints before it begins reasoning.
-
Before tool execution (tool resolver). Action permissions, approval gates, and delegation constraints are checked when the agent attempts to call a tool. If the policy denies the action, the tool call returns an error to the agent's reasoning loop. If the policy requires approval, execution pauses and a confirmation request surfaces to the user.
-
After LLM response (usageHandler). Cost limits and usage quotas are enforced. If the agent has exceeded its budget, subsequent LLM calls are blocked until the budget resets or is increased.
-
After turn completion (audit step). Audit requirements determine what is logged and whether a review flag is raised for human attention.
2.2 Policy resolution¶
When multiple policies apply (account + team + user), they are merged using the most restrictive wins rule:
effectivePolicy = merge(accountPolicies, teamPolicies, userPreferences)
Rules:
- If account says "deny", the action is denied regardless of team or user settings
- If account says "allow" and team says "deny", the action is denied
- If account says "allow" and team says "allow" and user says "deny", the action is denied
- Numeric limits use the lowest value (strictest budget wins)
- Enum permissions use the most restrictive level
Policy resolution is performed by a single resolveEffectivePolicy() function for testability and consistency. The resolved policy is snapshotted at turn start alongside the agent configuration (03-agent-architecture.md (turn-start snapshot section)).
2.3 Policy scope¶
Every policy record specifies what it applies to:
- Agent scope: applies to a specific agent, or all agents (wildcard)
- Channel scope: applies in a specific channel, channel type, or all channels
- Tool scope: applies to a specific tool, tool type, or all tools
- User scope: applies to interactions initiated by a specific user, or all users
This allows precise targeting: "the mail agent can send without confirmation in team channels, but requires confirmation in organisation-wide channels" or "the research agent is limited to $2 per interaction for viewer-role users but $10 for editors".
3. Policy categories¶
3.1 Action permissions¶
Controls what actions an agent can take and at what level of autonomy.
Permission levels (ordered from most restrictive to least):
| Level | Meaning |
|---|---|
deny |
Agent cannot perform this action at all |
read |
Agent can read/observe but not act |
draft |
Agent can prepare the action but must present it for review without executing |
confirm |
Agent can prepare and execute, but only after explicit user confirmation |
autonomous |
Agent can execute without asking |
Examples:
// Mail agent: can read freely, must confirm before sending
{
category: "action_permission",
agentScope: "mail-agent",
rule: {
permissions: [
{ action: "email:read", level: "autonomous" },
{ action: "email:draft", level: "autonomous" },
{ action: "email:send", level: "confirm" },
{ action: "email:send_external", level: "confirm" },
],
},
}
// Calendar agent: can read and create tentative, must confirm invitations
{
category: "action_permission",
agentScope: "calendar-agent",
rule: {
permissions: [
{ action: "calendar:read", level: "autonomous" },
{ action: "calendar:create_tentative", level: "autonomous" },
{ action: "calendar:send_invitation", level: "confirm" },
{ action: "calendar:cancel_event", level: "confirm" },
{ action: "calendar:modify_recurring", level: "confirm" },
],
},
}
// CRM agent: read freely, update notes, but confirm deal stage changes
{
category: "action_permission",
agentScope: "crm-agent",
rule: {
permissions: [
{ action: "crm:read", level: "autonomous" },
{ action: "crm:update_notes", level: "autonomous" },
{ action: "crm:update_deal_stage", level: "confirm" },
{ action: "crm:send_outbound", level: "deny" },
],
},
}
// Practice Assistant: read patient data, but deny access to clinical notes
// unless treating practitioner is in the channel
{
category: "action_permission",
agentScope: "practice-assistant",
rule: {
permissions: [
{ action: "patient:read_demographics", level: "autonomous" },
{ action: "patient:read_appointments", level: "autonomous" },
{ action: "patient:read_clinical_notes", level: "deny" },
{ action: "patient:update_record", level: "confirm" },
],
},
}
Evaluation point: Tool resolver, before tool execution.
3.2 Cost limits¶
Controls spending on LLM tokens, external API calls, and delegated agent turns.
Limit types:
| Limit | Scope | Example |
|---|---|---|
| Token budget per interaction | Per agent | Research agent: max 50,000 tokens per turn |
| Token budget per period | Per agent per account | All agents: max 500,000 tokens per day |
| Monetary budget per interaction | Per agent | Data agent: max $2.00 per interaction |
| Monetary budget per period | Per account | Account: max $500/month across all agents |
| Monetary budget per period | Per team | Marketing team: max $200/month |
| Monetary budget per period | Per user | Individual user: max $50/month |
| External API call budget | Per tool | Credit check API: max 10 calls per day |
| Model tier restriction | Per agent or account | This account may only use Sonnet-tier models |
Examples:
// Account-level monthly budget
{
category: "cost_limit",
rule: {
type: "monetary_period",
maxAmount: 500.00,
currency: "USD",
period: "month",
scope: "account",
},
}
// Per-agent interaction limit
{
category: "cost_limit",
agentScope: "research-agent",
rule: {
type: "token_interaction",
maxTokens: 50000,
},
}
// Per-user daily budget
{
category: "cost_limit",
rule: {
type: "monetary_period",
maxAmount: 10.00,
currency: "USD",
period: "day",
scope: "user",
},
}
// Model tier restriction
{
category: "cost_limit",
rule: {
type: "model_restriction",
allowedModels: ["anthropic/claude-sonnet-4", "openai/gpt-4o-mini"],
deniedModels: ["anthropic/claude-opus-4"],
},
}
// External API call limit
{
category: "cost_limit",
toolScope: "credit-check-api",
rule: {
type: "call_count_period",
maxCalls: 10,
period: "day",
},
}
Evaluation point: usageHandler (token and monetary budgets), tool resolver (API call limits, model restrictions).
3.3 Data boundaries¶
Controls what data agents can access and where data can flow. Critical for regulated industries (healthcare, finance, legal).
Boundary types:
| Boundary | Purpose | Example |
|---|---|---|
| Classification ceiling | Agent cannot access data above a certain sensitivity level | Agent limited to "internal" classification; cannot see "confidential" or "restricted" documents |
| PII handling | Controls whether agent can include personally identifiable information in responses | Agent must redact patient names in organisation-wide channels |
| External processing | Controls whether data can be sent to external APIs | Clinical data cannot be sent to external LLM providers or parsing services |
| Data residency | Controls where data is processed | Australian patient data must not leave AU-region infrastructure |
| Cross-channel data | Controls whether data from one channel can be referenced in another | Private DM content cannot be surfaced in team channels |
Examples:
// Classification ceiling
{
category: "data_boundary",
agentScope: "*",
rule: {
type: "classification_ceiling",
maxClassification: "internal",
classifications: ["public", "internal", "confidential", "restricted"],
},
}
// PII handling in broad channels
{
category: "data_boundary",
channelScope: { type: "organisation" },
rule: {
type: "pii_handling",
action: "redact",
fields: ["patient_name", "date_of_birth", "phone", "email", "address"],
},
}
// External processing restriction
{
category: "data_boundary",
rule: {
type: "external_processing",
allowExternalLLM: false,
allowExternalParsing: false,
allowExternalStorage: false,
exemptTools: [], // no exemptions
},
}
// Cross-channel data isolation
{
category: "data_boundary",
rule: {
type: "cross_channel",
allowCrossReference: false,
exemptChannelTypes: ["team"], // team channels can reference each other
},
}
Evaluation point: contextHandler (classification and PII filtering applied to knowledge results before they enter the prompt), tool resolver (external processing checks before data leaves the platform).
3.4 Temporal constraints¶
Controls when agents can take autonomous actions.
Constraint types:
| Constraint | Purpose | Example |
|---|---|---|
| Operating hours | Agent can only act autonomously during specified hours | Autonomous email sending only 8am-6pm AEST weekdays |
| Cooldown period | Minimum time between certain actions | No more than one outbound email to the same recipient per hour |
| Quiet hours | Agent switches to draft-only mode outside hours | All agents draft-only between 10pm and 7am user-local time |
| Rate-of-action limit | Maximum number of a specific action type per period | No more than 20 outbound emails per day per agent |
Examples:
// Operating hours for autonomous actions
{
category: "temporal_constraint",
rule: {
type: "operating_hours",
timezone: "Australia/Sydney",
hours: {
monday: { start: "08:00", end: "18:00" },
tuesday: { start: "08:00", end: "18:00" },
wednesday: { start: "08:00", end: "18:00" },
thursday: { start: "08:00", end: "18:00" },
friday: { start: "08:00", end: "16:00" },
},
outsideHoursBehaviour: "draft", // switch to draft mode outside hours
},
}
// Cooldown between emails to the same recipient
{
category: "temporal_constraint",
agentScope: "mail-agent",
rule: {
type: "cooldown",
action: "email:send",
groupBy: "recipient",
minInterval: 3600000, // 1 hour in ms
},
}
// Daily action cap
{
category: "temporal_constraint",
agentScope: "mail-agent",
rule: {
type: "rate_of_action",
action: "email:send",
maxCount: 20,
period: "day",
},
}
Evaluation point: Tool resolver (checks current time and action history before permitting execution).
3.5 Delegation constraints¶
Controls how agents compose and delegate to each other. Extends the basic delegationSet and maxDelegationDepth on the agent record with policy-level rules.
Constraint types:
| Constraint | Purpose | Example |
|---|---|---|
| External agent restriction | Limits delegation to platform agents only | Custom agents cannot delegate to externally-hosted agents |
| Trust escalation prevention | Prevents accumulation of elevated trust through delegation chains | A chain cannot include more than one elevated-trust agent |
| Circular delegation prevention | Prevents agent A delegating to B which delegates back to A | Built into the runtime, but policy can further restrict |
| Prohibited combinations | Certain agents cannot delegate to each other | Compliance checker cannot delegate to the agent it is reviewing |
| Cost attribution | How delegation costs are attributed | All delegation costs roll up to the originating user's budget |
Examples:
// Restrict to platform agents only
{
category: "delegation_constraint",
rule: {
type: "agent_origin",
allowedOrigins: ["platform", "custom"],
deniedOrigins: ["external"],
},
}
// Prevent trust escalation
{
category: "delegation_constraint",
rule: {
type: "trust_escalation",
maxElevatedAgentsInChain: 1,
},
}
// Prohibited combination
{
category: "delegation_constraint",
agentScope: "compliance-checker",
rule: {
type: "prohibited_delegate",
deniedAgents: ["content-writer"], // can't delegate to the agent it reviews
reason: "Compliance checker must not delegate to the agent whose output it audits",
},
}
// Cost attribution
{
category: "delegation_constraint",
rule: {
type: "cost_attribution",
mode: "originating_user", // all costs charged to the user who started the interaction
},
}
Evaluation point: Tool resolver (when a delegation tool is invoked, before the delegate agent turn is scheduled).
3.6 Content policies¶
Controls the tone, topic boundaries, and output format of agent responses. These are injected into the agent's system prompt so the LLM is aware of them during reasoning.
Policy types:
| Policy | Purpose | Example |
|---|---|---|
| Brand voice | Enforces tone and style guidelines | All customer-facing responses must be professional but warm, use Australian English |
| Topic restriction | Certain topics are off-limits or require careful handling | Agents must not provide medical diagnosis, legal advice, or financial recommendations |
| Mandatory disclaimer | Certain response types must include caveats | Any health-related information must include "This is general information only, not medical advice" |
| Response format | Controls length, structure, or format requirements | Responses in the support channel must be under 500 words |
| Competitor mention | Controls how competitors are referenced | Agents must not make comparative claims about competitor products |
| Language | Controls which languages agents respond in | All agent responses in this account must be in English |
Examples:
// Brand voice
{
category: "content_policy",
rule: {
type: "brand_voice",
guidelines: "Professional but approachable. Use Australian English throughout. "
+ "Avoid jargon unless the user has demonstrated technical familiarity. "
+ "Never use corporate buzzwords or marketing speak.",
},
}
// Topic restriction
{
category: "content_policy",
rule: {
type: "topic_restriction",
restrictions: [
{
topic: "medical_diagnosis",
action: "refuse_with_referral",
message: "I can share general health information but cannot provide a diagnosis. "
+ "Please consult your treating practitioner.",
},
{
topic: "financial_advice",
action: "refuse_with_referral",
message: "I can provide factual financial information but not personalised advice. "
+ "Please consult a licensed financial adviser.",
},
],
},
}
// Mandatory disclaimer
{
category: "content_policy",
rule: {
type: "mandatory_disclaimer",
triggers: ["health", "medical", "treatment", "symptom", "diagnosis"],
disclaimer: "This is general information only and does not constitute medical advice. "
+ "Please consult a qualified healthcare professional for advice specific to your situation.",
position: "end", // append to end of response
},
}
// Response length limit
{
category: "content_policy",
channelScope: { type: "public_group" },
rule: {
type: "response_format",
maxWords: 500,
guideline: "Keep responses concise in public channels. Offer to provide more detail in a thread or DM.",
},
}
Evaluation point: contextHandler (injected into system prompt before LLM call).
3.7 Audit requirements¶
Controls what is logged, how much detail is retained, and when human review is triggered.
Requirement types:
| Requirement | Purpose | Example |
|---|---|---|
| Logging depth | Controls how much detail is recorded per agent turn | Full reasoning trace for elevated-trust agents, summary only for read-only agents |
| Review trigger | Flags certain actions or patterns for human review | Any email sent to an external domain triggers a 24-hour review flag |
| Disclosure requirement | Requires agents to identify themselves as automated | Agent emails must include a footer identifying them as AI-generated |
| Periodic review | Triggers review after volume thresholds | If an agent has sent more than 50 emails this week, flag for admin review |
| Retention policy | Controls how long audit records are kept | Audit records for financial transactions retained for 7 years |
Examples:
// Full reasoning trace for elevated agents
{
category: "audit_requirement",
rule: {
type: "logging_depth",
agentTrustLevel: ["elevated", "admin"],
depth: "full_trace", // includes reasoning, tool call payloads, and response drafts
},
}
// Summary logging for standard agents
{
category: "audit_requirement",
rule: {
type: "logging_depth",
agentTrustLevel: ["read", "standard"],
depth: "summary", // includes action taken, tokens used, tools called, but not full payloads
},
}
// Review trigger for external emails
{
category: "audit_requirement",
agentScope: "mail-agent",
rule: {
type: "review_trigger",
action: "email:send_external",
reviewWindow: 86400000, // 24 hours
assignTo: "account_admin",
},
}
// Disclosure in outbound communications
{
category: "audit_requirement",
rule: {
type: "disclosure",
channels: ["email", "whatsapp", "sms"],
disclosure: "This message was composed with AI assistance.",
position: "footer",
},
}
// Volume-based periodic review
{
category: "audit_requirement",
agentScope: "mail-agent",
rule: {
type: "periodic_review",
action: "email:send",
threshold: 50,
period: "week",
assignTo: "account_admin",
message: "Mail agent has sent {count} emails this week. Please review for appropriateness.",
},
}
// Retention policy
{
category: "audit_requirement",
rule: {
type: "retention",
eventTypes: ["financial_transaction", "patient_data_access"],
retentionPeriod: "7y",
},
}
Evaluation point: rawResponseHandler and workflow audit step (after turn completion).
3.8 Approval gates¶
Defines conditions under which an agent must pause and request human confirmation before proceeding. This is the human-in-the-loop mechanism.
Gate types:
| Gate | Purpose | Example |
|---|---|---|
| Action type gate | Certain action types always require approval | Financial transactions above $100 require confirmation |
| First-of-type gate | First time an agent performs an action type in this account | Learn-then-trust: first 5 emails require approval, then autonomous |
| Confidence gate | Agent indicates uncertainty and defers to the user | Agent is unsure which patient "Chen" refers to, asks for clarification |
| External party gate | Actions affecting people outside the organisation | Any communication to an external recipient requires confirmation |
| Escalation gate | Agent recognises the request is beyond its capability or authority | Agent suggests escalating to a human when it detects frustration or a complaint |
Examples:
// Financial threshold gate
{
category: "approval_gate",
rule: {
type: "action_threshold",
action: "financial:*",
condition: { field: "amount", operator: "gt", value: 100.00 },
message: "This transaction is for {amount}. Please confirm to proceed.",
},
}
// Learn-then-trust gate (first N actions require approval)
{
category: "approval_gate",
agentScope: "mail-agent",
rule: {
type: "first_of_type",
action: "email:send",
approvalCount: 5, // first 5 sends require approval, then autonomous
scope: "per_user", // each user has their own learning period
},
}
// External party gate
{
category: "approval_gate",
rule: {
type: "external_party",
actions: ["email:send", "calendar:send_invitation", "sms:send"],
condition: "recipient_is_external",
message: "This will contact someone outside the organisation. Please confirm.",
},
}
// Escalation gate (agent self-identifies need for human)
{
category: "approval_gate",
rule: {
type: "escalation",
triggers: ["complaint_detected", "frustration_detected", "out_of_scope"],
action: "route_to_human",
channelBehaviour: "notify_team_lead",
},
}
Evaluation point: Tool resolver (checks gate conditions before executing the tool). When a gate triggers, the workflow pauses via awaitEvent and a confirmation request is posted to the channel. The user's response resumes the workflow.
4. Rule type definitions¶
This section provides formal TypeScript type definitions for every rule body referenced in section 3. These types are the contract between policy storage, the resolveEffectivePolicy() function, and the evaluation points in the agent harness. All code that creates, reads, or evaluates policies must conform to these types.
4.1 Shared types¶
/** Permission levels, ordered from most restrictive to least. */
type PermissionLevel = "deny" | "read" | "draft" | "confirm" | "autonomous";
/** Time periods used in budgets and rate limits. */
type BudgetPeriod = "hour" | "day" | "week" | "month";
/** Budget scope: what entity the limit is charged against. */
type BudgetScope = "account" | "team" | "user" | "agent";
/** Data classification levels, ordered from least sensitive to most. */
type Classification = "public" | "internal" | "confidential" | "restricted";
/** How PII fields are handled when a boundary applies. */
type PIIAction = "redact" | "mask" | "deny";
/** Agent trust levels, as defined in `03-agent-architecture.md` (trust levels section). */
type AgentTrustLevel = "read" | "standard" | "elevated" | "admin";
/** Audit logging depth. */
type LoggingDepth = "summary" | "full_trace";
/** Behaviour when an agent attempts an action outside operating hours. */
type OutsideHoursBehaviour = "deny" | "draft" | "queue";
/** Retention period shorthand: number followed by d (days), m (months), or y (years). */
type RetentionPeriod = `${number}${"d" | "m" | "y"}`;
/** A time window within a single day. */
interface TimeWindow {
start: string; // "HH:mm" in 24-hour format
end: string; // "HH:mm" in 24-hour format
}
/** Threshold condition used in approval gates and other conditional rules. */
interface ThresholdCondition {
field: string;
operator: "gt" | "gte" | "lt" | "lte" | "eq" | "neq";
value: number | string | boolean;
}
4.2 Action permission rules¶
interface ActionPermissionEntry {
action: string; // namespaced action, e.g. "email:send", "calendar:read"
level: PermissionLevel;
}
interface ActionPermissionRule {
permissions: ActionPermissionEntry[];
}
4.3 Cost limit rules¶
interface TokenInteractionLimit {
type: "token_interaction";
maxTokens: number;
}
interface TokenPeriodLimit {
type: "token_period";
maxTokens: number;
period: BudgetPeriod;
scope: BudgetScope;
}
interface MonetaryInteractionLimit {
type: "monetary_interaction";
maxAmount: number;
currency: string; // ISO 4217, e.g. "USD", "AUD"
}
interface MonetaryPeriodLimit {
type: "monetary_period";
maxAmount: number;
currency: string;
period: BudgetPeriod;
scope: BudgetScope;
}
interface CallCountPeriodLimit {
type: "call_count_period";
maxCalls: number;
period: BudgetPeriod;
}
interface ModelRestriction {
type: "model_restriction";
allowedModels?: string[]; // if set, only these models may be used
deniedModels?: string[]; // if set, these models are blocked
}
type CostLimitRule =
| TokenInteractionLimit
| TokenPeriodLimit
| MonetaryInteractionLimit
| MonetaryPeriodLimit
| CallCountPeriodLimit
| ModelRestriction;
4.4 Data boundary rules¶
interface ClassificationCeilingRule {
type: "classification_ceiling";
maxClassification: Classification;
classifications: Classification[]; // ordered list for comparison
}
interface PIIHandlingRule {
type: "pii_handling";
action: PIIAction;
fields: string[]; // e.g. ["patient_name", "date_of_birth", "email"]
}
interface ExternalProcessingRule {
type: "external_processing";
allowExternalLLM: boolean;
allowExternalParsing: boolean;
allowExternalStorage: boolean;
exemptTools: string[]; // tool names exempt from this restriction
}
interface DataResidencyRule {
type: "data_residency";
allowedRegions: string[]; // e.g. ["au", "nz"]
dataTypes: string[]; // e.g. ["patient_data", "financial_data"]
}
interface CrossChannelRule {
type: "cross_channel";
allowCrossReference: boolean;
exemptChannelTypes: string[]; // channel types where cross-reference is allowed
}
type DataBoundaryRule =
| ClassificationCeilingRule
| PIIHandlingRule
| ExternalProcessingRule
| DataResidencyRule
| CrossChannelRule;
4.5 Temporal constraint rules¶
interface OperatingHoursRule {
type: "operating_hours";
timezone: string; // IANA timezone, e.g. "Australia/Sydney"
hours: Partial<Record<
"monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday",
TimeWindow
>>;
outsideHoursBehaviour: OutsideHoursBehaviour;
}
interface CooldownRule {
type: "cooldown";
action: string; // namespaced action to throttle
groupBy: string; // field to group by, e.g. "recipient"
minInterval: number; // minimum milliseconds between actions in the same group
}
interface RateOfActionRule {
type: "rate_of_action";
action: string;
maxCount: number;
period: BudgetPeriod;
}
type TemporalConstraintRule =
| OperatingHoursRule
| CooldownRule
| RateOfActionRule;
4.6 Delegation constraint rules¶
type AgentOrigin = "platform" | "custom" | "external";
interface AgentOriginRule {
type: "agent_origin";
allowedOrigins: AgentOrigin[];
deniedOrigins: AgentOrigin[];
}
interface TrustEscalationRule {
type: "trust_escalation";
maxElevatedAgentsInChain: number;
}
interface ProhibitedDelegateRule {
type: "prohibited_delegate";
deniedAgents: string[]; // agent slugs
reason: string;
}
interface CostAttributionRule {
type: "cost_attribution";
mode: "originating_user" | "delegating_agent" | "receiving_agent";
}
type DelegationConstraintRule =
| AgentOriginRule
| TrustEscalationRule
| ProhibitedDelegateRule
| CostAttributionRule;
4.7 Content policy rules¶
interface BrandVoiceRule {
type: "brand_voice";
guidelines: string; // free-text guidelines injected into the system prompt
}
interface TopicRestrictionEntry {
topic: string;
action: "refuse" | "refuse_with_referral" | "warn_then_proceed";
message: string; // response to give if the topic is raised
}
interface TopicRestrictionRule {
type: "topic_restriction";
restrictions: TopicRestrictionEntry[];
}
interface MandatoryDisclaimerRule {
type: "mandatory_disclaimer";
triggers: string[]; // keywords that activate the disclaimer
disclaimer: string; // text to include
position: "start" | "end" | "inline";
}
interface ResponseFormatRule {
type: "response_format";
maxWords?: number;
guideline: string;
}
interface LanguageRule {
type: "language";
allowedLanguages: string[]; // ISO 639-1 codes, e.g. ["en", "fr"]
defaultLanguage: string;
}
interface CompetitorMentionRule {
type: "competitor_mention";
action: "deny" | "neutral_only"; // deny = never mention, neutral_only = factual only
competitors?: string[]; // specific competitor names, or omit for all
}
type ContentPolicyRule =
| BrandVoiceRule
| TopicRestrictionRule
| MandatoryDisclaimerRule
| ResponseFormatRule
| LanguageRule
| CompetitorMentionRule;
4.8 Audit requirement rules¶
interface LoggingDepthRule {
type: "logging_depth";
agentTrustLevel: AgentTrustLevel[];
depth: LoggingDepth;
}
interface ReviewTriggerRule {
type: "review_trigger";
action: string;
reviewWindow: number; // milliseconds
assignTo: string; // role or user identifier
}
interface DisclosureRule {
type: "disclosure";
channels: string[]; // channel types where disclosure is required
disclosure: string; // the disclosure text
position: "header" | "footer" | "signature";
}
interface PeriodicReviewRule {
type: "periodic_review";
action: string;
threshold: number;
period: BudgetPeriod;
assignTo: string;
message: string; // supports {count} placeholder
}
interface RetentionRule {
type: "retention";
eventTypes: string[];
retentionPeriod: RetentionPeriod;
}
type AuditRequirementRule =
| LoggingDepthRule
| ReviewTriggerRule
| DisclosureRule
| PeriodicReviewRule
| RetentionRule;
4.9 Approval gate rules¶
interface ActionThresholdGate {
type: "action_threshold";
action: string; // supports wildcards, e.g. "financial:*"
condition: ThresholdCondition;
message: string; // supports {field} placeholders
}
interface FirstOfTypeGate {
type: "first_of_type";
action: string;
approvalCount: number; // how many times approval is required
scope: "per_user" | "per_agent" | "per_account";
}
interface ExternalPartyGate {
type: "external_party";
actions: string[];
condition: "recipient_is_external";
message: string;
}
interface EscalationGate {
type: "escalation";
triggers: string[];
action: "route_to_human" | "pause_and_notify";
channelBehaviour: string;
}
type ApprovalGateRule =
| ActionThresholdGate
| FirstOfTypeGate
| ExternalPartyGate
| EscalationGate;
4.10 Union of all rule types¶
/** Discriminated union of all policy rule types. Use with the `category` field
* on the policy record to narrow to the correct variant. */
type PolicyRule =
| ActionPermissionRule
| CostLimitRule
| DataBoundaryRule
| TemporalConstraintRule
| DelegationConstraintRule
| ContentPolicyRule
| AuditRequirementRule
| ApprovalGateRule;
5. Supporting data structures¶
5.1 Policy tables¶
The policy tables from 03-agent-architecture.md are extended with the full category set and scoping fields:
account_policies: defineTable({
accountId: v.string(),
category: v.union(
v.literal("action_permission"),
v.literal("cost_limit"),
v.literal("data_boundary"),
v.literal("temporal_constraint"),
v.literal("delegation_constraint"),
v.literal("content_policy"),
v.literal("audit_requirement"),
v.literal("approval_gate"),
),
agentScope: v.optional(v.string()), // agent slug, or "*" for all
channelScope: v.optional(v.any()), // channel ID, channel type filter, or null for all
toolScope: v.optional(v.string()), // tool name, or null for all
userScope: v.optional(v.string()), // user ID, or null for all
rule: v.any(), // category-specific rule body (see section 4)
enabled: v.boolean(),
priority: v.number(), // lower number = higher priority
description: v.optional(v.string()), // human-readable description for admin UI
createdBy: v.string(),
createdAt: v.number(),
updatedAt: v.optional(v.number()),
})
.index("by_account", ["accountId"])
.index("by_account_category", ["accountId", "category"])
.index("by_account_agent", ["accountId", "agentScope"]),
team_policies: defineTable({
teamId: v.id("teams"),
accountId: v.string(),
category: v.union(
v.literal("action_permission"),
v.literal("cost_limit"),
v.literal("data_boundary"),
v.literal("temporal_constraint"),
v.literal("content_policy"),
v.literal("approval_gate"),
),
agentScope: v.optional(v.string()),
toolScope: v.optional(v.string()),
rule: v.any(), // category-specific rule body (see section 4)
enabled: v.boolean(),
priority: v.number(),
description: v.optional(v.string()),
createdBy: v.string(),
createdAt: v.number(),
updatedAt: v.optional(v.number()),
})
.index("by_team", ["teamId"])
.index("by_team_category", ["teamId", "category"]),
Note that team_policies supports a subset of categories. Delegation constraints and audit requirements are account-level only, as teams should not be able to weaken delegation controls or reduce audit coverage.
User preferences are stored in user_agent_config (03-agent-architecture.md (runtime configuration section).5) and support overrides for trigger mode, notification mode, and a limited set of policy tightenings (e.g. "require confirmation for all actions in my DMs").
5.2 Approval state¶
When an approval gate triggers, the system needs to track the pending approval:
pending_approvals: defineTable({
accountId: v.string(),
channelId: v.id("channels"),
messageId: v.optional(v.id("messages")), // the confirmation request message
agentId: v.id("agents"),
userId: v.string(), // user who needs to approve
workflowId: v.optional(v.string()), // workflow waiting for this approval
correlationId: v.string(), // event key to resume the workflow
gateCategory: v.string(), // which gate triggered
gateRule: v.any(), // the specific rule that triggered
actionSummary: v.string(), // human-readable description of what the agent wants to do
actionPayload: v.optional(v.any()), // the tool call payload for reference
status: v.union(
v.literal("pending"),
v.literal("approved"),
v.literal("denied"),
v.literal("expired"),
),
createdAt: v.number(),
resolvedAt: v.optional(v.number()),
resolvedBy: v.optional(v.string()),
expiresAt: v.number(), // auto-expire if not resolved
})
.index("by_channel", ["channelId", "status"])
.index("by_user", ["userId", "status"])
.index("by_correlation", ["correlationId"])
.index("by_expiry", ["status", "expiresAt"]),
Approval flow:
- Gate triggers during tool execution
- System creates a
pending_approvalsrecord and posts a confirmation message to the channel - Workflow pauses via
awaitEvent(correlationId) - User responds (approve/deny) via channel interaction or UI button
- System updates the approval record and resumes the workflow with the decision
- If the approval expires (user never responds), the workflow resumes with a denial and the agent notifies the user
5.3 Policy templates¶
Organisations should not need to build policies from scratch. The platform provides templates for common governance patterns:
policy_templates: defineTable({
name: v.string(),
description: v.string(),
industry: v.optional(v.string()), // "healthcare", "finance", "legal", "general"
category: v.string(),
rule: v.any(),
isDefault: v.boolean(), // applied automatically to new accounts
})
.index("by_industry", ["industry"])
.index("by_category", ["category"]),
5.4 Default templates¶
The following templates are applied automatically when a new account is provisioned. Together they provide a safe baseline that organisations can then adjust. Each entry below corresponds to one policy_templates record with isDefault: true.
Action permissions: external communication requires confirmation:
{
name: "default_external_comms_confirm",
description: "All outbound communication tools require user confirmation by default.",
industry: "general",
category: "action_permission",
isDefault: true,
rule: {
permissions: [
{ action: "email:send", level: "confirm" },
{ action: "email:send_external", level: "confirm" },
{ action: "sms:send", level: "confirm" },
{ action: "whatsapp:send", level: "confirm" },
{ action: "calendar:send_invitation", level: "confirm" },
],
},
}
Cost limits: per-user daily budget scaled to subscription plan:
// Starter plan
{
name: "default_cost_limit_starter",
description: "Daily per-user budget for Starter plan accounts.",
industry: "general",
category: "cost_limit",
isDefault: true, // applied when account is on Starter plan
rule: {
type: "monetary_period",
maxAmount: 5.00,
currency: "USD",
period: "day",
scope: "user",
},
}
// Professional plan
{
name: "default_cost_limit_professional",
description: "Daily per-user budget for Professional plan accounts.",
industry: "general",
category: "cost_limit",
isDefault: true,
rule: {
type: "monetary_period",
maxAmount: 20.00,
currency: "USD",
period: "day",
scope: "user",
},
}
// Business plan
{
name: "default_cost_limit_business",
description: "Daily per-user budget for Business plan accounts.",
industry: "general",
category: "cost_limit",
isDefault: true,
rule: {
type: "monetary_period",
maxAmount: 50.00,
currency: "USD",
period: "day",
scope: "user",
},
}
Content policy: baseline brand voice:
{
name: "default_brand_voice",
description: "Baseline tone: professional, clear, and helpful.",
industry: "general",
category: "content_policy",
isDefault: true,
rule: {
type: "brand_voice",
guidelines: "Be professional, clear, and helpful. Avoid profanity, sarcasm, and "
+ "overly casual language. Do not make promises or guarantees on behalf of the "
+ "organisation unless explicitly instructed to do so. If uncertain, say so.",
},
}
Audit: summary logging for standard agents, full trace for elevated:
{
name: "default_audit_standard",
description: "Summary-level logging for read and standard trust agents.",
industry: "general",
category: "audit_requirement",
isDefault: true,
rule: {
type: "logging_depth",
agentTrustLevel: ["read", "standard"],
depth: "summary",
},
}
{
name: "default_audit_elevated",
description: "Full reasoning trace for elevated and admin trust agents.",
industry: "general",
category: "audit_requirement",
isDefault: true,
rule: {
type: "logging_depth",
agentTrustLevel: ["elevated", "admin"],
depth: "full_trace",
},
}
Approval gate: learn-then-trust for external communications:
{
name: "default_learn_then_trust",
description: "First 5 outbound messages per agent per user require confirmation.",
industry: "general",
category: "approval_gate",
isDefault: true,
rule: {
type: "first_of_type",
action: "email:send",
approvalCount: 5,
scope: "per_user",
},
}
Delegation: platform and custom agents only, trust escalation capped:
{
name: "default_delegation_origin",
description: "Agents may only delegate to platform and custom agents, not external.",
industry: "general",
category: "delegation_constraint",
isDefault: true,
rule: {
type: "agent_origin",
allowedOrigins: ["platform", "custom"],
deniedOrigins: ["external"],
},
}
{
name: "default_trust_escalation",
description: "A delegation chain may include at most one elevated-trust agent.",
industry: "general",
category: "delegation_constraint",
isDefault: true,
rule: {
type: "trust_escalation",
maxElevatedAgentsInChain: 1,
},
}
{
name: "default_cost_attribution",
description: "All delegation costs are attributed to the user who started the interaction.",
industry: "general",
category: "delegation_constraint",
isDefault: true,
rule: {
type: "cost_attribution",
mode: "originating_user",
},
}
5.5 Industry template packs¶
Industry packs are opt-in sets of policies that an account administrator can apply during onboarding or at any time from the governance settings. They layer on top of the defaults.
5.5.1 Healthcare¶
// PII redaction in broad channels
{
name: "healthcare_pii_redaction",
description: "Redact patient PII in organisation-wide and public channels.",
industry: "healthcare",
category: "data_boundary",
isDefault: false,
rule: {
type: "pii_handling",
action: "redact",
fields: [
"patient_name", "date_of_birth", "phone", "email", "address",
"medicare_number", "health_fund_id", "next_of_kin",
],
},
}
// Classification ceiling for standard agents
{
name: "healthcare_classification_ceiling",
description: "Standard agents limited to 'internal' classification. Clinical data requires elevated trust.",
industry: "healthcare",
category: "data_boundary",
isDefault: false,
rule: {
type: "classification_ceiling",
maxClassification: "internal",
classifications: ["public", "internal", "confidential", "restricted"],
},
}
// External processing restriction for clinical data
{
name: "healthcare_external_processing",
description: "Clinical data must not be sent to external LLM providers or parsing services.",
industry: "healthcare",
category: "data_boundary",
isDefault: false,
rule: {
type: "external_processing",
allowExternalLLM: false,
allowExternalParsing: false,
allowExternalStorage: false,
exemptTools: [],
},
}
// Mandatory health disclaimer
{
name: "healthcare_disclaimer",
description: "Append a general-information-only disclaimer to any health-related response.",
industry: "healthcare",
category: "content_policy",
isDefault: false,
rule: {
type: "mandatory_disclaimer",
triggers: ["health", "medical", "treatment", "symptom", "diagnosis", "medication", "prescription"],
disclaimer: "This is general information only and does not constitute medical advice. "
+ "Please consult a qualified healthcare professional for advice specific to your situation.",
position: "end",
},
}
// Topic restriction: no diagnosis
{
name: "healthcare_no_diagnosis",
description: "Agents must not provide medical diagnoses or treatment plans.",
industry: "healthcare",
category: "content_policy",
isDefault: false,
rule: {
type: "topic_restriction",
restrictions: [
{
topic: "medical_diagnosis",
action: "refuse_with_referral",
message: "I can share general health information but cannot provide a diagnosis. "
+ "Please consult your treating practitioner.",
},
{
topic: "treatment_recommendation",
action: "refuse_with_referral",
message: "I cannot recommend specific treatments. Please discuss options with your "
+ "treating practitioner.",
},
],
},
}
// Patient data access audit retention
{
name: "healthcare_audit_retention",
description: "Retain audit records for patient data access for 7 years.",
industry: "healthcare",
category: "audit_requirement",
isDefault: false,
rule: {
type: "retention",
eventTypes: ["patient_data_access", "clinical_note_read", "clinical_note_write"],
retentionPeriod: "7y",
},
}
// Disclosure on outbound comms
{
name: "healthcare_ai_disclosure",
description: "All outbound patient communications must disclose AI involvement.",
industry: "healthcare",
category: "audit_requirement",
isDefault: false,
rule: {
type: "disclosure",
channels: ["email", "sms", "whatsapp"],
disclosure: "This message was composed with AI assistance and reviewed by a member of our team.",
position: "footer",
},
}
5.5.2 Finance¶
// Financial transaction approval gate
{
name: "finance_transaction_gate",
description: "Financial transactions above $100 require explicit approval.",
industry: "finance",
category: "approval_gate",
isDefault: false,
rule: {
type: "action_threshold",
action: "financial:*",
condition: { field: "amount", operator: "gt", value: 100.00 },
message: "This transaction is for ${amount}. Please confirm to proceed.",
},
}
// Compliance checker delegation restriction
{
name: "finance_compliance_delegation",
description: "Compliance checker cannot delegate to agents it audits.",
industry: "finance",
category: "delegation_constraint",
isDefault: false,
rule: {
type: "prohibited_delegate",
deniedAgents: ["content-writer", "report-generator", "outbound-comms"],
reason: "Compliance checker must maintain independence from agents whose output it reviews.",
},
}
// Full audit trace for all agents
{
name: "finance_full_audit",
description: "Full reasoning trace for all agent trust levels in financial accounts.",
industry: "finance",
category: "audit_requirement",
isDefault: false,
rule: {
type: "logging_depth",
agentTrustLevel: ["read", "standard", "elevated", "admin"],
depth: "full_trace",
},
}
// Financial transaction audit retention
{
name: "finance_audit_retention",
description: "Retain audit records for financial transactions for 7 years.",
industry: "finance",
category: "audit_requirement",
isDefault: false,
rule: {
type: "retention",
eventTypes: ["financial_transaction", "payment_processed", "invoice_generated", "refund_issued"],
retentionPeriod: "7y",
},
}
// Daily outbound communication cap
{
name: "finance_daily_outbound_cap",
description: "Limit outbound emails to 20 per day per agent to prevent spam risk.",
industry: "finance",
category: "temporal_constraint",
isDefault: false,
rule: {
type: "rate_of_action",
action: "email:send",
maxCount: 20,
period: "day",
},
}
// Topic restriction: no financial advice
{
name: "finance_no_advice",
description: "Agents must not provide personalised financial advice.",
industry: "finance",
category: "content_policy",
isDefault: false,
rule: {
type: "topic_restriction",
restrictions: [
{
topic: "financial_advice",
action: "refuse_with_referral",
message: "I can provide factual financial information but not personalised advice. "
+ "Please consult a licensed financial adviser.",
},
{
topic: "investment_recommendation",
action: "refuse_with_referral",
message: "I cannot recommend specific investments. Please speak with a licensed "
+ "financial adviser.",
},
],
},
}
// External party gate for financial comms
{
name: "finance_external_party_gate",
description: "All communications to external parties require approval in financial accounts.",
industry: "finance",
category: "approval_gate",
isDefault: false,
rule: {
type: "external_party",
actions: ["email:send", "sms:send", "whatsapp:send"],
condition: "recipient_is_external",
message: "This will contact someone outside the organisation. Please confirm.",
},
}
5.5.3 Legal¶
// Classification enforcement: privilege-aware
{
name: "legal_classification_ceiling",
description: "Standard agents limited to 'internal'. Privileged documents require elevated trust.",
industry: "legal",
category: "data_boundary",
isDefault: false,
rule: {
type: "classification_ceiling",
maxClassification: "internal",
classifications: ["public", "internal", "confidential", "restricted"],
},
}
// Cross-channel data isolation: strict
{
name: "legal_cross_channel_isolation",
description: "No cross-channel data references. Matter-specific channels are fully isolated.",
industry: "legal",
category: "data_boundary",
isDefault: false,
rule: {
type: "cross_channel",
allowCrossReference: false,
exemptChannelTypes: [], // no exemptions: full isolation
},
}
// External processing restriction
{
name: "legal_external_processing",
description: "Client data must not be sent to external services.",
industry: "legal",
category: "data_boundary",
isDefault: false,
rule: {
type: "external_processing",
allowExternalLLM: false,
allowExternalParsing: false,
allowExternalStorage: false,
exemptTools: [],
},
}
// Full audit trace for all agents
{
name: "legal_full_audit",
description: "Full reasoning trace for all agent trust levels in legal accounts.",
industry: "legal",
category: "audit_requirement",
isDefault: false,
rule: {
type: "logging_depth",
agentTrustLevel: ["read", "standard", "elevated", "admin"],
depth: "full_trace",
},
}
// Audit retention: regulatory compliance
{
name: "legal_audit_retention",
description: "Retain audit records for client matters for 7 years after matter closure.",
industry: "legal",
category: "audit_requirement",
isDefault: false,
rule: {
type: "retention",
eventTypes: ["document_access", "document_edit", "client_data_access", "privileged_access"],
retentionPeriod: "7y",
},
}
// Topic restriction: no legal advice
{
name: "legal_no_advice",
description: "Agents must not provide legal opinions or advice.",
industry: "legal",
category: "content_policy",
isDefault: false,
rule: {
type: "topic_restriction",
restrictions: [
{
topic: "legal_advice",
action: "refuse_with_referral",
message: "I can help locate and summarise legal information, but I cannot provide "
+ "legal advice or opinions. Please consult the supervising solicitor.",
},
],
},
}
// Mandatory disclaimer on all agent output
{
name: "legal_ai_disclaimer",
description: "All agent responses include a disclaimer that output is AI-generated and must be reviewed.",
industry: "legal",
category: "content_policy",
isDefault: false,
rule: {
type: "mandatory_disclaimer",
triggers: [], // empty triggers = always apply
disclaimer: "This content was generated by an AI assistant and has not been reviewed by "
+ "a qualified legal professional. It must not be relied upon as legal advice.",
position: "end",
},
}
// Review trigger for document edits
{
name: "legal_document_review_trigger",
description: "Any agent edit to a client document triggers a 48-hour review flag.",
industry: "legal",
category: "audit_requirement",
isDefault: false,
rule: {
type: "review_trigger",
action: "document:edit",
reviewWindow: 172800000, // 48 hours
assignTo: "account_admin",
},
}
6. Governance UI concepts¶
The admin interface for governance should make the policy model accessible without requiring understanding of the underlying data structures.
6.1 Agent policy view¶
For each agent, show a consolidated view of effective policies (merged across account, team, and user layers):
- What it can do: list of action permissions with their current levels
- What it can spend: active cost limits and current usage against those limits
- What it can see: data boundary rules in effect
- When it can act: temporal constraints and operating hours
- Who it can delegate to: delegation set with any constraints
- What it must say: content policies and mandatory disclaimers
- What is logged: audit requirements and any active review flags
6.2 Policy conflict view¶
When a team or user policy interacts with an account policy, show clearly which layer is setting the effective rule. Highlight any cases where a lower layer is attempting to loosen a restriction (which should be prevented by the system, but worth surfacing for admin awareness).
6.3 Approval dashboard¶
A queue of pending approvals, filterable by agent, user, channel, and gate type. Shows: what the agent wants to do, why approval is required, how long the approval has been pending, and one-click approve/deny actions.
6.4 Audit trail¶
Searchable log of all agent actions, filterable by agent, user, action type, and date range. Review flags are surfaced prominently. Full reasoning traces are available for elevated-trust agent turns.
7. Policy evaluation performance¶
Policy evaluation must not introduce noticeable latency. The design addresses this through:
- Reactive caching. Account and team policies are loaded via Convex queries, which are reactively cached. Policy changes propagate automatically without manual cache invalidation.
- Snapshot at turn start. The full effective policy set is resolved once at the beginning of each agent turn, not re-evaluated for every tool call. This means a single policy resolution per turn, not per step.
- Lightweight evaluation. Policy rules are declarative data evaluated by simple matching logic (permission level comparisons, numeric threshold checks, time range checks). No LLM calls, no external lookups.
- Content policies are string injection. Content policies are formatted as system prompt text and injected via the contextHandler. This adds a few hundred tokens to the prompt but no evaluation overhead.
The governance component (if implemented as a custom Convex component per 03-agent-architecture.md (component integration section)) can maintain its own resolved policy cache table for frequently accessed policy combinations.
8. What this framework does not cover¶
- Policy CRUD implementation (admin UI and API endpoints): deferred to implementation
- Specific compliance framework mappings (HIPAA, SOC 2, GDPR): these would be built as extensions to the industry template packs defined in section 5.5
- Rate limiter and sharded counter configuration: see
03-agent-architecture.md(rate limiter and sharded counter sections) - Agent trust level definitions: see
03-agent-architecture.md(trust levels section) - User role and permission model (Clerk RBAC): see
02-system-architecture.md(who can set policies is a Clerk role question, not a governance policy question)
Part C: Credential management¶
Part C covers how Thinklio stores, protects, and injects credentials that agents and MCP servers need to call external systems. It has three layers: a brief overview of the three credential scopes (platform, account, user) and how they map to storage; the full Secrets Vault product specification (a first-class Thinklio feature for user-managed secrets with sharing, agent integration, and access logging); and the MCP credential injection model that resolves credentials per-request and handles OAuth refresh transparently.
Note on storage backend. The original Secrets Vault specification was designed against Supabase Vault. In the Convex-first architecture, the vault is implemented on top of Convex tables using envelope encryption: values are encrypted client-side by a server action with a key held in platform secrets (or a KMS), and only ciphertext plus a vault_ref is stored in the secret table. The API surface, security properties, and data model described below are unchanged. Where the text below references vault.create_secret() or vault.decrypted_secrets, treat these as operation names rather than specific Postgres functions; the implementation binds them to the equivalent Convex actions.
1. Overview and credential scopes¶
Every external integration needs credentials to authenticate to the upstream API. Thinklio recognises three scopes:
| Scope | Who provides | Storage | Lifecycle | Example |
|---|---|---|---|---|
| Platform | Thinklio (us) | Infrastructure secrets / Convex env | Managed by Thinklio ops | OpenRouter API key, internal service tokens |
| Account | Organisation admin | Account-scoped Secrets Vault entry | Admin configures once, shared by all agents | Xero OAuth tokens, shared Notion workspace token |
| User | Individual user | User-scoped Secrets Vault entry | Each user provides their own | Cliniko API key (per-clinician), personal Notion token |
The scope an integration needs is declared in its MCP /meta endpoint (see Part D section 8). Thinklio's admin UI uses this to show the credential input in the right place: account-scoped credentials appear in Settings > Integrations (admin-only); user-scoped credentials appear in each user's personal Settings > Integrations.
Platform secrets sit outside the vault: they are loaded as environment variables on the Convex deployment or injected by the operations pipeline. The vault is exclusively for account- and user-scoped secrets.
2. Secrets Vault specification¶
The Secrets Vault is a Thinklio product feature: users store credentials, API keys, TOTP seeds, configuration blocks, and other sensitive data with fine-grained sharing, full access logging, and agent integration. Values are never displayed in the UI except via explicit reveal by the creator; shared users can only copy to clipboard. Agents can use stored credentials only with per-secret, per-session approval and every access is logged.
2.1 Vault overview and design principles¶
Thinklio's Secrets Vault provides secure storage, retrieval, and sharing of credentials, API keys, certificates, configuration blocks, and other sensitive data. It is a core data primitive alongside tasks, contacts, items, and notes.
The vault integrates with the agent system: agents can use stored credentials on the user's behalf (with explicit approval) without ever exposing values in conversation history. The vault also supports secure sharing between team members with single-use and expiring options.
Design Principles¶
- Zero display by default. Secret values are never shown in the UI except via an explicit reveal action by the creator. Shared users can only copy to clipboard: the value never appears on screen.
- Values never in our tables. The
secrettable stores metadata (name, type, URL, username). The actual secret value lives in Supabase Vault, referenced byvault_ref. Even if thesecrettable is compromised, values remain encrypted. - Every access is logged. A dedicated
secret_access_logtable records every vault read with user, agent, access type, IP, and timestamp. No silent reads. - Agent access requires approval. An agent must request user approval per-secret, per-session before reading a value. Delegation does not inherit secret access.
- Sharing is controlled. Only the creator can share. Shared users cannot re-share. Shares can be single-use, time-limited, and revocable.
2.2 Vault data model¶
2.2.1 Secret¶
The metadata record for a stored secret. The actual value is in Supabase Vault.
| Field | Type | Description |
|---|---|---|
| id | UUID | PK |
| account_id | UUID | FK → account (CASCADE) |
| scope | TEXT | user, team, account |
| scope_id | UUID | Scope entity ID |
| secret_type | TEXT | password, api_key, ssh_key, certificate, config_block, secure_note, totp |
| name | TEXT | Human-readable name (e.g. "Staging DB", "AWS Production") |
| vault_ref | TEXT | Reference to the Supabase Vault entry: the ONLY link to the encrypted value |
| username | TEXT | Optional: for login credentials |
| url | TEXT | Optional: for website passwords |
| visibility | TEXT | private, team, account |
| expires_at | TIMESTAMPTZ | Optional: for rotation/expiry alerts |
| last_accessed_at | TIMESTAMPTZ | Updated on every vault read |
| last_accessed_by | UUID | FK → user_profile |
| access_count | INTEGER | Total number of vault reads |
| metadata | JSONB | Notes, tags, custom fields, rotation schedule |
| created_by | UUID | FK → user_profile |
| created_at | TIMESTAMPTZ | |
| updated_at | TIMESTAMPTZ |
Constraints:
- UNIQUE(account_id, scope, scope_id, name): no duplicate names per scope
- RLS: private secrets visible only to creator + explicit shares
Secret types:
| Type | Fields Used | Description |
|---|---|---|
password |
username + vault_ref + url | Website/service login credentials |
api_key |
vault_ref + url | API key with optional endpoint URL |
ssh_key |
vault_ref | SSH private key (multi-line) |
certificate |
vault_ref + expires_at | TLS/SSL certificate (multi-line) |
config_block |
vault_ref | .env files, server configs, connection strings (multi-line) |
secure_note |
vault_ref | Free-form encrypted text |
totp |
vault_ref | TOTP seed: agent can compute current 6-digit code |
2.2.2 Secret share¶
Granular per-user sharing with single-use and expiry options.
| Field | Type | Description |
|---|---|---|
| id | UUID | PK |
| secret_id | UUID | FK → secret (CASCADE) |
| user_id | UUID | FK → user_profile (recipient) |
| shared_by | UUID | FK → user_profile (creator) |
| is_single_use | BOOLEAN | If true, share is invalidated after first copy |
| expires_at | TIMESTAMPTZ | Nullable: no expiry if NULL |
| used_at | TIMESTAMPTZ | Set on first copy if single_use |
| status | TEXT | active, used, expired, revoked |
| created_at | TIMESTAMPTZ |
Constraints:
- UNIQUE(secret_id, user_id): one active share per user per secret
- Only the secret's creator can create shares
- Shared users cannot re-share
2.2.3 Secret access log¶
Dedicated audit trail for every vault value read. Separate from the general event table for security isolation.
| Field | Type | Description |
|---|---|---|
| id | UUID | PK |
| secret_id | UUID | FK → secret |
| user_id | UUID | FK → user_profile (who accessed) |
| agent_id | UUID | FK → agent (nullable: set if accessed by agent) |
| access_type | TEXT | reveal (creator views), copy (shared user copies), agent_use (agent reads for operation) |
| ip_address | TEXT | Client IP |
| user_agent | TEXT | Client user agent string |
| created_at | TIMESTAMPTZ |
Retention: Access logs retained for 12 months minimum. Not subject to standard data retention policies: security audit requirements take precedence.
2.3 API endpoints¶
2.3.1 Secret CRUD¶
Create a secret. The request body includes the plaintext value, which the server stores in Supabase Vault and records the vault_ref. The value is never stored in thesecret table.
Request:
{
"name": "Staging Database",
"secret_type": "password",
"scope": "user",
"scope_id": "<user_id>",
"username": "admin",
"url": "https://db.staging.example.com",
"value": "the-actual-password",
"visibility": "private",
"expires_at": null,
"metadata": { "notes": "Rotated monthly" }
}
Response 201:
The value field is accepted on create/update only. It is NEVER returned in any GET response.
value is provided, the old Vault entry is replaced with a new one.
Delete the secret. Removes the Vault entry, cascades to shares and access logs.
2.3.2 Value access¶
Creator only. Returns the decrypted value. Logged insecret_access_log with access_type: reveal.
Response:
Response headers: Cache-Control: no-store, no-cache, must-revalidate
Frontend renders with a mask/reveal toggle.
Creator or valid share holder. Returns the decrypted value for clipboard copy. The frontend copies programmatically and clears the clipboard after 30 seconds. The value is NEVER displayed on screen for shared users.Server checks:
1. Is the user the creator? → allow
2. Does the user have an active share? → check:
- status = 'active'
- expires_at IS NULL OR expires_at > NOW()
- If is_single_use: used_at IS NULL
3. If valid: read from Vault, return value, log access
4. If is_single_use: set used_at = NOW(), status = 'used'
Response: same as reveal. Headers: Cache-Control: no-store
2.3.3 Sharing¶
Creator only. Share with a specific user.Request:
Creator only. List all shares for this secret with status and usage info. Creator only. Revoke a share. Setsstatus = 'revoked'.
2.3.4 Access log¶
Creator or account admin. Returns chronological access history.Response:
[
{
"user_id": "uuid",
"agent_id": null,
"access_type": "copy",
"ip_address": "203.0.113.42",
"created_at": "2026-03-21T14:30:00Z"
}
]
2.3.5 Password generation¶
Generate a secure password without storing it.Request:
{
"length": 32,
"include_uppercase": true,
"include_lowercase": true,
"include_numbers": true,
"include_symbols": true,
"exclude_ambiguous": true
}
Response:
The generated password is returned but NOT stored. The client can then call POST /v1/secrets to save it.
2.4 Agent integration¶
2.4.1 Agent tools¶
| Tool | Trust Level | Description |
|---|---|---|
native_secret_create |
low_risk_write | Create a new secret |
native_secret_list |
read | List secrets by name/type (metadata only, never values) |
native_secret_read |
sensitive_read | Read a secret value (requires per-secret user approval) |
native_secret_update |
high_risk_write | Update a secret's value or metadata |
native_secret_share |
low_risk_write | Share a secret with another user |
native_secret_generate |
read | Generate a secure password |
2.4.2 Approval flow¶
When an agent calls native_secret_read:
- Harness checks: does the agent have the tool assigned?
- Harness checks: is the secret within the agent's assignment scope?
- First access to this specific secret in this session: the agent sends an approval request to the user:
- If approved: value read from Vault, passed to agent's tool execution context
- If denied: agent receives "access denied" and must proceed without the credential
- Subsequent accesses to the SAME secret in the SAME session: no re-approval needed
- Access logged in
secret_access_logwithaccess_type: agent_useand the agent_id
2.4.3 Value handling in agent context¶
The value MUST NOT appear in:
- Agent response text (use secure_fields content type instead)
- Step output_data or input_data (store "vault_ref accessed" not the value)
- Knowledge facts (extraction pipeline skips vault-flagged interactions)
- Event payloads
- Logs
Post-processing filter: After every agent response that follows a native_secret_read call, a filter checks the response text against the recently-read vault values. If a match is found, it is redacted and replaced with [REDACTED: credential]. This is a safety net: the system prompt instructs agents not to display credentials, but LLMs can't be fully trusted.
2.4.4 Secure fields content type¶
When an agent retrieves credentials for the user, the response uses a special content type:
{
"text": "Here are the credentials for Staging DB.",
"secure_fields": [
{
"label": "Username",
"value": "admin",
"type": "text"
},
{
"label": "Password",
"vault_ref": "vault_ref_456",
"type": "masked"
}
]
}
Channel rendering:
| Channel | Secure fields rendering |
|---|---|
| Web app | Card with masked values, reveal toggle (creator), copy button, 30s clipboard clear |
| Telegram | Text only: "I've retrieved the credentials. Open the web app to view them securely." |
| Text only: same message | |
| API | Full JSON including vault_ref: consumer handles security |
| Voice | Text only: "I have the credentials ready. Check the web app." |
The vault_ref is resolved client-side: the web app calls /v1/secrets/{id}/reveal or /v1/secrets/{id}/copy when the user clicks reveal/copy. The actual value is NEVER in the conversation message payload.
2.5 Vault security model¶
2.5.1 Value lifecycle¶
CREATE: Client → [TLS] → Server → vault.create_secret(value) → vault_ref stored in secret table
↓
Value encrypted at rest in Supabase Vault
REVEAL: Client → [TLS] → Server → vault.decrypted_secrets WHERE name = vault_ref → value
(creator only) ↓
Logged in secret_access_log, returned over TLS
COPY: Client → [TLS] → Server → vault.decrypted_secrets WHERE name = vault_ref → value
(creator or share) ↓
Share validated, logged, returned over TLS
Frontend copies to clipboard, clears after 30 seconds
AGENT: Harness → approval → vault.decrypted_secrets WHERE name = vault_ref → value
↓
Used in tool execution, NEVER stored in output
Logged in secret_access_log with agent_id
2.5.2 Access control matrix¶
| Action | Creator | Shared User (active) | Shared User (expired/used) | Team Member | Account Admin | Agent |
|---|---|---|---|---|---|---|
| See metadata | ✓ | ✓ (name only) | ✗ | Scope-dependent | ✓ (metadata only) | Scope-dependent |
| Reveal value | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Copy value | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Create share | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Re-share | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Revoke share | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ |
| Use value | ✓ | ✗ | ✗ | ✗ | ✗ | With approval |
| View access log | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ |
| Delete secret | ✓ | ✗ | ✗ | ✗ | ✓ | ✗ |
2.5.3 Rate limiting¶
| Endpoint | Limit | Per |
|---|---|---|
/v1/secrets/{id}/reveal |
10/minute | User |
/v1/secrets/{id}/copy |
10/minute | User |
Agent native_secret_read |
5/interaction | Agent |
| Failed access attempts | After 5 failures, 15-minute lockout | User + secret |
2.5.4 Threat mitigations¶
| Threat | Mitigation |
|---|---|
| LLM leakage (agent includes value in response) | Post-processing redaction filter + system prompt instructions + knowledge extraction skip |
| Clipboard exposure | Auto-clear after 30 seconds (frontend). Never displayed on screen for shared users |
| Prompt injection (attacker tricks agent into revealing secrets) | Per-secret approval, scope validation, rate limiting, audit trail |
| Server-side access (insider with DB access) | Values encrypted in Vault. DB access shows vault_ref only. Vault decryption requires separate encryption key access |
| Session hijacking | Same as any JWT-authenticated system. Short JWT expiry. Re-authentication for sensitive operations possible via Supabase Auth |
| Share forwarding (recipient shares value externally) | Cannot prevent. Audit trail records who accessed when. Single-use shares reduce exposure window |
| Stale shares (value already copied before expiry) | Cannot prevent. Rotation reminders encourage credential changes after shares expire |
| Man-in-the-middle | TLS. Cache-Control: no-store. Short-lived responses |
2.5.5 Explicit trade-offs¶
-
Not zero-knowledge. The server handles plaintext values during create/reveal/copy operations (protected by TLS). Supabase Vault encrypts at rest but is not client-side encrypted. A Supabase administrator with encryption key access could theoretically decrypt values. For most users, this is acceptable: it's the same security model as 1Password's web vault, LastPass, and similar services. True zero-knowledge (client-side encryption) is a Phase 2 feature for enterprise customers requiring HSM or BYOK encryption.
-
Not a password manager replacement. No browser extension, no auto-fill, no password health scoring, no import from other managers. The value proposition is agent integration, team sharing within the work context, and elimination of context switching: not competing with dedicated password managers.
-
Agent access is a calculated risk. Allowing agents to read secrets means the LLM has the plaintext in its context window. The post-processing filter and audit trail mitigate but cannot fully eliminate leakage risk. Users must explicitly approve agent access per-secret and should use agent-accessible secrets only for credentials the agent genuinely needs.
2.6 UI and UX¶
2.6.1 Vault page¶
Accessible from the sidebar or as a view on the PA agent. Shows: - List of secrets, grouped by type or filtered by tag - Search by name - Each row: name, type icon, username (if applicable), URL (if applicable), expiry warning, share count - Actions: Copy, Reveal (creator only), Share, Edit, Delete
2.6.2 Create and edit form¶
Adaptive based on secret_type:
- Password: name, username, password (masked input with generate button), URL
- API Key: name, key (masked input), endpoint URL
- SSH Key / Certificate / Config Block: name, large textarea (monospace), expiry date for certificates
- Secure Note: name, textarea
- TOTP: name, seed input, shows current 6-digit code live
All forms include: scope selector, visibility, expiry date (optional), metadata/notes field.
2.6.3 Share dialog¶
- Recipient picker (searches account users)
- Single-use toggle
- Expiry date/time picker (or "no expiry")
- Confirm button
- List of current shares with status and revoke action
2.6.4 Shared secret view (recipient)¶
The recipient sees: - Secret name, type, who shared it, expiry countdown - "Copy to Clipboard" button (large, prominent) - NO reveal toggle, NO value display - After single-use copy: "This share has been used" with no further copy option
2.6.5 Access log view¶
Chronological table: date/time, who accessed, access type (reveal/copy/agent), IP address. Available to creator and account admins.
2.7 Expiry and rotation alerts¶
The existing reminder worker checks secret.expires_at and creates notifications:
| Condition | Notification |
|---|---|
| Expires in 30 days | Info notification (in-app) |
| Expires in 7 days | Warning notification (in-app + push) |
| Expires in 24 hours | Urgent notification (in-app + push + email) |
| Expired | Alert notification (in-app + push) |
Share expiry is handled by the comms worker: when secret_share.expires_at passes, status is set to expired.
2.8 Implementation notes¶
2.8.1 Vault operations¶
-- Create a secret value in Vault
SELECT vault.create_secret('the-actual-password', 'secret_<uuid>');
-- Read a secret value from Vault
SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'secret_<uuid>';
-- Update a secret value
SELECT vault.update_secret('secret_<uuid>', 'new-password-value');
-- Delete a secret from Vault
DELETE FROM vault.secrets WHERE name = 'secret_<uuid>';
The vault_ref stored in the secret table is always 'secret_' + secret.id for consistency.
2.8.2 TOTP code generation¶
For secrets with secret_type = 'totp', the server can compute the current TOTP code:
- Read the seed from Vault
- Generate the 6-digit code using HMAC-SHA1 with the current 30-second time window
- Return the code (valid for ~30 seconds)
This allows the agent to provide 2FA codes: "The current verification code for your AWS account is 847293. It expires in 18 seconds."
2.8.3 Search and discovery¶
Secrets are searchable by name and metadata via the list endpoint. The q parameter searches name and metadata (ILIKE). Agents can use native_secret_list to find relevant secrets: "Do I have credentials for Hetzner?" → searches name.
2.8.4 Tag integration¶
Secrets participate in the existing tag system via entity_tag with entity_type = 'secret'. Tags like "production", "staging", "personal", "shared" help organize the vault.
2.9 Impact on other systems¶
| System | Impact |
|---|---|
| Entity tag system | Add secret to entity_type enum on entity_tag |
| Reminder worker | Add secret.expires_at checks alongside task due dates |
| Knowledge extraction | Skip extraction for interactions that accessed vault data |
| Agent response filter | Add post-processing redaction for vault values |
| Agent manifest | Add native_secret_* tools to the tool registry |
| Secure fields content type | New content type for channel rendering |
| RLS policies | New policies for secret, secret_share, secret_access_log |
2.10 Future considerations¶
- Client-side encryption (Phase 2): For enterprise customers requiring true zero-knowledge. User's master password derives an encryption key. Values encrypted before leaving the browser. Server stores encrypted blobs. Sharing uses recipient's public key for re-encryption.
- Import/export: CSV import from 1Password, LastPass, Bitwarden. Encrypted export with master password.
- Password health: Strength scoring, reuse detection, breach database checking (via HaveIBeenPwned API).
- Hardware key support: WebAuthn/FIDO2 for re-authentication before reveal/copy.
- Secret versioning: Keep history of previous values for rotation auditing.
3. MCP credential injection¶
MCP servers are stateless by design: they receive credentials as HTTP headers on every request and never store credentials or manage token lifecycle. Thinklio handles storage, refresh cycles, and per-request injection. This section covers how credentials flow from the vault into MCP tool calls, including OAuth refresh handling and the credential configuration schema that every MCP server declares in its /meta endpoint.
3.1 Credential injection¶
MCP servers are stateless: they receive credentials as HTTP headers on every request. They never store credentials and never manage token lifecycle.
Request flow:
User sends message → Agent decides to call tool
→ Thinklio resolves credentials:
1. Look up MCP server's credentialConfig
2. Determine scope (platform / account / user)
3. If user-scoped: fetch requesting user's credential from vault
4. If account-scoped: fetch account credential from vault
5. If token needs refresh: refresh it (see 2.3)
→ Thinklio forwards tool call to MCP server with credential headers
→ MCP server authenticates to upstream API using the injected headers
→ Result returned to agent
Header convention per integration:
| Integration | Headers | Scope |
|---|---|---|
| Cliniko | X-Cliniko-Api-Key |
User (each clinician's key) |
| Notion | X-Notion-Api-Key |
Account (workspace integration token) |
| Xero | X-Xero-Access-Token + X-Xero-Tenant-Id |
Account (OAuth, managed by Thinklio) |
3.2 Managed credentials (OAuth)¶
Some integrations (e.g. Xero) use OAuth 2.0 with expiring access tokens. The MCP server does not handle token refresh: Thinklio does.
Xero example:
Admin authorises Xero via OAuth flow in Thinklio admin UI
→ Thinklio receives access_token + refresh_token
→ Stored encrypted in account secrets vault
→ access_token expires after 30 minutes
When agent calls a Xero tool:
→ Thinklio checks if access_token is expired
→ If expired: POST to Xero's /connect/token with refresh_token
→ Store new access_token + refresh_token
→ Inject fresh access_token as X-Xero-Access-Token header
→ Forward tool call to MCP server
The MCP server sees a valid bearer token every time. It has no awareness of the refresh cycle.
For Custom Connections (client_credentials):
Some Xero setups use client_credentials grant (no user interaction, single org). In this case: - Admin provides client_id + client_secret - Thinklio requests a new access_token every 30 minutes - No refresh token needed: just re-request with credentials
3.3 Credential states¶
From the user/agent perspective, a tool's credential state determines what happens:
| State | Cause | Agent behaviour | User sees |
|---|---|---|---|
| Ready | Credentials present and valid | Tool works normally | Nothing: invisible |
| Expired (auto-refresh) | OAuth token expired | Thinklio refreshes transparently, tool works | Nothing: invisible |
| Expired (refresh failed) | Refresh token revoked or invalid | Tool returns error | "The Xero connection needs to be re-authorised. Ask an admin." |
| Missing (account-scoped) | Admin hasn't configured the integration | Tool returns error | "The [integration] hasn't been set up yet. An admin needs to configure it in Settings → Integrations." |
| Missing (user-scoped) | User hasn't added their key | Tool returns error | "Connect your [service] account in Settings → Integrations to use this." |
These messages come from Thinklio's credential resolution layer, not the MCP server. The MCP server simply rejects with "missing header X" if credentials aren't present: Thinklio intercepts this before it reaches the agent and provides the user-friendly message.
3.4 Credential configuration data model¶
Each MCP server registration includes a credentialConfig that describes what it needs:
{
mcpServerUrl: "https://cliniko.api.thinklio.ai/mcp",
credentialConfig: {
scope: "user", // platform | account | user
headers: [
{
name: "X-Cliniko-Api-Key",
label: "Cliniko API Key",
description: "Your personal Cliniko API key. Find it in My Info → API Keys.",
required: true,
}
],
oauth: null, // or OAuth config for managed flows
}
}
For OAuth integrations:
{
mcpServerUrl: "https://xero.api.thinklio.ai/mcp",
credentialConfig: {
scope: "account",
headers: [
{ name: "X-Xero-Access-Token", managed: true }, // injected by Thinklio
{ name: "X-Xero-Tenant-Id", managed: true },
],
oauth: {
provider: "xero",
authorizeUrl: "https://login.xero.com/identity/connect/authorize",
tokenUrl: "https://identity.xero.com/connect/token",
scopes: ["openid", "profile", "email", "accounting.transactions", "accounting.contacts", "accounting.reports.read"],
tokenExpirySeconds: 1800,
refreshable: true,
}
}
}
Part D: MCP tool permissions¶
Part D defines how Thinklio decides, for each channel and each tool call, which MCP tools an agent may actually use. It covers the core intersection principle, the grant model, the resolution algorithm, channel-level downgrade, agent visibility states, delegation chains, whose credentials are used, the /meta endpoint MCP servers expose to advertise their capabilities, and the end-to-end lifecycle for adding and configuring integrations.
1. Core principle¶
In any channel, an agent's effective tool set is the intersection of all human participants' tool permissions.
This prevents privilege escalation via shared channels. A user cannot gain access to data by being in a channel with someone who has higher permissions: the agent's capabilities are constrained to what the least-privileged participant is allowed to see.
2. Permission grants¶
Tool access is granted at three levels, with most-specific winning:
| Level | Scope | Example |
|---|---|---|
| Organisation | All users in the account | "Everyone can use the Calendar Agent" |
| Team | Members of a specific team | "Finance team can use Xero tools" |
| User | Individual user | "Dr. Smith can access Cliniko" |
Grants are stored in the access_grants table:
{
subjectType: "tool" | "agent",
subjectId: Id<"tools"> | Id<"agents">,
scope: "organisation" | "team" | "user",
scopeId: Id<"teams"> | Id<"users"> | null, // null for org-wide
permissionLevel: "deny" | "read" | "standard" | "elevated" | "admin",
accountId: Id<"accounts">,
}
Permission levels align with tool trust levels (see Part B section 4.5):
| Level | What it permits |
|---|---|
deny |
No access at all |
read |
Read-only tools (list, get, search) |
standard |
Read + modify existing data (update status, edit notes) |
elevated |
Read + modify + create/delete (create invoices, book appointments) |
admin |
Full access including destructive operations |
3. Resolving a user's effective tool set¶
For a given user, their effective permissions for a tool are resolved by layering:
1. Start with organisation-level grant (if any)
2. If user is in a team with a team-level grant, apply it:
- If team grant is more restrictive, use team grant
- If team grant is less restrictive than org, org wins (most restrictive)
- If user is in multiple teams, use the LEAST restrictive team grant
(teams add permissions within the org ceiling)
3. If user has a user-level grant, apply it:
- Same most-restrictive-wins rule against the effective level so far
effectiveLevel = min(orgLevel, max(teamLevels...), userLevel)
Important nuance: Multiple team memberships are OR'd (user gets the best of their teams), but the result is still capped by the org-level grant. User-level grants can further restrict but not exceed the team/org ceiling. A user-level deny always wins.
4. Channel tool resolution¶
When an agent receives a message in a channel, the system computes the available tools:
channelParticipants = human participants in the channel (excludes bots/agents)
For each tool in the agent's assigned tool set:
For each participant:
resolve their effective permission level for this tool
channelLevel = min(all participants' effective levels)
If channelLevel == "deny" or tool's trustLevelRequired > channelLevel:
tool is EXCLUDED (invisible to agent)
Else:
tool is INCLUDED with channelLevel as the effective permission
4.1 Permission level downgrade in channels¶
When a channel contains users with different permission levels for the same tool, the tool operates at the lowest common level.
Example: Xero tools in a group chat
The Xero MCP server has tools at different trust levels:
| Tool | Trust level required |
|---|---|
xero_list_invoices |
read |
xero_get_report |
read |
xero_create_invoice |
elevated |
xero_create_payment |
elevated |
xero_update_invoice |
standard |
Scenario: Group chat with Alice (elevated Xero access) and Bob (read Xero access).
Channel effective level for Xero = min(elevated, read) = read
Available tools:
xero_list_invoices → requires read → ✓ available
xero_get_report → requires read → ✓ available
xero_update_invoice → requires standard → ✗ hidden (read < standard)
xero_create_invoice → requires elevated → ✗ hidden (read < elevated)
xero_create_payment → requires elevated → ✗ hidden (read < elevated)
Alice can see invoices and reports in this group chat, but cannot create invoices or payments. If she DMs the agent directly, she gets full elevated access.
Scenario: Same group but with Carol who has no Xero access at all.
The agent cannot use any Xero tools in this channel.
5. Agent visibility states¶
Based on the resolved permissions, agents show different states in a channel:
| State | Condition | UI treatment |
|---|---|---|
| Active | Agent has at least one usable tool in this channel | Normal: agent responds to messages |
| Limited | Agent has some tools available but others hidden due to permissions | Normal, but may say "I can't do that in this conversation" if asked for a restricted action |
| Unavailable | Agent has no usable tools in this channel (all denied by permission intersection) | Greyed out / muted indicator. Agent can explain: "I don't have the right access in this group. Try messaging me directly." |
| Needs setup | Agent's tools require credentials that haven't been configured | Shows setup prompt (see Part C section 3.3) |
An agent is never completely invisible in a channel it's been added to: it's either active, limited, unavailable (with explanation), or needs setup. The user always understands why.
6. Delegation chains¶
When a compound agent (e.g. PA Agent) delegates to a sub-agent (e.g. Finance Agent), the permission check applies at every level:
PA Agent → delegates to → Finance Agent → calls → xero_get_report
Permission checks:
1. Is Finance Agent in PA Agent's delegation set? ✓
2. Is Finance Agent granted to all channel participants? ✓ (resolved per section 4)
3. Is xero_get_report in Finance Agent's tool set? ✓
4. Is xero_get_report available at the channel's effective level? ✓ (read ≤ read)
→ Allowed
Principle from Part A: "Delegation does not escalate. Agent-to-agent delegation can only narrow permissions, never widen them."
The delegated agent operates under the intersection of:
- Its own permissions
- The delegating agent's permissions
- The channel's effective permissions (from participant intersection)
No delegation chain can result in a tool call that any channel participant wouldn't be permitted to trigger directly.
7. Whose credentials are used¶
Tool calls use the credentials of the user who triggered the request.
When Dr. Smith asks the PA Agent to "show me treatment notes for Lorraine Salt":
- The PA Agent delegates to the Cliniko tool
- Thinklio injects Dr. Smith's Cliniko API key
- Cliniko returns clinician-level data (treatment notes visible)
When Jane (admin) asks the same question in the same group chat:
- Thinklio injects Jane's Cliniko API key
- Cliniko returns admin-level data (treatment notes may not be visible)
Both are in the same channel, both can trigger the same tool, but the external system's response differs because different credentials are used. This is correct: the external system's own permission model is respected.
This means: In a group chat, if Dr. Smith asks for treatment notes and they appear in the chat, Jane can read them (the clinician chose to surface that information in a shared context). But Jane cannot independently pull the same data: her API key won't return it.
This matches real-world behaviour: a clinician can share patient information with their assistant verbally, but the assistant can't access the clinical system themselves.
For account-scoped credentials (e.g. Xero), the same token is used regardless of which user triggers the request. Access control is handled by Thinklio's permission layer (sections 4 and 4.1), not by the external system.
8. MCP server metadata endpoint¶
Each MCP server exposes a /meta endpoint that Thinklio uses for self-service integration setup.
8.1 Endpoint¶
8.2 Response¶
{
"name": "cliniko",
"version": "1.0.0",
"description": "Cliniko practice management system integration. Provides access to patients, appointments, treatment notes, and invoices.",
"documentationUrl": "https://docs.thinklio.ai/integrations/cliniko",
"credentialConfig": {
"scope": "user",
"headers": [
{
"name": "X-Cliniko-Api-Key",
"label": "Cliniko API Key",
"description": "Your personal Cliniko API key. Find it in Cliniko → My Info → Manage API Keys.",
"required": true
}
]
},
"tools": [
{
"name": "cliniko_search_patients",
"trustLevel": "read",
"description": "Search for patients by last name."
},
{
"name": "cliniko_get_patient",
"trustLevel": "read",
"description": "Get full details of a specific patient."
},
{
"name": "cliniko_get_treatment_notes",
"trustLevel": "read",
"description": "Get treatment notes for a patient."
}
]
}
8.3 Usage¶
Thinklio calls /meta when:
- An admin registers a new MCP server: populates the integration setup UI
- Periodically (alongside tool discovery): detects version changes, new tools
- When a user needs to configure credentials: shows setup instructions
The /meta response is cached and refreshed on the same cycle as MCP tool discovery (every 24 hours or on manual refresh).
8.4 Versioning¶
The version field follows semver. Thinklio can display version info in the admin UI and optionally alert admins when a new version is detected (tools added/removed/changed).
The MCP server's initialize response already includes serverInfo.version. The /meta endpoint provides additional context that MCP's protocol doesn't cover (credential requirements, trust levels, documentation).
9. Integration lifecycle¶
9.1 Admin adds an integration¶
1. Admin → Settings → Integrations → Add MCP Server
2. Enters MCP server URL (e.g. https://cliniko.api.thinklio.ai/mcp)
3. Thinklio calls /meta on the MCP server
4. UI shows: name, description, credential requirements, tool list
5. If account-scoped: admin provides credentials (or starts OAuth flow)
6. If user-scoped: admin enables the integration; users configure their own keys later
7. Thinklio calls MCP initialize + tools/list to discover and register tools
8. Tools appear in the Tool Library
9.2 Admin assigns tools to agents¶
1. Admin → Agent Studio → select agent
2. Add tools from Tool Library (individually or by integration group)
3. Optionally set trust level requirements per tool per agent
4. Save: agent_tools records created
9.3 Admin assigns agents and tools to teams¶
1. Admin → Teams → select team → Permissions
2. Grant access to agents and/or individual tools
3. Set permission level (read / standard / elevated)
4. Save: access_grants records created
9.4 User configures personal credentials¶
1. User tries to use an agent that needs user-scoped credentials
2. Agent responds: "Connect your Cliniko account in Settings → Integrations"
3. User → Settings → Integrations → Cliniko → enters API key
4. Key stored in user's secrets vault
5. Next tool call succeeds: credential resolved from vault
10. Summary¶
| Concern | Where it lives | MCP server's role |
|---|---|---|
| Credential storage | Thinklio secrets vault | None: receives credentials per-request |
| Token refresh (OAuth) | Thinklio credential manager | None: always receives valid token |
| Tool permission grants | Thinklio access_grants table | None: Thinklio decides which tools to expose |
| Channel permission resolution | Thinklio governance layer | None: if the call reaches the MCP server, it's permitted |
| External data-level access | Upstream API (e.g. Cliniko) | Passes through the injected credential |
| Tool discovery | MCP protocol (tools/list) | Returns available tools with schemas |
| Integration metadata | /meta endpoint |
Returns credential config, trust levels, docs |
| Trust level enforcement | Thinklio governance layer | MCP server declares trust levels in /meta |
References¶
- Convex access control patterns (RLS, row-level security): see
11-convex-reference.mdsection 8 (RLS / row-level security) and section 9 (triggers). - Clerk authentication and organisations: see
02-system-architecture.md(identity & auth) and12-developer-guide.md(Convex + Clerk setup). - Agent trust levels and execution tiers: see
03-agent-architecture.md(trust levels, execution tiers, rate limiters). - Tool catalogue and registration: see
09-external-api-tool-integration.md(MCP tool registration, server reference). - Data model tables referenced: see
04-data-model.mdforaccount,account_user,team,team_member,agent,agent_assignment,access_grants,secret,secret_share,secret_access_log,mcp_server,mcp_tool,credential_configdefinitions. - Content policies and system prompt injection: see
03-agent-architecture.md(contextHandler, system prompt assembly). - Governance component integration: see
03-agent-architecture.md(component integration) and11-convex-reference.mdPart B for building custom components.
Revision history¶
| Version | Date | Change |
|---|---|---|
| 1.0.0 | 2026-04-16 | Initial consolidated release. Supersedes pre-consolidation docs 08 (Security Model v03), 36 (Secrets Vault v01), 47 (Governance Policy Framework v02), 52 (MCP Credential & Permission Model v01). Editorially updated to Convex + Clerk architecture: Supabase Auth replaced with Clerk throughout; Supabase Vault storage note added to Part C (envelope encryption on Convex tables preserves API surface); cross-references retargeted to the 14-document canonical set. No content loss beyond renumbering and sentence-case normalisation. |