Files
Jarvis/scripts/check-applescript-confirmation.sh
james.maesandClaude Opus 4.7 2d9a25a61e chore(gates): add check-applescript-confirmation.sh — defense-in-depth grep gate
P3-18 / security LOW-3: the run_applescript MCP tool MUST be registered
with requiresConfirmation: true so the HUD's confirmation panel gates
every AppleScript execution. Today the production registration lives
in App/MCP/MCPRuntimeWiring.swift:119 and is exercised by
MCPRuntimeWiringTests, but a grep gate adds defense in depth — every
line of source is covered, not just the path the test exercises.

Detection:
  For each non-comment, non-test line containing the literal
  `run_applescript`, walk down up to 5 lines (until the next `)` —
  the close of the enclosing register call), accumulate the call body,
  and assert requiresConfirmation: true is present. The window
  terminates early at the closing paren so a sibling register block
  with the flag set differently can't leak in via overlap.

Verified the gate catches a regression: temporarily flipped line 119
to `requiresConfirmation: false` — gate FAILed with the file:line
citation. Reverted; gate PASSes on clean tree.

Comment-only references to run_applescript (HudStateIntent.swift:19,
InProcessTool.swift:9) are filtered by is_comment_line, so they don't
trigger false positives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:45:45 -05:00

121 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# check-applescript-confirmation.sh
#
# P3-18 / security LOW-3 — defense-in-depth grep gate. The
# `run_applescript` MCP tool MUST be registered with
# `requiresConfirmation: true` so the HUD's confirmation panel gates
# every AppleScript execution. Today the registration lives in
# `App/MCP/MCPRuntimeWiring.swift:119`; this gate fails the build if any
# production registration drifts to `requiresConfirmation: false` (or
# omits the flag, which would default to false at the registry level).
#
# Why a grep gate when there's also a runtime test
# (`MCPRuntimeWiringTests.requiresConfirmation` asserts the flag)?
# Defense in depth — the test covers the path the test exercises; the
# grep gate covers every line of source. A future contributor adding a
# second registration site (e.g. an in-process AppleScript tool) won't
# accidentally ship an unconfirmed path; the lint catches the regression
# before the test does.
#
# Scope:
# - Scans `App/` and `packages/MCP/Sources/` for production references
# to `run_applescript`.
# - Skips `*/Tests/*`, `*/test-fixtures/*`, `.build/`, this script.
# - Ignores pure comment / docstring lines (the literal in a `///` or
# `//` comment is not a registration).
#
# Detection:
# For each non-comment, non-test, non-string-literal-only line
# containing `run_applescript`, the gate looks within a tight window
# (the line itself plus up to 5 following lines, terminated early at
# the next `)`) — i.e. the body of the enclosing `register(...)` call.
# That window MUST contain `requiresConfirmation: true`. Anything else
# (`false`, omitted, or a `true` outside the call body) fails the gate.
#
# Exit:
# 0 — clean (PASS)
# 1 — at least one production `run_applescript` site missing
# `requiresConfirmation: true` inside its register-call body
# (FAIL + file:line citation)
#
# Usage:
# bash scripts/check-applescript-confirmation.sh
set -euo pipefail
ROOT="${SRCROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
cd "$ROOT"
# Production sources: App/** and packages/MCP/Sources/**.
mapfile -t FILES < <(find App packages/MCP/Sources -type f -name '*.swift' \
-not -path '*/.build/*' \
-not -path '*/Tests/*' \
-not -path '*/test-fixtures/*' \
2>/dev/null)
# Lines to scan beyond the `run_applescript` line for the closing `)`
# of the enclosing call. Real registrations close within 1-3 lines; 5
# is a safe ceiling for a wrapped call without picking up a sibling
# `register(...)` block below.
WINDOW_DOWN=5
is_comment_line() {
# Returns 0 (true) when the line, after stripping leading whitespace,
# starts with `//`, `///`, `/**`, or `*` (block-comment continuation).
local line="$1"
local stripped="${line#"${line%%[![:space:]]*}"}"
case "$stripped" in
//*|/\*\*|\**) return 0 ;;
*) return 1 ;;
esac
}
FOUND=0
for f in "${FILES[@]}"; do
# Find every line containing the literal `run_applescript`. We grep
# the raw file (no comment-strip), then filter comment lines per-hit
# because is_comment_line needs the whole line.
while IFS=: read -r lineno line; do
if is_comment_line "$line"; then
continue
fi
# Walk from the hit line down for up to WINDOW_DOWN lines, until
# we see a `)` that closes the enclosing call. Accumulate the
# text we see in `block`. The hit line itself is included — many
# registrations fit on one line (`registry.register(toolName:
# "run_applescript", …, requiresConfirmation: true)`).
block=""
end=$(( lineno + WINDOW_DOWN ))
cur=$lineno
while [ "$cur" -le "$end" ]; do
cur_line="$(sed -n "${cur}p" "$f")"
block="${block}
${cur_line}"
# Stop at the first un-escaped `)` — that's the close of the
# enclosing register call. (Swift call expressions don't
# contain bare `)` except as the call terminator; this is
# heuristic, not a parser, but it's correct for the
# registration shapes used in this codebase.)
case "$cur_line" in
*\)*) break ;;
esac
cur=$((cur + 1))
done
if ! printf '%s\n' "$block" | grep -qE 'requiresConfirmation:[[:space:]]*true'; then
echo " $f:$lineno: run_applescript registration missing requiresConfirmation: true within the enclosing call body"
echo " >>> $line"
FOUND=$((FOUND + 1))
fi
done < <(grep -nE 'run_applescript' "$f" 2>/dev/null || true)
done
if [ "$FOUND" -gt 0 ]; then
echo "[check-applescript-confirmation] FAIL — $FOUND missing requiresConfirmation" >&2
exit 1
fi
echo "[check-applescript-confirmation] PASS"
exit 0