docs/plans/MACOS_NOTARIZATION.md
Status: Diagnosis — Homebrew Cask CI rejects v0.4.5 on macOS 15 (Sequoia); fix pending
Touches: .github/workflows/release.yml, Tauri bundler config, sidecar signing
Last reviewed: 2026-04-24
Homebrew Cask PR #260314 adds brew install --cask voicebox. CI is green on macOS 14 and macOS 26 (arm + intel) but fails on macOS 15 (arm + intel). The 0.4.3 release added DMG-level stapling to address this, and it didn't move CI — 0.4.5 still fails. A maintainer reproduced the failure in a fresh Sequoia VM.
This document is the working diagnosis plus the ordered fix plan.
The failing step is brew audit --cask --online --signing --new voicebox, not brew install. brew install succeeds end-to-end in CI (the log shows Uninstalling Cask voicebox after the install phase). The --signing audit:
urlspctl --assess -t open --context context:primary-signature against the .app insideThat policy tests the first-launch Gatekeeper path on the extracted bundle. It reads the .app's own code signature and notarization ticket — the DMG wrapper is not involved. The staple added in 0.4.3 covers the DMG, so it has no effect on this audit.
spctl -t open on macOS 15 enforces checks that 14 tolerated:
.dylib or helper binary carries an ad-hoc signature (or a signature with a different Team ID), 15 rejects the whole bundle; 14 often accepted it.Local dev machines pass spctl because the first-party developer context and cached notarization tickets mask these failures. A fresh Sequoia VM with no prior trust state does not.
Voicebox ships PyInstaller sidecars declared in tauri.conf.json under externalBin:
voicebox-server only (single --onefile Mach-O on macOS)voicebox-server and voicebox-mcp (voicebox-mcp is new in 0.5.0)Tauri's bundler signs each externalBin with the configured identity but does not apply --options=runtime or --timestamp automatically, and does not merge the outer app's entitlements into the sidecar signature. The outer Voicebox binary is correctly signed with hardened runtime + disable-library-validation; the sidecars likely are not.
Order of likelihood:
voicebox-server lacks hardened runtime or a secure timestamp in its signature..dylib at runtime that Gatekeeper inspects during assessment.The 0.5.0 fix must cover both sidecars.
Run against a freshly downloaded release DMG (not a dev build, and from a machine that has never opened the app before):
hdiutil attach Voicebox_0.4.5_aarch64.dmg
xcrun stapler validate "/Volumes/Voicebox 0.4.5/Voicebox.app"
spctl -a -vvv -t open --context context:primary-signature "/Volumes/Voicebox 0.4.5/Voicebox.app"
codesign --verify --deep --strict --verbose=2 "/Volumes/Voicebox 0.4.5/Voicebox.app"
codesign -dv --verbose=4 "/Volumes/Voicebox 0.4.5/Voicebox.app/Contents/MacOS/voicebox-server"
The last command is the tell — look for flags=0x10000(runtime) and a Timestamp= line. If either is missing, the sidecar is the failure.
spctl -t install (what 0.4.3 verified with) is a different policy and can pass while -t open fails — any future verification should use -t open --context context:primary-signature to match what Homebrew's audit runs.
Pull the 0.4.5 DMG on a fresh Sequoia environment or a VM snapshot with no trust state. Run the diagnostic block above. Record the exact failing command and its CSSMERR / rejection reason. This disambiguates between the three hypotheses before we change the workflow.
Between tauri-action's build step and the DMG-notarization step already in release.yml, add a step that re-signs every externalBin present under Voicebox.app/Contents/MacOS/ with:
--options=runtime (hardened runtime)--timestamp (secure timestamp)--entitlements pointing at Entitlements.plist or a sidecar-specific subsetAPPLE_SIGNING_IDENTITY the outer app usesRe-sign the outer .app afterward so its seal covers the updated nested signatures.
Covers voicebox-server on 0.4.x and both sidecars from 0.5.0 forward.
.appAfter sidecars are re-signed the outer bundle's notarization ticket is stale. Submit the .app (zipped) to notarytool, wait, then xcrun stapler staple Voicebox.app. This puts the ticket directly on the .app so the spctl -t open audit passes without any online ticket lookup.
Then rebuild the DMG from the stapled .app and keep the existing DMG-level notarize/staple step — it still helps Finder drag-install.
Before upload, run the same four diagnostic commands against the built artifact inside the workflow. If any fail, fail the release job rather than shipping a DMG that Homebrew (and Sequoia Finder users) will reject. This is the check that would have caught the 0.4.3 and 0.4.5 attempts before they cost PR review cycles.
Once a tagged release passes Phase 4 locally, push a cask update to #260314. Expect test voicebox (macos-15, arm) and test voicebox (macos-15-intel, intel) to go green.
APPLE_API_KEY_PATH to the bundler's notarize path, or does it rely on the ~/.appstoreconnect/private_keys/AuthKey_*.p8 auto-discovery the staple step already sets up? If the former isn't working, tauri may be signing but never notarizing the .app, which would make the ticket absent entirely rather than stale. Worth a grep -i notariz on a full release job log.