Skip to content

Commit 75922f6

Browse files
authored
Merge pull request #82 from writingmate/codex/macos-release-handler-registration
Register the macOS release app before auth verification
2 parents 831f7c0 + b0ba7be commit 75922f6

5 files changed

Lines changed: 265 additions & 2 deletions

File tree

.github/workflows/apple-audio-recovery.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ on:
1414
- 'scripts/validate_macos_recording_recovery.swift'
1515
- 'scripts/validate_macos_transcription_attempt_snapshot.swift'
1616
- 'scripts/validate_macos_vad_recovery.swift'
17+
- 'scripts/validate_macos_url_handler_registration.sh'
18+
- 'scripts/verify_macos_url_handler.swift'
19+
- 'scripts/verify_macos_release_auth.sh'
1720
- 'scripts/validate_parakeet_runtime_recovery.swift'
1821
- 'scripts/validate_transcription_prompt_routing.py'
1922
- 'scripts/validate_transcription_cleanup_prompt.swift'
@@ -35,6 +38,9 @@ on:
3538
- 'scripts/validate_macos_recording_recovery.swift'
3639
- 'scripts/validate_macos_transcription_attempt_snapshot.swift'
3740
- 'scripts/validate_macos_vad_recovery.swift'
41+
- 'scripts/validate_macos_url_handler_registration.sh'
42+
- 'scripts/verify_macos_url_handler.swift'
43+
- 'scripts/verify_macos_release_auth.sh'
3844
- 'scripts/validate_parakeet_runtime_recovery.swift'
3945
- 'scripts/validate_transcription_prompt_routing.py'
4046
- 'scripts/validate_transcription_cleanup_prompt.swift'
@@ -71,6 +77,9 @@ jobs:
7177
exit 1
7278
fi
7379
80+
- name: Validate macOS callback registration
81+
run: bash scripts/validate_macos_url_handler_registration.sh
82+
7483
- name: Run deterministic recovery matrix
7584
run: bash scripts/validate_apple_audio_recovery.sh
7685

.github/workflows/release-macos.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ jobs:
184184
-o /tmp/validate_macos_history_row_refresh
185185
/tmp/validate_macos_history_row_refresh
186186
187+
- name: Validate macOS callback registration
188+
run: bash scripts/validate_macos_url_handler_registration.sh
189+
187190
- name: Resolve Swift package dependencies
188191
run: |
189192
xcodebuild -resolvePackageDependencies \
@@ -302,6 +305,23 @@ jobs:
302305
echo "Packaged app is $packaged_version ($packaged_build), expected $VERSION ($BUILD)." >&2
303306
exit 1
304307
fi
308+
python3 - "$INFO_PLIST" <<'PYEOF'
309+
import plistlib
310+
import sys
311+
312+
with open(sys.argv[1], "rb") as plist_file:
313+
info = plistlib.load(plist_file)
314+
schemes = [
315+
scheme
316+
for url_type in info.get("CFBundleURLTypes", [])
317+
for scheme in url_type.get("CFBundleURLSchemes", [])
318+
]
319+
if schemes != ["aidictation"]:
320+
raise SystemExit(
321+
"exported release app does not declare exactly the aidictation callback scheme"
322+
)
323+
print("verified exact exported aidictation callback scheme")
324+
PYEOF
305325
scripts/validate_macos_universal_app.sh "$APP"
306326
python3 scripts/validate_release_transcription_config.py \
307327
--secrets "$APP/Contents/Resources/Secrets.plist" \
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
HANDLER_VERIFIER="$SCRIPT_DIR/verify_macos_url_handler.swift"
6+
LSREGISTER_PATH="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
7+
USER_APPLICATIONS_DIR="${HOME}/Applications"
8+
WORK_PREFIX="${TMPDIR:-/tmp}/aidictation-url-handler-contract."
9+
INSTALL_PREFIX="$USER_APPLICATIONS_DIR/AIDictationURLHandlerContract."
10+
WORK_DIR=""
11+
INSTALL_ROOT=""
12+
INSTALLED_APP=""
13+
14+
cleanup() {
15+
if [[ -n "$INSTALL_ROOT" \
16+
&& "$INSTALL_ROOT" == "$INSTALL_PREFIX"* \
17+
&& "$(dirname "$INSTALL_ROOT")" == "$USER_APPLICATIONS_DIR" \
18+
&& "$INSTALLED_APP" == "$INSTALL_ROOT/CallbackProbe.app" ]]; then
19+
"$LSREGISTER_PATH" -u "$INSTALLED_APP" >/dev/null 2>&1 || true
20+
if [[ -d "$INSTALL_ROOT" ]]; then
21+
rm -rf -- "$INSTALL_ROOT"
22+
fi
23+
fi
24+
if [[ -n "$WORK_DIR" && "$WORK_DIR" == "$WORK_PREFIX"* && -d "$WORK_DIR" ]]; then
25+
rm -rf -- "$WORK_DIR"
26+
fi
27+
}
28+
trap cleanup EXIT
29+
30+
test -x "$LSREGISTER_PATH"
31+
test -f "$HANDLER_VERIFIER"
32+
mkdir -p "$USER_APPLICATIONS_DIR"
33+
34+
WORK_DIR="$(mktemp -d "${WORK_PREFIX}XXXXXX")"
35+
INSTALL_ROOT="$(mktemp -d "${INSTALL_PREFIX}XXXXXX")"
36+
SOURCE_APP="$WORK_DIR/CallbackProbe.app"
37+
INSTALLED_APP="$INSTALL_ROOT/CallbackProbe.app"
38+
SCHEME="aidictationhandlercontract${RANDOM}${RANDOM}"
39+
40+
mkdir -p "$SOURCE_APP/Contents/MacOS"
41+
cat > "$WORK_DIR/CallbackProbe.swift" <<'SWIFT'
42+
import AppKit
43+
import Foundation
44+
45+
final class AppDelegate: NSObject, NSApplicationDelegate {
46+
func application(_ application: NSApplication, open urls: [URL]) {
47+
let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes")
48+
as? [[String: Any]]
49+
let expectedScheme = (urlTypes?.first?["CFBundleURLSchemes"] as? [String])?.first
50+
guard urls.contains(where: {
51+
$0.scheme == expectedScheme && $0.host == "auth-callback"
52+
}) else {
53+
return
54+
}
55+
56+
let sentinel = Bundle.main.bundleURL
57+
.deletingLastPathComponent()
58+
.appendingPathComponent("callback-received")
59+
try? Data("received\n".utf8).write(to: sentinel, options: .atomic)
60+
application.terminate(nil)
61+
}
62+
63+
func applicationDidFinishLaunching(_ notification: Notification) {
64+
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
65+
NSApplication.shared.terminate(nil)
66+
}
67+
}
68+
}
69+
70+
let app = NSApplication.shared
71+
let delegate = AppDelegate()
72+
app.delegate = delegate
73+
app.setActivationPolicy(.prohibited)
74+
app.run()
75+
SWIFT
76+
77+
cat > "$SOURCE_APP/Contents/Info.plist" <<PLIST
78+
<?xml version="1.0" encoding="UTF-8"?>
79+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
80+
<plist version="1.0">
81+
<dict>
82+
<key>CFBundleExecutable</key>
83+
<string>CallbackProbe</string>
84+
<key>CFBundleIdentifier</key>
85+
<string>com.writingmate.aidictation.release-handler-contract</string>
86+
<key>CFBundleName</key>
87+
<string>CallbackProbe</string>
88+
<key>CFBundlePackageType</key>
89+
<string>APPL</string>
90+
<key>CFBundleURLTypes</key>
91+
<array>
92+
<dict>
93+
<key>CFBundleTypeRole</key>
94+
<string>Editor</string>
95+
<key>CFBundleURLSchemes</key>
96+
<array>
97+
<string>$SCHEME</string>
98+
</array>
99+
</dict>
100+
</array>
101+
</dict>
102+
</plist>
103+
PLIST
104+
105+
swiftc "$WORK_DIR/CallbackProbe.swift" -o "$SOURCE_APP/Contents/MacOS/CallbackProbe"
106+
codesign --force --deep --sign - "$SOURCE_APP" >/dev/null
107+
108+
if swift "$HANDLER_VERIFIER" "$SOURCE_APP" "$SCHEME" 0.25 >/dev/null 2>&1; then
109+
echo "Synthetic callback unexpectedly resolved to the uninstalled source app." >&2
110+
exit 1
111+
fi
112+
113+
ditto "$SOURCE_APP" "$INSTALLED_APP"
114+
codesign --verify --deep --strict "$INSTALLED_APP"
115+
"$LSREGISTER_PATH" -f "$INSTALLED_APP"
116+
swift "$HANDLER_VERIFIER" "$INSTALLED_APP" "$SCHEME"
117+
if ! open "$SCHEME://auth-callback" >/dev/null 2>&1; then
118+
echo "macOS could not deliver the synthetic callback to the registered app." >&2
119+
exit 1
120+
fi
121+
for attempt in {1..20}; do
122+
if [[ -s "$INSTALL_ROOT/callback-received" ]]; then
123+
break
124+
fi
125+
if [[ "$attempt" -eq 20 ]]; then
126+
echo "The registered synthetic app did not receive the callback." >&2
127+
exit 1
128+
fi
129+
sleep 0.25
130+
done
131+
132+
cleanup
133+
trap - EXIT
134+
if [[ -e "$INSTALL_ROOT" || -e "$WORK_DIR" ]]; then
135+
echo "Synthetic callback verification did not clean up its temporary apps." >&2
136+
exit 1
137+
fi
138+
139+
echo "Verified exact LaunchServices registration, app callback receipt, and cleanup."

scripts/verify_macos_release_auth.sh

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ EXPECTED_VERSION="${2:-}"
66
BROWSER_SCRIPT="${AIDICTATION_RELEASE_AUTH_BROWSER_SCRIPT:-/tmp/aidictation-release-browser/capture_release_auth_callback.mjs}"
77
CALLBACK_PATH="${AIDICTATION_RELEASE_AUTH_CALLBACK_PATH:-/tmp/aidictation-release-auth-callback}"
88
RESULT_PATH="${AIDICTATION_RELEASE_AUTH_RESULT_PATH:-/tmp/aidictation-release-auth-result.json}"
9+
HANDLER_VERIFIER="${AIDICTATION_RELEASE_URL_HANDLER_VERIFIER:-scripts/verify_macos_url_handler.swift}"
10+
LSREGISTER_PATH="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
11+
USER_APPLICATIONS_DIR="${HOME}/Applications"
12+
INSTALL_PREFIX="$USER_APPLICATIONS_DIR/AIDictationReleaseVerification."
13+
INSTALL_ROOT=""
14+
INSTALLED_APP=""
915

1016
if [[ -z "$APP_PATH" || ! -d "$APP_PATH" ]]; then
1117
echo "A signed AIDictation.app path is required." >&2
@@ -19,23 +25,64 @@ if [[ ! -f "$BROWSER_SCRIPT" ]]; then
1925
echo "The live browser verifier is not installed at $BROWSER_SCRIPT." >&2
2026
exit 1
2127
fi
28+
if [[ ! -f "$HANDLER_VERIFIER" || ! -x "$LSREGISTER_PATH" ]]; then
29+
echo "The macOS callback-handler verifier is unavailable." >&2
30+
exit 1
31+
fi
2232

2333
cleanup() {
2434
launchctl unsetenv AIDICTATION_RELEASE_AUTH_SMOKE >/dev/null 2>&1 || true
2535
launchctl unsetenv AIDICTATION_RELEASE_AUTH_RESULT >/dev/null 2>&1 || true
2636
pkill -x AIDictation >/dev/null 2>&1 || true
2737
rm -f "$CALLBACK_PATH" "$RESULT_PATH"
38+
if [[ -n "$INSTALL_ROOT" \
39+
&& "$INSTALL_ROOT" == "$INSTALL_PREFIX"* \
40+
&& "$(dirname "$INSTALL_ROOT")" == "$USER_APPLICATIONS_DIR" \
41+
&& "$INSTALLED_APP" == "$INSTALL_ROOT/AIDictation.app" ]]; then
42+
"$LSREGISTER_PATH" -u "$INSTALLED_APP" >/dev/null 2>&1 || true
43+
if [[ -d "$INSTALL_ROOT" ]]; then
44+
rm -rf -- "$INSTALL_ROOT"
45+
fi
46+
fi
2847
}
2948
trap cleanup EXIT
3049

50+
python3 - "$APP_PATH/Contents/Info.plist" <<'PYEOF'
51+
import plistlib
52+
import sys
53+
54+
with open(sys.argv[1], "rb") as plist_file:
55+
info = plistlib.load(plist_file)
56+
57+
schemes = [
58+
scheme
59+
for url_type in info.get("CFBundleURLTypes", [])
60+
for scheme in url_type.get("CFBundleURLSchemes", [])
61+
]
62+
if schemes != ["aidictation"]:
63+
raise SystemExit("The release app does not declare exactly the aidictation callback scheme.")
64+
PYEOF
65+
66+
mkdir -p "$USER_APPLICATIONS_DIR"
67+
INSTALL_ROOT="$(mktemp -d "${INSTALL_PREFIX}XXXXXX")"
68+
if [[ "$INSTALL_ROOT" != "$INSTALL_PREFIX"* ]]; then
69+
echo "The release verification install path is outside the user Applications directory." >&2
70+
exit 1
71+
fi
72+
INSTALLED_APP="$INSTALL_ROOT/AIDictation.app"
73+
ditto "$APP_PATH" "$INSTALLED_APP"
74+
codesign --verify --deep --strict --verbose=2 "$INSTALLED_APP"
75+
"$LSREGISTER_PATH" -f "$INSTALLED_APP"
76+
swift "$HANDLER_VERIFIER" "$INSTALLED_APP" aidictation
77+
3178
export AIDICTATION_RELEASE_AUTH_CALLBACK_PATH="$CALLBACK_PATH"
3279
node "$BROWSER_SCRIPT"
3380
test -s "$CALLBACK_PATH"
3481

3582
rm -f "$RESULT_PATH"
3683
launchctl setenv AIDICTATION_RELEASE_AUTH_SMOKE 1
3784
launchctl setenv AIDICTATION_RELEASE_AUTH_RESULT "$RESULT_PATH"
38-
open -na "$APP_PATH"
85+
open -na "$INSTALLED_APP"
3986

4087
for attempt in {1..20}; do
4188
if pgrep -x AIDictation >/dev/null; then
@@ -49,7 +96,10 @@ for attempt in {1..20}; do
4996
done
5097

5198
CALLBACK_URL="$(<"$CALLBACK_PATH")"
52-
open "$CALLBACK_URL"
99+
if ! open "$CALLBACK_URL" >/dev/null 2>&1; then
100+
echo "macOS could not deliver the browser callback to the registered release app." >&2
101+
exit 1
102+
fi
53103
unset CALLBACK_URL
54104
rm -f "$CALLBACK_PATH"
55105

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import CoreServices
2+
import Foundation
3+
4+
guard CommandLine.arguments.count == 3 || CommandLine.arguments.count == 4 else {
5+
fputs("Usage: verify_macos_url_handler.swift <app-path> <scheme> [timeout-seconds]\n", stderr)
6+
exit(2)
7+
}
8+
9+
let expectedApp = URL(fileURLWithPath: CommandLine.arguments[1])
10+
.resolvingSymlinksInPath()
11+
.standardizedFileURL
12+
let scheme = CommandLine.arguments[2]
13+
let timeout = CommandLine.arguments.count == 4
14+
? (Double(CommandLine.arguments[3]) ?? 10)
15+
: 10
16+
17+
guard let callbackURL = URL(string: "\(scheme)://auth-callback") else {
18+
fputs("The release callback scheme is invalid.\n", stderr)
19+
exit(2)
20+
}
21+
22+
let deadline = Date().addingTimeInterval(max(timeout, 0))
23+
repeat {
24+
var lookupError: Unmanaged<CFError>?
25+
if let unmanagedHandler = LSCopyDefaultApplicationURLForURL(
26+
callbackURL as CFURL,
27+
.all,
28+
&lookupError
29+
) {
30+
let actualApp = (unmanagedHandler.takeRetainedValue() as URL)
31+
.resolvingSymlinksInPath()
32+
.standardizedFileURL
33+
if actualApp == expectedApp {
34+
print("LaunchServices resolves the callback to the exact release app.")
35+
exit(0)
36+
}
37+
}
38+
39+
if Date() < deadline {
40+
Thread.sleep(forTimeInterval: 0.25)
41+
}
42+
} while Date() < deadline
43+
44+
fputs("LaunchServices does not resolve the callback to the exact release app.\n", stderr)
45+
exit(1)

0 commit comments

Comments
 (0)