Skip to content

fix(hooks): close four trust-boundary holes in hook execution - #8396

Open
wenshao wants to merge 30 commits into
mainfrom
fix/hooks-security
Open

fix(hooks): close four trust-boundary holes in hook execution#8396
wenshao wants to merge 30 commits into
mainfrom
fix/hooks-security

Conversation

@wenshao

@wenshao wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR closes four independent trust-boundary holes in the hook system, all in the area where repository-controlled configuration meets code execution or network egress:

  1. HTTP hooks no longer follow redirects. Previously the URL whitelist and the DNS-level SSRF checks (private ranges, cloud metadata endpoints) were validated only against the initially configured URL, but the underlying client followed redirects automatically — so any 30x response from a configured or compromised endpoint could re-POST the full hook payload (prompts, tool inputs and outputs, session id) to an arbitrary internal or metadata address with no re-validation. Redirects are now disabled; a 3xx response is treated as a hook failure (non-blocking, same as other HTTP errors).

  2. The HTTP-hook URL whitelist can no longer be set from workspace settings. The settings merge already strips the private-network relaxation flag from workspace scope so a repository cannot self-grant an SSRF bypass; the sibling whitelist key was still honored from workspace scope, letting a repository replace or widen the user's whitelist (e.g. with *) and exfiltrate hook payloads past the boundary the user configured. Both keys are now stripped from workspace scope; user and system scopes are unaffected.

  3. Qwen-internal secrets are never substituted into hook commands, URLs, or headers. An earlier fix stripped the daemon tokens and the private ACP capability from hook child-process environments, but two other paths still resolved them from the process environment: environment-variable references in settings files (resolved at load time, before any child-env sanitization applies) and the HTTP hook feature that interpolates whitelisted environment variables into request URLs and headers. A repository-controlled settings file or hook config could name the daemon bearer token and have it sent over the network. All env-var resolution paths now refuse these variables.

  4. Project-level frontmatter hooks require a trusted folder. Hooks declared in the frontmatter of project subagents (.qwen/agents/) and project skills (.qwen/skills/) were registered unconditionally when the agent was spawned or the skill invoked — even in folders the user explicitly marked untrusted, where the same hooks declared in the repository's settings file would have been blocked. Both registration paths now skip project-level hooks with a warning in untrusted folders; user-level subagents and skills are unaffected, matching the existing user-hook trust semantics.

Why it's needed

These were found during a systematic security audit of the hooks module, and each was reproduced end-to-end before fixing:

  • A repository could define an HTTP hook pointing at an allowlisted host that answers a redirect to a cloud metadata or private address — the guards never ran on the redirect target, and the hook payload (potentially including file contents and credentials present in tool inputs) was delivered there.
  • A user who whitelisted hook destinations to a corporate endpoint could have that boundary silently replaced by any checked-out repository.
  • In daemon mode, a repository could exfiltrate the daemon's bearer token through a single settings entry — no local command execution required.
  • A repository opened in an untrusted folder could achieve arbitrary code execution in the user's session by shipping a subagent or skill whose frontmatter registers command hooks, and nudging the model to invoke it.

Reviewer Test Plan

How to verify

Each fix ships with focused unit tests; run them from the repo root:

  • cd packages/core && npx vitest run src/hooks/httpHookRunner.test.ts — a 302 response is not followed and the redirect target is never contacted (the test asserts the client is invoked with redirect-following disabled).
  • cd packages/cli && npx vitest run src/config/settings.test.ts — workspace-scope whitelist and private-network keys are stripped even when trusted; a user-scope whitelist survives a workspace * entry.
  • cd packages/core && npx vitest run src/hooks/envInterpolator.test.ts src/utils/envVarResolver.test.ts and cd packages/cli && npx vitest run src/utils/envVarResolver.test.ts — internal secret variables are never substituted, even when explicitly whitelisted by config.
  • cd packages/core && npx vitest run src/tools/skill.test.ts src/subagents/subagent-manager.test.ts — project-level frontmatter hooks are not registered when the folder is untrusted, are registered when trusted, and user-level configs register regardless of trust. The two gate tests were mutation-checked: with the gates removed they fail; with them restored all 220 tests in the two files pass.

Broader regression: cd packages/core && npx vitest run src/hooks/ src/subagents/ src/skills/ src/tools/skill.test.ts — 1385 tests green; npm run typecheck clean.

Evidence (Before & After)

N/A — non-UI security fixes; verification is the test output above.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested (CI)
🐧 Linux ⚠️ not tested (CI)

Environment (optional)

Unit tests via npx vitest run; no runtime environment required.

Risk & Scope

  • Main risk or tradeoff: HTTP hooks behind endpoints that legitimately redirect (moved endpoints, load balancers issuing 301s) now fail as hooks — non-blocking, so tool calls proceed, but the hook stops running until its URL is updated. Workspaces that intentionally set the hook URL whitelist in project settings must move it to user settings. Project-level subagents/skills with frontmatter hooks stop firing those hooks in untrusted folders (the intended effect).
  • Not validated / out of scope: Windows/Linux local runs (covered by CI). Two adjacent findings from the same audit are not addressed here: the folder-trust check defaults to trusted when the host does not thread a trust value, and the unrelated command-parsing/permission findings (to be handled separately).
  • Breaking changes / migration notes: behavior changes listed above; no data migration needed.

Linked Issues

N/A — found by internal security audit.

中文说明

本 PR 内容

本 PR 关闭了 hook 系统中四个相互独立的信任边界漏洞,全部位于"仓库可控配置"与"代码执行/网络出口"的交界处:

  1. HTTP hooks 不再跟随重定向。 此前 URL 白名单与 DNS 级 SSRF 检查(内网网段、云元数据端点)只针对初始配置的 URL 校验,但底层客户端会自动跟随重定向——任何来自已配置或被攻陷端点的 30x 响应,都可以把完整的 hook 负载(用户 prompt、工具输入输出、session id)重新 POST 到任意内网或元数据地址,且不做任何重新校验。现在重定向被禁用;3xx 响应按 hook 失败处理(非阻塞,与其他 HTTP 错误一致)。

  2. HTTP hook 的 URL 白名单不再接受 workspace 设置。 设置合并逻辑此前已从 workspace scope 剥离"内网放行"开关,防止仓库自我授权 SSRF 绕过;但姊妹白名单键仍可从 workspace scope 生效,仓库可以替换或扩大用户的白名单(例如写成 *),从而越过用户配置的边界外泄 hook 负载。现在两个键都从 workspace scope 剥离;user 与 system scope 不受影响。

  3. Qwen 内部 secrets 永远不会被替换进 hook 命令、URL 或请求头。 此前的一次修复已把 daemon token 与私有 ACP capability 从 hook 子进程环境中剥离,但仍有两条路径直接从进程环境解析它们:settings 文件中的环境变量引用(加载时解析,早于任何子进程 env 清洗),以及 HTTP hook 将白名单环境变量插值进请求 URL 与请求头的功能。仓库可控的 settings 文件或 hook 配置只要点名 daemon bearer token,就能让它经网络外泄。现在所有环境变量解析路径都拒绝这些变量。

  4. 项目级 frontmatter hooks 需要文件夹受信。 在项目 subagent(.qwen/agents/)与项目 skill(.qwen/skills/)的 frontmatter 中声明的 hooks,此前在 agent 派生或 skill 调用时无条件注册——即使用户已显式把文件夹标记为不可信,而同样的 hooks 写在仓库 settings 文件里本来会被拦住。现在两条注册路径在不可信文件夹下都会跳过项目级 hooks 并告警;user 级 subagent 与 skill 不受影响,与现有 user hook 信任语义一致。

为什么需要

这四个漏洞来自对 hooks 模块的一次系统性安全审计,修复前均已端到端复现:

  • 仓库可以定义一个指向白名单主机的 HTTP hook,该主机应答一个指向云元数据或内网地址的重定向——所有防护都不会对重定向目标再次执行,hook 负载(可能包含工具输入中的文件内容与凭据)被直接投递过去。
  • 用户如果把 hook 目的地白名单限定为公司端点,任何被检出的仓库都可以悄悄替换这个边界。
  • 在 daemon 模式下,仓库只需一条 settings 配置即可外泄 daemon 的 bearer token——无需任何本地命令执行。
  • 在不可信文件夹中打开的仓库,可以通过携带一个 frontmatter 注册了 command hooks 的 subagent 或 skill,并诱导模型调用它,在用户会话中实现任意代码执行。

Reviewer 验证计划

如何验证

每个修复都带有聚焦的单元测试,从仓库根目录运行:

  • cd packages/core && npx vitest run src/hooks/httpHookRunner.test.ts —— 302 响应不会被跟随,重定向目标从未被请求(测试断言客户端以禁用重定向的方式被调用)。
  • cd packages/cli && npx vitest run src/config/settings.test.ts —— workspace scope 的白名单与内网放行键在受信时也被剥离;user scope 的白名单在 workspace 写入 * 时仍然存活。
  • cd packages/core && npx vitest run src/hooks/envInterpolator.test.ts src/utils/envVarResolver.test.ts 以及 cd packages/cli && npx vitest run src/utils/envVarResolver.test.ts —— 即使配置显式把内部 secret 变量列入白名单,也永远不会被替换。
  • cd packages/core && npx vitest run src/tools/skill.test.ts src/subagents/subagent-manager.test.ts —— 不可信文件夹下项目级 frontmatter hooks 不注册,受信时正常注册,user 级配置无论信任与否都注册。这两个门禁测试做过 mutation 验证:去掉门禁它们失败,恢复后两个文件的 220 个测试全部通过。

更广的回归:cd packages/core && npx vitest run src/hooks/ src/subagents/ src/skills/ src/tools/skill.test.ts —— 1385 个测试全绿;npm run typecheck 干净。

证据(前后对比)

N/A —— 非 UI 的安全修复,验证即上述测试输出。

测试平台

macOS ✅ 已测试;Windows ⚠️ 未本地测试(CI 覆盖);Linux ⚠️ 未本地测试(CI 覆盖)。

环境(可选)

仅通过 npx vitest run 运行单元测试,无需运行时环境。

风险与范围

  • 主要风险或取舍:位于合法重定向端点之后(迁移过的端点、返回 301 的负载均衡)的 HTTP hooks 现在会作为 hook 失败——非阻塞,工具调用照常进行,但在 URL 更新前该 hook 不再运行。有意在项目 settings 中配置 hook URL 白名单的 workspace 需要把它移到 user settings。不可信文件夹下,带 frontmatter hooks 的项目级 subagent/skill 将不再触发这些 hooks(预期效果)。
  • 未验证 / 范围外:Windows/Linux 本地运行(由 CI 覆盖)。同一审计中的两个相邻发现不在本 PR 处理:宿主未传递信任值时文件夹信任检查默认为受信;以及不相关的命令解析/权限发现(另行处理)。
  • 破坏性变更 / 迁移说明:行为变更如上;无需数据迁移。

关联 Issue

N/A —— 由内部安全审计发现。

wenshao and others added 5 commits August 3, 2026 01:14
The URL whitelist and DNS-level SSRF checks validate only the initial
URL, but undici's default redirect:'follow' would re-POST the hook
payload (prompts, tool inputs, session data) to any 307/308 target and
connect to blocked metadata/private ranges on 30x — bypassing every
guard the module exists to enforce. Pass redirect:'manual' so a 3xx
lands in the existing non-2xx non-blocking error path.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The workspace merge already strips security.allowPrivateNetworkHooks so
a repository cannot self-grant an SSRF relaxation, but the sibling
whitelist security.allowedHttpHookUrls was still honored from workspace
scope — letting a repo replace the user's hook-payload whitelist (e.g.
with "*") and exfiltrate hook data past the boundary the user
configured. Strip both; user/system scopes keep working.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Fix #7527 stripped INTERNAL_SECRET_ENV_VARS (daemon tokens, private ACP
capability) from hook child environments, but two other env-construction
paths read raw process.env with no denylist: settings  resolution
(envVarResolver, baking values into hook commands/URLs at settings load)
and HTTP hook allowedEnvVars interpolation into URLs/headers sent over
the network (envInterpolator). A repo-controlled settings file or hook
config could name QWEN_SERVER_TOKEN and exfiltrate the daemon bearer
token. Both paths now refuse those names; placeholders stay unresolved
(children get a sanitized env anyway) or interpolate to empty.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Project subagents (.qwen/agents/*.md) and project skills
(.qwen/skills/) are discovered regardless of folder trust — fine for
instructions that only influence the model. But their frontmatter
hooks are repo-supplied code execution, and both registration paths
(addAgentHooks on subagent spawn, registerSkillHooks on skill
invocation) ran unconditionally — so a malicious repo in an UNTRUSTED
folder could get arbitrary commands registered for the session by
inducing a subagent spawn or skill invoke, bypassing the folder-trust
gate Config.getProjectHooks() applies to the same hooks declared in
settings.json. Both paths now skip registration with a warning when
the folder is untrusted; user-level configs are unaffected, matching
the user-hooks trust semantics.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…lver copy

Follow-up to 38754ef — the resolver exists in two copies (cli and
core, already drifted apart on circular-reference handling). The core
copy feeds extensionManager manifest resolution; a third-party
extension could name QWEN_SERVER_TOKEN the same way a repo settings
file could. Apply the identical denylist so the two copies agree on
security semantics.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@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 Author

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

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run at the new head (edb11de1…); my previous pass reviewed aed434a4…, and the PR grew substantially since — the maintainer review rounds asked for more, and got it.

Template looks good ✓

Problem: Still real holes, not theoretical hardening — each has a concrete attack narrative (redirect-to-metadata SSRF, a workspace replacing the user's hook whitelist, one settings entry exfiltrating the daemon bearer token, frontmatter hooks firing in an untrusted folder) and a unit test asserting the gated negative. The maintainer's own review traced each fix's completeness (single call sites for addAgentHooks/registerSkillHooks, no daemon-side bypass via the serve fast path, no other $VAR resolver) — that bar is met.

Direction: Aligned. Closing trust-boundary holes where repo-controlled config meets code execution / network egress is core mission, and the fixes extend existing semantics (getProjectHooks() trust gating, the narrow INTERNAL_SECRET_ENV_VARS denylist) rather than inventing new policy.

Size: Core paths are touched (packages/core/src/{hooks,utils,tools,subagents}, packages/cli/src/{config,utils,serve,services}). Production logic is now ~730 lines vs ~2015 test lines and 2 schema lines — up from ~122 production lines at my last pass, because the review rounds replaced "strip the workspace whitelist" with the reviewer's preferred intersect/narrow design, added the skill side-effect deferral state machine, and hardened edge cases. 730 ≥ 500, so per the core-size policy this is flagged for maintainer awareness (informational — it's a fix, not a refactor, so nothing is blocked on size; the author is a maintainer, noted for the record). Under the 1000-line split advisory.

Approach: Scope feels right for what it became — four headline fixes plus the increments the maintainer explicitly asked for (intersection instead of stripping, session-level decision made explicitly, debug-log diagnosability for the silent substitution, malformed-input hardening). One hygiene note, non-blocking: the PR body is stale relative to the code — it still describes fix #2 as "stripped from workspace scope" and tells users to move workspace whitelists to user settings, while the implementation now narrows (a trusted workspace whitelist survives when covered by a higher-scope one); the added items (hookAggregator concatenation, allowedTools gating, the resolver move, case-insensitive child-env) also aren't listed. Worth a body update before merge so the record matches the diff.

Risk: No elevated risk signals — none of the changed files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

在新 head(edb11de1…)上重跑;上一轮审的是 aed434a4…,此后 PR 显著变大——维护者评审提出了更多要求,也都被落实了。

模板完整 ✓

问题:仍是真实漏洞而非理论性加固——每个都有具体攻击路径(重定向到元数据的 SSRF、workspace 替换用户的 hook 白名单、一条 settings 配置外泄 daemon bearer token、不可信文件夹中触发 frontmatter hooks),且各有断言"被门禁的否定面"的单元测试。维护者本人的评审逐一追踪了每个修复的完整性(addAgentHooks/registerSkillHooks 的单一调用点、serve 快速路径无 daemon 侧绕过、不存在其他 $VAR 解析器)——该标准已满足。

方向:对齐。在"仓库可控配置"与"代码执行/网络出口"交界处关闭信任边界漏洞属于核心使命,且修复是扩展已有语义(getProjectHooks() 的信任门禁、收窄的 INTERNAL_SECRET_ENV_VARS 拒绝名单),而非新造策略。

规模:触及核心路径(packages/core/src/{hooks,utils,tools,subagents}packages/cli/src/{config,utils,serve,services})。生产逻辑现为约 730 行,测试约 2015 行、schema 2 行——上一轮时生产逻辑约 122 行,增长来自评审轮次:用审阅者更倾向的"求交/收窄"设计替换了"剥离 workspace 白名单",新增了 skill 副作用延迟状态机,并加固了边界情况。730 ≥ 500,按核心规模策略标记为维护者知悉(仅提示——这是 fix 而非 refactor,规模不构成拦截;作者本身是维护者,记录在案)。未达 1000 行的拆分建议线。

方案:就其现状而言范围合理——四个标题修复,加上维护者明确要求的增量(用求交替代剥离、显式决定 session 级别、为静默替换补上调试日志可诊断性、畸形输入加固)。一个非阻塞的卫生问题:PR 描述相对代码已过时——仍把修复 #2 描述为"从 workspace scope 剥离",并建议用户把 workspace 白名单移到 user settings,而实现现在是收窄(被更高 scope 白名单覆盖的受信 workspace 白名单会存活);新增项(hookAggregator 拼接、allowedTools 门禁、resolver 迁移、大小写不敏感的子进程环境)也未列出。合并前值得更新描述,使记录与 diff 一致。

风险:无升级风险信号——改动文件均未命中与回滚相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at edb11de1f7af9d40d57656e7d7f50e4851b8c5ac · 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

Independent proposal first (title + motivation only): disable redirects at the client (redirect: 'manual', 3xx = non-blocking failure — never a re-validating redirect follower); strip or intersect the workspace whitelist key next to the already-stripped SSRF flag; reuse the narrow INTERNAL_SECRET_ENV_VARS denylist in both env-resolution paths; gate frontmatter hook registration on isTrustedFolder(), mirroring getProjectHooks(). The PR matches this on all four, and where it goes further it's either the maintainer's explicit ask from the review rounds or a natural companion fix. I verified the load-bearing pieces at the current head rather than taking the diff on faith:

  • Redirect branch is complete. Exactly one request goes out; 3xx lands in a dedicated non-blocking branch that releases the once-slot (a redirect delivers no payload, so it must not burn the hook's single execution), sanitizes the attacker-controlled Location (CRLF/NUL stripped) before it reaches a systemMessage, truncates it to the generic 10 KB cap, and emits the user-visible remedy once per url:event — later firings stay debug-only, and resetOnceHooks() re-arms both sets. The earlier review round's catastrophic-backtracking concern about the pattern matcher is gone: hookUrlPatternCovers is now a linear chunk scan that fails closed on any regex-active character beyond the documented \. escape, with tests pinning lookalike hosts, pre-escaped alternation, and a 100k-segment near-miss staying linear.
  • Whitelist narrowing is enforced where it counts. The intersection lives in mergeSettings, and the serve/daemon paths (channel-settings-store, the workspace-settings route) all load through loadSettings — so the enforcement reaches daemon users even though the warning is still only surfaced by gemini.tsx. An empty intersection falls back to the higher-scope policy (never "allow all"), malformed workspace values (security: null, non-array lists, non-string entries) are dropped without throwing, and System scope wins. I walked the hookUrlPatternCovers edge cases (catch-all both directions, lookalike host, escape normalization) — sound, and the uncertainty direction is fail-closed.
  • Secrets: the denylist check sits before the customEnv lookup, so no caller-supplied map can resolve an internal secret; it's case-insensitive (Windows process.env); the interpolator now warns naming the variable but never its value; and the channel-config path throws hard — which also closes the bonus hole where secretEnv would have returned the variable name as the HMAC secret.
  • Trust gates: the subagent allowlist now includes 'session' (host-supplied agents, the reviewer's item 2 decided explicitly), fails closed for unknown levels, and each arm is pinned by a test. The skill side defers rather than drops: side effects skipped in an untrusted folder are re-applied on re-invocation once trust is granted mid-session, all four state-machine transitions tested (including the double-registration traps). SkillCommandLoader gates allowedTools with the same check and self-heals by re-reading trust per invocation.
  • Prior automated review findings (round-10 ledger R10-1…R10-10): I checked each against the current tree — raw-JSON filtering is now type-guarded, the coverage check is the linear scan, the systemDefaults arm has a mutation-verifiable test, the non-object security guard exists, and scripts/dev.js remaps the memoryScopes subpath too. All addressed.

No correctness blockers, no security holes. The residue is non-blocking and mostly items the maintainer review already raised and the author deliberately answered: isTrustedSubagentLevel stays inline rather than exported next to isTrustedSkillLevel (both arms test-pinned; the bundled/builtin spellings genuinely differ), extension-level trust rests on a documented assumption with no assertion at the extension-load site, the deferred set accepts gated skills that have no side effects (minor log noise), Location is capped at 10 KB rather than a few hundred chars, and getSettingsWarnings still isn't surfaced on serve startup (a diagnostic gap only — enforcement is in the merge). Plus the stale PR body flagged in Stage 1.

Files changed (30 of 36 shown)
File What changed
packages/core/src/hooks/httpHookRunner.ts redirects disabled; 3xx non-blocking with sanitized one-shot warning; once-slot released
packages/core/src/hooks/urlValidator.ts new hookUrlPatternCovers linear coverage check for whitelist narrowing
packages/core/src/hooks/envInterpolator.ts internal-secret denylist; debug warn names the variable, never the value
packages/core/src/hooks/hookAggregator.ts systemMessage concatenated across hooks instead of last-wins
packages/core/src/hooks/index.ts exports hookUrlPatternCovers
packages/core/src/utils/envVarResolver.ts resolver moved from CLI; denylist checked before customEnv
packages/core/src/utils/sanitize-child-env.ts case-insensitive denylist; exports isInternalSecretEnvVar
packages/core/src/tools/skill.ts trust gate plus deferred side-effect state machine
packages/core/src/tools/skill-utils.ts isTrustedSkillLevel helper, fails closed
packages/core/src/subagents/subagent-manager.ts frontmatter hooks gated; user/builtin/extension/session skip the gate
packages/core/src/index.ts new public exports
packages/cli/src/config/settings.ts workspace whitelist narrowed against higher scopes; warnings
packages/cli/src/config/settingsSchema.ts description documents honored scopes and narrowing
packages/cli/src/commands/channel/config-utils.ts internal secrets hard-throw, including secretEnv name leak
packages/cli/src/services/SkillCommandLoader.ts allowedTools gated on level plus folder trust
packages/cli/src/serve/fast-path-settings.ts import path follows the resolver move
packages/cli/src/utils/envVarResolver.ts removed — moved to core
packages/core/package.json envVarResolver subpath export
packages/cli/tsconfig.json subpath mapping
packages/cli/vitest.config.ts subpath alias for tests
scripts/dev.js dev loader remaps the new subpaths
docs/users/features/hooks.md documents scope semantics of the whitelist
packages/vscode-ide-companion/schemas/settings.schema.json description sync
packages/core/src/hooks/httpHookRunner.test.ts redirect matrix, sanitization, one-shot warning, once-slot
packages/core/src/hooks/urlValidator.test.ts coverage edge cases incl. lookalike hosts and linearity
packages/cli/src/config/settings.test.ts narrowing matrix incl. malformed inputs and system precedence
packages/core/src/tools/skill.test.ts gates plus all deferral transitions
packages/core/src/subagents/subagent-manager.test.ts every level arm incl. session; fail-closed unknown levels
packages/core/src/hooks/envInterpolator.test.ts denylist holds even when whitelisted; warn never leaks value
…and 7 more files (remaining test files and aliases)

Testing

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 via the API. The unit tests are load-bearing by construction: they assert the gated negatives (redirect: 'manual' passed to the client, addAgentHooks not called, secret not substituted, once-slot not consumed), so reverting any one fix fails its test. The case-insensitive denylist paths are exercised by explicit case-variant unit tests on the ubuntu job (the Windows job itself is gated/skipped at this stage).

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Post Coverage Comment (ubuntu-latest, 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
ubuntu-latest / Java 11 · 17 · 21, macos-latest / Java 21, windows-latest / Java 21 ✅ success
Classify PR ✅ success
route ✅ success
review-pr ⏳ in progress (bot orchestration, not PR CI)

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle the remaining efficacy question: @qwen-code /verify — whether the four gates hold as an A/B against the base build with mock-free harnesses is not provable from the diff alone. That run was already triggered by this /triage invocation and is in progress (run 30967868098); its report will land in the verification thread here.

Not verified: I did not re-run the unit suite or the author's mutation checks myself (this run never executes PR code) — the "mutation-checked gates" and "tested on macOS" statements are the author's claims, consistent with but not substituted by the green CI above. Windows/macOS CLI test jobs are gated/skipped at this stage.

中文说明

代码审查

先写独立方案(仅凭标题与动机):在客户端禁用重定向(redirect: 'manual',3xx 按非阻塞失败处理——绝不做"重新校验的重定向跟随器");把 workspace 白名单键与已剥离的 SSRF 开关一起剥离或求交;在两条环境变量解析路径中复用收窄的 INTERNAL_SECRET_ENV_VARS 拒绝名单;用 isTrustedFolder() 给 frontmatter hooks 加门禁,镜像 getProjectHooks()。PR 在四点上都与此一致;超出部分要么是评审轮次中维护者明确提出的要求,要么是自然的配套修复。承重的部分我逐一在当前 head 上核实,而非照单全收:

  • 重定向分支是完整的。 只发出一次请求;3xx 落入专门的非阻塞分支:释放 once 槽位(重定向未投递负载,不能烧掉 hook 的唯一一次执行)、在攻击者可控的 Location 进入 systemMessage 前做净化(剥离 CRLF/NUL)、按通用 10 KB 上限截断、并按 url:event 只向用户提示一次——后续触发只进调试日志,resetOnceHooks() 会重置两个集合。上一轮评审担心的模式匹配器灾难性回溯已消除:hookUrlPatternCovers 现在是线性分块扫描,对 \. 转义之外的任何正则活性字符一律失败关闭,且有测试钉住形似主机、预转义选择分支、以及 10 万分段近似匹配仍保持线性。
  • 白名单收窄在关键处生效。 求交逻辑位于 mergeSettings,而 serve/daemon 路径(channel-settings-store、workspace-settings 路由)都经 loadSettings 加载——因此即使警告仍只由 gemini.tsx 呈现,强制力也覆盖 daemon 用户。空交集回退到更高 scope 的策略(绝不变成"全放行"),畸形 workspace 值(security: null、非数组列表、非字符串条目)被丢弃而不抛异常,System scope 最终胜出。我逐一检查了 hookUrlPatternCovers 的边界情况(双向通配、形似主机、转义归一化)——可靠,且不确定时方向为失败关闭。
  • Secrets: 拒绝名单检查位于 customEnv 查找之前,调用方提供的映射也无法解析内部 secret;大小写不敏感(Windows process.env);插值器现在会发出点名变量(绝不输出其值)的警告;channel 配置路径直接抛错——顺带堵上了 secretEnv 会把变量当作 HMAC secret 返回的额外漏洞。
  • 信任门禁: subagent 允许名单现在包含 'session'(宿主提供的 agent,评审第 2 点已被显式决策),未知级别失败关闭,每个分支都有测试钉住。skill 侧采用"延迟"而非"丢弃":不可信文件夹中被跳过的副作用,会在会话中途信任授予后的再次调用时补上,状态机四种转移均有测试(包括双重注册的陷阱)。SkillCommandLoader 用同一检查给 allowedTools 加门禁,并靠每次调用重读信任状态自愈。
  • 上一轮自动评审的发现(round-10 台账 R10-1…R10-10): 逐条对当前代码树核对——原始 JSON 过滤已有类型守卫、覆盖检查已是线性扫描、systemDefaults 分支有可 mutation 验证的测试、非对象 security 守卫存在、scripts/dev.js 也重映射了 memoryScopes 子路径。全部已处理。

无正确性阻塞、无安全漏洞。遗留项均非阻塞,且多为维护者评审已提出、作者已有意回应过的:isTrustedSubagentLevel 仍为内联而非与 isTrustedSkillLevel 并列导出(两个分支均有测试钉住;bundled/builtin 拼写本就不同)、extension 级信任依赖文档化假设而扩展加载点无断言、延迟集合会接纳没有副作用的被门禁 skill(轻微日志噪音)、Location 上限为 10 KB 而非几百字符、getSettingsWarnings 仍未在 serve 启动路径呈现(仅诊断性缺口——强制力在合并逻辑中)。再加上 Stage 1 指出的 PR 描述过时。

测试

无人值守的 CI 运行——我未构建或执行任何 PR 代码;以下证据是 PR 自身在被审 commit 上的 CI,经 API 读取。单元测试在构造上即承重:断言被门禁的否定面(客户端收到 redirect: 'manual'addAgentHooks 未被调用、secret 未被替换、once 槽位未被消耗),回滚任一修复都会让对应测试失败。大小写不敏感的拒绝名单路径由 ubuntu job 上显式的大小写变体单元测试覆盖(Windows job 本身在此阶段被门禁/跳过)。

沙箱验证可以收尾剩余的"有效性"问题:@qwen-code /verify——四个门禁是否相对 base 构建在免 mock 的 harness 下以 A/B 方式成立,仅凭 diff 无法证明。该运行已由本次 /triage 触发且进行中(run 30967868098),报告会发布在本帖的验证线程中。

未验证:我未重跑单元测试或作者的 mutation 验证(本运行从不执行 PR 代码)——"门禁做过 mutation 验证"与"在 macOS 上测试"是作者声明,与上面的绿色 CI 一致,但不以其替代。Windows/macOS 的 CLI 测试 job 在此阶段被门禁/跳过。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review; the cap is pure policy — ~730 production-logic lines in core paths trip the Stage 0 maintainer-awareness escalation, and that path never auto-approves regardless of how clean the stages looked.

Stepping back: this is the version of the PR I'd want to merge. The implementation matches my independent proposal on all four holes and exceeds it exactly where the maintainer's review asked for more — intersection instead of stripping keeps the narrowing use case, the session-level decision is made explicitly and test-pinned, the silent substitution got a diagnosable warning, and the once-slot/deferral edge cases show someone actually thinking about what happens after the gate fires. My round-10 automated findings were each answered in the current tree, the tests assert the negatives so a regression fails loudly, and CI is green on the reviewed head. If I had to maintain this in six months I'd thank the author — every gate carries its threat model in comments, which is what security code needs. The residue is hygiene: a stale PR body, a couple of deliberately-declined refactorings, and diagnostic gaps that don't touch enforcement.

Why defer instead of approve: the core-size escalation policy requires a human maintainer's sign-off on a change this size, and that is a rule I apply mechanically rather than waive on vibes — including the vibe that here it's arguably satisfied already, since the author is a maintainer and @doudouOUC approved exactly this commit a few hours ago. I'm not registering an approval vote from this run; that's the point of the defer.

⏸️ Deferring to @doudouOUC (most recent human reviewer; @wenshao as author/maintainer) — the review itself found nothing blocking, and an approving review already stands on this exact head, so this is the policy's final human checkpoint rather than an open question: merge if you're satisfied, or say what you'd like changed. Needs a human call on this one.

中文说明

置信度:3/5 —— 审查本身干净;封顶纯因策略——核心路径约 730 行生产逻辑触发了 Stage 0 的"维护者知悉"升级,该路径无论各阶段多干净都不自动批准。

退一步看:这是我会愿意合并的版本。实现在四个漏洞上都与我的独立方案一致,并且恰好在维护者评审要求更多的地方超出了它——用求交替代剥离保留了"收窄"用法,session 级别的决定被显式做出并有测试钉住,静默替换有了可诊断的警告,once 槽位/延迟的边界情况说明有人真正在思考门禁触发之后会发生什么。我上一轮(round-10)的自动评审发现在当前代码树中逐条得到回应,测试断言否定面、回归会大声失败,被审 head 上 CI 全绿。如果六个月后由我维护,我会感谢作者——每个门禁处都用注释写着威胁模型,这正是安全代码所需要的。遗留项是卫生问题:过时的 PR 描述、几处被有意回绝的重构、以及不影响强制力的诊断性缺口。

为什么是 defer 而非 approve:核心规模升级策略要求这个体量的改动由人类维护者签核,这是一条我机械执行、不凭感觉豁免的规则——哪怕这里的感觉是"其实已经满足了":作者本人是维护者,且 @doudouOUC 几小时前刚在这个 commit 上批准。本轮不投批准票;这正是 defer 的意义。

⏸️ 转交 @doudouOUC(最近的人类评审;@wenshao 为作者/维护者)——审查本身未发现阻塞项,且这个 head 上已有一份批准评审,因此这是策略意义上的人类终检,而非悬而未决的问题:满意即合并,或指出想改的地方。这一处需要人来拍板。

Qwen Code · qwen3.8-max

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x), Post Coverage Comment (ubuntu-latest, 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x), Post Coverage Comment (ubuntu-latest, 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

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

Copy link
Copy Markdown
Collaborator

⚠️ AutoFix round 19 ended without publishing a reportview run.

中文说明

⚠️ AutoFix 第 19 轮结束但未发布报告 —— 查看运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review — no code change this round

Triage. The only feedback newer than the last evaluation is two failed CI
checks (Test (ubuntu-latest, Node 22.x) and Post Coverage Comment). There
are no review bodies, no inline review comments, and no actionable maintainer
instructions — the issue-level comments are CI-bot lifecycle/triage automation
plus a /takeover. So there is no reviewer finding to address in code.

The failed checks are not caused by this PR's code. The workflow's own
base-update step already verified this and recorded it on the PR: the same two
checks pass on current main, main was merged into the branch via
update-branch, and CI was re-run. Post Coverage Comment only fails as a
downstream consequence of Test, so both share one root cause that is not a
defect introduced here.

Local verification of the PR (all green). I reproduced the PR's behavior in
the checkout and ran the trusted required checks:

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run generate:settings-schema — regenerated with no diff, so the
    committed schema is already up to date (no freshness failure)
  • Focused Vitest for every file this PR touches — all passed:
    • packages/core: envInterpolator, httpHookRunner, subagent-manager,
      skill, envVarResolver (289 tests)
    • packages/cli: settings, envVarResolver (173 tests)

I also ran the full packages/core unit suite. The only failures there are
artifacts of this restricted sandbox, not the PR: EACCES: permission denied, mkdir '/home/runner/.qwen', sandbox-mode editor detection, and git-worktree /
disk round-trip integration tests. None of them touch a file this PR changed,
and the integration tests run by the Test job (subagents.test.ts, etc.) do
not exercise the trust-gating, redirect, or env-interpolation behavior this PR
adds.

Conclusion. No evidence-backed code defect exists to fix, so no change is
made. Inventing an edit to force a CI re-run would be a speculative no-op. The
branch already carries the merged current main and a fresh CI run; the
independent CI gate remains the final authority.

中文说明

Autofix 复查 —— 本轮不做代码改动

分类。 相对上次评估,唯一的最新反馈是两个失败的 CI 检查(Test (ubuntu-latest, Node 22.x)Post Coverage Comment)。没有 review 正文、没有行内 review 评论,也没有可执行的 maintainer 指令 —— issue 级评论都是 CI 机器人的生命周期/分流自动化,外加一条 /takeover。因此没有需要在代码中处理的 reviewer 发现。

失败的检查并非由本 PR 的代码引起。 工作流自身的 base 更新步骤已经验证了这一点并记录在 PR 上:同样的两个检查在当前 main 上是通过的,已通过 update-branch 将 main 合入本分支,并重新运行了 CI。Post Coverage Comment 只是作为 Test 的下游后果而失败,所以两者同属一个根因,且并非此处引入的缺陷。

本 PR 的本地验证(全部通过)。 我在 checkout 中复现了 PR 的行为,并运行了可信的必需检查:

  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • npm run generate:settings-schema —— 重新生成后没有差异,说明已提交的 schema 本就是最新的(不存在 schema 过期失败)
  • 针对本 PR 触及的每个文件做聚焦 Vitest —— 全部通过:
    • packages/coreenvInterpolatorhttpHookRunnersubagent-managerskillenvVarResolver(289 个测试)
    • packages/clisettingsenvVarResolver(173 个测试)

我还运行了 packages/core 的完整单元测试套件。其中唯一的失败都是这个受限沙箱环境造成的假象,而非 PR 所致:EACCES: permission denied, mkdir '/home/runner/.qwen'、沙箱模式下的编辑器检测,以及 git-worktree / 磁盘往返类集成测试。它们都不触及本 PR 修改的文件;Test 任务所运行的集成测试(subagents.test.ts 等)也不会触发本 PR 新增的信任门控、重定向或环境变量插值行为。

结论。 不存在有证据支撑、可修复的代码缺陷,因此不做任何改动。为了强制重跑 CI 而凭空制造一处改动,只会是一个投机性的空操作。分支已经合入了当前 main 并触发了新一轮 CI;独立的 CI 门禁仍是最终裁决依据。

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


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

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): 1385 tests green — this review observed 16960, 18998 passed.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

Test Plan(非阻断):1385 tests green — this review observed 16960, 18998 passed

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

Comment on lines +38 to +40
if (INTERNAL_SECRET_ENV_VARS.includes(varName)) {
return match;
}

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.

[Critical] The INTERNAL_SECRET_ENV_VARS denylist is checked with a case-sensitive Array.includes(), but process.env is case-insensitive on Windows. A repo-controlled settings/hook config referencing a mixed-case spelling ($qwen_server_token) bypasses the denylist (the list holds uppercase constants) yet still resolves the daemon bearer token from process.env on win32 — the match regexes accept any casing. The same defect exists at packages/cli/src/utils/envVarResolver.ts:39 and packages/core/src/hooks/envInterpolator.ts:74. — Failure scenario: on Windows (a supported platform; Node documents process.env as case-insensitive there), a user who exported QWEN_SERVER_TOKEN opens a folder whose .qwen/settings.json hook command / MCP arg / HTTP-hook allowedEnvVars references $qwen_server_token; includes('qwen_server_token') is false, process.env['qwen_server_token'] returns the canonically-cased value, and the token is baked into the resolved command/URL/header and leaves the process — the exact exfiltration this PR closes on Linux/macOS stays open on Windows. A probe confirmed resolveEnvVarsInString('$qwen_server_token') returns the secret when both casings are in process.env, and flips to the blocked placeholder under a case-insensitive comparison.

Fix (spans all three sites, so a regular block rather than one-click): compare case-insensitively via a shared helper in sanitize-child-env.ts:

const INTERNAL_SECRET_ENV_VARS_UPPER = new Set(
  INTERNAL_SECRET_ENV_VARS.map((v) => v.toUpperCase()),
);
export const isInternalSecretEnvVar = (name: string): boolean =>
  INTERNAL_SECRET_ENV_VARS_UPPER.has(name.toUpperCase());

then call if (isInternalSecretEnvVar(varName)) { … } at all three sites.

中文说明

INTERNAL_SECRET_ENV_VARS 拒绝列表用大小写敏感的 Array.includes() 检查,但 process.env 在 Windows 上大小写不敏感。仓库可控的 settings/hook 配置只要用混合大小写写法($qwen_server_token)就能绕过拒绝列表(列表里全是大写常量),却仍能在 win32 上从 process.env 解析出 daemon bearer token——匹配正则接受任意大小写。同样的缺陷还存在于 packages/cli/src/utils/envVarResolver.ts:39packages/core/src/hooks/envInterpolator.ts:74。失败场景:在 Windows(受支持平台;Node 官方文档说明此处 process.env 大小写不敏感)上,已导出 QWEN_SERVER_TOKEN 的用户打开一个文件夹,其 .qwen/settings.json 的 hook 命令 / MCP 参数 / HTTP-hook allowedEnvVars 引用 $qwen_server_tokenincludes('qwen_server_token') 为 false,process.env['qwen_server_token'] 返回规范大小写的值,token 被烤进解析后的命令/URL/请求头并离开进程——本 PR 在 Linux/macOS 上关闭的外泄路径在 Windows 上仍然敞开。探针已确认:当 process.env 中同时存在两种大小写时 resolveEnvVarsInString('$qwen_server_token') 返回 secret,改为大小写不敏感比较后恢复为被拦截的占位符。修复(涉及三处):在 sanitize-child-env.ts 中用大小写不敏感的共享 helper(isInternalSecretEnvVar,对列表做 toUpperCase() 后用 Set 查找),并在三处调用点替换。

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

* SPDX-License-Identifier: Apache-2.0
*/

import { INTERNAL_SECRET_ENV_VARS } from '@qwen-code/qwen-code-core';

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.

[Critical] This import pulls INTERNAL_SECRET_ENV_VARS from @qwen-code/qwen-code-core — the whole package index. fast-path-settings.ts statically imports this resolver and is dynamically imported by the serve fast-path bootstrap before runQwenServe, so evaluating it now pulls the entire core module graph into the fast path. — Failure scenario: every qwen serve fast-path invocation now evaluates the whole core graph before the HTTP listener is handed to runQwenServe; the first runQwenServe call lands ~3831 ms after tryRunServeFastPath (this import alone costs ~3.6 s — no throw, the call is simply late) versus <1 s at base, so both tests in src/serve/fast-path-open.test.ts deterministically time out their 1000 ms vi.waitFor (measured netNew: fails in isolation on this PR, passes in isolation on the merge base) — and every cold qwen serve startup pays the ~3.6 s the fast path's deferred-import design exists to avoid.

Fix: don't import the whole core index here — consume the constant from a narrow leaf subpath (expose/consume a subpath export for utils/sanitize-child-env.js, as the CLI vitest config already does for other core subpaths), or inline the short frozen list in this file so the fast-path module graph stays free of the core index.

中文说明

此 import 从 @qwen-code/qwen-code-core(整个包索引)引入 INTERNAL_SECRET_ENV_VARSfast-path-settings.ts 静态引入本解析器,而 serve 快速路径引导会在 runQwenServe 之前动态引入 fast-path-settings.ts,因此求值本文件会把整个 core 模块图拖进快速路径。失败场景:每次 qwen serve 快速路径调用现在都会在把 HTTP listener 交给 runQwenServe 之前求值整个 core 图;第一次 runQwenServe 调用落在 tryRunServeFastPath 之后约 3831 ms(仅这个 import 就耗约 3.6 s——不抛错,只是迟到),而 base 上 <1 s,于是 src/serve/fast-path-open.test.ts 的两个测试都会确定性地超过其 1000 ms vi.waitFor(实测为净新增:在本 PR 上单独运行失败,在合并基上单独运行通过)——并且每次冷启动 qwen serve 都要付出这约 3.6 s,而快速路径的延迟引入设计正是为了避免它。修复:不要在此引入整个 core 索引——从窄的叶子子路径引入该常量(为 utils/sanitize-child-env.js 暴露/使用子路径导出,CLI 的 vitest 配置已对其他 core 子路径这样做),或在本文件内联这个简短的冻结列表,使快速路径模块图不依赖 core 索引。

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

Comment thread packages/cli/src/config/settings.ts Outdated
settings.security;
const {
allowPrivateNetworkHooks: _strippedFlag,
allowedHttpHookUrls: _strippedUrls,

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 PR newly strips security.allowedHttpHookUrls from workspace scope, but getSettingsWarnings() still only warns about allowPrivateNetworkHooks — so the whitelist strip is silent. — Concrete cost: a user who sets security.allowedHttpHookUrls in workspace .qwen/settings.json gets no startup diagnostic (a colleague setting allowPrivateNetworkHooks in the same file does); the whitelist is silently discarded and HTTP hooks fall back to the user-scope list or, if that is empty, the schema default (allow all), while the operator believes their configured boundary is in effect. The asymmetry is created by this diff — before it, the workspace whitelist was honored.

Fix: add a parallel warning in getSettingsWarnings() that fires when workspaceFile.originalSettings.security?.allowedHttpHookUrls !== undefined (naming both keys), and update the comment above that block to mention both settings.

中文说明

本 PR 新增从 workspace scope 剥离 security.allowedHttpHookUrls,但 getSettingsWarnings() 仍然只对 allowPrivateNetworkHooks 告警——因此白名单的剥离是静默的。具体代价:在 workspace .qwen/settings.json 中设置 security.allowedHttpHookUrls 的用户不会得到任何启动诊断(而在同一文件里设置 allowPrivateNetworkHooks 的同事会得到);白名单被静默丢弃,HTTP hooks 回退到 user scope 列表,或在其为空时回退到 schema 默认(放行所有),而操作者以为自己配置的边界仍生效。这种不对称是本 diff 造成的——在此之前 workspace 白名单是被接受的。修复:在 getSettingsWarnings() 中增加一条并行告警,当 workspaceFile.originalSettings.security?.allowedHttpHookUrls !== undefined 时触发(并点名两个键),同时更新该告警块上方的注释以同时提及两个设置。

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

expect(unregisterSpy).toHaveBeenCalledTimes(1);
});

it('does not register hooks for a project-level subagent in an untrusted folder', async () => {

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] No test covers a non-project-level subagent in an untrusted folder, so the config.level === 'project' conjunct of the new gate is unguarded — the mutation to plain !runtimeContext.isTrustedFolder() survives the whole file. Both new tests use level: 'project'; the pre-existing dispose test uses level: 'session' but never spies isTrustedFolder (which defaults to true). The sister change in skill.ts ships exactly this case ("registers hooks for a user-level skill regardless of folder trust"). — Concrete cost: if a future edit drops the level check, user-owned agents from ~/.qwen/agents (SubagentLevel includes user/extension/builtin) that declare frontmatter hooks would silently have those hooks ignored in an untrusted workspace, and the suite would not catch it.

Fix: mirror the skill.ts test —

it('registers hooks for a user-level subagent regardless of folder trust', async () => {
  const addAgentHooksSpy = vi.fn().mockReturnValue(vi.fn());
  vi.spyOn(mockConfig, 'getHookSystem').mockReturnValue({
    getRegistry: () => ({ addAgentHooks: addAgentHooksSpy }),
  } as unknown as ReturnType<Config['getHookSystem']>);
  vi.spyOn(mockConfig, 'isTrustedFolder').mockReturnValue(false);

  const result = await manager.createAgentHeadless(
    { ...baseConfig, level: 'user', hooks: { /* one command hook */ } },
    mockConfig,
  );

  expect(addAgentHooksSpy).toHaveBeenCalledTimes(1);
  await result.dispose();
});
中文说明

没有测试覆盖"不可信文件夹下的非 project 级 subagent",因此新门禁中 config.level === 'project' 这一合取项无人守护——把它突变为单纯的 !runtimeContext.isTrustedFolder() 后整个测试文件仍能通过。两个新测试都用 level: 'project';既有的 dispose 测试用 level: 'session' 但从不 spy isTrustedFolder(其默认为 true)。skill.ts 中的姊妹改动恰好带了这个用例("无论文件夹信任与否都为用户级 skill 注册 hooks")。具体代价:如果未来某次编辑删掉了 level 检查,来自 ~/.qwen/agentsSubagentLeveluser/extension/builtin)、声明了 frontmatter hooks 的用户自有 agent,会在不可信 workspace 中被静默忽略 hooks,而测试套件无法捕获。修复:仿照 skill.ts 的测试,补一个 level: 'user' + isTrustedFolder 返回 false、并断言 addAgentHooksSpy 被调用一次的用例。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Review feedback addressed

[Critical] Case-sensitive denylist bypass on Windows (rc:3700141961)

Decision: Fixed.

The INTERNAL_SECRET_ENV_VARS denylist was checked with case-sensitive Array.includes(), but process.env is case-insensitive on Windows. A mixed-case reference like $qwen_server_token would bypass the denylist while still resolving the secret.

Changes:

  • Added isInternalSecretEnvVar() helper in packages/core/src/utils/sanitize-child-env.ts that uses a pre-computed uppercase Set for case-insensitive lookup.
  • Replaced INTERNAL_SECRET_ENV_VARS.includes(varName) with isInternalSecretEnvVar(varName) at all three sites: packages/core/src/utils/envVarResolver.ts, packages/cli/src/utils/envVarResolver.ts, and packages/core/src/hooks/envInterpolator.ts.
  • Added case-insensitive regression tests at all three sites.

[Critical] Fast-path import pulls whole core module graph (rc:3700141963)

Decision: Fixed.

The CLI's envVarResolver.ts imported INTERNAL_SECRET_ENV_VARS from @qwen-code/qwen-code-core (the full package index), which pulled the entire core module graph into the serve fast-path via fast-path-settings.ts.

Changes:

  • Changed the import to use a narrow subpath: @qwen-code/qwen-code-core/sanitizeChildEnv.
  • Added the ./sanitizeChildEnv subpath export to packages/core/package.json, following the existing pattern (./transcriptRecords, ./goalWire, etc.).
  • Added the corresponding path mapping in packages/cli/tsconfig.json and vitest alias in packages/cli/vitest.config.ts.

[Suggestion] Missing warning for stripped allowedHttpHookUrls (rc:3700141965)

Decision: Fixed.

This PR strips security.allowedHttpHookUrls from workspace scope but getSettingsWarnings() only warned about allowPrivateNetworkHooks, making the whitelist strip silent.

Changes:

  • Added a parallel warning in getSettingsWarnings() that fires when workspace settings define security.allowedHttpHookUrls.
  • Updated the comment above the warning block to mention both settings.
  • Added a test for the new warning.

[Suggestion] Missing test for non-project-level subagent in untrusted folder (rc:3700141968)

Decision: Fixed.

No test covered a non-project-level subagent in an untrusted folder, leaving the config.level === 'project' conjunct of the trust gate unguarded.

Changes:

  • Added a test 'registers hooks for a user-level subagent regardless of folder trust' mirroring the existing skill.ts test, using level: 'user' with isTrustedFolder returning false.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run bundle — passed
  • vitest packages/core src/utils/envVarResolver.test.ts — 18 passed
  • vitest packages/core src/hooks/envInterpolator.test.ts — 32 passed
  • vitest packages/core src/hooks/httpHookRunner.test.ts — 21 passed
  • vitest packages/core src/tools/skill.test.ts — 84 passed
  • vitest packages/core src/subagents/subagent-manager.test.ts (new test) — 1 passed (3 pre-existing failures in unrelated loadSubagent/deleteSubagent tests, confirmed failing on base)
  • vitest packages/cli src/utils/envVarResolver.test.ts — 18 passed
  • vitest packages/cli src/config/settings.test.ts (new test) — 1 passed (12 pre-existing failures in unrelated env-resolution tests, confirmed failing on base)
中文说明

已处理的审查反馈

[Critical] Windows 上大小写敏感的拒绝列表可被绕过 (rc:3700141961)

决定: 已修复。

INTERNAL_SECRET_ENV_VARS 拒绝列表使用大小写敏感的 Array.includes() 检查,但 process.env 在 Windows 上大小写不敏感。混合大小写引用(如 $qwen_server_token)会绕过拒绝列表,同时仍能解析出 secret。

变更:

  • packages/core/src/utils/sanitize-child-env.ts 中新增 isInternalSecretEnvVar() helper,使用预计算的大写 Set 进行大小写不敏感查找。
  • 在三处调用点(packages/core/src/utils/envVarResolver.tspackages/cli/src/utils/envVarResolver.tspackages/core/src/hooks/envInterpolator.ts)将 INTERNAL_SECRET_ENV_VARS.includes(varName) 替换为 isInternalSecretEnvVar(varName)
  • 在三处均添加了大小写不敏感的回归测试。

[Critical] 快速路径 import 拉入整个 core 模块图 (rc:3700141963)

决定: 已修复。

CLI 的 envVarResolver.ts@qwen-code/qwen-code-core(完整包索引)导入 INTERNAL_SECRET_ENV_VARS,通过 fast-path-settings.ts 将整个 core 模块图拖入 serve 快速路径。

变更:

  • 将 import 改为使用窄子路径:@qwen-code/qwen-code-core/sanitizeChildEnv
  • packages/core/package.json 中添加 ./sanitizeChildEnv 子路径导出,遵循现有模式(./transcriptRecords./goalWire 等)。
  • packages/cli/tsconfig.jsonpackages/cli/vitest.config.ts 中添加对应的路径映射和 vitest 别名。

[Suggestion] 缺少对已剥离的 allowedHttpHookUrls 的告警 (rc:3700141965)

决定: 已修复。

本 PR 从 workspace scope 剥离 security.allowedHttpHookUrls,但 getSettingsWarnings() 仅对 allowPrivateNetworkHooks 告警,导致白名单剥离是静默的。

变更:

  • getSettingsWarnings() 中新增并行告警,当 workspace 设置定义了 security.allowedHttpHookUrls 时触发。
  • 更新告警块上方的注释以同时提及两个设置。
  • 添加新告警的测试。

[Suggestion] 缺少不可信文件夹下非 project 级 subagent 的测试 (rc:3700141968)

决定: 已修复。

没有测试覆盖不可信文件夹下的非 project 级 subagent,导致信任门禁中 config.level === 'project' 这一合取项无人守护。

变更:

  • 新增测试 'registers hooks for a user-level subagent regardless of folder trust',仿照 skill.ts 的既有测试,使用 level: 'user'isTrustedFolder 返回 false

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npm run bundle — 通过
  • vitest packages/core src/utils/envVarResolver.test.ts — 18 通过
  • vitest packages/core src/hooks/envInterpolator.test.ts — 32 通过
  • vitest packages/core src/hooks/httpHookRunner.test.ts — 21 通过
  • vitest packages/core src/tools/skill.test.ts — 84 通过
  • vitest packages/core src/subagents/subagent-manager.test.ts(新测试)— 1 通过(3 个预存失败位于无关的 loadSubagent/deleteSubagent 测试中,已确认在 base 上同样失败)
  • vitest packages/cli src/utils/envVarResolver.test.ts — 18 通过
  • vitest packages/cli src/config/settings.test.ts(新测试)— 1 通过(12 个预存失败位于无关的 env-resolution 测试中,已确认在 base 上同样失败)

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-preview

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI N/A% N/A% N/A% N/A%
Core 88.11% 88.11% 89.57% 86.7%
CLI Package - Full Text Report
CLI full-text-summary.txt not found at: coverage_artifact/cli/coverage/full-text-summary.txt
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   88.11 |     86.7 |   89.57 |   88.11 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |   90.55 |     84.7 |    94.9 |   90.55 |                   
  ...transcript.ts |   88.49 |    84.09 |     100 |   88.49 | ...32,640,646-650 
  ...ent-resume.ts |   85.59 |    77.75 |   83.33 |   85.59 | ...1794-1798,1801 
  ...ound-tasks.ts |   94.63 |    90.13 |   96.38 |   94.63 | ...1773,1793-1796 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ent-result.ts |    96.8 |    92.68 |     100 |    96.8 | 106,129-131       
  ...n-registry.ts |   94.79 |     87.7 |     100 |   94.79 | ...1067,1081-1083 
  ...w-snapshot.ts |   92.12 |    77.14 |     100 |   92.12 | ...65,189,196-198 
  worktree-pin.ts  |     100 |    88.23 |     100 |     100 | 78,99             
 src/agents/arena  |   76.94 |    68.22 |   78.94 |   76.94 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.89 |     65.2 |   78.57 |   75.89 | ...1887,1893-1894 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    72.34 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   78.09 |    85.23 |   76.28 |   78.09 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |    90.9 |    85.36 |   93.33 |    90.9 | ...70,672,674-675 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   91.13 |    86.87 |   89.86 |   91.13 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   84.45 |    76.43 |   77.19 |   84.45 | ...2344,2390-2392 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   93.49 |    89.41 |   83.33 |   93.49 | ...96-497,500-501 
  ...nteractive.ts |   81.01 |    82.35 |   76.66 |   81.01 | ...33,535-538,541 
  ...statistics.ts |   98.29 |    82.55 |     100 |   98.29 | 141,165,206,239   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ool-policy.ts |   98.38 |      100 |    92.3 |   98.38 | 85-86             
  ...low-budget.ts |     100 |      100 |     100 |     100 |                   
  ...-scheduler.ts |   97.43 |    96.36 |     100 |   97.43 | 128-130           
  ...ow-journal.ts |    92.3 |    75.86 |     100 |    92.3 | ...49-150,190-192 
  ...chestrator.ts |   93.43 |    91.19 |   90.47 |   93.43 | ...2145,2194-2197 
  ...ow-prompts.ts |     100 |      100 |     100 |     100 |                   
  ...low-runner.ts |   94.85 |     87.5 |   92.85 |   94.85 | ...93,260,280-283 
  ...ow-sandbox.ts |   96.85 |    91.28 |     100 |   96.85 | ...1750,1756-1757 
  ...flow-saved.ts |   96.51 |    94.36 |     100 |   96.51 | 134-135,234-237   
  ...flow-stall.ts |    97.9 |    83.33 |     100 |    97.9 | 138-139,236       
 src/agents/tasks  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/team   |   82.71 |    84.46 |   88.97 |   82.71 |                   
  TeamManager.ts   |    73.6 |    80.64 |   79.62 |    73.6 | ...1706,1729-1730 
  identity.ts      |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...sionBridge.ts |     100 |      100 |     100 |     100 |                   
  mailbox.ts       |   96.02 |    87.23 |     100 |   96.02 | 352-358           
  ...ptAddendum.ts |     100 |      100 |     100 |     100 |                   
  tasks.ts         |   89.24 |    82.82 |     100 |   89.24 | ...-994,1038-1039 
  team-events.ts   |   60.52 |      100 |      50 |   60.52 | ...40-144,151-155 
  teamHelpers.ts   |   91.71 |    94.54 |      95 |   91.71 | ...18-319,355-365 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...eam/test-utils |   94.39 |    94.35 |   98.21 |   94.39 |                   
  ...on-harness.ts |   96.49 |       85 |     100 |   96.49 | 128-129,141-142   
  fake-agent.ts    |   98.49 |    95.16 |     100 |   98.49 | 201-203           
  fake-backend.ts  |   86.46 |    97.61 |   95.83 |   86.46 | 124-146           
 src/config        |   84.21 |    86.79 |   75.31 |   84.21 |                   
  approval-mode.ts |     100 |      100 |     100 |     100 |                   
  ...xtDefaults.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |   83.52 |    86.51 |   73.78 |   83.52 | ...8844,8848-8849 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   94.39 |    91.57 |   88.23 |   94.39 | ...45-446,449-450 
 ...nfirmation-bus |   98.27 |    97.14 |     100 |   98.27 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |   92.35 |    88.16 |   93.05 |   92.35 |                   
  baseLlmClient.ts |    88.4 |     83.8 |   81.81 |    88.4 | ...59,672,678-680 
  client.ts        |   92.46 |    87.73 |   91.76 |   92.46 | ...4146,4244-4245 
  ...tGenerator.ts |   86.34 |    87.34 |   84.61 |   86.34 | ...96-497,542-548 
  ...lScheduler.ts |   89.77 |     84.7 |   94.73 |   89.77 | ...6422,6450-6466 
  geminiChat.ts    |    94.7 |    90.13 |   95.53 |    94.7 | ...5059,5107-5108 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  genai-compat.ts  |     100 |      100 |     100 |     100 |                   
  ...MediaLimit.ts |     100 |       96 |     100 |     100 | 96                
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | ...9,62-66,69-110 
  ...ream-error.ts |     100 |      100 |     100 |     100 |                   
  logger.ts        |   87.41 |    87.02 |     100 |   87.41 | ...64-568,614-628 
  ...lay-buffer.ts |     100 |      100 |     100 |     100 |                   
  ...dispatcher.ts |     100 |      100 |     100 |     100 |                   
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   93.54 |    83.33 |      50 |   93.54 | 49-50             
  ...on-helpers.ts |   93.49 |    78.57 |     100 |   93.49 | ...10-211,228-229 
  ...issionFlow.ts |   98.97 |    96.96 |     100 |   98.97 | 107               
  ...try-policy.ts |     100 |      100 |     100 |     100 |                   
  ...ell-policy.ts |   94.89 |    88.54 |     100 |   94.89 | ...51-252,297-298 
  prompts.ts       |   93.64 |    91.42 |   83.33 |   93.64 | ...1209,1412-1413 
  ...ing-effort.ts |     100 |      100 |     100 |     100 |                   
  ...n-recovery.ts |   95.13 |       80 |     100 |   95.13 | ...06-107,142-144 
  ...t-profiler.ts |    97.9 |    81.15 |   88.23 |    97.9 | 117,124-125,130   
  ...port-retry.ts |     100 |      100 |     100 |     100 |                   
  tokenLimits.ts   |     100 |    91.89 |     100 |     100 | 87,122-139        
  ...reparation.ts |     100 |      100 |     100 |     100 |                   
  ...tion-guard.ts |   90.38 |    94.73 |     100 |   90.38 | 83-87             
  ...allIdUtils.ts |   98.41 |    93.47 |     100 |   98.41 | 36,45             
  ...okTriggers.ts |   99.45 |    92.43 |     100 |   99.45 | 182,193           
  ...terruption.ts |     100 |     92.3 |     100 |     100 | 86,104            
  turn.ts          |   99.19 |    94.48 |     100 |   99.19 | 682-683,752       
  ...l-fallback.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   96.33 |    88.12 |   96.15 |   96.33 |                   
  ...tGenerator.ts |   97.24 |    86.72 |   94.87 |   97.24 | ...1436,1465,1476 
  converter.ts     |   96.19 |    89.25 |     100 |   96.19 | ...1329,1550-1552 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
  usage.ts         |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   88.78 |    72.36 |   89.47 |   88.78 |                   
  ...tGenerator.ts |   87.18 |    71.83 |   88.88 |   87.18 | ...58-364,382-383 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |   96.12 |     91.3 |    90.9 |   96.12 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   96.06 |    90.75 |   90.47 |   96.06 | ...1309-1310,1338 
  ...tDetection.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |    91.9 |    90.54 |   95.76 |    91.9 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |    91.3 |    89.49 |   96.87 |    91.3 | ...1942,2111-2126 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   68.25 |    82.35 |      50 |   68.25 | 44-53,74-78,90-94 
  ...tGenerator.ts |    66.4 |    70.58 |   88.88 |    66.4 | ...51-157,168-169 
  pipeline.ts      |   95.27 |     90.9 |     100 |   95.27 | ...1434,1442,1541 
  ...ix-caching.ts |   95.23 |    92.85 |     100 |   95.23 | 45-46,69-70       
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   92.24 |     92.4 |     100 |   92.24 | ...28-529,549-552 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   97.39 |    92.28 |    98.5 |   97.39 |                   
  dashscope.ts     |   98.36 |    95.08 |   96.42 |   98.36 | ...08-709,851-852 
  deepseek.ts      |   94.91 |    89.36 |     100 |   94.91 | ...31-132,145-146 
  default.ts       |   99.18 |    97.05 |     100 |   99.18 | 208               
  index.ts         |     100 |      100 |     100 |     100 |                   
  mimo.ts          |   94.11 |    66.66 |     100 |   94.11 | 29,52-53          
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
  utils.ts         |     100 |      100 |     100 |     100 |                   
  zai.ts           |   92.13 |    82.14 |     100 |   92.13 | ...,39-40,135-137 
 src/extension     |   87.71 |    84.66 |   92.57 |   87.71 |                   
  ...ive-safety.ts |     100 |      100 |     100 |     100 |                   
  ...-converter.ts |   80.55 |    73.66 |     100 |   80.55 | ...1133,1179-1180 
  corruptFile.ts   |     100 |       50 |     100 |     100 | 40-45             
  ...-converter.ts |     100 |      100 |     100 |     100 |                   
  ...me-refresh.ts |     100 |      100 |     100 |     100 |                   
  ...sion-store.ts |   90.94 |     86.5 |   97.91 |   90.94 | ...1230-1236,1280 
  ...ionManager.ts |   83.89 |    82.86 |   81.72 |   83.89 | ...2832,2861-2862 
  ...references.ts |     100 |     90.9 |     100 |     100 | ...05,129,197,200 
  ...onSettings.ts |    92.3 |     94.4 |     100 |    92.3 | ...98-501,570-571 
  ...-converter.ts |    75.9 |    85.71 |   85.71 |    75.9 | ...98,202,214-248 
  github.ts        |   90.48 |    82.71 |     100 |   90.48 | ...4,994-995,1005 
  http-client.ts   |   84.61 |       80 |     100 |   84.61 | 20-21             
  i18n.ts          |   78.26 |       96 |      50 |   78.26 | 104-110,116-123   
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   88.39 |    83.11 |     100 |   88.39 | ...08,494,507-508 
  ...ork-policy.ts |   89.72 |       90 |     100 |   89.72 | ...36,148-154,156 
  npm.ts           |   89.02 |    81.81 |     100 |   89.02 | ...86-688,695-700 
  override.ts      |   94.11 |    93.33 |     100 |   94.11 | 63-64,81-82       
  ...-converter.ts |   94.89 |    90.41 |     100 |   94.89 | ...50-151,222-224 
  redaction.ts     |     100 |      100 |     100 |     100 |                   
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-107,141-146    
  ...ceRegistry.ts |   94.01 |    83.14 |     100 |   94.01 | ...38-344,365-366 
  storage.ts       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.95 |    84.21 |     100 |   88.95 | ...32-235,238-241 
  ...extraction.ts |   85.77 |       81 |   89.47 |   85.77 | ...02-205,260-261 
 ...ent-plugins-v1 |   84.94 |    79.51 |     100 |   84.94 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  manifest.ts      |   81.87 |    84.48 |     100 |   81.87 | ...55-156,161-174 
  mcp.ts           |   84.98 |    79.56 |     100 |   84.98 | ...88-389,419-420 
  paths.ts         |     100 |    94.44 |     100 |     100 | 59                
  skills.ts        |   82.31 |    63.88 |     100 |   82.31 | ...38-141,150-151 
 src/followup      |   81.45 |    79.25 |   84.21 |   81.45 |                   
  followupState.ts |   98.44 |    95.74 |     100 |   98.44 | 236-237           
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   96.29 |    88.88 |     100 |   96.29 | 78,108,122        
  speculation.ts   |   75.98 |    67.22 |   58.33 |   75.98 | ...42-743,750-751 
  ...onToolGate.ts |   97.97 |     87.5 |     100 |   97.97 | 105,110           
  ...nGenerator.ts |   72.03 |    81.15 |   83.33 |   72.03 | ...68-219,331-333 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/goals         |   92.96 |    89.06 |   94.34 |   92.96 |                   
  ...eGoalStore.ts |   87.61 |    88.88 |   86.66 |   87.61 | ...85-188,196-204 
  ...t-verifier.ts |   96.27 |     90.9 |     100 |   96.27 | ...20,143-146,163 
  ...checkpoint.ts |   81.48 |    76.19 |     100 |   81.48 | ...02-105,115-118 
  goal-evidence.ts |   88.34 |    87.02 |    97.5 |   88.34 | ...1162,1185-1188 
  ...projection.ts |   66.66 |    72.97 |   33.33 |   66.66 | ...87,190,194-196 
  ...ersistence.ts |   87.29 |    85.96 |    87.5 |   87.29 | ...53-154,185-190 
  goal-protocol.ts |   96.87 |    95.65 |     100 |   96.87 | 200-201           
  goal-reducer.ts  |      95 |    92.34 |   97.05 |      95 | ...43,520,538-539 
  goal-runtime.ts  |   96.89 |    89.95 |   95.74 |   96.89 | ...1315-1316,1437 
  goal-tools.ts    |   98.38 |    94.05 |   95.45 |   98.38 | ...98-199,300-301 
  ...rn-context.ts |     100 |      100 |     100 |     100 |                   
  goal-verifier.ts |   92.46 |    92.85 |     100 |   92.46 | ...69-172,185-187 
  goal-wire.ts     |       0 |        0 |       0 |       0 | 1-28              
  goalHook.ts      |   96.91 |    92.42 |     100 |   96.91 | 115-120,221-222   
  goalJudge.ts     |   95.84 |    87.09 |     100 |   95.84 | ...55-356,448-449 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/hooks         |   88.35 |    86.89 |   88.69 |   88.35 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  context-usage.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |    97.1 |    94.11 |     100 |    97.1 | 71-72             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   97.35 |    94.33 |     100 |   97.35 | ...25-326,411,413 
  ...entHandler.ts |   95.57 |    84.76 |   94.73 |   95.57 | ...1040-1041,1051 
  hookPlanner.ts   |   87.55 |    85.54 |   86.66 |   87.55 | ...22-226,233-244 
  hookRegistry.ts  |   92.53 |    85.43 |     100 |   92.53 | ...39,458,462,466 
  hookRunner.ts    |   62.65 |    72.34 |   66.66 |   62.65 | ...70-771,780-781 
  hookSystem.ts    |   87.64 |     98.5 |   70.83 |   87.64 | ...58-759,765-766 
  ...HookRunner.ts |   82.53 |    71.42 |      80 |   82.53 | ...95-496,515-519 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...edCallback.ts |     100 |      100 |     100 |     100 |                   
  ...HookRunner.ts |   94.19 |    84.37 |   81.81 |   94.19 | ...76-384,458-459 
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |   94.87 |    88.88 |     100 |   94.87 | ...84,325,327-329 
  ssrfGuard.ts     |   86.45 |    89.13 |     100 |   86.45 | ...85,289-295,301 
  stopHookCap.ts   |     100 |      100 |     100 |     100 |                   
  trustedHooks.ts  |      90 |    52.63 |     100 |      90 | ...53,66-67,97-98 
  types.ts         |   94.25 |    96.09 |   88.88 |   94.25 | ...46-547,632-636 
  urlValidator.ts  |   97.14 |       96 |     100 |   97.14 | 251-252,265-266   
  ...it-context.ts |     100 |      100 |     100 |     100 |                   
 src/ide           |   76.98 |    85.03 |   79.03 |   76.98 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |   69.16 |    84.65 |   68.29 |   69.16 | ...1068,1097-1105 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   58.96 |    70.57 |   66.14 |   58.96 |                   
  ...nfigLoader.ts |   80.55 |       72 |   95.45 |   80.55 | ...02-504,508-514 
  ...ionFactory.ts |   42.81 |    73.07 |      50 |   42.81 | ...76-427,433-450 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   75.73 |     80.1 |   79.66 |   75.73 | ...1346,1352-1382 
  ...eLspClient.ts |   32.78 |       80 |   16.66 |   32.78 | ...89-293,299-300 
  ...LspService.ts |      60 |    73.36 |   78.26 |      60 | ...1575,1635-1645 
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |    82.3 |    77.81 |   78.33 |    82.3 |                   
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   79.31 |    58.06 |     100 |   79.31 | ...26-933,940-942 
  ...en-storage.ts |   98.78 |    97.95 |     100 |   98.78 | 106-107           
  oauth-utils.ts   |   73.61 |    85.48 |    92.3 |   73.61 | ...46-366,392-421 
  ...n-provider.ts |   89.83 |       96 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   82.12 |    88.19 |   89.28 |   82.12 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   87.08 |    87.03 |   95.23 |   87.08 | ...00-201,214-215 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   87.83 |    83.93 |   90.47 |   87.83 |                   
  ...y-document.ts |   89.52 |    84.61 |     100 |   89.52 | ...24-325,329-330 
  ...nel-memory.ts |   97.36 |    96.63 |   96.42 |   97.36 | ...91-293,367-368 
  const.ts         |   94.28 |     92.3 |     100 |   94.28 | 66-67             
  dream.ts         |    64.6 |    72.22 |      50 |    64.6 | ...04-109,124-165 
  ...entPlanner.ts |     100 |    83.33 |     100 |     100 | 136,146           
  entries.ts       |   75.59 |    84.84 |   83.33 |   75.59 | ...56-157,172-180 
  extract.ts       |   92.41 |    79.41 |     100 |   92.41 | 56-61,100,119-122 
  ...entPlanner.ts |   91.59 |    76.74 |     100 |   91.59 | ...05,114-117,293 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |   81.83 |       75 |   83.33 |   81.83 | ...51,474,478-507 
  indexer.ts       |   94.14 |       84 |     100 |   94.14 | ...32-233,334,337 
  ...kill-agent.ts |   97.94 |    89.36 |     100 |   97.94 | 82-83,179-180     
  manager.ts       |    78.4 |    82.29 |   77.77 |    78.4 | ...1482,1495-1497 
  ...ent-config.ts |   86.99 |    82.69 |   86.36 |   86.99 | ...69,389,396-402 
  memoryAge.ts     |   90.47 |       80 |     100 |   90.47 | 50-51             
  paths.ts         |     100 |      100 |     100 |     100 |                   
  ...ing-skills.ts |     100 |       72 |     100 |     100 | 31-35,73-78,97    
  prompt.ts        |   97.26 |    86.79 |     100 |   97.26 | ...10-218,222,225 
  recall.ts        |   82.06 |       75 |    90.9 |   82.06 | ...59-364,395-406 
  refresh.ts       |   93.58 |    89.58 |     100 |   93.58 | ...75-176,183-184 
  ...ceSelector.ts |    93.1 |    81.81 |     100 |    93.1 | ...25,127-128,136 
  remember.ts      |   98.89 |    90.19 |     100 |   98.89 | 50,70             
  scan.ts          |   93.12 |    77.41 |     100 |   93.12 | ...08-109,154,157 
  scopes.ts        |     100 |      100 |     100 |     100 |                   
  ...et-scanner.ts |     100 |      100 |     100 |     100 |                   
  ...entPlanner.ts |   77.24 |    74.07 |   72.22 |   77.24 | ...52-456,459,465 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   92.92 |    81.81 |     100 |   92.92 | ...16-117,147-148 
  ...git-status.ts |     100 |     87.5 |     100 |     100 | 30                
  ...cret-guard.ts |     100 |      100 |     100 |     100 |                   
  ...emory-sync.ts |   94.24 |    82.85 |     100 |   94.24 | ...34-236,246-247 
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...ontextFile.ts |   81.21 |    81.53 |   81.81 |   81.21 | ...63-277,291-296 
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   92.55 |    88.62 |   91.13 |   92.55 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   97.77 |    91.83 |     100 |   97.77 | 155,161,171       
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |       44 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.71 |    93.33 |     100 |   98.71 | 166,328,334       
  modelRegistry.ts |     100 |    98.11 |     100 |     100 | 177,261           
  modelsConfig.ts  |   89.36 |    86.93 |   88.09 |   89.36 | ...1404,1433-1434 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |   83.79 |    91.16 |   71.07 |   83.79 |                   
  autoMode.ts      |   97.66 |    93.13 |     100 |   97.66 | ...82-589,635,712 
  ...transcript.ts |      98 |       84 |     100 |      98 | 200-201           
  classifier.ts    |      94 |    94.54 |     100 |      94 | 158-165,389-393   
  ...erousRules.ts |     100 |    89.36 |     100 |     100 | 110,133,147,175   
  ...alTracking.ts |     100 |      100 |     100 |     100 |                   
  ...e-commands.ts |   86.77 |     73.8 |     100 |   86.77 | 131-141,210-214   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   86.63 |    88.88 |      80 |   86.63 | ...1111,1217-1221 
  rule-parser.ts   |   94.49 |    92.72 |     100 |   94.49 | ...1447,1481-1483 
  ...-semantics.ts |   70.44 |    91.09 |   46.66 |   70.44 | ...2237,2311-2314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...sifier-prompts |   99.04 |    95.23 |     100 |   99.04 |                   
  system-prompt.ts |   99.04 |    95.23 |     100 |   99.04 | 220               
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/providers     |   83.71 |     78.6 |   81.25 |   83.71 |                   
  all-providers.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  install.ts       |   93.11 |     84.5 |     100 |   93.11 | ...56-257,330-331 
  ...der-config.ts |   75.85 |    74.04 |   78.26 |   75.85 | ...73-474,502-503 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...viders/presets |   97.82 |    91.66 |   63.63 |   97.82 |                   
  ...oding-plan.ts |   87.34 |      100 |       0 |   87.34 | 81-83,86-88,90-93 
  ...a-standard.ts |     100 |      100 |     100 |     100 |                   
  ...token-plan.ts |     100 |      100 |     100 |     100 |                   
  ...m-provider.ts |   97.05 |    81.25 |      75 |   97.05 | 118-119           
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  grok.ts          |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  requesty.ts      |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/qwen          |   85.41 |    78.52 |   95.89 |   85.41 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   82.79 |    73.29 |   90.62 |   82.79 | ...1205-1221,1251 
  ...kenManager.ts |   85.36 |    76.61 |     100 |   85.36 | ...52-757,778-783 
 src/resources     |     100 |      100 |     100 |     100 |                   
  ...e-registry.ts |     100 |      100 |     100 |     100 |                   
 src/services      |   90.37 |    85.99 |   96.77 |   90.37 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   98.48 |    87.28 |     100 |   98.48 | 81-82,105,474-475 
  branch-points.ts |     100 |    95.23 |     100 |     100 | ...20,211,224,327 
  ...ionService.ts |   97.51 |    96.15 |     100 |   97.51 | ...,929,1072-1080 
  ...ingService.ts |   90.96 |    86.66 |    92.5 |   90.96 | ...2439,2466-2467 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |    97.2 |    94.17 |     100 |    97.2 | ...39-340,378-381 
  cronScheduler.ts |   94.17 |    90.45 |      98 |   94.17 | ...1333,1736-1737 
  cronTasksFile.ts |   95.49 |    90.82 |     100 |   95.49 | ...37,346-347,483 
  cronTasksLock.ts |   94.44 |    89.47 |     100 |   94.44 | ...02-103,132-133 
  ...eryService.ts |   96.22 |    93.54 |      90 |   96.22 | 121,155-156,161   
  ...oryService.ts |   88.17 |    79.02 |    92.3 |   88.17 | ...1303,1344-1347 
  fileReadCache.ts |    97.5 |    96.07 |     100 |    97.5 | 349-350,363-364   
  ...temService.ts |    92.8 |    84.68 |   94.11 |    92.8 | ...53,479-486,531 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  ...reeService.ts |   74.75 |    70.66 |   96.07 |   74.75 | ...2296,2325-2326 
  ...on-service.ts |   87.38 |       72 |     100 |   87.38 | ...01-305,343-344 
  ...references.ts |   98.39 |    88.88 |     100 |   98.39 | 154-155,215-216   
  ...ionService.ts |   98.26 |    97.35 |     100 |   98.26 | ...13-714,761-762 
  ...ticsDumper.ts |   98.37 |    95.23 |     100 |   98.37 | 185-186           
  ...ureMonitor.ts |   95.82 |    90.52 |   97.05 |   95.82 | ...60,861,875-877 
  ...orRegistry.ts |   97.22 |    90.99 |     100 |   97.22 | ...55-456,609-610 
  ...ttachments.ts |   97.74 |     90.9 |     100 |   97.74 | 298-308,646       
  ...pi-history.ts |   98.94 |    88.88 |     100 |   98.94 | 43                
  ...ersistence.ts |   91.66 |    80.75 |     100 |   91.66 | ...1060-1061,1089 
  ...tory-state.ts |     100 |    95.23 |     100 |     100 | 31                
  ...on-service.ts |   94.49 |    92.26 |   97.14 |   94.49 | ...98-600,656-664 
  ...ce-service.ts |    98.5 |    94.11 |    90.9 |    98.5 | 64-65             
  ...n-registry.ts |   98.73 |    96.29 |     100 |   98.73 | 584,638-639,692   
  ...ken-counts.ts |     100 |       96 |     100 |     100 | 58                
  ...ipt-reader.ts |   93.71 |    91.05 |   97.77 |   93.71 | ...2755-2756,2833 
  ...turn-state.ts |   94.11 |     90.9 |   91.66 |   94.11 | 108-112,129-130   
  ...est-helper.ts |       0 |        0 |       0 |       0 | 1-65              
  ...iter-lease.ts |   83.14 |    74.47 |   97.61 |   83.14 | ...2433,2445-2448 
  sessionRecap.ts  |   67.56 |    43.47 |     100 |   67.56 | ...60,178,180-183 
  ...ionService.ts |   89.31 |    85.86 |   96.05 |   89.31 | ...2642,2656-2676 
  sessionTitle.ts  |   95.75 |    77.41 |     100 |   95.75 | ...53-256,287-288 
  ...ionService.ts |   84.43 |    78.45 |   97.18 |   84.43 | ...2496,2502-2507 
  ...pInhibitor.ts |   97.42 |    92.77 |     100 |   97.42 | ...30,169,369-370 
  ...Estimation.ts |     100 |    94.11 |     100 |     100 | 118               
  ...ageService.ts |   97.76 |    91.59 |   93.75 |   97.76 | ...61-262,366,567 
  ...ite-origin.ts |     100 |    93.33 |     100 |     100 | 32                
  ...UseSummary.ts |   94.63 |    88.46 |     100 |   94.63 | ...62-164,214-215 
  ...rd-service.ts |     100 |    88.37 |     100 |     100 | ...29,145-146,241 
  ...oryService.ts |   90.72 |    84.07 |     100 |   90.72 | ...06-509,561-562 
  ...reeCleanup.ts |   14.42 |      100 |   33.33 |   14.42 | 58-186            
  ...ionService.ts |   88.36 |     87.7 |     100 |   88.36 | ...48-449,465-466 
 ...icrocompaction |    98.9 |    95.08 |     100 |    98.9 |                   
  microcompact.ts  |    98.9 |    95.08 |     100 |    98.9 | ...40,749,758-759 
 ...s/visionBridge |   98.81 |    92.12 |     100 |   98.81 |                   
  ...capability.ts |     100 |      100 |     100 |     100 |                   
  ...part-utils.ts |     100 |      100 |     100 |     100 |                   
  ...ion-bridge.ts |   98.72 |    82.35 |     100 |   98.72 | 65,71             
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  ...ge-service.ts |   98.61 |     94.7 |     100 |   98.61 | ...06,666,679-680 
 src/skills        |   89.56 |    86.17 |   93.61 |   89.56 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |    93.33 |     100 |     100 | 93,112            
  skill-curator.ts |   89.71 |    81.54 |     100 |   89.71 | ...01-902,904-907 
  skill-load.ts    |   94.84 |    87.69 |     100 |   94.84 | ...03,223,235-237 
  skill-manager.ts |   85.55 |    86.05 |   83.33 |   85.55 | ...1254,1261-1265 
  skill-paths.ts   |   90.42 |     87.5 |     100 |   90.42 | ...19-120,125-126 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |   97.91 |    98.03 |     100 |   97.91 | 288-289           
 ...ataviz/scripts |   80.06 |    95.23 |   88.23 |   80.06 |                   
  ...te_palette.js |   80.06 |    95.23 |   88.23 |   80.06 | 261-296,306-328   
 ...s/bundled/loop |   97.48 |    95.77 |     100 |   97.48 |                   
  ...omous-loop.ts |     100 |      100 |     100 |     100 |                   
  ...-task-file.ts |   94.85 |     92.4 |     100 |   94.85 | ...56,367,375-376 
  ...k-resolver.ts |     100 |      100 |     100 |     100 |                   
 src/subagents     |   87.97 |    89.42 |   96.55 |   87.97 |                   
  ...ter-schema.ts |     100 |    98.07 |     100 |     100 | 99                
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   84.87 |    86.55 |   94.87 |   84.87 | ...1616,1693-1694 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 47-52,63-68,71-76 
 src/telemetry     |   82.41 |    84.65 |   85.74 |   82.41 |                   
  ...ty-tracker.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...on-metrics.ts |   99.08 |    80.95 |     100 |   99.08 | 185,199           
  ...on-tracing.ts |   76.92 |    75.71 |   73.68 |   76.92 | ...88,395-397,413 
  ...attributes.ts |   96.98 |    91.37 |     100 |   96.98 | ...47-348,366-367 
  ...ag-metrics.ts |     100 |    77.77 |     100 |     100 | 21,40             
  ...t-loop-lag.ts |   96.85 |    85.71 |     100 |   96.85 | 170-173           
  ...-exporters.ts |   65.78 |    83.33 |   55.55 |   65.78 | ...04-105,108-109 
  ...ai-content.ts |    74.5 |    66.41 |   91.66 |    74.5 | ...1480,1493-1502 
  ...i-provider.ts |     100 |       99 |     100 |     100 | 99                
  ...ai-request.ts |   87.52 |    92.79 |   83.78 |   87.52 | ...55-561,564-570 
  gen-ai-usage.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |   99.12 |    96.03 |      95 |   99.12 | 150,379-380       
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |   60.73 |    78.01 |   66.66 |   60.73 | ...1507,1524-1544 
  metrics.ts       |   80.37 |    82.35 |   80.95 |   80.37 | ...1150,1153-1164 
  otlp-urls.ts     |     100 |      100 |     100 |     100 |                   
  ...attributes.ts |     100 |      100 |     100 |     100 |                   
  ...ime-config.ts |       0 |        0 |       0 |       0 | 1                 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  ...rters-grpc.ts |     100 |      100 |     100 |     100 |                   
  ...rters-http.ts |     100 |      100 |     100 |     100 |                   
  sdk-impl.ts      |   93.89 |    86.32 |      75 |   93.89 | ...39,489-490,506 
  sdk.ts           |    82.7 |     90.9 |   66.66 |    82.7 | ...00-204,242-264 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...ion-events.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   91.17 |    88.72 |    97.5 |   91.17 | ...1920,1949-1952 
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  trace-context.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   98.56 |    88.63 |     100 |   98.56 | 52,101            
  types.ts         |   83.09 |     95.1 |   86.36 |   83.09 | ...1467,1471-1478 
  uiTelemetry.ts   |   97.18 |    93.93 |      88 |   97.18 | ...70,314,461-462 
 ...ry/qwen-logger |   74.23 |    80.35 |      70 |   74.23 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   74.23 |    80.18 |   69.49 |   74.23 | ...1122,1160-1161 
 src/test-utils    |   96.38 |    98.61 |   83.33 |   96.38 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...mised-lock.ts |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   94.85 |      100 |   78.78 |   94.85 | ...53,227-228,241 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   86.39 |    85.15 |   88.84 |   86.39 |                   
  ...erQuestion.ts |   89.71 |    80.76 |   91.66 |   89.71 | ...66-367,374-375 
  ...-registrar.ts |    77.7 |    66.66 |   66.66 |    77.7 | ...72-277,292-294 
  ...ub-session.ts |   89.67 |     91.3 |   81.81 |   89.67 | ...03-304,315-322 
  cron-create.ts   |   90.64 |    92.85 |   72.72 |   90.64 | ...,73-74,223-231 
  cron-delete.ts   |   97.56 |      100 |   83.33 |   97.56 | 31-32             
  cron-list.ts     |   98.23 |    95.34 |    87.5 |   98.23 | 57-58             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  display-image.ts |   87.42 |    84.84 |   88.88 |   87.42 | ...29-134,194-195 
  edit.ts          |   82.76 |    86.77 |   81.25 |   82.76 | ...45-746,865-915 
  ...r-worktree.ts |   83.14 |    67.56 |    87.5 |   83.14 | ...84-187,278-279 
  enterPlanMode.ts |      85 |     82.6 |    87.5 |      85 | ...28-133,161-175 
  exit-worktree.ts |   83.29 |    83.65 |   94.44 |   83.29 | ...14-515,537-538 
  exitPlanMode.ts  |      95 |    85.29 |     100 |      95 | ...21-325,344,378 
  ...permission.ts |     100 |      100 |     100 |     100 |                   
  glob.ts          |   96.33 |     88.5 |     100 |   96.33 | ...24-225,373,376 
  grep.ts          |   90.73 |    86.61 |   85.71 |   90.73 | ...76-677,727-728 
  ...adTracking.ts |     100 |      100 |     100 |     100 |                   
  image-gen.ts     |   91.66 |    77.41 |    90.9 |   91.66 | ...13-214,221-222 
  list-agents.ts   |   94.02 |    82.35 |   83.33 |   94.02 | 31-32,47-48       
  loop-wakeup.ts   |   99.27 |    92.85 |     100 |   99.27 | 45                
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.71 |     59.5 |   90.32 |   72.71 | ...1212,1214-1215 
  ...nt-manager.ts |   82.13 |    80.47 |   85.71 |   82.13 | ...3234,3236-3237 
  mcp-client.ts    |   80.03 |    86.58 |   89.47 |   80.03 | ...2272,2276-2279 
  ...ry-timeout.ts |     100 |      100 |     100 |     100 |                   
  mcp-errors.ts    |     100 |      100 |     100 |     100 |                   
  ...pool-entry.ts |   79.21 |    85.71 |   81.57 |   79.21 | ...1341,1349-1350 
  ...ool-events.ts |       8 |        0 |       0 |       8 | 132-158           
  mcp-pool-key.ts  |   97.46 |    93.93 |     100 |   97.46 | 176-177           
  ...ce-content.ts |   96.55 |    91.17 |     100 |   96.55 | 80-82             
  mcp-retry.ts     |   97.67 |    95.65 |     100 |   97.67 | 131-132           
  ...ion-config.ts |     100 |      100 |     100 |     100 |                   
  mcp-status.ts    |     100 |      100 |     100 |     100 |                   
  mcp-tool.ts      |   98.35 |    93.71 |     100 |   98.35 | ...-990,1045-1046 
  ...sport-pool.ts |   83.98 |     80.3 |   88.46 |   83.98 | ...1409,1416-1420 
  ...ace-budget.ts |   87.27 |     82.6 |     100 |   87.27 | ...00-305,340-345 
  memory-config.ts |     100 |      100 |     100 |     100 |                   
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 101,108           
  monitor.ts       |   91.82 |    83.09 |   88.46 |   91.82 | ...99,612,810-815 
  notebook-edit.ts |   85.71 |    77.08 |   81.25 |   85.71 | ...96-912,958-959 
  ...escendants.ts |   36.17 |    64.51 |   55.55 |   36.17 | ...46-310,385-390 
  ...nforcement.ts |   83.21 |    90.69 |     100 |   83.21 | 147-158,207-220   
  read-file.ts     |   95.49 |    88.52 |   86.66 |   95.49 | ...49,464,536-537 
  ...p-resource.ts |   96.85 |      100 |   91.66 |   96.85 | 92-96             
  ...d-artifact.ts |   91.18 |    86.71 |    87.5 |   91.18 | ...26-427,441-453 
  ripGrep.ts       |    94.6 |    87.26 |   95.23 |    94.6 | ...33-734,740-741 
  ...-transport.ts |   71.42 |    55.55 |   71.42 |   71.42 | ...36-137,143-144 
  send-message.ts  |   81.13 |    89.74 |    62.5 |   81.13 | ...80-286,363-371 
  ...n-mcp-view.ts |   94.07 |    91.89 |    90.9 |   94.07 | 131-139           
  shell.ts         |   78.96 |    84.29 |      93 |   78.96 | ...5036,5111-5112 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   96.82 |    94.49 |   90.47 |   96.82 | ...39,574-577,581 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-create.ts   |    94.4 |    93.33 |   81.81 |    94.4 | 45-49,63-64,95    
  task-list.ts     |   78.22 |    84.21 |   83.33 |   78.22 | ...66,105,109-116 
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  task-update.ts   |   82.89 |    83.92 |    92.3 |   82.89 | ...14-422,454-465 
  team-create.ts   |   97.22 |    85.71 |   83.33 |   97.22 | 48-49,129-130     
  team-delete.ts   |   86.74 |    83.33 |   83.33 |   86.74 | 37-38,42-48,72-73 
  ...n-approval.ts |   92.14 |    96.77 |   77.77 |   92.14 | 38-39,42-43,93-99 
  todoWrite.ts     |   95.13 |    87.85 |   93.33 |   95.13 | ...23-527,540-545 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   78.57 |    79.59 |    82.6 |   78.57 | ...89-990,998-999 
  tool-search.ts   |   96.19 |    89.72 |   93.33 |   96.19 | ...09,259-264,426 
  tools.ts         |   93.11 |    92.53 |   91.66 |   93.11 | ...76-577,593-599 
  ...reapproved.ts |   99.27 |    94.11 |     100 |   99.27 | 170               
  web-fetch.ts     |   96.05 |    90.54 |   96.77 |   96.05 | ...85-786,800-801 
  web-search.ts    |   90.58 |    83.57 |      80 |   90.58 | ...1025,1083-1086 
  write-file.ts    |   86.72 |    84.92 |   88.88 |   86.72 | ...25-828,865-900 
  zoom-image.ts    |   95.76 |    93.75 |      90 |   95.76 | 54-59,203-204     
 src/tools/agent   |   86.91 |    87.59 |   88.49 |   86.91 |                   
  agent.ts         |   85.49 |    86.49 |   86.02 |   85.49 | ...4244,4278-4288 
  fork-profile.ts  |   93.65 |       90 |     100 |   93.65 | ...33-134,171-174 
  fork-subagent.ts |   98.73 |       95 |     100 |   98.73 | 101-102,173       
 ...tools/artifact |   95.78 |    92.51 |   88.63 |   95.78 |                   
  artifact-tool.ts |   91.46 |    88.46 |   71.42 |   91.46 | ...13-314,322-325 
  ...-publisher.ts |     100 |    85.71 |     100 |     100 | 32                
  ...-publisher.ts |   96.74 |    97.72 |    87.5 |   96.74 | 29-30,156-157     
  html.ts          |     100 |    96.77 |     100 |     100 | 122               
  ...-publisher.ts |     100 |       80 |     100 |     100 | 30                
  oss-publisher.ts |    98.1 |    91.48 |     100 |    98.1 | 43-45             
  publisher.ts     |     100 |      100 |     100 |     100 |                   
 ...s/computer-use |   90.21 |    82.17 |   78.08 |   90.21 |                   
  bootstrap.ts     |   59.42 |    80.95 |   41.66 |   59.42 | ...35-339,341-345 
  client.ts        |   80.11 |       90 |   77.77 |   80.11 | ...97,242-243,274 
  constants.ts     |     100 |    94.73 |     100 |     100 | 129,256           
  downloader.ts    |   65.29 |    52.77 |   58.33 |   65.29 | ...99-300,316-355 
  index.ts         |     100 |      100 |     100 |     100 |                   
  install-state.ts |   94.44 |    72.72 |     100 |   94.44 | 44-45             
  ...n-detector.ts |     100 |     87.5 |     100 |     100 | 50                
  schemas.ts       |     100 |      100 |     100 |     100 |                   
  tool.ts          |    96.3 |    85.71 |     100 |    96.3 | 75-76,184,252-258 
 ...tools/workflow |   86.64 |    84.81 |      75 |   86.64 |                   
  workflow.ts      |   86.64 |    84.81 |      75 |   86.64 | ...95,540,542-543 
 src/utils         |   93.08 |    89.82 |    96.8 |   93.08 |                   
  LruCache.ts      |     100 |      100 |     100 |     100 |                   
  ...Controller.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |      95 |     92.7 |     100 |      95 | ...49-550,657-661 
  bareMode.ts      |   81.81 |      100 |      50 |   81.81 | 18-19             
  ...ry-content.ts |   98.45 |    95.45 |     100 |   98.45 | 132-133,159-160   
  browser.ts       |   86.84 |    78.94 |     100 |   86.84 | 34,36-37,65-66    
  btwUtils.ts      |   13.95 |      100 |       0 |   13.95 | 17-31,34-55       
  bundlePaths.ts   |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   91.06 |    89.47 |     100 |   91.06 | ...46-147,154-155 
  ...n-branches.ts |   95.88 |    94.11 |      95 |   95.88 | ...98-499,511-524 
  ...tion-chain.ts |     100 |      100 |     100 |     100 |                   
  cronDisplay.ts   |     100 |    97.61 |     100 |     100 | 46                
  cronParser.ts    |   95.34 |    93.33 |     100 |   95.34 | 41-42,47-48,70-71 
  debugLogger.ts   |   96.66 |    96.61 |   88.88 |   96.66 | 192-196           
  editHelper.ts    |   93.63 |     83.9 |     100 |   93.63 | ...27-428,462-463 
  editor.ts        |   97.65 |    95.45 |     100 |   97.65 | ...35-336,338-339 
  encoding.ts      |     100 |      100 |     100 |     100 |                   
  env.ts           |     100 |      100 |     100 |     100 |                   
  ...arResolver.ts |   94.59 |    90.32 |     100 |   94.59 | 45-46,142-143     
  ...entContext.ts |   96.63 |    90.13 |   96.66 |   96.63 | ...42,444-445,512 
  errorParsing.ts  |     100 |      100 |     100 |     100 |                   
  ...rReporting.ts |   95.65 |    93.33 |     100 |   95.65 | 37-38             
  errors.ts        |   88.92 |    92.99 |   66.66 |   88.92 | ...92,394,410-411 
  fetch.ts         |   90.68 |    82.51 |     100 |   90.68 | ...72,483-484,503 
  file-identity.ts |     100 |      100 |     100 |     100 |                   
  fileUtils.ts     |   94.87 |    92.97 |   96.15 |   94.87 | ...1907,1915-1916 
  forkedAgent.ts   |   92.64 |    82.85 |   93.75 |   92.64 | ...47,655,660-667 
  formatters.ts    |     100 |      100 |     100 |     100 |                   
  ...eUtilities.ts |    92.4 |    86.95 |     100 |    92.4 | ...52-158,168-169 
  ...rStructure.ts |   94.39 |    94.28 |     100 |   94.39 | ...29-132,343-348 
  getPty.ts        |   31.57 |       50 |     100 |   31.57 | 26-38             
  git-branches.ts  |    91.6 |    84.21 |    92.3 |    91.6 | ...90,405-410,570 
  ...fig-safety.ts |   97.01 |       80 |     100 |   97.01 | 53-54             
  gitDiff.ts       |   95.19 |    81.36 |     100 |   95.19 | ...1073,1419-1420 
  gitDirect.ts     |   98.84 |    94.28 |     100 |   98.84 | 234,318           
  ...noreParser.ts |   94.48 |    93.22 |     100 |   94.48 | ...23-124,158-159 
  gitUtils.ts      |   78.83 |    82.35 |    87.5 |   78.83 | ...22-123,164-215 
  github-prs.ts    |   95.74 |    82.27 |     100 |   95.74 | 216,314-322       
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  image-view.ts    |   95.08 |    93.33 |     100 |   95.08 | ...62-166,234-238 
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 27                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |   95.41 |    93.47 |     100 |   95.41 | ...27-328,370-373 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...iconv-lite.ts |     100 |      100 |     100 |     100 |                   
  ...simple-git.ts |   96.77 |    91.66 |     100 |   96.77 | 38                
  ...m-headless.ts |      96 |    88.88 |     100 |      96 | 34                
  ...iagnostics.ts |    96.4 |     94.2 |     100 |    96.4 | ...66,293-294,376 
  ...yDiscovery.ts |    92.4 |    89.13 |     100 |    92.4 | ...28,331,522-525 
  ...tProcessor.ts |   94.01 |       90 |     100 |   94.01 | ...47-353,445-446 
  ...Inspectors.ts |     100 |      100 |     100 |     100 |                   
  modelId.ts       |   98.96 |    98.21 |     100 |   98.96 | 153               
  ...kerChecker.ts |    90.9 |    91.66 |     100 |    90.9 | 73-79             
  notebook.ts      |   94.57 |    89.91 |   95.83 |   94.57 | ...21,333,385-387 
  openaiLogger.ts  |   91.66 |    89.74 |     100 |   91.66 | ...26-228,251-256 
  osc8.ts          |   54.26 |    64.86 |   83.33 |   54.26 | ...72-195,197-257 
  partUtils.ts     |     100 |    98.64 |     100 |     100 | 211               
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   90.88 |     90.6 |     100 |   90.88 | ...25-626,628-630 
  pdf.ts           |   92.17 |    85.81 |     100 |   92.17 | ...64-565,606-611 
  ...s-liveness.ts |     100 |    93.47 |     100 |     100 | 62,72,108         
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  projectRoot.ts   |   71.73 |    78.57 |     100 |   71.73 | 54-66             
  ...ectSummary.ts |   89.62 |    72.41 |     100 |   89.62 | ...40-145,196-199 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   71.15 |       86 |     100 |   71.15 | ...-90,96-101,147 
  ...noreParser.ts |   92.63 |    91.66 |     100 |   92.63 | ...77-178,197-198 
  rateLimit.ts     |   93.75 |    89.62 |     100 |   93.75 | ...13,218-219,262 
  ...text-range.ts |   96.98 |    87.15 |     100 |   96.98 | ...87-688,763-764 
  readManyFiles.ts |   95.75 |    80.86 |     100 |   95.75 | ...05,558,568-572 
  retry.ts         |   96.09 |    92.52 |     100 |   96.09 | ...67,558-559,577 
  retryContext.ts  |     100 |      100 |     100 |     100 |                   
  ...sification.ts |   97.63 |    97.08 |     100 |   97.63 | ...17,251-252,278 
  retryPolicy.ts   |   97.72 |    90.56 |     100 |   97.72 | 130-131           
  ripgrepUtils.ts  |   90.04 |    93.43 |   95.45 |   90.04 | ...55-565,598-599 
  ...sDiscovery.ts |   97.46 |    93.05 |     100 |   97.46 | ...04,182-183,202 
  ...iagnostics.ts |   83.08 |     67.5 |   92.59 |   83.08 | ...23,543-544,550 
  ...tchOptions.ts |   84.87 |    86.71 |   96.29 |   84.87 | ...71,696,725-734 
  ...odelPrefix.ts |     100 |      100 |     100 |     100 |                   
  runtimeStatus.ts |   97.77 |    91.48 |     100 |   97.77 | 172-173           
  safe-mode.ts     |     100 |      100 |     100 |     100 |                   
  safeJsonParse.ts |     100 |      100 |     100 |     100 |                   
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...-child-env.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   98.03 |    97.75 |     100 |   98.03 | 100,102-103       
  ...aValidator.ts |   92.09 |    83.65 |   90.47 |   92.09 | ...60,882-883,896 
  ...r-launcher.ts |   96.35 |    93.97 |   85.71 |   96.35 | ...35-336,347-348 
  sedEditParser.ts |   91.78 |    92.18 |     100 |   91.78 | ...66-569,645-646 
  ...nIdContext.ts |     100 |      100 |     100 |     100 |                   
  ...orageUtils.ts |   96.21 |    85.21 |     100 |   96.21 | ...70,386,466,485 
  ...-pager-env.ts |     100 |      100 |     100 |     100 |                   
  ...fety-rules.ts |     100 |     89.7 |     100 |     100 | ...01,304,309-311 
  shell-utils.ts   |   86.26 |    88.58 |     100 |   86.26 | ...2295,2302-2306 
  ...lAstParser.ts |    98.3 |    91.59 |     100 |    98.3 | ...1340-1342,1352 
  ...ContextEnv.ts |     100 |    96.42 |     100 |     100 | 75                
  ...nlyChecker.ts |   96.33 |    96.57 |     100 |   96.33 | ...83-284,292-293 
  sideQuery.ts     |   86.82 |    86.66 |     100 |   86.82 | ...79-185,187-193 
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |       50 |     100 |   77.77 | 44,54-59          
  ...e-encoding.ts |   85.96 |    76.47 |     100 |   85.96 | 58-61,64-65,78-79 
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  textUtils.ts     |      65 |      100 |      75 |      65 | 56-75             
  thoughtUtils.ts  |     100 |    95.65 |     100 |     100 | 99                
  ...-converter.ts |   95.23 |    85.71 |     100 |   95.23 | 36-37             
  ...name-utils.ts |     100 |      100 |     100 |     100 |                   
  ...-finalizer.ts |    98.1 |     92.3 |   93.33 |    98.1 | ...34-235,237-241 
  ...iagnostics.ts |   99.06 |     97.7 |   91.66 |   99.06 | 132-133,204       
  ...-retention.ts |     100 |    95.83 |     100 |     100 | 116               
  tool-utils.ts    |    95.2 |    93.61 |     100 |    95.2 | ...58-159,162-163 
  ...ultCleanup.ts |   54.62 |       64 |      75 |   54.62 | ...03-105,108-134 
  ...Compaction.ts |   96.13 |    96.39 |     100 |   96.13 | ...34-339,341-346 
  ...pt-records.ts |   87.55 |    86.13 |     100 |   87.55 | ...78-482,512-527 
  truncation.ts    |   90.61 |    90.51 |     100 |   90.61 | ...53-461,498-504 
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   95.39 |    89.47 |     100 |   95.39 | ...16-317,321-322 
  xml.ts           |    97.8 |    87.69 |     100 |    97.8 | 98-99             
  yaml-parser.ts   |   83.87 |    77.27 |     100 |   83.87 | ...31-234,239-240 
 ...ils/filesearch |   83.94 |    80.72 |   94.73 |   83.94 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |    82.9 |    76.81 |   95.08 |    82.9 | ...1563,1597-1598 
  fileSearch.ts    |   93.78 |    87.67 |     100 |   93.78 | ...71-272,274-275 
  fzfWorker.ts     |       0 |        0 |       0 |       0 | 1-109             
  ...rkerHandle.ts |   84.05 |    75.43 |   89.47 |   84.05 | ...30-334,340-341 
  ignore.ts        |     100 |    97.36 |     100 |     100 | 187               
  result-cache.ts  |     100 |    93.75 |     100 |     100 | 49                
 ...uest-tokenizer |   69.76 |    75.47 |   85.29 |   69.76 |                   
  ...eTokenizer.ts |   65.72 |    74.02 |    92.3 |   65.72 | ...65-466,479-533 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |   76.92 |      100 |   33.33 |   76.92 | 46-49,56-57       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

@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 — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Test Plan (not a blocker): 1385 tests green — this review observed 16964, 19001 passed.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。 Test Plan(非阻断):1385 tests green — this review observed 16964, 19001 passed

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

Comment on lines +66 to +71
it('should block internal secrets regardless of casing (Windows process.env)', () => {
process.env['QWEN_SERVER_TOKEN'] = 'daemon-secret';

const result = resolveEnvVarsInString(
'curl https://x/t=$qwen_server_token',
);

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 casing regression test is vacuous on the platform CI runs on. It sets only the uppercase key process.env['QWEN_SERVER_TOKEN'] but references the lowercase $qwen_server_token; on Linux process.env is case-sensitive, so even without the isInternalSecretEnvVar guard the lowercase lookup is undefined and every assertion still passes. A probe (removing the guard) confirmed the uppercase test fails but this casing test still passes — the exact round-1 Critical (case-sensitive denylist) would ship green on Linux CI. — Failure scenario: remove .toUpperCase() from isInternalSecretEnvVar (or the guard entirely) and all three casing tests stay green on Linux.

Suggested change
it('should block internal secrets regardless of casing (Windows process.env)', () => {
process.env['QWEN_SERVER_TOKEN'] = 'daemon-secret';
const result = resolveEnvVarsInString(
'curl https://x/t=$qwen_server_token',
);
it('should block internal secrets regardless of casing (Windows process.env)', () => {
process.env['QWEN_SERVER_TOKEN'] = 'daemon-secret';
process.env['qwen_server_token'] = 'daemon-secret';
const result = resolveEnvVarsInString(
'curl https://x/t=$qwen_server_token',
);

The same gap exists in the parallel copies packages/cli/src/utils/envVarResolver.test.ts and packages/core/src/hooks/envInterpolator.test.ts — set the lowercase key there too (for the interpolator, so the whitelisted lookup would otherwise resolve it). Alternatively, add one platform-independent unit test for isInternalSecretEnvVar in packages/core/src/utils/sanitize-child-env.test.ts.

中文说明

大小写回归测试在 CI 运行的平台(Linux)上是空转的。测试只设置了大写键 process.env['QWEN_SERVER_TOKEN'],却引用小写的 $qwen_server_token;Linux 上 process.env 大小写敏感,因此即使没有 isInternalSecretEnvVar 守卫,小写查找也返回 undefined,所有断言依然通过。探针(移除守卫)确认:大写测试会失败,但本大小写测试仍然通过——第一轮的那个 Critical(大小写敏感的拒绝列表)在 Linux CI 上会绿灯通过。失败场景:从 isInternalSecretEnvVar 移除 .toUpperCase()(或整个守卫),三个大小写测试在 Linux 上仍全部为绿。同样的问题还存在于 packages/cli/src/utils/envVarResolver.test.tspackages/core/src/hooks/envInterpolator.test.ts——请在那里也设置小写键(对 interpolator,使白名单查找否则会解析出它)。或者,在 packages/core/src/utils/sanitize-child-env.test.ts 中为 isInternalSecretEnvVar 增加一个平台无关的单元测试。

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

* SPDX-License-Identifier: Apache-2.0
*/

import { isInternalSecretEnvVar } from '@qwen-code/qwen-code-core/sanitizeChildEnv';

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 file is a near-byte-identical copy of packages/core/src/utils/envVarResolver.ts (only the import path and one doc-comment clause differ — the doc comments have already drifted in this PR). This change applied the same isInternalSecretEnvVar guard and the same two tests to both copies. — Concrete cost: the next resolver change (a new denylist entry, a regex fix) must be applied to both files; missing one silently leaves the CLI path unpatched while core is fixed, or vice versa.

The './sanitizeChildEnv' subpath-export mechanism this PR adds could equally export the resolver. Consolidate by mirroring that entry in packages/core/package.json exports (e.g. "./envVarResolver"./dist/src/utils/envVarResolver.js), adding the matching alias in packages/cli/tsconfig.json and packages/cli/vitest.config.ts, deleting this copy and its test, and pointing the two CLI import sites (settings.ts, fast-path-settings.ts) at core.

中文说明

本文件与 packages/core/src/utils/envVarResolver.ts 几乎逐字节相同(仅 import 路径与一处文档注释不同——文档注释在本 PR 中已经漂移)。本次改动把相同的 isInternalSecretEnvVar 守卫与相同的两个测试同时应用到两份拷贝。具体代价:下一次解析器改动(新增拒绝列表项、正则修复)必须同时改两个文件;漏改其一会在 core 已修复时让 CLI 路径悄悄保持未打补丁(或反之)。本 PR 新增的 './sanitizeChildEnv' 子路径导出机制同样可以导出该解析器,建议合并:在 packages/core/package.json 的 exports 中仿照该条增加 "./envVarResolver",在 packages/cli/tsconfig.jsonpackages/cli/vitest.config.ts 增加对应别名,删除本拷贝及其测试,并把两处 CLI 引入点(settings.tsfast-path-settings.ts)指向 core。

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

Comment thread packages/cli/src/config/settings.ts Outdated
Comment on lines +420 to +422
function stripWorkspaceHookSecurityOverrides(settings: Settings): Settings {
const { allowPrivateNetworkHooks, allowedHttpHookUrls } =
settings.security ?? {};

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 set of workspace-stripped hook-security fields is now encoded independently in two functions that must be kept in sync by hand: stripWorkspaceHookSecurityOverrides strips the pair via destructuring, and getSettingsWarnings re-declares the same pair as two separate if blocks. This PR is the second field added to the pattern, so the recurrence is demonstrated, not hypothetical. — Failure scenario: for the next such field, adding it to the strip but forgetting getSettingsWarnings leaves it silently stripped with no warning; adding the warning but forgetting the strip tells the user it "is ignored" while it is still honored from workspace scope (the exact hole this PR class closes). No test couples the two field sets, so either divergence ships green.

Drive both from one constant, e.g.:

const WORKSPACE_STRIPPED_SECURITY_FIELDS = [
  'allowPrivateNetworkHooks',
  'allowedHttpHookUrls',
] as const;

with stripWorkspaceHookSecurityOverrides deleting each named key from security and getSettingsWarnings iterating the same array to emit warnings.

中文说明

需要从 workspace 剥离的 hook 安全字段集合,现在被独立编码在两个必须手工保持同步的函数里:stripWorkspaceHookSecurityOverrides 通过解构剥离这两个字段,而 getSettingsWarnings 又把同样的两个字段重写为两个独立的 if 块。本 PR 是向该模式添加的第二个字段,说明这种重复是现实存在的,而非假设。失败场景:对下一个此类字段,若只加进剥离函数而漏了 getSettingsWarnings,它会被静默剥离、没有任何告警;若只加告警而漏了剥离,用户会被告知该设置"被忽略",但它实际上仍从 workspace 作用域生效(正是本 PR 要关闭的那类漏洞)。没有任何测试把两个函数的字段集合耦合起来,因此任一方向的偏离都会绿灯通过。建议用同一个常量驱动两者:stripWorkspaceHookSecurityOverridessecurity 中删除其中每个具名键,getSettingsWarnings 遍历同一数组来发出告警。

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

Comment thread packages/cli/src/config/settings.ts Outdated
Comment on lines +374 to +379
if (
workspaceFile.rawJson !== undefined &&
workspaceFile.originalSettings.security?.allowedHttpHookUrls !== undefined
) {
warningSet.add(
`Warning: security.allowedHttpHookUrls in workspace settings (${workspaceFile.path}) is ignored. This setting is only honored from User, System, or SystemDefaults scope settings.`,

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 change makes a workspace-scoped security.allowedHttpHookUrls silently stripped (with the warning above), but it is undocumented — while the sibling security.allowPrivateNetworkHooks documents the identical restriction. In docs/users/features/hooks.md the sibling's scope rule is documented ("only honored from User, System, and SystemDefaults settings scopes … ignored and logged as a warning"), but the page's only mention of allowedHttpHookUrls advises users to "pair this flag with a whitelist" with no scope caveat. The settings-schema description (settingsSchema.ts) likewise omits the "workspace values are ignored" note the sibling carries. — Failure scenario: a user who follows the docs/schema and puts the whitelist in Workspace settings gets a non-working whitelist plus a warning, and neither the docs page nor the schema explains why this key behaves like its sibling.

Extend the scope bullet in docs/users/features/hooks.md to cover security.allowedHttpHookUrls (and add the same scope note where the whitelist is mentioned), and append the same scope note to the allowedHttpHookUrls description in packages/cli/src/config/settingsSchema.ts, re-syncing packages/vscode-ide-companion/schemas/settings.schema.json.

中文说明

本改动让 workspace 作用域的 security.allowedHttpHookUrls 被静默剥离(并触发上面的告警),但没有相应文档——而姊妹设置 security.allowPrivateNetworkHooks 对相同限制有文档。在 docs/users/features/hooks.md 中,姊妹设置的作用域规则有说明("仅从 User、System、SystemDefaults 设置作用域生效……被忽略并记录告警"),但该页对 allowedHttpHookUrls 的唯一提及却建议用户"将此标志与白名单配对使用",没有任何作用域提示。设置 schema 描述(settingsSchema.ts)同样缺少姊妹设置所带的"workspace 值被忽略"说明。失败场景:用户按文档/schema 把白名单放进 workspace 设置,会得到一个不生效的白名单加一条告警,而文档页与 schema 都没有解释为什么这个键的行为与姊妹设置一致。建议把 docs/users/features/hooks.md 中的作用域说明扩展到 security.allowedHttpHookUrls(并在提到白名单处补上同样的作用域提示),同时在 packages/cli/src/config/settingsSchema.tsallowedHttpHookUrls 描述中追加同样的作用域说明,并重新同步 packages/vscode-ide-companion/schemas/settings.schema.json

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review-feedback summary — PR #8396 (round 2)

All four inline findings from the round-2 review were verified against the code and resolved in code in commit 1353d0b60 (12 files changed, +47/−490 — a net deletion). No conflicts (--conflict false); origin/main was not merged.

Findings and dispositions

  • [Suggestion] Vacuous casing regression tests on Linux (rc:3700529680) — RESOLVED. Verified: the casing tests set only process.env['QWEN_SERVER_TOKEN'] while referencing $qwen_server_token, so on case-sensitive Linux process.env the lookup is undefined and the assertions pass even without the denylist guard. Fixed by also setting the lowercase key in both remaining casing tests (packages/core/src/utils/envVarResolver.test.ts, packages/core/src/hooks/envInterpolator.test.ts). The third copy named in the finding (packages/cli/src/utils/envVarResolver.test.ts) was deleted as part of the resolver consolidation below, so its gap is gone with it. Probe verification: temporarily removing the isInternalSecretEnvVar guards makes all four denylist tests — including both casing tests — fail on this Linux runner (4 failed); with the guards restored all pass. The round-1 Critical (case-sensitive denylist) can no longer ship green on Linux CI.
  • [Suggestion] Duplicated envVarResolver copy in the CLI package (rc:3700529684) — RESOLVED. Consolidated exactly as suggested, mirroring the ./sanitizeChildEnv mechanism this PR already introduced: added a ./envVarResolver subpath export to packages/core/package.json, matching aliases in packages/cli/tsconfig.json and packages/cli/vitest.config.ts, deleted the byte-near-identical CLI copy and its test, and repointed the two CLI import sites (settings.ts, fast-path-settings.ts) at core. The fast path stays narrow: the subpath pulls in only the resolver and sanitize-child-env, the same two modules the deleted copy already imported. Net effect: one implementation, one test suite, no drift surface.
  • [Suggestion] Workspace-stripped hook-security fields encoded twice (rc:3700529685) — RESOLVED. Introduced WORKSPACE_STRIPPED_SECURITY_FIELDS in packages/cli/src/config/settings.ts; stripWorkspaceHookSecurityOverrides deletes each named key and getSettingsWarnings iterates the same array, so the two functions can no longer diverge. Warning message text and strip behavior are unchanged (verified by the existing settings tests, including the strip/warning tests added in round 1).
  • [Suggestion] Undocumented workspace-scope restriction for security.allowedHttpHookUrls (rc:3700529686) — RESOLVED. Extended the scope bullet in docs/users/features/hooks.md where the whitelist is mentioned ("only honored from User, System, and SystemDefaults settings scopes … ignored and logged as a warning"), appended the same scope note to the allowedHttpHookUrls description in packages/cli/src/config/settingsSchema.ts, and regenerated packages/vscode-ide-companion/schemas/settings.schema.json via npm run generate:settings-schema (committed in the same commit).

Note on the review-body observation (non-blocker)

The review noted the PR Test Plan says "1385 tests green" while it observed 16964/19001. The counts differ because the plan quoted a focused subset; the full verification list below records what this round actually ran.

Environment note

This autofix shell runs inside a Qwen Code session that exports QWEN_HOME/SANDBOX. Twelve settings.test.ts tests (the #4466 home-.env family) are sensitive to that variable; they fail identically at the pre-round-2 HEAD (verified by reverse-applying this round's diff) and pass with env -u QWEN_HOME, i.e. in CI's clean environment. Focused suites were therefore run with QWEN_HOME/SANDBOX unset.

Verification

Commands actually run against the final tree (commit 1353d0b60):

  • npm run build — passed
  • npm run typecheck — passed (re-run after final restore of the tree)
  • npm run lint — passed (re-run after final restore of the tree)
  • npm run generate:settings-schema — passed; regenerated packages/vscode-ide-companion/schemas/settings.schema.json (exactly the allowedHttpHookUrls description line), committed
  • npm run bundle — passed; esbuild resolves the new @qwen-code/qwen-code-core/envVarResolver subpath export
  • Vitest (core, focused): src/utils/envVarResolver.test.ts + src/hooks/envInterpolator.test.ts + src/utils/sanitize-child-env.test.ts — 3 files, 56 passed
  • Vitest (cli, focused): src/config/settings.test.ts — 157 passed; src/serve/fast-path.test.ts + src/serve/process-env-guard.test.ts — 77 passed; src/config/settingsSchema.test.ts — 39 passed (all with env -u QWEN_HOME -u SANDBOX; see Environment note)
  • Guard-removal probe (reviewer's failure scenario): with the isInternalSecretEnvVar guards temporarily removed, 4 denylist tests fail on Linux (both casing tests included); guards restored, green again
  • Integration (focused, QWEN_SANDBOX=false, against the fresh bundle): cli/settings-migration.test.ts — 14 passed; cli/qwen-config-dir.test.ts — 6 passed, 1 failed. The single failure ("1d: CLI functions normally when QWEN_HOME is not set") is environmental: EACCES: permission denied, mkdir '/home/runner/.qwen' — this sandboxed shell cannot write to the runner's real HOME, and the crash happens in writeOutputLanguageFile at startup, before any settings resolution touched by this PR.
  • The full "Integration Tests (CLI, No Sandbox)" suite was not run locally (it was skipped in CI per the review); the focused settings-migration/config-dir runs above exercise the bundled CLI's settings-loading path, which is where this round's changes land. The workflow's CI remains the final gate.
中文说明

Autofix 审查反馈处理总结 — PR #8396(第 2 轮)

第 2 轮审查中的四条行内发现均已对照代码核实,并在提交 1353d0b60于代码层面解决(12 个文件变更,+47/−490 — 净删除)。无冲突(--conflict false);未合并 origin/main

发现与处理结果

  • [Suggestion] Linux 上空转的大小写回归测试(rc:3700529680)— 已解决。 已核实:大小写测试只设置了 process.env['QWEN_SERVER_TOKEN'],却引用 $qwen_server_token,因此在大小写敏感的 Linux process.env 上查找结果为 undefined,即使没有拒绝列表守卫断言也会通过。修复方式是在剩余两个大小写测试(packages/core/src/utils/envVarResolver.test.tspackages/core/src/hooks/envInterpolator.test.ts)中同时设置小写键。发现中提到的第三份拷贝(packages/cli/src/utils/envVarResolver.test.ts)已随下面的解析器合并一并删除,其缺口随之消失。探针验证:临时移除 isInternalSecretEnvVar 守卫后,全部四个拒绝列表测试——包括两个大小写测试——在本 Linux 环境上失败(4 failed);恢复守卫后全部通过。第 1 轮的 Critical(大小写敏感的拒绝列表)不再可能在 Linux CI 上绿灯通过。
  • [Suggestion] CLI 包中重复的 envVarResolver 拷贝(rc:3700529684)— 已解决。 完全按建议合并,复用本 PR 已引入的 ./sanitizeChildEnv 机制:在 packages/core/package.json 增加 ./envVarResolver 子路径导出,在 packages/cli/tsconfig.jsonpackages/cli/vitest.config.ts 增加对应别名,删除几乎逐字节相同的 CLI 拷贝及其测试,并把两处 CLI 引入点(settings.tsfast-path-settings.ts)指向 core。fast path 依然轻量:该子路径只引入解析器与 sanitize-child-env 两个模块,与被删除拷贝原本引入的完全相同。净效果:单一实现、单一测试套件、无漂移面。
  • [Suggestion] 从 workspace 剥离的 hook 安全字段被编码了两次(rc:3700529685)— 已解决。packages/cli/src/config/settings.ts 中引入 WORKSPACE_STRIPPED_SECURITY_FIELDSstripWorkspaceHookSecurityOverrides 删除其中每个具名键,getSettingsWarnings 遍历同一数组发出告警,两个函数不再可能彼此偏离。告警文案与剥离行为不变(已由现有 settings 测试验证,包括第 1 轮新增的剥离/告警测试)。
  • [Suggestion] security.allowedHttpHookUrls 的 workspace 作用域限制缺少文档(rc:3700529686)— 已解决。docs/users/features/hooks.md 中提到白名单处扩展了作用域说明("仅从 User、System、SystemDefaults 设置作用域生效……被忽略并记录告警"),在 packages/cli/src/config/settingsSchema.tsallowedHttpHookUrls 描述中追加了同样的作用域说明,并通过 npm run generate:settings-schema 重新生成 packages/vscode-ide-companion/schemas/settings.schema.json(与代码在同一提交中)。

关于审查正文中的观察项(非阻断)

审查指出 PR Test Plan 写的是 "1385 tests green",而其观察到 16964/19001。数字不同是因为计划中引用的是聚焦子集;下方的完整验证清单记录了本轮实际运行的内容。

环境说明

本 autofix shell 运行在一个导出了 QWEN_HOME/SANDBOX 的 Qwen Code 会话内。settings.test.ts 中 12 个测试(#4466 的 home-.env 系列)对该变量敏感;它们在第 2 轮改动之前的 HEAD 上同样失败(通过反向应用本轮 diff 验证),在 env -u QWEN_HOME 下通过,即 CI 的干净环境下通过。因此聚焦测试套件均在取消设置 QWEN_HOME/SANDBOX 后运行。

验证

针对最终代码树(提交 1353d0b60)实际运行的命令:

  • npm run build — 通过
  • npm run typecheck — 通过(在最终恢复代码树后重新运行)
  • npm run lint — 通过(在最终恢复代码树后重新运行)
  • npm run generate:settings-schema — 通过;重新生成 packages/vscode-ide-companion/schemas/settings.schema.json(恰好是 allowedHttpHookUrls 描述那一行),已提交
  • npm run bundle — 通过;esbuild 能解析新的 @qwen-code/qwen-code-core/envVarResolver 子路径导出
  • Vitest(core,聚焦):src/utils/envVarResolver.test.ts + src/hooks/envInterpolator.test.ts + src/utils/sanitize-child-env.test.ts — 3 个文件,56 通过
  • Vitest(cli,聚焦):src/config/settings.test.ts — 157 通过;src/serve/fast-path.test.ts + src/serve/process-env-guard.test.ts — 77 通过;src/config/settingsSchema.test.ts — 39 通过(均以 env -u QWEN_HOME -u SANDBOX 运行;见环境说明)
  • 移除守卫探针(审查给出的失败场景):临时移除 isInternalSecretEnvVar 守卫后,4 个拒绝列表测试在 Linux 上失败(含两个大小写测试);恢复守卫后重新全绿
  • 集成测试(聚焦,QWEN_SANDBOX=false,基于新构建的 bundle):cli/settings-migration.test.ts — 14 通过;cli/qwen-config-dir.test.ts — 6 通过,1 失败。唯一失败项("1d: CLI functions normally when QWEN_HOME is not set")是环境问题:EACCES: permission denied, mkdir '/home/runner/.qwen' — 本沙箱 shell 无法写入 runner 的真实 HOME,且崩溃发生在启动时的 writeOutputLanguageFile,早于本 PR 触及的任何设置解析逻辑。
  • 完整的 "Integration Tests (CLI, No Sandbox)" 套件未在本地运行(CI 中按审查所述被跳过);上述聚焦的 settings-migration/config-dir 运行已覆盖打包 CLI 的设置加载路径,即本轮改动落点。工作流的 CI 仍是最终验证关卡。

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-preview

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head ccf1a7a, 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-dev-bot

Copy link
Copy Markdown
Collaborator

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

🤖 Reviewed the latest feedback on PR #8396 (head 1353d0b60) — no changes needed.

Why, point by point

  • Automated reviewer (reviews + inline findings): no new reviews or inline comments since the last evaluation (2026-08-02T23:02:08Z). The two prior review batches are already addressed:
    • 2026-08-02T19:49Z batch (case-sensitive secret denylist, heavy fast-path import, workspace-strip warning, untrusted-subagent test gap) → fixed in 50b7f8657.
    • 2026-08-02T23:02Z batch (vacuous casing test, duplicated envVarResolver, duplicated workspace-strip field set, silent strip of workspace-scoped allowedHttpHookUrls) → fixed in 1353d0b60 (meaningful casing tests, single env resolver, shared workspace-strip constant).
  • Issue-level comments: the only comment newer than the cutoff is the serve daemon A/B report, which found no response changes against the PR base across 4 scenarios. It is informational and requires no action.
  • Failed checks: none. All completed CI checks on the current head pass; there are no still-red checks in this round's evaluation.
  • Base conflict: none reported (--conflict false), so no merge of origin/main was performed.

No code changes were made and no commit was created this round.

中文说明

🤖 已审阅 PR #8396(HEAD 为 1353d0b60)的最新反馈——无需任何改动。

逐点说明

  • 自动审查器(评审 + 行内意见): 自上次评估(2026-08-02T23:02:08Z)以来没有新的评审或行内评论。此前两批意见均已处理完毕:
    • 2026-08-02T19:49Z 批次(大小写敏感的密钥拒绝名单、重量级 fast-path 导入、workspace 剥离警告、不可信子代理测试缺口)→ 已在 50b7f8657 中修复。
    • 2026-08-02T23:02Z 批次(空洞的大小写回归测试、重复的 envVarResolver、重复编码的 workspace 剥离字段集合、workspace 级 allowedHttpHookUrls 被静默剥离)→ 已在 1353d0b60 中修复(有意义的大小写测试、单一 env resolver、共享的 workspace 剥离常量)。
  • Issue 级评论: 晚于评估截止时间的唯一评论是 serve 守护进程 A/B 报告,结论是在 4 个场景下与 PR 基础分支相比响应无变化。该评论仅为信息通报,无需处理。
  • 失败的检查: 无。当前 HEAD 上所有已完成的 CI 检查均通过;本轮评估中没有持续失败的检查。
  • 基础分支冲突: 无(--conflict false),因此未执行 origin/main 合并。

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

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


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

@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. 3 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not reviewed: reverse audit — stopped before round 4 by the review time budget. Not reviewed: verification and reverse audit — each ran and opened its brief, but neither was launched with the prompt the CLI built — the launches were written by hand, so the posted findings cannot be counted as verified, and what the agents were actually asked is not what this skill certifies. Test Plan (not a blocker): 1385 tests green — this review observed 17658, 19229, 475 passed.

中文说明

已审查。 建议见行内评论。 3 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。 未审查:反向审计——评审时间预算不足,未能开始第 4 轮。 未审查:验证与反向审计——两者都运行并打开了各自的 brief,但都不是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,发布的发现不能算作已验证,agent 实际被要求做的也不是本 skill 所认证的内容。 Test Plan(非阻断):1385 tests green — this review observed 17658, 19229, 475 passed

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

Comment on lines +939 to +941
const homeIsProjectRoot =
path.resolve(runtimeContext.getProjectRoot()) ===
path.resolve(os.homedir());

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.

[Critical] R13-3: Worktree-isolation spawn bypasses the home-root trust gate. The gate reads runtimeContext.getProjectRoot(), but the worktree-isolation / working_dir spawn paths rebind that getter to the worktree path (agent.ts:2977, workflow-orchestrator.ts:1086, InProcessBackend.ts:521), while agent listing — which decides that repo agents surface at 'user' level at home — reads this.config.getProjectRoot(). In an untrusted folder whose project root IS the home directory, spawning a 'user'-surfaced repo agent with isolation: 'worktree' makes homeIsProjectRoot false → trustedAgentLevel true → addAgentHooks registers repo-supplied hooks session-wide despite isTrustedFolder() false. There is no trust gate on the isolation provisioning path. — Failure scenario: user runs with $HOME as project root (dotfiles repo / CI container), folder untrusted; repo .qwen/agents/evil.md (hooks frontmatter) surfaces at 'user' level; spawning it with isolation: 'worktree' registers those hooks for every event session-wide — the exact code execution this gate blocks on the normal path. Probe-verified mirroring the real rebind shape: worktree-rebind arm → addAgentHooks called once; no-isolation arm → 0; reading this.config.getProjectRoot() instead flips the rebind arm to 0 with all 14 gate tests still green. Residual sibling entrance of the R12-6 fix (direct spawn is gated correctly).

Suggested change
const homeIsProjectRoot =
path.resolve(runtimeContext.getProjectRoot()) ===
path.resolve(os.homedir());
const homeIsProjectRoot =
path.resolve(this.config.getProjectRoot()) ===
path.resolve(os.homedir());

Apply the same rebind-proof root source to the skill-side gate (see the R13-9 thread on skill.ts).

中文说明

[Critical] R13-3:worktree 隔离派生绕过了 home-root 信任门禁。门禁读取 runtimeContext.getProjectRoot(),但 worktree 隔离 / working_dir 派生路径会把该 getter 重绑为 worktree 路径(agent.ts:2977workflow-orchestrator.ts:1086InProcessBackend.ts:521),而 agent 列表逻辑(决定 home 下仓库 agent 以 'user' 级出现的那一侧)读的是 this.config.getProjectRoot()。在项目根目录就是主目录且文件夹不受信时,用 isolation: 'worktree' 派生一个以 'user' 级出现的仓库 agent,会使 homeIsProjectRoot 为 false → trustedAgentLevel 为 true → 尽管 isTrustedFolder() 为 false,addAgentHooks 仍把仓库提供的 hooks 注册到整个会话。隔离准备路径上没有任何信任门禁。—— 失败场景:用户以 $HOME 为项目根(dotfiles 仓库 / CI 容器)、文件夹不受信;仓库 .qwen/agents/evil.md(frontmatter 带 hooks)以 'user' 级出现;以 isolation: 'worktree' 派生它,这些 hooks 就会在整个会话内为每个事件注册——正是该门禁在普通路径上所阻止的代码执行。已按真实重绑形态探针验证:worktree 重绑分支 → addAgentHooks 被调用一次;无隔离分支 → 0 次;改读 this.config.getProjectRoot() 后重绑分支翻转为 0,且 14 个门禁测试全部保持绿色。属 R12-6 修复的残留同类入口(直接派生已被正确门禁拦截)。

修复:从 per-agent 覆盖无法重绑的根来源计算 homeIsProjectRoot——例如 this.config.getProjectRoot()(manager 自身的 config,与列表侧及 skill.ts 一致),并对 skill 侧门禁(见 skill.ts 上的 R13-9 线程)使用同一来源。

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

Comment thread packages/core/src/tools/skill.ts Outdated
Comment on lines +381 to +382
const homeIsProjectRoot =
path.resolve(this.config.getProjectRoot()) === path.resolve(os.homedir());

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.

[Critical] R13-9: Skill-side sibling entrance of the R13-3 bypass. This gate reads this.config.getProjectRoot() — but in a subagent context this.config IS the per-agent override config whose getProjectRoot is rebound (worktree isolation agent.ts:2977; working_dir pin InProcessBackend.ts:521): rebuildToolRegistryOnOverride builds a fresh SkillTool on the override while the shared SkillManager keeps the skill's 'user' level. When a worktree-isolated / working_dir-pinned subagent invokes a repo skill surfaced at 'user' level (home-root topology, untrusted folder), homeIsProjectRoot = worktree path === homedir → false → sideEffectsGated false despite isTrustedFolder() false. — Failure scenario: untrusted folder whose project root IS home; repo skills surface at 'user' level; top-level invocation gates correctly, but the model spawns a subagent with isolation: 'worktree' and invokes the skill inside it → repo-supplied hooks register on the PARENT session's hooks manager (SessionHooksManager.addSessionHook has no dedupe; getHookSystem/getSessionId are inherited from the base config) and allowedTools become session-wide permission auto-approvals. Probe-verified through the real SkillTool.execute(): top-level arm → 0 registrations; worktree-rebind arm → registerSkillHooks once + addSessionAllowRule once with ['Bash(curl *)']; flips when the home-root detection is made rebind-proof. Note: the R13-3 fix (read this.config.getProjectRoot()) does NOT close this path — this gate already reads it, and it is the rebound value.

Suggested fix (spans locations): don't compute home-root shadowing from a per-agent-rebindable getter — propagate SkillManager's home-root detection instead (e.g. a flag on SkillConfig set at collection time, or gate any 'user'-level skill whose filePath resolves inside the repository tree), and use the same source of truth in SkillCommandLoader.ts:164 and subagent-manager.ts:939.

中文说明

[Critical] R13-9:R13-3 绕过的 skill 侧同类入口。此门禁读取 this.config.getProjectRoot()——但在 subagent 上下文中,this.config 正是那个 getProjectRoot 被重绑过的 per-agent 覆盖 config(worktree 隔离见 agent.ts:2977working_dir 固定见 InProcessBackend.ts:521):rebuildToolRegistryOnOverride 会在该覆盖 config 上构建新的 SkillTool,而共享的 SkillManager 保持该 skill 的 'user' 级不变。当 worktree 隔离 / working_dir 固定的 subagent 调用一个以 'user' 级出现的仓库 skill(home 为项目根、文件夹不受信)时,homeIsProjectRoot = worktree 路径 === 主目录 → false → 尽管 isTrustedFolder() 为 false,sideEffectsGated 仍为 false。—— 失败场景:项目根即主目录的不受信文件夹;仓库 skill 以 'user' 级出现;顶层调用被正确门禁拦截,但模型派生一个 isolation: 'worktree' 的 subagent 并在其中调用该 skill → 仓库提供的 hooks 注册到父会话的 hooks 管理器(SessionHooksManager.addSessionHook 无去重;getHookSystem/getSessionId 继承自基础 config),allowedTools 成为整个会话的权限自动批准。已通过真实 SkillTool.execute() 探针验证:顶层分支 → 0 次注册;worktree 重绑分支 → registerSkillHooks 一次 + addSessionAllowRule 一次(['Bash(curl *)']);使 home-root 判定不受重绑影响后翻转。注意:R13-3 的修复(改读 this.config.getProjectRoot())无法关闭此路径——本门禁读的本来就是它,而它正是被重绑的值。

修复建议(跨位置):不要从可被 per-agent 重绑的 getter 计算 home-root 影子判定——改为传播 SkillManager 的 home-root 判定(例如在收集期为 SkillConfig 设置一个标志,或对任何 filePath 落在仓库树内的 'user' 级 skill 施加门禁),并在 SkillCommandLoader.ts:164subagent-manager.ts:939 使用同一事实来源。

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

Comment on lines +227 to +230
const nonAscii = /[\u0080-\uFFFF]/;
if (nonAscii.test(outerPattern) || nonAscii.test(innerPattern)) {
return false;
}

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] R13-4: hookUrlPatternCovers fail-closes on chars ≥ U+0080 but lets ASCII line terminators \n (0x0A) and \r (0x0D) through, which diverge from the runtime regex (no s flag, strict $, . cannot cross terminators). Probe-verified: hookUrlPatternCovers('https://corp.com/*', 'https://corp.com/*\n') → true, while new UrlValidator(['https://corp.com/*\n']).isAllowed('https://corp.com/x\n') is true but new UrlValidator(['https://corp.com/*']).isAllowed('https://corp.com/x\n') is false — so such a workspace entry survives narrowWorkspaceHookSecurityOverrides and displaces the higher-scope whitelist. Today's sole consumer neutralizes it (httpHookRunnernew URL()/fetch() apply WHATWG normalization, which strips \t\n\r — probe-verified, no host escape), so no widened network destination is reachable now — Concrete cost: latent widening for any future consumer that matches raw strings without normalization, plus a policy-integrity hit today (the user's list is displaced by a newline-quirked subset, so plain-URL hooks the user allowed stop validating). — Suggested fix:

  const unsafe = /[\u0000-\u001f\u007f\u0080-\uFFFF]/;
  if (unsafe.test(outerPattern) || unsafe.test(innerPattern)) {
    return false;
  }

(mirrors the existing non-ASCII fail-closed gate; all 41 existing urlValidator tests stay green with it).

中文说明

[Suggestion] R13-4:hookUrlPatternCovers 对 ≥ U+0080 的字符 fail closed,但放过了 ASCII 行终止符 \n(0x0A)与 \r(0x0D),而它们与运行时正则(无 s 标志、严格 $. 不能跨越行终止符)存在语义分歧。已用探针验证:hookUrlPatternCovers('https://corp.com/*', 'https://corp.com/*\n') → true,而 new UrlValidator(['https://corp.com/*\n']).isAllowed('https://corp.com/x\n') 为 true、new UrlValidator(['https://corp.com/*']).isAllowed('https://corp.com/x\n') 为 false——因此这类 workspace 条目能在 narrowWorkspaceHookSecurityOverrides 中存活并替换更高 scope 的白名单。当前唯一消费者会将其消解(httpHookRunnernew URL()/fetch() 应用 WHATWG 规范化,会剥离 \t\n\r——已探针验证,无主机逃逸),所以目前无可达的放宽目的地——具体代价:对未来任何不做规范化、直接匹配原始字符串的消费者构成潜在放宽;当下则有策略完整性损失(用户列表被一个带换行的子集替换,用户本已放行的普通 URL hooks 反而不再通过校验)。—— 修复建议:把 fail-closed 门禁扩展到全部 ASCII 控制字符(见上方代码块),与现有非 ASCII 门禁同构;现有 41 个 urlValidator 测试在该修复下全部保持绿色。

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

Comment on lines +252 to +254
if (specifier === '@qwen-code/qwen-code-core/envVarResolver') {
return { shortCircuit: true, url: '${envVarResolverSrcUrl}', format: 'module' };
}

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] R13-2: The 17 lines added to this file (the envVarResolver subpath source-remap in the embedded ESM loader) are unreachable by every test command in the project — the file sits outside every npm workspace, and every vitest config under integration-tests/ includes only **/*.test.ts-style globs, so nothing collects it. — Concrete cost: if the specifier string, the remapped path, or the shortCircuit shape is wrong, nothing in CI or any suite run goes red; the defect surfaces only as a runtime resolution failure the next time a human manually runs the skill-review-harness after mode (which imports SkillReviewDialog → cli config/settings.js → the envVarResolver subpath), with no gate having ever validated it. — Suggested fix: exercise the remap somewhere a test command collects it (e.g. a scenario or a small *.test.ts under integration-tests/ asserting the loader resolves the subpath to the source file), or attach a manual terminal-capture run of the after mode as PR evidence.

中文说明

[Suggestion] R13-2:本文件新增的 17 行(内嵌 ESM loader 中的 envVarResolver 子路径源码重映射)无法被项目中任何测试命令触达——该文件不在任何 npm workspace 内,而 integration-tests/ 下的每个 vitest 配置只收录 **/*.test.ts 形式的文件,因此没有任何套件会加载它。—— 具体代价:如果 specifier 字符串、重映射路径或 shortCircuit 形态有误,CI 或任何套件运行都不会变红;该缺陷只会在下次有人手动运行 skill-review-harness 的 after 模式时(其导入链为 SkillReviewDialog → cli config/settings.jsenvVarResolver 子路径)以运行时解析失败的形式出现,而此前没有任何门禁验证过它。—— 修复建议:在某个会被测试命令收录的位置行使该重映射(例如一个场景,或 integration-tests/ 下一个断言 loader 能把该子路径解析到源文件的小型 *.test.ts),或者把一次 after 模式的手动 terminal-capture 运行结果作为 PR 证据附上。

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

Comment on lines +860 to +861
// secretEnv used to return the variable *name* as the secret — a
// public constant that makes HMAC verification bypassable.

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] R13-5: This rationale comment misdescribes the pre-fix behavior. Verified against the merge base: resolveWebhookSecretEnv resolved the variable's VALUE (const envValue = env[envName]; … return envValue;) — it never returned the name. The name-as-secret behavior was this PR's own intermediate denylist branch (commit 9569dbf71), replaced by the throw in 832f82b47 — the same commit that introduced this comment. — Concrete cost: once merged, "used to" reads as the last-shipped behavior, which resolved the value: with QWEN_SERVER_TOKEN set, the live daemon token itself would have become the HMAC secret of a repo-configured webhook — a strictly worse exposure than the "public constant" described here. A future maintainer or incident reviewer using this test as the canonical denylist rationale would understate the historical exposure and could wrongly scope rotation/audit. (The adjacent code comment in config-utils.ts is fine — it describes the hypothetical keep-the-placeholder design, not merge-base history.)

Suggested change
// secretEnv used to return the variable *name* as the secret — a
// public constant that makes HMAC verification bypassable.
// Before the denylist, secretEnv resolved the variable's *value* into
// the webhook secret — with QWEN_SERVER_TOKEN set, the live daemon token
// itself became the HMAC secret of a repo-configured webhook.
中文说明

[Suggestion] R13-5:该理由注释误述了修复前的行为。已对合并基核实:resolveWebhookSecretEnv 解析的是变量的const envValue = env[envName]; … return envValue;)——从未返回过变量名。把名字当 secret 的行为是本 PR 自己的中间态 denylist 分支(commit 9569dbf71),在 832f82b47 中被改为抛错——正是引入本注释的同一 commit。—— 具体代价:合入后 "used to" 会被读作上一个已发布行为,而那个行为解析的是值:在设置了 QWEN_SERVER_TOKEN 时,活跃的 daemon token 本身会成为仓库所配置 webhook 的 HMAC secret——比这里描述的"公开常量"严重得多的暴露面。未来以本测试作为 denylist 权威理由的维护者或事件复盘者会低估历史暴露面,并可能错误地界定凭据轮换/审计范围。(config-utils.ts 中相邻的代码注释没有问题——它描述的是假设的"保留占位符"设计,而非合并基历史。)

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

Comment on lines +847 to +848
it('grants allowedTools for a user-level skill regardless of folder trust', async () => {
vi.mocked(config.isTrustedFolder).mockReturnValue(false);

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] R13-8: The new allowedTools trust gating describe mirrors the sibling hooks describe for every level/trust case except the home-directory-rooted 'user' skill case — the topology this PR explicitly calls out as breaking the 'user' trust premise. The hooks side pins it with two tests (~612, ~634); this side has zero home-root coverage. Mutation probe (ran): removing (skill.level === 'user' && homeIsProjectRoot) from the shared sideEffectsGated predicate keeps all 7 allowedTools tests green — only the hooks-side test fails. — Concrete cost: today both side effects hang off one shared boolean, so nothing is wrong yet; but the hooks and allowedTools branches are separate if blocks in applySkillSideEffects — if a later change detaches the allowedTools grant from the shared condition without the home-root clause (the exact divergence class this PR already exhibited once, before the gate moved above applySkillAllowedTools), a repo skill in a home-rooted project would silently gain session-wide permission auto-approvals while untrusted, and no test in this PR would go red. — Suggested fix: mirror the two hooks-side home-root tests here: getProjectRoot mocked to os.homedir(), level: 'user', asserting mockAddSessionAllowRule is NOT called when isTrustedFolder returns false and called twice when it returns true.

中文说明

[Suggestion] R13-8:新的 allowedTools trust gating describe 对每个级别/信任组合都与姊妹 hooks describe 对齐,唯独缺少 home 目录为项目根的 'user' 级 skill 场景——而本 PR 明确指出该拓扑会打破 'user' 级的信任前提。hooks 一侧已用两个测试钉住(约 612、634 行);本侧对 home-root 零覆盖。已跑变异探针:从共享的 sideEffectsGated 谓词中移除 (skill.level === 'user' && homeIsProjectRoot),全部 7 个 allowedTools 测试保持绿色——只有 hooks 一侧的测试失败。—— 具体代价:当前两种副作用共用同一个布尔量,所以现在还没有问题;但 hooks 与 allowedTools 是 applySkillSideEffects 中两个独立的 if 分支——如果后续改动把 allowedTools 授予从共享条件中拆出且不带 home-root 子句(本 PR 在此前迭代中已经出现过一次的同类分歧),home 为项目根时的仓库 skill 将在文件夹不受信时悄然获得整个会话的权限自动批准,而本 PR 中没有任何测试会变红。—— 修复建议:在此镜像 hooks 一侧的两个 home-root 测试:getProjectRoot mock 为 os.homedir()level: 'user',断言 isTrustedFolder 返回 false 时 mockAddSessionAllowRule 不被调用、返回 true 时被调用两次。

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

Comment on lines +607 to +610
it('does not grant allowedTools for a user-level skill when the project root is the home directory', async () => {
// SkillManager skips the 'project' level when the project root IS
// the home directory, so repository-committed skills surface at
// 'user' level there and must stay gated on folder trust.

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] R13-10: No test pairs home-root with a trusted folder — the only homedir() use in this file (line ~612, this test) is the untrusted case, so the gate's "trusted folder always wins" disjunct is untested for the home-root combination. The over-gating mutant !(skill.level === 'user' && homeIsProjectRoot) && (isTrustedSkillLevel(skill.level) || this.config?.isTrustedFolder()) — a plausible "home-rooted user skills stay gated" simplification — passes all 35 tests in this file (ran it), yet silently withholds allowedTools auto-approvals from user-level skills when the project root IS home and the folder is explicitly trusted. The sibling gates pin this arm (skill.test.ts home-root-trusted, subagent-manager.test.ts home-root-trusted); the loader is the lone gate without it. — Concrete cost: that regression ships green and surfaces only as a user-visible loss of skill permission grants in that topology. — Suggested fix: add a companion test — getProjectRoot mocked to os.homedir(), isTrustedFolder mocked true, a user-level skill with two allowedToolsexpect(mockAddSessionAllowRule).toHaveBeenCalledTimes(2).

中文说明

[Suggestion] R13-10:没有测试把 home-root 与受信文件夹组合在一起——本文件中唯一使用 homedir() 的地方(约 612 行,即本测试)是不受信场景,因此门禁中"受信文件夹始终放行"这一分支在 home-root 组合下没有测试覆盖。过度门禁变异 !(skill.level === 'user' && homeIsProjectRoot) && (isTrustedSkillLevel(skill.level) || this.config?.isTrustedFolder())——一个看似合理的"home 为根的 user skill 保持门禁"简化——通过本文件全部 35 个测试(已实际运行),却会在项目根即主目录且文件夹已明确受信时,悄然拒绝 user 级 skill 的 allowedTools 自动批准。姊妹门禁都钉住了该分支(skill.test.ts 的 home-root 受信用例、subagent-manager.test.ts 的 home-root 受信用例);loader 是三者中唯一没有的。—— 具体代价:该回归会以全绿状态合入,只在该拓扑下以用户可见的 skill 权限授予丢失形式显现。—— 修复建议:补一个配套测试——getProjectRoot mock 为 os.homedir()isTrustedFolder mock 为 true、带两个 allowedTools 的 user 级 skill → expect(mockAddSessionAllowRule).toHaveBeenCalledTimes(2)

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

Comment on lines +323 to +325
expect(
hookUrlPatternCovers('https://corp.com/*', 'https://corp.com/a+b'),
).toBe(false);

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] R13-11: hookUrlPatternCovers fails closed on regex-active characters (? + $ () …) even for patterns with NO \. escape, although compilePattern's escape branch literalizes all of them for unescaped patterns — so coverage of query-string URL entries is provable but rejected, and narrowWorkspaceHookSecurityOverrides silently drops the workspace narrowing for the most common hook-endpoint shape. Probe-verified: hookUrlPatternCovers('https://corp.com/*', 'https://corp.com/ci?branch=main') → false, while new UrlValidator(['https://corp.com/ci?branch=main']).isAllowed('https://corp.com/ci?branch=main') → true and the lookalike 'https://corp.com/cbranch=main' → false, proving literalization; the outer list also admits the entry. Fails safe (never widens) — Concrete cost: user scope ['https://corp.com/*'] + trusted workspace narrowing to ['https://corp.com/ci?branch=main'] → entry filtered out → empty intersection → workspace list deleted → the broader user list stands and the repo author's explicit narrowing silently never applies; the always-on generic warning cannot distinguish successful narrowing from dropped narrowing. The docstring's fail-closed rationale ("the pre-escaped compilePattern branch would read it as raw regex") only holds for patterns containing \.; this pinned a+b assertion has no rationale of its own. — Suggested fix: apply the regexActive fail-closed only when either pattern contains \. (for \.-free patterns the escape branch guarantees literalness, so specials compare as literals); verified the conditioning flips the probe and breaks only this pinned assertion while all other fail-closed tests stay green. Alternatively, document in the docstring/test comment why blanket conservatism is preferred when coverage is provable.

中文说明

[Suggestion] R13-11:hookUrlPatternCovers 对含正则活性字符(? + $ () …)的模式一律 fail closed,即使模式不含任何 \. 转义——而 compilePattern 的转义分支对未转义模式会把这些字符全部字面量化,因此 query 字符串 URL 条目的覆盖关系是可证明的,却被拒绝,narrowWorkspaceHookSecurityOverrides 会对最常见的 hook 端点形态悄悄丢弃 workspace 收窄。已探针验证:hookUrlPatternCovers('https://corp.com/*', 'https://corp.com/ci?branch=main') → false,而 new UrlValidator(['https://corp.com/ci?branch=main']).isAllowed('https://corp.com/ci?branch=main') → true、形似串 'https://corp.com/cbranch=main' → false,证明确实被字面量化;外层列表同样放行该条目。方向安全(从不放宽)—— 具体代价:user scope ['https://corp.com/*'] + 受信 workspace 收窄为 ['https://corp.com/ci?branch=main'] → 条目被过滤 → 交集为空 → workspace 列表被删除 → 更宽的 user 列表保持不变,仓库作者明确的收窄悄悄失效;常开的通用告警无法区分"收窄成功"与"收窄被丢弃"。docstring 的 fail-closed 理由("pre-escaped 的 compilePattern 分支会把它们当原生正则")只对含 \. 的模式成立;此处钉住的 a+b 断言没有自己的理由说明。—— 修复建议:仅当任一模式含 \. 时才应用 regexActive fail-closed(不含 \. 时转义分支保证字面性,特殊字符可按字面比较);已验证该条件化使探针翻转,且仅破坏此钉住的断言,其余 fail-closed 测试全部保持绿色。或者在 docstring/测试注释中说明:在覆盖关系可证明时为何仍偏好一刀切的保守。

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

Comment on lines +3452 to +3456
// System scope is the final override: admin policy wins over both
// the user list and any workspace narrowing.
expect(settings.merged.security?.allowedHttpHookUrls).toEqual([
'https://managed.example.com/*',
]);

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] R13-13: This test never exercises the interaction it names: the workspace entry ['https://hooks.corp.com/ci/*'] is NOT covered by the system list ['https://managed.example.com/*'], so narrowWorkspaceHookSecurityOverrides deletes it before the merge and the assertion passes by merge order alone (system merged last, arrays replace). Mutant verified (ran): deleting the system.security?.allowedHttpHookUrls ?? arm from the coverage chain keeps every test green — the arm is structurally unobservable (whenever system defines the key, the last-wins merge overwrites whatever narrowing produced). No test in this diff pairs a system list with a workspace entry COVERED by it — the only shape where a narrowed workspace list survives to the merge and must then lose to system. — Concrete cost: the documented "System-scope whitelist always takes precedence over the workspace value" guarantee (schema description, warning text) has no load-bearing test; a regression letting a surviving narrowed workspace list outrank system for this key ships green in that untested scenario. — Suggested fix: change the workspace entry to one covered by the system list (e.g. workspace ['https://managed.example.com/ci/*'], user ['https://hooks.corp.com/*'], system ['https://managed.example.com/*']) and still expect ['https://managed.example.com/*'] — the narrowed list then survives to the merge and pins system precedence over it. Optionally drop the dead system ?? arm, or keep it and let this test justify it.

中文说明

[Suggestion] R13-13:本测试从未真正行使它标题所称的交互:workspace 条目 ['https://hooks.corp.com/ci/*'] 并不被 system 列表 ['https://managed.example.com/*'] 覆盖,因此 narrowWorkspaceHookSecurityOverrides 在合并前就把它删除,断言仅靠合并顺序(system 最后合并、数组整体替换)通过。已跑变异:删除覆盖链中的 system.security?.allowedHttpHookUrls ?? 分支,全部测试保持绿色——该分支在结构上不可观测(只要 system 定义了该键,last-wins 合并就会覆盖收窄产生的任何结果)。本 diff 中没有任何测试把 system 列表与一个被其覆盖的 workspace 条目同时设置——那是收窄后的 workspace 列表存活到合并、随后必须输给 system 的唯一形态。—— 具体代价:文档承诺的 "System-scope whitelist always takes precedence over the workspace value"(schema 描述、告警文案)没有任何承重测试;若某个回归让存活的收窄 workspace 列表在该键上压过 system,将在这一未测场景中以全绿状态合入。—— 修复建议:把 workspace 条目改为被 system 列表覆盖的条目(例如 workspace ['https://managed.example.com/ci/*']、user ['https://hooks.corp.com/*']、system ['https://managed.example.com/*']),仍期望 ['https://managed.example.com/*']——收窄列表便能存活到合并,从而钉住 system 对其的优先权。也可选删除已死的 system ?? 分支,或保留它并用本测试为其正名。

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

Comment on lines +277 to +279
// A redirect delivers no payload, so it cannot consume the one
// execution: both firings must fetch instead of skipping.
expect(mockFetch).toHaveBeenCalledTimes(2);

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] R13-14: The once-slot restoration is only pinned for the 302→302 case; the flow the restoration exists for — redirect resolves, the restored slot delivers the payload exactly once, then skips — has no test (no 302→200→third-call sequence anywhere in this file). Mutant verified (ran): gating the restore on the warn slot — if (hookConfig.once && !this.redirectWarnedHooks.has(warnKey)) this.executedOnceHooks.delete(onceKey) — passes all 31 current tests, but a 302→302→200→200 probe fails under it (the slot stays consumed after the second redirect; the payload is never delivered) and passes on the PR code. — Concrete cost: after any second redirect of the same URL the once slot stays consumed under that mutant, so once the URL starts returning 2xx (redirect fixed in place) the hook never fires for the rest of the session — the exact "silently no-op-ing forever" outcome the restore was added to prevent. The inverse regression (also deleting the slot after a post-restore success) re-delivers the once payload on every subsequent event, equally unseen by current tests. — Suggested fix: add a test — first call 302, second call 200 with a JSON payload (assert the payload is delivered, mockFetch called twice), third call must skip (no further fetch; the exhausted-slot output).

中文说明

[Suggestion] R13-14:once 槽位恢复目前只被 302→302 场景钉住;而恢复机制存在的意义——重定向解除后,被恢复的槽位恰好投递一次负载、随后跳过——没有任何测试覆盖(整个文件中不存在 302→200→第三次调用的序列)。已跑变异:把恢复用 warn 槽位门禁化——if (hookConfig.once && !this.redirectWarnedHooks.has(warnKey)) this.executedOnceHooks.delete(onceKey)——通过当前全部 31 个测试,但 302→302→200→200 探针在其下失败(第二次重定向后槽位仍被占用,负载永远不投递),在 PR 代码上则通过。—— 具体代价:在该变异下,同一 URL 经历任意第二次重定向后 once 槽位保持被占用,一旦该 URL 开始返回 2xx(重定向被原地修复),该 hook 在会话剩余时间内永不再触发——正是恢复机制要防止的"悄然永久 no-op"结局。反向回归(在恢复后的成功投递后也删除槽位)会让 once 负载在后续每个事件重复投递,当前测试同样看不见。—— 修复建议:补一个测试——第一次调用 302,第二次调用 200 且带 JSON 负载(断言负载被投递、mockFetch 被调用两次),第三次调用必须跳过(不再 fetch,输出为槽位耗尽路径的结果)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round — PR #8396 (Critical-only tail)

Two Critical findings from the automated reviewer, both describing the same
class of bypass of the home-root trust gate introduced by this PR. Both are
addressed by one coherent root-cause fix.

Feedback and dispositions

[Critical] R13-3 — worktree-isolation spawn bypasses the home-root trust gate (subagent-manager.ts) — FIXED

The spawn-time gate computed homeIsProjectRoot from
runtimeContext.getProjectRoot(). Worktree-isolation / working_dir spawn
paths (agent.ts, workflow-orchestrator.ts, InProcessBackend.ts) pass a
per-agent Config override whose getProjectRoot is rebound to the worktree
path, so in the untrusted home-root topology a 'user'-surfaced repo agent
spawned with isolation: 'worktree' flipped the detection and registered its
repo-supplied hooks session-wide.

[Critical] R13-9 — skill-side sibling entrance of the same bypass (skill.ts) — FIXED

The skill side-effect gate already read this.config.getProjectRoot() — but
inside a subagent this.config IS the per-agent override whose
getProjectRoot is rebound (rebuildToolRegistryOnOverride builds a fresh
SkillTool on the override while the shared SkillManager keeps the skill's
'user' level). As R13-9 notes, the R13-3-style fix alone cannot close this
path; the finding recommended propagating the managers' listing-time
home-root detection and using the same source of truth at all three gates.

The fix (single source of truth)

The home-root shadow is now decided once, at collection time, by the shared
managers — the only code that reads the session's real project root and the
same code that makes repo skills/agents surface at 'user' level when the
project root IS home:

  • SkillManager.listSkillsAtLevel tags each 'user'-level skill listed
    under that topology with homeRootShadow: true (new optional field on
    SkillConfig).
  • SubagentManager.listSubagentsAtLevel tags each 'user'-level agent the
    same way (new optional field on SubagentConfig, not serialized to
    frontmatter).

All three gates now consume the flag instead of re-deriving the shadow from a
per-agent-rebindable getter:

  • subagent-manager.ts spawn gate: config.homeRootShadow !== true replaces
    the runtimeContext.getProjectRoot() comparison.
  • skill.ts applySkillSideEffects: skill.homeRootShadow === true
    replaces the this.config.getProjectRoot() comparison.
  • SkillCommandLoader.ts slash-command gate: same flag consumption.

The flag travels with the skill/agent object, so it is immune to every
existing rebind site (agent.ts worktree isolation, InProcessBackend
working_dir pin, workflow-orchestrator worktree override) and to any
future one. The flag never weakens the gate: listing sets it exactly when the
shadow topology exists, and the gate remains fail-closed on levels.

Tests

  • New regression tests mirroring the reported bypass shape: a spawn context /
    subagent config whose getProjectRoot is rebound to a worktree path must
    keep hooks gated for a shadowed 'user'-level agent/skill (both verified
    to FAIL against the pre-fix gate via a temporary mutation).
  • New listing tests: homeRootShadow is set for 'user'-level
    skills/agents when the project root is the home directory and absent in a
    normal project.
  • The five existing home-root gate tests were updated to drive the gate via
    the flag (their former getProjectRoot mocks no longer reach the gate).

Conflict notes

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

Verification

Commands actually run this round (all on the final tree):

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0, all workspaces)
  • npm run lint — passed (exit 0, full repo)
  • npx eslint (10 touched files) — passed, no findings
  • npx prettier --check (10 touched files) — passed
  • vitest (packages/core, CI-equivalent env): subagent-manager.test.ts + skill.test.ts + skill-manager.test.ts — 328 passed
  • vitest (packages/core): skill-utils.test.ts + registerSkillHooks.test.ts — 18 passed
  • vitest (packages/cli): SkillCommandLoader.test.ts — 35 passed
  • Mutation checks: both new rebind regression tests fail against the pre-fix
    gates, pass against the fix

Note: in this particular runner QWEN_HOME is exported, which
Storage.getGlobalQwenDir() prefers over os.homedir(); that makes a set of
pre-existing user-level listing tests (7, verified failing on the unmodified
branch too) resolve paths against the real home dir. CI does not export
QWEN_HOME; with it unset all suites above are fully green.

中文说明

Autofix 评审轮次 — PR #8396(仅处理 Critical 阶段)

自动评审器提出两个 Critical 发现,描述的是同一类对本 PR 引入的 home-root
信任门禁的绕过。两者由一个一致的根因修复解决。

反馈与处置

[Critical] R13-3 — worktree 隔离派生绕过 home-root 信任门禁(subagent-manager.ts)— 已修复

派生时门禁原来从 runtimeContext.getProjectRoot() 计算
homeIsProjectRoot。worktree 隔离 / working_dir 派生路径(agent.ts
workflow-orchestrator.tsInProcessBackend.ts)传入的是把
getProjectRoot 重绑为 worktree 路径的 per-agent Config 覆盖,因此在
home 为项目根且文件夹不受信的拓扑下,用 isolation: 'worktree' 派生一个以
'user' 级出现的仓库 agent 会翻转判定,把仓库提供的 hooks 注册到整个会话。

[Critical] R13-9 — 同一绕过的 skill 侧同类入口(skill.ts)— 已修复

skill 侧副作用门禁用的是 this.config.getProjectRoot()——但在 subagent 内
this.config 正是那个 getProjectRoot 被重绑过的 per-agent 覆盖
rebuildToolRegistryOnOverride 在覆盖上构建新的 SkillTool,而共享的
SkillManager 保持 skill 的 'user' 级)。正如 R13-9 指出,仅靠 R13-3
式的修复无法关闭该路径;该发现建议传播 manager 在收集期的 home-root 判定,
并在三个门禁使用同一事实来源。

修复(单一事实来源)

home-root 影子判定现在只在收集期做一次,由共享 manager 完成——它们是唯一
读取会话真实项目根、也是决定 home 为项目根时仓库 skill/agent 以 'user'
级出现的代码:

  • SkillManager.listSkillsAtLevel 给该拓扑下列出的每个 'user' 级 skill
    打上 homeRootShadow: trueSkillConfig 新增可选字段)。
  • SubagentManager.listSubagentsAtLevel'user' 级 agent 做同样标记
    SubagentConfig 新增可选字段,不写入 frontmatter)。

三个门禁都改为消费该标志,而不是从可被 per-agent 重绑的 getter 重新推导:

  • subagent-manager.ts 派生门禁:以 config.homeRootShadow !== true 取代
    runtimeContext.getProjectRoot() 比较。
  • skill.tsapplySkillSideEffects:以 skill.homeRootShadow === true
    取代 this.config.getProjectRoot() 比较。
  • SkillCommandLoader.ts slash 命令门禁:同样消费该标志。

标志随 skill/agent 对象传播,因此对所有现有重绑点(agent.ts worktree
隔离、InProcessBackend 的 working_dir 固定、workflow-orchestrator
worktree 覆盖)以及未来新增的重绑都免疫。该标志不会削弱门禁:收集期只在
影子拓扑存在时设置它,门禁对 level 仍保持 fail-closed。

测试

  • 新增按所报绕过形态构造的回归测试:当派生上下文 / subagent config 的
    getProjectRoot 被重绑为 worktree 路径时,影子 'user' 级 agent/skill
    的 hooks 必须保持门禁拦截(两个测试均通过临时变异验证:对修复前的门禁
    会失败)。
  • 新增收集期测试:项目根为主目录时 'user' 级 skill/agent 被设置
    homeRootShadow;普通项目下不设置。
  • 原有 5 个 home-root 门禁测试改为通过标志驱动门禁(它们原先的
    getProjectRoot mock 不再被门禁读取)。

冲突说明

无(--conflict false),未做 merge。

验证

本轮在最终代码树上实际执行的命令:

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0,全部 workspace)
  • npm run lint — 通过(exit 0,全仓库)
  • npx eslint(10 个改动文件)— 通过,无发现
  • npx prettier --check(10 个改动文件)— 通过
  • vitest(packages/core,与 CI 等价的环境):subagent-manager.test.ts + skill.test.ts + skill-manager.test.ts — 328 通过
  • vitest(packages/core):skill-utils.test.ts + registerSkillHooks.test.ts — 18 通过
  • vitest(packages/cli):SkillCommandLoader.test.ts — 35 通过
  • 变异检查:两个新的重绑回归测试对修复前门禁失败、对修复后通过

注:本 runner 环境导出了 QWEN_HOME,而 Storage.getGlobalQwenDir() 优先
使用它而非 os.homedir(),导致一组本来就存在的 user 级列表测试(7 个,
已验证在未改动分支上同样失败)按真实主目录解析路径。CI 不导出
QWEN_HOME;取消该变量后上述全部测试套件为全绿。

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

🤖 AutoFix ran out of time before finishing (timeout (1080000ms)) (attempt 18/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: timeout (1080000ms).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31040664859


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

🤖 No code changes this round — the one failed check was diagnosed as an environment failure that happened before this PR's code was built or run.

Autofix address-review — PR #8396 (no action)

Critical-only mode is active; the deferred non-Critical section was left untouched (audit record only, per the round rules). No inline comments, reviews, or issue-level comments were actionable this round. No conflict resolution was requested (--conflict false); no merge was performed. Budget warning acknowledged: only the single blocking item below was investigated; nothing else was retried.

Failed check: Serve A/B (ubuntu-latest, Node 22.x) — FAILURE

Verdict: not caused by this PR's code. No fix to implement; the check needs a re-run.

Evidence:

  1. The job failed before any PR code could run. The failing run lasted 24 seconds total (startedAt 2026-08-05T19:45:20Z → completedAt 19:45:44Z, from checks.json). This workflow performs two checkouts, Node setup, then npm ci + npm run build + a daemon drive for BOTH the PR head and the merge-base — the previous complete run of this same workflow for this PR took long enough to post its comment. A 24-second wall time means the failure happened in the early infrastructure steps (runner workspace prep, checkout, merge-base resolution, Node setup, or an instant npm ci abort), not in the build, drive, or diff steps where this PR's code executes.
  2. The previous full run of this exact workflow PASSED one commit earlier. Head f36390e produced "✅ No response changes against the PR base across 4 scenario(s)" (comment updated 2026-08-05T12:45:35Z). The only commit pushed after that success is eedd9243e, which touches 10 TypeScript source files only (skill/subagent home-root trust gates) — no package.json, no lockfile, no workflow/script/build changes, nothing the early workflow steps consume.
  3. The full runnable surface of the check reproduces green from a clean install on the current head (eedd9243e). See the Verification section below.
  4. The instant-abort failure profile matches an environment defect, and I reproduced that class of failure locally. My first local npm ci attempt aborted within seconds with EACCES because the npm cache directory contained root-owned files (npm error code EACCES / mkdir /home/runner/.npm) — an environment permissions problem, not a code one. A cache/permissions fault on the shared runner aborts npm ci (or an earlier step) in exactly the observed 24-second window, while a code defect cannot: the lockfile is in sync (clean npm ci exits 0) and the build passes.

Recommendation: re-run the Serve A/B workflow. If it fails again in under a minute, inspect the runner's workspace-prep / checkout / setup-node / npm-cache steps for that run — the code paths of this PR are not reached in that window.

Verification

Commands actually run this round (on head eedd9243e), mirroring the workflow's head-side steps:

  • npm ci --prefer-offline --no-audit --progress=false (clean install, writable cache) — passed (exit 0; lockfile in sync). Note: a first attempt failed with EACCES on a root-owned ~/.npm cache dir — an environment fault of this container, cited above as evidence item 4.
  • npm run buildpassed (exit 0), run twice (before and after the clean install).
  • node .github/scripts/serve-ab-drive.mjs packages/cli/dist/index.js <out>passed (exit 0), all 4 scenarios captured (health 200, health-deep, capabilities 200, health-deep-with-session 200; POST /session setup 200), run twice.
  • node .github/scripts/serve-ab-diff.mjs comment ...passed (exit 0) in both the normal and baseline-missing modes.
  • No TypeScript/source files were modified this round; git status is clean and HEAD is unchanged.
中文说明

🤖 本轮未改动任何代码 —— 唯一失败的检查被诊断为环境问题:该任务在构建或运行本 PR 的代码之前就已失败。

Autofix address-review — PR #8396(无操作)

当前处于仅处理 Critical 的模式;被延后的非 Critical 部分未做任何改动(按本轮规则仅作为审计记录保留)。本轮没有可处理的行内评论、评审或议题级评论。未请求解决冲突(--conflict false),因此未执行任何合并。已知悉预算警告:本轮只调查了下方这一个阻塞项,未重试其他任何内容。

失败检查:Serve A/B (ubuntu-latest, Node 22.x) — FAILURE

结论:并非本 PR 代码所致。无需实施修复;该检查需要重新运行。

证据:

  1. 任务在运行到任何 PR 代码之前就已失败。 失败一次的运行总共只持续了 24 秒checks.jsonstartedAt 2026-08-05T19:45:20Z → completedAt 19:45:44Z)。该工作流需要执行两次 checkout、Node 安装,然后对 PR head 和 merge-base 各自执行 npm ci + npm run build + 驱动 daemon —— 本 PR 上一次完整运行该工作流耗时足以发出其评论。24 秒的总时长意味着失败发生在早期基础设施步骤(runner 工作区准备、checkout、merge-base 解析、Node 安装,或 npm ci 的瞬间中止),而不是在构建、驱动或 diff 这些会执行本 PR 代码的步骤。
  2. 同一工作流在上一个 commit 上的完整运行是通过的。 head f36390e 产出了 "✅ No response changes against the PR base across 4 scenario(s)"(评论更新于 2026-08-05T12:45:35Z)。该成功之后推送的唯一 commit 是 eedd9243e,它仅改动了 10 个 TypeScript 源文件(skill/subagent 的 home-root 信任门)—— 没有改 package.json、没有改 lockfile、没有改工作流/脚本/构建配置,没有任何早期工作流步骤会用到的内容。
  3. 该检查的全部可运行面在当前 head(eedd9243e)上从干净安装开始复现均为绿色。 见下方 Verification 部分。
  4. 瞬间中止的失败特征与环境缺陷相符,且我在本地复现了这一类失败。 我本地第一次 npm ci 尝试在数秒内以 EACCES 中止,原因是 npm 缓存目录中包含 root 拥有的文件(npm error code EACCES / mkdir /home/runner/.npm)—— 这是环境权限问题,不是代码问题。共享 runner 上的缓存/权限故障恰好会在观察到的 24 秒窗口内中止 npm ci(或更早的步骤),而代码缺陷做不到这一点:lockfile 是同步的(干净的 npm ci 以退出码 0 完成),构建也通过。

建议:重新运行 Serve A/B 工作流。如果它再次在一分钟内失败,请检查该次运行的 runner 工作区准备 / checkout / setup-node / npm 缓存步骤 —— 在该时间窗口内本 PR 的代码路径尚未被执行到。

验证

本轮实际运行的命令(在 head eedd9243e 上),与工作流 head 侧步骤一致:

  • npm ci --prefer-offline --no-audit --progress=false(干净安装,使用可写缓存)—— 通过(退出码 0;lockfile 同步)。注:第一次尝试因 ~/.npm 缓存目录为 root 所有而以 EACCES 失败 —— 这是本容器的环境故障,已作为上方证据第 4 条引用。
  • npm run build —— 通过(退出码 0),运行了两次(干净安装前后各一次)。
  • node .github/scripts/serve-ab-drive.mjs packages/cli/dist/index.js <out> —— 通过(退出码 0),4 个场景全部捕获成功(health 200、health-deepcapabilities 200、health-deep-with-session 200;POST /session 准备请求 200),运行了两次。
  • node .github/scripts/serve-ab-diff.mjs comment ... —— 通过(退出码 0),正常模式与 baseline 缺失模式均通过。
  • 本轮未修改任何 TypeScript/源文件;git status 干净,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

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows.

Not reviewed: reverse audit — stopped before round 5 by the review time budget.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows。

未审查:反向审计——评审时间预算不足,未能开始第 5 轮。

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

Comment on lines +251 to +253
// Never follow redirects: the whitelist and DNS-level SSRF
// checks above cover only this URL, and a 307/308 would re-send
// the hook payload to an unvalidated target. A 3xx response

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.

[Critical] R14-1: The whitelist static check regex-tests the RAW hook URL string, but fetch() sends the WHATWG-normalized URL — URL normalization (dot-segment removal) moves the hook payload to a path on a user-whitelisted host that the whitelist does not cover. Same static-check/runtime-consumer divergence class this PR closes for redirects, left open for normalization. — Failure scenario: user-scope whitelist ["https://corp.example.com/api/*"]; a trusted repo configures hook url https://corp.example.com/api/../telemetry/exfilvalidate() tests the raw string → allowed; fetch normalizes and POSTs the hook payload (prompts, tool inputs/outputs) to https://corp.example.com/telemetry/exfil, which the whitelist regex does NOT match. Probe-verified end-to-end with a real fetch against a live server; the fix below flips the probe and all 72 hook tests stay green. The userinfo cross-host variant fails closed at fetch (undici rejects credentials in Request construction), so confirmed harm is bounded to normalization-without-credentials destinations — but every path-shaped pattern (…/prefix/*, all documented examples) is bypassable.

// In HttpHookRunner.execute (or UrlValidator.validate): parse once and
// validate the exact string that will be fetched:
const parsed = new URL(url); // invalid URL already fails closed in isBlocked
// run isBlocked + isAllowed against parsed.href, then fetch(parsed.href, ...)
中文说明

白名单静态检查对原始 hook URL 字符串做正则匹配,但 fetch() 发送的是 WHATWG 规范化后的 URL——URL 规范化(点段移除)会把 hook 负载投递到用户白名单主机上一个白名单并未覆盖的路径。这与本 PR 为重定向关闭的"静态检查/运行时消费者分歧"是同一类问题,但在规范化方向上仍然敞开。——失败场景:user 白名单 ["https://corp.example.com/api/*"],受信仓库配置 hook url https://corp.example.com/api/../telemetry/exfilvalidate() 对原始字符串判定放行;fetch 规范化后把 hook 负载(prompt、工具输入/输出)POST 到白名单正则并不匹配的 https://corp.example.com/telemetry/exfil。已用真实 fetch 对活服务器端到端探针验证;下方修复可翻转探针且全部 72 个 hook 测试保持绿色。userinfo 跨主机变体在 fetch 处 fail closed(undici 拒绝构造含凭据的 Request),因此已确认的危害限于不含凭据的规范化目的地——但所有路径形态的模式(…/prefix/*,即全部文档示例)都可被绕过。

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

Comment on lines +684 to +686
// Extensions load only from the user-scope extensions directory, so
// they are in the trusted allowlist and must not regress behind the
// fail-closed gate.

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.

[Critical] R14-2: The new gate's 'extension'-level exemption violates its own fail-closed invariant in the home-root topology. When the project root IS $HOME, ~/.qwen/extensions == <repo>/.qwen/extensions is repo-controlled; unknown extensions are active by default (ExtensionStore.getActivation returns effective 'enabled' with no store policy — no install/consent/state needed), and their skills surface at level 'extension', where isTrustedSkillLevel skips the gate unconditionally — repo-supplied frontmatter allowedTools become session-wide auto-approvals and frontmatter hooks register, despite the untrusted folder. This is the exact topology this PR hardens at 'user' level via homeRootShadow — the tag is applied only when level === 'user', so the identical topology is hardened there and left open here (extensions additionally carry MCP servers, commands, and hooks). The subagent gate's 'extension' arm is the same surface (extensions supply agents). Scope note: pre-PR, allowedTools were granted unconditionally for all skills, so this vector is not newly reachable — the Critical stands on the gate's own stated premise ("only levels that cannot originate from the repository skip the folder-trust gate") being falsified by a topology the gate explicitly models elsewhere. — Failure scenario: victim runs qwen with project root == $HOME, folder untrusted; repo commits .qwen/extensions/evil/ with skills/s/SKILL.md declaring allowedTools: ['Bash(*)'] → chain verified at HEAD: auto-active → level 'extension' → gate skipped → session-wide auto-approval of attacker-chosen tool patterns.

Suggested fix: extend the home-root tag to extension-level skills (inside the extension branch of listSkillsAtLevel, which returns before the current tagging block — if ((level === 'user' || level === 'extension') && isHomeDirectory)), broaden the gate disjunct to level-independent skill.homeRootShadow === true in skill.ts, SkillCommandLoader.ts, and the subagent gate; condition this test on not-home-root-shadowed and add a gated counterpart.

中文说明

新门禁对 'extension' 级的豁免在 home-root 拓扑下违反了其自身的 fail-closed 不变量。当项目根就是 $HOME 时,~/.qwen/extensions == <repo>/.qwen/extensions 是仓库可控的;未知扩展默认处于激活状态(无 store 策略时 ExtensionStore.getActivation 返回 effective 'enabled'——无需安装/同意/状态文件),其 skill 以 'extension' 级出现,而 isTrustedSkillLevel 对该级无条件跳过门禁——仓库提供的 frontmatter allowedTools 成为整个会话的自动批准、frontmatter hooks 被注册,全程没有文件夹信任。这正是本 PR 在 'user' 级用 homeRootShadow 加固的拓扑——标记仅在 level === 'user' 时应用,因此同一拓扑在 'user' 级被加固、在这里却敞开(扩展还额外携带 MCP server、命令与 hooks)。subagent 门禁的 'extension' 分支是同一表面(扩展可提供 agent)。范围说明:PR 之前 allowedTools 对所有 skill 无条件授予,因此该向量并非新近可达——Critical 成立的依据是门禁自身声明的前提("只有不可能来源于仓库的级别才跳过文件夹信任门禁")被门禁在别处显式建模的拓扑所证伪。——失败场景:受害者以 $HOME 为项目根运行 qwen、文件夹不受信;仓库提交带 skills/s/SKILL.md(声明 allowedTools: ['Bash(*)'])的 .qwen/extensions/evil/ → 已在 HEAD 逐步核实:自动激活 → 'extension' 级 → 门禁跳过 → 攻击者选定的工具模式获得会话级自动批准。

修复建议:把 home-root 标记扩展到 extension 级 skill(在 listSkillsAtLevel 的 extension 分支内——该分支在当前标记块之前就返回——if ((level === 'user' || level === 'extension') && isHomeDirectory)),并把门禁析取项放宽为与级别无关的 skill.homeRootShadow === true(skill.ts、SkillCommandLoader.ts 与 subagent 门禁同步);将本测试限定为非 home-root-shadow 场景并补充受门禁的对照测试。

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

Comment on lines +249 to +252
const chunks = outer.split('*');
if (chunks.length === 1) {
return inner === outer;
}

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] R14-3: The no-wildcard (exact-URL) branch of hookUrlPatternCovers has zero test coverage — all 20 invocations in urlValidator.test.ts use an outer pattern containing *. — Failure scenario: exact URLs are valid allowedHttpHookUrls entries; if a future edit relaxes this branch to prefix/containment comparison (e.g. inner.startsWith(outer)), hookUrlPatternCovers('https://corp.com/ci', 'https://corp.com/ci-evil') flips to true, the workspace entry survives narrowing, replaces the user's list in the merge, and hook payloads POST to a destination the user's whitelist does not cover — suite-green.

Suggested fix: add cases such as expect(hookUrlPatternCovers('https://corp.com/ci', 'https://corp.com/ci')).toBe(true), expect(hookUrlPatternCovers('https://corp.com/ci', 'https://corp.com/ci-evil')).toBe(false), plus a wildcard inner vs exact outer (must fail closed).

中文说明

hookUrlPatternCovers 的无通配符(精确 URL)分支零测试覆盖——urlValidator.test.ts 中全部 20 处调用的 outer 模式都含 *。——失败场景:精确 URL 是合法的 allowedHttpHookUrls 条目;若未来把该分支放宽为前缀/包含比较(如 inner.startsWith(outer)),hookUrlPatternCovers('https://corp.com/ci', 'https://corp.com/ci-evil') 会翻转为 true,workspace 条目在收窄中存活并替换用户列表,hook 负载被 POST 到用户白名单未覆盖的目的地——且测试全绿。修复建议:补充上述用例(精确相等为 true、前缀污染为 false、通配 inner 对精确 outer 必须 fail closed)。

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

Comment on lines +269 to +271
if (hookConfig.once) {
this.executedOnceHooks.delete(onceKey);
}

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] R14-4: No test pins that a once hook failing with a non-3xx status (4xx/5xx) still consumes its single execution slot — the 3xx exemption is tested, but the complementary behavior is not (once: true appears only in 200/302 tests; the 500 test uses a non-once hook). — Failure scenario: if a future refactor hoists this.executedOnceHooks.delete(onceKey) out of the 3xx branch to the top of the general failure path (plausible, since 4xx/5xx also deliver no payload), a once hook behind a 4xx/5xx endpoint would re-fire on every subsequent event instead of running once, re-sending its payload indefinitely — suite-green.

Suggested fix: add a test: once: true hook, mock a 500 response, execute twice for the same event, assert mockFetch was called exactly once (slot consumed on non-3xx failure).

中文说明

没有测试钉住:once hook 以非 3xx 状态(4xx/5xx)失败时仍消耗其唯一一次执行额度——3xx 豁免有测试,但其互补行为没有(once: true 只出现在 200/302 测试中;500 测试用的是非 once hook)。——失败场景:若未来重构把 this.executedOnceHooks.delete(onceKey) 从 3xx 分支提升到通用失败路径顶部(4xx/5xx 同样未投递负载,这种改动很自然),位于 4xx/5xx 端点后的 once hook 会在后续每个事件重复触发、无限重发负载——且测试全绿。修复建议:新增测试——once: true hook、mock 500 响应、同一事件执行两次、断言 mockFetch 恰被调用一次。

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

if (specifier === '@qwen-code/qwen-code-core') {
return { shortCircuit: true, url: '${coreSrcUrl}', format: 'module' };
}
if (specifier === '@qwen-code/qwen-code-core/envVarResolver') {

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] R13-2 (still standing from round 13; re-detected independently this round by the build-test efficacy probe, harness validated): the envVarResolver subpath source-remap added to this file is unreachable by every test command in the project — the file sits outside every npm workspace. — Concrete cost: if the remap specifier or mapped path is wrong (or drifts from core package.json's ./envVarResolver subpath or from scripts/dev.js), an after-mode terminal capture of the SkillReviewDialog flow fails at import time on a fresh checkout, and nothing automated catches the break.

Suggested fix: confirm the terminal-capture harness run for this PR actually executed this path in CI, or exercise the loader remap in a collected test.

中文说明

R13-2(第 13 轮遗留,本轮仍然成立;本轮 build-test 有效性探针独立再次发现,harness 已验证):本文件新增的 envVarResolver 子路径源码重映射对项目内任何测试命令都不可达——该文件位于所有 npm workspace 之外。——具体代价:若重映射的说明符或目标路径错误(或与 core package.json 的 ./envVarResolver 子路径、scripts/dev.js 发生漂移),新检出上的 after 模式 SkillReviewDialog 终端采集会在 import 时失败,且没有任何自动化手段能捕获该破坏。修复建议:确认本 PR 的 terminal-capture harness 运行在 CI 中确实执行了该路径,或在一个被收集的测试中执行该 loader 重映射。

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

Comment on lines +314 to +315
// The remedy rides a systemMessage. How it surfaces is
// event-dependent: Stop/SubagentStop show it to the user; other

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] R14-9: This pinned surfacing contract — "Stop/SubagentStop show it to the user" (repeated near line 462) — is false for SubagentStop: no consumer ever displays a SubagentStop hook's systemMessage. Verified against every SubagentStop consumer at HEAD: runSubagentStopHookLoop (agent.ts) and background-agent-resume.ts read only isBlockingDecision()/shouldStopExecution() and the cap warning; client.ts emits HookSystemMessage only in the Stop path; hookEventHandler.processCommonHookOutputFields only debug-logs it. — Failure scenario: a SubagentStop HTTP hook behind a redirecting LB — the first 3xx burns the one-shot redirectWarnedHooks slot for <url>:SubagentStop and produces a systemMessage no UI ever renders; the user gets no warning and no remedy, ever, while these comments tell maintainers SubagentStop is a user-visible surface (the stated reason the per-event slot keeps SubagentStop separate from PreToolUse). Both the remedy and its surfacing gap are new with this diff (pre-PR redirects were followed, so no warning existed).

Suggested fix: surface the SubagentStop systemMessage where the loop consumes the hook output (mirroring client.ts's Stop path), or correct both comments to say only Stop is user-visible and SubagentStop is debug-only like PreToolUse.

中文说明

R14-9:此处钉住的呈现契约——"Stop/SubagentStop show it to the user"(约第 462 行处重复出现)——对 SubagentStop 不成立:没有任何消费者会展示 SubagentStop hook 的 systemMessage。已在 HEAD 核实全部 SubagentStop 消费者:runSubagentStopHookLoop(agent.ts)与 background-agent-resume.ts 只读取 isBlockingDecision()/shouldStopExecution() 与上限告警;client.ts 仅在 Stop 路径发出 HookSystemMessagehookEventHandler 只写 debug 日志。——失败场景:位于重定向负载均衡后的 SubagentStop HTTP hook——首个 3xx 会烧掉 <url>:SubagentStop 的一次性 redirectWarnedHooks 槽位并产生一条没有任何 UI 会呈现的 systemMessage;用户永远得不到告警与补救,而这些注释却告诉维护者 SubagentStop 是用户可见表面(也正是 per-event 槽位把 SubagentStop 与 PreToolUse 分开的理由)。补救本身及其呈现缺口都是本 diff 新增(PR 前重定向会被跟随,因此根本不存在该告警)。修复建议:在 hook 输出的消费处(loop 内)呈现 SubagentStop 的 systemMessage(镜像 client.ts 的 Stop 路径),或修正两处注释——只有 Stop 用户可见,SubagentStop 与 PreToolUse 一样仅 debug 可见。

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

// runtime language past the literal reading used here. A bare `.` is
// regex-active in that branch too, but only after unescaping: the `\.`
// sequences it came from are literal dots, so strip them before checking.
const regexActive = /[+?^${}()|[\]\\]/;

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] R13-4 (still standing from round 13): hookUrlPatternCovers fail-closes on chars ≥ U+0080 but lets ASCII line terminators \n (0x0A) and \r (0x0D) through, which diverge from the runtime regex semantics (no s flag; $ matches before a trailing newline; . cannot cross terminators) — the same checker/runtime divergence family the PR's other guards close, left open for the ASCII control characters. Settings patterns embedding terminators are rare but possible in hand-edited JSON, and the comparison treats them as ordinary literal text. Fail-closed overall, hence Suggestion.

Suggested change
const regexActive = /[+?^${}()|[\]\\]/;
const regexActive = /[+?^${}()|[\]\\\n\r]/;
中文说明

R13-4(第 13 轮遗留,本轮仍然成立):hookUrlPatternCovers 对 ≥ U+0080 的字符 fail closed,却放行 ASCII 行终止符 \n(0x0A)与 \r(0x0D),而它们与运行时正则语义存在分歧(无 s 标志;$ 可在结尾换行前匹配;. 不能跨越终止符)——与 PR 其他守卫所关闭的"检查器/运行时分歧"同族,唯独对 ASCII 控制字符敞开。settings 模式中嵌入终止符虽罕见但手工编辑 JSON 时可能出现,而比较逻辑把它们当普通字面文本处理。总体 fail-closed,故为 Suggestion。上方 suggestion 将 \n/\r 并入 fail-closed 字符类。

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

}),
).rejects.toThrow(`${SECRET} is a Qwen-internal secret`);

// secretEnv used to return the variable *name* as the secret — a

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] R13-5 (still standing from round 13): this rationale comment misdescribes the pre-fix behavior. Verified against the merge base: resolveWebhookSecretEnv resolved the variable's VALUE (const envValue = env[envName]; … return envValue;) and threw when unset — it never returned the name. — Concrete cost: a maintainer reading this security test gets an inverted history of what the old code did; the comment is the only rationale documentation for the secretEnv denylist.

Suggested fix: reword to describe the actual pre-fix behavior (resolved the variable's value into channel config).

中文说明

R13-5(第 13 轮遗留,本轮仍然成立):该原理性注释对修复前行为的描述有误。已对合并基核实:resolveWebhookSecretEnv 解析的是变量的值(const envValue = env[envName]; … return envValue;),未设置时抛错——它从不返回变量名。——具体代价:维护者阅读该安全测试时会得到关于旧代码行为的颠倒历史;而该注释是 secretEnv 拒绝列表唯一的原理说明。修复建议:改写为真实的修复前行为(把变量的值解析进 channel 配置)。

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

});

it('should strip security.allowPrivateNetworkHooks from workspace scope even when trusted', () => {
it('should strip security.allowPrivateNetworkHooks and security.allowedHttpHookUrls from workspace scope even when trusted', () => {

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] R13-6 (still standing from round 13): this test title makes a blanket strip claim for security.allowedHttpHookUrls while the same describe block adds narrowing tests — the final behavior is narrow-with-fallback, not strip. — Concrete cost: a future maintainer reading the title concludes workspace allowedHttpHookUrls is always stripped and reasons incorrectly about the merge (e.g. debugging why a trusted workspace's covered entry survived); the title contradicts its own sibling tests.

Suggested change
it('should strip security.allowPrivateNetworkHooks and security.allowedHttpHookUrls from workspace scope even when trusted', () => {
it('should strip security.allowPrivateNetworkHooks and narrow security.allowedHttpHookUrls against higher scopes in workspace scope even when trusted', () => {
中文说明

R13-6(第 13 轮遗留,本轮仍然成立):该测试标题对 security.allowedHttpHookUrls 做了"一律剥离"的断言,而同一 describe 块内新增了收窄(narrowing)测试——最终行为是"收窄并在无存活时回退",不是剥离。——具体代价:未来维护者读标题会以为 workspace 的 allowedHttpHookUrls 总是被剥离,从而对合并行为做出错误推理(例如排查为何受信 workspace 的被覆盖条目存活了下来);标题与其姊妹测试自相矛盾。上方 suggestion 给出与真实语义一致的标题。

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

Comment on lines +277 to +278
// A redirect delivers no payload, so it cannot consume the one
// execution: both firings must fetch instead of skipping.

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] R13-14 (still standing from round 13): the once-slot restoration is only pinned for the 302→302 case; the flow the restoration exists for — redirect resolves, the restored slot delivers the payload exactly once, then skips — has no test (no 302→200→third-call). — Failure scenario: if the restoration delete is moved or conditioned differently (e.g. removed under the theory that non-2xx should consume), the delivery-then-skip half of the contract is untested, so a regression there ships green while once-hooks either double-deliver or never deliver after a transient redirect.

Suggested fix: add a test: once hook, first execute → 302 (no consumption), mock 200, second execute → payload delivered, third execute → skipped (mockFetch called twice total).

中文说明

R13-14(第 13 轮遗留,本轮仍然成立):once 槽位恢复只被钉住了 302→302 的情形;恢复机制真正服务的流程——重定向解除后,被恢复的槽位恰好投递一次负载、随后跳过——没有测试(没有 302→200→第三次调用)。——失败场景:若恢复用的 delete 被移动或附加条件(例如以"非 2xx 也应消耗"为由删除),契约中"投递一次后跳过"的一半没有测试保护,相关回归会在测试全绿的情况下发布,once-hook 在瞬时重定向后要么重复投递、要么永不投递。修复建议:新增测试——once hook,第一次执行 → 302(不消耗),mock 200,第二次执行 → 负载投递,第三次执行 → 跳过(mockFetch 共被调用两次)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is 3 full agent runs that pushed nothing. A human should split or reduce the PR (or raise the agent time budget AND its step backstop together), then comment @qwen-code /retry to re-arm. Until then future scans will skip this PR.

What I found before stopping:
Qwen failed during address-review: timeout (7200000ms).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31060843430


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

⏸️ Takeover paused: this PR reached its round cap (100/100). Comment @qwen-code /takeover to re-arm a fresh window and continue management, or @qwen-code /takeover stop to release.

中文说明

⏸️ 托管已暂停:本 PR 达到轮次上限(100/100)。评论 @qwen-code /takeover 可重新武装、开启新窗口继续托管;或评论 @qwen-code /takeover stop 释放。

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows.

Test Plan (not a blocker): 1385 tests green — this review observed 19232, 475, 1067, 1455, 2825, 444 passed.

[Critical] R14-1 (prior-round thread, comment 3725050089 at packages/core/src/hooks/httpHookRunner.ts:253) re-checked at dce013c and STILL STANDS: the whitelist check regex-tests the raw interpolated URL string while fetch() sends the WHATWG-normalized form. Probe at this commit: new UrlValidator(['https://corp.example.com/api/*']).validate('https://corp.example.com/api/../telemetry/exfil') returns allowed:true, while the normalized URL (https://corp.example.com/telemetry/exfil) is NOT covered by the whitelist — dot-segment removal delivers the hook payload to a path the whitelist does not cover. The suggested fix (parse once and validate/fetch parsed.href) is not applied.

[Critical] R14-2 (prior-round thread, comment 3725050095 at packages/core/src/tools/skill.test.ts:686) re-checked at dce013c and STILL STANDS: at the home-root topology (~/.qwen == /.qwen), repo-committed extensions load as active by default (ExtensionStore.getActivation returns effective 'enabled' with no store policy) and their skills/agents surface at 'extension' level, which both trust gates exempt; homeRootShadow only tags the 'user' level, so the fail-closed invariant the gates state ('only levels that cannot originate from the repository skip the gate') is violated for this topology.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows。

Test Plan(非阻断):1385 tests green — this review observed 19232, 475, 1067, 1455, 2825, 444 passed

[Critical] R14-1 (prior-round thread, comment 3725050089 at packages/core/src/hooks/httpHookRunner.ts:253) re-checked at dce013c and STILL STANDS: the whitelist check regex-tests the raw interpolated URL string while fetch() sends the WHATWG-normalized form. Probe at this commit: new UrlValidator(['https://corp.example.com/api/*']).validate('https://corp.example.com/api/../telemetry/exfil') returns allowed:true, while the normalized URL (https://corp.example.com/telemetry/exfil) is NOT covered by the whitelist — dot-segment removal delivers the hook payload to a path the whitelist does not cover. The suggested fix (parse once and validate/fetch parsed.href) is not applied.

[Critical] R14-2 (prior-round thread, comment 3725050095 at packages/core/src/tools/skill.test.ts:686) re-checked at dce013c and STILL STANDS: at the home-root topology (~/.qwen == /.qwen), repo-committed extensions load as active by default (ExtensionStore.getActivation returns effective 'enabled' with no store policy) and their skills/agents surface at 'extension' level, which both trust gates exempt; homeRootShadow only tags the 'user' level, so the fail-closed invariant the gates state ('only levels that cannot originate from the repository skip the gate') is violated for this topology.

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

* check fails closed.
*/
export function isTrustedSkillLevel(level: SkillLevel | undefined): boolean {
return level === 'user' || level === 'bundled' || level === 'extension';

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.

[Critical] R15-6: The gates exempt 'user'-level skills on the premise that they live in ~/.qwen, but two supported configurations put repository-controlled directories into the 'user' level: (A) settings.skills.directories — including RELATIVE entries, which the code itself resolves against the working directory (skill-manager.ts:930-943); (B) QWEN_HOME resolving inside the project root (relative QWEN_HOME is documented to resolve from cwd), which makes <repo>/<QWEN_HOME>/skills the user-level skills dir. homeRootShadow is only tagged when projectRoot === homedir, so repo-shipped skills surface at 'user' level untagged and skip both gates (skill.ts applySkillSideEffects and SkillCommandLoader.ts:164-168) — repo-authored skill hooks are registered and repo-authored allowedTools granted as session-wide auto-approvals in an untrusted folder.

Failure scenario: probe-verified end-to-end at this commit — with customSkillDirs: ['./skills'], an untrusted folder, and a repo-shipped skills/evil/SKILL.md declaring allowedTools: ['Bash(git push *)'] plus a PreToolUse command hook: listing returned level 'user' with no homeRootShadow; both gates opened (hooks registered, allow rule granted), while the identical content at 'project' level was gated (0 calls). Extending the tag to dirs inside getProjectRoot() flipped both arms. Vector B verified the same way with QWEN_HOME=<repo>/.qhome. The repo cannot inject skills.directories itself (workspace settings are dropped while untrusted) — the indirection needs user/system-scoped config or a victim-side QWEN_HOME; that narrows the exposed population, not the mechanism.

Suggested fix: tag by provenance — in listSkillsAtLevel('user'), set the shadow flag for any skill whose resolved base dir lies inside getProjectRoot() (covers both custom-dir entries and a repo-interior QWEN_HOME); mirror in listSubagentsAtLevel (see the subagent-manager thread). Cross-links: R15-9 (subagent side, same root cause), R15-5 (the over-blocking direction of the same predicate — do not fix by blanket-removing the 'user' exemption).

中文说明

[Critical] 门禁对 'user' 级 skill 的豁免基于“它们位于 ~/.qwen”这一前提,但有两种受支持的配置会把仓库可控目录放进 'user' 级:(A) settings.skills.directories——包括相对路径条目,代码本身会按工作目录解析(skill-manager.ts:930-943);(B) QWEN_HOME 解析到项目根目录内部(文档明确相对 QWEN_HOME 从 cwd 解析),使 <repo>/<QWEN_HOME>/skills 成为 user 级 skill 目录。homeRootShadow 仅在 projectRoot === homedir 时打标,因此仓库自带的 skill 会以无标记的 'user' 级身份出现,绕过两处门禁(skill.tsapplySkillSideEffectsSkillCommandLoader.ts:164-168)——在不可信文件夹中注册仓库编写的 skill hooks,并把仓库编写的 allowedTools 授予为全会话自动批准。

失败场景:已在本 commit 端到端探针验证——customSkillDirs: ['./skills']、不可信文件夹、仓库自带 skills/evil/SKILL.md(声明 allowedTools: ['Bash(git push *)'] 与 PreToolUse 命令 hook):列表返回 level 'user' 且无 homeRootShadow;两处门禁均打开(hooks 已注册、allow 规则已授予),而同样内容在 'project' 级被拦截(0 次调用)。把打标扩展到 getProjectRoot() 内部目录后两个分支均翻转。向量 B 用 QWEN_HOME=<repo>/.qhome 同样验证。仓库自身无法注入 skills.directories(不可信时 workspace 设置被整体丢弃)——该间接路径需要 user/system 作用域配置或受害端的 QWEN_HOME;这只缩小暴露人群,不改变机制本身。

修复建议:按来源打标——在 listSkillsAtLevel('user') 中,对解析后基础目录位于 getProjectRoot() 内部的 skill 设置 shadow 标记(同时覆盖自定义目录条目与仓库内部的 QWEN_HOME);并在 listSubagentsAtLevel 做镜像处理(见 subagent-manager 的评论)。交叉引用:R15-9(subagent 侧,同一根因)、R15-5(同一判定条件的过度拦截方向——修复时不要整体移除 'user' 豁免)。

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

// rebinds to the worktree path, opening the gate for exactly the
// repo-supplied agents the shadow surfaced.
const trustedAgentLevel =
(config.level === 'user' && config.homeRootShadow !== true) ||

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.

[Critical] R15-9: The 'user'-level exemption assumes the user agents directory is ~/.qwen, but Storage.getGlobalQwenDir() honors QWEN_HOME (storage.ts:183-193; relative values resolve against cwd — documented in settings.md), and listSubagentsAtLevel('user') reads getGlobalQwenDir()/agents. When QWEN_HOME resolves inside the project root (e.g. a direnv/devcontainer convention QWEN_HOME="$PWD/.qwen-home"), repo-committed agent files ARE the 'user' level; projectRoot !== homedir, so no homeRootShadow tag is applied, this gate never calls isTrustedFolder(), and HookRegistry.addAgentHooks (which has no gate of its own) registers the repo-supplied hooks session-wide.

Failure scenario: probe-verified at this commit — QWEN_HOME=/test/project/.qwen-home, repo shipping only .qwen-home/agents/helper.md with a PreToolUse command hook, folder untrusted: listing returned level=user, homeRootShadow=undefined; addAgentHooks was called once with isTrustedFolder()=false; the project-level control arm was gated (0 calls). Exempting 'user' only when its file is outside getProjectRoot() flipped the observation. Precondition: the victim's QWEN_HOME must resolve inside the repo (monorepo tooling, CI checkouts, direnv layouts) — the repo cannot set QWEN_HOME itself; that affects trigger likelihood, not the mechanism.

Suggested fix: at listing time, also tag 'user'-level agents whose resolved directory lies inside the project root (or gate any 'user' agent whose filePath resolves under getProjectRoot()). This is the under-blocking twin of R15-5 and the subagent-side mirror of R15-6 — all three share one root cause (level label trusted instead of directory provenance) and the fixes should land together.

中文说明

[Critical] 'user' 级豁免假定 user agents 目录是 ~/.qwen,但 Storage.getGlobalQwenDir() 会尊重 QWEN_HOME(storage.ts:183-193;相对值按 cwd 解析——settings.md 有文档说明),而 listSubagentsAtLevel('user') 读取 getGlobalQwenDir()/agents。当 QWEN_HOME 解析到项目根目录内部(例如 direnv/devcontainer 约定 QWEN_HOME="$PWD/.qwen-home")时,仓库提交的 agent 文件就是 'user' 级内容;projectRoot !== homedir,因此不会打 homeRootShadow 标记,此门禁根本不会调用 isTrustedFolder(),而 HookRegistry.addAgentHooks(自身没有门禁)会把仓库提供的 hooks 注册为全会话生效。

失败场景:已在本 commit 探针验证——QWEN_HOME=/test/project/.qwen-home,仓库仅携带 .qwen-home/agents/helper.md(含 PreToolUse 命令 hook),文件夹不可信:列表返回 level=user, homeRootShadow=undefinedaddAgentHooksisTrustedFolder()=false 时被调用一次;project 级对照分支被拦截(0 次调用)。把 'user' 豁免限定为文件位于 getProjectRoot() 之外后观测翻转。前提条件:受害者的 QWEN_HOME 需解析到仓库内部(monorepo 工具链、CI checkout、direnv 布局)——仓库自身无法设置 QWEN_HOME;这只影响触发概率,不改变机制。

修复建议:在列表阶段,对解析目录位于项目根内部的 'user' 级 agent 同样打标(或对 filePath 解析到 getProjectRoot() 之下的任何 'user' agent 执行门禁)。这是 R15-5 的欠拦截孪生问题、R15-6 的 subagent 侧镜像——三者同根因(信任 level 标签而非目录来源),修复应一起落地。

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

Comment on lines +55 to +56
it('should never resolve Qwen-internal secrets from process.env', () => {
process.env['QWEN_SERVER_TOKEN'] = 'daemon-secret';

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] R15-1: The PR body's "How to verify" instructs reviewers to run cd packages/cli && npx vitest run src/utils/envVarResolver.test.ts, but this diff DELETES that file (and the module it tested) — the denylist coverage now lives here in the core package, and the new channel-config denylist tests (packages/cli/src/commands/channel/config-utils.test.ts, "internal-secret denylist" describe) are not listed in the plan at all. Probe: the documented CLI command exits 1 with "No test files found".

Concrete cost: a reviewer verifying claim 3 follows the PR's own instructions and the CLI-side half of the denylist verification fails to execute, while the channel-credential path (the one that used to resolve $QWEN_SERVER_TOKEN) has no entry point in the test plan.

Suggested fix: update the PR body — drop the deleted CLI command (the core-side command already listed covers this file) and add cd packages/cli && npx vitest run src/commands/channel/config-utils.test.ts.

中文说明

[Suggestion] PR 正文的 "How to verify" 要求评审者运行 cd packages/cli && npx vitest run src/utils/envVarResolver.test.ts,但本 diff 删除了该文件(及其测试的模块)——denylist 覆盖现在位于 core 包的这个文件中,而新增的 channel 配置 denylist 测试(packages/cli/src/commands/channel/config-utils.test.ts 的 "internal-secret denylist" describe)完全没有列入验证计划。探针:按文档运行该 CLI 命令会以 "No test files found" 退出码 1 结束。

具体代价:评审者按 PR 自身的说明验证第 3 项时,CLI 侧的 denylist 验证无法执行,而 channel 凭据路径(过去会解析 $QWEN_SERVER_TOKEN 的那条)在测试计划中没有入口。

修复建议:更新 PR 正文——删除已删除文件的 CLI 命令(已列出的 core 侧命令覆盖本文件),并补充 cd packages/cli && npx vitest run src/commands/channel/config-utils.test.ts

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

- This setting is **only honored from User, System, and SystemDefaults settings scopes**. A value set in Workspace (project) settings is ignored and logged as a warning, so a cloned repository can never self-grant this bypass.
- The flag relaxes only the general private/CGNAT/link-local **range** checks. Cloud metadata endpoints stay blocked in every configuration: the `BLOCKED_HOSTS` list is matched literally (`metadata.google.internal`, `metadata.azure.internal`, ...), and the metadata IPs `169.254.169.254` and `100.100.100.200` are blocked in all serialized forms (including IPv4-mapped IPv6 such as `::ffff:a9fe:a9fe`) and after DNS resolution.
- The `security.allowedHttpHookUrls` whitelist still applies independently. In managed environments, pair this flag with a whitelist so only the intended internal endpoints are reachable.
- The `security.allowedHttpHookUrls` whitelist still applies independently. In managed environments, pair this flag with a whitelist so only the intended internal endpoints are reachable. Like this flag, the whitelist is **honored from User, System, and SystemDefaults settings scopes**; a value set in Workspace (project) settings can only _narrow_ the User or SystemDefaults whitelist and is logged as a warning: workspace entries that no higher-scope entry covers are dropped, when no higher scope sets a whitelist the workspace value is ignored entirely, and a System-scope whitelist always takes precedence over the workspace value (an empty whitelist means "allow all", so a repository can neither widen where hook payloads may be sent nor establish a whitelist of its own).

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] R15-2: The PR description says "Both keys are now stripped from workspace scope" (and Risk & Scope says workspaces "must move it to user settings"), but the shipped behavior — this docs line, narrowWorkspaceHookSecurityOverrides in packages/cli/src/config/settings.ts, the schema descriptions, and the narrowing tests — is narrow-only intersection: a trusted workspace may still NARROW a higher-scope whitelist; only uncovered entries are dropped.

Probe: user ['https://hooks.corp.com/*'] + workspace ['https://hooks.corp.com/ci/*'] → effective ['https://hooks.corp.com/ci/*'] (the "should let a trusted workspace whitelist narrow the user whitelist" test passes at this commit).

Concrete cost: a maintainer validating claim 2 reads "stripped" and concludes a repository can never influence the effective whitelist, while the shipped code lets a trusted repo change it by narrowing; conversely, a user following the migration note moves a deliberately narrowing project whitelist to user scope and loses the per-project restriction.

Suggested fix: update the PR body before merge — describe item 2 as narrow-only intersection (workspace entries may only narrow a higher-scope whitelist; uncovered entries dropped; workspace ignored entirely when no higher scope sets one; System precedence), and replace the blanket migration note.

中文说明

[Suggestion] PR 描述写的是 "Both keys are now stripped from workspace scope"(风险与范围一节还要求把工作区白名单"移到 user settings"),但实际实现——本行文档、packages/cli/src/config/settings.ts 的 narrowWorkspaceHookSecurityOverrides、schema 描述以及收窄测试——是“仅收窄”的交集语义:受信任的工作区仍可收窄更高作用域的白名单,只有未被覆盖的条目会被丢弃。

探针:user ['https://hooks.corp.com/*'] + workspace ['https://hooks.corp.com/ci/*'] → 生效 ['https://hooks.corp.com/ci/*']("should let a trusted workspace whitelist narrow the user whitelist" 测试在本 commit 通过)。

具体代价:维护者验证第 2 项时读到 "stripped",会以为仓库永远无法影响生效白名单,而实际代码允许受信仓库通过收窄来改变它;反过来,用户照迁移说明把刻意收窄用的项目白名单移到 user 作用域,反而失去按项目粒度的限制。

修复建议:合并前更新 PR 正文——把第 2 项描述为仅收窄交集(workspace 条目只能收窄更高作用域白名单;未覆盖条目被丢弃;更高作用域未设置时 workspace 值被完全忽略;System 优先),并替换一刀切的迁移说明。

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

'Warning: security.allowedHttpHookUrls is not a list and was ignored; HTTP hooks are unrestricted apart from SSRF protection.',
);
}
return Array.isArray(hookUrls)

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] R15-3: An array whose entries are ALL non-strings is silently coerced to [] — which the UrlValidator reads as "allow all" — without the warning the adjacent branch adds for the identical outcome. Array.isArray is true, so the warning branch above is skipped; the filter drops every entry; the user's attempted restriction vanishes with no startup warning (the diff's own rationale comment says to "surface the lost restriction instead of silently starting unrestricted").

Failure scenario: probe at this commit — settings { security: { allowedHttpHookUrls: [42, null] } }getAllowedHttpHookUrls() returned [] with zero warnings mentioning the field (expected 0 to be greater than 0); applying the implied fix flipped the probe and the suite stayed green. Boundary: mixed arrays keep their surviving strings; an explicitly empty [] stays warning-free by design ("If empty, all URLs are allowed").

      const filtered = Array.isArray(hookUrls)
        ? hookUrls.filter((entry): entry is string => typeof entry === 'string')
        : [];
      if (Array.isArray(hookUrls) && hookUrls.length > 0 && filtered.length === 0) {
        resolvedCliConfig.warnings.push(
          'Warning: security.allowedHttpHookUrls contains no string entries and was ignored; HTTP hooks are unrestricted apart from SSRF protection.',
        );
      }
      return filtered;
中文说明

[Suggestion] 全部条目都不是字符串的数组会被静默强转为 []——UrlValidator 将其读作"全部允许"——却没有相邻分支为同样结果添加的那条告警。Array.isArray 为 true,因此上方的告警分支被跳过;filter 丢弃所有条目;用户试图设置的限制在没有启动告警的情况下消失(diff 自身的注释正写着"surface the lost restriction instead of silently starting unrestricted")。

失败场景:已在本 commit 探针验证——settings { security: { allowedHttpHookUrls: [42, null] } }getAllowedHttpHookUrls() 返回 [] 且没有任何提及该字段的告警(expected 0 to be greater than 0);应用隐含修复后探针翻转、套件保持绿色。边界:混合数组保留存活的字符串;显式空数组 [] 按设计不告警("If empty, all URLs are allowed")。

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

Comment on lines 2311 to 2313
allowPrivateNetworkHooks:
bareMode || safeMode
? false

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] R15-10: The malformed-value hardening added four lines above covers only allowedHttpHookUrls; the adjacent allowPrivateNetworkHooks (next line) is still passed through raw, and every consumption site tests truthiness (!allowPrivateNetworkHosts && isBlockedAddress(...) in httpHookRunner.ts:66/92 and urlValidator.ts:126) — so any non-empty string silently ENABLES the private-network relaxation. Settings are env-interpolated whole before merging (resolveEnvVarsInObject runs on system/systemDefaults/user/workspace scopes), env values are always strings, and an unset variable leaves the literal $PLACEHOLDER — also truthy.

Failure scenario: probe at this commit — ALLOW_HOOKS_PRIVATE_IP=false with "security": { "allowPrivateNetworkHooks": "$ALLOW_HOOKS_PRIVATE_IP" }loadCliConfig returned 'false' (string, truthy), and HttpHookRunner([], 'false') POSTed to a private-range URL, while the boolean-false control arm was blocked by the SSRF guard. Changing the next line to === true flipped the probe. Not attacker-reachable (workspace scope is stripped) — the direction is admin-disables → silently enabled; the docs section this PR edits ("pair this flag with a whitelist") is exactly where an admin would env-reference the flag.

Suggested fix: coerce on the next line — : settings.security?.allowPrivateNetworkHooks === true, — and push a warning into resolvedCliConfig.warnings when the value is present but not a boolean (mirroring the array field's warning).

中文说明

[Suggestion] 上方四行新增的畸形值加固只覆盖了 allowedHttpHookUrls;相邻的 allowPrivateNetworkHooks(下一行)仍原样透传,而所有消费点都做真值判断(httpHookRunner.ts:66/92 与 urlValidator.ts:126 的 !allowPrivateNetworkHosts && isBlockedAddress(...))——因此任何非空字符串都会静默启用内网放行。settings 在合并前整体做环境变量插值(resolveEnvVarsInObject 作用于 system/systemDefaults/user/workspace 各作用域),环境变量值总是字符串,未设置的变量还会留下字面量 $PLACEHOLDER——同样是真值。

失败场景:已在本 commit 探针验证——ALLOW_HOOKS_PRIVATE_IP=false"security": { "allowPrivateNetworkHooks": "$ALLOW_HOOKS_PRIVATE_IP" }loadCliConfig 返回 'false'(字符串,真值),HttpHookRunner([], 'false') 成功 POST 到内网地址,而布尔 false 的对照分支被 SSRF 防护拦截。把下一行改为 === true 后探针翻转。攻击者不可达(workspace 作用域已被剥离)——方向是管理员关闭 → 被静默开启;本 PR 编辑的文档小节("pair this flag with a whitelist")恰是管理员会用环境变量引用该标志的地方。

修复建议:在下一行强转——: settings.security?.allowPrivateNetworkHooks === true,——并在值存在但不是布尔类型时向 resolvedCliConfig.warnings 推送告警(与数组字段的告警对称)。

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

Comment on lines +1402 to +1404
if (level === 'user' && isHomeDirectory) {
for (const subagent of subagents) {
subagent.homeRootShadow = true;

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] R15-5: The tag assumes the user-level agents directory is <projectRoot>/.qwen/agents, but that only holds when Storage.getGlobalQwenDir() is ~/.qwen. When QWEN_HOME redirects the global dir elsewhere (storage.ts:183-186) and the project root is the home directory, genuinely user-supplied agents at $QWEN_HOME/agents are still tagged homeRootShadow: true even though nothing repo-supplied surfaces at that level — the spawn gate then silently drops their hooks in untrusted folders (pre-PR they registered unconditionally). Fail-closed direction: functionality loss only, no security exposure. Mirror instance in skill-manager.ts:1061-1072.

Failure scenario: QWEN_HOME=/custom/qwen, project root == home, folder untrusted — a legitimate user agent at /custom/qwen/agents/helper.md with hooks: frontmatter is listed at 'user' level, tagged, and its hooks are ignored with a debug-only warning.

Suggested fix: key the tag on actual directory coincidence — set homeRootShadow only for entries loaded from a user-level dir that coincides with (or lies inside) the project root. This is the same provenance fix as R15-6/R15-9 and resolves the over- and under-blocking directions together; do not drop the 'user' exemption wholesale (that would trade this bug for the other).

中文说明

[Suggestion] 该标记假定 user 级 agents 目录是 <projectRoot>/.qwen/agents,但这只在 Storage.getGlobalQwenDir()~/.qwen 时成立。当 QWEN_HOME 把全局目录重定向到别处(storage.ts:183-186)且项目根就是家目录时,位于 $QWEN_HOME/agents 的真正用户自带 agent 仍会被打上 homeRootShadow: true——尽管该层级并没有任何仓库提供的内容出现——spawn 门禁随后会在不可信文件夹中静默丢弃其 hooks(PR 前它们无条件注册)。fail-closed 方向:只是功能损失,无安全暴露。镜像实例在 skill-manager.ts:1061-1072。

失败场景:QWEN_HOME=/custom/qwen、项目根 == 家目录、文件夹不可信——位于 /custom/qwen/agents/helper.md 的合法用户 agent(带 hooks: frontmatter)以 'user' 级列出、被打标,其 hooks 被忽略且只有一条 debug 告警。

修复建议:按实际目录重合来打标——仅对从与项目根重合(或位于其内部)的 user 级目录加载的条目设置 homeRootShadow。这与 R15-6/R15-9 是同一个“按来源判定”的修复,可同时解决过度拦截与欠拦截两个方向;不要整体移除 'user' 豁免(那会把这个 bug 换成另一个)。

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

Comment on lines +1068 to +1070
if (level === 'user' && isHomeDirectory) {
for (const skill of skills) {
skill.homeRootShadow = true;

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] R15-8: The home-root shadow tag applies to EVERY 'user'-level skill, including skills loaded from getCustomSkillDirs() directories, which the 'project' level never reads and which therefore cannot be repository-committed skills surfacing through the skipped project level. Running from ~ with the folder untrusted, the user's own custom-dir skills get their hooks silently deferred and allowedTools ignored — pre-PR they applied unconditionally.

Failure scenario: user has settings.skills.directories pointing at an absolute path outside the home (e.g. /srv/shared-team-skills); qwen runs from /home/user with the folder marked untrusted — listSkillsAtLevel('user') tags all user-level skills including the custom-dir ones, and both gates then defer/ignore the user-authored skill's side effects even though its directory can never coincide with a project-level dir.

Suggested fix: tag per origin directory — when collecting 'user'-level skills under isHomeDirectory, set homeRootShadow only for skills whose base dir is one of the provider config dirs (exclude getCustomSkillDirs() origins), or reuse the directory-coincidence predicate from the R15-5/R15-6/R15-9 fix.

中文说明

[Suggestion] home-root shadow 标记会打到每一个 'user' 级 skill 上,包括从 getCustomSkillDirs() 目录加载的 skill——而 'project' 级从不读取这些目录,因此它们不可能是经由被跳过的 project 级浮出的仓库提交 skill。当家目录作为项目根且文件夹不可信时,用户自己的自定义目录 skill 会被静默延迟注册 hooks、忽略 allowedTools——PR 前它们无条件生效。

失败场景:用户的 settings.skills.directories 指向家目录之外的绝对路径(如 /srv/shared-team-skills);qwen 在 /home/user 运行且文件夹被标记为不可信——listSkillsAtLevel('user') 会给所有 user 级 skill(包括自定义目录的)打标,两处门禁随即延迟/忽略该用户自创 skill 的副作用,尽管其目录永远不可能与 project 级目录重合。

修复建议:按来源目录打标——在 isHomeDirectory 下收集 'user' 级 skill 时,仅对基础目录属于 provider 配置目录的 skill 设置 homeRootShadow(排除 getCustomSkillDirs() 来源),或复用 R15-5/R15-6/R15-9 修复中的目录重合判定。

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

@wenshao

wenshao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Root cause

Main's voice PR #8350 (732f4d8) landed after this branch's last merge. It renamed stripWorkspacePrivateNetworkHooksstripWorkspaceSecurityBypasses in packages/cli/src/config/settings.ts to also strip the new security.allowedInsecureVoiceBaseUrls from Workspace scope. This PR had replaced that same function with narrowWorkspaceHookSecurityOverrides (strips allowPrivateNetworkHooks, narrows allowedHttpHookUrls against the higher-scope whitelist). Same function + same mergeSettings call site → the only conflict (2 hunks, one file).

Semantic, not just textual

Both sides rewrote the workspace security strip. Resolution keeps the PR's function (strict superset) and grafts main's voice strip in; stripWorkspaceSecurityBypasses is deleted, main's voice warning in getSettingsWarnings merged cleanly:

if (
  WORKSPACE_STRIPPED_SECURITY_FIELDS.every(
    (field) => security[field] === undefined,
  ) &&
  security.allowedInsecureVoiceBaseUrls === undefined
) {
  return workspace;
}
const restSecurity = { ...security };
delete restSecurity.allowPrivateNetworkHooks;
delete restSecurity.allowedInsecureVoiceBaseUrls;

Call site kept: tagMcpServerScope(narrowWorkspaceHookSecurityOverrides(workspace, system, user, systemDefaults), 'workspace').

What is load-bearing

  • The voice field must appear in BOTH the early-return guard and the strip body: missing from the guard, a workspace setting only that field bypasses the function and leaks into the merge, regressing feat(voice): support trusted private ASR base URLs #8350's security property.
  • The voice list is stripped outright like the boolean bypass; narrowing applies only to allowedHttpHookUrls. User/System/SystemDefaults voice values are untouched.

Could not verify

No build/tests run here. Known breakage in the AUTO-MERGED, non-conflicted packages/cli/src/config/settings.test.ts: main's test allowedInsecureVoiceBaseUrls scope handling > 'should strip and warn about the allowlist from workspace scope' still asserts pre-PR behavior — workspace allowedHttpHookUrls survives when no higher-scope whitelist exists. This PR intentionally discards it there (its own test asserts toBeUndefined()), so that assertion now fails and should become toBeUndefined(). Left untouched per scope rules; other voice tests use User/System scope and are unaffected.

中文说明

根因:main 的语音 PR #8350(732f4d8a2)把 settings.ts 的 stripWorkspacePrivateNetworkHooks 改名为 stripWorkspaceSecurityBypasses,增加从 Workspace 作用域剥离新增的 security.allowedInsecureVoiceBaseUrls;本 PR 已把同一函数替换为更全面的 narrowWorkspaceHookSecurityOverrides。同一函数与调用点冲突,是唯一冲突(一文件两处)。

语义冲突:双方都重写了 workspace 安全剥离逻辑。解决方式:保留本 PR 函数(严格超集),把语音剥离嫁接进去——守卫加 allowedInsecureVoiceBaseUrls === undefined,函数体加 delete restSecurity.allowedInsecureVoiceBaseUrlsstripWorkspaceSecurityBypasses 删除,其语音警告已干净合并。

关键点:语音字段必须同时在守卫与剥离两处——否则仅设置该字段的 workspace 会绕过函数泄漏进合并结果,破坏 #8350 的安全属性。语音列表整体剥离;收窄仅适用于 allowedHttpHookUrls。User/System/SystemDefaults 作用域不受影响。

未能验证:未运行构建/测试。已知问题:自动合并(未冲突)的 settings.test.ts 中,main 的测试 'should strip and warn about the allowlist from workspace scope' 仍断言旧行为——无更高作用域白名单时 workspace 的 allowedHttpHookUrls 保留;本 PR 有意丢弃它,该断言会失败,应改为 toBeUndefined()。因该文件未冲突,按规则未修改。

@wenshao

wenshao commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /resolve

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge resolution — PR #8396 (fix/hooks-security ← main)

Root cause

Both sides inserted a new alias at the same spot in packages/cli/vitest.config.ts, immediately after the memoryScopes alias:

That was the only conflict; the remaining 211 commits from main merged cleanly.

Textual, not semantic

The two alias additions are independent and both target files exist in the merged tree. The resolution keeps both:

'@qwen-code/qwen-code-core/envVarResolver': path.resolve(
  __dirname,
  '../core/src/utils/envVarResolver.ts',
),
'@qwen-code/qwen-code-core/toolWriteOrigin': path.resolve(
  __dirname,
  '../core/src/services/tool-write-origin.ts',
),

What is load-bearing

  • Alias order: both subpath aliases must appear BEFORE the generic '@qwen-code/qwen-code-core' alias. Vite matches aliases in declaration order, so moving the generic entry above them would silently shadow every core-subpath import in cli tests.
  • This PR deletes packages/cli/src/utils/envVarResolver.ts. Main's refactored fast-path-settings.ts now imports @qwen-code/qwen-code-core/envVarResolver, which only resolves because three pieces coexist: core's ./envVarResolver package.json export, cli's tsconfig paths entry, and the vitest alias. Dropping any one of the three breaks that import.

What I could not verify

No build, typecheck, or tests were run. Cross-checks performed instead: git diff origin/main matches the PR's original stat exactly (43 files, 2922+/525−), the PR's narrowing logic in settings.ts and the config.ts whitelist coercion survived intact, no dangling references to the deleted cli-local resolver remain, and main touched none of the PR's core hook/skill/subagent files.

中文说明

根因:PR #8396(提交 1353d0b603)与 main(提交 fa8cae5418,PR #8852)都在 packages/cli/vitest.config.tsmemoryScopes 别名之后插入了新别名:前者加 envVarResolver(本 PR 将该模块从 cli 移到 core 并删除了 cli 副本),后者加 toolWriteOrigin。这是唯一的冲突,其余 211 个 main 提交均自动合并成功。

纯文本冲突,无语义冲突:两个别名互相独立,目标文件均存在,解决方案是两者都保留(见上方代码块)。

关键约束:① 两个子路径别名必须位于通用 '@qwen-code/qwen-code-core' 别名之前——Vite 按声明顺序匹配,通用项若提前会遮蔽所有子路径导入;② 本 PR 删除了 cli 本地的 envVarResolver.ts,main 重构后的 fast-path-settings.ts 改为导入 core 子路径,该导入依赖 core package.json 的 ./envVarResolver export、cli tsconfig 的 paths 映射和 vitest 别名三者同时存在,缺一即断。

未能验证:未运行构建/类型检查/测试。已做的交叉核对:合并结果对 origin/main 的差异与原 PR 统计完全一致(43 个文件,+2922/−525);settings.ts 的白名单收窄逻辑与 config.ts 的白名单类型校正完整保留;无对已删除 cli 模块的悬空引用;main 未改动本 PR 的 core hooks/skills/subagents 文件。

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows.

Not explored to full depth (tool budget reached): "PR #8396 (QwenLM/qwen-code) security-hardening review,…": none — all checks I started were completed within budget.; "PR #8396 (QwenLM/qwen-code) security-hardening review,…": none — all checks above completed within budget (~24 calls).; "PR #8396 (QwenLM/qwen-code) security-hardening review,…": none** — all checks completed within budget.; "PR #8396 (QwenLM/qwen-code) security-hardening review,…": none — all checks above completed within budget.; "PR #8396 (QwenLM/qwen-code) security-hardening review,…": none — all planned checks completed within budget (~17 of ~52 calls used)., and 16 more.

Test Plan (not a blocker): 1385 tests green — this review observed 494, 1285, 1511, 3393, 555 passed.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows。

未探索到全部深度(达到工具调用预算):"PR #8396 (QwenLM/qwen-code) security-hardening review,…"none — all checks I started were completed within budget."PR #8396 (QwenLM/qwen-code) security-hardening review,…"none — all checks above completed within budget (~24 calls)."PR #8396 (QwenLM/qwen-code) security-hardening review,…"none** — all checks completed within budget."PR #8396 (QwenLM/qwen-code) security-hardening review,…"none — all checks above completed within budget."PR #8396 (QwenLM/qwen-code) security-hardening review,…"none — all planned checks completed within budget (~17 of ~52 calls used).,另有 16 条。

Test Plan(非阻断):1385 tests green — this review observed 494, 1285, 1511, 3393, 555 passed

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

Comment on lines +519 to +523
if (narrowed.length > 0) {
restSecurity.allowedHttpHookUrls = narrowed;
} else {
delete restSecurity.allowedHttpHookUrls;
}

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.

[Critical] The workspace-whitelist narrowing added here breaks a pre-existing, unmodified test — the packages/cli suite is red at this head. narrowWorkspaceHookSecurityOverrides deliberately discards a workspace-scoped security.allowedHttpHookUrls when no higher-scope whitelist exists, but the pre-existing test allowedInsecureVoiceBaseUrls scope handling > should strip and warn about the allowlist from workspace scope (settings.test.ts:3776, assertion at :3797, landed via the main merge 732f4d8) still asserts the old pass-through semantics. — Failure scenario: workspace settings define allowedHttpHookUrls: ['https://hooks.example.com/*'] with no User/System/SystemDefaults whitelist → higherUrls === undefined → narrowed = [] → delete → merged is undefinedAssertionError: expected undefined to deeply equal ['https://hooks.example.com/*']. Measured: npm test --workspace=packages/cli fails exactly this test at this head; the same test passes at the merge base (A/B-verified); a probe flip (asserting toBeUndefined()) passes. The discard is documented, intended behavior — the stale test is the overlooked artifact.

// settings.test.ts:3797 — update the stale assertion to the new semantics:
expect(settings.merged.security?.allowedHttpHookUrls).toBeUndefined();
中文说明

此处新增的 workspace 白名单收窄逻辑破坏了一个既有且未被本 PR 修改的测试——packages/cli 测试套件在当前 head 上是红的。narrowWorkspaceHookSecurityOverrides 在更高 scope 未设置白名单时会刻意丢弃 workspace scope 的 security.allowedHttpHookUrls,但既有测试 allowedInsecureVoiceBaseUrls scope handling > should strip and warn about the allowlist from workspace scope(settings.test.ts:3776,断言位于 :3797,经由 main 合并 732f4d8 进入本分支)仍在断言旧的直通合并语义。——失败场景:workspace 设置定义 allowedHttpHookUrls: ['https://hooks.example.com/*'] 且 User/System/SystemDefaults 均未设置白名单 → higherUrls === undefined → narrowed = [] → delete → 合并结果为 undefined → 断言失败。实测:npm test --workspace=packages/cli 在本 head 上恰好失败于该测试;同一测试在合并基上通过(已做 A/B 验证);探针翻转(改为断言 toBeUndefined())后通过。丢弃行为本身是有文档记载的预期行为——被遗漏的是这个过时的测试。

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

Comment on lines +605 to +609
// Side effects skipped by the trust gate on first load are applied
// on re-invocation once the folder becomes trusted (the gate is
// live), without re-injecting the skill body into context. Drop the
// entry only once the side effects were actually applied, so
// still-untrusted re-invocations keep the deferral alive.

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.

[Critical] This gate is live in one direction only: untrusted→trusted re-applies deferred repo-supplied side effects, but trusted→untrusted never revokes already-applied ones. Config.isTrustedFolder() live-reads the IDE context on every call (a mid-session revocation flips it instantly), yet registered skill hooks keep executing — unregisterSkillHooks (registerSkillHooks.ts:136) is a no-op stub with zero call sites, HookSystem.clearSessionHooks has zero production callers, HookEventHandler.executeHooks merges session hooks into every event with no trust check, and addSessionAllowRule grants have no removal API. IDE trust revocation only surfaces the non-blocking IdeTrustChangeDialog banner ('Press r to restart'). — Failure scenario: IDE mode, trusted workspace; the user invokes a repo skill whose SKILL.md declares command hooks → hooks register in the shared SessionHooksManager; mid-session the IDE revokes trust → repo-controlled hooks keep executing on every event (background agents inherit the same hook system/sessionId and never surface the banner) and the session-wide allowedTools grants keep auto-approving — the exact surface this security-hardening PR exists to gate. Maintainer ruling needed on whether restart-based revocation is the accepted mechanism (settings-file hooks share the same restart-based pattern) — but this diff introduced liveness into this gate and implemented only the capability-granting half.

// Fix: implement the removal side — track hook IDs per skill at registration,
// implement unregisterSkillHooks for real, and purge session hooks + session
// allow rules from the trust-change listener; or re-check isTrustedFolder()
// in executeHooks before merging session hooks.
中文说明

这个门禁只在一个方向上是"活"的:不可信→受信时会重新应用被延迟的仓库侧效果,但受信→不可信时从不撤销已经应用的效果。Config.isTrustedFolder() 每次调用都实时读取 IDE 上下文(会话中途撤销信任会立即翻转其结果),但已注册的 skill hooks 仍会继续执行——unregisterSkillHooks(registerSkillHooks.ts:136)是一个零调用点的空壳 stub,HookSystem.clearSessionHooks 没有任何生产调用者,HookEventHandler.executeHooks 在合并 session hooks 到每个事件时不做任何信任检查,addSessionAllowRule 授予的规则也没有移除 API。IDE 侧的信任撤销只会弹出非阻塞的 IdeTrustChangeDialog 横幅("按 r 重启")。——失败场景:IDE 模式、受信工作区;用户调用一个 SKILL.md 声明了 command hooks 的仓库 skill → hooks 注册进共享的 SessionHooksManager;会话中途 IDE 撤销信任 → 仓库可控的 hooks 在每个事件上继续执行(后台 agent 继承同一 hook 系统/sessionId,永远不会看到横幅),会话级 allowedTools 授权也继续自动批准——这正是本安全加固 PR 要门禁的表面。需要维护者裁决"基于重启的撤销"是否为可接受机制(settings 文件 hooks 同样是重启生效模式)——但本 diff 为这个门禁引入了活性,却只实现了授予能力的那一半。

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

Comment thread scripts/dev.js
Comment on lines +103 to +109
if (specifier === '@qwen-code/qwen-code-core/memoryScopes') {
return {
shortCircuit: true,
url: memoryScopesSourceUrl,
format: 'module',
};
}

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 remap set this hunk extends is still missing the toolWriteOrigin core subpath — the comment's own invariant ('each one cli source imports needs its own remap to the source file') remains violated. Cli source imports @qwen-code/qwen-code-core/toolWriteOrigin at packages/cli/src/serve/bridge-file-system-adapter.ts:62 and packages/cli/src/acp-integration/service/filesystem.ts:25; this same hunk fixed the sibling pre-existing memoryScopes gap while leaving toolWriteOrigin out. — Concrete cost: probe-verified at this head: with no built packages/core/dist, node scripts/dev.js serve --port 0 exits 1 with ERR_MODULE_NOT_FOUND .../dist/src/services/tool-write-origin.js imported from .../bridge-file-system-adapter.ts; adding the remap flips the probe. Affects npm run dev:daemon / npm run dev -- serve on fresh checkouts.

// Fix: mirror the two new remaps —
const toolWriteOriginSourceUrl = pathToFileURL(
  join(root, 'packages', 'core', 'src', 'services', 'tool-write-origin.ts'),
).href;
// + interpolate into loaderCode and add an exact-match branch for
// '@qwen-code/qwen-code-core/toolWriteOrigin' returning that URL.
中文说明

本 hunk 扩展的 remap 集合仍然遗漏了 toolWriteOrigin 这个 core 子路径——注释自己声明的不变量("cli 源码引入的每一个子路径都需要一条到源文件的 remap")仍未满足。cli 源码在 packages/cli/src/serve/bridge-file-system-adapter.ts:62packages/cli/src/acp-integration/service/filesystem.ts:25 引入 @qwen-code/qwen-code-core/toolWriteOrigin;同一个 hunk 修复了同类的既有 memoryScopes 缺口,却漏掉了 toolWriteOrigin。——具体代价:已在本 head 探针验证:在没有构建产物 packages/core/dist 的检出上,node scripts/dev.js serve --port 0ERR_MODULE_NOT_FOUND .../dist/src/services/tool-write-origin.js imported from .../bridge-file-system-adapter.ts 退出(exit 1);补上该 remap 即可翻转探针。影响全新检出上的 npm run dev:daemon / npm run dev -- serve

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

Comment on lines +14 to +16
"@qwen-code/qwen-code-core/envVarResolver": [
"../core/src/utils/envVarResolver.ts"
],

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 new envVarResolver subpath gets resolution entries here, in packages/cli/vitest.config.ts, and in core's exports map — but not in integration-tests/tsconfig.json, whose own comment mandates 'every subpath the program imports needs an explicit entry naming its source file … Keep these in sync with the packages' exports maps'. integration-tests/terminal-capture/skill-review-harness/text-capture.tsx:27 imports cli settings.ts relatively, pulling the new subpath import (settings.ts:34, and config-utils.ts:7) into the tsc -p integration-tests program. — Concrete cost: probe-verified: on a clean checkout (no built dist) tsc -p integration-tests --noEmit reports TS2307: Cannot find module '@qwen-code/qwen-code-core/envVarResolver' at both sites; adding the missing entry removes both errors. With a stale dist present the subpath silently resolves to stale declarations — the two failure modes that comment says these entries exist to prevent. Blast radius is editor TS servers and ad-hoc runs (nothing in CI runs that program today).

// Fix: add to integration-tests/tsconfig.json paths:
"@qwen-code/qwen-code-core/envVarResolver": [
  "../packages/core/src/utils/envVarResolver.ts"
]
// (consider `toolWriteOrigin` too — also absent there while imported by cli sources)
中文说明

新的 envVarResolver 子路径在此处、packages/cli/vitest.config.ts 以及 core 的 exports map 中都拿到了解析条目——唯独 integration-tests/tsconfig.json 没有,而该文件的注释明确要求"程序引入的每一个子路径都需要一条指名源文件的显式条目……与包的 exports map 保持同步"。integration-tests/terminal-capture/skill-review-harness/text-capture.tsx:27 以相对路径引入 cli 的 settings.ts,把新的子路径引入(settings.ts:34,以及 config-utils.ts:7)拉进了 tsc -p integration-tests 程序。——具体代价:已探针验证:在干净检出(无构建产物)上,tsc -p integration-tests --noEmit 在两处均报 TS2307: Cannot find module '@qwen-code/qwen-code-core/envVarResolver';补上缺失条目后两个错误消失。若存在过期的 dist,该子路径会悄悄解析到过期的声明文件——正是该注释所说这些条目要避免的两类失败。影响范围是编辑器 TS 服务与临时 typecheck(目前 CI 不运行该程序)。

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

Comment on lines +251 to +254
// Never follow redirects: the whitelist and DNS-level SSRF
// checks above cover only this URL, and a 307/308 would re-send
// the hook payload to an unvalidated target. A 3xx response
// falls into the non-2xx branch below (non-blocking error).

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.

[Critical] R14-1 re-checked at d4a76b9 and STILL STANDS: the whitelist static check regex-tests the RAW interpolated hook URL string, but fetch() sends the WHATWG-normalized URL — dot-segment removal moves the hook payload to a path on a user-whitelisted host that the whitelist does not cover. This is the same static-check/runtime-consumer divergence class this PR closes for redirects (as this very comment describes), left open for normalization. — Failure scenario: user-scope whitelist ['https://corp.example.com/api/*']; a trusted repo configures hook url https://corp.example.com/api/../telemetry/exfilvalidate() tests the raw string → allowed; fetch normalizes and POSTs the hook payload (prompts, tool inputs/outputs) to https://corp.example.com/telemetry/exfil, which the whitelist regex does NOT match. Probe-verified end-to-end with a real fetch in prior rounds; every path-shaped pattern (…/prefix/* — all documented examples) is bypassable.

// Fix: parse once and validate the exact string that will be fetched:
const parsed = new URL(url); // invalid URLs already fail closed upstream
// run isBlocked + isAllowed against parsed.href, then fetch(parsed.href, ...)
中文说明

R14-1 已在 d4a76b9 复查,仍然存在:白名单静态检查对原始插值后的 hook URL 字符串做正则匹配,但 fetch() 发送的是 WHATWG 规范化后的 URL——点段移除会把 hook 负载投递到用户白名单主机上一个白名单并未覆盖的路径。这与本 PR 为重定向关闭的"静态检查/运行时消费者分歧"是同一类问题(正如本条注释所描述),但在规范化方向上仍然敞开。——失败场景:user scope 白名单 ['https://corp.example.com/api/*'];受信仓库配置 hook url 为 https://corp.example.com/api/../telemetry/exfilvalidate() 对原始字符串判定放行;fetch 规范化后把 hook 负载(prompt、工具输入/输出)POST 到白名单正则并不匹配的 https://corp.example.com/telemetry/exfil。此前几轮已用真实 fetch 端到端探针验证;所有路径形态的模式(…/prefix/*——即全部文档示例)均可被绕过。

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

Comment on lines +944 to +948
const trustedAgentLevel =
(config.level === 'user' && config.homeRootShadow !== true) ||
config.level === 'builtin' ||
config.level === 'extension' ||
config.level === 'session';

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.

[Critical] R15-9 re-checked at d4a76b9 and STILL STANDS: the 'user' arm of this allowlist assumes the user agents directory is ~/.qwen, but Storage.getGlobalQwenDir() honors QWEN_HOME (storage.ts:183-193; relative values resolve against cwd) and listSubagentsAtLevel('user') reads getGlobalQwenDir()/agents. When QWEN_HOME resolves inside the project root (e.g. a direnv/devcontainer convention), repo-controlled agents surface at 'user' level WITHOUT the homeRootShadow tag — the tag is set only when path.resolve(projectRoot) === path.resolve(os.homedir()), which that geometry falsifies. — Failure scenario: QWEN_HOME=.qwen-home (relative, inside the repo) → $QWEN_HOME/agents/*.md with frontmatter hooks list at 'user' level untagged → this gate exempts them → repo-supplied agent hooks register in an untrusted folder. Fix (shared root cause with R15-6): base the shadow decision on directory containment — tag per-agent when the resolved agents directory (or loaded filePath) is inside path.resolve(projectRoot), regardless of home equality.

中文说明

R15-9 已在 d4a76b9 复查,仍然存在:此允许清单的 'user' 分支假设用户级 agents 目录是 ~/.qwen,但 Storage.getGlobalQwenDir() 会遵循 QWEN_HOME(storage.ts:183-193;相对值按 cwd 解析),而 listSubagentsAtLevel('user') 读取的是 getGlobalQwenDir()/agents。当 QWEN_HOME 解析到项目根目录内部(例如 direnv/devcontainer 约定)时,仓库可控的 agents 会以 'user' 级别出现且不带 homeRootShadow 标记——该标记只在 path.resolve(projectRoot) === path.resolve(os.homedir()) 时设置,而上述几何形状恰好使其为假。——失败场景:QWEN_HOME=.qwen-home(相对路径,位于仓库内)→ 带 frontmatter hooks 的 $QWEN_HOME/agents/*.md 以未打标的 'user' 级别被列出 → 此门禁将其豁免 → 仓库提供的 agent hooks 在不可信文件夹中被注册。修复(与 R15-6 同根因):把 shadow 判定改为基于目录包含关系——当解析出的 agents 目录(或加载的 filePath)位于 path.resolve(projectRoot) 内部时逐 agent 打标,而不依赖与 home 目录的相等比较。

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

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ Downgraded from Request changes to Comment: self-PR; CI failing: Test (ubuntu-latest, Node 22.x). Reviewed.

Test Plan (not a blocker): 1385 tests green — this review observed 19368, 19901 passed.

中文说明

⚠️ 已从请求修改降级为评论:self-PR; CI failing: Test (ubuntu-latest, Node 22.x)。 已审查。

Test Plan(非阻断):1385 tests green — this review observed 19368, 19901 passed

— qwen-code via Qwen Code /review (v0.21.11)

*/
private appendSystemMessage(merged: HookOutput, output: HookOutput): void {
if (output.systemMessage !== undefined) {
merged.systemMessage = merged.systemMessage

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Critical] Preserve suppressOutput per producing message. The new concatenation appends every systemMessage, while mergeWithOrLogic independently keeps the last defined suppression value. For example, { systemMessage: 'private diagnostic', suppressOutput: true } followed by { systemMessage: 'visible', suppressOutput: false } aggregates to both messages with suppression disabled; processCommonHookOutputFields then emits the private diagnostic too. Filter suppressed messages before concatenation or retain per-message metadata until emission, and add a mixed suppressed/visible regression test.

[Critical] 请按消息来源分别保留 suppressOutput。新的拼接逻辑会加入每个 systemMessage,但 mergeWithOrLogic 独立采用最后一个已定义的 suppression 值。例如先返回 { systemMessage: 'private diagnostic', suppressOutput: true },再返回 { systemMessage: 'visible', suppressOutput: false },聚合结果会包含两条消息且 suppression 被关闭,最终私有诊断也会被输出。请在拼接前过滤被抑制的消息,或保留逐消息元数据直到输出阶段,并增加“抑制消息 + 可见消息”的回归测试。

— qwen-code via Qwen Code /review (v0.21.11)

merged.systemMessage = merged.systemMessage
? [merged.systemMessage, output.systemMessage]
.filter(Boolean)
.join('\n')

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Critical] Add a final aggregate size limit here. Command and HTTP hook runners cap individual outputs, but this loop concatenates an unbounded number of individually valid messages; a large hook set can therefore create a multi-megabyte systemMessage that is logged and emitted to terminal/ACP consumers as one payload. Cap while appending or truncate the final aggregate deterministically, and cover a many-hook case.

[Critical] 请为最终聚合结果增加总长度上限。命令和 HTTP hook runner 只限制单个输出,而这里可以拼接任意数量的合法消息;大量 hooks 因此能够生成数 MB 的 systemMessage,并作为一个 payload 写入日志及终端/ACP 消费端。请在追加时限制剩余容量,或对最终结果进行确定性截断,并增加多 hook 场景测试。

— qwen-code via Qwen Code /review (v0.21.11)

if (!response.ok) {
if (response.status >= 300 && response.status < 400) {
// A redirect delivers no payload, so it must not consume a
// once hook's single execution: drop the slot added above so

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Critical] Restrict this branch to actual Fetch redirect statuses (301, 302, 303, 307, 308). 304 Not Modified is currently classified as a redirect, gets the redirect warning, and deletes a once hook's execution marker; the supposedly one-shot hook then sends another request on every matching event. Let other non-2xx 3xx statuses use the generic error path and add a 304 regression test.

[Critical] 请把该分支限制为 Fetch 实际识别的重定向状态码(301302303307308)。当前 304 Not Modified 也会被当作重定向,输出重定向警告并删除 once hook 的执行标记,导致本应只执行一次的 hook 在后续每个匹配事件中再次请求。其他非 2xx 的 3xx 状态应走通用错误路径,并增加 304 回归测试。

— qwen-code via Qwen Code /review (v0.21.11)


const INTERNAL_SECRET_ENV_VARS_UPPER = new Set(
INTERNAL_SECRET_ENV_VARS.map((v) => v.toUpperCase()),
);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Critical] Include QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN in the shared internal-secret denylist. Serve startup intentionally retains this daemon-local credential while fast-path settings are interpolated, and deletes the original only afterward. A trusted workspace setting can therefore reference it and copy the value under another environment key, after which repository hooks or child commands inherit the credential. Cover settings resolution, child-env sanitization, and the serve fast path.

[Critical] 请把 QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN 加入共享内部秘密 denylist。serve 启动时会在 fast-path settings 插值期间保留这个 daemon 本地凭证,之后才删除原始变量;因此可信 workspace 设置可以引用它并把值复制到另一个环境变量名,随后仓库 hooks 或子进程命令就能继承该凭证。请覆盖 settings 解析、子进程环境清理和 serve fast path。

— qwen-code via Qwen Code /review (v0.21.11)

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

Partially reviewed — gaps disclosed.

Unresolved, please confirm:

  • [Critical] R16-2 (thread at packages/core/src/tools/skill.ts:609): one-directional gate liveness — the trusted→untrusted revocation half is factually absent at this head (registered skill hooks keep executing; unregisterSkillHooks is a no-op stub with zero call sites, clearSessionHooks has no production callers, session allow rules have no removal API), but the thread itself awaits a maintainer ruling on whether restart-based revocation is the accepted mechanism (settings-file hooks share the same restart-based pattern, and pre-PR behavior was identical — the diff narrows the grant side without widening the revocation gap). Could not be ruled fixed or defect from code alone.

Not reviewed: reverse audit — reached the 5-round cap without two consecutive dry rounds (rounds 3, 4 and 5 each reported findings; the loop ended at the cap, not by convergence).

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows.

Not reviewed: build-and-test — packages/webui test suite did not run (whole-call budget); test-efficacy probe capped (47 hunk probes / 7 mutants unprobed).

Not explored to full depth (tool budget reached): "agent reverse-audit (round 4)": verify the tool scheduler actually executes same-turn tool calls concurrently (my grep in packages/core/src/core found no tool-execution loop; the finding's inc…; "agent 1c": full packages/cli and packages/core test suites and integration tests were not run — only the test files the PR touches plus package-level typechecks.; "agent reverse-audit (round 2)": Config.isTrustedFolder() semantics (core/src/config/config.ts:7331) not read — trust default in non-IDE mode unverified**; "agent reverse-audit (round 2)": packages/cli suite not run — chunk's tests' green status taken from confirmed finding #1, which attributes the red suite solely to settings.test.ts**; "agent reverse-audit (round 1)": conclusively verifying whether project-level extensions require explicit user consent/enablement before activation (the covering control for the extension arm….

[Critical] R17-6 (re-asserts @wenshao's open blocker, comment 3779654325 at packages/core/src/utils/sanitize-child-env.ts:37 — could not be anchored to a changed line; the denylist entries are unchanged context): QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN is absent from INTERNAL_SECRET_ENV_VARS — re-checked at this head and still standing, the denylist is unchanged (exactly QWEN_SERVER_TOKEN, QWEN_DAEMON_TOKEN, and the private ACP capability). Serve startup intentionally retains this daemon-local credential while fast-path settings are interpolated and deletes the original only afterward; a trusted workspace setting can reference it and copy the value under another env key, after which repository hooks or child commands can exfiltrate it. Its three denylisted siblings have the identical lifecycle; this one is omitted, so the invariant this PR canonizes ('Qwen-internal secrets are never substituted into hook commands, URLs, or headers') is false for it. Fix: add 'QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN' to INTERNAL_SECRET_ENV_VARS.

中文说明

仅完成部分审查,审查缺口已披露。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未审查:reverse audit — reached the 5-round cap without two consecutive dry rounds (rounds 3, 4 and 5 each reported findings; the loop ended at the cap, not by convergence)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows。

未审查:build-and-test — packages/webui test suite did not run (whole-call budget); test-efficacy probe capped (47 hunk probes / 7 mutants unprobed)。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 4)"verify the tool scheduler actually executes same-turn tool calls concurrently (my grep in packages/core/src/core found no tool-execution loop; the finding's inc…"agent 1c"full packages/cli and packages/core test suites and integration tests were not run — only the test files the PR touches plus package-level typechecks."agent reverse-audit (round 2)"Config.isTrustedFolder() semantics (core/src/config/config.ts:7331) not read — trust default in non-IDE mode unverified**"agent reverse-audit (round 2)"packages/cli suite not run — chunk's tests' green status taken from confirmed finding #1, which attributes the red suite solely to settings.test.ts**"agent reverse-audit (round 1)"conclusively verifying whether project-level extensions require explicit user consent/enablement before activation (the covering control for the extension arm…

[Critical] R17-6 (re-asserts @wenshao's open blocker, comment 3779654325 at packages/core/src/utils/sanitize-child-env.ts:37 — could not be anchored to a changed line; the denylist entries are unchanged context): QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN is absent from INTERNAL_SECRET_ENV_VARS — re-checked at this head and still standing, the denylist is unchanged (exactly QWEN_SERVER_TOKEN, QWEN_DAEMON_TOKEN, and the private ACP capability). Serve startup intentionally retains this daemon-local credential while fast-path settings are interpolated and deletes the original only afterward; a trusted workspace setting can reference it and copy the value under another env key, after which repository hooks or child commands can exfiltrate it. Its three denylisted siblings have the identical lifecycle; this one is omitted, so the invariant this PR canonizes ('Qwen-internal secrets are never substituted into hook commands, URLs, or headers') is false for it. Fix: add 'QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN' to INTERNAL_SECRET_ENV_VARS.

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

Comment on lines +519 to +522
if (narrowed.length > 0) {
restSecurity.allowedHttpHookUrls = narrowed;
} else {
delete restSecurity.allowedHttpHookUrls;

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.

[Critical] R16-1: The workspace-whitelist narrowing added here breaks a pre-existing, unmodified test — the packages/cli suite is red at this head (re-checked this round; still stands). narrowWorkspaceHookSecurityOverrides deliberately discards a workspace-scoped security.allowedHttpHookUrls when no higher-scope whitelist exists, but the pre-existing test allowedInsecureVoiceBaseUrls scope handling > should strip and warn about the allowlist from workspace scope (settings.test.ts:3776, assertion at :3797) still asserts the old pass-through semantics. The sibling stale test was updated; this one was missed. — Failure scenario: workspace settings define allowedHttpHookUrls: ['https://hooks.example.com/*'] with no User/System/SystemDefaults whitelist → higherUrls === undefined → narrowed = [] → delete → merged is undefinedAssertionError: expected undefined to deeply equal ['https://hooks.example.com/*']npm test --workspace=packages/cli and CI fail.

Witness (this round): the test fails in isolation on the PR branch, passes at merge base c396fe3d in the identical environment, and still fails with QWEN_HOME unset (1 failed | 176 passed) — PR-caused, not environmental. The discard is documented, intended behavior; the stale test is the overlooked artifact.

// settings.test.ts:3797 — update the stale assertion to the new semantics:
expect(settings.merged.security?.allowedHttpHookUrls).toBeUndefined();
中文说明

[Critical] R16-1:此处新增的 workspace 白名单收窄逻辑破坏了一个既有且未被本 PR 修改的测试——packages/cli 测试套件在当前 head 上是红的(本轮复查确认仍然存在)。narrowWorkspaceHookSecurityOverrides 在更高 scope 未设置白名单时会刻意丢弃 workspace scope 的 security.allowedHttpHookUrls,但既有测试 allowedInsecureVoiceBaseUrls scope handling > should strip and warn about the allowlist from workspace scope(settings.test.ts:3776,断言位于 :3797)仍在断言旧的直通合并语义。同类的另一个过时测试已更新,这一个被遗漏了。——失败场景:workspace 设置定义 allowedHttpHookUrls: ['https://hooks.example.com/*'] 且 User/System/SystemDefaults 均未设置白名单 → higherUrls === undefined → narrowed = [] → delete → 合并结果为 undefined → 断言失败 → npm test --workspace=packages/cli 与 CI 失败。

见证(本轮):该测试在 PR 分支上单独运行即失败,在合并基 c396fe3d 的相同环境下通过,且移除 QWEN_HOME 后仍然失败(1 failed | 176 passed)——是 PR 导致的,不是环境问题。丢弃行为本身是有文档记载的预期行为,被遗漏的是这个过时的测试。

修复:把 settings.test.ts:3797 的断言更新为新语义 expect(settings.merged.security?.allowedHttpHookUrls).toBeUndefined();

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

Comment on lines +214 to +216
export function hookUrlPatternCovers(
outerPattern: string,
innerPattern: string,

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.

[Critical] R17-1: The whitelist boundary is enforced by regex-testing the RAW URL string — both in validate()/compilePattern and in this new hookUrlPatternCovers gate — but neither models WHATWG URL parsing, so URL-structure tokens bypass it. Probe-verified against this head, three token families (with wildcard-host patterns such as https://*.corp.com/*): (1) userinfohttps://x@evil.com/.corp.com/payload validates allowed: true while new URL(...).hostname is evil.com, where fetch() sends the payload; hookUrlPatternCovers also certifies such entries as legitimate narrowings. (2) fragment / newline — a workspace entry https://evil.com#.corp.com/* is certified as covered (# is not in regexActive), survives narrowWorkspaceHookSecurityOverrides and REPLACES the higher-scope list, matches at runtime, and fetch strips the fragment → payload POSTs to evil.com; the \n axis behaves the same (WHATWG strips \n\r\t). (3) authority/path boundaryhttps://evil.com/.corp.com/payload and https://evil.com\.corp.com/x are both isAllowed: true with parsed hostname evil.com (backslash folds to / mid-authority), and covers() certifies https://evil.com/.corp.com/* under https://*.corp.com/*. This defeats the guarantee this PR writes into settings.ts, the schema, and the docs — that a workspace may only NARROW where hook payloads may be sent — and subsumes prior-round R14-1 (raw string vs normalized fetch). — Failure scenario: user/system whitelist ['https://*.corp.com/*']; a trusted-but-malicious repo adds the workspace narrowing entry ['https://evil.com#.corp.com/*'] plus an HTTP hook targeting it → the entry survives narrowing, becomes the effective whitelist, and the hook payload (prompts, tool inputs/outputs) POSTs to evil.com.

Witness (probes this round, unmodified PR): userinfo — validate('https://x@evil.com/.corp.com/payload').allowed === true vs hostname evil.com (flip: rejecting URL.username + @ in covers → 5/5 cases pass, all 41 existing tests green). Fragment/newline — covers=true, survivesNarrowing=['https://evil.com#.corp.com/*'], runtime allowed, hostname evil.com; after fail-closed fix on [# \n \r \t \0]: covers=false, controls unchanged. Authority boundary — both URLs isAllowed: true, hostname evil.com; E2E with a real HttpHookRunner + local HTTP server: payload received at /.corp.com/exfil; with a parse-based origin guard: validate: false, 0 requests received. Note: normalizing input to its serialized href does NOT close / and \ — the serialized href still textually matches.

Suggested fix: close the class structurally — parse the hook URL once with new URL() and enforce the pattern against parsed components (reject non-empty username, fragments and C0 control chars; compare hostname/path against the pattern's authority/path split), and lift hookUrlPatternCovers's contract to the same fetch-destination semantics (fail closed on @, #, and control characters in either pattern).

中文说明

[Critical] R17-1:白名单边界靠对原始 URL 字符串做正则测试来执行——validate()/compilePattern 如此,本 PR 新增的 hookUrlPatternCovers 门禁亦如此——但两者都没有建模 WHATWG URL 解析,因此 URL 结构 token 可以绕过它。已在本 head 上用探针验证三类 token(针对 https://*.corp.com/* 这类通配主机模式):(1) userinfo——https://x@evil.com/.corp.com/payload 校验结果为 allowed: true,而 new URL(...).hostnameevil.comfetch() 实际把负载发往该主机;hookUrlPatternCovers 还会把这类条目认证为合法收窄。(2) fragment / 换行——workspace 条目 https://evil.com#.corp.com/* 被认证为被覆盖(# 不在 regexActive 中),在 narrowWorkspaceHookSecurityOverrides 中存活并替换更高 scope 列表,运行时匹配通过,fetch 剥掉 fragment → 负载 POST 到 evil.com\n 轴同理(WHATWG 会剥掉 \n\r\t)。(3) authority/path 边界——https://evil.com/.corp.com/payloadhttps://evil.com\.corp.com/xisAllowed: true,解析主机名为 evil.com(反斜杠在 authority 中段被折叠为 /),且 covers()https://evil.com/.corp.com/* 认证在 https://*.corp.com/* 之下。这击穿了本 PR 写入 settings.ts、schema 与文档的保证——workspace 只能收窄 hook 负载的去向——并涵盖上一轮的 R14-1(原始字符串 vs 归一化 fetch)。——失败场景:user/system 白名单 ['https://*.corp.com/*'];受信但恶意的仓库添加 workspace 收窄条目 ['https://evil.com#.corp.com/*'] 及指向它的 HTTP hook → 条目存活收窄、成为生效白名单,hook 负载(prompt、工具输入/输出)POST 到 evil.com。

见证(本轮探针,未修改的 PR):userinfo——validate('https://x@evil.com/.corp.com/payload').allowed === true 而 hostname 为 evil.com(翻转:拒绝 URL.username 并把 @ 加入 covers 的 fail-closed 集 → 5/5 用例通过,现有 41 个测试全绿)。Fragment/换行——covers=truesurvivesNarrowing=['https://evil.com#.corp.com/*']、运行时放行、hostname evil.com;对 [# \n \r \t \0] fail-closed 后 covers=false,对照不变。Authority 边界——两个 URL 均 isAllowed: true、hostname evil.com;用真实 HttpHookRunner + 本地 HTTP server 的端到端:负载到达 /.corp.com/exfil;加上基于解析的 origin 守卫后 validate: false、0 请求到达。注意:把输入归一化为序列化 href 不能关闭 /\——序列化 href 在文本上仍然匹配。

修复建议:以结构化方式关闭整类问题——用 new URL() 解析一次 hook URL,对解析后的分量执行模式匹配(拒绝非空 username、fragment 与 C0 控制字符;按模式的 authority/path 切分比较 hostname/path),并把 hookUrlPatternCovers 的契约提升到同样的 fetch 目的地语义(任一模式含 @#、控制字符时 fail closed)。

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

* — are repo-controllable, so callers gate them on folder trust and this
* check fails closed.
*/
export function isTrustedSkillLevel(level: SkillLevel | undefined): boolean {

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.

[Critical] R17-2: R14-2 (thread at packages/core/src/tools/skill.test.ts:686) re-checked at this head and STILL STANDS — independently re-detected twice by this round's audit. The 'extension'-level exemption violates the gate's own fail-closed premise in the home-root topology: when the project root IS $HOME, ~/.qwen/extensions == <repo>/.qwen/extensions is repo-controlled; repo-committed extensions are discovered by the default cache refresh and auto-activate with zero user consent (ExtensionStore mints defaultActivation: 'enabled' with no store policy), and their skills/agents surface at level 'extension', where isTrustedSkillLevel skips the folder-trust gate unconditionally — homeRootShadow is only tagged for level === 'user', so the identical topology is hardened there and left open here. Repo-supplied frontmatter allowedTools become session-wide auto-approvals and frontmatter hooks register despite the untrusted folder; the subagent gate's 'extension' arm is the same surface. — Failure scenario: victim runs qwen with project root == $HOME and the folder untrusted; the repo commits .qwen/extensions/evil/ with skills/s/SKILL.md declaring allowedTools: ['Bash(*)'] and hooks → auto-active → level 'extension' → gate skipped → session-wide auto-approval of attacker-chosen tool patterns and hook registration without folder trust.

Witness: traced at HEAD ccf1a7a — skill-manager.ts extension collection feeds getActiveExtensions() entries with level: 'extension' and no homeRootShadow tag (the tagging block conditions on level === 'user' only); extension-store activation resolves effective 'enabled' with no store policy; isTrustedSkillLevel('extension') === true opens both gates (skill.ts:385-388, SkillCommandLoader.ts:164-168). Two independent audit traces this round reproduce the original thread's chain. (not run — E2E activation probe not re-executed this round; mechanism verified by trace.)

Suggested fix: extend the home-root shadow to extension-level entries — tag homeRootShadow on extension-sourced skills/agents during collection when the extensions directory lies inside the project root (inside the extension branch of listSkillsAtLevel, which returns before the current tagging block), and broaden the gate disjuncts to re-gate homeRootShadow === true regardless of level in skill.ts, SkillCommandLoader.ts, and the subagent gate.

中文说明

[Critical] R17-2:R14-2(线程位于 packages/core/src/tools/skill.test.ts:686)在本 head 复查仍然存在——本轮审计独立地两次重新发现。'extension' 级豁免在 home-root 拓扑下违反了门禁自身的 fail-closed 前提:当项目根就是 $HOME 时,~/.qwen/extensions == <repo>/.qwen/extensions 是仓库可控的;仓库提交的扩展会被默认缓存刷新发现,并在零用户同意下自动激活(无 store 策略时 ExtensionStore 铸造 defaultActivation: 'enabled'),其 skill/agent 以 'extension' 级出现,而 isTrustedSkillLevel 对该级无条件跳过文件夹信任门禁——homeRootShadow 只对 level === 'user' 打标,因此同一拓扑在 'user' 级被加固、在这里却敞开。仓库提供的 frontmatter allowedTools 成为全会话自动批准、frontmatter hooks 在不受信文件夹中照常注册;subagent 门禁的 'extension' 分支是同一表面。——失败场景:受害者以 $HOME 为项目根运行 qwen、文件夹不受信;仓库提交带 skills/s/SKILL.md(声明 allowedTools: ['Bash(*)'] 与 hooks)的 .qwen/extensions/evil/ → 自动激活 → 'extension' 级 → 门禁跳过 → 攻击者选定的工具模式获得会话级自动批准、hooks 无信任注册。

见证:已在 HEAD ccf1a7a 逐步追踪——skill-manager.ts 的扩展收集把 getActiveExtensions() 条目以 level: 'extension' 送入且无 homeRootShadow 标记(打标块仅以 level === 'user' 为条件);extension-store 在无 store 策略时解析为 effective 'enabled'isTrustedSkillLevel('extension') === true 打开两处门禁(skill.ts:385-388、SkillCommandLoader.ts:164-168)。本轮两条独立审计追踪复现了原线程的链路。(未运行——本轮未重跑端到端激活探针;机制经代码追踪核实。)

修复建议:把 home-root 标记扩展到 extension 级条目——在收集阶段,当扩展目录位于项目根内部时对 extension 来源的 skill/agent 打 homeRootShadow 标记(在 listSkillsAtLevel 的 extension 分支内——该分支在当前打标块之前就返回),并把三处门禁析取项放宽为与级别无关的 homeRootShadow === true 重门禁(skill.ts、SkillCommandLoader.ts、subagent 门禁)。

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

Comment on lines +449 to +450
merged.systemMessage = merged.systemMessage
? [merged.systemMessage, output.systemMessage]

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.

[Critical] Re-asserts @wenshao's open blocker (comment 3779654313 at :449) — verified still standing at this head; the file has not been modified since. suppressOutput is not preserved per producing message: the new concatenation appends every systemMessage unconditionally, while mergeWithOrLogic independently keeps the LAST defined suppression value (hookAggregator.ts:202-203). — Failure scenario: hook A returns { systemMessage: 'private diagnostic', suppressOutput: true }, hook B returns { systemMessage: 'visible', suppressOutput: false } → the aggregate carries BOTH messages with suppression disabled → processCommonHookOutputFields emits the private diagnostic that hook A explicitly suppressed.

Witness: re-read at HEAD ccf1a7aappendSystemMessage (hookAggregator.ts:447-455) appends output.systemMessage with no suppressOutput consultation; suppression is last-wins at :202-203; comment-status confirms the file is unchanged since the blocker was filed.

Suggested fix: filter suppressed messages before concatenation, or retain per-message suppression metadata until emission; add a mixed suppressed/visible regression test.

中文说明

[Critical] 重申 @wenshao 的未关闭 blocker(comment 3779654313,位于 :449)——已在本 head 核实仍然存在;该文件自评论以来未被修改。suppressOutput 未按消息来源分别保留:新的拼接会无条件加入每个 systemMessage,而 mergeWithOrLogic 独立采用最后一个已定义的 suppression 值(hookAggregator.ts:202-203)。——失败场景:hook A 返回 { systemMessage: 'private diagnostic', suppressOutput: true },hook B 返回 { systemMessage: 'visible', suppressOutput: false } → 聚合结果包含两条消息且 suppression 被关闭 → processCommonHookOutputFields 输出了 hook A 明确抑制的私有诊断。

见证:已在 HEAD ccf1a7a 重读——appendSystemMessage(hookAggregator.ts:447-455)追加 output.systemMessage 时不查询 suppressOutput;suppression 在 :202-203 为 last-wins;comment-status 确认该文件自 blocker 提出后未变更。

修复建议:在拼接前过滤被抑制的消息,或保留逐消息元数据直到输出阶段;增加“抑制消息 + 可见消息”的回归测试。

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

Comment on lines +447 to +448
private appendSystemMessage(merged: HookOutput, output: HookOutput): void {
if (output.systemMessage !== undefined) {

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.

[Critical] Re-asserts @wenshao's open blocker (comment 3779654318 at :452) — verified still standing at this head; the file has not been modified since. No final aggregate size limit: command and HTTP hook runners cap individual outputs (truncateOutput), but this concatenation joins an unbounded number of individually valid messages. — Failure scenario: a session with many hooks on one event, each returning a near-cap (~10KB) systemMessage → the merged systemMessage grows without bound (N × 10KB) and is logged and emitted to terminal/ACP consumers as one payload — unbounded memory and render cost from user/repo-configurable hooks.

Witness: re-read at HEAD ccf1a7aappendSystemMessage joins with no size bound; no cap exists between aggregation and the consumers (processCommonHookOutputFields is a string pass-through); file unchanged since the blocker was filed.

Suggested fix: cap while appending (track remaining budget in appendSystemMessage) or truncate the final aggregate deterministically; cover a many-hook case in hookAggregator.test.ts.

中文说明

[Critical] 重申 @wenshao 的未关闭 blocker(comment 3779654318,位于 :452)——已在本 head 核实仍然存在;该文件自评论以来未被修改。最终聚合结果无总长度上限:命令和 HTTP hook runner 只限制单个输出(truncateOutput),而此拼接会连接任意数量的合法消息。——失败场景:一个事件上挂大量 hooks、每个返回接近上限(约 10KB)的 systemMessage → 合并后的 systemMessage 无上界增长(N × 10KB),并作为一个 payload 写入日志、发送到终端/ACP 消费端——用户/仓库可配置的 hooks 带来无上界的内存与渲染开销。

见证:已在 HEAD ccf1a7a 重读——appendSystemMessage 拼接时无尺寸限制;聚合与消费端之间无任何上限(processCommonHookOutputFields 是字符串透传);文件自 blocker 提出后未变更。

修复建议:在追加时限制剩余容量(在 appendSystemMessage 中跟踪剩余配额),或对最终结果做确定性截断;在 hookAggregator.test.ts 增加多 hook 场景测试。

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

Comment on lines +265 to +266
if (response.status >= 300 && response.status < 400) {
// A redirect delivers no payload, so it must not consume a

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.

[Critical] Re-asserts @wenshao's open blocker (comment 3779654320 at :267) — verified still standing at this head; the branch is unchanged. The redirect branch uses the full 300-399 band, so 304 Not Modified is classified as a redirect: it gets the redirect warning and deletes a once hook's execution marker — the supposedly one-shot hook then sends another request on every matching event. — Failure scenario: a once: true HTTP hook whose endpoint answers 304 (caching proxies / conditional requests) → the once slot is deleted on every event → the hook re-fires on every matching event for the life of the runner, with the redirect warning treatment for a response that redirected nothing.

Witness: re-read at HEAD ccf1a7aif (response.status >= 300 && response.status < 400) at httpHookRunner.ts:265 with executedOnceHooks.delete(onceKey) at :270 inside the branch; branch unchanged since the blocker was filed.

Suggested change
if (response.status >= 300 && response.status < 400) {
// A redirect delivers no payload, so it must not consume a
if ([301, 302, 303, 307, 308].includes(response.status)) {
// A redirect delivers no payload, so it must not consume a

Let other non-2xx 3xx statuses (300/304/…) use the generic error path, and add a 304 regression test.

中文说明

[Critical] 重申 @wenshao 的未关闭 blocker(comment 3779654320,位于 :267)——已在本 head 核实仍然存在;该分支未变更。重定向分支使用完整的 300-399 区间,因此 304 Not Modified 被归类为重定向:它会收到重定向告警并删除 once hook 的执行标记——本应一次性的 hook 之后在每个匹配事件上都会再次发送请求。——失败场景:once: true 的 HTTP hook 的端点应答 304(缓存代理/条件请求)→ once 槽位每个事件都被删除 → 该 hook 在 runner 生命周期内每个匹配事件都重新触发,且对一个并未重定向的响应按重定向告警处理。

见证:已在 HEAD ccf1a7a 重读——httpHookRunner.ts:265 为 if (response.status >= 300 && response.status < 400),分支内部 :270 为 executedOnceHooks.delete(onceKey);分支自 blocker 提出后未变更。

修复建议(见上方 suggestion):把重定向分支限定为实际的 Fetch 重定向状态码(301、302、303、307、308),让其他非 2xx 的 3xx 走通用错误路径,并增加 304 回归测试。

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

Comment thread scripts/dev.js
Comment on lines +103 to +108
if (specifier === '@qwen-code/qwen-code-core/memoryScopes') {
return {
shortCircuit: true,
url: memoryScopesSourceUrl,
format: 'module',
};

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] R16-3: The remap set this hunk extends is still missing the toolWriteOrigin core subpath (re-checked this round; still stands) — the comment's own invariant ('each one cli source imports needs its own remap to the source file') remains violated. Cli source imports @qwen-code/qwen-code-core/toolWriteOrigin in exactly two files (serve/bridge-file-system-adapter.ts:62run-qwen-serve.ts:75, and acp-integration/service/filesystem.ts:25); core's exports map resolves it to dist/, which is git-ignored and absent on a fresh checkout. — Concrete cost: on a fresh checkout, npm run dev:daemon / npm run dev -- serve / ACP dev mode die with ERR_MODULE_NOT_FOUND one specifier past memoryScopes; with a stale dist/ the import silently loads built code instead of source — the exact source/dist skew the remap mechanism exists to prevent.

Witness (probe this round): a faithful copy of dev.js's post-PR loader resolves toolWriteOrigin to .../core/dist/src/services/tool-write-origin.jsERR_MODULE_NOT_FOUND on a fresh-checkout model; adding the remap → LOADED OK. Mechanical sweep: cli source imports exactly 3 core subpaths (envVarResolver ×3, toolWriteOrigin ×2, memoryScopes ×1) — 1 of 3 lacks the remap.

const toolWriteOriginSourceUrl = pathToFileURL(
  join(root, 'packages', 'core', 'src', 'services', 'tool-write-origin.ts'),
).href;
// and in the loader, beside the block above:
if (specifier === '@qwen-code/qwen-code-core/toolWriteOrigin') {
  return { shortCircuit: true, url: toolWriteOriginSourceUrl, format: 'module' };
}

Note: the transcriptRecords/goalWire subpaths consumed through acp-bridge dist (open thread R14-5) remain unresolved by this fix.

中文说明

[Suggestion] R16-3:本 hunk 扩展的 remap 集合仍然缺少 toolWriteOrigin core 子路径(本轮复查确认仍然存在)——注释自带的不变量(“cli source 引入的每一个子路径都需要一个指向源文件的 remap”)依旧被违反。cli source 在两个文件中引入 @qwen-code/qwen-code-core/toolWriteOriginserve/bridge-file-system-adapter.ts:62run-qwen-serve.ts:75,以及 acp-integration/service/filesystem.ts:25);core 的 exports map 把它解析到 git-ignored、fresh checkout 上不存在的 dist/。——具体代价:fresh checkout 上 npm run dev:daemon / npm run dev -- serve / ACP dev 模式会在 memoryScopes 之后一个 specifier 处以 ERR_MODULE_NOT_FOUND 失败;带着过期 dist/ 时则悄悄加载构建产物而非源码——正是 remap 机制要避免的 source/dist 偏差。

见证(本轮探针):dev.js 改动后 loader 的忠实副本把 toolWriteOrigin 解析到 .../core/dist/src/services/tool-write-origin.js → fresh-checkout 模型下 ERR_MODULE_NOT_FOUND;加上 remap 后 → LOADED OK。机械扫描:cli source 恰好引入 3 个 core 子路径(envVarResolver ×3、toolWriteOrigin ×2、memoryScopes ×1)——3 缺 1。

修复:见上方代码块(新增 toolWriteOriginSourceUrl 常量与 loader 分支)。注意:经 acp-bridge dist 消费的 transcriptRecords/goalWire 子路径(未关闭线程 R14-5)此修复不覆盖。

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

Comment on lines +37 to +38
// literal would turn a secret *name* into a public, guessable value.
if (isInternalSecretEnvVar(envName)) {

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] R17-7: No test pins the $$-escape-vs-denylist ordering. The new denylist checks in resolveEnvVars and resolveConfigEnvVar are deliberately placed AFTER the if (value.startsWith('$$')) return value.substring(1) escape (resolveWebhookSecretEnv takes a bare name — no escape, fail-closed), but the only escape test uses a non-secret name ('$$literal-token') and all new denylist tests use single-$ references. — Concrete cost: a future refactor consolidating the three duplicated denylist blocks can hoist the check above the escape with every existing test still green; after that, a channel config holding a literal value spelled $$QWEN_SERVER_TOKEN — a previously valid, explicitly-literal configuration — aborts at config-parse time instead of resolving to the literal $QWEN_SERVER_TOKEN.

Witness (probe this round): resolveEnvVars('$$QWEN_SERVER_TOKEN') returns '$QWEN_SERVER_TOKEN' at HEAD. Flip: hoisting a denylist check above the escape makes the probe FAIL while all 54 existing config-utils tests stay green (1 failed | 54 passed under mutation) — the unguarded fragility, demonstrated; mutation reverted, tree clean.

// in the new internal-secret denylist describe:
expect(resolveEnvVars('$$QWEN_SERVER_TOKEN')).toBe('$QWEN_SERVER_TOKEN');
// plus the parseChannelConfig equivalent through a credential field
中文说明

[Suggestion] R17-7:没有测试钉住 $$ 转义与内部 secret 拒绝名单的顺序。resolveEnvVarsresolveConfigEnvVar 中新增的拒绝检查刻意放在 if (value.startsWith('$$')) return value.substring(1) 转义之后(resolveWebhookSecretEnv 接收裸变量名——无转义、fail-closed),但唯一的转义测试用的是非 secret 名('$$literal-token'),所有新的拒绝名单测试都用单 $ 引用。——具体代价:未来把三处重复拒绝块合并的重构可以把检查提升到转义之前而所有现有测试仍绿;此后,字面值写作 $$QWEN_SERVER_TOKEN 的 channel 配置——此前合法的显式字面量配置——会在配置解析时中止,而不是解析为字面量 $QWEN_SERVER_TOKEN

见证(本轮探针):HEAD 上 resolveEnvVars('$$QWEN_SERVER_TOKEN') 返回 '$QWEN_SERVER_TOKEN'。翻转:把拒绝检查提升到转义之前会使探针失败,而现有 54 个 config-utils 测试全部仍绿(变异下 1 failed | 54 passed)——无防护的脆弱性已被演示;变异已还原,工作区干净。

修复:在新的 internal-secret denylist describe 中增加 expect(resolveEnvVars('$$QWEN_SERVER_TOKEN')).toBe('$QWEN_SERVER_TOKEN')(以及通过凭据字段的 parseChannelConfig 等价用例),钉住转义在两处都先于拒绝名单短路。

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

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/needs-human The autofix loop stopped on this PR — a human must re-arm, split, merge, or close it label Aug 15, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔓 Takeover auto-released: the autofix loop paused on this PR 9 day(s) ago (🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this ) and no re-arm followed, so the autofix/takeover label is removed to keep the managed pool honest. autofix/needs-human stays as the reminder that this PR needs a human decision: merge it, close it, or split/reduce it and comment @qwen-code /takeover to re-engage with a fresh round window.

中文说明

🔓 已自动释放接管:autofix 循环在 9 天前暂停于此 PR(🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this ),此后无人重新武装,现移除 autofix/takeover 标签以保持托管池真实可用。保留 autofix/needs-human 作为待办提醒 —— 本 PR 需要人工决策:合并、关闭,或拆分/缩小后评论 @qwen-code /takeover 以全新轮次窗口重新接管。

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

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

Partially reviewed — gaps disclosed.

3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R15-3 all-non-string-array coerced to silent allow-all without warning (config.ts:2348) — already reported (comment 3728764862)
  • R13-8 allowedTools trust-gating describe omits the homeRootShadow/worktree topology cases its sibling hooks describe pins (skill.test.ts) — already reported (comments 3722849940, 3725050104)
  • R13-2 text-capture.tsx envVarResolver remap unreachable by any test command — already reported (comments 3722849891, 3725050100)

Unresolved, please confirm:

  • [Critical] R16-2 one-directional gate liveness (packages/core/src/tools/skill.ts:609 thread): the trusted→untrusted revocation half is factually absent at this head (re-verified this round: unregisterSkillHooks is a no-op stub with zero call sites, cl…

Not reviewed: reverse audit — reached the 5-round cap without two consecutive dry rounds (rounds 3, 4 and 5 each reported findings; the loop ended at the cap, not by convergence).

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows.

Not explored to full depth (tool budget reached): chunk 6: execute packages/core vitest for envInterpolator.test.ts and hookAggregator.test.ts — node_modules is not installed in the review worktree or parent check….

Test Plan (not a blocker): 1385 tests green — this review observed 21293, 20381, 494, 1541, 1597, 3723, 529 passed.

Deferred under the convergence posture (round 18, not a blocker) — recorded, not requested in this round:

  • scripts/dev.js:110 — [probe] The dev.js subpath remap set is still missing…
  • packages/cli/src/commands/channel/config-utils.ts:38 — [review] No test pins the $$-escape-vs-denylist ordering
  • packages/core/src/hooks/httpHookRunner.ts:269 — [probe] Once-slot re-arm fires only for 3xx; connection…
  • scripts/dev.js:103 — [probe] Nothing pins the dev.js subpath remap table to cli's…
  • packages/core/src/hooks/httpHookRunner.ts:270 — [probe] Unconditional 3xx re-arm turns a once hook behind a…
  • packages/core/src/skills/skill-manager.ts:1068 — [probe] homeRootShadow over-tags user-level skills loaded from…
  • packages/core/src/skills/skill-manager.test.ts:783 — [probe] New homeRootShadow tests fail whenever QWEN_HOME is set in…
  • packages/core/src/hooks/httpHookRunner.ts:287 — [probe] Redirect-warning systemMessage exceeds the 10,000-char cap…
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。

未审查:reverse audit — reached the 5-round cap without two consecutive dry rounds (rounds 3, 4 and 5 each reported findings; the loop ended at the cap, not by convergence)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI; the case-insensitive env-denylist paths this PR adds are only exercisable on Windows。

未探索到全部深度(达到工具调用预算):chunk 6:execute packages/core vitest for envInterpolator.test.ts and hookAggregator.test.ts — node_modules is not installed in the review worktree or parent check…

Test Plan(非阻断):1385 tests green — this review observed 21293, 20381, 494, 1541, 1597, 3723, 529 passed

收敛姿态下延后(第 18 轮,非阻断)——已记录,本轮不要求修改:共 8 条(原文未翻译,列表见上方英文部分)。

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

Comment on lines +519 to +523
if (narrowed.length > 0) {
restSecurity.allowedHttpHookUrls = narrowed;
} else {
delete restSecurity.allowedHttpHookUrls;
}

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.

[Critical] R16-1: The workspace-whitelist narrowing added here breaks a pre-existing, unmodified test — the packages/cli suite is red at this head (re-measured this round; still stands). narrowWorkspaceHookSecurityOverrides deliberately discards a workspace-scoped security.allowedHttpHookUrls when no higher-scope whitelist exists, but the pre-existing test allowedInsecureVoiceBaseUrls scope handling > should strip and warn about the allowlist from workspace scope (settings.test.ts:3776, assertion at :3797) still asserts the old pass-through semantics. The sibling stale test was updated; this one was missed. — Failure scenario: workspace settings define allowedHttpHookUrls: ['https://hooks.example.com/*'] with no User/System/SystemDefaults whitelist → higherUrls === undefined → narrowed = [] → delete → merged is undefinedAssertionError: expected undefined to deeply equal ['https://hooks.example.com/*']npm test --workspace=packages/cli and CI fail.

Witness (this round): name-level junit A/B — the test fails in isolation on the PR head (AssertionError: expected undefined to deeply equal [ 'https://hooks.example.com/*' ]) and passes at merge base 259951c5 in the identical environment; it is the only net-new failure of 97 (the other 96 fail identically on base — environmental QWEN_HOME/home-path mismatch, gone under env -u QWEN_HOME). The discard is documented, intended behavior; the stale test is the overlooked artifact. (Raised at Critical over the file-level test-delta hold, which is confounded by that environmental cluster; both sides quoted above.)

Suggested change
if (narrowed.length > 0) {
restSecurity.allowedHttpHookUrls = narrowed;
} else {
delete restSecurity.allowedHttpHookUrls;
}
expect(settings.merged.security?.allowedHttpHookUrls).toBeUndefined();
中文说明

[Critical] R16-1:此处新增的 workspace 白名单收窄逻辑破坏了一个既有且未被本 PR 修改的测试——packages/cli 测试套件在当前 head 上是红的(本轮重新实测,仍然存在)。narrowWorkspaceHookSecurityOverrides 在更高 scope 未设置白名单时会刻意丢弃 workspace scope 的 security.allowedHttpHookUrls,但既有测试 allowedInsecureVoiceBaseUrls scope handling > should strip and warn about the allowlist from workspace scope(settings.test.ts:3776,断言位于 :3797)仍在断言旧的直通合并语义。同类的另一个过时测试已更新,这一个被遗漏了。——失败场景:workspace 设置定义 allowedHttpHookUrls: ['https://hooks.example.com/*'] 且 User/System/SystemDefaults 均未设置白名单 → higherUrls === undefined → narrowed = [] → delete → 合并结果为 undefined → 断言失败 → npm test --workspace=packages/cli 与 CI 失败。

见证(本轮):测试名级 junit A/B——该测试在 PR 分支上单独运行即失败(AssertionError: expected undefined to deeply equal [ 'https://hooks.example.com/*' ]),在合并基 259951c5 的相同环境下通过;是 97 个失败中唯一净新增的(其余 96 个在 base 上同样失败——属环境性 QWEN_HOME/home 路径不匹配,env -u QWEN_HOME 后消失)。丢弃行为本身是有文档记载的预期行为,被遗漏的是这个过时的测试。(file 级 test-delta hold 被该环境性失败簇混淆,故按测试名级双侧证据恢复为 Critical。)

修复:把 settings.test.ts:3797 的断言更新为新语义(见上方 suggestion)。

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

Comment on lines +214 to +217
export function hookUrlPatternCovers(
outerPattern: string,
innerPattern: string,
): boolean {

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.

[Critical] R17-1: The whitelist boundary is enforced by regex-testing the RAW URL string — both in validate()/compilePattern and in this new hookUrlPatternCovers gate — but neither models WHATWG URL parsing, so URL-structure tokens bypass it. Probe-verified in round 17, three token families (with wildcard-host patterns such as https://*.corp.com/*): (1) userinfohttps://x@evil.com/.corp.com/payload validates allowed: true while new URL(...).hostname is evil.com, where fetch() sends the payload; hookUrlPatternCovers also certifies such entries as legitimate narrowings. (2) fragment / newline — a workspace entry https://evil.com#.corp.com/* is certified as covered (# is not in regexActive), survives narrowWorkspaceHookSecurityOverrides and REPLACES the higher-scope list, matches at runtime, and fetch strips the fragment → payload POSTs to evil.com; the \n axis behaves the same (WHATWG strips \n\r\t). (3) authority/path boundaryhttps://evil.com/.corp.com/payload and https://evil.com\.corp.com/x are both isAllowed: true with parsed hostname evil.com (backslash folds to / mid-authority), and covers() certifies https://evil.com/.corp.com/* under https://*.corp.com/*. This defeats the guarantee this PR writes into settings.ts, the schema, and the docs — that a workspace may only NARROW where hook payloads may be sent — and subsumes prior-round R14-1. Re-checked this round: the branch code is unchanged since the round-17 probes (only a main-merge commit), and no @/#/control-char guard exists at HEAD. — Failure scenario: user/system whitelist ['https://*.corp.com/*']; a trusted-but-malicious repo adds the workspace narrowing entry ['https://evil.com#.corp.com/*'] plus an HTTP hook targeting it → the entry survives narrowing, becomes the effective whitelist, and the hook payload (prompts, tool inputs/outputs) POSTs to evil.com.

Witness: round-17 probes (branch unchanged since): userinfo — validate('https://x@evil.com/.corp.com/payload').allowed === true vs hostname evil.com (flip: rejecting URL.username + @ in covers → 5/5 cases pass, all 41 existing tests green); fragment — covers=true, entry survives narrowing, runtime allows, hostname evil.com; authority boundary — both URLs isAllowed: true, hostname evil.com; E2E with a real HttpHookRunner + local HTTP server: payload received at /.corp.com/exfil; with a parse-based origin guard: validate: false, 0 requests received.

Suggested fix: close the class structurally — parse the hook URL once with new URL() and enforce the pattern against parsed components (reject non-empty username, fragments and C0 control chars; compare hostname/path against the pattern's authority/path split), and lift hookUrlPatternCovers's contract to the same fetch-destination semantics (fail closed on @, #, and control characters in either pattern). Note: normalizing input to its serialized href does NOT close / and \ — the serialized href still textually matches.

中文说明

[Critical] R17-1:白名单边界靠对原始 URL 字符串做正则测试来执行——validate()/compilePattern 如此,本 PR 新增的 hookUrlPatternCovers 门禁亦如此——但两者都没有建模 WHATWG URL 解析,因此 URL 结构 token 可以绕过它。已在第 17 轮用探针验证三类 token(针对 https://*.corp.com/* 这类通配主机模式):(1) userinfo——https://x@evil.com/.corp.com/payload 校验结果为 allowed: true,而 new URL(...).hostnameevil.comfetch() 实际把负载发往该主机;hookUrlPatternCovers 还会把这类条目认证为合法收窄。(2) fragment / 换行——workspace 条目 https://evil.com#.corp.com/* 被认证为被覆盖(# 不在 regexActive 中),在收窄中存活并替换更高 scope 列表,运行时匹配通过,fetch 剥掉 fragment → 负载 POST 到 evil.com\n 轴同理(WHATWG 会剥掉 \n\r\t)。(3) authority/path 边界——https://evil.com/.corp.com/payloadhttps://evil.com\.corp.com/xisAllowed: true,解析主机名为 evil.com(反斜斜杠在 authority 中段被折叠为 /),且 covers()https://evil.com/.corp.com/* 认证在 https://*.corp.com/* 之下。这击穿了本 PR 写入 settings.ts、schema 与文档的保证——workspace 只能收窄 hook 负载的去向——并涵盖上一轮的 R14-1。本轮复查:分支代码自第 17 轮探针以来未变(仅合并 main),HEAD 上仍无 @/#/控制字符守卫。——失败场景:user/system 白名单 ['https://*.corp.com/*'];受信但恶意的仓库添加 workspace 收窄条目 ['https://evil.com#.corp.com/*'] 及指向它的 HTTP hook → 条目存活收窄、成为生效白名单,hook 负载(prompt、工具输入/输出)POST 到 evil.com。

见证:第 17 轮探针(分支此后未变):userinfo——validate('https://x@evil.com/.corp.com/payload').allowed === true 而 hostname 为 evil.com(翻转:拒绝 URL.username 并把 @ 加入 covers 的 fail-closed 集 → 5/5 用例通过,现有 41 个测试全绿);fragment——covers=true、条目存活收窄、运行时放行、hostname evil.com;authority 边界——两个 URL 均 isAllowed: true、hostname evil.com;真实 HttpHookRunner + 本地 HTTP server 的端到端:负载到达 /.corp.com/exfil;加上基于解析的 origin 守卫后 validate: false、0 请求到达。

修复建议:以结构化方式关闭整类问题——用 new URL() 解析一次 hook URL,对解析后的分量执行模式匹配(拒绝非空 username、fragment 与 C0 控制字符;按模式的 authority/path 切分比较 hostname/path),并把 hookUrlPatternCovers 的契约提升到同样的 fetch 目的地语义(任一模式含 @#、控制字符时 fail closed)。注意:把输入归一化为序列化 href 不能关闭 /\——序列化 href 在文本上仍然匹配。

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

Comment on lines 202 to +205
if (output.suppressOutput !== undefined) {
merged.suppressOutput = output.suppressOutput;
}
if (output.systemMessage !== undefined) {
merged.systemMessage = output.systemMessage;
}
this.appendSystemMessage(merged, output);

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.

[Critical] R17-3: Re-asserts @wenshao's open blocker (comment 3779654313 at :449) — verified still standing at this head; the file has not been modified since. suppressOutput is not preserved per producing message: the new concatenation appends every systemMessage unconditionally (appendSystemMessage, :447-455), while mergeWithOrLogic independently keeps the LAST defined suppression value (these lines). For example, { systemMessage: 'private diagnostic', suppressOutput: true } followed by { systemMessage: 'visible', suppressOutput: false } aggregates to both messages with suppression disabled; processCommonHookOutputFields then emits the private diagnostic too. — Failure scenario: two hooks on one event, the first returning a suppressed diagnostic, the second a visible message with suppressOutput: false → the merged output carries both messages with suppression off → content the producing hook explicitly suppressed reaches the user/ACP consumers.

Witness: code read at HEAD ad00d83if (output.suppressOutput !== undefined) { merged.suppressOutput = output.suppressOutput; } (last-defined wins) immediately followed by this.appendSystemMessage(merged, output) appending every message regardless of suppression.

Suggested fix: filter suppressed messages before concatenation (skip the append when output.suppressOutput === true), or retain per-message metadata until emission; add a mixed suppressed/visible regression test.

中文说明

[Critical] R17-3:重申 @wenshao 的未决 blocker(comment 3779654313,位于 :449)——已在本 head 核实仍然存在;该文件自那以后未修改。suppressOutput 未按消息来源分别保留:新的拼接逻辑无条件加入每个 systemMessageappendSystemMessage,:447-455),而 mergeWithOrLogic 独立采用最后一个已定义的 suppression 值(即这几行)。例如先返回 { systemMessage: 'private diagnostic', suppressOutput: true },再返回 { systemMessage: 'visible', suppressOutput: false },聚合结果会包含两条消息且 suppression 被关闭,processCommonHookOutputFields 最终连私有诊断一并输出。——失败场景:同一事件上两个 hooks,第一个返回被抑制的诊断、第二个返回 suppressOutput: false 的可见消息 → 聚合输出携带两条消息且抑制关闭 → 产生方明确抑制的内容到达用户/ACP 消费端。

见证:HEAD ad00d83 代码阅读——last-wins 的 suppressOutput 赋值之后紧跟无条件的 appendSystemMessage

修复建议:拼接前过滤被抑制的消息(output.suppressOutput === true 时跳过追加),或保留逐消息元数据直到输出阶段;增加“抑制 + 可见”混合回归测试。

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

* Append an output's systemMessage to merged, concatenating so a
* one-shot message from an earlier hook survives later outputs.
*/
private appendSystemMessage(merged: HookOutput, output: HookOutput): void {

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.

[Critical] R17-4: Re-asserts @wenshao's open blocker (comment 3779654318 at :452) — verified still standing at this head; the file has not been modified since. No final aggregate size limit: command and HTTP hook runners cap individual outputs (truncateOutput), but this concatenation joins an unbounded number of individually valid messages; a large hook set can therefore create a multi-megabyte systemMessage that is logged and emitted to terminal/ACP consumers as one payload. — Failure scenario: a session with many hooks on one event, each returning a near-cap (~10 KB) systemMessage → the merged systemMessage grows without bound → multi-MB payload logged and rendered/emitted in one piece.

Witness: code read at HEAD ad00d83appendSystemMessage joins [merged.systemMessage, output.systemMessage] with no length check anywhere in the three merge paths; truncateOutput is applied by individual producers only.

Suggested fix: cap while appending or truncate the final aggregate deterministically (apply truncateOutput to the merged systemMessage after the loop), and cover a many-hook case.

中文说明

[Critical] R17-4:重申 @wenshao 的未决 blocker(comment 3779654318,位于 :452)——已在本 head 核实仍然存在;该文件自那以后未修改。最终聚合没有总长度上限:命令与 HTTP hook runner 只限制单个输出(truncateOutput),而此处拼接可以连接任意数量的合法消息;大量 hooks 因此能生成数 MB 的 systemMessage,并作为单个 payload 写入日志、发送到终端/ACP 消费端。——失败场景:一个事件上挂多个 hooks,每个都返回接近上限(约 10 KB)的 systemMessage → 合并后的 systemMessage 无限增长 → 数 MB 负载被整体记录/渲染/发出。

见证:HEAD ad00d83 代码阅读——appendSystemMessage 在三条合并路径中均无长度检查地拼接;truncateOutput 仅由各个产生方单独应用。

修复建议:在追加时限制剩余容量,或对最终结果做确定性截断(循环结束后对合并值应用 truncateOutput),并增加多 hook 场景测试。

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

// Per Qwen Code spec: Non-2xx status is a non-blocking error
// Execution continues, but we log a warning
if (!response.ok) {
if (response.status >= 300 && response.status < 400) {

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.

[Critical] R17-5: Re-asserts @wenshao's open blocker (comment 3779654320 at :267) — verified still standing at this head; the branch is unchanged. The redirect branch uses the full 300-399 band, so 304 Not Modified is classified as a redirect: it gets the redirect warning and deletes a once hook's execution marker — the supposedly one-shot hook then sends another request on every matching event. — Failure scenario: an endpoint returns 304 Not Modified → the branch deletes the once hook's executedOnceHooks slot → the one-shot hook re-fires on every matching event instead of running exactly once.

Witness: code read at HEAD ad00d83if (response.status >= 300 && response.status < 400) (the full band, no 304 exclusion); the slot delete at :269-271 runs for every status in it.

Suggested fix: restrict this branch to actual Fetch redirect statuses (301, 302, 303, 307, 308); let other non-2xx 3xx statuses use the generic error path, and add a 304 regression test.

中文说明

[Critical] R17-5:重申 @wenshao 的未决 blocker(comment 3779654320,位于 :267)——已在本 head 核实仍然存在;该分支未变。重定向分支使用完整的 300-399 区间,因此 304 Not Modified 也被当作重定向:输出重定向警告并删除 once hook 的执行标记——本应只执行一次的 hook 会在后续每个匹配事件中再次发送请求。——失败场景:端点返回 304 Not Modified → 分支删除该 once hook 的 executedOnceHooks 槽位 → 一次性 hook 在每个匹配事件上重复触发,而不是恰好执行一次。

见证:HEAD ad00d83 代码阅读——if (response.status >= 300 && response.status < 400)(完整区间,未排除 304);:269-271 的槽位删除对该区间内所有状态码执行。

修复建议:把该分支限制为 Fetch 实际识别的重定向状态码(301302303307308),其他非 2xx 的 3xx 状态走通用错误路径,并增加 304 回归测试。

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

Comment on lines +385 to +388
const sideEffectsGated =
(!isTrustedSkillLevel(skill.level) ||
(skill.level === 'user' && skill.homeRootShadow === true)) &&
!this.config.isTrustedFolder();

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.

[Critical] R15-6: The 'user'-level gate exemption is bypassed when QWEN_HOME (or a relative settings.skills.directories entry) resolves inside the project root — re-checked at this head and STILL STANDS (probe-verified this round; previously reported in round 15, comment 3728764835, then dropped from the round-17 ledger). The gates exempt 'user'-level skills on the premise that they live in ~/.qwen, but Storage.getGlobalQwenDir() honors QWEN_HOME (storage.ts:183-194; relative values resolve against cwd) and custom skill dirs are resolved against the working directory (skill-manager.ts:930-943), both surfacing at 'user' level. homeRootShadow is only tagged when path.resolve(projectRoot) === path.resolve(os.homedir()) (skill-manager.ts:1062-1071), which that geometry falsifies — so repo-controlled skills surface at 'user' level UNTAGGED and skip this gate and the SkillCommandLoader gate. The repo cannot inject skills.directories itself (workspace settings are dropped while untrusted), but a victim-side QWEN_HOME needs no config injection at all — direnv/devcontainer conventions set it, and this repo's own automation uses a qwen-home dir pattern. The mirrored gate in packages/cli/src/services/SkillCommandLoader.ts:163-169 has the same shape. — Failure scenario: QWEN_HOME=<repo>/.qwen-home$QWEN_HOME/skills/evil/SKILL.md with frontmatter hooks + allowedTools: ['Bash(curl *)'] lists at 'user' level untagged → both gates exempt → repo-supplied hooks register and allowedTools grant session-wide auto-approvals in an UNTRUSTED folder.

Witness (probe this round, HEAD ad00d83): [PROBE-R15-6] {"level":"user","filePath":".../.qwen-home/skills/evil/SKILL.md","allowedTools":["Bash(curl *)"],"hasHooks":true} with sideEffectsGated = false in an untrusted folder. Flip check: patching the tag to source-dir containment sets homeRootShadow: true and the probe's bypass assertions fail (patch reverted, tree clean).

Suggested fix: tag by provenance — in listSkillsAtLevel('user'), set the shadow flag for any skill whose resolved base dir lies inside path.resolve(projectRoot) (covers both custom-dir entries and a repo-interior QWEN_HOME); mirror in listSubagentsAtLevel (R15-9).

中文说明

[Critical] R15-6:当 QWEN_HOME(或相对的 settings.skills.directories 条目)解析到项目根目录内部时,'user' 级门禁豁免被绕过——在本 head 复查仍然存在(本轮探针验证;第 15 轮已报告,comment 3728764835,随后从第 17 轮 ledger 中丢失)。门豁免 'user' 级 skill 的前提是它们位于 ~/.qwen,但 Storage.getGlobalQwenDir() 会遵循 QWEN_HOME(storage.ts:183-194;相对值按 cwd 解析),自定义 skill 目录也按工作目录解析(skill-manager.ts:930-943),两者都以 'user' 级出现。homeRootShadow 仅在 path.resolve(projectRoot) === path.resolve(os.homedir()) 时打标(skill-manager.ts:1062-1071),而上述几何形状恰好使其为假——于是仓库可控的 skill 以未打标'user' 级出现,同时绕过此门禁与 SkillCommandLoader 门禁。仓库自身无法注入 skills.directories(不受信时 workspace 设置被整体丢弃),但受害端的 QWEN_HOME 无需任何配置注入——direnv/devcontainer 约定会设置它,本仓库自己的自动化也使用 qwen-home 目录模式。packages/cli/src/services/SkillCommandLoader.ts:163-169 的镜像门禁同样如此。——失败场景:QWEN_HOME=<repo>/.qwen-home$QWEN_HOME/skills/evil/SKILL.md(frontmatter 声明 hooks + allowedTools: ['Bash(curl *)'])以未打标的 'user' 级被列出 → 两处门禁均豁免 → 仓库提供的 hooks 在不受信文件夹中注册、allowedTools 获得会话级自动批准。

见证(本轮探针,HEAD ad00d83):不受信文件夹下 [PROBE-R15-6] {"level":"user","filePath":".../.qwen-home/skills/evil/SKILL.md","allowedTools":["Bash(curl *)"],"hasHooks":true}sideEffectsGated = false。翻转验证:把打标改为按源目录包含关系后 homeRootShadow: true,探针的绕过断言失败(补丁已还原,工作区干净)。

修复建议:按来源打标——在 listSkillsAtLevel('user') 中,对解析后基础目录位于 path.resolve(projectRoot) 内部的 skill 设置 shadow 标记(同时覆盖自定义目录条目与仓库内部的 QWEN_HOME);并在 listSubagentsAtLevel 做镜像处理(R15-9)。

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

Comment on lines +944 to +948
const trustedAgentLevel =
(config.level === 'user' && config.homeRootShadow !== true) ||
config.level === 'builtin' ||
config.level === 'extension' ||
config.level === 'session';

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.

[Critical] R15-9: Subagent side of R15-6 (same root cause) — re-checked at this head and STILL STANDS (probe-verified this round; previously reported in round 15, comments 3728764842 / 3773026779, then dropped from the round-17 ledger). The 'user' arm of this allowlist assumes the user agents directory is ~/.qwen, but Storage.getGlobalQwenDir() honors QWEN_HOME (storage.ts:183-193; relative values resolve against cwd) and listSubagentsAtLevel('user') reads getGlobalQwenDir()/agents (:1368-1371). When QWEN_HOME resolves inside the project root (e.g. a direnv/devcontainer convention), repo-controlled agents surface at 'user' level WITHOUT the homeRootShadow tag — the tag is set only when path.resolve(projectRoot) === path.resolve(os.homedir()) (:1402-1406), which that geometry falsifies — and this gate's 'user' arm trusts them unconditionally. — Failure scenario: QWEN_HOME=.qwen-home (relative, inside the repo) → $QWEN_HOME/agents/evil.md with frontmatter hooks lists at 'user' level untagged → trustedAgentLevel === true → repo-supplied agent hooks register in an untrusted folder.

Witness (probe this round, HEAD ad00d83): [PROBE-R15-9] {"level":"user","filePath":".../.qwen-home/agents/evil-agent.md","hasHooks":true} with trustedAgentLevel = true. Flip check: the mirrored containment patch sets homeRootShadow: true and the probe fails (patch reverted, tree clean).

Suggested fix: mirror the R15-6 containment tag in listSubagentsAtLevel: tag agents whose resolved dir lies inside path.resolve(projectRoot).

中文说明

[Critical] R15-9:R15-6 的 subagent 侧(同一根因)——在本 head 复查仍然存在(本轮探针验证;第 15 轮已报告,comments 3728764842 / 3773026779,随后从第 17 轮 ledger 中丢失)。此允许清单的 'user' 分支假设用户级 agents 目录是 ~/.qwen,但 Storage.getGlobalQwenDir() 会遵循 QWEN_HOME(storage.ts:183-193;相对值按 cwd 解析),而 listSubagentsAtLevel('user') 读取 getGlobalQwenDir()/agents(:1368-1371)。当 QWEN_HOME 解析到项目根目录内部(例如 direnv/devcontainer 约定)时,仓库可控的 agents 会以 'user' 级别出现且不带 homeRootShadow 标记——该标记只在 path.resolve(projectRoot) === path.resolve(os.homedir()) 时设置(:1402-1406),上述几何形状恰好使其为假——而此门禁的 'user' 分支无条件信任它们。——失败场景:QWEN_HOME=.qwen-home(相对路径,位于仓库内)→ $QWEN_HOME/agents/evil.md(frontmatter 声明 hooks)以未打标的 'user' 级被列出 → trustedAgentLevel === true → 仓库提供的 agent hooks 在不受信文件夹中被注册。

见证(本轮探针,HEAD ad00d83):[PROBE-R15-9] {"level":"user","filePath":".../.qwen-home/agents/evil-agent.md","hasHooks":true}trustedAgentLevel = true。翻转验证:镜像的包含关系打标补丁使 homeRootShadow: true,探针失败(补丁已还原,工作区干净)。

修复建议:在 listSubagentsAtLevel 镜像 R15-6 的包含关系打标:对解析目录位于 path.resolve(projectRoot) 内部的 agent 打标。

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/needs-human The autofix loop stopped on this PR — a human must re-arm, split, merge, or close it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants