Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions script/probes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ swift script/probes/displays.swift # displays, persistent UUIDs
./script/probes/sound-sweep.sh # alert volume, beep sound, startup chime
./script/probes/locale-sweep.sh # region keys, input sources
./script/probes/power-sweep.sh # sleep/pmset (section C needs root)
./script/probes/inert-keys.sh # com.apple.Accessibility, AppleInterfaceStyle (VM only)
```

`accessibility-effective.swift` reports what macOS *actually* honours, not what
Expand Down Expand Up @@ -111,6 +112,36 @@ effect) from keys with none (persistence only — it pauses ~10s so you can look
That split is deliberate: "the write succeeded" was never sufficient evidence
here, since `com.apple.Accessibility` writes succeed and change nothing.

## `inert-keys.sh`

The two "writes and lies" rows that had no probe: `com.apple.Accessibility`
(section A) and a `defaults` write of `AppleInterfaceStyle` (B). Section C
flips dark mode through System Events as the control, because a "nothing moved"
only counts once the oracle has been seen to move. C repaints the desktop, so
run the whole thing in a tart VM, never on the Mac someone is using.

## Last full re-run: macOS 27.0 (26A428), 2026-09-23

Every sweep ran in two tart guests side by side, a 26.6.2 control and a 27.0
guest, and the outputs matched line for line apart from the base image's own
defaults (27's guest starts on `ABC` with a long language list). The read-only
probes ran on a 27.0 host and agreed with the doc.

The FDA refusal needs the host too: cirruslabs guests run with SIP off, so TCC
doesn't gate Full Disk Access there (a launchd agent with no grant read
`TCC.db` and wrote `com.apple.universalaccess` without complaint). On the 27.0
host, a throwaway launchd agent with no grant wrote an unprotected domain fine
and got `Could not write domain com.apple.universalaccess; exiting`, exit 1,
for a junk key. Unchanged.

Two rows a VM cannot answer, so they still stand on 26 evidence:

- **The by-eye rows.** The private `CGSGetCursorScale` read 1.0 after a
`mouseDriverCursorSize = 3.0` write and a `universalaccessd` restart on the
26 control too, so it is no oracle in a headless guest.
- **Power's battery/AC split.** No battery in a guest; `pmset -c` didn't land
there on either version.

## `sound-sweep.sh` · `locale-sweep.sh` · `power-sweep.sh` — §5.6's last three groups

Added 2026-08-08 to settle the three curated-settings groups the roadmap had
Expand Down
109 changes: 109 additions & 0 deletions script/probes/inert-keys.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# The two "writes and lies" rows of haus's docs/macos-settings.md that had no
# probe of their own: `com.apple.Accessibility` and
# `NSGlobalDomain AppleInterfaceStyle`. Both write cleanly and both move nothing,
# so the only evidence is an effective-state oracle read in a FRESH process.
#
# A. com.apple.Accessibility write the modern-looking keys, read NSWorkspace
# B. AppleInterfaceStyle write Dark (+ activateSettings -u), read AppKit's
# effective appearance and listen for
# AppleInterfaceThemeChangedNotification
# C. the control for B flip dark mode through System Events, which
# SHOULD move both. Without it, a "nothing moved"
# in B could be a broken oracle.
#
# RUN IT IN A VM. Section C repaints the whole desktop, and if macOS ever makes
# B live it repaints there too; that is the question being asked. `haus skill
# vm` has the loop. C also needs an Automation grant for System Events; a
# cirruslabs guest over ssh has one.
#
# Safe: A deletes exactly the keys it wrote. B and C put the appearance back to
# what the oracle read before they started, through System Events, and restore
# AppleInterfaceStyle's plist value. No sudo, no rebuild.
#
# Run history (dates belong here, not in the doc):
# 2026-09-23 26.6.2 (25G83) tart control and 27.0 (26A428) tart guest,
# identical: A inert, B inert with no notification, C flips
# appearance and posts the notification.

set -uo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
tmp="$(mktemp -d)"
say() { printf '\n\033[1m%s\033[0m\n' "$*"; }

cat >"$tmp/appearance.swift" <<'EOF'
import AppKit
let app = NSApplication.shared
Thread.sleep(forTimeInterval: 0.5)
let dark = app.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
print("appearance=\(dark ? "dark" : "light") key=\(UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "<absent>")")
EOF
cat >"$tmp/notewatch.swift" <<'EOF'
import Foundation
let c = DistributedNotificationCenter.default()
var seen = false
c.addObserver(forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"), object: nil, queue: nil) { _ in
seen = true; print(" POSTED AppleInterfaceThemeChangedNotification")
}
RunLoop.main.run(until: Date().addingTimeInterval(Double(CommandLine.arguments[1]) ?? 6))
if !seen { print(" no AppleInterfaceThemeChangedNotification") }
EOF

a11y() { swift "$here/accessibility-effective.swift" 2>/dev/null; }
look() { swift "$tmp/appearance.swift" 2>/dev/null; }
se_dark() { osascript -e "tell application \"System Events\" to tell appearance preferences to set dark mode to $1" 2>&1; }

ax_keys=(ReduceMotionEnabled ReduceTransparencyEnabled DifferentiateWithoutColor EnhancedBackgroundContrastEnabled)
# Per-key XML fragments, so a restore keeps the type (bash 3.2: no assoc arrays).
for k in "${ax_keys[@]}"; do
/usr/libexec/PlistBuddy -x -c "Print :$k" ~/Library/Preferences/com.apple.Accessibility.plist >"$tmp/ax.$k" 2>/dev/null || rm -f "$tmp/ax.$k"
done
style_before="$(defaults read -g AppleInterfaceStyle 2>/dev/null || true)"
start="$(look)"
start_dark=false; case "$start" in appearance=dark*) start_dark=true ;; esac

cleanup() {
printf '\n→ restoring…\n'
for k in "${ax_keys[@]}"; do
if [ -s "$tmp/ax.$k" ]; then defaults write com.apple.Accessibility "$k" "$(cat "$tmp/ax.$k")"
else defaults delete com.apple.Accessibility "$k" >/dev/null 2>&1; fi
done
se_dark "$start_dark" >/dev/null
if [ -n "$style_before" ]; then defaults write -g AppleInterfaceStyle "$style_before"
else defaults delete -g AppleInterfaceStyle >/dev/null 2>&1; fi
printf ' now: %s (started %s)\n' "$(look)" "$start"
rm -rf "$tmp"
}
trap cleanup EXIT INT TERM

say "A. com.apple.Accessibility — expect: plist moves, NSWorkspace does not"
printf ' before: %s\n' "$(a11y)"
for k in "${ax_keys[@]}"; do defaults write com.apple.Accessibility "$k" -bool true; done
notifyutil -p com.apple.accessibility.cache.ax 2>/dev/null
sleep 2
printf ' wrote %s = 1, poked com.apple.accessibility.cache.ax\n' "${ax_keys[*]}"
printf ' after: %s\n' "$(a11y)"

say "B. AppleInterfaceStyle via defaults — expect: key moves, appearance does not"
printf ' before: %s\n' "$start"
swift "$tmp/notewatch.swift" 8 >"$tmp/nw.out" 2>&1 &
sleep 2
if $start_dark; then defaults delete -g AppleInterfaceStyle 2>/dev/null; w="delete"; else defaults write -g AppleInterfaceStyle Dark; w="write Dark"; fi
/System/Library/PrivateFrameworks/SystemAdministration.framework/Resources/activateSettings -u >/dev/null 2>&1
sleep 2
printf ' %s + activateSettings -u → %s\n' "$w" "$(look)"
wait
cat "$tmp/nw.out"

say "C. Control: System Events — expect: appearance moves, notification posted"
swift "$tmp/notewatch.swift" 8 >"$tmp/nw.out" 2>&1 &
sleep 2
if $start_dark; then target=false; else target=true; fi
out="$(se_dark "$target")" || printf ' osascript refused: %s (no Automation grant? then C proves nothing)\n' "$out"
sleep 1
printf ' dark mode → %s: %s\n' "$target" "$(look)"
wait
cat "$tmp/nw.out"

say "Read it: A and B moving nothing only counts if C moved. Record in haus's docs/macos-settings.md."
Loading