mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 12:47:42 +02:00
[eric] voice: mic entitlement + TCC prompt, the reason prod dictation died silently while dev worked
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user