Spaces:
Sleeping
Sleeping
| // βββ 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, number> = { | |
| [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<string, PermissionMode> = { | |
| // 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<string, PermissionMode>; | |
| 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; | |
| } | |
| } | |