| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- /**
- * 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<Record<string, string>> = {
- // 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,
- };
|