Skip to content

feat(voice): support trusted private ASR base URLs - #8350

Merged
rockybot2026 merged 31 commits into
QwenLM:mainfrom
rockybot2026:feat/voice-private-base-url-allowlist
Aug 6, 2026
Merged

feat(voice): support trusted private ASR base URLs#8350
rockybot2026 merged 31 commits into
QwenLM:mainfrom
rockybot2026:feat/voice-private-base-url-allowlist

Conversation

@rockybot2026

@rockybot2026 rockybot2026 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

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:// or https:// 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 /v1 suffix, 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

  1. Put an HTTP private gateway URL in both a User/System provider baseUrl and security.allowedInsecureVoiceBaseUrls, select that provider ID as voiceModel, and confirm voice configuration resolves with the private-network opt-in.
  2. Remove the allowlist entry or change its scheme, host, port, or path and confirm resolution fails closed.
  3. Put the setting in Workspace scope and confirm it is ignored with a settings warning.
  4. Try unspecified, link-local, cloud metadata, mapped-loopback, and compact or fully expanded IPv4-compatible IPv6 forms and confirm they remain blocked.
  5. Configure duplicate providers with the selected voice model ID and confirm Desktop rejects the ambiguous credentials.

Locally verified commands:

cd packages/cli && npx vitest run src/config/settings.test.ts src/serve/routes/workspace-voice.test.ts src/ui/voice/voice-transcriber.test.ts
cd packages/desktop && bun test packages/server-core/src/voice
cd packages/desktop && bunx tsc -p packages/server-core/tsconfig.json --noEmit
npm run lint
npm run typecheck
npm run build

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

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally
🐧 Linux ⚠️ not tested locally

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

  • Main risk or tradeoff: an administrator can opt a voice endpoint into cleartext/private-network egress. This is constrained to trusted scopes, exact full-URL matches, and voice-only traffic; metadata/link-local/unspecified addresses remain blocked.
  • Not validated / out of scope: no live ASR request was sent because it requires a private gateway and credentials; Windows and Linux were not tested locally and are left to CI. Full npm run preflight was 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.
  • Breaking changes / migration notes: the allowlist still defaults to empty. Desktop now treats a provider whose ID exactly matches the selected voice model as authoritative; duplicate or incomplete entries and unresolved keys fail instead of silently falling back to environment credentials. Complete or remove such an entry to restore the legacy DashScope/environment fallback.
  • PR size: the diff is slightly above the 1,200-line guideline because the same security contract is covered across CLI, daemon, Desktop, schema, documentation, and regression tests. Splitting it would temporarily leave egress paths with inconsistent policy.

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 测试计划

如何验证

  1. 在 User/System provider 的 baseUrlsecurity.allowedInsecureVoiceBaseUrls 中同时填写一个 HTTP 私网网关 URL,将该 provider ID 选为 voiceModel,确认语音配置解析出私网例外。
  2. 删除白名单条目,或修改其 scheme、host、port、path,确认解析 fail closed。
  3. 将该设置放入 Workspace 作用域,确认它被忽略并产生设置警告。
  4. 尝试未指定地址、link-local、云元数据、映射 loopback,以及紧凑或完全展开的 IPv4-compatible IPv6 形式,确认它们仍被阻断。
  5. 配置两个与所选语音模型 ID 相同的 provider,确认 Desktop 拒绝歧义凭证。

本地验证命令与英文部分相同。结果: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 变化。

测试平台

OS 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 本地未测试
🐧 Linux ⚠️ 本地未测试

环境

macOS 26.4、Node.js v24.10.0、npm 11.6.1、Bun 1.3.11。单元测试使用注入的 lookup,不访问真实网络或凭证。

风险与范围

  • 主要风险或取舍:管理员可以允许语音端点使用明文/私网出口。该能力仅限可信作用域、完整 URL 精确匹配和语音流量;元数据、link-local、未指定地址仍被阻断。
  • 未验证/范围外:未发送真实 ASR 请求,因为这需要私网网关和凭证;Windows 和 Linux 未在本地测试,交由 CI。已尝试完整 npm run preflight,但聚合测试阶段受到机器级 Aone Git hook 注入临时仓库测试以及无关基线/偶发失败影响;改动相关套件已在隔离 Git 配置下重跑并通过。
  • 破坏性变更/迁移说明:白名单仍默认空。Desktop 现在将 ID 与所选语音模型精确匹配的 provider 视为权威配置;重复或不完整条目、无法解析的 key 会直接失败,不再静默回退到环境凭证。补全或移除该条目后可恢复原 DashScope/环境回退。
  • PR 规模:diff 略高于 1,200 行建议值,因为同一安全契约需要同时覆盖 CLI、daemon、Desktop、schema、文档和回归测试。拆分会暂时造成不同出口路径的策略不一致。

关联 Issue

关联 #8286

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Overview

Adds security.allowedInsecureVoiceBaseUrls, an empty-by-default exact-match allowlist that lets trusted (User/System/SystemDefaults) configuration opt a voice provider base URL into HTTP and/or private-network egress. The resolved allowInsecureBaseUrl flag travels with the voice config and is enforced at every guard call site (CLI batch transcribeVoiceAudio, daemon streaming voice-ws.ts, Desktop batch/streaming via assertVoiceConfigNetworkAllowed). Workspace-scoped values are stripped before merge (extending the existing allowPrivateNetworkHooks mechanism) and surfaced as a settings warning. Metadata, link-local, unspecified, and mapped/compat loopback addresses stay blocked even when allowlisted.

What looks good

  • The trust model is right: workspace stripping reuses the proven stripWorkspacePrivateNetworkHooks path (now stripWorkspaceSecurityBypasses), with tests for user-scope honor, workspace-scope strip + warning, and user-over-workspace precedence.
  • I verified the new allowInsecureBaseUrl field is consumed at every guard call site on both surfaces — not a dead switch.
  • Always-blocked classes (loopback aliases, ::ffff: mapped forms, 0.0.0.0/::, 169.254/16, 100.100.100.200, AWS IMDSv6 fd00:ec2::254, fe80::/10) are enforced both on IP-literal base URLs and on DNS results, with thorough tests including compact IPv4-compatible IPv6 forms.
  • Desktop test DI (readNoJson when readQwenJson is injected) correctly prevents tests from bleeding into the host machine's real system settings, and the desktop system/system-defaults paths + env overrides match the CLI's storage-paths-lite.ts exactly.
  • Docs, CLI settings schema, and the VS Code companion JSON schema are all updated consistently.

Findings

1. Allowlist entry normalization diverges between CLI and Desktop (Suggestion, cross-surface correctness)

Desktop's normalizeAllowedVoiceBaseUrl (resolve-voice-config.ts) runs entries through normalizeBaseUrl(raw), which defaults the scheme to https:// and appends /v1. The CLI's normalizeAllowedVoiceBaseUrl (services/voice-transcriber.ts) only URL-parses and trims trailing slashes — a scheme-less entry throws in new URL() and is silently dropped, and /v1 is never appended. So the same settings.json entry voice.internal.example/v1 or http://voice.internal.example is honored on Desktop but ignored by the CLI, and the failure mode is a generic "must use an https baseUrl" error with no hint that the allowlist entry didn't match. Suggest: (a) document that entries must be complete URLs with explicit scheme and full path (the docs table currently says "exact normalized" without defining normalization), and (b) emit a settings warning for entries that fail to parse instead of dropping them silently. Aligning the two normalizers would be even better.

2. Desktop credential resolution now hard-fails configs that previously worked (Suggestion, behavior change)

fromQwenSettings previously scanned for any DashScope-compatible provider with a resolvable key and fell back to env credentials otherwise. It now throws for an id-matching provider that is incomplete (missing baseUrl/envKey, or unresolved key) and for duplicate ids, and the throw short-circuits the ?? fromEnv(...) fallback. An existing user with a provider entry whose id happens to equal the selected voice model but who relies on DASHSCOPE_API_KEY from the environment regresses from working → error. The fail-closed behavior is stated in the PR description, so this may be intended — but consider limiting the hard fail to non-DashScope/allowlisted endpoints, or at least calling the break out in release notes.

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 https:// endpoint — must appear in security.allowedInsecureVoiceBaseUrls ("uses a custom baseUrl that is not listed…"), while the CLI accepts a public HTTPS custom baseUrl without any listing. Besides the cross-surface inconsistency, gating a perfectly secure endpoint behind a setting named allowedInsecureVoiceBaseUrls is confusing for operators. Worth either aligning the surfaces or documenting the Desktop-only rule explicitly.

4. Security-guard logic is duplicated with non-identical implementations (Suggestion, maintainability)

isAlwaysBlockedVoiceAddress, isAwsIpv6MetadataAddress, and readIpv4CompatibleIpv6 now exist in two hand-maintained copies (CLI services/voice-transcriber.ts vs desktop net-guard.ts) that already differ: desktop's trailing dotted-quad regex catches embedded-IPv4 forms like 64:ff9b::169.254.169.254 (NAT64-prefixed metadata), while the CLI's ^::ffff:/^::-anchored patterns do not; the hex NAT64 form (64:ff9b::a9fe:a9fe) is missed by both. The link-local checks are written differently (>= 0xfe80 && <= 0xfebf vs (h & 0xffc0) === 0xfe80) even though currently equivalent. For a security guard this duplication is a drift hazard — consider extracting a shared module, or at minimum mirroring the two test suites so a divergence fails CI. Blocking 64:ff9b::/96-embedded always-blocked addresses on both surfaces would close the remaining niche gap.

Verdict

No 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-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Gate re-run after @wenshao's hands-on verification and approval — no commits since the last pass, so this re-evaluates the same head (ce6b4803).

Template ✓ — all required sections present; Before/After is a justified N/A (settings/network-policy behavior, no TUI surface); bilingual body complete.

Problem. Feature request, not a bug fix — no reproduction required. Linked issue #8286 (same author, type/feature-request + category/security + need-discussion) is real for managed deployments: regional private ASR gateways fail the unconditional HTTPS/public-network checks, and a hard-coded vendor/region host list would not scale.

Direction. This deliberately widens a security-sensitive default, so it was escalated for maintainer awareness early (Stage 0). That escalation is now resolved the strongest way it can be: @wenshao built the PR from source, drove a real cleartext-HTTP gateway on a private LAN through a 14-scenario batch matrix, the streaming WebSocket path, and the Workspace trust boundary, mutation-tested the new drift gate, and submitted an approving review pinned to this exact commit.

Size. Core paths touched (cli config/services + desktop server-core, cross-package). Production runtime ≈ 1,306 lines (CLI 355 / desktop server-core 951) + 232 lines of drift-guard script + ~250 tooling/config; tests ≈ 3,686 lines; docs 77; schema 8. Above the 1,000-line advisory — the PR body's split argument (one security contract across all egress paths; splitting would temporarily leave paths with inconsistent policy) is reasonable.

Approach. Matches the independent proposal from the prior pass: allowlist stripped at the workspace merge boundary, one exact normalized-URL decision made at config resolution and carried to every egress path, always-blocked address classes unaffected by the allowlist, mirrored desktop policy with a mechanical AST-based sync gate in CI (stronger than the comment-sync I would have settled for). The desktop resolver grew a per-model-ID authoritative-provider credential chain on top of the policy mirror — that is the price of desktop not importing CLI settings code, and it is covered by tests plus the maintainer's runtime verification.

Risk. No Stage 1e high-risk-path matches. The deeper-review conditions from earlier rounds are all satisfied by now: full CI evidence in the Stage 2 comment, sandboxed /verify round 2 green at this exact SHA (153/153 scripted assertions), and the maintainer's real-egress verification.

Moving on to the code review result below. 🔍

中文说明

@wenshao 完成实机验证并批准后重跑门禁 —— 上次评审后没有新 commit,本次仍针对同一 head(ce6b4803)重新评估。

模板 ✓ —— 必填章节齐全;Before/After 属合理的 N/A(设置/网络策略行为,无 TUI 界面变化);双语正文完整。

问题。 这是功能请求而非 bug 修复——不需要复现。关联 issue #8286(同一作者,type/feature-request + category/security + need-discussion)对受管部署是真实需求:区域私有 ASR 网关过不了无条件的 HTTPS/公网检查,而硬编码厂商/区域主机名列表无法扩展。

方向。 本 PR 有意放宽了一个安全敏感的默认值,因此在 Stage 0 就升级为维护者知悉。该升级现在以最强方式闭环:@wenshao 从源码构建了本 PR,用私有局域网上的真实明文 HTTP 网关跑了 14 场景批量矩阵、流式 WebSocket 路径和 Workspace 信任边界,并对新的漂移门禁做了变异测试,随后在指向本 commit 的评审中批准。

规模。 触及核心路径(cli config/services + desktop server-core,跨包)。生产运行时代码约 1,306 行(CLI 355 / desktop server-core 951)+ 232 行漂移门禁脚本 + 约 250 行工具/配置;测试约 3,686 行;文档 77;schema 8。超过 1,000 行建议线 —— PR 正文的拆分论证(同一安全契约覆盖全部出口路径,拆分会暂时留下策略不一致的出口)合理。

方案。 与上一轮独立提案一致:白名单在 workspace 合并边界被剥离、规范化 URL 精确匹配在配置解析时一次决定并传递到每个出口路径、永久封禁地址类不受白名单影响、desktop 镜像策略 + CI 中基于 AST 的机械同步门禁(比我原本会接受的注释同步更强)。desktop 解析器在策略镜像之上长出了按模型 ID 的权威 provider 凭证链——这是 desktop 无法导入 CLI 设置代码的代价,已有测试 + 维护者运行时验证覆盖。

风险。 未命中 Stage 1e 高风险路径。此前各轮要求的更深 review 条件现已全部满足:Stage 2 评论中的完整 CI 证据、在本 SHA 上通过的沙箱 /verify 第 2 轮(153/153 脚本断言)、以及维护者的真实出口验证。

进入下方代码审查结果。🔍

Qwen Code · qwen3.8-max

Reviewed at ce6b48032ca7b2c68724e2183e37e3f7dd474ed8 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Code review (re-run at ce6b4803, post-approval)

Fresh read of the full diff at this SHA, independently of the 15 earlier rounds. No new blockers found — the implementation matches the contract in the PR description, and it matches what @wenshao's real-environment verification observed. What I checked this pass:

  • Trust boundary. security.allowedInsecureVoiceBaseUrls is stripped from Workspace scope at merge time (generalized stripWorkspaceSecurityBypasses, same mechanism as allowPrivateNetworkHooks) with a startup warning; only User/System/SystemDefaults survive. Tests pin the subtle cases too — a workspace entry can't shadow a user entry, and a System-scope empty list revokes user entries.
  • One decision, every egress path. The allowlist match happens once at config resolution (isInsecureVoiceBaseUrlAllowed, exact normalized-URL equality incl. scheme/host/port/path) and the resulting flag rides the resolved config into all four guard call sites: CLI batch transcribeVoiceAudio, daemon streaming (serve/voice/voice-ws.ts), TUI streaming (InputPrompt.tsx), and desktop's batch + streaming defaults (voice-ws-handler.ts). I verified each call site passes the resolved config, not a bare URL.
  • Always-blocked classes stay blocked under the allowlist. Metadata (incl. fd00:ec2::254 and 100.100.100.200), link-local, unspecified, loopback — including DNS-to-loopback results and IPv4-mapped/compatible/NAT64 spellings — are re-checked at egress regardless of the flag; malformed ::ffff: forms fail closed. Transition ranges (local-use NAT64, IETF-assignment/Teredo container, 6to4) are blocked as unwrap-bypass channels.
  • Hardening along the way. Object.hasOwn on env-key lookup (a constructor-style envKey can't reach .trim() as a function), embedded-credentials baseUrl now hard-fails instead of silently stripping userinfo, prototype-pollution keys dropped in the desktop settings merge.
  • The CLI/desktop twin. net-guard.ts mirrors the CLI classification unit-for-unit, and the new check-voice-guard-sync.js compares them as canonicalized ASTs in CI (full profile) — drift here fails the build instead of failing open. The desktop resolver's trusted-settings merge, dotenv grammar, and storage paths are shape-different mirrors pinned by parity tests instead, as documented.

Standing non-blockers (all previously disclosed, unchanged):

  • @wenshao's note (a): the daemon's 200-char error truncation clips the actionable URL in the remediation message (~23 chars survive). Worth the follow-up line of code; left open for human follow-up by the autofix Critical-only mode.
  • Notes (b)/(c): the embedded-credentials hard-fail and the providerProtocol voice-model visibility change are both documented in the design doc and covered by tests, but still deserve a sentence each in the PR body's breaking-changes/migration section.
  • The desktop bun voice suites still run outside every CI job; the new drift gate and the CI-green CLI suites mitigate but don't replace them. not verified here: desktop bun suites + Electron runtime (this run never executes PR code; the maintainer ran the bun suites locally — 135 + 15 passing — and explicitly notes Electron itself was not launched). Windows/Linux: author and maintainer tested macOS; other platforms left to CI (builds green below).

Testing

This is an unattended CI run — I did not build or execute any PR code. The evidence below is the PR's own CI on the reviewed commit, read through the API, plus two already-completed verification layers attributed to their owners:

  • Sandboxed /verify (round 2) — ✅ merge-ready at this exact SHA (ce6b4803): 153/153 scripted mock-free assertions, targeted CLI gate 300/300. Advisory evidence, not a review.
  • Maintainer verification (@wenshao, not bot-executed): real cleartext-HTTP ASR gateway on a private LAN, real qwen serve daemon, real audio — 14-scenario before/after batch matrix (every DENY row produced zero gateway requests), streaming WebSocket path, Workspace-scope self-grant denial surfaced as a settings warning, plus three mutation tests confirming the suites and the drift gate actually pin the contract. Concluded "LGTM to merge".

CI on ce6b4803: every check that ran succeeded — 0 failures, 0 pending (84 skipped checks are conditional orchestration jobs). The check:voice-guard-sync drift gate runs as a step of the green Qwen Code CI workflow.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
Build Qwen Live Host ✅ success
Live Host (macos-latest) ✅ success
Prepare Live Host release ✅ success
Java builds (ubuntu 11/17/21, macOS 21, windows 21) ✅ success
precheck-pr / Classify PR / review-pr ✅ success
中文说明

代码审查(在 ce6b4803 上重跑,批准后)

在本 SHA 上对完整 diff 重新做了一遍独立审读(不依赖此前 15 轮结论)。未发现新的阻断问题——实现与 PR 描述中的契约一致,也与 @wenshao 实机验证观察到的行为一致。本轮核对内容:

  • 信任边界。 security.allowedInsecureVoiceBaseUrls 在合并阶段从 Workspace 作用域剥离(泛化后的 stripWorkspaceSecurityBypasses,与 allowPrivateNetworkHooks 同机制),启动时给出警告;仅 User/System/SystemDefaults 生效。测试也锁住了细节场景——workspace 条目无法覆盖 user 条目,System 作用域的空列表可以撤销 user 条目。
  • 一次决定、全出口执行。 白名单匹配只在配置解析时发生一次(isInsecureVoiceBaseUrlAllowed,规范化 URL 全等,含协议/主机/端口/路径),结果标志随解析后的配置进入全部四个守卫调用点:CLI 批量 transcribeVoiceAudio、daemon 流式(serve/voice/voice-ws.ts)、TUI 流式(InputPrompt.tsx)、desktop 批量+流式默认路径(voice-ws-handler.ts)。逐一核对了每个调用点传递的是解析后的配置而非裸 URL。
  • 永久封禁类在白名单下仍被封禁。 元数据地址(含 fd00:ec2::254100.100.100.200)、链路本地、未指定地址、环回——包括 DNS 解析到环回的结果和 IPv4 映射/兼容/NAT64 写法——在出口处无视标志位复查;畸形 ::ffff: 形式失败即拒绝。过渡地址段(本地 NAT64、IETF 分配/Teredo 容器、6to4)作为解包绕过通道被封禁。
  • 顺带加固。 env-key 查找使用 Object.hasOwnconstructor 之类的 envKey 无法以函数身份到达 .trim());内嵌凭证的 baseUrl 由静默剥离改为硬失败;desktop 设置合并丢弃原型污染键。
  • CLI/desktop 孪生体。 net-guard.ts 逐单元镜像 CLI 地址分类,新增的 check-voice-guard-sync.js 在 CI(full profile)中以规范化 AST 对比两侧——此处漂移会让构建失败而不是静默放行。desktop 的可信设置合并、dotenv 语法与存储路径是形状不同的镜像,按文档由对等测试锁定。

长期存在的非阻断项(均已披露,未变化):

  • @wenshao 的备注 (a):daemon 的 200 字符错误截断会把补救消息中可操作的 URL 剪掉(只剩约 23 字符)。值得一个后续 commit;autofix 的 Critical-only 模式已将其留待人工跟进。
  • 备注 (b)/(c):内嵌凭证硬失败与 providerProtocol 语音模型可见性变更均已写入设计文档并有测试覆盖,但 PR 正文的破坏性变更/迁移一节仍值得各补一句。
  • desktop 的 bun 语音套件仍然不在任何 CI job 中运行;新的漂移门禁与 CI 全绿的 CLI 套件能缓解但不能替代。本次未验证:desktop bun 套件 + Electron 运行时(本运行从不执行 PR 代码;维护者已在本地运行 bun 套件——135 + 15 通过——并明确说明未启动 Electron 本体)。Windows/Linux:作者与维护者均在 macOS 上测试,其他平台交由 CI(下方构建全绿)。

测试

这是无人值守 CI 运行——我未构建或执行任何 PR 代码。 下方证据为被审 commit 上 PR 自身的 CI(经 API 读取),以及两个已完成、归属于其所有者的验证层:

  • 沙箱 /verify(第 2 轮)——在本 SHA(ce6b4803)上 ✅ 可合入:153/153 无 mock 脚本断言,定向 CLI 门禁 300/300。属咨询性证据,不构成评审。
  • 维护者验证(@wenshao,非机器人执行): 私有局域网上的真实明文 HTTP ASR 网关、真实 qwen serve daemon、真实音频——14 场景 before/after 批量矩阵(每个 DENY 行网关请求数为零)、流式 WebSocket 路径、Workspace 作用域自授权被拒绝并以设置警告呈现,另有三个变异测试确认套件与漂移门禁确实锁住了契约。结论为 "LGTM to merge"。

ce6b4803 上的 CI:所有实际运行的检查全部成功——0 失败、0 待定(84 个跳过的检查为条件性编排 job)。check:voice-guard-sync 漂移门禁作为全绿的 Qwen Code CI 工作流中的一个步骤运行。

Qwen Code · qwen3.8-max

Reviewed at ce6b48032ca7b2c68724e2183e37e3f7dd474ed8 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — a genuinely clean pass on the diff, green CI, sandboxed verification, and a maintainer who verified it against real network egress before approving; the last point is held back only by standing follow-ups, none of them blocking.

Stepping back: sixteen rounds in, this PR is what it claimed to be and nothing more. The allowlist is exact-URL, trusted-scope-only, fail-closed on every path I traced, and the one piece of unavoidable duplication (the desktop network guard) is the only mirrored security code in this repo that is mechanically drift-checked in CI. Every earlier reservation has been answered with evidence rather than prose: the sandboxed /verify run at this SHA, the maintainer's 14-scenario real-gateway matrix, and mutation tests showing the suites would actually catch a regression. My independent proposal from the first pass matches the landed architecture; the places the PR goes beyond it (the desktop authoritative-provider chain) exist because desktop cannot import CLI settings code, and they were runtime-verified by the maintainer.

On the policy cap, plainly: earlier passes deferred this PR under the Stage 0 rule — a fork feature at ~1,300 production lines touching core and opening new security surface needs a maintainer's sign-off, not a bot approval. That escalation has been answered in the strongest form available: @wenshao built the branch from source, exercised the exact risk this PR manages (cleartext credentials and audio on a private wire), confirmed the trust boundary and the fail-closed behavior firsthand, and approved this exact commit. Deferring again would defer to a decision that has already been made. The three open notes (error-message truncation follow-up, two PR-body sentences) are recorded in the Stage 2 comment as follow-ups, not conditions.

Reservations kept on record: the desktop bun suites and the Electron runtime remain outside CI and were not exercised end-to-end here; Windows/Linux coverage rests on CI builds. None of that blocks merge given the evidence above — flagging for the merge commit's awareness.

Approving, pinned to the reviewed commit. ✅

中文说明

置信度:4/5 —— 对 diff 的一次真正干净的审读、全绿 CI、沙箱验证,以及维护者在批准前已对真实网络出口做过验证;最后一点保留仅来自长期存在的跟进项,均不阻塞。

退一步看:十六轮之后,这个 PR 就是它所声称的东西,不多不少。白名单是精确 URL、仅受信作用域、在我追踪的每条路径上失败即拒绝;唯一不可避免的重复(desktop 网络守卫)是本仓库中唯一被 CI 机械性漂移检查的镜像安全代码。此前的每一项保留意见都用证据而非文字回应了:本 SHA 上的沙箱 /verify、维护者的 14 场景真实网关矩阵、以及证明套件确实能捕获回归的变异测试。我第一轮的独立提案与落地的架构一致;PR 超出提案的部分(desktop 权威 provider 链)源于 desktop 无法导入 CLI 设置代码,且已由维护者做了运行时验证。

关于政策上限,直说:此前各轮按 Stage 0 规则 defer 了本 PR——一个约 1,300 行生产代码、触及核心并打开新安全面的 fork 功能需要维护者签核,而不是机器人批准。该升级现已以最强形式得到回应:@wenshao 从源码构建了本分支,实际演练了本 PR 所管理的确切风险(私有链路上的明文凭证与音频),亲自确认了信任边界与失败即拒绝行为,并批准了这个 commit。再次 defer 将是在向一个已经做出的决定 defer。三个未结备注(错误消息截断的后续代码、PR 正文的两句话)已作为跟进项记录在 Stage 2 评论中,不构成合并条件。

保留在案的意见:desktop bun 套件与 Electron 运行时仍在 CI 之外,本次也未做端到端演练;Windows/Linux 覆盖依赖 CI 构建。鉴于上述证据,这些都不阻塞合并——提请合并时知悉。

批准,锁定在被审 commit。✅

Qwen Code · qwen3.8-max

Reviewed at ce6b48032ca7b2c68724e2183e37e3f7dd474ed8 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to @wenshao — this is a well-built PR (clean review, thorough tests, conservative fail-closed defaults), but it's a large (~716 production lines) core-touching feat that deliberately widens a security-sensitive default (cleartext + private-network egress for voice audio), so it needs a human call rather than an auto-approval. Three things I couldn't resolve from the diff alone:

  1. Desktop credential-resolution behavior changefromQwenSettings now selects the provider by exact id === voiceModel and throws on ambiguous/missing-field configs where it previously took the first DashScope-compatible provider. There's a legacy fallback, but please confirm the "no breaking changes" claim holds for existing Desktop voice users.
  2. New system-settings surface — Desktop voice resolution now reads /etc/qwen-code/, /Library/Application Support/QwenCode/, and C:\ProgramData\qwen-code\ settings; consistent with the CLI trusted-scope model, but worth an explicit sign-off on the threat model.
  3. CI still running on the reviewed commit (main unit + integration legs pending), so there's no green signal yet.

Also worth a @qwen-code /verify sponsored run to pin the behavioral security claim (exact-match opens egress; metadata/link-local/scheme/port/path mismatches stay blocked) — details in the Stage 2 comment. Needs a maintainer's eye on this one.

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head ce6b480, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline.

中文说明

已审查。 建议见行内评论。

— qwen3.8-max-preview via Qwen Code /review

Comment thread packages/desktop/packages/server-core/src/voice/resolve-voice-config.test.ts Outdated
Comment thread packages/cli/src/services/voice-transcriber.ts Outdated
Comment thread packages/desktop/packages/server-core/src/voice/net-guard.ts Outdated
@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🚫 Takeover not engaged: fork takeover requires the PR author to hold write access on this repository (author rockybot2026 currently: read). A maintainer can adopt the PR instead: snapshot the head into an in-repo branch, open a new PR (commit authorship is preserved), and take that over.

中文说明

🚫 未接管:fork 托管要求 PR 作者在本仓库持有 write 及以上权限(作者 rockybot2026 当前为:read)。维护者可改用领养:将 head 快照为本仓库分支并另开 PR(commit 署名保留),再对新 PR 执行接管。

@rockybot2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback in 797499a:

  1. CLI and Desktop now use the same strict allowlist contract: entries and exact model-provider URLs must include an explicit scheme and full path; only URL serialization and trailing slashes are normalized. The docs/schema and rejection messages now spell this out and point operators to security.allowedInsecureVoiceBaseUrls.
  2. Desktop exact-ID provider failures remain intentionally fail-closed. Falling back to unrelated environment credentials can cross a provider or regional boundary, so duplicate/incomplete entries and missing keys now require the operator to complete or remove the exact entry. The PR description and design doc now call out this migration behavior explicitly.
  3. Desktop now accepts public HTTPS custom providers without an insecure allowlist entry, matching CLI behavior. HTTP/private endpoints still require an exact trusted entry and the runtime network guard.
  4. Both guards now decode dotted and hexadecimal IPv4-mapped IPv6 forms. Trusted RFC1918 mapped addresses work; mapped loopback/metadata stay blocked; mapped public addresses are not misclassified. I opened refactor(voice): keep CLI and Desktop address guards in sync #8361 for shared policy vectors/NAT64 and ci(desktop): gate voice security tests on pull requests #8360 for a repository-owned Desktop voice CI gate.

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 /review quick passes; the latest report has no findings.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline.

中文说明

已审查——无阻断问题。 建议见行内评论。

— qwen3.8-max-preview via Qwen Code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. Not reviewed: You are review agent verify — Verification agent (round... — pointed at diff lines it never opened: it made tool calls, but none of them read the diff. Test Plan (not a blocker): 247 passed — this review observed 16108, 475 passed; 104 passed — this review observed 16108, 475 passed.

中文说明

已审查。 建议见行内评论。 未审查:You are review agent verify — Verification agent (round...——它被指向 diff 的行却从未打开:有工具调用,但没有一次读取 diff。 Test Plan(非阻断):247 passed — this review observed 16108, 475 passed; 104 passed — this review observed 16108, 475 passed

— qwen3.8-max-preview via Qwen Code /review (v0.21.3)

Comment thread packages/cli/src/services/voice-transcriber.ts Outdated
Comment thread packages/desktop/packages/server-core/src/voice/net-guard.ts
Comment thread packages/desktop/packages/server-core/src/voice/resolve-voice-config.ts Outdated
Comment thread packages/desktop/packages/server-core/src/voice/resolve-voice-config.ts Outdated
Comment thread packages/desktop/packages/server-core/src/voice/resolve-voice-config.ts Outdated
Comment thread packages/cli/src/services/voice-transcriber.ts
Comment thread packages/cli/src/ui/voice/voice-transcriber.test.ts
Comment thread packages/desktop/packages/server-core/src/voice/resolve-voice-config.ts Outdated
@rockybot2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
_Qwen Code review request accepted. Review is queued in [workflow run](https://github.com/QwenLM/qwen-code/actions/runs/30746387737)._

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 2, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 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/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 15/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 15/100 轮)。改动内容与我反驳保留之处如下:

Review round 11 — summary

Critical-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 decisions

Critical — fixed

  1. R10-1 (scripts/check-voice-guard-sync.js)normalizeMirroredCode stripped every {/} including inside string/template/regex literal content, so semantically different mirrored units normalized identically (probe-verified in the finding: mutating ${prefix}$prefix in desktop deriveWebSocketBase passed the guard; block-structure drift also normalized equal).
    Fixed with the fix the finding suggested — comparing parse trees instead of brace-stripped text: each unit is parsed with the TypeScript parser and re-printed with removeComments: true; single-statement blocks are unwrapped (keeping the documented brace-style tolerance); source positions are stripped so the printer emits canonical layout. Literal contents and statement structure are now compared exactly. Verified against the finding's exact scenarios: the ${prefix}$prefix mutation is detected, block-structure drift is detected, the brace-free control mutation is still detected, and the unmutated real mirrors still pass.
  2. R10-2 (scripts/check-voice-guard-sync.js) — the whitespace collapse erased whitespace inside preserved literal content and ASI-significant newlines (probe-verified in the finding: '/compatible-mode/v1''/compatible-mode/ v1' masked; a newline inserted after return masked, flipping the drifted side's classification).
    Fixed by the same parse-tree comparison: literals are printed verbatim from the AST (inner whitespace preserved), and the parser itself models ASI (return + newline parses as return;), so both holes are closed structurally rather than patched. Verified against the finding's exact scenarios on the real mirrored units.

The fix removes the entire ~200-line hand-rolled scanner (stripComments, REGEX_PRECEDING_KEYWORDS, the regex-vs-division heuristic) in favor of ~90 lines built on the project's own TypeScript compiler (a devDependency of every workspace, hoisted to the root node_modules; CI runs the guard after npm ci). Net diff: +144/−185 lines. Five tests pin the previously masked drift classes and the unmangled normalization; the existing "passes on the real mirrored sources" test keeps pinning the whole mirror set.

Suggestions — deferred to follow-up issue/PR (Critical-only mode)

  • R10-3 scanner/normalizer spurious-drift edge cases — partially overtaken: the parse-tree rewrite replaces the scanner (the TypeScript parser handles regex literals and ${} natively); the remaining export const extraction and arrow-body style edges stay known loud, safe-direction gaps. Deferred.
  • R10-4 guard test-suite gaps — items 1–2 overtaken (the scanner no longer exists; the new tests pin unmangled normalization and the drift mutations); items 3–4 (spawnSync failure-path test, pinned MIRROR_SETS inventory) deferred.
  • R10-5 two policy-unit groups without a sync mechanism — deferred (touches production code on both runtimes).
  • R10-6 IP-literal-path always-blocked message — deferred (blocking is correct on every path; diagnostic polish).
  • R10-7 streaming-negative default-wiring coverage — deferred (additive test coverage).
  • R10-8 CLI no-/v1-inference pin — deferred (additive test coverage).
  • R10-9 desktop top-level scheme recheck test — deferred (additive test coverage).
  • R10-10 non-string allowlist entry tests — deferred (additive test coverage).
  • R10-11 parent ~/.env fallback content tests — deferred (additive test coverage).
  • R10-12 reversed-ordering dedup test — deferred (additive test coverage).
  • R10-13 derived-WebSocket-URL docs rewording — deferred (docs accuracy, not a behavior defect).
  • R10-14 CGNAT/ULA trusted-permission pins — deferred (additive test coverage).
  • R10-15 dotenv parity corpus / version pin — deferred (needs a dependency-placement decision).

Each deferral is posted as a reply on its own thread (comment-replies.json) with the thread left open.

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.

Conflicts

None (--conflict false; no merge performed).

Verification

  • npm run build — passed (exit 0). Also required to bring this checkout to a built state: two script tests (check-i18n, install-script standalone packaging) failed only while the checkout was unbuilt and pass after the build; neither imports the guard script.
  • npm run typecheck — passed (exit 0, all workspaces)
  • npm run lint — passed (exit 0)
  • npm run test:scripts — 47/47 test files passed, 948 tests passed | 14 skipped
  • npx vitest run --config ./scripts/tests/vitest.config.ts check-voice-guard-sync — 11/11 passed (focused suite for the touched script)
  • node scripts/check-voice-guard-sync.js — "Voice guard mirror check passed.", exit 0
  • Mutation harness on the finding's probe-verified scenarios (throwaway script outside the repo): all four previously masked drift classes (template-brace stripping, string whitespace, ASI newline, block structure) are now detected; the unmutated mirrors report no drift
  • npx prettier --check on both touched files — clean
  • Not run: npm run bundle + integration tests — the change touches only the CI guard script and its unit tests, not the bundled CLI surface; no settings source changed, so generate:settings-schema is not applicable.

Commit: fix(scripts): compare voice guard mirrors as parse trees (#8350)

中文说明

第 11 轮评审 — 总结

在完成 10 个产生改动的评审轮后已进入仅处理 Critical 的模式。按仓库评审策略,本轮只合入两个 Critical 修复,13 条建议(Suggestion)延后到后续 issue/PR;每条延后都会在其所属讨论串中以回复形式记录,不会被静默丢弃。

反馈点与决定

Critical — 已修复

  1. R10-1(scripts/check-voice-guard-sync.js)normalizeMirroredCode 无条件去除所有 {/},包括字符串/模板/正则字面量内容里的花括号,导致语义不同的镜像单元规范化后相同(该发现已探针验证:将 desktop deriveWebSocketBase 中的 ${prefix} 突变为 $prefix 时守卫通过;块结构漂移同样规范化相等)。
    已修复,采用该发现建议的方案——比较语法树而非去花括号后的文本:每个单元用 TypeScript 解析器解析,并以 removeComments: true 重新打印;单语句块被展开(保留文档承诺的大括号风格容忍);剥离源码位置信息使打印机输出规范布局。字面量内容与语句结构现在逐字比较。已按该发现的原始场景验证:${prefix}$prefix 突变被检出,块结构漂移被检出,不含花括号的对照突变仍被检出,未突变的真实镜像仍然通过。
  2. R10-2(scripts/check-voice-guard-sync.js) — 空白压缩抹掉了保留字面量内容中的空白与影响 ASI 的换行(该发现已探针验证:'/compatible-mode/v1''/compatible-mode/ v1' 被掩盖;return 后插入换行被掩盖,漂移侧的分类结果翻转)。
    已修复,同样由语法树比较解决:字面量由 AST 逐字打印(内部空白保留),解析器本身即建模 ASI(return 后跟换行解析为 return;),两个漏洞从结构上闭合而非打补丁。已在真实镜像单元上按该发现的原始场景验证。

本次修复整体删除了约 200 行手写扫描器(stripCommentsREGEX_PRECEDING_KEYWORDS、正则/除法判定启发式),替换为约 90 行基于项目自身 TypeScript 编译器的实现(typescript 是每个 workspace 的 devDependency,提升到根 node_modules;CI 在 npm ci 之后运行该守卫)。净 diff:+144/−185 行。五个测试钉住了此前被掩盖的漂移类别与无破坏的规范化输出;既有的 "passes on the real mirrored sources" 测试继续钉住整个镜像集合。

建议类 — 延后到后续 issue/PR(仅处理 Critical 模式)

  • R10-3 扫描器/规范化器虚假漂移边界场景 — 部分被覆盖:语法树重写替换了扫描器(TypeScript 解析器原生处理正则字面量与 ${});剩余的 export const 提取与箭头函数体风格边界仍是已知的、高声失败且方向安全的缺口。延后。
  • R10-4 守卫测试套件缺口 — 第 1、2 项已被覆盖(扫描器已不存在;新测试钉住无破坏的规范化与漂移突变);第 3、4 项(spawnSync 失败路径测试、钉住 MIRROR_SETS 清单)延后。
  • R10-5 两组无同步机制的策略单元 — 延后(涉及两个运行时的生产代码)。
  • R10-6 IP 字面量路径的 always-blocked 消息 — 延后(所有路径阻断行为正确,属诊断信息打磨)。
  • R10-7 流式负向默认装配覆盖 — 延后(增量测试覆盖)。
  • R10-8 CLI 不做 /v1 推断的钉住测试 — 延后(增量测试覆盖)。
  • R10-9 desktop 顶层 scheme 复查测试 — 延后(增量测试覆盖)。
  • R10-10 非字符串白名单条目测试 — 延后(增量测试覆盖)。
  • R10-11 父级 ~/.env 回退带内容测试 — 延后(增量测试覆盖)。
  • R10-12 反序去重测试 — 延后(增量测试覆盖)。
  • R10-13 派生 WebSocket URL 的文档改写 — 延后(文档准确性问题,非行为缺陷)。
  • R10-14 CGNAT/ULA 受信放行钉住 — 延后(增量测试覆盖)。
  • R10-15 dotenv 对等语料/版本固定 — 延后(需要先做依赖放置决策)。

每条延后都会在其所属讨论串中发布回复(comment-replies.json),讨论保持开放。

评审主体(CHANGES_REQUESTED)

评审中的 "Not reviewed" 说明(desktop bun 套件未在 CI 中运行——作者已在 #8360 跟踪;CLI 集成套件在 CI 中被跳过;反向审计因时间预算中止;actionlint 内嵌 shell 的限制;Test Plan 计数观察)是评审自身覆盖面的陈述,不是代码发现,也不要求任何代码改动。

冲突

无(--conflict false,未执行合并)。

验证

  • npm run build — 通过(exit 0)。该命令同时用于将本检出置于已构建状态:两个脚本测试(check-i18ninstall-script 独立打包)仅在检出未构建时失败,构建后即通过;二者均不导入该守卫脚本。
  • npm run typecheck — 通过(exit 0,全部 workspace)
  • npm run lint — 通过(exit 0)
  • npm run test:scripts — 47/47 个测试文件通过,948 个测试通过 | 14 个跳过
  • npx vitest run --config ./scripts/tests/vitest.config.ts check-voice-guard-sync — 11/11 通过(针对被改脚本的聚焦套件)
  • node scripts/check-voice-guard-sync.js — "Voice guard mirror check passed.",exit 0
  • 针对该发现探针验证场景的突变测试(仓库外的一次性脚本):此前被掩盖的四个漂移类别(模板花括号剥离、字符串空白、ASI 换行、块结构)现在全部被检出;未突变镜像报告无漂移
  • npx prettier --check 两个被改文件 — 干净
  • 未运行:npm run bundle + 集成测试 — 本次改动只涉及 CI 守卫脚本及其单元测试,不涉及打包后的 CLI 行为面;未改动 settings 源,故无需 generate:settings-schema

提交:fix(scripts): compare voice guard mirrors as parse trees (#8350)

Deferred non-Critical feedback

Critical-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. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 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 将重新运行。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. Not reviewed: build-and-test — the desktop server-core voice suites (resolve-voice-config.test.ts, voice.test.ts, voice-ws-handler.isolated.ts) are bun tests outside every npm workspace; no CI job runs them and this review's harness could not execute them (tracked by the author in #8360); the desktop half was reviewed statically and via probes only. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (integration-tests/cli, which covers the serve route surface including voice routes) did not run locally. Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted. Test Plan (not a blocker): 247 passed — this review observed 17803, 475 passed; 109 passed — this review observed 17803, 475 passed.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — the desktop server-core voice suites (resolve-voice-config.test.ts, voice.test.ts, voice-ws-handler.isolated.ts) are bun tests outside every npm workspace; no CI job runs them and this review's harness could not execute them (tracked by the author in #8360); the desktop half was reviewed statically and via probes only。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (integration-tests/cli, which covers the serve route surface including voice routes) did not run locally。 未检查(工具限制,非阻断):the executable-script lint — .github/workflows/ci.yml: actionlint embedded-shell source mapping is not yet supported — not linted。 Test Plan(非阻断):247 passed — this review observed 17803, 475 passed; 109 passed — this review observed 17803, 475 passed

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +169 to +171
for (let k = startLine + 1; k < lines.length; k += 1) {
if (lines[k] === '}') {
return lines.slice(startLine, k + 1).join('\n');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Unit extraction ends at the first line that is textually } at column 0 — so a multi-line template literal inside a mirrored unit that contains a column-0 } line (or a statement appended after a kind: 'block' unit's closing brace) truncates the extracted unit, and any drift after that line is silently ignored: a fail-open hole in the drift guard this PR adds.

Failure scenario: probe-verified against the shipped exports — (1) a unit whose body contains a multi-line template literal with a column-0 } is truncated (a 9-line unit extracts as 3 lines), and two sources differing only after that line make checkMirrorSet return [] — no drift; (2) appending one statement after the BLOCKED_TRANSITION_IPV6_ADDRESSES block unit on only one side reads CLEAN. Not live today (no mirrored unit contains such a line), but adding a multi-line error message to a mirrored unit, or extending the transition blocklist with an appended statement, silently disables drift detection for the remainder of that unit. Note: scripts/tests/check-voice-guard-sync.test.js test 5 currently pins the truncation boundary as intended behavior — a fix must update it too.

Suggested fix: derive unit bounds from the parse tree — ts.createSourceFile the whole file, locate the top-level statement whose declaration starts the unit, and print that node (or tokenize with string/template/regex span awareness when hunting the closing brace).

中文说明

[Suggestion] 单元提取在第一行文本上等于列首 } 的行处终止——因此镜像单元中的多行模板字面量若包含一行列首 }(或在 kind: 'block' 单元的收尾花括号之后追加语句),提取出的单元会被截断,该行之后的任何漂移都会被静默忽略:这是本 PR 新增的漂移守卫中的一个 fail-open 漏洞。

失败场景:已对交付的导出函数探针验证——(1) 函数体包含含列首 } 的多行模板字面量的单元会被截断(9 行单元只提取出 3 行),两个仅在该行之后有差异的源文件会使 checkMirrorSet 返回 []——无漂移;(2) 仅在 BLOCKED_TRANSITION_IPV6_ADDRESSES 块单元之后一侧追加一条语句,检查读取为 CLEAN。当前未触发(没有任何镜像单元包含此类行),但一旦给镜像单元添加多行错误消息、或以追加语句的方式扩展过渡前缀黑名单,就会静默关闭对该单元其余部分的漂移检测。注意:scripts/tests/check-voice-guard-sync.test.js 的测试 5 目前把该截断边界钉为预期行为——修复时需一并更新。

建议修复:从解析树推导单元边界——对整个文件 ts.createSourceFile,定位以该单元声明开头的顶层语句并打印该节点(或在寻找收尾花括号时对字符串/模板/正则字面量区间保持感知)。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +256 to +257
'64:ff9b::10.0.0.8',
'64:ff9b:1::a00:8',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] (Pattern, site 1/3 — sibling comments at packages/cli/src/ui/voice/voice-transcriber.test.ts:664 and packages/desktop/packages/server-core/src/voice/resolve-voice-config.test.ts:683.) The reserved-bit / >2-suffix-group address class that fails open in the NAT64 classifier (see the low-confidence Critical on voice-transcriber.ts in this review's summary) is absent from every guard test suite on both surfaces: these loops pin every well-formed 64:ff9b::/96 spelling while omitting exactly the escaping class, so nothing pins its behavior — today the escape forms pass both suites untested, and if the classifier is fixed fail-closed, a later simplification reintroducing the hole stays green everywhere.

Failure scenario: probe-verified — 64:ff9b::1:203:405 and 64:ff9b::1:2.3.4.5 pass assertVoiceBaseUrlNetworkAllowed today (untrusted, as DNS record and as IP literal) while their well-formed siblings are blocked; a repo-wide grep finds no such literal in any voice suite. If a fix lands without these pins, a regression reintroducing the hole ships green in both twins.

Suggested fix: when the classifier disposition is settled, add '64:ff9b::1:203:405' and '64:ff9b::1:2.3.4.5' to this loop asserting the blocked outcome the fix adopts — or, if these forms are deliberately kept public, pin that decision deliberately.

中文说明

[Suggestion](模式发现,位置 1/3——姊妹评论见 packages/cli/src/ui/voice/voice-transcriber.test.ts:664packages/desktop/packages/server-core/src/voice/resolve-voice-config.test.ts:683。)在 NAT64 分类器中 fail-open 的保留位 / 超过 2 个后缀组地址类(见本次评审摘要中针对 voice-transcriber.ts 的低置信度 Critical)在两端的所有守卫测试套件中均缺席:这些循环钉住了所有合法形式的 64:ff9b::/96 拼写,却恰好遗漏了会逃逸的那一类,因此没有任何测试钉住其行为——今天这些逃逸形式在两套测试中未经测试地通过;若分类器被修复为 fail-closed,之后重新引入该漏洞的简化改动在所有测试中仍为绿色。

失败场景:已探针验证——64:ff9b::1:203:40564:ff9b::1:2.3.4.5 今天能通过 assertVoiceBaseUrlNetworkAllowed(未受信模式下,无论作为 DNS 记录还是 IP 字面量),而其合法形式的同类地址被阻断;全仓库 grep 未在任何语音套件中找到此类字面量。若修复落地而没有这些钉子,重新引入该漏洞的回归将在两端一路绿灯地合入。

建议修复:待分类器的处置确定后,在本循环中加入 '64:ff9b::1:203:405''64:ff9b::1:2.3.4.5',断言修复所采用的阻断结果——或者,若刻意保留这些形式为公网,请刻意地把该决定钉住。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +664 to +666
'::a9fe:a9fe',
'::6464:64c8',
'::5db8',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] (Pattern, site 2/3 — sibling comments at packages/desktop/packages/server-core/src/voice/voice.test.ts:256 and packages/desktop/packages/server-core/src/voice/resolve-voice-config.test.ts:683.) This loop pins the well-formed ::/96 embeddings (::a9fe:a9fe, ::6464:64c8, ::5db8) but omits the reserved-bit / >2-suffix-group siblings that fail open in the IPv4-compatible classifier (see the low-confidence Critical on this file's implementation in this review's summary): ::1:a9fe:a9fe, ::1:6464:64c8, ::1:7f00:1 classify as public and pass even the untrusted gate today, untested in both twins.

Failure scenario: probe-verified — the escape forms pass assertVoiceBaseUrlNetworkAllowed for trusted and untrusted configs (and as IP literals, and through resolveVoiceTranscriptionConfig) while the pinned well-formed siblings are blocked; the desktop twin loop and both opt-in loops have the same gap. If the classifier is fixed fail-closed without these pins, a later simplification reintroducing the hole stays green everywhere.

Suggested fix: when the classifier disposition is settled, add '::1:a9fe:a9fe' (and a dotted-quad sibling) here asserting the blocked outcome, mirror it in the desktop twin's loop and both opt-in loops, and add the 64:ff9b::1:… reserved-bit forms to the NAT64 canonicalization loop above.

中文说明

[Suggestion](模式发现,位置 2/3——姊妹评论见 packages/desktop/packages/server-core/src/voice/voice.test.ts:256packages/desktop/packages/server-core/src/voice/resolve-voice-config.test.ts:683。)本循环钉住了合法形式的 ::/96 嵌入(::a9fe:a9fe::6464:64c8::5db8),却遗漏了在 IPv4-compatible 分类器中 fail-open 的保留位 / 超过 2 个后缀组的同类地址(见本次评审摘要中针对本文件实现类的低置信度 Critical):::1:a9fe:a9fe::1:6464:64c8::1:7f00:1 今天会被分类为公网地址,即使在未受信门禁下也通过,且在两端均无测试。

失败场景:已探针验证——这些逃逸形式在受信与未受信配置下(以及作为 IP 字面量、经由 resolveVoiceTranscriptionConfig)都能通过 assertVoiceBaseUrlNetworkAllowed,而被钉住的合法形式同类地址被阻断;desktop 孪生循环与两个 opt-in 循环存在同样缺口。若分类器被修复为 fail-closed 而没有这些钉子,之后重新引入该漏洞的简化改动在所有测试中仍为绿色。

建议修复:待分类器的处置确定后,在此加入 '::1:a9fe:a9fe'(及一个点分十进制同类地址)并断言阻断结果,同步到 desktop 孪生循环与两个 opt-in 循环,并在上方 NAT64 规范化循环中加入 64:ff9b::1:… 保留位形式。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +683 to +685
it('rejects always-blocked base URLs at config time even when allowlisted', async () => {
for (const baseUrl of [
'http://169.254.169.254/v1',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] (Pattern, site 3/3 — sibling comments at packages/desktop/packages/server-core/src/voice/voice.test.ts:256 and packages/cli/src/ui/voice/voice-transcriber.test.ts:664.) The config-time always-blocked loop pins only well-formed blocked literals and omits the NAT64 reserved-bit class, so the config-time surface gives the escape no signal either — a third suite the two net-guard-loop fixes do not cover.

Failure scenario: probe-verified — today an exact provider with baseUrl: 'https://[64:ff9b::1:203:405]/v1' classifies as public HTTPS (the classifier returns undefined for 3 suffix groups), and classifyVoiceProviderEntry silently falls through to the OAuth/legacy chain instead of rejecting — while its well-formed sibling https://[64:ff9b::169.254.169.254]/v1 throws 'must not use a private-network baseUrl'. If the classifier is fixed fail-closed, this class flips from silent fall-through to hard rejection in both the provider path and the top-level env-path recheck, and nothing in this suite pins either outcome.

Suggested fix: when the classifier disposition is settled, add 'http://[64:ff9b::1:203:405]/v1' (and optionally an ::1:a9fe:a9fe-style IPv4-compatible sibling) to this loop asserting the rejected outcome.

中文说明

[Suggestion](模式发现,位置 3/3——姊妹评论见 packages/desktop/packages/server-core/src/voice/voice.test.ts:256packages/cli/src/ui/voice/voice-transcriber.test.ts:664。)配置期的 always-blocked 循环只钉住了合法形式的被阻断字面量,遗漏了 NAT64 保留位类,因此配置期面对该逃逸同样没有任何信号——这是两个 net-guard 循环修复覆盖不到的第三套测试。

失败场景:已探针验证——今天一个 baseUrl: 'https://[64:ff9b::1:203:405]/v1' 的精确 provider 会被分类为公网 HTTPS(分类器对 3 个后缀组返回 undefined),classifyVoiceProviderEntry 会静默落入 OAuth/legacy 链而不是拒绝——而其合法形式的同类地址 https://[64:ff9b::169.254.169.254]/v1 会抛出 'must not use a private-network baseUrl'。若分类器被修复为 fail-closed,这一类会从静默落入变为硬拒绝(provider 路径与顶层 env 路径复查均如此),而本套件对两种结果都没有钉子。

建议修复:待分类器的处置确定后,在本循环中加入 'http://[64:ff9b::1:203:405]/v1'(可选再加一个 ::1:a9fe:a9fe 式的 IPv4-compatible 同类地址),断言拒绝结果。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +208 to +210
for (const [key, value] of Object.entries(parseEnvFileContent(content))) {
if (!Object.hasOwn(env, key) || env[key] === '') {
result[key] ??= value;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The home-.env fallback freezes an EMPTY value from the more-specific file: result[key] ??= value stores '' (not nullish), blocking a lower-priority candidate's real value — while the CLI's loadEnvironment treats '' as 'effectively unset' per file and lets the later file win. The same two .env files yield different credentials on the two surfaces.

Failure scenario: A/B-probed on both surfaces with identical input (~/.qwen/.env = DASHSCOPE_API_KEY= empty, ~/.env = DASHSCOPE_API_KEY=sk-real, no process-env value): CLI loadEnvironment resolves sk-real; desktop resolveDesktopVoiceConfig rejects with 'Voice dictation needs Qwen credentials'. The one-line flip (skip empty file values) was verified to resolve it, then reverted. Variants: with an id-matching provider naming that envKey it throws 'requires DASHSCOPE_API_KEY'; with OPENAI_API_KEY/OPENAI_BASE_URL set it silently resolves a different endpoint. Distinct from the earlier R7-14 thread (the process-env presence gate, fixed — this gate already includes env[key] === '').

Suggested fix: in the credential-lookup path only, treat an empty-string file value as 'not a definition' so a later candidate can fill it (e.g. skip value === '' here) — leaving the shared homeEnvFallback first-wins semantics that resolveSettingsEnvVars consumes untouched.

中文说明

[Suggestion] home .env 回退会把来自更具体文件的空值冻结下来:result[key] ??= value 会存入 ''(并非 nullish),从而阻止更低优先级候选文件中的真实值——而 CLI 的 loadEnvironment 对每个文件将 '' 视为"实际上未设置",允许后续文件覆盖。同样的两个 .env 文件在两端会解析出不同的凭证。

失败场景:已在两端用相同输入做 A/B 探针(~/.qwen/.env = DASHSCOPE_API_KEY= 空值,~/.env = DASHSCOPE_API_KEY=sk-real,无进程环境变量):CLI loadEnvironment 解析出 sk-real;desktop resolveDesktopVoiceConfig 以 'Voice dictation needs Qwen credentials' 拒绝。单行翻转(跳过空文件值)已验证可解决,随后还原。变体:当存在 id 匹配且以该 envKey 命名的 provider 时会抛出 'requires DASHSCOPE_API_KEY';当设置了 OPENAI_API_KEY/OPENAI_BASE_URL 时会静默解析到另一个端点。与早前的 R7-14 讨论不同(进程环境变量存在性门禁已修复——此处的门禁已包含 env[key] === '')。

建议修复:仅在凭证查找路径上,把空字符串的文件值视为"未定义",使后续候选文件可以填充(例如在此处跳过 value === '')——保持供 resolveSettingsEnvVars 消费的共享 homeEnvFallback 的先到优先语义不变。

— qwen3.8-max via Qwen Code /review (v0.21.6)

Comment on lines +273 to +275
it('rejects loopback DNS results even when trusted, with loopback guidance', async () => {
for (const address of [
'127.0.0.1',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This diff deletes the only untrusted-mode loopback-DNS rejections in this suite (old 127.0.0.1 / ::ffff:127.0.0.1/private-network/) and replaces them with a loop running every loopback record with allowInsecureBaseUrl: true — no remaining test exercises untrusted (default) mode against a loopback DNS result, even though the design doc makes 'loopback DNS results are always blocked, with or without an allowlist entry' an explicit behavior change. The CLI twin suite has the same gap.

Failure scenario: probe-verified against all 35 remaining (address, allow, expectation) vectors — today's code blocks untrusted + loopback DNS (no live defect), but a plausible trust-coupled refactor of the block condition (e.g. (!allow && isPrivateNetworkIp(a)) || (allow && isAlwaysBlockedVoiceAddress(a))) passes every remaining pin while flipping the untested combination to ALLOWED, because isPrivateNetworkIp('127.0.0.1') is deliberately false (loopback-exempt). A public hostname resolving to 127.0.0.1 would then carry voice audio to a loopback endpoint with no opt-in.

Suggested fix: add untrusted-mode entries alongside the trusted loop — at minimum '127.0.0.1' and '::ffff:7f00:1' with no allowInsecureBaseUrl, expecting /loopback/ — in this suite and the CLI twin's loopback-DNS loop.

中文说明

[Suggestion] 本 diff 删除了本套件中唯一的未受信模式 loopback-DNS 拒绝用例(旧的 127.0.0.1 / ::ffff:127.0.0.1/private-network/),取而代之的是一个对所有 loopback 记录都以 allowInsecureBaseUrl: true 运行的循环——即使设计文档把"loopback DNS 结果始终被阻断,无论是否在白名单中"列为明确的行为变更,剩余的测试中已没有任何一个在未受信(默认)模式下验证 loopback DNS 结果。CLI 孪生套件存在同样缺口。

失败场景:已对剩余全部 35 个(地址, allow, 期望)向量探针验证——今天的代码会阻断未受信 + loopback DNS(无现行缺陷),但一个看似合理的、把阻断条件与信任模式耦合的重构(例如 (!allow && isPrivateNetworkIp(a)) || (allow && isAlwaysBlockedVoiceAddress(a)))能通过所有剩余的钉子,同时把未测试的组合翻转为允许——因为 isPrivateNetworkIp('127.0.0.1') 刻意返回 false(loopback 豁免)。届时一个解析到 127.0.0.1 的公网主机名将在没有任何 opt-in 的情况下把语音音频送往 loopback 端点。

建议修复:在受信循环旁补充未受信模式条目——至少包括不带 allowInsecureBaseUrl'127.0.0.1''::ffff:7f00:1',期望 /loopback/——同时覆盖本套件与 CLI 孪生的 loopback-DNS 循环。

— qwen3.8-max via Qwen Code /review (v0.21.6)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action taken

PR: #8350 · Base: main · Conflict: none

This round had no actionable feedback:

  • Reviews: none newer than the last evaluation.
  • Inline comments: none in scope.
  • Issue-level comments: none.
  • Failed checks: none, and no still-red checks persisting from earlier rounds.

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 · 基础分支: main · 冲突:

本轮没有可处理的反馈

  • 审查(Reviews): 自上次评估以来没有新的审查。
  • 行内评论(Inline comments): 本轮范围内没有。
  • Issue 级评论: 没有。
  • 失败的检查(Failed checks): 没有,也没有此前轮次遗留的持续失败检查。

当前处于仅处理 Critical 的模式(在 10 个产生改动的轮次之后)。本轮反馈中"已延后"部分列出的非 Critical 问题,按照棘轮策略保持开放、留待人工跟进;本轮没有为这些条目修改代码、解决讨论串或发布回复。

本轮没有产生任何提交;分支 HEAD 保持不变。

Deferred non-Critical feedback

Critical-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. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 153 passed · 0 failed · 153 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:153 通过 · 0 失败 · 153 总计

Verification report

PR #8350 Deep Verification (round 2) — feat(voice): support trusted private ASR base URLs

Verdict: merge-ready — scripted harness assertions 153 pass / 0 fail (6 mock-free harnesses + vacuity + generated-artifact check); targeted CLI gate 300/300 (6 suites); scripts gate 843/843; no blocking findings.
Verified head: ce6b48032ca7b2c68724e2183e37e3f7dd474ed8 (git rev-parse HEAD^2); base tip 8fd0162c6821138ce7fab6472fbae06ca4717a7c (HEAD^1); merge commit 20ee5e432. Follow-up round — previous report verified head 18a56da; 10 commits landed since (round-8/9/10 fixes, desktop duplicate classification, parse-tree mirror script).

中文摘要
  • 结论:merge-ready(可合入)。6 个免 mock harness 共 153 条脚本断言全部通过(0 失败);CLI 定向门禁 300/300(6 个套件);scripts 门禁 843/843;空性正向对照与生成 schema 校验通过;未发现阻断问题。
  • A/B 结论01-ab-cli-resolve-base-vs-head.png):核心改动在新 head 上仍然承重。21 个场景中 4 个按功能预期翻转(S02/S07/S16/S17:精确白名单使 HTTP/私网端点由拒绝变为接受),S18 证明内嵌凭证由 base 的"剥离后继续"改为 head 的拒绝(加固)。受保护地址类别即使列入白名单也全部保持阻断。
  • 与上一轮的差异:base 尖端已从 41f0e3c 前移到 8fd0162c6,main 已吸收 IPv4-mapped/compatible 解码(含 ::ffff: 一刀切规则),因此 PR 的净加固差收窄为:DNS 重绑定 loopback、NAT64 包装 metadata、Teredo、6to4、local-use NAT64(D06/D11–D14 翻转仍然成立)。见"Corrections"。
  • 新增提交全部验证:round-8 形状防护(非对象条目跳过、非字符串 baseUrl/envKey 给出修复性错误而非崩溃)、重复 provider 先分类后判歧义(全公网 HTTPS 重复不再误报歧义)、DashScope /v1 重写在白名单匹配之前、CLI 修复提示指明生效作用域、providerProtocol 贯通(定向套件覆盖);round-10 的镜像同步脚本经变异矩阵证明是活的(语义变异全部捕获,格式/注释差异不误报),并已接入 CI。
  • 未覆盖:Desktop bun 套件(容器无 bun,已用 tsx 直驱源码补偿 20 条断言);真实 ASR 请求;逐提交归因(浅克隆 depth 2);Windows/Linux 运行时。

Previous-finding status (follow-up round)

Previous round: head 18a56da, base 41f0e3c, verdict merge-ready, 62 assertions. All carried measurements were re-run at the new head (input closure changed: 10 new commits incl. merges of a moved main; nothing qualified for the identical-closure shortcut).

# Previous finding / item Severity Status at ce6b4803
1 Central allowlist A/B (4 flip cells, protected classes blocked) load-bearing stands — re-measured, 42/42 (01-ab-cli-resolve-base-vs-head.png)
2 DNS guard: allowlist never defeats always-blocked classes load-bearing stands — re-measured, 66/66 across 3 arms (02-dns-guard-three-arm.png)
3 "PR hardens base SSRF guard: base allowed NAT64-metadata/Teredo/6to4/loopback-rebind/mapped forms" observation superseded in part — the new base tip already blocks IPv4-mapped/compatible forms (main absorbed that hardening); remaining deltas re-confirmed: D06 rebind, D11 NAT64-metadata, D12 Teredo, D13 6to4, D14 local-use NAT64. See Corrections.
4 Workspace cannot self-grant (strip + warn + reject) load-bearing stands — re-measured through the real loadSettings, 8/8 (03-trust-boundary-workspace-self-grant.png)
5 Desktop fail-closed resolution load-bearing stands + extended — 20/20 incl. 10 new delta probes (04-desktop-resolve-delta-probes.png)
6 Vacuity: neutered allowlist turns the acceptance tests red positive control stands — re-measured: now exactly 5 tests red (suite grew 47→61), restore green 61/61, diff clean (07-vacuity-positive-control.png)
7 Generated settings.schema.json byte-identical on regeneration artifact check stands — re-ran generator at new head: zero diff
8 12 as-is settings.test.ts failures are environmental note stands — re-observed identical signature (12 failed | 147 passed of 159); green under sanitized env
9 Desktop bun suite not runnable in container not covered stands — still no bun; compensated by tsx harnesses (H2 desktop arm + H4)
10 Per-commit attribution unreachable not covered stands — depth 2; rev-list returns bogus 1 vs 27 commits in metadata

Scope selected

  • Central claim (carried): a voice provider whose baseUrl is HTTP or private-network is rejected by default but accepted when its exact normalized URL is listed in security.allowedInsecureVoiceBaseUrls from a trusted scope — enforced identically across CLI resolution, the CLI/Desktop DNS guard, and Desktop resolution.
  • Secondary claim 1 (carried): Workspace-scoped allowlist is stripped before merge; a cloned repo cannot self-grant; User/System/SystemDefaults honored.
  • Secondary claim 2 (carried): metadata / link-local / unspecified / loopback-alias / transition-IPv6 stay blocked even when listed, in every embedded-IPv4 encoding, including via DNS.
  • Delta claims (new this round): (a) desktop provider scan is shape-guarded (non-object entries skipped; non-string baseUrl/envKey → remediation error, not crash); (b) same-ID duplicates are classified before the ambiguity check — all-public-HTTPS duplicate sets keep legacy fall-through; exact (id, baseUrl) duplicates keep first registration; (c) the DashScope-compatible /v1 rewrite is computed before allowlist match; (d) CLI remediation messages name the honored scopes; (e) providerProtocol is threaded through CLI voice seams; (f) the new check-voice-guard-sync.js mirror check is live and wired into CI.

Out of scope (listed under Not covered): live ASR egress, Desktop bun suite, Windows/Linux runtime, per-commit attribution.

Central claim — A/B load-bearing proof (re-measured)

21 scenarios driven through the real resolveVoiceTranscriptionConfig from head and from a base worktree at HEAD^1 (8fd0162c6). Base control is clean: git diff HEAD^1..HEAD -- packages/core is empty, and readlink -f node_modules/@qwen-code/qwen-code-core/__w/qwen-code/qwen-code/packages/core, so both arms load identical core; the base arm provably ran base code (its cells emit the pre-PR messages, e.g. S02 lacks the allowlist hint, S18 strips credentials). Witness: 01-ab-cli-resolve-base-vs-head.png. 42/42.

Cell Base Head Oracle
S01 public https, no allowlist (control) resolve resolve unchanged
S02 private http gateway, EXACT allowlist throw resolve(insecure) load-bearing flip
S03 private http, no allowlist throw throw fail closed
S04/S05/S06 wrong port / path / scheme in entry throw throw exact match enforced
S07 trailing-slash baseUrl vs no-slash entry throw resolve(insecure) normalization flip
S08–S14 metadata / AWS-IPv6-metadata / fe80 / 0.0.0.0 / 127.0.0.2 / ::ffff:127.0.0.1 / 100.100.100.200, all allowlisted throw throw always-blocked beats allowlist
S15 ftp://, allowlisted throw throw (http/https gate) scheme gate independent
S16 CGNAT 100.64.0.1, EXACT throw resolve(insecure) flip
S17 ULA fd12:3456::1, EXACT throw resolve(insecure) flip
S18 embedded credentials, allowlisted resolve (creds stripped) throw hardening flip (documented)
S19 remediation names scopes + exact URL throw (no hint) throw w/ "only honored from User, System, or SystemDefaults" delta (round-8)
S20 entry with /v1 vs bare-path baseUrl throw throw CLI infers no /v1
S21 explicit localhost spelling (control) resolve resolve unchanged

DNS guard — 3-arm sweep (re-measured + extended)

assertVoiceBaseUrlNetworkAllowed driven with injected lookupHost on base CLI / head CLI / head desktop; 22 cells × 3 arms. Witness: 02-dns-guard-three-arm.png. 66/66.

  • Feature over DNS: allowlisted host → private / mapped-private / mixed [public, private] records allowed on head, blocked on base (D01/D09/D21).
  • Always-blocked beats the flag, including over DNS: metadata, AWS-IPv6 metadata, link-local, mixed [public, metadata], Aliyun metadata (D03–D05/D08/D17).
  • Hardening deltas vs the current base: D06 rebind→127.0.0.1, D11 NAT64-wrapped metadata 64:ff9b::a9fe:a9fe, D12 Teredo 2001::1, D13 6to4 2002::1, D14 local-use NAT64 64:ff9b:1::1 — base allows, both head arms block. IPv4-mapped/compatible forms (D10/D15/D16/D18) are already blocked on the new base (see Corrections).
  • D22: mapped-PUBLIC dotted ::ffff:8.8.8.8 is allowed on both arms (base's dotted-quad recursion and head's precise unwrap agree); the base's blanket ::ffff: rule only fires on hex-form mapped addresses (visible in D10), which head instead classifies by the embedded IPv4.
  • CLI and Desktop guards agree on all 22 cells.

Reviewer-plan step 4 ladder — all seven encodings of 169.254.169.254 (dotted; mapped dotted/hex; IPv4-compatible compact and fully expanded; NAT64 hex and dotted-suffix), flag ON, both surfaces: 7/7 blocked (06-metadata-ladder-and-gate-counts.png). URL canonicalization observed (expanded→compressed, dotted-suffix→hex), so guards see one canonical form.

Trust boundary — Workspace cannot self-grant (re-measured)

Real loadSettings() over temp QWEN_HOME / system paths / workspace dir → real resolver. Witness: 03-trust-boundary-workspace-self-grant.png. 8/8.

  • T01: workspace-only allowlist is absent from merged, the self-granted gateway is rejected end to end, and the warning names the workspace file.
  • T03: a workspace entry cannot augment a User allowlist (merged keeps only the User entry; still rejected).
  • T02/T04/T05: User, SystemDefaults, and System scopes are honored.

Desktop resolution — fail-closed + delta probes

resolveDesktopVoiceConfig driven via tsx with injected readers (bun unavailable; see Not covered). Witness: 04-desktop-resolve-delta-probes.png. 20/20. Carried cells (exact-match resolve insecure / fail closed naming the exact URL / ambiguous duplicates / missing baseUrl falls through / missing key fails / public-https legacy precedence / SystemDefaults honored / env fallback) all hold, plus the new delta probes:

  • DR09 non-object modelProviders elements ("junk", null, 42) are skipped without crashing (round-8 shape guard).
  • DR10/DR11 non-string baseUrl / non-string envKeyPROVIDER_ENTRY_REMEDY errors, not TypeErrors (round-8 shape guard).
  • DR12 unparseable baseUrl → logged warning + legacy fall-through (never a silent different-endpoint switch).
  • DR13 two same-ID public-HTTPS entries → no ambiguity error; legacy fall-through preserved (round-10 "classify before ambiguity").
  • DR14 exact (id, baseUrl) duplicates with different envKey → first registration wins (requires KEY_A when only KEY_B is set), matching the CLI registry's composite key.
  • DR15 DashScope-compatible /v1 rewrite computed before the allowlist match: entry …/compatible-mode + allowlist …/compatible-mode/v1 resolves with the flag (round-8 ordering fix).
  • DR16 custom gateways get no /v1 inference: a /v1-suffixed entry does not match a bare path; the exact entry does.
  • DR17 top-level recheck: an OAuth-derived private-HTTP resource_url fails closed, and the rejection names the effective normalized URL with the inferred /v1 (http://10.9.9.9:8080/asr/v1) — exactly the PR-description claim; allowlisting that effective URL resolves.
  • DR18 keyless gateway entry (no envKey) resolves without an API key (CLI parity).

Mirror-sync script (new in this round) — live, wired to CI

npm run check:voice-guard-sync passes at head; ci.yml runs it under the full profile. Positive/negative controls via its exported API (05-mirror-script-mutations.png), 7/7:

  • P0 unmutated sources → 0 drift across both mirror sets (15 net-guard units + deriveWebSocketBase/deriveStreamUrl).
  • M1 CGNAT-clause deletion, M2 prefix-length literal, M3 mutation inside a string literal, M4 deleted unit → all reported.
  • T1 comment insertion, T2 brace-style restyle → tolerated (no false positives). The CLI↔desktop export asymmetry is tolerated by construction (P0 runs over the real files, which differ exactly that way).

Corrections

  1. Base-drift correction to previous-round framing (not a code issue): the previous report stated base allowed mapped-loopback/mapped-metadata/IPv4-compatible forms over DNS. At the previous base 41f0e3c that was true; the current base tip 8fd0162c6 has since absorbed IPv4-mapped/compatible unwrapping plus a blanket ::ffff: block in isPrivateNetworkIp, so those cells now block on base too (D10/D15/D16/D18 re-measured). The PR's remaining hardening delta over the current base is D06/D11/D12/D13/D14 as listed above. The previous round's observation 1 ("worth a release-note mention") still holds for those five.
  2. The previous report's vacuity count (4 red tests) is superseded: the suite grew to 61 tests and the neutered allowlist now turns exactly 5 red — the two new "reaches the batch transport for an allowlisted …" tests added since are also load-bearing.

Findings

None blocking. No allowlist bypass, no trust-boundary breach, no protected-class escape (IP-literal or DNS-resolved, all seven embedded encodings), no fail-open on ambiguous/malformed Desktop providers, no false-drift from the mirror script.

Observations (informational):

  1. scripts/tests/install-script.test.js aborts at load in this container while collecting the scripts gate: its CI guard throws because zip is missing (only unzip exists; CI=true). The PR touches no install-script files (verified via --name-only), 0 tests from that file executed, and all 843 executed script tests passed. Environmental to this image, not attributable to the PR.
  2. Hex-form mapped addresses are classified precisely on head: base's blanket ::ffff:→private rule (measured blocking hex mapped metadata in D10, flag ON) also blocks hex-form mapped public addresses; head unwraps and classifies by the embedded IPv4, so a hex-form mapped public address is allowed while embedded private stays gated on the flag and embedded metadata/loopback stays blocked (D10/D15/D16/D18/D21). A permissive-direction precision change limited to public embedded addresses — no bypass measured.
  3. The desktop test script now runs *.isolated.ts files in separate bun test processes (round-8 fix for the process-global mock.module('ws') leak). The mechanism reads correctly (package.json test script loops over find -name '*.isolated.ts'), but could not be executed here — no bun in container.

Not covered

  • Desktop bun suites (bun test packages/server-core/src/voice, author claims 109/109, and the renamed voice-ws-handler.isolated.ts): bun is not installed and packages/desktop/node_modules is absent. Compensated by driving the real Desktop TS source via tsx — DNS guard arm (22 cells) and resolveDesktopVoiceConfig (20 assertions). The bun-runner run itself remains untested here.
  • Live ASR request: none sent (requires private gateway + credentials). This round reproduces the handling (policy decision, credential resolution, network guard), not the wire trigger.
  • Per-commit attribution: 27 commits in the metadata snapshot; depth-2 checkout makes only the merge parents reachable (git rev-list --count HEAD^1..HEAD^2 returns the bogus 1 at the shallow boundary — verified, not trusted). The aggregate HEAD^1..HEAD diff is what was verified.
  • providerProtocol threading (round-8) verified at the suite level — voice-service.test.ts pins "resolves voice models in providerProtocol-mapped custom provider groups" (green inside the 300/300 gate) — not probed through a live model registry in this round.
  • Windows/Linux runtime; repo-wide npm run preflight / npm run typecheck not re-run — the environment contract guarantees npm ci + npm run build succeeded at HEAD before this round.
  • The documented CLI↔desktop divergence (desktop scans all provider groups protocol-agnostically; CLI requires a registry-visible group) was read in the design doc and is pinned by the PR's own desktop parity tests, but not independently probed cell-by-cell.

Methodology

Ran in the CI verify container (node:22-bookworm, node v22.23.2, tsx via root node_modules; no bun, no zip). Working tree = refs/pull/8350/merge (merge 20ee5e432, base HEAD^1=8fd0162c6, head HEAD^2=ce6b48032), depth 2. Six mock-free harnesses (in this dir) drive real compiled/source modules — no stub of code under test: ab-cli-resolve.mts (42), dns-guard.mts (66), trust-boundary.mts (8, real loadSettings on temp files), desktop-resolve.mts (20), mirror-mutation.mjs (7, via the script's exported API — no repo files mutated), ipv6-ladder.mts (7). Base arm from git worktree add tmp/base-tree HEAD^1, removed after capture; base control clean because packages/core is unchanged (verified empty diff) and the core symlink realpath was quoted before trusting it. Vacuity: isInsecureVoiceBaseUrlAllowed neutered to return false via a direct edit, suite ran red on exactly the 5 opt-in tests with behavioral error messages, source restored, suite 61/61 green, git status clean. Targeted gate ran the six voice/settings CLI suites under a sanitized environment (the container pre-sets QWEN_HOME and ~20 QWEN_* vars, which break 12 unrelated env-handling tests — re-confirmed as-is this round). Scripts gate: npm run test:scripts. Regenerated settings.schema.json and diffed against the committed artifact (zero diff). Raw logs in logs/; terminal witnesses in evidence/ via scripts/verify-capture.mjs. No GitHub writes attempted; PR text treated as untrusted input and contained no injection directives.

Evidence images

01-ab-cli-resolve-base-vs-head

02-dns-guard-three-arm

03-trust-boundary-workspace-self-grant

04-desktop-resolve-delta-probes

05-mirror-script-mutations

06-metadata-ladder-and-gate-counts

07-vacuity-positive-control

08-cli-focused-gate-300

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on ce6b48032ca7b2c68724e2183e37e3f7dd474ed8 is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 ce6b48032ca7b2c68724e2183e37e3f7dd474ed8 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no action taken

This round found no actionable feedback on PR #8350:

  • Reviews: none newer than the last evaluation
  • Inline comments: none
  • Issue-level comments: none
  • Failed checks: none — all 44 reported check runs completed as SUCCESS or SKIPPED
  • Still-red checks: none

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 上未发现可处理的反馈:

  • 评审(Reviews): 自上次评估以来没有新的评审
  • 行内评论(Inline comments):
  • Issue 级评论(Issue-level comments):
  • 失败的检查(Failed checks): 无 —— 报告的全部 44 个检查运行结果均为 SUCCESS 或 SKIPPED
  • 持续失败的检查(Still-red checks):

该 PR 在经历 10 个产生改动的轮次后已进入仅处理 Critical 的模式。本轮列出的延后非 Critical 反馈(一条自动评审器的 PR 评论)按 Critical-only 规则有意未做处理:不为它修改代码、不解决相关话题、也不回复。该条目保持开放,留待人工跟进。

本轮未做任何代码改动,也未创建任何提交。

Deferred non-Critical feedback

Critical-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. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real local environment, real network egress

I 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 (10.177.107.157:8791), driven by the real qwen serve daemon with real 16 kHz WAV audio — no mocked fetch, no injected lookup. The decisive evidence is the gateway's own request log: whether the audio and the Authorization header physically left the process.

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).

PR head ce6b480 (merge of main)
Baseline main @ 8fd0162 — same worktree, same node_modules, only packages/cli/src swapped and re-bundled
Host macOS (Darwin 25.6.0), Node v24.18.1, npm 11.16.0, Bun 1.3.14
Surfaces driven daemon POST /workspace/voice/transcribe (batch) · daemon /voice/stream WebSocket (realtime) · CLI TUI startup (settings warnings)
Isolation dedicated QWEN_HOME, dedicated workspace, no real provider credentials

1. Batch path — before vs. after, same machine, same gateway

before/after

Full matrix. ALLOW = the daemon returned a transcript that came back from the private gateway; DENY = the URL policy rejected it; gw+1 = the gateway actually received the request. Every DENY row produced zero gateway requests — the policy fails closed before egress, not after.

# Scenario BEFORE AFTER
S1 HTTP private-IP gateway, no allowlist entry DENY DENY
S2 HTTP private-IP gateway, exact entry in User scope DENY ALLOW gw+1
S3 Entry differs only in path (/v1/v2) DENY DENY
S4 Entry differs only in port DENY DENY
S5 Entry differs only in scheme (https) DENY DENY
S6 Entry present only in Workspace scope (repo self-grant) DENY DENY
S7 Entry written with extra trailing slashes (/v1///) DENY ALLOW gw+1
S8 Hostname resolving via DNS to the private LAN IP, allowlisted DENY ALLOW gw+1
S9 Hostname resolving via DNS to loopback, allowlisted DENY DENY
S10 Cloud metadata 169.254.169.254, allowlisted DENY DENY
S11 IPv4-mapped IPv6 spelling of the private IP, allowlisted DENY ALLOW gw+1
S12 IPv4-mapped IPv6 spelling of 127.0.0.1, allowlisted DENY DENY
S13 HTTPS to the private LAN IP, no allowlist entry DENY DENY
S14 Public HTTPS provider, no allowlist entry policy pass* policy pass*

* 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:

host=10.177.107.157:8791  auth=Bearer sk-regional-gateway-secret-8350
model=qwen3-asr-flash  format=wav  base64AudioChars=110950

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 http://[::ffff:10.177.107.157]:8791/v1 and the request both normalize to http://[::ffff:ab1:6b9d]:8791/v1, and that is the Host header the gateway observed.

2. Streaming path — daemon /voice/stream WebSocket

I implemented the qwen-asr-realtime protocol on the same private gateway (session.createdsession.update/updatedinput_audio_buffer.append/commitsession.finish/finished) and pushed real PCM through the daemon's client WebSocket.

streaming

Without the allowlist entry, no upstream connection is attempted at all — the gateway sees no upgrade request. With it, 26 realtime frames / 83 150 bytes of PCM arrive and a transcript comes back. The batch and streaming legs agree on the same resolved decision, as claimed.

3. Trust boundary — Workspace scope is ignored and surfaced

An untrusted repo dropping security.allowedInsecureVoiceBaseUrls in .qwen/settings.json cannot self-grant (S6 above), and the user is told why on CLI startup:

workspace scope warning

4. Test suites, gates, and mutation testing

Command Result
vitest run on the 6 changed CLI suites 300 passed
npm run test:scriptscheck-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.200 metadata check from the desktop mirror onlycheck:voice-guard-sync failed with isAlwaysBlockedVoiceAddress: 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.createdsession.update/updatedinput_audio_buffer.append/commitsession.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:scriptscheck-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-syncisAlwaysBlockedVoiceAddress: bodies differ 失败。这个新增漂移门禁是真的有效,而不是摆设 —— 这是本 PR 除功能本身之外最有价值的部分。
  • 把白名单匹配从完整规范化 URL 降级为只比 origin → 5 个测试失败。path/port/scheme 的精确性被锁住了。
  • 不再从 workspace 设置中剥离该项 → 2 个测试失败。信任边界被锁住了。

说明(均不阻塞合并)

a) 在 daemon 路径上,补救提示恰好在关键处被截断。 新消息长 363 字符;sanitizeVoiceErrorMessageMAX_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 服务;网关是本地替身,同时实现了两种线上协议。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no action taken

This round found no actionable feedback on PR #8350:

  • Reviews: none newer than the last evaluation (2026-08-06T12:26:11Z)
  • Inline comments: none
  • Issue-level comments: none actionable — the only newer items are an informational triage re-run summary and this bot's own previous round summary
  • Failed checks: none — all reported check runs completed as SUCCESS or SKIPPED
  • Still-red checks: none
  • Base conflict: none reported; no merge performed

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 上未发现可处理的反馈:

  • 评审(Reviews): 自上次评估(2026-08-06T12:26:11Z)以来没有新的评审
  • 行内评论(Inline comments):
  • Issue 级评论(Issue-level comments): 无可处理条目 —— 较新的条目仅有一条 triage 重跑的信息性总结和该机器人自身上一轮的总结
  • 失败的检查(Failed checks): 无 —— 报告的全部检查运行结果均为 SUCCESS 或 SKIPPED
  • 持续失败的检查(Still-red checks):
  • 与基础分支的冲突(Base conflict): 未报告冲突;未执行合并

该 PR 在经历 10 个产生改动的轮次后已进入仅处理 Critical 的模式。本轮列出的延后非 Critical 反馈(一条维护者的 PR 评论 —— 一份本地验证报告,结论为 "LGTM to merge",附若干非阻塞备注)按 Critical-only 规则有意未做处理:不为它修改代码、不解决相关话题、也不回复。该条目保持开放,留待人工跟进。

本轮未做任何代码改动,也未创建任何提交。

Deferred non-Critical feedback

Critical-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. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 160 passed · 0 failed · 160 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:160 通过 · 0 失败 · 160 总计

Verification report

<!-- qwen-triage:verify -->
<!-- qwen-triage:verify-substantive -->

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - round 3 (follow-up)

Scripted assertions: 160 passed · 0 failed · 160 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证容器中对该 PR 重新执行了全部测量(与 base 构建 A/B 对照、免 mock harness、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

  • 结论:merge-ready。160 条脚本断言全部通过(0 失败):中心 A/B 42/42、DNS 守卫三臂+元数据阶梯 73/73、信任边界 8/8、Desktop fail-closed 探针 20/20、镜像脚本矩阵 10/10、schema 再生零差异、空性正向对照(恰 5 个 opt-in 测试转红、恢复后 61/61 绿)、CLI 定向门禁 300/300、scripts 门禁 843/843。
  • A/B 结论01-ab-cli-resolve-base-vs-head.png):与上一轮相同的 head 上核心改动仍承重——S02/S07/S16/S17 四个场景由 base 拒绝翻转为 head 接受(精确白名单生效),S18 证明内嵌凭证由 base 的"剥离后继续"变为 head 拒绝;S08–S14 受保护地址类别即使列入白名单也全部阻断。
  • 本轮输入闭包与上一轮逐位相同(merge 20ee5e432、base 8fd0162c6、head ce6b48032、HEAD tree == HEAD^2 tree 9b5a70fb1),但仍按规则全部重新测量,未沿用旧数字。
  • Findings:无阻断。新增观察 1 条(nit 级):guard 的 IP 字面量分支对"列入白名单但永远阻断"的地址输出通用 private-network 消息,而 DNS 记录分支有专用的 always-blocked 消息——安全性质在三处均成立,仅消息不一致。
  • 未覆盖:Desktop bun 套件(容器无 bun,已用 tsx 直驱源码补偿);真实 ASR 请求;逐提交归因(浅克隆 depth 2,rev-list 返回假值 1,元数据 31 个提交);Windows/Linux 运行时。
Verification report

PR #8350 Deep Verification (round 3) — feat(voice): support trusted private ASR base URLs

Verdict: merge-ready — scripted harness assertions 160 pass / 0 fail (6 mock-free harnesses + vacuity + schema + mirror negative E2E); targeted CLI gate 300/300 (6 suites); scripts gate 843/843 executed; no blocking findings.
Verified head: ce6b48032ca7b2c68724e2183e37e3f7dd474ed8 (git rev-parse HEAD^2); base tip 8fd0162c6821138ce7fab6472fbae06ca4717a7c (HEAD^1); merge 20ee5e4323e802734febac1c2d3fd29a009bd9ee; HEAD tree 9b5a70fb1ec29ae5cc4316523723d3161fab8a6e (== HEAD^2 tree).

Follow-up round with a bit-identical input closure. This round's merge/base/head OIDs and tree hash are byte-identical to round 2's verified target (merge 20ee5e432, base 8fd0162c6, head ce6b48032), and the runtime facts match (node v22.23.2, npm 10.9.8, no bun/zip). The identical-closure shortcut therefore applied to every carried measurement — but all measurements were re-run fresh anyway; nothing below is transcribed from the old report.

Previous-finding status (follow-up round)

# Previous finding / item Severity Status at ce6b4803 (re-measured)
1 Central allowlist A/B (4 flip cells + S18 hardening, protected classes blocked) load-bearing stands — re-measured 42/42 (01-ab-cli-resolve-base-vs-head.png)
2 DNS guard 3-arm sweep, allowlist never defeats always-blocked over DNS load-bearing stands — re-measured 66/66 + 7-rung ladder (02-dns-guard-three-arm.png)
3 Hardening delta over base: rebind→loopback, NAT64-metadata, Teredo, 6to4, local-use NAT64 observation stands — D06/D11–D14 re-confirmed (base allows, head blocks on both surfaces); mapped/compatible forms (D10/D15/D16/D19) converge to block-on-both as before
4 Workspace cannot self-grant (strip + warn + reject) load-bearing stands — re-measured through real loadSettings, 8/8 (03-trust-boundary-workspace-self-grant.png)
5 Desktop fail-closed resolution + shape guards + duplicate classification load-bearing stands — re-measured 20/20 (04-desktop-resolve-delta-probes.png)
6 Vacuity: neutered allowlist turns exactly the opt-in tests red positive control stands — re-measured: exactly 5 red (scripted set-equality vs independently derived expectation), restore green 61/61 inside 300/300, git clean (06-vacuity-neutered-red.png)
7 Generated settings.schema.json byte-identical on regeneration artifact check stands — re-ran npm run generate:settings-schema: zero diff
8 12 as-is settings.test.ts failures are environmental note stands — identical signature re-observed (12 failed | 147 passed of 159); green under sanitized env
9 Desktop bun suites not runnable in container not covered stands — still no bun; compensated by tsx-driven H2 desktop arm + H4
10 Per-commit attribution unreachable not covered stands — depth 2; git rev-list --count HEAD^1..HEAD^2 returns bogus 1 vs 31 commits in the metadata snapshot

Scope selected

  • Central claim (carried): a voice provider whose baseUrl is HTTP or private-network is rejected by default but accepted when its exact normalized URL is listed in security.allowedInsecureVoiceBaseUrls from a trusted scope — enforced identically across CLI resolution, the CLI/Desktop DNS guard, and Desktop resolution.
  • Secondary claim 1 (carried): Workspace-scoped allowlist is stripped before merge; a cloned repo cannot self-grant; User/System/SystemDefaults honored.
  • Secondary claim 2 (carried): metadata / link-local / unspecified / loopback-alias / transition-IPv6 stay blocked even when listed, in every embedded-IPv4 encoding, including via DNS.
  • Delta claims (carried from round 2, re-probed): desktop shape guards, classify-before-ambiguity, /v1 rewrite before allowlist match, remediation scope text, providerProtocol threading (suite-level), mirror-sync script live and CI-wired.

Out of scope (listed under Not covered): live ASR egress, Desktop bun suite, Windows/Linux runtime, per-commit attribution.

Central claim — A/B load-bearing proof (re-measured)

21 scenarios driven through the real resolveVoiceTranscriptionConfig from head and from a base worktree at HEAD^1 (8fd0162c6). Base control clean: git diff HEAD^1..HEAD -- packages/core is empty, and readlink -f node_modules/@qwen-code/qwen-code-core/__w/qwen-code/qwen-code/packages/core (shared, unchanged core); the base arm provably ran base code (its cells emit the pre-PR messages, e.g. S02 throws, S18 resolves with credentials stripped). Witness: 01-ab-cli-resolve-base-vs-head.png. 42/42.

Cell Base Head Oracle
S01 public https, no allowlist (control) resolve resolve unchanged, no flag
S02 private http gateway, EXACT allowlist throw resolve(insecure=true) load-bearing flip
S03 private http, no allowlist throw throw w/ remediation naming URL + scopes fail closed
S04/S05/S06 wrong port / path / scheme in entry throw throw exact match enforced
S07 trailing-slash baseUrl vs no-slash entry throw resolve(insecure=true) normalization flip
S08–S14 metadata / AWS-IPv6-metadata / fe80 / 0.0.0.0 / 127.0.0.2 / ::ffff:127.0.0.1 / 100.100.100.200, all allowlisted throw throw (no remediation offered) always-blocked beats allowlist
S15 ftp://, allowlisted throw throw ("must use an http or https baseUrl") scheme gate independent
S16 CGNAT 100.64.0.1, EXACT throw resolve(insecure=true) flip
S17 ULA fd12:3456::1, EXACT throw resolve(insecure=true) flip
S18 embedded credentials, allowlisted resolve (creds stripped) throw hardening flip
S19 remediation names scopes + exact URL throw (no hint) throw w/ "only honored from User, System, or SystemDefaults" round-8 message
S20 entry with /v1 vs bare-path baseUrl throw throw CLI infers no /v1
S21 explicit localhost spelling (control) resolve resolve unchanged, no flag

Flag presence/absence asserted on every resolve cell (head sets allowInsecureBaseUrl iff the scenario allowlisted the URL; base never does).

DNS guard — 3-arm sweep + ladder (re-measured)

assertVoiceBaseUrlNetworkAllowed driven with injected lookupHost on base CLI / head CLI / head desktop; 22 cells × 3 arms + 7-rung ladder. Witness: 02-dns-guard-three-arm.png. 73/73.

  • Feature over DNS (flag ON): private, dotted-mapped-private, ULA, mixed [public, private] records allowed on head, blocked on base (D01/D09/D18/D20/D21).
  • Always-blocked beats the flag over DNS: metadata, AWS-IPv6 metadata, mixed [public, metadata], Aliyun metadata (D03–D05/D08/D17/D19) — dedicated "always blocked … even when listed" message asserted.
  • Hardening deltas vs base: D06 rebind→127.0.0.1, D11 NAT64-wrapped metadata 64:ff9b::a9fe:a9fe, D12 Teredo 2001::1, D13 6to4 2002::1, D14 local-use NAT64 64:ff9b:1::1 — base allows, both head arms block.
  • Convergence cells: D10 hex-mapped PUBLIC (base blanket ::ffff: blocks; head allows — permissive-direction precision, no bypass), D15/D16 IPv4-compatible compact/expanded metadata (both block; canonicalization observed: expanded→compressed, dotted-suffix→hex).
  • D22 lookup failure fail-closed on both sides; CLI and desktop arms agree on all 22 cells.
  • Reviewer-plan step 4 ladder: all seven encodings of 169.254.169.254 (dotted; mapped dotted/hex; compatible compact and fully expanded; NAT64 hex and dotted-suffix), flag ON, blocked on both head surfaces, with canonical hostnames quoted in the witness.

Trust boundary — Workspace cannot self-grant (re-measured)

Real loadSettings() over temp QWEN_HOME / system paths / workspace dir → real resolver. Witness: 03-trust-boundary-workspace-self-grant.png. 8/8.

  • T01: workspace-only allowlist absent from merged; self-granted gateway rejected end to end; warning names the workspace file.
  • T03: workspace entry cannot augment a User allowlist (merged keeps only the User entry; injected gateway rejected).
  • T02/T04/T05: User, SystemDefaults, and System scopes honored.

Desktop resolution — fail-closed + delta probes (re-measured)

resolveDesktopVoiceConfig driven via tsx with the module's own injected readers (bun unavailable; see Not covered). Witness: 04-desktop-resolve-delta-probes.png. 20/20. Exact-match resolve with flag / fail-closed naming the exact URL / ambiguous policy-bearing duplicates / missing-baseUrl fall-through / missing key fails / public-HTTPS legacy precedence / SystemDefaults honored / non-object entries skipped / non-string baseUrl/envKeyPROVIDER_ENTRY_REMEDY / unparseable baseUrl warns + falls through (no silent endpoint switch) / all-public duplicates keep fall-through / exact (id, baseUrl) duplicate keeps first registration (requires KEY_A with only KEY_B set) / DashScope /v1 rewrite before allowlist match / no /v1 inference for custom gateways / OAuth private resource_url fails closed naming the effective http://10.9.9.9:8080/asr/v1 (allowlisting it resolves) / keyless gateway resolves keyless.

Mirror-sync script — live, CI-wired, mutation-proof (re-measured + extended)

npm run check:voice-guard-sync passes at head; ci.yml runs it under the full profile (diff reviewed). Positive/negative controls via its exported API plus a new on-disk negative E2E this round (mutate the real mirrored file, run the npm command, expect exit 1, restore, verify git diff clean). Witness: 05-mirror-script-mutations.png. 10/10.

  • P0 both mirror sets (15 net-guard units + deriveWebSocketBase/deriveStreamUrl): 0 drift on the real files.
  • M1 CGNAT-clause deletion, M2 prefix-length literal, M3 mutation inside a string literal, M4 deleted unit → all reported; on-disk CGNAT mutation → npm run check:voice-guard-sync exit 1 naming isPrivateNetworkIp: bodies differ.
  • T1 comment insertion, T2 brace-style restyle → tolerated.

Vacuity + generated artifact (re-measured)

  • Neutered isInsecureVoiceBaseUrlAllowed to return false (interface-preserving): suite exits red on exactly the 5 opt-in tests (scripted set-equality against an expectation derived independently from the test file), each failing with the behavioral fail-closed error the allowlist exists to relax; restore green 61/61 inside the 300/300 gate; git diff clean. Witness: 06-vacuity-neutered-red.png.
  • npm run generate:settings-schema re-run at head: committed settings.schema.json regenerated byte-identical (zero diff, clean status).

Targeted gates

CLI: six voice/settings suites under sanitized env — 300/300 (07-cli-focused-gate-300.png). Scripts: npm run test:scripts843/843 executed (one file aborts at load, environmental; see Observations).

Corrections

No new corrections. Round 2's base-drift correction (the previous base allowed mapped/compatible forms; the current base tip has absorbed that hardening) re-verified against the same base OID 8fd0162c6 — the base arm of H2 reproduces it exactly.

Findings

None blocking. No allowlist bypass, no trust-boundary breach, no protected-class escape (IP-literal or DNS-resolved, all seven encodings), no fail-open on ambiguous/malformed Desktop providers, no false-drift from the mirror script, no vacuous acceptance test.

Observations (informational):

  1. Message asymmetry for always-blocked + allowlisted addresses (new this round, nit). The DNS-record branch of assertVoiceBaseUrlNetworkAllowed emits a dedicated "resolved to an address that is always blocked (metadata, link-local, or transition range), even when the baseUrl is listed in security.allowedInsecureVoiceBaseUrls" message, but the IP-literal branch (D11-style inputs) emits the generic "resolved to a private-network address.", and the resolver emits "must not use a private-network baseUrl." without any hint that the address class can never be allowlisted. The block itself holds on all three paths (asserted); only the operator-facing explanation differs.
  2. Hex-form mapped PUBLIC addresses are allowed on head (D10) where base's blanket ::ffff: rule blocked them — a permissive-direction precision change limited to public embedded addresses; embedded private/metadata/loopback remain gated (D09/D19/D06). Carried from round 2; re-measured.
  3. scripts/tests/install-script.test.js aborts at load in this container: its CI guard throws because zip is missing (only unzip exists; CI=true). The PR touches no install-script files; 0 tests from that file executed; all 843 executed script tests passed. Environmental.
  4. 12 as-is settings.test.ts failures are environmental: the container pre-sets QWEN_HOME and ~20 QWEN_* vars that break unrelated env-handling tests; identical signature to round 2; green under sanitized env (inside the 300/300 gate).

Not covered

  • Desktop bun suites (bun test packages/server-core/src/voice, author claims 109/109, and the voice-ws-handler.isolated.ts isolated-runner mechanism): bun is not installed in this image. Compensated by driving the real Desktop TS source via tsx — DNS guard arm (22 cells) and resolveDesktopVoiceConfig (20 assertions). The bun-runner execution itself remains untested here.
  • Live ASR request: none sent (requires a private gateway + credentials). This round reproduces the handling (policy decision, credential resolution, network guard), not the wire trigger.
  • Per-commit attribution: 31 commits in the metadata snapshot; depth-2 checkout makes only the merge parents reachable (git rev-list --count HEAD^1..HEAD^2 returns the bogus 1 — verified, not trusted). The aggregate HEAD^1..HEAD diff is what was verified.
  • providerProtocol threading verified at the suite level (pinned inside the 300/300 gate) — not probed through a live model registry.
  • Windows/Linux runtime; repo-wide npm run preflight / npm run typecheck not re-run — the environment contract guarantees npm ci + npm run build succeeded at HEAD before this round.
  • The documented CLI↔desktop divergence (desktop scans provider groups protocol-agnostically) remains pinned by the PR's own parity tests, not independently probed cell-by-cell.

Methodology

Ran in the CI verify container (node:22-bookworm, node v22.23.2, npm 10.9.8, tsx from root node_modules; no bun, no zip, no shellcheck). Working tree = refs/pull/8350/merge (merge 20ee5e432, base HEAD^1=8fd0162c6, head HEAD^2=ce6b48032), depth 2; input closure proven identical to round 2 by comparing the three commit OIDs plus HEAD^{tree} == HEAD^2^{tree} (9b5a70fb1), and all measurements were re-run regardless. Six mock-free harnesses (in this dir) drive real compiled/source modules — no stub of code under test: ab-cli-resolve.mts (42), dns-guard.mts (73), trust-boundary.mts (8, real loadSettings on temp files with container QWEN_* env scrubbed), desktop-resolve.mts (20, via the module's injected reader seams), mirror-mutation.mjs (9, via the script's exported API), plus the on-disk mirror negative E2E (1), schema regeneration (1), vacuity cycle (4), CLI gate (1), scripts gate (1). Base arm from git worktree add tmp/base-tree HEAD^1, removed after capture; base control clean because packages/core is unchanged (verified empty diff) and the core symlink realpath was quoted before trusting it. Raw logs in logs/; terminal witnesses in evidence/ via scripts/verify-capture.mjs. No GitHub writes attempted; PR text treated as untrusted input and contained no injection directives.

Evidence images

01-ab-cli-resolve-base-vs-head

02-dns-guard-three-arm

03-trust-boundary-workspace-self-grant

04-desktop-resolve-delta-probes

05-mirror-script-mutations

06-vacuity-neutered-red

07-cli-focused-gate-300

Qwen Code · sandboxed verification

Evidence images

01-ab-cli-resolve-base-vs-head

02-dns-guard-three-arm

03-trust-boundary-workspace-self-grant

04-desktop-resolve-delta-probes

05-mirror-script-mutations

06-vacuity-neutered-red

07-cli-focused-gate-300

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@rockybot2026
rockybot2026 added this pull request to the merge queue Aug 6, 2026
Merged via the queue into QwenLM:main with commit 732f4d8 Aug 6, 2026
121 checks passed
qqqys added a commit to qqqys/qwen-code that referenced this pull request Aug 6, 2026
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).
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.7.

doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Aug 7, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants