diff --git a/apps/desktop/electron/main/live-voice/microphone-permissions.ts b/apps/desktop/electron/main/live-voice/microphone-permissions.ts index de55edc37..a3d298b6d 100644 --- a/apps/desktop/electron/main/live-voice/microphone-permissions.ts +++ b/apps/desktop/electron/main/live-voice/microphone-permissions.ts @@ -25,6 +25,15 @@ export function installLiveMicrophonePermissionHandlers(input: { hasReservation: (owner: LiveOwner) => boolean; }): void { input.targetSession.setPermissionRequestHandler((contents, permission, callback, details) => { + // Installing a handler replaces the platform default for every permission on + // the session, and this session also carries the main renderer. Keep the + // microphone policy to `media` so unrelated web APIs keep working: denying + // the rest silently broke the app's own copy buttons, which need + // `clipboard-sanitized-write`. + if (permission !== "media") { + callback(true); + return; + } const owner = requestOwner(input.getMainWindow(), contents, details.requestingUrl, details.isMainFrame); const allowed = allowsLiveMicrophonePermission({ permission, @@ -37,6 +46,8 @@ export function installLiveMicrophonePermissionHandlers(input: { }); input.targetSession.setPermissionCheckHandler((contents, permission, requestingOrigin, details) => { + // Same scope rule as the request handler above. + if (permission !== "media") return true; const owner = contents ? checkOwner(input.getMainWindow(), contents, requestingOrigin) : null; return allowsLiveMicrophonePermission({ permission, diff --git a/apps/desktop/test/live-voice-permission.test.mjs b/apps/desktop/test/live-voice-permission.test.mjs index 62637ca01..1a34283f1 100644 --- a/apps/desktop/test/live-voice-permission.test.mjs +++ b/apps/desktop/test/live-voice-permission.test.mjs @@ -54,7 +54,12 @@ test("Live Voice media permission requires the trusted main frame and an active assert.equal(requestDecision({ requestingUrl: trustedUrl, isMainFrame: false, mediaTypes: ["audio"] }), false); assert.equal(requestDecision({ requestingUrl: "https://outside.example", isMainFrame: true, mediaTypes: ["audio"] }), false); assert.equal(checkDecision({ mediaType: "audio" }, "https://outside.example"), false); - assert.equal(requestDecision({ requestingUrl: trustedUrl, isMainFrame: true, mediaTypes: ["audio"] }, "notifications"), false); + // Regression: the microphone policy must not shadow permissions it does not own. + // The default session carries the main renderer, where the transcript and table + // copy buttons rely on `clipboard-sanitized-write`; denying it broke them. + assert.equal(requestDecision({ requestingUrl: trustedUrl, isMainFrame: true, mediaTypes: ["audio"] }, "notifications"), true); + assert.equal(requestDecision({ requestingUrl: trustedUrl, isMainFrame: true, mediaTypes: ["audio"] }, "clipboard-sanitized-write"), true); + assert.equal(checkDecision({ mediaType: "unknown" }, "http://localhost:5173", "clipboard-sanitized-write"), true); assert.equal(requestDecision({ requestingUrl: trustedUrl, isMainFrame: true, mediaTypes: ["audio"] }, "media", {}), false); leaseActive = false; assert.equal(requestDecision({ requestingUrl: trustedUrl, isMainFrame: true, mediaTypes: ["audio"] }), false);