diff --git a/README.md b/README.md index 7148080..0d8ca01 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ Devices API key section). All laptops share one key per event. Delete it after t # Update seed.json only (no .app copy — e.g. when rotating the API key): ./deploy/deploy.sh --seed-only all + +# Deploy and re-grant Accessibility permission in one pass: +./deploy/deploy.sh --fix-accessibility all ``` The script auto-detects each Mac's CPU architecture, copies the correct `.app` build, writes @@ -74,6 +77,43 @@ continues, then reports which laptops need a retry. After the script completes, launch the app on each laptop and confirm it connects and shows the correct device name in the Launcher UI. +### macOS Accessibility Permission + +The launcher sends keystrokes to PowerPoint and Keynote via AppleScript. macOS requires +explicit **Accessibility** access for this. Every time a new `.app` binary is deployed, macOS +invalidates the stored permission because the code signature changes — even when rsync +updates in-place. + +**Symptom:** Slide control silently fails; `osascript` eventually returns a permissions error. + +**Manual fix** (GUI — always works): +1. System Settings → Privacy & Security → Accessibility +2. Find `aether_launcher` in the list → remove it (− button) +3. Re-add it (+ button → `/Applications/aether_launcher.app`) and toggle it on + +**Automated fix** — use the `--fix-accessibility` deploy flag: +```bash +./deploy/deploy.sh --fix-accessibility 01 02 03 +./deploy/deploy.sh --build --fix-accessibility all +``` +This runs `tccutil reset` (no password required) then attempts a direct TCC database grant +via `sudo sqlite3`. The sqlite3 step requires NOPASSWD sudo on each Mac — one-time setup: + +```bash +ssh "speaker ready"@192.168.32.1XX "sudo visudo -f /etc/sudoers.d/aether-tcc" +``` +Add this line, then save: +```text +speaker ready ALL=(ALL) NOPASSWD: /usr/bin/sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db * +``` + +If the sqlite3 grant fails (SIP enabled, sudoers not configured), the script logs a warning +and falls back gracefully — a fresh permission prompt appears the first time the app uses +accessibility. The `--fix-accessibility` flag can be combined with any other flag. + +**Long-term fix:** Code-sign the app with an Apple Developer certificate. A stable signature +means macOS never invalidates the permission on updates. Currently out of scope. + ### Adding SSH key to a new laptop (first time only) ```bash diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 5e3ecd4..88d2911 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -2,12 +2,14 @@ # deploy.sh — Deploy Aether Native Launcher to onsite Mac laptops # # USAGE: -# ./deploy.sh [num ...] Deploy to one or more laptops (e.g. 03 04 05) -# ./deploy.sh all Deploy to all laptops in devices.conf -# ./deploy.sh --seed-only Update seed.json only — skip .app copy +# ./deploy.sh [num ...] Deploy to one or more laptops (e.g. 03 04 05) +# ./deploy.sh all Deploy to all laptops in devices.conf +# ./deploy.sh --seed-only Update seed.json only — skip .app copy # ./deploy.sh --seed-only all -# ./deploy.sh --build [num ...] Build first (npm run package:mac), then deploy +# ./deploy.sh --build [num ...] Build first (npm run package:mac), then deploy # ./deploy.sh --build all +# ./deploy.sh --fix-accessibility [num ...] Re-grant macOS Accessibility permission after .app update +# ./deploy.sh --fix-accessibility all (requires NOPASSWD sudo for sqlite3 — see README) # # REQUIRES: # event.env — copy from event.env.example and fill in AETHER_API_KEY @@ -27,7 +29,10 @@ BUILD_DIR="$SCRIPT_DIR/../builds" SEED_ONLY=false BUILD_FIRST=false +FIX_ACCESSIBILITY=false TARGETS=() +# Bundle ID as embedded in Info.plist by electron-packager (no --app-bundle-id override) +BUNDLE_ID="com.electron.aetherlauncher" usage() { grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//' @@ -38,9 +43,10 @@ if [[ $# -eq 0 ]]; then usage; fi while [[ $# -gt 0 ]]; do case "$1" in - --seed-only) SEED_ONLY=true ;; - --build) BUILD_FIRST=true ;; - --help|-h) usage ;; + --seed-only) SEED_ONLY=true ;; + --build) BUILD_FIRST=true ;; + --fix-accessibility) FIX_ACCESSIBILITY=true ;; + --help|-h) usage ;; all) TARGETS+=("all") ;; *) TARGETS+=("$1") ;; esac @@ -162,6 +168,26 @@ deploy_laptop() { } EOF + # ── Accessibility permission ─────────────────────────────────────────── + if [[ "$FIX_ACCESSIBILITY" == "true" ]]; then + echo " Resetting Accessibility permission (tccutil)..." + if ssh "$SSH_USER@$ip" "tccutil reset Accessibility $BUNDLE_ID" 2>/dev/null; then + echo " tccutil reset OK." + else + echo " WARNING: tccutil reset failed (non-fatal)." + fi + + echo " Granting Accessibility via TCC database (requires NOPASSWD sudo)..." + # shellcheck disable=SC2016 + TCC_SQL="INSERT OR REPLACE INTO access(service,client,client_type,auth_value,auth_reason,auth_version) VALUES('kTCCServiceAccessibility','$BUNDLE_ID',0,2,4,1);" + if ssh "$SSH_USER@$ip" "sudo sqlite3 '/Library/Application Support/com.apple.TCC/TCC.db' \"$TCC_SQL\"" 2>/dev/null; then + echo " ✓ Accessibility granted." + else + echo " WARNING: TCC grant failed — manual re-authorization required." + echo " See README: macOS Accessibility Permission." + fi + fi + # ── Verify ───────────────────────────────────────────────────────────── echo " Verifying..." ssh "$SSH_USER@$ip" "cat ~/seed.json"