Capability System
Plugin capability declarations and tool permission middleware for controlling what extensions can do.
The capability and permission modules control what plugins and agents are allowed to do. The capability system declares what a plugin contributes (tools, channels, skills), while the permission system enforces access control at runtime.
Design Philosophy
- Declarative capabilities — Plugins declare what they provide via
CapabilityDeclaration - Layered permissions — Global defaults + per-agent overrides, most-restrictive-wins
- Middleware enforcement — Permission checks happen in the tool middleware layer before execution
Capability Declarations
Plugins declare their contributions via a unified enum:
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CapabilityDeclaration {
Tool(ToolDeclaration),
Hook(HookDeclaration),
Channel(ChannelDeclaration),
Provider(ProviderDeclaration),
GatewayMethod(GatewayMethodDeclaration),
HttpRoute(HttpRouteDeclaration),
Cli(CliDeclaration),
Service(ServiceDeclaration),
Command(CommandDeclaration),
Skill(SkillDeclaration),
Agent(AgentDeclaration),
McpServer(McpServerDeclaration),
}Each variant maps 1:1 to a registration type in the PluginRegistry. The #[serde(tag = "type")] attribute serializes as { "type": "tool", "name": "my_tool", ... }.
Permission System
Classification
Tool invocations are classified into three outcomes:
pub enum Classification {
Allow, // Execute without prompting
Confirm { reason: String }, // Ask user for approval
Deny { reason: String }, // Reject unconditionally
}SmartFilter
pub trait SmartFilter: Send + Sync {
fn classify(&self,
name: &str,
input: &Value,
) -> Classification;
}LayeredPermissionResolver
Merges global + per-agent permissions using most-restrictive-wins semantics:
pub struct LayeredPermissionResolver {
merged: Arc<ArcSwap<ToolPermissionsConfig>>,
}Merge rules: Deny > Ask > Allow
Exec-class tools (code_exec, bash) have special handling — their approval is owned by WorkspaceSandbox, not the general permission layer.
Live reload: Uses ArcSwap so permission updates from config RPC handlers take effect without restarting.
ApprovalGate
For Confirm classifications, the ApprovalGate prompts the user:
#[async_trait]
pub trait Approver: Send + Sync {
async fn ask(
&self,
tool_name: &str,
reason: &str,
) -> ApprovalOutcome;
}The real ApprovalGate implements this; a mock impl enables unit testing without full approval UI plumbing.
Permission Config
Permissions are configured in providers.toml or via the config RPC:
[permissions.tools]
code_exec = "ask"
bash = "deny"
file_ops = "allow"Actions:
allow— Execute without confirmationask— Prompt user before executingdeny— Block execution
Safety Properties
- Deny-first evaluation — All patterns checked for
Denybefore anyAskprompts - Deterministic ordering — Config rules are sorted by key for reproducible evaluation
- No recursion overflow — Pattern matching uses bounded recursion (patterns come from config, not user input)
Code Location
src/extension/capability.rs—CapabilityDeclarationenumsrc/tools/middleware/permission/mod.rs— Permission layersrc/tools/middleware/permission/resolver.rs—LayeredPermissionResolversrc/tools/middleware/permission/agent_filter.rs— Per-agent filteringsrc/config/types/policies/tool_permissions.rs— Config types
See Also
- Extensions — Plugin architecture
- Approval — Desktop/browser approval system