From 47cb4ea1fecc94cc8e1bff0177ff37f810ebe560 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 28 Jul 2026 13:40:44 -0500 Subject: [PATCH 1/2] fix(desktop): allow camera capture through the permission handlers The session permission hooks were written for the voice composer and denied video outright, so any renderer getUserMedia({video}) failed with NotAllowedError before the OS was ever consulted. Rename isAudioCapturePermission to isMediaCapturePermission and accept video alongside audio in both the request and check handlers. The OS capture permission still applies, so the user keeps a real allow/deny. --- apps/desktop/electron/main.ts | 51 +++++++++++++++++------------------ 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index dfb4747f0392..81ddfe2f5bc8 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -5510,18 +5510,22 @@ function installContextMenu(window) { }) } -// Microphone capture for the voice composer. The renderer drives mic access +// Microphone and camera capture. The voice composer drives mic access and +// renderer features (e.g. desktop plugins) can drive camera access, both // through getUserMedia, which Chromium gates behind these two session hooks. // // The naive `details.mediaTypes.includes('audio')` check works on macOS but -// breaks on Windows: Chromium frequently fires the mic permission request with -// an empty/undefined `mediaTypes`, so the strict check denies it and -// getUserMedia throws NotAllowedError ("Microphone permission was denied"). -// We therefore treat an audio-capture request as allowed whenever it's the -// 'media'/'audioCapture' permission AND mediaTypes either includes 'audio' OR -// is empty/absent (the Windows case). Video is still denied. -function isAudioCapturePermission(permission, details) { - if (permission === 'audioCapture') { +// breaks on Windows: Chromium frequently fires the request with an empty or +// undefined `mediaTypes`, so a strict check denies it and getUserMedia throws +// NotAllowedError. We therefore allow the capture permissions and treat absent +// metadata as allowed. +// +// Granting here is not the last gate: the OS still applies its own capture +// permission (macOS TCC prompts on first use, per the NSMicrophone/NSCamera +// usage strings), so the user keeps a real allow/deny and can revoke it in +// System Settings afterwards. +function isMediaCapturePermission(permission, details) { + if (permission === 'audioCapture' || permission === 'videoCapture') { return true } @@ -5531,38 +5535,31 @@ function isAudioCapturePermission(permission, details) { const mediaTypes = details?.mediaTypes + // Windows: mediaTypes is often empty for a capture request. Don't deny on + // missing metadata. if (!Array.isArray(mediaTypes) || mediaTypes.length === 0) { - // Windows: mediaTypes is often empty for a mic request. Don't deny on - // missing metadata. (A video request would carry mediaTypes:['video'].) return true } - return mediaTypes.includes('audio') && !mediaTypes.includes('video') + return mediaTypes.includes('audio') || mediaTypes.includes('video') } function installMediaPermissions() { // Async request handler: the prompt-style path (most platforms). session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback, details) => { - callback(isAudioCapturePermission(permission, details)) + callback(isMediaCapturePermission(permission, details)) }) // Synchronous check handler: Chromium consults this for getUserMedia on // Windows in addition to (or instead of) the request handler. Without it, - // the check defaults to false and the mic is denied before the request + // the check defaults to false and capture is denied before the request // handler ever runs. - session.defaultSession.setPermissionCheckHandler((_webContents, permission, _origin, details) => { - if (permission === 'media' || permission === ('audioCapture' as any) /* todo: is this needed? */) { - // details.mediaType is a single string here (not the mediaTypes array). - const mediaType = details?.mediaType - - if (mediaType === 'video') { - return false - } - - return true - } - - return false + session.defaultSession.setPermissionCheckHandler((_webContents, permission) => { + return ( + permission === 'media' || + permission === ('audioCapture' as any) /* todo: is this needed? */ || + permission === ('videoCapture' as any) + ) }) } From eaf30a4de79ab1ec916bd7a5162353ec7787d5d4 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 28 Jul 2026 13:40:44 -0500 Subject: [PATCH 2/2] build(desktop): declare camera usage for signed macOS builds A hardened-runtime build needs the camera entitlement and an NSCameraUsageDescription string, or the packaged app is killed on first camera access instead of prompting. --- apps/desktop/electron/entitlements.mac.inherit.plist | 2 ++ apps/desktop/electron/entitlements.mac.plist | 2 ++ apps/desktop/package.json | 1 + 3 files changed, 5 insertions(+) diff --git a/apps/desktop/electron/entitlements.mac.inherit.plist b/apps/desktop/electron/entitlements.mac.inherit.plist index 53fdf0fc4370..a3defc5f8cfa 100644 --- a/apps/desktop/electron/entitlements.mac.inherit.plist +++ b/apps/desktop/electron/entitlements.mac.inherit.plist @@ -10,5 +10,7 @@ com.apple.security.device.audio-input + com.apple.security.device.camera + diff --git a/apps/desktop/electron/entitlements.mac.plist b/apps/desktop/electron/entitlements.mac.plist index 53fdf0fc4370..a3defc5f8cfa 100644 --- a/apps/desktop/electron/entitlements.mac.plist +++ b/apps/desktop/electron/entitlements.mac.plist @@ -10,5 +10,7 @@ com.apple.security.device.audio-input + com.apple.security.device.camera + diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 91e51e6fbf02..01f0e45eb7b0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -220,6 +220,7 @@ "CFBundleExecutable": "Hermes", "CFBundleName": "Hermes", "NSAudioCaptureUsageDescription": "Hermes uses audio capture for voice conversations.", + "NSCameraUsageDescription": "Hermes uses the camera when a plugin or feature you enable requests it.", "NSMicrophoneUsageDescription": "Hermes uses the microphone for voice input and voice conversations." }, "gatekeeperAssess": false,