Live test 2026-05-06 surfaced that the production app never actually triggered
the macOS mic or camera permission dialogs — the wizard's TCC stage was
explainer-only ("(later)") and the "first-use prompt" code that was supposed
to land later was never written. Result: AVAudioEngine opened cleanly, mic
input was silent, no wake-word ever fired, and CameraCapture.open() failed
.cameraDenied without ever asking.
Fix: explicit AVCaptureDevice.requestAccess(for:) calls at the first-use
boundary of each subsystem.
* App/AppDelegate.swift installVoice — checks mic auth status; on
.notDetermined awaits requestAccess(for: .audio) BEFORE
graphOwner.open(); on .denied/.restricted logs and returns dormant. Voice
loop only proceeds with confirmed mic grant.
* App/AppDelegate.swift installVision — same pattern for camera. Checks
status; .notDetermined → requestAccess(for: .video); .denied/.restricted
surfaces the existing degradationStream banner via cameraDegradationTask.
* App/Wizard/WizardStageTCCView.swift — wizard rows updated from
"Microphone (later)" / "Camera (later)" to "(auto-prompts shortly)" with
body text matching reality: prompts fire automatically after the wizard,
when the matching subsystem starts. The previous "later" copy implied
indefinite deferral when actually the prompts fire ~5s after wizard
completion.
Live verification:
- tccutil reset Microphone com.koftwentytwo.jarvis
- Launch → wizard appears → click through
- tccd log: "AUTHREQ_PROMPTING ... kTCCServiceMicrophone" — system
dialog displayed
- After click-Allow: AudioGraph: probed format sampleRate=48000 channels=4,
AudioGraphOwner: opened with variant <private>, WakeWordDAG: started
ring-feed loop, VoiceController: starting
Voice 87/87 pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>