Repository navigation
feat(voice): support trusted private ASR base URLs - #8350
rockybot2026 merged 31 commits into
Conversation
Code ReviewOverviewAdds What looks good
Findings1. Allowlist entry normalization diverges between CLI and Desktop (Suggestion, cross-surface correctness) Desktop's 2. Desktop credential resolution now hard-fails configs that previously worked (Suggestion, behavior change)
3. Desktop requires allowlisting even for public HTTPS custom providers; CLI does not (Suggestion, consistency/naming) On Desktop, any non-DashScope provider — including a public 4. Security-guard logic is duplicated with non-identical implementations (Suggestion, maintainability)
VerdictNo Critical findings. The security posture is sound: default deny is preserved, the opt-in is exact-match and trusted-scope only, and the always-blocked classes are enforced on both literal and resolved addresses with good test coverage. Findings 1–2 are the ones I'd most like addressed (or explicitly acknowledged) before merge, since both produce silent or surprising failures for operators; 3–4 can be follow-ups. |
|
@qwen-code /takeover |
|
🚫 Takeover not engaged: fork takeover requires the PR author to hold write access on this repository (author 中文说明🚫 未接管:fork 托管要求 PR 作者在本仓库持有 write 及以上权限(作者 |
|
Addressed the review feedback in 797499a:
Final local verification: CLI focused suites 246/246, Desktop voice suites 103/103, Desktop server-core TypeScript check, root lint/typecheck/build, and pre-commit formatting/lint all pass. I also iterated three completed Qwen local |
|
@qwen-code /review |
| _Qwen Code review request accepted. Review is queued in [workflow run](https://github.com/QwenLM/qwen-code/actions/runs/30746387737)._ |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
|
🔀 Base updated: red check(s) [ubuntu-latest / Java 11] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [ubuntu-latest / Java 11] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
…#8350) Deduplicate the IPv6-transition unwrapping sequence (mapped, compatible, NAT64, dotted-quad) that was repeated verbatim between isPrivateNetworkIp and isAlwaysBlockedVoiceAddress on both CLI and Desktop surfaces. A single unwrapIpv6TransitionStep helper now yields the next canonical address (or 'blocked' for unrecognized ::ffff: forms), and each predicate recurses through it, preserving the exact re-check semantics at every unwrap level.
|
@qwen-code /triage |
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Round summaryAddressed the two remaining automated-reviewer suggestions (round 2) about the duplicated IPv6-transition unwrapping ladder in the voice network guards. Feedback points
All earlier findings (rc:3697507205 through rc:3698686266) were already resolved in prior commits (797499a, 0692403, 46c9b1a, 0741498) and re-verified as holding. Changed files
Conflict notesNo conflicts ( Verification
中文说明轮次总结处理了自动化审查器(第 2 轮)剩余的两条建议,关于语音网络守卫中重复的 IPv6 过渡机制解包阶梯。 反馈要点
所有早期发现(rc:3697507205 至 rc:3698686266)已在先前提交(797499a93、06924038d、46c9b1a91、074149854)中解决,并经重新验证仍然成立。 变更文件
冲突说明无冲突( 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action neededThe latest review round on PR #8350 contains no actionable feedback:
No code changes were made this round. 中文说明Autofix 审查轮次 — 无需操作PR #8350 的最新审查轮次不包含可操作的反馈:
本轮未做任何代码变更。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
🤖 AutoFix ran out of time before finishing (timeout (7200000ms)) (attempt 13/100) — it will retry on the next scan. What I found before stopping: See the Qwen Autofix agent step logs for model/tool output. Run log: https://github.com/QwenLM/qwen-code/actions/runs/31020135934 🧠 Handled by Qwen Code · model/模型 |
OverviewAdds The security core is genuinely good work:
Below are the things I'd want addressed. 1. Desktop: the ambiguity check fires before the public-HTTPS fall-through, breaking dictation for configs that work today
if (distinct.length > 1) {
throw new Error(`Voice model '${voiceModel}' is ambiguous. ${PROVIDER_ENTRY_REMEDY}`);
}
...
if (isPublicHttps && !allowInsecureBaseUrl) {
return undefined; // legacy fall-through
}The throw is unconditional on classification, so it runs before the entry is ever classified. That contradicts the design doc's own contract:
That guarantee only holds for the single-entry case. Concrete break — an OAuth-signed-in desktop user with two public HTTPS entries sharing the voice model id: "modelProviders": {
"openai": [{ "id": "qwen3-asr-flash", "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1", "envKey": "DASHSCOPE_API_KEY" }],
"dashscope": [{ "id": "qwen3-asr-flash", "baseUrl": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "envKey": "DASHSCOPE_API_KEY" }]
}Before this PR desktop ignored id-matching entirely and OAuth resolved. After it, dictation dies with The design doc widens this further and acknowledges it: because desktop scans provider groups protocol-agnostically while the CLI only sees groups whose id resolves to a protocol, a duplicate in a custom group with no Suggested fix: classify first, then decide. Only raise ambiguity when at least one distinct entry actually needs a policy decision (allowlisted / cleartext / private / loopback); otherwise return The same ordering applies to the Test gap: the only ambiguity test ( 2. None of the desktop changes run in CI
That means the desktop half of this PR — the 235-line The new 3.
|
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
🤖 Addressed the latest review feedback (round 14/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 14/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #8350This round triaged @wenshao's issue-level review. Critical-only mode is active, and the previous round exhausted the time budget, so per the budget rule this round fixed the smallest blocking subset (item 1) and deferred the rest with recorded reasons. There were no inline comments this round, so there are no thread resolutions or reply comments; every disposition is recorded here. Item 1 (blocking) — ambiguity throw fires before classification — FIXEDCommit
Item 2 — desktop changes run in no CI — deferred (follow-up)Adding a CI workflow for the bun workspace is infrastructure needing maintainer decisions (runner capacity, bun toolchain), not a Critical fix, and is out of scope for a Critical-only round under the budget warning. Evidence gathered this round confirms the gap: (a) the Item 3 —
|
Review:
|
|
🤖 Addressed the latest review feedback (round 15/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 15/100 轮)。改动内容与我反驳保留之处如下: Review round 11 — summaryCritical-only mode is active after 10 change-producing rounds. Per the repository's review policy, this round lands only the two Critical fixes and defers the 13 Suggestions to follow-up issues/PRs; every deferral is recorded as a reply on its own thread so nothing is dropped silently. Feedback points and decisionsCritical — fixed
The fix removes the entire ~200-line hand-rolled scanner ( Suggestions — deferred to follow-up issue/PR (Critical-only mode)
Each deferral is posted as a reply on its own thread ( Review body (CHANGES_REQUESTED)The review's "Not reviewed" notes (the desktop bun suites not running in CI — tracked by the author in #8360; the CLI integration suite skipped in CI; the reverse audit stopped by the time budget; the actionlint embedded-shell limitation; the Test Plan count observation) are coverage statements about the review itself, not code findings, and request no code change. ConflictsNone ( Verification
Commit: 中文说明第 11 轮评审 — 总结在完成 10 个产生改动的评审轮后已进入仅处理 Critical 的模式。按仓库评审策略,本轮只合入两个 Critical 修复,13 条建议(Suggestion)延后到后续 issue/PR;每条延后都会在其所属讨论串中以回复形式记录,不会被静默丢弃。 反馈点与决定Critical — 已修复
本次修复整体删除了约 200 行手写扫描器( 建议类 — 延后到后续 issue/PR(仅处理 Critical 模式)
每条延后都会在其所属讨论串中发布回复( 评审主体(CHANGES_REQUESTED)评审中的 "Not reviewed" 说明(desktop bun 套件未在 CI 中运行——作者已在 #8360 跟踪;CLI 集成套件在 CI 中被跳过;反向审计因时间预算中止;actionlint 内嵌 shell 的限制;Test Plan 计数观察)是评审自身覆盖面的陈述,不是代码发现,也不要求任何代码改动。 冲突无( 验证
提交: Deferred non-Critical feedbackCritical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (
中文说明完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round: no action takenPR: #8350 · Base: This round had no actionable feedback:
Critical-only mode is active (after 10 change-producing rounds). The non-Critical findings listed in the deferred section of this round's feedback remain open for human follow-up, per the ratchet policy; this round did not modify code, resolve threads, or post replies for them. No commits were made; the branch head is unchanged. 中文说明Autofix 审查轮次:未采取任何操作PR: #8350 · 基础分支: 本轮没有可处理的反馈:
当前处于仅处理 Critical 的模式(在 10 个产生改动的轮次之后)。本轮反馈中"已延后"部分列出的非 Critical 问题,按照棘轮策略保持开放、留待人工跟进;本轮没有为这些条目修改代码、解决讨论串或发布回复。 本轮没有产生任何提交;分支 HEAD 保持不变。 Deferred non-Critical feedbackCritical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (
中文说明完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
@qwen-code /triage |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action takenThis round found no actionable feedback on PR #8350:
The PR is in Critical-only mode after 10 change-producing rounds. The deferred non-Critical feedback listed for this round (one automated-reviewer PR comment) was intentionally left untouched per the Critical-only rules: no code change, no thread resolution, and no reply for it. It remains open for human follow-up. No code changes were made and no commits were created in this round. 中文说明Autofix 评审轮次 — 未采取任何操作本轮在 PR #8350 上未发现可处理的反馈:
该 PR 在经历 10 个产生改动的轮次后已进入仅处理 Critical 的模式。本轮列出的延后非 Critical 反馈(一条自动评审器的 PR 评论)按 Critical-only 规则有意未做处理:不为它修改代码、不解决相关话题、也不回复。该条目保持开放,留待人工跟进。 本轮未做任何代码改动,也未创建任何提交。 Deferred non-Critical feedbackCritical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (
中文说明完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
Maintainer verification — real local environment, real network egressI built this PR from source and exercised the policy against a real cleartext-HTTP ASR gateway bound to this machine's actual private LAN address ( Verdict: the contract in the PR description holds on every case I could construct. LGTM to merge, with three small notes below (one worth a follow-up line of code, two worth a sentence in the description).
1. Batch path — before vs. after, same machine, same gatewayFull matrix. ALLOW = the daemon returned a transcript that came back from the private gateway; DENY = the URL policy rejected it;
* S14 passed the URL policy on both builds and then failed at the TLS handshake (deliberately: the hostname resolves to a public IP whose certificate does not match, so the audio body is never transmitted). This confirms public HTTPS still needs no allowlist entry — the change is scoped to cleartext/private endpoints. Notable details from the gateway log on the ALLOW rows: The provider key really is on the wire in cleartext — the risk the settings description warns about is accurate, and it is worth keeping that wording prominent. S11 also exercises normalization end-to-end: the entry 2. Streaming path — daemon
|
| Command | Result |
|---|---|
vitest run on the 6 changed CLI suites |
300 passed |
npm run test:scripts → check-voice-guard-sync.test.js |
11 passed |
npm run check:voice-guard-sync |
passed |
bun test packages/server-core/src/voice |
135 passed (8 files) |
bun test ./…/voice-ws-handler.isolated.ts |
15 passed |
npm run typecheck · npm run lint:ci · desktop tsc --noEmit |
all passed |
Passing tests only prove the tests run, so I broke the implementation three ways to check the tests and the new CI gate actually bind the contract:
- Removed the
100.100.100.200metadata check from the desktop mirror only →check:voice-guard-syncfailed withisAlwaysBlockedVoiceAddress: bodies differ. The new drift gate is real, not decorative — this is the most valuable thing the PR adds beyond the feature itself. - Made the allowlist match on origin instead of the full normalized URL → 5 tests failed. Path/port/scheme exactness is pinned.
- Stopped stripping the setting from workspace settings → 2 tests failed. The trust boundary is pinned.
Notes (none blocking)
a) The remediation hint is truncated exactly where it matters, on the daemon path. The new message is 363 chars; sanitizeVoiceErrorMessage cuts at MAX_TRANSCRIPTION_ERROR_LENGTH = 200 (pre-existing). The fixed prefix before the URL is 177 chars, so only ~23 characters of URL survive. What an operator actually sees in the daemon log is:
… add its exact complete normalized URL (http://10.177.107.157:...
The HTTP client always gets the generic voice_transcription_failed, so this log line is the only place the actionable URL appears — and any realistic managed hostname is longer than 23 chars. The TUI path is unaffected (the message reaches the history item unmodified). Cheapest fix: shorten the sentence, or log the URL as its own field. Worth one follow-up commit; not a reason to hold the merge.
b) Embedded credentials in baseUrl now hard-fail (behavior change, verified A/B). main silently stripped user:pass@; this PR throws Voice model '…' baseUrl must not contain embedded credentials. I confirmed both builds on the same input. Failing loudly is the right call, but it belongs in Breaking changes / migration notes next to the Desktop credential change.
c) providerProtocol is now honored for voice model discovery — a real functional change beyond the allowlist. On main, a custom provider id mapped through providerProtocol yields availableVoiceModels: [] and 400 unknown_voice_model; with the two added providerProtocolConfig: lines it resolves normally. That is a genuine (welcome) fix, but it is invisible in the PR description and untestable from the stated test plan — worth a sentence so reviewers know it is intentional.
Not covered by this run
Desktop was verified by its unit suites and typecheck only — I did not launch the Electron app, so the ambiguous-duplicate-provider fail-closed behavior is unit-verified, not runtime-verified. Windows and Linux untested. No real vendor ASR service was contacted; the gateway is a local stand-in that speaks both wire protocols.
中文版本
维护者验证 —— 本地真实环境、真实网络出口
我从源码构建了本 PR,并把策略放到绑定在本机真实私网地址上的真实明文 HTTP ASR 网关(10.177.107.157:8791)前验证,由真实 qwen serve daemon 驱动,使用真实 16 kHz WAV 音频 —— 没有 mock fetch,也没有注入 lookup。判据是网关自己的请求日志:音频和 Authorization 头到底有没有真的离开进程。
结论:PR 描述中的契约在我能构造的每个用例上都成立,建议合并,另有三点说明(一点值得一行后续代码,两点值得在描述里补一句)。
| PR head | ce6b480(已 merge main) |
| 基线 | main @ 8fd0162 —— 同一 worktree、同一 node_modules,只替换 packages/cli/src 后重新 bundle |
| 主机 | macOS(Darwin 25.6.0)、Node v24.18.1、npm 11.16.0、Bun 1.3.14 |
| 覆盖面 | daemon POST /workspace/voice/transcribe(批量)· daemon /voice/stream WebSocket(实时)· CLI TUI 启动(设置警告) |
| 隔离 | 独立 QWEN_HOME、独立 workspace,不使用真实 provider 凭证 |
1. 批量路径 —— 同一台机器、同一网关的 before / after
ALLOW = daemon 返回了来自私网网关的转写结果;DENY = URL 策略拒绝;gw+1 = 网关确实收到了请求。所有 DENY 行的网关请求数都是 0 —— 策略在出网之前 fail closed,而不是之后。
| # | 场景 | BEFORE | AFTER |
|---|---|---|---|
| S1 | HTTP 私网 IP 网关,无白名单条目 | DENY | DENY |
| S2 | HTTP 私网 IP 网关,User 作用域精确条目 | DENY | ALLOW gw+1 |
| S3 | 仅 path 不同(/v1 → /v2) |
DENY | DENY |
| S4 | 仅端口不同 | DENY | DENY |
| S5 | 仅 scheme 不同(https) |
DENY | DENY |
| S6 | 条目只写在 Workspace 作用域(仓库自授权) | DENY | DENY |
| S7 | 条目多写了尾部斜杠(/v1///) |
DENY | ALLOW gw+1 |
| S8 | DNS 解析到私网 IP 的域名,已加白 | DENY | ALLOW gw+1 |
| S9 | DNS 解析到 loopback 的域名,已加白 | DENY | DENY |
| S10 | 云元数据地址 169.254.169.254,已加白 |
DENY | DENY |
| S11 | 私网 IP 的 IPv4-mapped IPv6 写法,已加白 | DENY | ALLOW gw+1 |
| S12 | 127.0.0.1 的 IPv4-mapped IPv6 写法,已加白 |
DENY | DENY |
| S13 | 到私网 IP 的 HTTPS,无白名单条目 | DENY | DENY |
| S14 | 公网 HTTPS provider,无白名单条目 | 策略放行* | 策略放行* |
* S14 在两个构建上都通过了 URL 策略,随后在 TLS 握手阶段失败(这是刻意设计:域名解析到一个证书不匹配的公网 IP,因此音频体从未发出)。这印证了公网 HTTPS 仍然不需要白名单条目 —— 改动只作用于明文/私网端点。
ALLOW 行上网关日志的关键信息:
host=10.177.107.157:8791 auth=Bearer sk-regional-gateway-secret-8350
model=qwen3-asr-flash format=wav base64AudioChars=110950
provider key 确实以明文出现在链路上 —— 设置项描述里的风险提示是准确的,这段措辞值得保留在显眼位置。
S11 同时端到端验证了规范化:条目 http://[::ffff:10.177.107.157]:8791/v1 与请求都规范化为 http://[::ffff:ab1:6b9d]:8791/v1,网关观察到的 Host 头也正是这个值。
2. 流式路径 —— daemon /voice/stream WebSocket
我在同一个私网网关上实现了 qwen-asr-realtime 协议(session.created → session.update/updated → input_audio_buffer.append/commit → session.finish/finished),并通过 daemon 的客户端 WebSocket 推入真实 PCM。
没有白名单条目时,根本不会发起上游连接 —— 网关看不到任何 upgrade 请求。加白后,26 个实时帧 / 83 150 字节 PCM 到达网关并返回转写结果。批量与流式两条腿给出一致的解析结果,与 PR 描述相符。
3. 信任边界 —— Workspace 作用域既被忽略,也有提示
不可信仓库在 .qwen/settings.json 中写入 security.allowedInsecureVoiceBaseUrls 无法自授权(见 S6),并且 CLI 启动时会明确告知原因(见英文部分截图)。
4. 测试套件、门禁与变异测试
| 命令 | 结果 |
|---|---|
对 6 个改动 CLI 套件执行 vitest run |
300 通过 |
npm run test:scripts → check-voice-guard-sync.test.js |
11 通过 |
npm run check:voice-guard-sync |
通过 |
bun test packages/server-core/src/voice |
135 通过(8 个文件) |
bun test ./…/voice-ws-handler.isolated.ts |
15 通过 |
npm run typecheck · npm run lint:ci · desktop tsc --noEmit |
全部通过 |
测试通过只能证明测试跑了,所以我用三种方式破坏实现,检验测试和新 CI 门禁是否真的锁住了契约:
- 只在 desktop 镜像里删掉
100.100.100.200元数据判断 →check:voice-guard-sync以isAlwaysBlockedVoiceAddress: bodies differ失败。这个新增漂移门禁是真的有效,而不是摆设 —— 这是本 PR 除功能本身之外最有价值的部分。 - 把白名单匹配从完整规范化 URL 降级为只比 origin → 5 个测试失败。path/port/scheme 的精确性被锁住了。
- 不再从 workspace 设置中剥离该项 → 2 个测试失败。信任边界被锁住了。
说明(均不阻塞合并)
a) 在 daemon 路径上,补救提示恰好在关键处被截断。 新消息长 363 字符;sanitizeVoiceErrorMessage 在 MAX_TRANSCRIPTION_ERROR_LENGTH = 200 处截断(该常量是既有的)。URL 之前的固定前缀是 177 字符,所以只有约 23 个字符的 URL 能留下。运维在 daemon 日志里实际看到的是:
… add its exact complete normalized URL (http://10.177.107.157:...
HTTP 客户端拿到的始终是通用的 voice_transcription_failed,所以这行日志是运维唯一能看到可操作 URL 的地方 —— 而任何现实中的受管域名都长于 23 个字符。TUI 路径不受影响(消息原样进入 history item)。最省事的修法:缩短这句话,或把 URL 作为独立字段输出。值得一个后续 commit,不必因此拦住合并。
b) baseUrl 内嵌凭证现在会硬失败(行为变更,已 A/B 验证)。 main 会静默剥离 user:pass@;本 PR 抛出 Voice model '…' baseUrl must not contain embedded credentials. 我在同一输入上确认了两个构建的差异。显式失败是正确取舍,但这一条应当写进破坏性变更/迁移说明,与 Desktop 凭证变更并列。
c) providerProtocol 现在会参与语音模型发现 —— 这是白名单之外的真实功能变更。 在 main 上,通过 providerProtocol 映射的自定义 provider id 会得到 availableVoiceModels: [] 和 400 unknown_voice_model;加上新增的两行 providerProtocolConfig: 后即可正常解析。这是一个真实且值得欢迎的修复,但在 PR 描述里完全看不到,按现有测试计划也测不出来 —— 建议补一句,让 reviewer 知道这是有意为之。
本次未覆盖
Desktop 仅通过单元套件和类型检查验证,我没有启动 Electron 应用,因此"重复 provider 歧义 fail closed"是单测层面验证,而非运行时验证。Windows 与 Linux 未测试。没有访问任何真实厂商 ASR 服务;网关是本地替身,同时实现了两种线上协议。
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no action takenThis round found no actionable feedback on PR #8350:
The PR is in Critical-only mode after 10 change-producing rounds. The deferred non-Critical feedback listed for this round (one maintainer PR comment — a local verification report concluding "LGTM to merge" with non-blocking notes) was intentionally left untouched per the Critical-only rules: no code change, no thread resolution, and no reply for it. It remains open for human follow-up. No code changes were made and no commits were created in this round. 中文说明Autofix 评审轮次 — 未采取任何操作本轮在 PR #8350 上未发现可处理的反馈:
该 PR 在经历 10 个产生改动的轮次后已进入仅处理 Critical 的模式。本轮列出的延后非 Critical 反馈(一条维护者的 PR 评论 —— 一份本地验证报告,结论为 "LGTM to merge",附若干非阻塞备注)按 Critical-only 规则有意未做处理:不为它修改代码、不解决相关话题、也不回复。该条目保持开放,留待人工跟进。 本轮未做任何代码改动,也未创建任何提交。 Deferred non-Critical feedbackCritical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (
中文说明完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
@qwen-code /triage |
Picks up main so `Test (ubuntu-latest, Node 22.x)` stops failing. CI checks out `refs/pull/8410/head` but runs the base branch's `ci.yml`, so main's "Check voice guard mirror sync" step ran `npm run check:voice-guard-sync` against this branch's older `package.json`, which predates that script (added in QwenLM#8350) -> `npm error Missing script`, exit 1. Nothing in this PR caused it; the branch was simply 17 commits behind. Merge resolution: both sides had added a `const reviewAddressJob` in `scripts/tests/qwen-autofix-workflow.test.js`, in different hunks, so git merged them textually into a duplicate `const` (SyntaxError at import, whole suite unloadable). Kept main's bounded slice `(?=\n {2}[a-z][a-z0-9-]*:\n|$)` and dropped this branch's older unbounded `[\s\S]*$` form — main's is strictly more general (it still allows EOF, so it keeps working while review-address is the last job, and it shrinks correctly once a job is appended after it).
Syncs 46 commits of base drift. CI's 'Check voice guard mirror sync' step runs from main and invokes `npm run check:voice-guard-sync`, a script added alongside that step in 732f4d8 (QwenLM#8350) and absent from this branch — so the check failed on missing-script, not on anything in this diff. Merged rather than rebased so existing review comments stay anchored.



What this PR does
This PR adds
security.allowedInsecureVoiceBaseUrls, an empty-by-default exact allowlist for voice provider base URLs. A matching entry lets managed deployments route voice transcription through an HTTP or private-network ASR gateway while preserving the existing default deny behavior.The exception is accepted only from User, System, and SystemDefaults settings. Workspace values are ignored with a warning. Entries must include an explicit
http://orhttps://scheme and the full provider path. Matching includes the normalized scheme, host, port, and path; only URL serialization and trailing slashes are normalized, and wildcards or hostname suffixes are not supported. When a legacy environment/OAuth input is normalized with an inferred/v1suffix, a rejection names the effective normalized URL that must be allowlisted; the allowlist itself still never infers missing path segments.The same resolved decision is enforced across CLI batch transcription, CLI/daemon streaming, and Desktop batch/streaming. Known metadata, link-local, unspecified, and mapped-loopback addresses remain blocked even when listed. Desktop also resolves credentials from exactly one provider matching the selected voice model and fails closed on ambiguous or malformed entries. Public HTTPS custom providers do not require an insecure allowlist entry; cleartext or private-network endpoints still require an exact match.
Why it's needed
Managed deployments often expose regional ASR services through isolated APIG/VPC endpoints whose hostnames differ by region. The current unconditional HTTPS/public-network checks reject these endpoints, and a hard-coded vendor or region hostname list would not scale. A trusted exact-URL policy lets operators declaratively own each regional endpoint without weakening defaults for ordinary users.
Reviewer Test Plan
How to verify
baseUrlandsecurity.allowedInsecureVoiceBaseUrls, select that provider ID asvoiceModel, and confirm voice configuration resolves with the private-network opt-in.Locally verified commands:
Results: CLI focused suites 247/247 passed; Desktop voice suites 109/109 passed; Desktop server-core typecheck passed; root lint, workspace typecheck, and build passed; repository pre-commit formatting and lint hooks passed. All eight GitHub Qwen inline suggestions were addressed and resolved. A verified local medium review then found two mutation-test coverage gaps; both were fixed, and the completed follow-up low-effort review of the final diff reports Findings: None.
Evidence (Before & After)
Before: a non-loopback HTTP voice endpoint or a hostname resolving to a private address is rejected unconditionally.
After: the endpoint is accepted only when its complete normalized URL is explicitly listed in trusted configuration. Non-matches and protected address classes continue to fail closed.
N/A — this is settings and network-policy behavior with no visual/TUI change.
Tested on
Environment (optional)
macOS 26.4, Node.js v24.10.0, npm 11.6.1, Bun 1.3.11. Unit tests use injected lookups and no real network or credentials.
Risk & Scope
npm run preflightwas attempted, but its aggregate test phase was affected by a machine-level Aone Git hook in temporary-repository tests plus unrelated baseline/flaky failures; changed-path suites were rerun with sanitized Git configuration and passed.Linked Issues
Relates to #8286.
中文说明
本 PR 做了什么
本 PR 新增
security.allowedInsecureVoiceBaseUrls:一个默认空的语音 provider base URL 精确白名单。匹配后,受管部署可以通过 HTTP 或私网 ASR 网关转发语音转写,同时保持现有默认拒绝行为。该例外仅接受 User、System 和 SystemDefaults 作用域配置。Workspace 中的值会被忽略并产生警告。条目必须包含显式
http://或https://scheme 和完整 provider path。匹配包含规范化后的 scheme、host、port 和 path;仅 URL 序列化和尾部斜杠会被规范化,不支持通配符或域名后缀匹配。旧环境变量/OAuth 输入若被自动补上/v1,拒绝信息会展示实际需要加入白名单的规范化 URL;白名单本身仍不会推断缺失 path。同一个解析结果会一致应用到 CLI 批量转写、CLI/daemon 流式转写以及 Desktop 批量/流式转写。即使被列入白名单,已知元数据、link-local、未指定地址和映射 loopback 地址仍会被阻断。Desktop 还会从与所选语音模型精确匹配的唯一 provider 解析凭证,并对歧义或畸形配置 fail closed。公网 HTTPS 自定义 provider 无需不安全白名单条目;明文或私网端点仍要求精确匹配。
为什么需要
受管部署通常通过隔离的 APIG/VPC 端点暴露区域 ASR 服务,而且不同区域使用不同域名。当前无条件 HTTPS/公网检查会拒绝这些端点,硬编码厂商或区域域名列表也无法扩展。可信的完整 URL 精确策略允许运维方以声明式方式管理每个区域端点,同时不削弱普通用户的默认安全策略。
Reviewer 测试计划
如何验证
baseUrl和security.allowedInsecureVoiceBaseUrls中同时填写一个 HTTP 私网网关 URL,将该 provider ID 选为voiceModel,确认语音配置解析出私网例外。本地验证命令与英文部分相同。结果:CLI 定向套件 247/247 通过;Desktop voice 套件 109/109 通过;Desktop server-core 类型检查通过;根目录 lint、workspace typecheck 和 build 通过;仓库 pre-commit 格式化和 lint hook 通过。GitHub Qwen 的 8 条行内建议均已修复并 resolve;随后本地 verified medium review 发现 2 个 mutation 测试覆盖缺口,也已修复,最终 diff 的后续 low-effort review 报告 Findings: None。
证据(Before & After)
Before:非 loopback HTTP 语音端点或解析到私网地址的域名会被无条件拒绝。
After:仅当完整规范化 URL 在可信配置中被明确列出时才接受该端点。非匹配项和受保护地址类别仍然 fail closed。
N/A——这是设置和网络策略行为,没有可视化/TUI 变化。
测试平台
环境
macOS 26.4、Node.js v24.10.0、npm 11.6.1、Bun 1.3.11。单元测试使用注入的 lookup,不访问真实网络或凭证。
风险与范围
npm run preflight,但聚合测试阶段受到机器级 Aone Git hook 注入临时仓库测试以及无关基线/偶发失败影响;改动相关套件已在隔离 Git 配置下重跑并通过。关联 Issue
关联 #8286。