* Returns a non-asking decision based purely on policy. * Returns 'ask' when policy is undecided.
(req: PermissionRequest)
| 34 | this.denyPatterns = config.security.autoReject.map(p => new RegExp(p)); |
| 35 | this.alwaysAskPatterns = (config.security.alwaysAsk ?? []).map(p => new RegExp(p)); |
| 36 | this.denyRules = [...(config.security.denyRules ?? [])]; |
| 37 | } |
| 38 | |
| 39 | /** |
| 40 | * Returns a non-asking decision based purely on policy. |
| 41 | * Returns 'ask' when policy is undecided. |
| 42 | */ |
| 43 | evaluate(req: PermissionRequest): PermissionDecision { |
| 44 | // User deny rules outrank everything, including /auto and yolo — that is the point of |
| 45 | // being able to write one. |
| 46 | if (this.denyRules.length && matchDenyRule(req.operation, this.denyRules)) return 'deny'; |
| 47 | |
| 48 | // Hard deny patterns next — even auto-approve mode can't bypass these. |
| 49 | for (const p of this.denyPatterns) { |
| 50 | if (p.test(req.operation)) return 'deny'; |
| 51 | } |
| 52 | |
| 53 | // Irreversible commands are confirmed EVERY time. No standing grant, no session |
| 54 | // auto-approve, no yolo: rollback cannot undo `rm -rf` or a force push, so a blanket |
| 55 | // yes must never reach one. Read-only tools are exempt (their operand is a path). |
| 56 | const irreversible = |
| 57 | !this.isReadOnlyTool(req.tool) && assessCommand(req.operation).tier === 'irreversible'; |
| 58 | if (irreversible) { |
| 59 | const k = `${req.tool}:${req.operation}`; |
| 60 | if (this.sessionDenies.has(k)) return 'deny'; |
| 61 | return 'ask'; |
| 62 | } |
| 63 | |
| 64 | // Always-ask patterns next — system-mutating commands (defaults write, sudo, |
| 65 | // package installs, disk/network config). These OVERRIDE session auto-approve |
| 66 | // and autoApprove patterns: silently running them risks destabilizing the |
| 67 | // user's machine, so we force an explicit prompt every time. The only escape |
| 68 | // is a per-pair/pattern decision the user themselves granted THIS session |
| 69 | // (handled below), so a user who already said "always allow" for one specific |
| 70 | // command isn't re-nagged — but `/auto on` alone never covers these. |
| 71 | const isAlwaysAsk = this.alwaysAskPatterns.some(p => p.test(req.operation)); |
| 72 | if (isAlwaysAsk) { |
| 73 | const key = `${req.tool}:${req.operation}`; |
| 74 | if (this.sessionDenies.has(key)) return 'deny'; |
| 75 | if (this.sessionAllows.has(key)) return 'allow'; |
| 76 | if (this.commandGrants.has(normalizeCommand(req.operation))) return 'allow'; |
| 77 | if (this.alwaysAllowPatterns.some(p => p.test(req.operation))) return 'allow'; |
| 78 | return 'ask'; |
| 79 | } |
| 80 | |
| 81 | // Session-wide auto-approve overrides everything except hard denies and |
| 82 | // always-ask (handled above). |
| 83 | // Set by `/auto on`; cleared by `/auto off`. |
| 84 | if (_autoApproveSession) return 'allow'; |
| 85 | |
| 86 | // "Allow this tool for the whole session" — from gradient picker |
| 87 | if (this.sessionToolAllows.has(req.tool)) return 'allow'; |
| 88 |