| |
| |
| |
| |
| |
| |
|
|
| export type PermissionMode = 'auto' | 'ask' | 'custom'; |
| export type GateDecision = 'ask' | 'allow'; |
|
|
| |
| export const ASK_DEFAULT_KEYS: ReadonlySet<string> = new Set([ |
| 'curl:external', |
| 'search', |
| 'generate-image', |
| 'rm', |
| ]); |
|
|
| |
| export interface GateCommand { |
| command: string; |
| label: string; |
| keys: string[]; |
| } |
|
|
| |
| |
| |
| export const GATE_COMMANDS: GateCommand[] = [ |
| { command: 'cat', label: 'cat (read file)', keys: ['cat'] }, |
| { command: 'head', label: 'head', keys: ['head'] }, |
| { command: 'tail', label: 'tail', keys: ['tail'] }, |
| { command: 'ls', label: 'ls', keys: ['ls'] }, |
| { command: 'tree', label: 'tree', keys: ['tree'] }, |
| { command: 'grep', label: 'grep', keys: ['grep'] }, |
| { command: 'rg', label: 'rg (ripgrep)', keys: ['rg'] }, |
| { command: 'find', label: 'find', keys: ['find'] }, |
| { command: 'wc', label: 'wc', keys: ['wc'] }, |
| { command: 'sort', label: 'sort', keys: ['sort'] }, |
| { command: 'uniq', label: 'uniq', keys: ['uniq'] }, |
| { command: 'tr', label: 'tr', keys: ['tr'] }, |
| { command: 'echo', label: 'echo', keys: ['echo'] }, |
| { command: 'mkdir', label: 'mkdir', keys: ['mkdir'] }, |
| { command: 'touch', label: 'touch', keys: ['touch'] }, |
| { command: 'mv', label: 'mv (move)', keys: ['mv'] }, |
| { command: 'cp', label: 'cp (copy)', keys: ['cp'] }, |
| { command: 'rm', label: 'rm / rmdir (delete)', keys: ['rm'] }, |
| { command: 'ss', label: 'ss (search/replace edit)', keys: ['ss'] }, |
| { command: 'sed', label: 'sed', keys: ['sed:read', 'sed:write'] }, |
| { command: 'curl', label: 'curl', keys: ['curl:local', 'curl:external'] }, |
| { command: 'sqlite3', label: 'sqlite3', keys: ['sqlite3:read', 'sqlite3:write'] }, |
| { command: 'python', label: 'python / python3', keys: ['python'] }, |
| { command: 'lua', label: 'lua', keys: ['lua'] }, |
| { command: 'preview', label: 'preview', keys: ['preview'] }, |
| { command: 'build', label: 'build', keys: ['build'] }, |
| { command: 'runtime', label: 'runtime', keys: ['runtime'] }, |
| { command: 'sleep', label: 'sleep', keys: ['sleep'] }, |
| { command: 'ask', label: 'ask (prompt the user)', keys: ['ask'] }, |
| { command: 'generate-image', label: 'generate-image', keys: ['generate-image'] }, |
| { command: 'search', label: 'search (web)', keys: ['search'] }, |
| ]; |
|
|
| |
| export const ALWAYS_ALLOWED_NOTES: { command: string; reason: string }[] = [ |
| { command: 'status', reason: 'the signal the agent uses to finish a run; gating it could stall generation.' }, |
| { command: 'agent', reason: 'spawning sub-agents is always allowed; the commands a sub-agent runs are gated individually, so control happens there.' }, |
| ]; |
|
|
| const WRITE_SQL = /\b(insert|update|delete|create|drop|alter|replace|truncate)\b/i; |
|
|
| export function extractCurlUrls(args: string[]): string[] { |
| |
| const takesValue = new Set(['-o', '--output', '-X', '--request', '-H', '--header', '-d', '--data', '--data-raw']); |
| const urls: string[] = []; |
| for (let i = 1; i < args.length; i++) { |
| const a = args[i]; |
| if (takesValue.has(a)) { i++; continue; } |
| if (a.startsWith('-')) continue; |
| urls.push(a); |
| } |
| return urls; |
| } |
|
|
| |
| export function isExternalUrl(raw: string): boolean { |
| if (!raw) return false; |
| const url = (raw.includes('://') ? raw : 'http://' + raw).toLowerCase(); |
| return !( |
| url.startsWith('http://localhost') || url.startsWith('https://localhost') || |
| url.startsWith('http://127.0.0.1') || url.startsWith('https://127.0.0.1') |
| ); |
| } |
|
|
| export function isExternalCurl(args: string[]): boolean { |
| return extractCurlUrls(args).some(isExternalUrl); |
| } |
|
|
| |
| export function classifyGateKey(args: string[]): string | null { |
| const name = args[0]; |
| if (!name) return null; |
| switch (name) { |
| case 'rmdir': return 'rm'; |
| case 'python3': return 'python'; |
| case 'curl': return isExternalCurl(args) ? 'curl:external' : 'curl:local'; |
| case 'sed': return args.includes('-i') ? 'sed:write' : 'sed:read'; |
| case 'sqlite3': { |
| const sql = args.slice(1).join(' '); |
| return WRITE_SQL.test(sql) ? 'sqlite3:write' : 'sqlite3:read'; |
| } |
| default: { |
| const known = GATE_COMMANDS.some(c => c.command === name); |
| return known ? name : null; |
| } |
| } |
| } |
|
|
| |
| export function needsApproval( |
| key: string, |
| mode: PermissionMode, |
| overrides: Record<string, GateDecision>, |
| ): boolean { |
| if (mode === 'auto') return false; |
| if (mode === 'ask') { |
| if (!ASK_DEFAULT_KEYS.has(key)) return false; |
| return overrides[key] !== 'allow'; |
| } |
| |
| const fallback: GateDecision = ASK_DEFAULT_KEYS.has(key) ? 'ask' : 'allow'; |
| return (overrides[key] ?? fallback) === 'ask'; |
| } |
|
|
| export interface ApprovalRequest { |
| command: string; |
| gateKey: string; |
| capabilityLabel: string; |
| } |
| export type ApprovalOutcome = 'once' | 'always' | 'deny'; |
|
|
| export function capabilityLabel(gateKey: string): string { |
| if (gateKey === 'search' || gateKey.startsWith('curl:')) return 'Web access'; |
| if (gateKey === 'generate-image') return 'Image generation'; |
| if (gateKey === 'rm') return 'File deletion'; |
| return gateKey; |
| } |
|
|