Skipping or denying a permission during onboarding is now a durable off for that capability's automatic path. Launch, app reactivation, key load, settings sync, wake, session rotations, and post-onboarding starts no longer raise a macOS permission sheet (or steal focus into System Settings) for a permission the user chose not to grant. Closes #12695. Closes SCA-440.
"Skip for now" on the mic step only advanced the flow. transcriptionEnabled's successor (audioRecordingMode) stayed on its onlyMeetings default, restorePersistedCaptureServices ran on launch, didBecomeActive, key load, and settings sync, and AppState.startTranscription() answered a missing mic grant with requestMicrophonePermission() — NSApp.activate() + AVCaptureDevice.requestAccess. Skip leaves TCC .notDetermined, so every restore re-armed the system sheet; dismissing it reactivated the app and the cycle repeated. The same unguarded request lived in PushToTalkManager.startAudioTranscription and ChatToolExecutor.requestMicrophonePermissionDirectly().
- Shared policy —
MicrophoneCaptureAuthorizationPolicy.action(for:userInitiated:): a non-proceedstatus on an automatic start resolves to a new.abandonAutomaticStartinstead of requesting or alerting. One rule, testable without a TCC database. - Mic —
answerMic()skip writesaudioRecordingMode = .off(Allow undoes an earlier skip);startTranscription(userInitiated:)gates the request on initiation;PersistedCaptureLaunchPolicy.shouldStartTranscriptionnow requires a live-answered mic grant soDesktopHomeView.restorePersistedCaptureServicesnever even attempts the start; automatic call sites (wake, mode-change observer, 4h/STT rotations, preferred-mic reconnect, both onboarding completions) passuserInitiated: false. The Settings audio-recording switcher requests permission explicitly on enable, matchingCaptureListeningLogic.cycleListening. - Screen —
answerScreen()records the user's answer as the standingscreenAnalysisEnabledintent;SBOnboardingModel.complete()no longer force-enables it after a skip (screenAnalysisIntentAtCompletionseam) and only callsstartMonitoringwhen the grant exists. - Sidebar skip ≠ denied —
SBOnboardingPermissionIntentPolicy: screen-denied now requires the capture intent on (skip turns it off); accessibility records a durable onboarding skip marker (onboardingAccessibilitySkipped) so a deliberate skip stops pulsing as denied. Mic and notifications already read the real TCC answer (notifications startup auto-prompt was already fixed atstartMonitoring— verified, not regressed). - Full Disk Access — automatic indexer paths (
scheduleInitialFileIndexingbackfill and the 3-hour rescan) probe FDA and dropDocuments/Desktop/Downloadsfrom scan roots when it is missing (FileIndexScanPolicy.automaticScanRoots), so a background scan can never throw a per-folder consent sheet. Explicit Settings "Rescan files" keeps the full root set. - Chat + PTT —
request_permissionfor microphone/notifications checks the current status first: a denied grant is reported honestly (no deadrequestAccesscall, no forced System Settings jump); settings panes open only when the user just declined a fresh sheet. PTT finishes the turn with.permissionDeniedinstead of re-running the no-op request on every press.
New SBOnboardingSkipPermissionIntentTests (skip mic ⇒ mode off + restore policy refuses; allow-after-skip restores; skip screen ⇒ intent off; allow ⇒ on; completion seam never forces on for a skipped grant; AX skip marker set/cleared; sidebar projections), plus new policy cases in MicrophoneCaptureAuthorizationPolicyTests (automatic never requests/surfaces), PersistedCaptureLaunchPolicyTests (restore refuses without authorization), and FileIndexScanPolicyTests (automatic roots without FDA exclude the TCC-protected folders; with FDA they match standard roots).
Commands run (all green):
xcrun swift build -c debug --package-path Desktopxcrun swift test --package-path Desktop --filter "PersistedCaptureLaunchPolicyTests|MicrophoneCaptureAuthorizationPolicyTests|SBOnboardingSkipPermissionIntentTests|FileIndexScanPolicyTests|SBOnboardingStepTelemetryTests|SBOnboardingPermissionFlowTests|OnboardingPermissionToolTests|FileIndexerServiceTests|SettingsSyncCaptureRestorationTests|ShellListeningCycleTests|PTTWarmMicKeepAliveTests"
Not exercised live: no named-bundle run of the onboarding flow this session (the changed paths are policy seams covered by the suites above; a live TCC exercise would need interactive consent dialogs).
Failure-Class: FC-privileged-consent-resampled-on-timer
scripts/pr-preflight --suggest reports no affected product invariants for this diff.