Overview
The permissions system controls which tools an agent can execute. You define rules using glob patterns, and the agent harness checks these rules before executing tools. When a tool call doesn’t match any allowlist rule, the harness yields a relay event and pauses until you respond with approval or denial.
This enables human-in-the-loop workflows where sensitive operations require explicit permission.
Permission structure
Permissions are defined in packages/ai/types.ts:49:
interface ToolPermission {
tool : string ; // Tool name (exact match)
params ?: Record < string , string >; // param name → glob pattern
}
interface Permissions {
allowlist ?: ToolPermission []; // Always allowed
allowOnce ?: ToolPermission []; // Allowed once, then removed
deny ?: Array <{ // Explicitly denied tool calls
toolCallId : string ;
reason ?: string ;
}>;
}
Rules that grant permanent permission. Checked on every tool call.
Rules that grant one-time permission. After the first match, the rule is removed from the list.
deny
Array<{ toolCallId, reason }>
Explicit denials for specific tool call IDs. Useful for denying a tool call that was previously approved in principle but shouldn’t execute now.
Basic usage
const permissions : Permissions = {
allowlist: [
{ tool: "bash" } // All bash calls allowed
]
};
for await ( const event of agent . invoke ({
model: "glm-4.7" ,
messages ,
tools: [ bashTool ],
permissions
})) {
// Agent can call bash freely, no relay events
}
Allow specific parameters
Use glob patterns to restrict parameters:
const permissions : Permissions = {
allowlist: [
{
tool: "bash" ,
params: {
command: "ls*" // Only allow bash commands starting with "ls"
}
}
]
};
Glob patterns use picomatch with bash-style matching:
Pattern Matches ls*ls, ls -la, ls /tmp*.txtfile.txt, data.txtsrc/**/*.tsAny .ts file under src/ {read,write}Either read or write file[0-9].txtfile0.txt, file1.txt, etc.
Multiple parameter patterns
All patterns must match for a tool call to be allowed:
const permissions : Permissions = {
allowlist: [
{
tool: "read" ,
params: {
filePath: "src/**/*.ts" , // Must be a .ts file in src/
limit: "100" // Must request exactly 100 lines
}
}
]
};
Permission checking flow
When the agent receives tool calls from the model, it processes them in two phases:
Phase 1: Permission checks (sequential)
For each tool call:
Check deny list : If toolCallId is in permissions.deny, yield tool_result with { status: "denied", reason } and skip execution
Check allowlist/allowOnce : If the tool call matches any rule in allowlist or allowOnce, approve it
No match : Yield a relay event and pause via a deferred promise
Wait for response : When consumer calls respond(), resume
Denied : Yield tool_result with denial reason
Approved : Continue to execution phase
From harness/agent.ts:137:
// Check deny list first
const denial = params . permissions ?. deny ?. find (( d ) => d . toolCallId === tc . id );
if ( denial ) {
yield { type: "tool_result" , id: tc . id , name: tc . name , output: { status: "denied" , reason: denial . reason } };
continue ;
}
// Check if allowed
const isAllowed = params . permissions && matchesPermissions ({ name: tc . name , arguments: args }, params . permissions );
if ( ! isAllowed ) {
// Yield relay event and pause
const { promise , resolve } = deferred < PermissionResponse >();
yield {
type: "relay" ,
kind: "permission" ,
toolCallId: tc . id ,
tool: tc . name ,
params: args ,
respond : ( response ) => resolve ( response )
};
const decision = await promise ; // Generator pauses here
if ( ! decision . approved ) {
yield { type: "tool_result" , id: tc . id , output: { status: "denied" , reason: decision . reason } };
continue ;
}
}
All approved tool calls execute concurrently via Promise.all() (from harness/agent.ts:264):
const results = await Promise . all (
approved . map ( async ({ tc , toolDef }) => {
const { context , result } = await toolDef . execute ! ( tc . arguments , toolCtx );
return {
event: { type: "tool_result" , id: tc . id , name: tc . name , output: { context , result } },
message: { role: "tool" , tool_call_id: tc . id , content: context ?? JSON . stringify ({ context , result }) }
};
})
);
Handling relay events
When the agent yields a relay event, you must call respond() to approve or deny:
for await ( const event of agent . invoke ({ messages , tools , permissions })) {
if ( event . type === "relay" && event . kind === "permission" ) {
console . log ( `Agent wants to call ${ event . tool } with` , event . params );
const approved = await askUser ( `Allow ${ event . tool } ?` );
event . respond ({
approved ,
reason: approved ? undefined : "User denied"
});
}
}
If you don’t call respond(), the agent will hang forever. Always handle relay events.
Permission response
type PermissionResponse =
| { approved : true ; always ?: boolean } // Approve this call (optionally add to allowlist)
| { approved : false ; reason ?: string }; // Deny with optional reason
Whether to allow the tool call.
If true, add this tool + params to the allowlist so future calls are auto-approved. (Note: not yet implemented in agent harness, but supported by orchestrator.)
Human-readable explanation for denial.
Using the orchestrator
The orchestrator manages relay events across multiple agents. When you spawn agents via the orchestrator, relay events have their respond callback stripped and you use resolveRelay() instead:
import { AgentOrchestrator } from "./packages/ai/orchestrator" ;
const orchestrator = new AgentOrchestrator ();
const agentId = orchestrator . spawn ({
model: "glm-4.7" ,
messages ,
tools: [ bashTool ],
permissions: { allowlist: [] } // No auto-approvals
});
for await ( const { agentId , event } of orchestrator . events ()) {
if ( event . type === "relay" && event . kind === "permission" ) {
const approved = await askUser ( `Allow ${ event . tool } ?` );
orchestrator . resolveRelay ( event . id , {
approved ,
reason: approved ? undefined : "User denied"
});
}
}
Pattern matching reference
The matchesPermission function in packages/ai/permissions.ts:9 implements the matching logic:
export function matchesPermission ( toolCall : ToolCallLike , permission : ToolPermission ) : boolean {
if ( toolCall . name !== permission . tool ) {
return false ; // Tool name must match exactly
}
if ( ! permission . params ) {
return true ; // No param constraints, allow all calls to this tool
}
for ( const [ paramName , pattern ] of Object . entries ( permission . params )) {
const value = toolCall . arguments ?.[ paramName ];
if ( value === undefined ) {
return false ; // Param is required but missing
}
if ( ! picomatch . isMatch ( String ( value ), pattern , { bash: true })) {
return false ; // Param doesn't match pattern
}
}
return true ; // All patterns matched
}
Key behaviors:
Tool name is exact match (not a pattern)
All param patterns must match (AND logic)
Missing params cause rejection
Param values are coerced to strings before matching
Common patterns
Read-only filesystem access
{
allowlist : [
{ tool: "read" }, // Allow all reads
{ tool: "bash" , params: { command: "ls*" } }, // Allow ls only
{ tool: "bash" , params: { command: "cat *" } } // Allow cat only
]
}
Restrict to specific directories
{
allowlist : [
{ tool: "read" , params: { filePath: "/app/data/**" } },
{ tool: "bash" , params: { command: "ls /app/data*" } }
]
}
{
allowlist : [
{ tool: "bash" , params: { command: "@(ls|cat|grep|find|head|tail) *" } }
]
}
{
allowOnce : [
{ tool: "bash" , params: { command: "rm -rf /tmp/cache" } }
]
}
// After first match, this rule is removed
Tools can include a derivePermission function to suggest permission rules:
const readTool : ToolDefinition = {
name: "read" ,
description: "Read a file" ,
schema: z . object ({ filePath: z . string () }),
execute : async ({ filePath }) => {
const content = await fs . readFile ( filePath , "utf-8" );
return { context: content };
},
derivePermission : ( params ) => ({
tool: "read" ,
params: { filePath: params . filePath as string }
})
};
This enables auto-suggesting allowlist rules based on tool call parameters.
Security considerations
Glob patterns are not sandboxing. They control which tool calls are allowed but don’t prevent path traversal, command injection, or other exploits. Always validate tool inputs.
Pattern src/**/*.ts matches src/../../../../etc/passwd.ts. Use absolute paths and validate file existence.
Pattern ls* matches ls; rm -rf /. Validate commands against a strict allowlist or use parameterized tools.
Models can craft arguments to exploit glob patterns. Review permissions carefully and prefer explicit allowlists.
Dynamic permissions
You can update permissions dynamically by mutating the orchestrator’s permission map:
const orchestrator = new AgentOrchestrator ();
for await ( const { agentId , event } of orchestrator . events ()) {
if ( event . type === "relay" && event . kind === "permission" ) {
const approved = await askUser ( `Allow ${ event . tool } ? (always/once/no)` );
if ( approved === "always" ) {
// Add to allowlist
orchestrator . updatePermissions ( agentId , {
allowlist: [
... orchestrator . getPermissions ( agentId ). allowlist ,
{ tool: event . tool , params: event . params }
]
});
}
orchestrator . resolveRelay ( event . id , { approved: approved !== "no" });
}
}
See orchestrator.ts:197 for updatePermissions() implementation.
Testing permissions
Use a deterministic harness to test permission logic without calling real LLMs:
import { createDeterministicHarness } from "./packages/ai/harness/providers/deterministic" ;
import { createAgentHarness } from "./packages/ai/harness/agent" ;
const mockProvider = createDeterministicHarness ([
{ type: "tool_call" , id: "tc-1" , name: "bash" , input: { command: "rm -rf /" } }
]);
const agent = createAgentHarness ({ harness: mockProvider });
const events = [];
for await ( const event of agent . invoke ({
messages ,
tools: [ bashTool ],
permissions: { allowlist: [{ tool: "bash" , params: { command: "ls*" } }] }
})) {
events . push ( event );
}
// Assert relay event was yielded
assert ( events . some ( e => e . type === "relay" ));
Next steps
Human-in-the-Loop Build interactive approval workflows
Orchestrator Multi-agent permission coordination
Custom Tools Add derivePermission to your tools
Agent API Full agent harness permission handling