// ─── permissions.ts — Matches original rust/crates/runtime/src/permissions.rs ─ // PermissionPolicy, PermissionEvaluator, glob matching, prompt flow // ─── Types ─────────────────────────────────────────────────────────────────── export enum PermissionMode { ReadOnly = "read-only", WorkspaceWrite = "workspace-write", DangerFullAccess = "danger-full-access", Prompt = "prompt", Allow = "allow", } export interface PermissionRequest { toolName: string; input: string; currentMode: PermissionMode; requiredMode: PermissionMode; } export type PermissionOutcome = | { type: "allow" } | { type: "deny"; reason: string }; export type PermissionPromptDecision = | { type: "allow" } | { type: "deny"; reason: string }; export interface PermissionPrompter { decide(request: PermissionRequest): PermissionPromptDecision; } // ─── Mode ordering (for comparison) ────────────────────────────────────────── // Mode ordering matches original Rust enum derive(PartialOrd, Ord): // ReadOnly < WorkspaceWrite < DangerFullAccess < Prompt < Allow const MODE_ORDER: Record = { [PermissionMode.ReadOnly]: 0, [PermissionMode.WorkspaceWrite]: 1, [PermissionMode.DangerFullAccess]: 2, [PermissionMode.Prompt]: 3, [PermissionMode.Allow]: 4, }; function modeGte(a: PermissionMode, b: PermissionMode): boolean { return MODE_ORDER[a] >= MODE_ORDER[b]; } // ─── Default tool requirements ─────────────────────────────────────────────── const DEFAULT_TOOL_REQUIREMENTS: Record = { // Read-only tools read_file: PermissionMode.ReadOnly, glob_search: PermissionMode.ReadOnly, grep_search: PermissionMode.ReadOnly, WebSearch: PermissionMode.ReadOnly, WebFetch: PermissionMode.ReadOnly, ToolSearch: PermissionMode.ReadOnly, TodoRead: PermissionMode.ReadOnly, lsp: PermissionMode.ReadOnly, // Workspace write tools write_file: PermissionMode.WorkspaceWrite, edit_file: PermissionMode.WorkspaceWrite, NotebookEdit: PermissionMode.WorkspaceWrite, TodoWrite: PermissionMode.WorkspaceWrite, Config: PermissionMode.WorkspaceWrite, // Danger/full access tools bash: PermissionMode.DangerFullAccess, REPL: PermissionMode.DangerFullAccess, powershell: PermissionMode.DangerFullAccess, Agent: PermissionMode.DangerFullAccess, mcp_tool: PermissionMode.DangerFullAccess, remote_trigger: PermissionMode.DangerFullAccess, }; // ─── PermissionPolicy ──────────────────────────────────────────────────────── export class PermissionPolicy { private activeMode: PermissionMode; private toolRequirements: Map; constructor(mode: PermissionMode) { this.activeMode = mode; this.toolRequirements = new Map(Object.entries(DEFAULT_TOOL_REQUIREMENTS)); } withToolRequirement( toolName: string, mode: PermissionMode ): PermissionPolicy { this.toolRequirements.set(toolName, mode); return this; } getActiveMode(): PermissionMode { return this.activeMode; } setActiveMode(mode: PermissionMode): void { this.activeMode = mode; } requiredModeFor(toolName: string): PermissionMode { return ( this.toolRequirements.get(toolName) || PermissionMode.DangerFullAccess ); } authorize( toolName: string, input: string, prompter?: PermissionPrompter ): PermissionOutcome { const currentMode = this.activeMode; const requiredMode = this.requiredModeFor(toolName); // Allow mode permits everything if (currentMode === PermissionMode.Allow || modeGte(currentMode, requiredMode)) { return { type: "allow" }; } const request: PermissionRequest = { toolName, input, currentMode, requiredMode, }; // Prompt mode or workspace-write escalating to danger-full-access if ( currentMode === PermissionMode.Prompt || (currentMode === PermissionMode.WorkspaceWrite && requiredMode === PermissionMode.DangerFullAccess) ) { if (prompter) { const decision = prompter.decide(request); if (decision.type === "allow") { return { type: "allow" }; } return { type: "deny", reason: decision.reason }; } return { type: "deny", reason: `tool '${toolName}' requires approval to escalate from ${currentMode} to ${requiredMode}`, }; } return { type: "deny", reason: `tool '${toolName}' requires ${requiredMode} permission; current mode is ${currentMode}`, }; } } import micromatch from "micromatch"; // ─── Glob matching for tool names ──────────────────────────────────────────── export function globMatchToolName( pattern: string, toolName: string ): boolean { return micromatch.isMatch(toolName, pattern); } // ─── Glob matching for file paths ──────────────────────────────────────────── export function globMatchPath(pattern: string, filePath: string): boolean { // Normalize: remove trailing slashes const normalizedPath = filePath.replace(/\/+$/, ""); const normalizedPattern = pattern.replace(/\/+$/, ""); return micromatch.isMatch(normalizedPath, normalizedPattern, { dot: true }); } // ─── Permission prompt text ────────────────────────────────────────────────── export function permissionPromptText(request: PermissionRequest): string { return `Tool "${request.toolName}" requires ${request.requiredMode} permission.\nCurrent mode: ${request.currentMode}\nInput: ${request.input.substring(0, 200)}${request.input.length > 200 ? "..." : ""}`; } // ─── Permission rule (for config-based rules) ──────────────────────────────── export interface PermissionRule { toolPattern: string; pathPattern?: string; action: "allow" | "deny"; scope: "session" | "persistent"; } export class PermissionEvaluator { private rules: PermissionRule[] = []; addRule(rule: PermissionRule): void { this.rules.push(rule); } removeRule(index: number): void { if (index >= 0 && index < this.rules.length) { this.rules.splice(index, 1); } } getRules(): PermissionRule[] { return [...this.rules]; } clearSessionRules(): void { this.rules = this.rules.filter((r) => r.scope === "persistent"); } evaluate( toolName: string, filePath?: string ): "allow" | "deny" | "no-match" { // Rules are evaluated in order, last match wins let result: "allow" | "deny" | "no-match" = "no-match"; for (const rule of this.rules) { if (!globMatchToolName(rule.toolPattern, toolName)) { continue; } if (rule.pathPattern && filePath) { if (!globMatchPath(rule.pathPattern, filePath)) { continue; } } result = rule.action; } return result; } toJSON(): PermissionRule[] { return this.rules; } static fromJSON(rules: PermissionRule[]): PermissionEvaluator { const evaluator = new PermissionEvaluator(); evaluator.rules = rules; return evaluator; } }