[eric] voice: mic entitlement + TCC prompt, the reason prod dictation died silently while dev worked

This commit is contained in:
ciregenz
2026-08-03 22:54:53 -07:00
parent dcffaa5067
commit eec6e5d7dc
6 changed files with 23 additions and 1 deletions
@@ -16,5 +16,8 @@
<true/>
<key>com.apple.security.inherit</key>
<true/>
<!-- Renderer helpers do the actual getUserMedia capture, so they need the mic entitlement too. -->
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
+3
View File
@@ -16,6 +16,9 @@
<true/>
<key>com.apple.security.inherit</key>
<true/>
<!-- Dictation: without this, a hardened-runtime signed build denies getUserMedia mic capture with NO TCC prompt, which is exactly how prod dictation stayed dead while dev worked (ENG-103). -->
<key>com.apple.security.device.audio-input</key>
<true/>
<!-- Secure-Enclave WebAuthn credential storage (Touch ID passkeys). Authorized by the embedded Developer ID provisioning profile (Y26NUZH4NG.* wildcard); must match the group passed to app.configureWebAuthn. Main app only, NOT the helper-inherit file. -->
<key>keychain-access-groups</key>
<array>
+2 -1
View File
@@ -62,7 +62,8 @@
"hardenedRuntime": true,
"notarize": false,
"extendInfo": {
"NSFaceIDUsageDescription": "OpenSwarm uses Touch ID to sign you in to websites with passkeys."
"NSFaceIDUsageDescription": "OpenSwarm uses Touch ID to sign you in to websites with passkeys.",
"NSMicrophoneUsageDescription": "OpenSwarm uses the microphone for voice dictation."
},
"provisioningProfile": "build/embedded.provisionprofile",
"entitlements": "build/entitlements.mac.plist",
+1
View File
@@ -93,6 +93,7 @@ contextBridge.exposeInMainWorld('openswarm', {
setVoiceHotkey: (combo) => ipcRenderer.send('voice:set-hotkey', combo),
voiceHoldCapable: () => ipcRenderer.invoke('voice:hold-capable'),
voiceRequestHoldPermission: () => ipcRenderer.invoke('voice:request-hold-permission'),
voiceRequestMicAccess: () => ipcRenderer.invoke('voice:request-mic-access'),
haptic: (pattern) => ipcRenderer.invoke('haptic:perform', pattern),
// Native-tap hold relay: real global key-down/key-up for the voice combo, focus-independent.
onVoiceHold: (onDown, onUp) => {
+11
View File
@@ -198,6 +198,17 @@ function installVoiceHotkey(getMainWindow) {
}
return tapProven;
});
// Fires the real TCC mic prompt BEFORE the first capture: with the entitlement present but no
// prior grant, getUserMedia would still fail once and burn the user's first dictation attempt.
ipcMain.handle('voice:request-mic-access', async () => {
if (process.platform !== 'darwin') return true;
try {
if (systemPreferences.getMediaAccessStatus('microphone') === 'granted') return true;
return await systemPreferences.askForMediaAccess('microphone');
} catch (_) {
return false;
}
});
}
module.exports = { installVoiceHotkey };
@@ -105,6 +105,9 @@ export function useVoiceDictation() {
if (!window.openswarm?.voiceTranscribe) { setError('desktop-only'); return; } // no Electron bridge = web build
setError(null);
try {
// Fire the OS mic prompt through the main process first: a packaged hardened-runtime build denies renderer getUserMedia outright until TCC granted (the prod dictation-dead cause, ENG-103).
const micOk = await (window.openswarm as any)?.voiceRequestMicAccess?.() ?? true;
if (micOk === false) { setError('mic-denied'); return; }
const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true } });
const ctx = new AudioContext({ sampleRate: VOICE_SAMPLE_RATE });
const source = ctx.createMediaStreamSource(stream);