-
Notifications
You must be signed in to change notification settings - Fork 3.2k
fix(desktop): enable microphone access on macOS #8715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e94c50c
4d44ed5
97e0e30
00eabce
4378146
18a812f
f294c8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
Comment on lines
+390
to
+393
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Dry-run rehearsal silently skips the app-side mic-entitlement gate: the audio-input check landed in 'Verify macOS signature' ( 中文说明干跑排练会静默跳过应用侧麦克风权限关卡:audio-input 检查放在了 'Verify macOS signature'( — DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8) |
||
|
|
||
| - 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' | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -111,10 +111,61 @@ function testDesktopReleaseSigningWorkflow() { | |||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| 'Unsigned Windows installers are only allowed when no signing config exists', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| const ripgrepStart = workflow.indexOf('# ripgrep vendor binaries'); | ||||||||||||||||||||||||||||||||||||||||
| const ripgrepEnd = workflow.indexOf('# Node.js runtime binary'); | ||||||||||||||||||||||||||||||||||||||||
| assert.ok( | ||||||||||||||||||||||||||||||||||||||||
| workflow.includes('--entitlements src-tauri/Entitlements.plist {} +'), | ||||||||||||||||||||||||||||||||||||||||
| 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/, | ||||||||||||||||||||||||||||||||||||||||
| 'ripgrep must not inherit the app entitlements', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
yiliang114 marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
| assert.match( | ||||||||||||||||||||||||||||||||||||||||
| workflow, | ||||||||||||||||||||||||||||||||||||||||
| /--options runtime --timestamp \\\n\s+\{\} \+/, | ||||||||||||||||||||||||||||||||||||||||
| 'ripgrep codesign failures must fail the signing step', | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+128
to
129
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The regex 中文说明正则 — DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8) |
||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+126
to
130
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The assertion message 'ripgrep codesign failures must fail the signing step' (carried over from the pre-diff assertion) does not match what the regex tests:
Suggested change
中文说明断言消息 "ripgrep codesign failures must fail the signing step"(沿用自改动前的断言)与正则实际测试的内容不符: — DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8) |
||||||||||||||||||||||||||||||||||||||||
| assert.ok( | ||||||||||||||||||||||||||||||||||||||||
| workflow.includes( | ||||||||||||||||||||||||||||||||||||||||
| '--entitlements src-tauri/NodeEntitlements.plist "$node_bin"', | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+131
to
+134
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The test pins the ripgrep sibling's full invocation (
Suggested change
中文说明测试固定了 ripgrep 兄弟命令的完整调用形态( — DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8) |
||||||||||||||||||||||||||||||||||||||||
| 'Node.js must use its minimal helper entitlements', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| const nodeEntitlements = fs.readFileSync( | ||||||||||||||||||||||||||||||||||||||||
| path.join(packageDir, 'src-tauri', 'NodeEntitlements.plist'), | ||||||||||||||||||||||||||||||||||||||||
| 'utf8', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
yiliang114 marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
| const appEntitlements = fs.readFileSync( | ||||||||||||||||||||||||||||||||||||||||
| path.join(packageDir, 'src-tauri', 'Entitlements.plist'), | ||||||||||||||||||||||||||||||||||||||||
| 'utf8', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| assert.match( | ||||||||||||||||||||||||||||||||||||||||
| appEntitlements, | ||||||||||||||||||||||||||||||||||||||||
| /<key>com\.apple\.security\.device\.audio-input<\/key>\s*<true\/>/, | ||||||||||||||||||||||||||||||||||||||||
| 'the app bundle must keep microphone access for voice dictation', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
yiliang114 marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
| const infoPlist = fs.readFileSync( | ||||||||||||||||||||||||||||||||||||||||
| path.join(packageDir, 'src-tauri', 'Info.plist'), | ||||||||||||||||||||||||||||||||||||||||
| 'utf8', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| assert.match( | ||||||||||||||||||||||||||||||||||||||||
| infoPlist, | ||||||||||||||||||||||||||||||||||||||||
| /NSMicrophoneUsageDescription<\/key>\s*<string>.+<\/string>/, | ||||||||||||||||||||||||||||||||||||||||
| 'the app bundle must declare a non-empty microphone usage description', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| assert.match( | ||||||||||||||||||||||||||||||||||||||||
| nodeEntitlements, | ||||||||||||||||||||||||||||||||||||||||
| /<key>com\.apple\.security\.cs\.allow-jit<\/key>\s*<true\/>/, | ||||||||||||||||||||||||||||||||||||||||
| 'the bundled Node.js runtime must keep its JIT entitlement', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| 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/, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -125,6 +176,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', | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+179
to
+183
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The regex pins only the PlistBuddy key name, not the load-bearing
Suggested change
中文说明正则只固定了 PlistBuddy 的键名,没有固定同一行上承载核心语义的 — DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8) |
||||||||||||||||||||||||||||||||||||||||
| 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)'"), | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>NSMicrophoneUsageDescription</key> | ||
| <string>Qwen Code uses the microphone for voice dictation in the prompt composer.</string> | ||
| </dict> | ||
| </plist> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>com.apple.security.cs.allow-jit</key> | ||
| <true/> | ||
| </dict> | ||
| </plist> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] Release-time verification is asymmetric: the new artifact-level assertions check only the app's main executable (audio-input entitlement here, usage description in
Contents/Info.plist). The PR's actual behavioral change — Node signed withNodeEntitlements.plist(allow-jit only), ripgrep signed with no entitlements — has no release-time artifact check; only the workflow-text/source-file unit assertions cover it. — Failure scenario: a future divergence between the source plists and the final bundle — a build step re-signing or replacing the embeddednode/rgbinaries, or the signing step dropping theNodeEntitlements.plistflag while the workflow-text test is updated in the same commit — ships Node carryingcom.apple.security.device.audio-input(or ripgrep the full app entitlements), exactly the exposure this PR exists to prevent.codesign --verify --deep --strictchecks validity, not entitlement content; every release gate stays green.中文说明
发布期验证不对称:新增的产物级断言只检查 App 主可执行文件(此处的 audio-input 权限,以及
Contents/Info.plist里的用途说明)。本 PR 真正的行为变更——Node 改用仅含 allow-jit 的NodeEntitlements.plist签名、ripgrep 不带任何权限签名——没有任何发布期产物检查,只有 workflow 文本/源文件级的单元断言在覆盖。— 失败场景:未来源 plist 与最终 bundle 出现偏差——某步构建重新签名或替换内嵌的node/rg二进制,或签名步骤丢掉NodeEntitlements.plist参数且同一提交里改了 workflow 文本测试——Node 带着com.apple.security.device.audio-input(或 ripgrep 带着完整应用权限)发布,这正是本 PR 要防止的暴露。codesign --verify --deep --strict只检查签名有效性,不检查权限内容;所有发布关卡仍然全绿。— DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8)