/** * Key combos that cross app boundaries or terminate processes. Gated behind * the `systemKeyCombos` grant flag. When that flag is off, the `key` tool * rejects these and returns a tool error telling the model to request the * flag; all other combos work normally. * * Matching is canonicalized: every modifier alias the Rust executor accepts * collapses to one canonical name. Without this, `command+q` / `meta+q` / * `cmd+alt+escape` bypass the gate — see keyBlocklist.test.ts for the three * bypass forms and the Rust parity check that catches future alias drift. */ /** * Every modifier alias enigo_wrap.rs accepts (two copies: :351-359, :564-572), * mapped to one canonical per Key:: variant. Left/right variants collapse — * the blocklist doesn't distinguish which Ctrl. * * Canonical names are Rust's own variant names lowercased. Blocklist entries * below use ONLY these. "meta" reads odd for Cmd+Q but it's honest: Rust * sends Key::Meta, which is Cmd on darwin and Win on win32. */ const CANONICAL_MODIFIER: Readonly> = { // Key::Meta — "meta"|"super"|"command"|"cmd"|"windows"|"win" meta: 'meta', super: 'meta', command: 'meta', cmd: 'meta', windows: 'meta', win: 'meta', // Key::Control + LControl + RControl ctrl: 'ctrl', control: 'ctrl', lctrl: 'ctrl', lcontrol: 'ctrl', rctrl: 'ctrl', rcontrol: 'ctrl', // Key::Shift + LShift + RShift shift: 'shift', lshift: 'shift', rshift: 'shift', // Key::Alt and Key::Option — distinct Rust variants but same keycode on // darwin (kVK_Option). Collapse: cmd+alt+escape and cmd+option+escape // both Force Quit. alt: 'alt', option: 'alt', } /** Sort order for canonicals. ctrl < alt < shift < meta. */ const MODIFIER_ORDER = ['ctrl', 'alt', 'shift', 'meta'] /** * Canonical-form entries only. Every modifier must be a CANONICAL_MODIFIER * *value* (not key), modifiers must be in MODIFIER_ORDER, non-modifier last. * The self-consistency test enforces this. */ const BLOCKED_DARWIN = new Set([ 'meta+q', // Cmd+Q — quit frontmost app 'shift+meta+q', // Cmd+Shift+Q — log out 'alt+meta+escape', // Cmd+Option+Esc — Force Quit dialog 'meta+tab', // Cmd+Tab — app switcher 'meta+space', // Cmd+Space — Spotlight 'ctrl+meta+q', // Ctrl+Cmd+Q — lock screen ]) const BLOCKED_WIN32 = new Set([ 'ctrl+alt+delete', // Secure Attention Sequence 'alt+f4', // close window 'alt+tab', // window switcher 'meta+l', // Win+L — lock 'meta+d', // Win+D — show desktop ]) /** * Partition into sorted-canonical modifiers and non-modifier keys. * Shared by normalizeKeySequence (join for display) and isSystemKeyCombo * (check mods+each-key to catch the cmd+q+a suffix bypass). */ function partitionKeys(seq: string): { mods: string[]; keys: string[] } { const parts = seq .toLowerCase() .split('+') .map(p => p.trim()) .filter(Boolean) const mods: string[] = [] const keys: string[] = [] for (const p of parts) { const canonical = CANONICAL_MODIFIER[p] if (canonical !== undefined) { mods.push(canonical) } else { keys.push(p) } } // Dedupe: "cmd+command+q" → "meta+q", not "meta+meta+q". const uniqueMods = [...new Set(mods)] uniqueMods.sort( (a, b) => MODIFIER_ORDER.indexOf(a) - MODIFIER_ORDER.indexOf(b), ) return { mods: uniqueMods, keys } } /** * Normalize "Cmd + Shift + Q" → "shift+meta+q": lowercase, trim, alias → * canonical, dedupe, sort modifiers, non-modifiers last. */ export function normalizeKeySequence(seq: string): string { const { mods, keys } = partitionKeys(seq) return [...mods, ...keys].join('+') } /** * True if the sequence would fire a blocked OS shortcut. * * Checks mods + EACH non-modifier key individually, not just the full * joined string. `cmd+q+a` → Rust presses Cmd, then Q (Cmd+Q fires here), * then A. Exact-match against "meta+q+a" misses; checking "meta+q" and * "meta+a" separately catches the Q. * * Modifiers-only sequences ("cmd+shift") are checked as-is — no key to * pair with, and no blocklist entry is modifier-only, so this is a no-op * that falls through to false. Covers the click-modifier case where * `left_click(text="cmd")` is legitimate. */ export function isSystemKeyCombo( seq: string, platform: 'darwin' | 'win32', ): boolean { const blocklist = platform === 'darwin' ? BLOCKED_DARWIN : BLOCKED_WIN32 const { mods, keys } = partitionKeys(seq) const prefix = mods.length > 0 ? mods.join('+') + '+' : '' // No non-modifier keys (e.g. "cmd+shift" as click-modifiers) — check the // whole thing. Never matches (no blocklist entry is modifier-only) but // keeps the contract simple: every call reaches a .has(). if (keys.length === 0) { return blocklist.has(mods.join('+')) } // mods + each key. Any hit blocks the whole sequence. for (const key of keys) { if (blocklist.has(prefix + key)) { return true } } return false } export const _test = { CANONICAL_MODIFIER, BLOCKED_DARWIN, BLOCKED_WIN32, MODIFIER_ORDER, }