| name | agent-permissions |
| description | Comprehensive guide for the agent permissions and approval system. Use when an agent needs to request user approval for sensitive operations, when implementing new approval policies, when integrating platform-specific approval UIs (Slack buttons, WhatsApp replies, Dashboard modals), or when troubleshooting approval flows. Covers policy definition, platform adapters, and the complete approval lifecycle. |
| metadata | {"author":"orient","version":"2.0.0"} |
Agent Permissions System
This skill covers the approval/permissions framework that enables agents to request user consent for sensitive operations across all platforms.
Quick Start: Requesting User Approval
When an agent needs approval for a sensitive action, the system automatically:
- Evaluates the tool call against defined policies
- If policy requires approval (
action: 'ask'), sends a prompt via the appropriate platform adapter
- Waits for user response (button click, reply, reaction, or dashboard action)
- Returns approval decision to continue or deny execution
Platform-Specific Approval UX
| Platform | Approval Method | Response Format |
|---|
| Slack | Interactive buttons in message | "Approve" / "Deny" button clicks |
| WhatsApp | Reply to message or emoji reaction | Reply "YES"/"NO" or 👍/👎 reaction |
| Dashboard | Modal with approve/deny buttons | Button click in UI |
Core Architecture
packages/agents/src/permissions/
├── types.ts # Core interfaces (PermissionPolicy, ApprovalRequest, etc.)
├── policyEngine.ts # Policy evaluation and approval coordination
├── approvalStore.ts # In-memory approval store interface
├── drizzlePermissionStore.ts # Database-backed store (persistent)
├── defaultPolicies.ts # Built-in policy definitions
└── adapters/
├── base.ts # PlatformApprovalAdapter interface
├── registry.ts # Adapter registration
├── dashboard.ts # Dashboard approval adapter
├── slack.ts # Slack Block Kit buttons
└── whatsapp.ts # WhatsApp reply/reaction handling
Policy Definition
Policies control which tools require approval:
interface PermissionPolicy {
id: string;
name: string;
toolPatterns: string[];
action: 'allow' | 'deny' | 'ask';
granularity: 'per_call' | 'per_session' | 'per_category';
riskLevel: 'low' | 'medium' | 'high' | 'critical';
promptTemplate?: string;
agentIds?: string[];
platforms?: Platform[];
priority?: number;
timeout?: number;
}
Default Policies
Located in packages/agents/src/permissions/defaultPolicies.ts:
export const DEFAULT_POLICIES: PermissionPolicy[] = [
{
id: 'file-write',
toolPatterns: ['file_write', 'file_delete', 'file_move', 'file_edit'],
action: 'ask',
granularity: 'per_call',
riskLevel: 'medium',
},
{
id: 'shell-execution',
toolPatterns: ['bash', 'shell', 'exec', 'run_command'],
action: 'ask',
granularity: 'per_call',
riskLevel: 'high',
},
{
id: 'external-api',
toolPatterns: ['http_*', 'api_*', 'webhook_*'],
action: 'ask',
granularity: 'per_session',
riskLevel: 'medium',
},
{
id: 'sensitive-data',
toolPatterns: ['secret_*', 'credential_*', 'token_*'],
action: 'ask',
granularity: 'per_call',
: ,
},
];
Integration with Tool Execution
The toolCallingService.ts integrates with the PolicyEngine:
for (const toolUse of toolUseBlocks) {
const decision = await policyEngine.evaluateToolCall(toolUse, context, agentId);
if (decision.action === 'deny') {
return toolDeniedResult(toolUse, decision.reason);
}
if (decision.action === 'ask') {
const approval = await policyEngine.requestApproval({
tool: toolUse,
policy: decision.policy,
context,
agentId,
});
if (approval.status !== 'approved') {
return toolDeniedResult(toolUse, 'User denied approval');
}
}
const result = await executor(toolUse.name, toolUse.input, context);
}
Platform Adapters
Implementing a New Adapter
Each adapter implements PlatformApprovalAdapter:
interface PlatformApprovalAdapter {
platform: Platform;
supportsNativeApproval: boolean;
supportedInteractionTypes: readonly InteractionType[];
requestApproval(
request: ApprovalRequest,
context: PlatformContext
): Promise<ApprovalPromptResult>;
handleApprovalResponse(response: unknown): Promise<ApprovalResult | null>;
cancelRequest(requestId: string): Promise<void>;
formatApprovalPrompt(request: ApprovalRequest): unknown;
formatApprovalResult(result: ApprovalResult): unknown;
}
WhatsApp Adapter Example
The WhatsApp adapter sends a message and listens for replies:
formatApprovalPrompt(request: ApprovalRequest): string {
return [
`Approval required`,
`Agent "${request.agentId}" wants to run "${request.tool.name}".`,
`Reply with YES/NO or APPROVE/DENY.`,
`Request ID: ${request.id}`,
`You can also approve in the dashboard: ${dashboardUrl}/approvals/${request.id}`,
].join('\n');
}
handleApprovalResponse(response: WhatsAppMessage): ApprovalResult | null {
const text = response.body?.trim().toLowerCase();
if (text === 'approve' || text === 'yes') {
return { requestId: extractRequestId(text), status: 'approved' };
}
if (text === 'deny' || text === 'no') {
return { requestId: extractRequestId(text), status: 'denied' };
}
return null;
}
Slack Adapter
Uses Block Kit interactive buttons:
formatApprovalPrompt(request: ApprovalRequest) {
return {
blocks: [
{ type: 'section', text: { type: 'mrkdwn', text: `*Approval Required*` } },
{ type: 'section', text: { type: 'mrkdwn', text: `Tool: \`${request.tool.name}\`` } },
{
type: 'actions',
elements: [
{ type: 'button', text: { type: 'plain_text', text: 'Approve' }, action_id: 'approve', value: request.id },
{ type: 'button', text: { type: 'plain_text', text: 'Deny' }, action_id: 'deny', value: request.id },
],
},
],
};
}
Database Schema
Tables in packages/database/src/schema/index.ts:
export const permissionPolicies = pgTable('permission_policies', {
id: text('id').primaryKey(),
name: text('name').notNull(),
toolPatterns: text('tool_patterns').notNull(),
action: text('action').notNull(),
granularity: text('granularity').notNull(),
riskLevel: text('risk_level').notNull(),
priority: integer('priority').default(0),
enabled: boolean('enabled').default(true),
...
});
export const approvalRequests = pgTable('approval_requests', {
id: text('id').primaryKey(),
sessionId: text().(),
: ().(),
: ().(),
: ().(),
: ().(),
: ().(),
: ().(),
...
});
approvalGrants = (, {
: ().(),
: ().(),
: ().(),
: ().(),
: ().(),
: (),
...
});
Adding a New Platform
To add a new platform (e.g., Telegram):
- Create adapter:
packages/agents/src/permissions/adapters/telegram.ts
- Implement
PlatformApprovalAdapter with platform-specific UX
- Register in startup:
import { TelegramApprovalAdapter } from './adapters/telegram.js';
adapterRegistry.register(new TelegramApprovalAdapter(telegramClient));
- Handle responses - Wire up message/callback handlers to call:
policyEngine.handlePlatformResponse('telegram', telegramResponse);
Approval Granularity
per_call: Prompt every time the tool is invoked
per_session: Prompt once per session, then remember grant
per_category: Prompt once per tool category, remember for session
When per_session or per_category is used, grants are stored in approval_grants table.
Testing
pnpm --filter @orientbot/agents test permissions
pnpm --filter @orientbot/agents test adapters
Troubleshooting
Approvals not working
- Check if policy matches tool pattern (glob patterns)
- Verify adapter is registered for the platform
- Check approval request is created in DB
- Verify platform message/button handler is wired up
Policy not applying
- Higher priority policies take precedence
enabled: false policies are skipped
- Check
platforms and agentIds filters
Approval expires immediately
- Check
timeout setting on policy
- Verify
defaultTimeoutMs in PolicyEngine config