The PermissionRegistry is the security gatekeeper of the tool system. It manages which tools (or specific actions within tools) require explicit user approval before they can be executed.

Core Concepts

The registry tracks Grants—permissions that have been approved by the user for a specific session.

pub struct PermissionRegistry {
    grants: RwLock<HashMap<String, Vec<PermissionGrant>>>,
    interaction_registry: Arc<UserInteractionRegistry>,
}

pub struct PermissionGrant {
    pub tool_name: String,
    pub action: String, // e.g., "read", "write", "execute"
    pub resource: String, // e.g., a file path or URL
}

The Permission Flow

  1. Check: A tool checks if it has the required permission via context.permission_registry.check(...).
  2. Grant Exists: If the user has already granted this permission in the current session, the tool proceeds immediately.
  3. Request: If no grant exists, the tool calls request_permission(...).
  4. UI Prompt: The registry uses the UserInteractionRegistry to show a permission prompt to the user (e.g., “Allow tool ‘FileWrite’ to write to ‘config.json’?”).
  5. User Decision:
    • Allow Once: The tool proceeds, but no permanent grant is stored.
    • Allow for Session: A grant is added to the PermissionRegistry, and the tool proceeds. Subsequent calls will not prompt the user.
    • Deny: The tool receives a PermissionDenied error and must handle it gracefully.

Grant Scoping

Grants are typically scoped by:

  • Tool Name: prevents one tool from using permissions granted to another.
  • Resource: allows for fine-grained control (e.g., “Allow read access to /tmp but not /etc”).
  • Session ID: Ensures that permissions don’t leak between different chat sessions.

Thread Safety and Persistence

The PermissionRegistry uses thread-safe primitives (RwLock) to handle concurrent checks from multiple tools. By default, grants are held in memory and expire when the agent process terminates.

Best Practices for Tools

Tools should always check for permissions as early as possible in their execute method to avoid performing partial work that might be denied later.

async fn execute(&self, context: ToolContext, input: Value) -> ToolResult {
    let path = input["path"].as_str().ok_or("Missing path")?;
    
    // Request permission before doing anything
    context.permission_registry
        .request_write_permission(self.name(), path)
        .await?;
        
    // Permission granted, proceed with file write...
}