From e94c50c1661fce42b02c6e7f3ade4d787b51c177 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sat, 8 Aug 2026 10:52:21 +0800 Subject: [PATCH 1/4] fix(desktop): enable microphone access on macOS --- .github/workflows/desktop-release.yml | 6 ++++++ packages/desktop-shell/src-tauri/Entitlements.plist | 2 ++ packages/desktop-shell/src-tauri/Info.plist | 8 ++++++++ 3 files changed, 16 insertions(+) create mode 100644 packages/desktop-shell/src-tauri/Info.plist diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index b70e80b62dd..20d3e4c3267 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -387,6 +387,10 @@ jobs: app="$(find packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app' -print -quit)" codesign --verify --deep --strict --verbose=2 "$app" spctl --assess --type execute --verbose=2 "$app" + entitlements="$(mktemp)" + trap 'rm -f "$entitlements"' EXIT + codesign -d --entitlements - --xml "$app" > "$entitlements" 2>/dev/null + test "$(/usr/libexec/PlistBuddy -c 'Print :com.apple.security.device.audio-input' "$entitlements")" = true - name: 'Verify Windows signature' if: "runner.os == 'Windows' && inputs.dry_run == false" @@ -427,6 +431,8 @@ jobs: run: | set -euo pipefail executable="$(find src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -path '*.app/Contents/MacOS/*' -type f -perm -111 -print -quit)" + info_plist="$(dirname "$(dirname "$executable")")/Info.plist" + /usr/libexec/PlistBuddy -c 'Print :NSMicrophoneUsageDescription' "$info_plist" >/dev/null npm run smoke:packaged -- "$executable" - name: 'Smoke packaged application' diff --git a/packages/desktop-shell/src-tauri/Entitlements.plist b/packages/desktop-shell/src-tauri/Entitlements.plist index b49b6618def..af81c06a43a 100644 --- a/packages/desktop-shell/src-tauri/Entitlements.plist +++ b/packages/desktop-shell/src-tauri/Entitlements.plist @@ -4,6 +4,8 @@ com.apple.security.cs.allow-jit + com.apple.security.device.audio-input + com.apple.security.network.client com.apple.security.network.server diff --git a/packages/desktop-shell/src-tauri/Info.plist b/packages/desktop-shell/src-tauri/Info.plist new file mode 100644 index 00000000000..5e0d6613fa5 --- /dev/null +++ b/packages/desktop-shell/src-tauri/Info.plist @@ -0,0 +1,8 @@ + + + + + NSMicrophoneUsageDescription + Qwen Code uses the microphone for voice dictation in the prompt composer. + + From 97e0e30ae091415928bd7868970947589afd3f4e Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sat, 8 Aug 2026 21:59:55 +0800 Subject: [PATCH 2/4] fix(desktop): narrow helper entitlements --- .github/workflows/desktop-release.yml | 4 +-- .../desktop-shell/scripts/test-release.js | 25 +++++++++++++++++-- .../src-tauri/NodeEntitlements.plist | 8 ++++++ 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 packages/desktop-shell/src-tauri/NodeEntitlements.plist diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 20d3e4c3267..8e50e2ce9f0 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -347,7 +347,7 @@ jobs: find "$rg_dir" -type f -name 'rg' -path '*-darwin/*' -exec \ codesign --force --sign "$APPLE_SIGNING_IDENTITY" \ --options runtime --timestamp \ - --entitlements src-tauri/Entitlements.plist {} + + {} + else echo "::warning::Ripgrep vendor directory not found at $rg_dir; no ripgrep binaries signed." fi @@ -356,7 +356,7 @@ jobs: if [ -f "$node_bin" ]; then codesign --force --sign "$APPLE_SIGNING_IDENTITY" \ --options runtime --timestamp \ - --entitlements src-tauri/Entitlements.plist "$node_bin" + --entitlements src-tauri/NodeEntitlements.plist "$node_bin" else echo "::warning::Node.js runtime binary not found at $node_bin; no Node.js binary signed." fi diff --git a/packages/desktop-shell/scripts/test-release.js b/packages/desktop-shell/scripts/test-release.js index e250137a963..7c6fcad5082 100755 --- a/packages/desktop-shell/scripts/test-release.js +++ b/packages/desktop-shell/scripts/test-release.js @@ -111,10 +111,31 @@ function testDesktopReleaseSigningWorkflow() { ), 'Unsigned Windows installers are only allowed when no signing config exists', ); - assert.ok( - workflow.includes('--entitlements src-tauri/Entitlements.plist {} +'), + assert.doesNotMatch( + workflow, + /--entitlements src-tauri\/Entitlements\.plist \{\} \+/, + 'ripgrep must not inherit the app entitlements', + ); + assert.match( + workflow, + /--options runtime --timestamp \\\n\s+\{\} \+/, 'ripgrep codesign failures must fail the signing step', ); + assert.ok( + workflow.includes( + '--entitlements src-tauri/NodeEntitlements.plist "$node_bin"', + ), + 'Node.js must use its minimal helper entitlements', + ); + const nodeEntitlements = fs.readFileSync( + path.join(packageDir, 'src-tauri', 'NodeEntitlements.plist'), + 'utf8', + ); + assert.doesNotMatch( + nodeEntitlements, + /com\.apple\.security\.device\.audio-input/, + 'Node.js must not receive microphone access', + ); assert.match( workflow, /Ripgrep vendor directory not found at \$rg_dir/, diff --git a/packages/desktop-shell/src-tauri/NodeEntitlements.plist b/packages/desktop-shell/src-tauri/NodeEntitlements.plist new file mode 100644 index 00000000000..86611d6bc73 --- /dev/null +++ b/packages/desktop-shell/src-tauri/NodeEntitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-jit + + + From 4378146f1975ccf0e9a2eed6d54725a313b47cd3 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sun, 9 Aug 2026 13:05:50 +0800 Subject: [PATCH 3/4] test(desktop): pin macOS permission packaging --- .../desktop-shell/scripts/test-release.js | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/desktop-shell/scripts/test-release.js b/packages/desktop-shell/scripts/test-release.js index 7c6fcad5082..96ab74a6c34 100755 --- a/packages/desktop-shell/scripts/test-release.js +++ b/packages/desktop-shell/scripts/test-release.js @@ -111,9 +111,13 @@ function testDesktopReleaseSigningWorkflow() { ), 'Unsigned Windows installers are only allowed when no signing config exists', ); + const ripgrepSigningBlock = workflow.slice( + workflow.indexOf('# ripgrep vendor binaries'), + workflow.indexOf('# Node.js runtime binary'), + ); assert.doesNotMatch( - workflow, - /--entitlements src-tauri\/Entitlements\.plist \{\} \+/, + ripgrepSigningBlock, + /--entitlements/, 'ripgrep must not inherit the app entitlements', ); assert.match( @@ -131,6 +135,29 @@ function testDesktopReleaseSigningWorkflow() { path.join(packageDir, 'src-tauri', 'NodeEntitlements.plist'), 'utf8', ); + const appEntitlements = fs.readFileSync( + path.join(packageDir, 'src-tauri', 'Entitlements.plist'), + 'utf8', + ); + assert.match( + appEntitlements, + /com\.apple\.security\.device\.audio-input/, + 'the app bundle must keep microphone access for voice dictation', + ); + const infoPlist = fs.readFileSync( + path.join(packageDir, 'src-tauri', 'Info.plist'), + 'utf8', + ); + assert.match( + infoPlist, + /NSMicrophoneUsageDescription<\/key>\s*.+<\/string>/, + 'the app bundle must declare a non-empty microphone usage description', + ); + assert.match( + nodeEntitlements, + /com\.apple\.security\.cs\.allow-jit/, + 'the bundled Node.js runtime must keep its JIT entitlement', + ); assert.doesNotMatch( nodeEntitlements, /com\.apple\.security\.device\.audio-input/, @@ -146,6 +173,16 @@ function testDesktopReleaseSigningWorkflow() { /Node\.js runtime binary not found at \$node_bin/, 'missing Node.js runtime binary must be visible in release logs', ); + assert.match( + workflow, + /Print :com\.apple\.security\.device\.audio-input/, + 'the macOS signature check must keep verifying the audio-input entitlement', + ); + assert.match( + workflow, + /Print :NSMicrophoneUsageDescription/, + 'the packaged smoke must keep verifying the microphone usage description', + ); assert.ok( workflow.indexOf("name: 'Prepare bundled runtime'") < workflow.indexOf("name: 'Sign bundled vendor binaries (macOS)'"), From f294c8d80c876aee451f2e567393e67dab09c4c2 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Sun, 9 Aug 2026 16:17:38 +0800 Subject: [PATCH 4/4] test(desktop): strengthen entitlement release guards --- packages/desktop-shell/scripts/test-release.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/desktop-shell/scripts/test-release.js b/packages/desktop-shell/scripts/test-release.js index 96ab74a6c34..26dd4590738 100755 --- a/packages/desktop-shell/scripts/test-release.js +++ b/packages/desktop-shell/scripts/test-release.js @@ -111,10 +111,13 @@ function testDesktopReleaseSigningWorkflow() { ), 'Unsigned Windows installers are only allowed when no signing config exists', ); - const ripgrepSigningBlock = workflow.slice( - workflow.indexOf('# ripgrep vendor binaries'), - workflow.indexOf('# Node.js runtime binary'), + const ripgrepStart = workflow.indexOf('# ripgrep vendor binaries'); + const ripgrepEnd = workflow.indexOf('# Node.js runtime binary'); + assert.ok( + ripgrepStart !== -1 && ripgrepEnd > ripgrepStart, + 'the vendor signing step must keep its ripgrep/Node section markers', ); + const ripgrepSigningBlock = workflow.slice(ripgrepStart, ripgrepEnd); assert.doesNotMatch( ripgrepSigningBlock, /--entitlements/, @@ -141,7 +144,7 @@ function testDesktopReleaseSigningWorkflow() { ); assert.match( appEntitlements, - /com\.apple\.security\.device\.audio-input/, + /com\.apple\.security\.device\.audio-input<\/key>\s*/, 'the app bundle must keep microphone access for voice dictation', ); const infoPlist = fs.readFileSync( @@ -155,7 +158,7 @@ function testDesktopReleaseSigningWorkflow() { ); assert.match( nodeEntitlements, - /com\.apple\.security\.cs\.allow-jit/, + /com\.apple\.security\.cs\.allow-jit<\/key>\s*/, 'the bundled Node.js runtime must keep its JIT entitlement', ); assert.doesNotMatch(