feat(deploy): add --fix-accessibility flag + document TCC requirement

macOS invalidates Accessibility permission whenever the app binary
changes (code signature shifts on each build). New --fix-accessibility
flag runs tccutil reset + a sudo sqlite3 TCC grant via SSH after the
.app is synced. Falls back gracefully if sqlite3 grant fails (SIP or
missing sudoers), logging a warning with a pointer to the manual steps.

README documents the symptom, manual fix, sudoers one-time setup,
and bundle ID (com.electron.aetherlauncher).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-12 14:09:34 -04:00
parent 1f90c819a0
commit ec29a576d5
2 changed files with 73 additions and 7 deletions

View File

@@ -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): # Update seed.json only (no .app copy — e.g. when rotating the API key):
./deploy/deploy.sh --seed-only all ./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 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 After the script completes, launch the app on each laptop and confirm it connects and shows
the correct device name in the Launcher UI. 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) ### Adding SSH key to a new laptop (first time only)
```bash ```bash

View File

@@ -2,12 +2,14 @@
# deploy.sh — Deploy Aether Native Launcher to onsite Mac laptops # deploy.sh — Deploy Aether Native Launcher to onsite Mac laptops
# #
# USAGE: # USAGE:
# ./deploy.sh <num> [num ...] Deploy to one or more laptops (e.g. 03 04 05) # ./deploy.sh <num> [num ...] Deploy to one or more laptops (e.g. 03 04 05)
# ./deploy.sh all Deploy to all laptops in devices.conf # ./deploy.sh all Deploy to all laptops in devices.conf
# ./deploy.sh --seed-only <num> Update seed.json only — skip .app copy # ./deploy.sh --seed-only <num> Update seed.json only — skip .app copy
# ./deploy.sh --seed-only all # ./deploy.sh --seed-only all
# ./deploy.sh --build <num> [num ...] Build first (npm run package:mac), then deploy # ./deploy.sh --build <num> [num ...] Build first (npm run package:mac), then deploy
# ./deploy.sh --build all # ./deploy.sh --build all
# ./deploy.sh --fix-accessibility <num> [num ...] Re-grant macOS Accessibility permission after .app update
# ./deploy.sh --fix-accessibility all (requires NOPASSWD sudo for sqlite3 — see README)
# #
# REQUIRES: # REQUIRES:
# event.env — copy from event.env.example and fill in AETHER_API_KEY # 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 SEED_ONLY=false
BUILD_FIRST=false BUILD_FIRST=false
FIX_ACCESSIBILITY=false
TARGETS=() TARGETS=()
# Bundle ID as embedded in Info.plist by electron-packager (no --app-bundle-id override)
BUNDLE_ID="com.electron.aetherlauncher"
usage() { usage() {
grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//' grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//'
@@ -38,9 +43,10 @@ if [[ $# -eq 0 ]]; then usage; fi
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--seed-only) SEED_ONLY=true ;; --seed-only) SEED_ONLY=true ;;
--build) BUILD_FIRST=true ;; --build) BUILD_FIRST=true ;;
--help|-h) usage ;; --fix-accessibility) FIX_ACCESSIBILITY=true ;;
--help|-h) usage ;;
all) TARGETS+=("all") ;; all) TARGETS+=("all") ;;
*) TARGETS+=("$1") ;; *) TARGETS+=("$1") ;;
esac esac
@@ -162,6 +168,26 @@ deploy_laptop() {
} }
EOF 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 ───────────────────────────────────────────────────────────── # ── Verify ─────────────────────────────────────────────────────────────
echo " Verifying..." echo " Verifying..."
ssh "$SSH_USER@$ip" "cat ~/seed.json" ssh "$SSH_USER@$ip" "cat ~/seed.json"