fix(x): fail-closed tool permissions with per-tool catalog declarations

The permission checker returned {required: false} for every non-builtin
toolId and for any builtin outside the executeCommand/file-tools switch,
so composio-execute-tool (email sends, GitHub/Jira writes), executeMcpTool,
and mcp:* attachments on user agents executed with no permission check —
even in manual mode — and never reached the auto-permission classifier.
The extension contract was inverted: a new side-effecting tool shipped
ungated unless someone remembered to extend a switch in another module.

Every builtin now declares its permission policy in the catalog itself
(required field, compile-enforced): "none", "prompt", "command-allowlist",
"file-boundary", "composio-execute", or "mcp-execute". The checker reads
the declaration and FAILS CLOSED: undeclared builtins, mcp:* attachments,
and unknown toolId families require permission. Composio and MCP requests
carry family-specific payloads (new ToolPermissionMetadata kinds, rendered
by the permission card; generic fallback for everything else). All
previously-ungated builtins keep today's behavior via explicit "none"
declarations, except addMcpServer which now prompts; a catalog test pins
the audited gated set so policy changes stay intentional.

Auto-permission flows (background tasks, live notes, channels) route the
newly gated calls through the existing classifier per the §9.3 matrix;
defer still denies without a human. No grant persistence yet — every
gated call prompts (or classifies) each time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-10 23:07:11 +05:30
parent e22d3b4796
commit ac7679ec16
23 changed files with 388 additions and 25 deletions

View file

@ -100,6 +100,24 @@ export const ToolPermissionMetadata = z.discriminatedUnion("kind", [
paths: z.array(z.string()),
pathPrefix: z.string(),
}),
// A Composio action execution (composio-execute-tool).
z.object({
kind: z.literal("composio"),
toolkitSlug: z.string(),
toolSlug: z.string(),
}),
// An MCP tool execution (executeMcpTool or an mcp:* attachment).
z.object({
kind: z.literal("mcp"),
serverName: z.string().optional(),
toolName: z.string(),
}),
// Generic fail-closed request: a tool with no more specific policy.
z.object({
kind: z.literal("tool"),
toolId: z.string(),
toolName: z.string(),
}),
]);
export const ToolPermissionRequestEvent = BaseRunEvent.extend({