Skip to content

feat: add hybrid code mode - #11854

Open
DragonnZhang wants to merge 32 commits into
mainfrom
dragon/add-codemode
Open

DragonnZhang wants to merge 32 commits into
mainfrom
dragon/add-codemode

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a Codex-aligned tools.mode enum with direct, code_mode, and code_mode_only. code_mode keeps ordinary tools directly callable while also exposing the isolated exec JavaScript tool; each visible ordinary tool carries its nested JavaScript declaration, while exec retains compact ALL_TOOLS metadata. code_mode_only preserves the strict exec-only ordinary-tool surface, and direct remains the default.

Updates filtered subagent tool surfaces so direct declarations and nested exec bindings honor the same allowlists, exposes the enum in Settings as Default / Code Mode / Code Mode Only, regenerates the settings JSON schema, and removes the former tools.codeModeOnly boolean setting entirely.

Adds synchronized English and Chinese design documents, and brings the existing CodeModeOnly design up to the repository's bilingual documentation standard (English, Chinese).

Why it's needed

Qwen Code previously offered only the default direct tool surface or the strict CodeModeOnly surface. Models could not combine direct calls for simple operations with exec orchestration for multi-tool workflows. A three-value enum matches Codex's ToolMode naming and gives users one explicit setting for all supported exposure modes.

Reviewer Test Plan

How to verify

  1. Leave tools.mode unset or set it to direct, restart Qwen Code, and confirm ordinary tools remain available while exec is absent.
  2. Set tools.mode to code_mode, restart, and confirm both ordinary tools and exec are available. Confirm ordinary tool descriptions include their tools.<name>(args) declaration and deferred tools remain discoverable through tool_search.
  3. Set tools.mode to code_mode_only, restart, and confirm ordinary tools are available through exec rather than as top-level calls, while direct control tools remain available.
  4. Open Settings and confirm Tool Mode cycles among Default, Code Mode, and Code Mode Only. Confirm tools.codeModeOnly is no longer present in the settings schema.
  5. Start with --safe-mode or --bare while either code mode is configured and confirm the effective mode is direct.

Automated verification completed: focused Core tests (121 tests), focused CLI tests (585 tests), a repeated settings-schema regression run (60 tests), full build, full typecheck, full lint, and both installed and branch-built CLI version smoke checks.

Evidence (Before & After)

Before: Settings exposed Code Mode Only (Experimental) as a boolean with false / true.

After: Settings exposes Tool Mode (Experimental) as an enum displaying Default / Code Mode / Code Mode Only. Updated TUI snapshots cover the default display.

Tested on

OS Status
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS, Node.js v24.18.0; local source build and package-level Vitest suites.

Risk & Scope

  • Main risk or tradeoff: Hybrid mode adds a short nested-call declaration to each ordinary tool description, increasing prompt size slightly; complete schemas are not duplicated in the exec description.
  • Not validated / out of scope: Live-provider interactive runs and Windows/Linux local execution were not tested; CI covers supported platforms.
  • Breaking changes / migration notes: tools.codeModeOnly is removed. Replace { "tools": { "codeModeOnly": true } } with { "tools": { "mode": "code_mode_only" } }.

Linked Issues

Related to #10377.

中文说明

本 PR 做了什么

新增与 Codex 对齐的 tools.mode 枚举,支持 direct、code_mode 和 code_mode_only。code_mode 在保留普通工具直接调用能力的同时提供隔离的 exec JavaScript 工具;每个可见普通工具会携带自己的嵌套 JavaScript 声明,而 exec 保留精简的 ALL_TOOLS 元数据。code_mode_only 保持严格的普通工具仅经 exec 调用模式,direct 仍为默认值。

更新经过过滤的子智能体工具面,使直接声明和 exec 嵌套 binding 遵守同一组 allowlist;在设置界面中将该枚举显示为 Default / Code Mode / Code Mode Only;重新生成 settings JSON schema;并彻底移除原来的 tools.codeModeOnly 布尔设置。

新增同步的英文和中文设计文档,并将现有 CodeModeOnly 设计补齐为仓库要求的双语文档(英文、中文)。

为什么需要

Qwen Code 此前只提供默认的直接工具面或严格的 CodeModeOnly 工具面。模型无法在简单操作中使用直接调用,同时在多工具工作流中使用 exec 编排。三值枚举与 Codex 的 ToolMode 命名一致,并为所有支持的暴露模式提供一个明确的统一设置项。

Reviewer 测试计划

如何验证

  1. 不设置 tools.mode 或将其设为 direct,重启 Qwen Code,确认普通工具仍可用且不存在 exec。
  2. 将 tools.mode 设为 code_mode 并重启,确认普通工具和 exec 同时可用;确认普通工具描述包含各自的 tools.<name>(args) 声明,延迟工具仍可通过 tool_search 发现。
  3. 将 tools.mode 设为 code_mode_only 并重启,确认普通工具通过 exec 而不是顶层调用使用,同时直接控制工具仍然可用。
  4. 打开设置界面,确认 Tool Mode 可在 Default、Code Mode 和 Code Mode Only 之间切换,并确认 settings schema 中不再存在 tools.codeModeOnly。
  5. 配置任一 code mode 后用 --safe-mode 或 --bare 启动,确认有效模式为 direct。

已完成自动化验证:Core 相关测试(121 个)、CLI 相关测试(585 个)、再次运行的 settings schema 回归测试(60 个)、完整 build、完整 typecheck、完整 lint,以及已安装版本和分支构建版本的 CLI 版本 smoke 检查。

前后对比证据

之前:设置界面以 false / true 布尔值展示 Code Mode Only (Experimental)。

之后:设置界面以枚举展示 Tool Mode (Experimental),可选 Default / Code Mode / Code Mode Only。更新后的 TUI snapshot 覆盖默认显示。

测试平台

OS 状态
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS、Node.js v24.18.0;本地源码构建和 package 级 Vitest 测试套件。

风险与范围

  • 主要风险或取舍:混合模式会在每个普通工具描述中增加一段简短的嵌套调用声明,使 prompt 略微增大;完整 schema 不会在 exec 描述中重复。
  • 未验证 / 范围外:未进行真实 provider 的交互式运行,也未在 Windows/Linux 本地执行;支持平台由 CI 覆盖。
  • 破坏性变更 / 迁移说明:tools.codeModeOnly 已移除。请将 { "tools": { "codeModeOnly": true } } 替换为 { "tools": { "mode": "code_mode_only" } }。

关联 Issue

关联 #10377。

@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

E2E test report

  • Baseline CLI: globally installed qwen reports 0.23.3-nightly.20260913.faa395885e.
  • Branch CLI: the built CLI reports 0.23.3.
  • Automated behavior coverage passed for all three enum values, safe/bare fallback to direct, direct/hybrid/only declaration surfaces, deferred-tool discovery, filtered subagent allowlists, and the Settings enum/default rendering.
  • Focused Core: 121 tests passed.
  • Focused CLI: 585 tests passed; the 60-test settings-schema suite was rerun after the final removal assertion and passed.
  • Repository checks: npm run build, npm run typecheck, and npm run lint passed. The first build attempt was interrupted by a transient SIGABRT in the Telegram workspace; that workspace passed independently and the complete build passed on retry.
  • Live-provider interactive execution was not run locally. Reviewer verification steps for direct, code_mode, and code_mode_only are included in the PR body.

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

Read the full diff of the ten production files and audited every non-test getToolMode/getCodeModeOnly/isCodeModeEnabled read site at this head. No new production defect found — and, per the review rules, I re-verified the four open Criticals against the code as it stands rather than against their threads. All four still stand; I am not re-filing them, listing them here only so the record shows they were measured at this commit.

  • R1-1 — still stands. packages/cli/src/config/config.ts:2261-2264 passes settings.tools?.mode straight through (? (settings.tools?.mode ?? ToolMode.Direct)), while packages/core/src/config/config.ts:10399 gates registration with a denylist (if (this.getToolMode() === ToolMode.Direct) return;). An out-of-union value therefore registers exec while the other read sites treat the session as direct. I confirmed there is no value validation on the settings-load path.
  • R1-22 — still stands. agent-core.ts:1720-1724: isCodeModeEnabled(...) && this.executionAllowedExactTools?.has(ToolNames.EXEC) && getToolExposure(toolName) === 'code-mode-callable' returns true, i.e. an allowlist containing only exec authorises direct calls to every code-mode-callable tool. In hybrid there is no second wall (the scheduler gate is CodeModeOnly-only, which I verified is correct for hybrid).
  • R1-2 and R1-3 I did not re-derive line by line; the first needs only that ToolMode is a value import evaluated at module load in settingsSchema.ts (it is), and the second only that agent-core.ts narrows declarationNames to {exec} ∪ configuredNames while the binding plan keeps the widened set (it does).

Verified correct for hybrid, so no change is needed there: the twelve unconverted === CodeModeOnly checks are each semantically right for code_mode (scheduler/ACP direct-call gates, deferred reveal and preload, getDeferredToolSummary, the zoom hint); planCodeModeBindings builds a fresh plan per call, so there is no cache-staleness path; tool_search stays registered and top-level-visible in hybrid with revealed deferred tools receiving augmented declarations through the normal refresh; and no codeModeOnly stragglers remain in any production file, with the regenerated settings.schema.json enum matching the schema source.

@wenshao

wenshao commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover from 5

@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 Sep 22, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This window's round counter starts at 5 (the rounds this PR spent in review before takeover), so the Critical-only brake engages after 0 more change-producing round(s) instead of a full fresh 5. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本窗口轮次计数从 5 起算(即本 PR 托管前已进行的评审轮数),因此再经过 0 个产生改动的轮次即进入 Critical-only,而非重新计满 5 轮。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

🔄 AutoFix is working on this PR — round 11/100. Watch live progress; this round posts its report here when it finishes.
⏱ Running for 40 min · agent active 0 min ago

中文说明

🔄 AutoFix 正在处理此 PR —— 第 11/100 轮。查看实时进度;本轮结束后会在此发布报告。
⏱ 已运行 40 分钟 · agent 最近活动在 0 分钟前

qwen-code-ci-bot added 2 commits September 22, 2026 15:27
- web-shell: call the exported isSettingVisible in the code-mode alias
  test and sync the mirrored alias table to tools.mode, fixing the
  ReferenceError and stale-alias failures in the Test job
- coreToolScheduler: render only the skill invocation surfaces the
  session actually has — no exec surface in direct mode where exec is
  never registered, no top-level Skill surface in code_mode_only where
  direct skill calls are rejected
- agent-core: let MCP tool names fall through to the exact-name and
  pattern checks before the nested exec gateway admits them, so a
  server-level executionAllowedTools pattern is honored for nested
  bindings
- docs/settings: sync the threshold bridge-mode wording, the container
  execution fallback, and the zh-CN withheld-tool translation with the
  shipped behavior
# Conflicts:
#	packages/cli/src/config/settingsSchema.ts
#	packages/core/src/config/config.ts
#	packages/vscode-ide-companion/schemas/settings.schema.json
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #11854

Round mode: Critical-only (window seeded at round 5). Scope this round: the failed Test (ubuntu-latest, Node 22.x) check, the four actionable Critical findings, the requested origin/main merge, and a bounded set of cheap in-footprint Suggestions. Everything else in the actionable sections is replied to on its thread and deferred to the next round.

Commits: 3239c6804c (review fixes) and f35beb1413 (merge of origin/main).

Resolved findings

R4-1 (rc:4066711873) — Critical — web-shell test calls nonexistent isSettingExcluded

packages/web-shell/client/settings.test.ts now calls the exported isSettingVisible('tools.mode', { excludeItems: ['setting:code-mode-only'] }) and asserts false, matching the real export and polarity. This was the ReferenceError failing the Test job.

R4-2 (rc:4066711886) — Critical — stale mirrored alias table

The mirror entry in the both-directions alias test is re-valued to 'setting:code-mode-only': 'tools.mode' (value only, key set unchanged), so the exhaustive alias test matches the production SETTING_KEYS repoint.

R4-3 (rc:4066711894) — Critical — skill-activation reminder names surfaces the session does not have

coreToolScheduler.ts now renders only the invocation surfaces that exist: the exec clause appears only when a code mode is enabled AND exec is registered (isCodeModeEnabled(getToolMode()) && getTool(EXEC)), and the top-level Skill clause is omitted under code_mode_only (where direct skill calls are rejected). Direct mode (the default, where exec is never registered) now names only the top-level Skill tool. The previously pinned direct-mode test is updated to assert the exec clause is absent; new tests cover hybrid (both clauses) and code_mode_only (exec clause only, via a nested source: 'code_mode' call).

R2-2 (rc:4051539079, rc:4066712191) — Critical — nested exec gateway bypasses MCP execution patterns

agent-core.ts route (a): the nested-binding exec gateway now lets MCP tool names fall through to the exact-name and pattern checks whenever executionAllowedMcpPatterns is non-empty ((!toolName.startsWith('mcp__') || patterns.length === 0) guard). A fork allowlisted ['exec', 'mcp__github__read_*'] no longer binds mcp__github__create_issue (or mcp__filesystem__*) inside exec, while pattern-free allowlists keep the pinned full-registry inheritance and built-ins keep the pinned code_mode_only semantic. New test: honors MCP execution patterns in the hybrid nested binding set.

R4-6 (rc:4066711908) — Suggestion — zh-CN mistranslation of "withheld-tool warnings"

docs/design/code-mode.zh-CN.md acceptance criterion now reads 被保留工具(withheld)的警告 instead of 工具延迟警告, no longer colliding with the doc's own 延迟 = deferred usage.

R4-8 (rc:4066711921) — Suggestion — container design doc names only the rejection

Both language versions of docs/design/2026-09-12-agent-container-execution.md now state that tools.mode: "code_mode" warns and continues with direct tools only (no exec), alongside the code_mode_only rejection.

R4-4 (rc:4066711899) — Suggestion — partially resolved

In-footprint half fixed: the tools.toolSearch.threshold description (settingsSchema.ts) now names direct and hybrid modes plus the exec-reachability sentence, and the generated settings.schema.json was regenerated in the same commit. The three packages/sdk-typescript copies are outside the PR's footprint; deferred with a reply on the thread.

R3-8 (rc:4051538893) — Suggestion — verified already resolved at this head

A repo-wide grep finds tools.codeModeOnly in docs only at the intentional deprecation row (settings.md:392); the three passages the finding named already use tools.mode: "code_mode_only". Re-verified, listed as resolved.

Declined with evidence

R2-16 (rc:4051538861) — drop SharedArrayBuffer from the guest-globals bullet

Refuted: host.ts:491 already deletes SharedArrayBuffer from the guest globalThis (and did at 6c919b7 when filed), and quickjs-emscripten exposes no such global anyway — so "the guest has no SharedArrayBuffer" is accurate and the bullet stays. Replied on the thread.

Merge with origin/main (--conflict true)

Merged origin/main (60 commits) into the branch. Conflicts and semantic resolutions:

  • packages/cli/src/config/settingsSchema.ts: kept main's new tools.executionSandbox block and this PR's tools.mode enum; main's codeModeOnly boolean stays removed (the schema test suite pins its absence).
  • packages/core/src/config/config.ts: main had moved the container-execution branch below the registration helpers and extended it (environment.toolNames filter, structured-output/goal-worker/host-session registration). Took main's moved block and re-applied this PR's two code-mode changes onto it: the tools.mode = "code_mode_only" rejection message and the hybrid warn-and-fallback branch. The branch still returns before registerExecIfEnabled.
  • packages/vscode-ide-companion/schemas/settings.schema.json: regenerated from the merged schema source (contains executionSandbox and mode, not codeModeOnly).
  • packages/cli/src/config/config.ts: main's new ssh-workspace path assigned the removed configParams.codeModeOnly = false; mapped to the renamed key configParams.toolMode = ToolMode.Direct, and main's SSH test was updated to assert the new key (behavior pinned is unchanged: SSH workspaces force direct mode).

Test weakening

One pre-existing assertion was re-keyed, not removed: config.test.ts's SSH-workspace isolation test asserted the constructor param codeModeOnly: false; after the PR's settings rename the equivalent pin is toolMode: ToolMode.Direct. Same behavioral guarantee (SSH workspace forces non-code-mode), updated to the renamed surface. Recorded here per the test-evidence boundary.

Mutation probes (guard witnesses)

  • R4-3: temporarily forced execSurfaceAvailable = true → the direct-mode assertion failed (1 failed); restored → 444/444 pass.
  • R2-2: temporarily removed the MCP fall-through guard → honors MCP execution patterns in the hybrid nested binding set failed (1 failed | 32 passed); restored → 33/33 pass. The probe also confirmed the pinned pattern-free inheritance cases stayed green under the mutant.
  • R4-1/R4-2 are test-only repairs of a suite that was itself red at the reviewed head (ReferenceError + stale alias); R4-6/R4-8/R4-4 are documentation or description text with no guard to mutate.

Deferred to next round (replied on each thread)

R2-4 (nested-only skill gate refactor), R2-17, R2-18, R3-1, R3-4, R3-5 (partially stale — warn exists; settings.md sentence + dedicated test remain), R3-6, R3-7 (collision-resolution semantics choice), R4-4 SDK half (out of footprint), R4-5, R4-7, R4-9, and the open round-1/round-2 Suggestions (R1-5..R1-23, R2-3, R2-5..R2-15).

Environment note

This runner sets SANDBOX=qwen-code-dbf59527; main's sandbox-selection code (deliberately) disables whole-CLI sandboxing when that variable is present, which reds 11 pre-existing sandbox-related tests locally. Those suites were run with env -u SANDBOX (matching CI, where the variable is unset) — all pass. No code was changed for this; it is an artifact of running inside the sandbox container.

Verification

  • npm run build (COREPACK_HOME=/tmp/corepack) — passed (after conflict resolution, on the merged tree)
  • npm run typecheck — passed (first post-merge run surfaced stale dist/ plus one real rename fallout in cli/config.ts, fixed as toolMode: ToolMode.Direct; clean rerun)
  • npm run lint — passed
  • npm run generate:settings-schema — ran; regenerated settings.schema.json committed (contains executionSandbox + mode, no codeModeOnly)
  • packages/web-shell: npx vitest run client/settings.test.ts — 20 passed
  • packages/core: npx vitest run src/core/coreToolScheduler.test.ts — 444 passed
  • packages/core: npx vitest run src/agents/runtime/agent-core.skill-gate.test.ts — 33 passed
  • packages/core: npx vitest run src/config/config-execution-environment.test.ts — 19 passed
  • packages/core: npx vitest run src/config/config.test.ts src/core/client.test.ts (env -u SANDBOX) — 1264 passed
  • packages/cli: npx vitest run src/config/config.test.ts src/config/settingsSchema.test.ts src/config/settingsUtils.test.ts (env -u SANDBOX) — 605 passed
  • packages/cli: npx vitest run src/acp-integration/session/Session.test.ts (env -u SANDBOX) — 1048 passed
  • Integration tests: not run — no behavior in this round is exercised only through the bundled CLI harness (the changed paths are unit-covered above).
中文说明

Autofix 本轮总结 — PR #11854

本轮模式:仅 Critical(窗口由第 5 轮起算)。本轮范围:失败的 Test (ubuntu-latest, Node 22.x) 检查、四个可执行的 Critical 发现、所请求的 origin/main 合并,以及一小批低成本的 footprint 内 Suggestion。可执行区域中的其余条目已在各自线程回复并推迟到下一轮。

提交:3239c6804c(评审修复)与 f35beb1413(合并 origin/main)。

已解决的发现

R4-1(rc:4066711873)— Critical — web-shell 测试调用了不存在的 isSettingExcluded

packages/web-shell/client/settings.test.ts 现在调用真实导出的 isSettingVisible('tools.mode', { excludeItems: ['setting:code-mode-only'] }) 并断言 false。这正是导致 Test 任务失败的 ReferenceError。

R4-2(rc:4066711886)— Critical — 镜像别名表过期

双向别名穷尽测试中的镜像条目值改为 'setting:code-mode-only': 'tools.mode'(只改值,不增删键),与生产侧 SETTING_KEYS 的重指向一致。

R4-3(rc:4066711894)— Critical — 技能激活提醒列出了会话不具备的调用入口

coreToolScheduler.ts 现在只渲染实际存在的调用入口:exec 子句仅在启用某个 code mode 且已注册 exec 时出现(isCodeModeEnabled(getToolMode()) && getTool(EXEC)),顶层 Skill 子句在 code_mode_only 下省略(该模式拒绝直接 skill 调用)。direct 模式(默认,从不注册 exec)现在只提顶层 Skill 工具。原先固定错误行为的 direct 测试已改为断言 exec 子句不出现;新增测试覆盖 hybrid(两个子句)与 code_mode_only(仅 exec 子句,经由 source: 'code_mode' 的嵌套调用)。

R2-2(rc:4051539079、rc:4066712191)— Critical — 嵌套 exec 网关绕过 MCP 执行模式

agent-core.ts 采用路线 (a):当 executionAllowedMcpPatterns 非空时,嵌套绑定的 exec 网关让 MCP 工具名落入后续的精确名与模式检查((!toolName.startsWith('mcp__') || patterns.length === 0) 守卫)。白名单为 ['exec', 'mcp__github__read_*'] 的 fork 不再在 exec 内绑定 mcp__github__create_issue(或 mcp__filesystem__*);不带模式的白名单保持被固定的全 registry 继承语义,内建工具保持被固定的 code_mode_only 语义。新增测试:honors MCP execution patterns in the hybrid nested binding set。

R4-6(rc:4066711908)— Suggestion — 中文误译 “withheld-tool warnings”

docs/design/code-mode.zh-CN.md 的验收标准改为「被保留工具(withheld)的警告」,不再与文档中「延迟=deferred」的既有用法冲突。

R4-8(rc:4066711921)— Suggestion — 容器设计文档只写了拒绝分支

docs/design/2026-09-12-agent-container-execution.md 中英两版现在都说明:tools.mode: "code_mode" 会告警并仅以直接工具继续(不注册 exec),与 code_mode_only 的拒绝并列。

R4-4(rc:4066711899)— Suggestion — 部分解决

footprint 内的一半已修复:settingsSchema.ts 的 tools.toolSearch.threshold 描述现在同时描述 direct 与 hybrid 模式并补充 exec 可达句,生成的 settings.schema.json 已在同一提交重新生成。packages/sdk-typescript 的三处副本在 PR footprint 之外,已在线程回复中说明并推迟。

R3-8(rc:4051538893)— Suggestion — 已在当前 head 验证为解决

全仓 grep 显示文档中 tools.codeModeOnly 只剩有意的弃用行(settings.md:392);该发现指出的三处段落已使用 tools.mode: "code_mode_only"。重新验证后列为已解决。

附证据驳回

R2-16(rc:4051538861)— 从 guest 全局变量条目删除 SharedArrayBuffer

驳回:host.ts:491 已经从 guest 的 globalThis 删除 SharedArrayBuffer(在该发现提交时的 6c919b7 就已如此),且 quickjs-emscripten 本来也不暴露该全局对象——因此「guest 没有 SharedArrayBuffer」的表述是准确的,条目保留。已在线程回复。

与 origin/main 的合并(--conflict true)

将 origin/main(60 个提交)合入本分支。冲突与语义消解:

  • packages/cli/src/config/settingsSchema.ts:保留 main 新增的 tools.executionSandbox 块与本 PR 的 tools.mode 枚举;main 的 codeModeOnly 布尔键保持移除(schema 测试套件固定了它不存在)。
  • packages/core/src/config/config.ts:main 把容器执行分支移到了注册辅助函数之后并做了扩展(environment.toolNames 过滤、structured-output/goal-worker/host-session 注册)。采用 main 移动后的块,并把本 PR 的两处 code-mode 改动重新落到其上:tools.mode = "code_mode_only" 的拒绝文案与 hybrid 告警回退分支。该分支仍在 registerExecIfEnabled 之前返回。
  • packages/vscode-ide-companion/schemas/settings.schema.json:从合并后的 schema 源重新生成(含 executionSandbox 与 mode,不含 codeModeOnly)。
  • packages/cli/src/config/config.ts:main 新增的 ssh-workspace 路径给已删除的 configParams.codeModeOnly = false 赋值;映射到重命名后的 configParams.toolMode = ToolMode.Direct,并把 main 的 SSH 测试更新为断言新键(固定的行为不变:SSH workspace 强制 direct 模式)。

测试削弱说明

有一处既有断言改了键而非删除:config.test.ts 的 SSH-workspace 隔离测试原断言构造参数 codeModeOnly: false;本 PR 设置键重命名后,等价的固定是 toolMode: ToolMode.Direct。行为保证相同(SSH workspace 强制非 code mode),仅随重命名更新。按测试证据边界在此记录。

变异探针(守卫见证)

  • R4-3:临时强制 execSurfaceAvailable = true → direct 模式断言失败(1 failed);还原后 444/444 通过。
  • R2-2:临时移除 MCP 落空守卫 → honors MCP execution patterns in the hybrid nested binding set 失败(1 failed | 32 passed);还原后 33/33 通过。探针同时确认不带模式的继承用例在变异体下保持绿色。
  • R4-1/R4-2 是对本就在评审 head 变红的测试套件的纯测试修复(ReferenceError 加过期别名);R4-6/R4-8/R4-4 为文档或描述文案,没有可供变异的守卫。

推迟到下一轮(均已在各自线程回复)

R2-4(nested-only skill 门控重构)、R2-17、R2-18、R3-1、R3-4、R3-5(部分过时——告警已存在;settings.md 句子与专门测试待做)、R3-6、R3-7(碰撞消解语义取舍)、R4-4 的 SDK 一半(footprint 外)、R4-5、R4-7、R4-9,以及仍未处理的第 1、2 轮 Suggestion(R1-5..R1-23、R2-3、R2-5..R2-15)。

环境说明

本 runner 设置了 SANDBOX=qwen-code-dbf59527;main 的 sandbox 选择逻辑(有意地)在该变量存在时禁用整 CLI 沙箱,导致 11 个既有的沙箱相关测试在本地变红。这些套件改用 env -u SANDBOX 运行(与 CI 一致,CI 无此变量)——全部通过。未为此改动任何代码;这是在沙箱容器内运行的环境产物。

验证

  • npm run build(COREPACK_HOME=/tmp/corepack)— 通过(冲突消解后、合并树上)
  • npm run typecheck — 通过(合并后首次暴露了过期的 dist/ 以及 cli/config.ts 中一处真实的重命名遗留,已修为 toolMode: ToolMode.Direct;重跑干净)
  • npm run lint — 通过
  • npm run generate:settings-schema — 已运行;重新生成的 settings.schema.json 已提交(含 executionSandbox 与 mode,不含 codeModeOnly)
  • packages/web-shell:npx vitest run client/settings.test.ts — 20 通过
  • packages/core:npx vitest run src/core/coreToolScheduler.test.ts — 444 通过
  • packages/core:npx vitest run src/agents/runtime/agent-core.skill-gate.test.ts — 33 通过
  • packages/core:npx vitest run src/config/config-execution-environment.test.ts — 19 通过
  • packages/core:npx vitest run src/config/config.test.ts src/core/client.test.ts(env -u SANDBOX)— 1264 通过
  • packages/cli:npx vitest run src/config/config.test.ts src/config/settingsSchema.test.ts src/config/settingsUtils.test.ts(env -u SANDBOX)— 605 通过
  • packages/cli:npx vitest run src/acp-integration/session/Session.test.ts(env -u SANDBOX)— 1048 通过
  • 集成测试:未运行——本轮改动的路径均已被上述单元测试覆盖,不存在只有打包 CLI 或集成设施才能触达的行为。

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 5 by @qwen-code /takeover from 5, plus 0 change-producing round(s) since). 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.)

中文说明

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

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

🧵 Resolved all 7 selected review thread(s). · 已关闭全部选中的 7 条评审线程。

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/模型 kimi-k3 · CLI 0.24.3

The execution-allowlist branch already restricted MCP nested bindings to
matching exact names or server patterns once the list mentioned MCP, but
the configured-tools branch — the only allowlist SubagentConfig exposes —
applied the exec carve-out to every code-mode-callable name, so
tools: ['exec', 'mcp__github__read_*'] admitted every connected server's
tools as nested exec bindings. Route MCP names through the shared
raw-identity matcher in that branch too, and pin the bridge-route zoom
hint case for narrowed agents.

Co-authored-by: Qwen-Coder <[email protected]>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address-review round — PR #11854

This round merged origin/main (the PR had fallen behind and the merge conflicted), resolved the one conflict, and settled all four open Critical inline findings: one with a code fix, one with the exact test the reviewer asked for, and two by re-verifying that the head commit 34711cfe7e had already fixed them.

Merge with origin/main (--conflict true)

Merged origin/main (d0cd622a68) into the branch as ce7e57d52e. One conflict, in packages/cli/src/ui/commands/contextCommand.test.ts:

  • Main's side (1afbcffe21, skills reload): the skill tool double must expose the new getLoadedSkillContentNames(): Map<content, name> API, and this test overrides it with an empty map so the tracked skill body does not enter the fixture's token arithmetic.
  • PR's side: the test was reworked from "charges the builtin-clamp deficit to the mcp row" into "bills the declared mcp schema to the mcp row", declaring the MCP schema alongside the skill schema (declared: [skillToolSchema, mcpToolDouble.schema]).
  • Resolution: keep the PR's scenario and assertions, adopt main's API adaptation — tools: [{ ...skillToolDouble, getLoadedSkillContentNames: () => new Map() }, mcpToolDouble] with the PR's declared. All 51 tests in the file pass.

Findings

  • [rc:4086709533] R7-1 (agent-core.ts:691) — fixed in code. The published rule "an execution allowlist that mentions any MCP tool additionally restricts MCP bindings to matching exact names or server patterns" was implemented only on the executionAllowedTools branch; the configuredAllowlist branch — the one every agent definition reaches, since SubagentConfig exposes only tools — applied the exec carve-out to every code-mode-callable name, so tools: ['exec', 'mcp__github__read_*'] admitted every connected server's MCP tools as nested exec bindings. Reproduced first: a new test in agent-core.skill-gate.test.ts mirroring "honors MCP execution patterns in the hybrid nested binding set" but passing only { tools: [EXEC, 'mcp__github__read_*'] } failed with ['mcp__github__read_file', 'mcp__github__create_issue', 'mcp__payments__charge']. The fix extracts the existing raw-identity matcher as matchesMcpAllowlist(toolName, exact, patterns) (preserving the sanitized-prefix warning) and, in the configuredAllowlist branch, routes MCP names through it once the configured list mentions MCP — mirroring the executionAllowedTools branch. The non-MCP blanket pass and the CodeModeOnly expansion are untouched. Mutation probe: removing the new MCP fall-through turns exactly the new test red again; restored, all 36 tests in the file pass. This settles the root of the superseded R5-2 thread as well.
  • [rc:4083462850] R6-1 (fileUtils.ts:1687) — already fixed at head; pinned the requested case. The head commit gates every zoom hint on ambientAllowedNames === undefined, so a narrowed agent never sees the bridge-route hint. This round adds the exact it.each case the finding asked for (declared: ['read_file','tool_search','tool_call'], deferred: ['zoom_image'], allowedNames: ['read_file'], hint '') to "uses only exposed tools for image guidance". It passes; removing the ambientAllowedNames === undefined guard turns it (and the seven sibling narrowed-agent cases) red; restored, all 24 cases pass and the full file is 229/229.
  • [rc:4083462869] R5-2 (sdk-typescript README coreTools row) — already fixed at head; re-verified. The row now carries the requested scoping verbatim: "agent allowlists that do not grant exec narrow nested bindings. Inheriting or explicitly granting exec keeps all otherwise admitted ordinary code-mode-callable bindings" plus the MCP carve-out sentence — whose last remaining false reading (the tools-list path) is what R7-1 fixed in code this round.
  • [rc:4083462878] R6-2 (README vs docs/developers mirror) — already fixed at head; re-verified. The coreTools rows in packages/sdk-typescript/README.md:68 and docs/developers/sdk-typescript.md:69 are now byte-identical (checked programmatically), and no artifact in the repo still claims a tools.eager-withheld tool is "not offered to the model".

Left open for humans

  • R2-1 (settings.md, discussion_r4020044292) — the reviewer could not rule on three claimed merge reverts at :200/:432/:805, and this environment has no GitHub credentials to read the original thread. What I could verify locally: the merge with origin/main did not touch docs/users/configuration/settings.md at all (zero-line diff vs the PR head), the PR's remaining diff vs main on that file is a single coherent hunk in the tools section, and there are no conflict markers. Nothing to fix; the thread stays open.
  • Triage comment 5664803576 — both review rounds could only read a truncated body; the visible portion endorses the twelve unchanged CodeModeOnly comparisons rather than asserting a defect. Unreadable from here (no credentials); left open.
  • Failed check macos-latest / Java 21 — no Java toolchain exists on this runner and CI logs are unreachable without credentials, so the failure could not be reproduced or diagnosed locally. Circumstantial evidence points away from this PR: among the sdk-java.yml trigger paths the PR touches only packages/cli/src/acp-integration/session/Session.test.ts (a test file), and code mode is opt-in (tools.mode defaults to direct), so the merged default path is unchanged. The workflow's independent CI remains the gate.

Environment note for local verification

This container exports SANDBOX=qwen-code-43077d20, which makes getSandboxCommand() return '' ("already inside a sandbox") and fails six pre-existing sandbox image resolution precedence tests in packages/cli/src/config/config.test.ts on ANY commit. They pass with env -u SANDBOX (shown below); the CLI suites were therefore run with SANDBOX unset. A durable fix (e.g. deleting SANDBOX in packages/cli/test-setup.ts, mirroring the existing QWEN_REVIEW_SANDBOX handling) is out of this round's Critical-only scope.

Verification

  • npm run build — passed (exit 0), run after the fix
  • npm run typecheck — passed (exit 0), run on the final tree
  • npm run lint — passed (exit 0)
  • npm run generate:settings-schema — passed (exit 0), no drift in settings.schema.json
  • vitest packages/core src/agents/runtime/agent-core.skill-gate.test.ts — 36 passed, including the new honors MCP patterns from the tools list alone in the hybrid nested binding set
  • vitest packages/core src/utils/fileUtils.test.ts — 229 passed, including the new bridge-route narrowed-agent case
  • vitest packages/core (PR-touched files: code-mode, scheduler, omni-integration, client, coreToolScheduler, tool-registry, agent, config-execution-environment, exec-context-*) — 1470 passed
  • vitest packages/cli (config, settingsSchema, Session, dialogs-settings) — 1606 passed with env -u SANDBOX
  • vitest packages/cli src/ui/commands/contextCommand.test.ts — 51 passed (merge resolution)
  • vitest packages/web-shell client/settings.test.ts — 20 passed
  • Mutation probe 1: deleted the new MCP fall-through in the configuredAllowlist branch → new skill-gate test red (1 failed), restored → 36 passed
  • Mutation probe 2: deleted the ambientAllowedNames === undefined zoom-hint guard → 8 cases red including the new one, restored → 24/24 passed
中文说明

审查处理轮次 — PR #11854

本轮合并了 origin/main(PR 已落后且存在冲突),解决了唯一一处冲突,并了结了全部 4 条未决的 Critical 行内发现:1 条通过代码修复,1 条通过补上审查者要求的测试,另外 2 条经复核确认头提交 34711cfe7e 已经修复。

与 origin/main 的合并(--conflict true)

将 origin/main(d0cd622a68)合入本分支,合并提交为 ce7e57d52e。唯一冲突在 packages/cli/src/ui/commands/contextCommand.test.ts:

  • main 一侧(1afbcffe21,skills 重载):skill 工具替身必须实现新的 getLoadedSkillContentNames(): Map<content, name> 接口,且该测试用空 Map 覆盖它,使被追踪的 skill 正文不进入该用例的 token 计算。
  • PR 一侧:该测试从「charges the builtin-clamp deficit to the mcp row」改写为「bills the declared mcp schema to the mcp row」,把 MCP schema 与 skill schema 一起声明(declared: [skillToolSchema, mcpToolDouble.schema])。
  • 解决方式:保留 PR 的场景与断言,采用 main 的接口适配——tools: [{ ...skillToolDouble, getLoadedSkillContentNames: () => new Map() }, mcpToolDouble] 配合 PR 的 declared。该文件 51 个测试全部通过。

发现处理

  • [rc:4086709533] R7-1(agent-core.ts:691)— 已在代码中修复。 对外公布的规则「提及任何 MCP 工具的 execution allowlist 会额外把 MCP 绑定限制为精确名称或 server 模式」此前只在 executionAllowedTools 分支实现;而 configuredAllowlist 分支——所有 agent 定义实际走到的分支,因为 SubagentConfig 只暴露 tools——对每一个 code-mode-callable 名称都套用 exec 豁免,导致 tools: ['exec', 'mcp__github__read_*'] 会把所有已连接 server 的 MCP 工具都作为 exec 嵌套绑定放行。先复现:在 agent-core.skill-gate.test.ts 中仿照「honors MCP execution patterns in the hybrid nested binding set」新增一个只传 { tools: [EXEC, 'mcp__github__read_*'] } 的用例,修复前失败,实际得到 ['mcp__github__read_file', 'mcp__github__create_issue', 'mcp__payments__charge']。修复方式:把原有的原始身份匹配器抽为 matchesMcpAllowlist(toolName, exact, patterns)(保留关于规范化前缀的警示注释),并在 configuredAllowlist 分支中,当配置列表提及 MCP 时让 MCP 名称改走该匹配器——与 executionAllowedTools 分支一致。非 MCP 的全量放行与 CodeModeOnly 的扩展均未改动。变异探针:删除新增的 MCP 下坠逻辑后,恰好只有新用例变红;恢复后该文件 36 个测试全部通过。这也一并了结了被取代的 R5-2 线程的根因。
  • [rc:4083462850] R6-1(fileUtils.ts:1687)— 头提交已修复;本轮补上要求的用例。 头提交已用 ambientAllowedNames === undefined 门控所有 zoom 提示,被收窄的agent 不会再看到桥接路径提示。本轮把发现中要求的 it.each 用例原样加入「uses only exposed tools for image guidance」(declared: ['read_file','tool_search','tool_call']、deferred: ['zoom_image']、allowedNames: ['read_file']、提示为空)。该用例通过;删除 ambientAllowedNames === undefined 门控后,它与另外 7 个收窄 agent 用例一起变红;恢复后 24 个用例全绿,整文件 229/229 通过。
  • [rc:4083462869] R5-2(sdk-typescript README 的 coreTools 行)— 头提交已修复;本轮复核确认。 该行现在逐字携带了所要求的限定:「agent allowlists that do not grant exec narrow nested bindings. Inheriting or explicitly granting exec keeps all otherwise admitted ordinary code-mode-callable bindings」,外加 MCP 限定句——该句最后一种不成立的解读(tools 列表路径)正是本轮 R7-1 在代码中修复的内容。
  • [rc:4083462878] R6-2(README 与 docs/developers 镜像)— 头提交已修复;本轮复核确认。 packages/sdk-typescript/README.md:68 与 docs/developers/sdk-typescript.md:69 的 coreTools 行现已逐字节一致(程序化比对),仓库中已没有任何产物仍声称被 tools.eager 降级的工具「not offered to the model」。

留待人工处理

  • R2-1(settings.md,discussion_r4020044292)——审查方无法判定 :200/:432/:805 三处声称的合并回退,而本环境没有 GitHub 凭据,读不到原始线程。本地能够核实的是:与 origin/main 的合并完全没有触碰 docs/users/configuration/settings.md(相对 PR 头提交为零行差异),该文件相对 main 的剩余差异是 tools 小节中单一连贯的 hunk,且不存在冲突标记。无可修之处,线程保持开放。
  • 分诊评论 5664803576——两轮审查都只读到被截断的正文;可见部分认可那十二处未改动的 CodeModeOnly 比较,并未断言缺陷。此处无法读取(无凭据),保持开放。
  • 失败检查 macos-latest / Java 21——本 runner 没有 Java 工具链,且没有凭据无法获取 CI 日志,因此该失败无法在本地复现或诊断。旁证指向与本 PR 无关:在 sdk-java.yml 的触发路径中,本 PR 只触碰了 packages/cli/src/acp-integration/session/Session.test.ts(一个测试文件),且 code mode 默认关闭(tools.mode 默认为 direct),合并后的默认路径未变。工作流的独立 CI 仍是最终门禁。

本地验证的环境说明

本容器导出了 SANDBOX=qwen-code-43077d20,这会使 getSandboxCommand() 返回 ''(「已在沙箱内」),从而导致 packages/cli/src/config/config.test.ts 中 6 个既有的「sandbox image resolution precedence」测试在任何提交上都失败。用 env -u SANDBOX 运行则全部通过(见下);因此 CLI 测试套件均在清除 SANDBOX 后运行。持久修复(例如在 packages/cli/test-setup.ts 中删除 SANDBOX,仿照现有的 QWEN_REVIEW_SANDBOX 处理)超出本轮仅处理 Critical 的范围。

验证

  • npm run build — 通过(exit 0),在修复之后运行
  • npm run typecheck — 通过(exit 0),在最终代码树上运行
  • npm run lint — 通过(exit 0)
  • npm run generate:settings-schema — 通过(exit 0),settings.schema.json 无漂移
  • vitest packages/core src/agents/runtime/agent-core.skill-gate.test.ts — 36 个通过,含新增的「honors MCP patterns from the tools list alone in the hybrid nested binding set」
  • vitest packages/core src/utils/fileUtils.test.ts — 229 个通过,含新增的桥接路径收窄 agent 用例
  • vitest packages/core(PR 触碰的文件:code-mode、scheduler、omni-integration、client、coreToolScheduler、tool-registry、agent、config-execution-environment、exec-context-*)— 1470 个通过
  • vitest packages/cli(config、settingsSchema、Session、dialogs-settings)— 在 env -u SANDBOX 下 1606 个通过
  • vitest packages/cli src/ui/commands/contextCommand.test.ts — 51 个通过(合并冲突解决)
  • vitest packages/web-shell client/settings.test.ts — 20 个通过
  • 变异探针 1:删除 configuredAllowlist 分支新增的 MCP 下坠逻辑 → 新的 skill-gate 用例变红(1 个失败),恢复后 36 个通过
  • 变异探针 2:删除 zoom 提示的 ambientAllowedNames === undefined 门控 → 8 个用例变红(含新用例),恢复后 24/24 通过

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 5 by @qwen-code /takeover from 5, plus 2 change-producing round(s) since). 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.)

中文说明

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

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

🧵 Resolved all 4 selected review thread(s). · 已关闭全部选中的 4 条评审线程。

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/模型 kimi-k3 · CLI 0.24.4

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round — no change

No code change was made in this round. One item was triaged — the CANCELLED web-shell E2E Smoke check — and the evidence shows a CI job-timeout cancellation, not a defect in this PR.

Actionable review feedback

None. The round ran in Critical-only mode and the actionable sections (Reviews / Inline comments / Issue-level comments) were empty — every open non-Critical item is deferred by the workflow for human follow-up and was not touched (no code, thread resolutions, or replies), as required.

Failed checks triage: web-shell E2E Smoke (ubuntu-latest, Node 22.x) — CANCELLED

Verdict: the job hit its own 20-minute timeout in CI; nothing in this PR's diff can influence what that job measures. Evidence:

  1. The cancellation is a timeout signature, not a test result. The job started at 22:10:11Z and was marked CANCELLED at 22:30:34Z — 20m23s against the job's timeout-minutes: 20 (.github/workflows/ci.yml:1310). A failing test would produce FAILURE with a step log; a cancellation landing exactly on the job's own timeout mark is the runner killing the job.
  2. Not a concurrency cancellation. ci.yml's concurrency group cancels in-progress runs only when a newer run starts for the same PR ref. No newer push exists: the branch head has been fa457bda6b (pushed 21:28 UTC) since before this run started (21:48 UTC), and the run's other 30 checks completed normally.
  3. The same commit's sibling browser job is green. Capture web-shell visuals (ubuntu-latest, Node 22.x) completed SUCCESS at 22:09:48Z on this exact head. That job boots the same npm run dev web-server (playwright.visuals.config.ts:30-31) and drives the same mock-daemon browser harness — proving the web-shell app builds, boots, and renders correctly in CI on fa457bda6b. It needed 21 of its 30 budgeted minutes; the smoke job packs the same overheads (pnpm install, Playwright --with-deps chromium+webkit install) plus two browser suites into a 20-minute budget.
  4. Zero diff overlap with the cancelled job's test surface. The three-dot diff against origin/main touches nothing under packages/web-shell/client/e2e/**, packages/web-templates/**, integration-tests/chat-transcript-document.test.ts, packages/cli/src/ui/utils/export/**, any Playwright config, or .github/** (verified: 0-line diff across those paths). The PR's only web-shell production change is a two-line settings alias remap (tools.codeModeOnly → tools.mode) in packages/web-shell/client/settings.ts, and no e2e spec references either key.
  5. The PR's core changes are inert in this job. The smoke suite runs entirely against an in-page mock daemon (installMockDaemon / createWebShellDaemonScenario), so this round's agent-core.ts / fileUtils changes — the head commit's subject — never execute in it.
  6. The check was green earlier on this PR. The PR's triage report snapshot (issue comment 5664803576) lists web-shell E2E Smoke (ubuntu-latest, Node 22.x) as ✅ success; the intermittent element is CI timing, not this diff.

Recommended remedy: re-run the cancelled job (requires maintainer/workflow credentials, which this bot does not have). If it repeatedly exhausts the 20-minute budget on main-class workloads, raising timeout-minutes or trimming the job's install overheads is a CI-machinery decision: .github/ is outside this PR's footprint and outside what an autofix round is permitted to modify, so that call belongs to a maintainer.

Local verification limits and surrogates

Full local reproduction of the browser gates was not possible in this container: Playwright's chromium/webkit downloaded but cannot launch (no root, no system browser libraries, no docker; sudo absent). Surrogates actually run, with results:

  • git diff origin/main...HEAD --stat -- packages/web-shell/client/e2e integration-tests/chat-transcript-document.test.ts packages/web-templates .github — 0 lines (the cancelled job's test surface is untouched)
  • grep 'code-mode-only\|codeModeOnly\|tools.mode' packages/web-shell/client/e2e/*.spec.ts — no matches (the PR's web-shell change has no e2e selector exposure)
  • npm run dev --workspace=packages/web-shell (vite dev server, same command as the CI webServer step) — ready in 184 ms; GET / → 200 in 26 ms; GET /client/settings.ts (the one changed production module) → 200 in 188 ms. (Local pre-transform errors resolving @qwen-code/acp-bridge/* subpaths are an artifact of this sandbox's unbuilt workspace links — node_modules/@qwen-code/acp-bridge has no dist/ here — not of the PR; CI's pnpm install builds them, and the same-commit visuals job passing confirms the dev server works in CI.)
  • Chromium/webkit launch probe — failed in this container for missing system libraries (libgtk-4, libgstreamer, …); recorded as the reproduction limit above.

No commit was created; the working tree is unchanged from the round's entry state (HEAD = fa457bda6b871a823f934d0a3ffd42203b8ed623, clean).

中文说明

Autofix 本轮 —— 无改动

本轮未做任何代码改动。共分诊一项——被 CANCELLED 的 web-shell E2E Smoke 检查——证据表明这是 CI 任务超时取消,而非本 PR 的缺陷。

可执行的评审反馈

无。本轮处于仅处理 Critical 的模式,可执行区域(Reviews / Inline comments / Issue-level comments)为空——所有未关闭的非 Critical 条目已由工作流延后留待人工跟进,并按要求未被触碰(未改代码、未解决会话、未回复)。

失败检查分诊:web-shell E2E Smoke (ubuntu-latest, Node 22.x) —— CANCELLED

结论:该任务在 CI 中触发了自身的 20 分钟超时;本 PR 的 diff 中没有任何内容能影响该任务所测试的对象。证据:

  1. 取消的时间特征指向超时,而非测试结果。 该任务 22:10:11Z 启动,22:30:34Z 被标记为 CANCELLED——历时 20 分 23 秒,恰对应该任务的 timeout-minutes: 20(.github/workflows/ci.yml:1310)。测试失败会产生 FAILURE 和步骤日志;精确落在任务自身超时时刻的取消是运行器强杀任务的表现。
  2. 并非并发组取消。 ci.yml 的 concurrency 组只在同一 PR 引用出现更新运行时才会取消进行中的运行。不存在更新的推送:分支头自本次运行开始(21:48 UTC)前一直是 fa457bda6b(21:28 UTC 推送),且同一次运行的其余 30 个检查均正常结束。
  3. 同一提交上的兄弟浏览器任务是绿的。 Capture web-shell visuals (ubuntu-latest, Node 22.x) 在同一 head 上于 22:09:48Z 成功完成。该任务启动同一个 npm run dev web 服务器(playwright.visuals.config.ts:30-31),并驱动同一套 mock-daemon 浏览器测试框架——证明 web-shell 应用在 CI 中的 fa457bda6b 上能够正常构建、启动与渲染。它用掉了 30 分钟预算中的 21 分钟;而 smoke 任务把同样的开销(pnpm install、Playwright --with-deps 安装 chromium+webkit)外加两个浏览器套件塞进 20 分钟预算。
  4. 与被取消任务的测试面零重叠。 与 origin/main 的三点 diff 未触及 packages/web-shell/client/e2e/**、packages/web-templates/**、integration-tests/chat-transcript-document.test.ts、packages/cli/src/ui/utils/export/**、任何 Playwright 配置或 .github/**(已验证:这些路径上为 0 行 diff)。本 PR 对 web-shell 唯一的生产代码改动是 packages/web-shell/client/settings.ts 中两行设置别名重映射(tools.codeModeOnly → tools.mode),且没有任何 e2e 用例引用这两个键。
  5. 本 PR 的 core 改动在该任务中不会执行。 smoke 套件完全基于页面内的 mock daemon(installMockDaemon / createWebShellDaemonScenario)运行,因此本轮头提交的主题——agent-core.ts / fileUtils 改动——在其中根本不会被执行。
  6. 该检查此前在本 PR 上是绿的。 PR 分诊报告快照(issue 评论 5664803576)中 web-shell E2E Smoke (ubuntu-latest, Node 22.x) 为 ✅ success;其中的不稳定因素是 CI 耗时,而非本 diff。

建议处理:重新运行该被取消的任务(需要本机器人不具备的维护者/工作流凭据)。若该任务在主干级负载下反复耗尽 20 分钟预算,则提高 timeout-minutes 或削减安装开销属于 CI 基础设施决策:.github/ 不在本 PR 的改动范围内,也不在 autofix 轮次被允许修改的范围内,应由维护者决定。

本地验证的限制与替代手段

本容器内无法完整复现浏览器门禁:Playwright 的 chromium/webkit 可以下载但无法启动(无 root、无系统浏览器依赖库、无 docker,sudo 不存在)。实际运行过的替代验证及结果:

  • git diff origin/main...HEAD --stat -- packages/web-shell/client/e2e integration-tests/chat-transcript-document.test.ts packages/web-templates .github —— 0 行(被取消任务的测试面未被触及)
  • grep 'code-mode-only\|codeModeOnly\|tools.mode' packages/web-shell/client/e2e/*.spec.ts —— 无匹配(本 PR 的 web-shell 改动不暴露任何 e2e 选择器)
  • npm run dev --workspace=packages/web-shell(vite dev 服务器,与 CI webServer 步骤同一命令)—— 184 ms 就绪;GET / → 200(26 ms);GET /client/settings.ts(唯一改动的生产模块)→ 200(188 ms)。(本地解析 @qwen-code/acp-bridge/* 子路径出现的 pre-transform 报错是本沙箱工作区链接未构建的环境伪影——此处的 node_modules/@qwen-code/acp-bridge 没有 dist/——与本 PR 无关;CI 的 pnpm install 会构建它们,且同提交的 visuals 任务通过已确认 CI 中 dev 服务器工作正常。)
  • Chromium/webkit 启动探针 —— 因缺少系统库(libgtk-4、libgstreamer 等)在本容器内失败;已记录为上述复现限制。

本轮未创建提交;工作树与进入本轮时保持一致(HEAD = fa457bda6b871a823f934d0a3ffd42203b8ed623,干净)。

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 5 by @qwen-code /takeover from 5, plus 3 change-producing round(s) since). 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.)

中文说明

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

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.4

Under tools.mode "code_mode_only", a configured agent tools list granting
exec expands to every code-mode-callable registry name — including every
connected MCP tool — so the includes() disjunct in isToolExecutionAllowed
short-circuited before the MCP raw-identity narrowing could fire:
tools: ['exec', 'mcp__github__read_*'] admitted mcp__payments__charge as
a nested exec binding. Stop the expansion from adding MCP names once the
configured list mentions MCP, letting the existing narrowing decide, and
pin the CodeModeOnly case mirroring the hybrid one.

Co-authored-by: Qwen-Coder <[email protected]>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address review round — PR #11854

Critical-only mode is active this round (window seeded at round 5 by takeover). The actionable surface held one Critical finding; the deferred non-Critical list and the advisory sections (diff-growth, residual-risk) are the workflow's own records and were not worked.

Feedback addressed

  • [rc:4094279023] (R7-1, Critical) — The MCP-narrowing guard added to the configured-tools branch could not fire under tools.mode: "code_mode_only": the CodeModeOnly expansion in getConfiguredToolExecutionAllowlist() adds every code-mode-callable registry name (which includes every mcp__* name) whenever the list grants exec, so the configuredAllowlist.includes(toolName) disjunct short-circuited before the MCP raw-identity narrowing was ever evaluated. tools: ['exec', 'mcp__github__read_*'] admitted mcp__payments__charge as a nested exec binding.
    • Reproduced first. A new test mirroring the hybrid honors MCP patterns from the tools list alone case but with ToolMode.CodeModeOnly failed on the unmodified tree: codeModeAllowedToolNames was ['mcp__github__read_file', 'mcp__github__create_issue', 'mcp__payments__charge'] instead of ['mcp__github__read_file'] — exactly the finding's witness.
    • Fix (the finding's first option). The expansion no longer adds MCP names once the configured list itself mentions MCP; those names now reach the existing raw-identity narrowing in isToolExecutionAllowed instead of entering through includes. isToolExecutionAllowed itself is unchanged — the guard added in the previous round now actually fires. The expansion's non-MCP effect is untouched: tools: ['exec'] under CodeModeOnly still admits every ordinary code-mode-callable binding (and, with no MCP entry in the list, every MCP tool, as documented).
    • Pinned. The new test passes with the fix; a mutation probe (guard condition removed) turned it red again, and restoring the guard returned the suite to green.
    • The published settings sentence ("An execution allowlist that mentions any MCP tool additionally restricts MCP bindings to matching exact names or server patterns") is mode-agnostic and is now true in both modes, so no doc/schema rewording was needed.
    • Side effect to be aware of: the configured allowlist published through runInAgentFrames (the fork-inheritance outer bound) no longer carries expansion-added MCP names for such agents, so a fork inherits no MCP tools instead of all of them — fail-closed, and reachable only when the agent also lists the direct-only agent tool explicitly. No existing test pinned the old shape (agent.test.ts: 374 passed, agent-core.test.ts: included in the 375 passed below).

Changes

  • packages/core/src/agents/runtime/agent-core.ts — expansion skips MCP names when the configured list mentions MCP (+9/−1).
  • packages/core/src/agents/runtime/agent-core.skill-gate.test.ts — new CodeModeOnly MCP-narrowing test (+48).

No merge was performed (--conflict false). No test was deleted or weakened. No deferred findings, no escalations, no open questions for maintainers from this round.

Verification

  • vitest agent-core.skill-gate.test.ts -t 'CodeModeOnly nested binding set' on the pre-fix tree — 1 failed (reproduction: received ['mcp__github__read_file', 'mcp__github__create_issue', 'mcp__payments__charge'])
  • Mutation probe: guard condition removed — new test failed again; guard restored — agent-core.skill-gate.test.ts 37 passed
  • vitest src/agents/runtime/agent-core.test.ts src/code-mode/code-mode.test.ts src/utils/fileUtils.test.ts (packages/core) — 375 passed
  • vitest src/tools/agent/agent.test.ts (packages/core) — 374 passed
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • Post-commit re-run of agent-core.skill-gate.test.ts at HEAD — 37 passed
中文说明

本轮评审处理 — PR #11854

本轮处于仅处理 Critical 的模式(窗口由 takeover 从第 5 轮起算)。可执行区域只有一条 Critical 发现;延后处理的非 Critical 清单与各项提示段(diff 增长、残余风险)是工作流自身的记录,本轮未处理。

已处理的反馈

  • [rc:4094279023](R7-1,Critical)——此前为 configured-tools 分支新增的 MCP 收窄守卫在 tools.mode: "code_mode_only" 下无法生效:getConfiguredToolExecutionAllowlist() 中的 CodeModeOnly 扩展会在列表授予 exec 时把每个 code-mode-callable 的注册表名字(包括所有 mcp__* 名字)都加进允许列表,于是 configuredAllowlist.includes(toolName) 这一析取项提前命中,MCP 原始身份收窄判定根本不会被求值。tools: ['exec', 'mcp__github__read_*'] 会把 mcp__payments__charge 放行为嵌套 exec 绑定。
    • 先复现。 新增了一个仿照 hybrid 用例 honors MCP patterns from the tools list alone、但使用 ToolMode.CodeModeOnly 的测试,在未改动的代码上失败:codeModeAllowedToolNames 实际为 ['mcp__github__read_file', 'mcp__github__create_issue', 'mcp__payments__charge'],而非 ['mcp__github__read_file']——与该发现的证据完全一致。
    • 修复(采用该发现给出的第一种方案)。 当 configured 列表本身提及 MCP 时,扩展不再加入 MCP 名字;这些名字改为走 isToolExecutionAllowed 中已有的原始身份收窄判定,而不是经由 includes 进入。isToolExecutionAllowed 本身未改动——上一轮新增的守卫现在真正生效了。扩展的非 MCP 效果保持不变:CodeModeOnly 下 tools: ['exec'] 仍会放行所有普通的 code-mode 可调用绑定(且当列表不含任何 MCP 条目时,仍放行所有 MCP 工具,与文档一致)。
    • 已固定。 新测试在修复后通过;变异探针(移除守卫条件)使其重新变红,恢复守卫后套件回到全绿。
    • 对外发布的 settings 文案("An execution allowlist that mentions any MCP tool additionally restricts MCP bindings to matching exact names or server patterns")本身不区分模式,如今在两种模式下都成立,因此无需修改文档/schema 措辞。
    • 需要注意的副作用: 经由 runInAgentFrames 发布的 configured 允许列表(fork 继承的外层边界)对这类 agent 不再携带扩展加入的 MCP 名字,因此 fork 将不再继承任何 MCP 工具而非继承全部——方向是失败即关闭(fail-closed),且只有当事 agent 同时显式列出了 direct-only 的 agent 工具时才可达。没有既有测试固定旧行为(agent.test.ts:374 通过,agent-core.test.ts:包含在下方 375 通过中)。

改动

  • packages/core/src/agents/runtime/agent-core.ts——configured 列表提及 MCP 时,扩展跳过 MCP 名字(+9/−1)。
  • packages/core/src/agents/runtime/agent-core.skill-gate.test.ts——新增 CodeModeOnly 的 MCP 收窄测试(+48)。

本轮未做合并(--conflict false)。未删除或削弱任何测试。本轮没有延后的发现、没有升级上报、没有留给 maintainer 的开放问题。

验证

  • 修复前代码上运行 vitest agent-core.skill-gate.test.ts -t 'CodeModeOnly nested binding set'——1 个失败(复现:实际收到 ['mcp__github__read_file', 'mcp__github__create_issue', 'mcp__payments__charge'])
  • 变异探针:移除守卫条件——新测试再次失败;恢复守卫——agent-core.skill-gate.test.ts 37 个通过
  • vitest src/agents/runtime/agent-core.test.ts src/code-mode/code-mode.test.ts src/utils/fileUtils.test.ts(packages/core)——375 个通过
  • vitest src/tools/agent/agent.test.ts(packages/core)——374 个通过
  • npm run build——通过
  • npm run typecheck——通过
  • npm run lint——通过
  • 提交后在 HEAD 重跑 agent-core.skill-gate.test.ts——37 个通过

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 5 by @qwen-code /takeover from 5, plus 3 change-producing round(s) since). 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.)

中文说明

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

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

🧵 Resolved all 1 selected review thread(s). · 已关闭全部选中的 1 条评审线程。

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/模型 kimi-k3 · CLI 0.24.4

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

⏸️ AutoFix paused by a review convergence signal: 3 consecutive automated review round(s) signalled machine-readable non-convergence (codes observed since the last maintainer response, or the window start if none: batch-fixes, root-cause-triage) — the reviewer keeps finding new defects at a rate that is not falling while the loop keeps widening the diff, so another automatic round is unlikely to converge this PR. The loop resumes once a maintainer responds on this PR (a review or comment counts, and steers the next rounds), and pauses again if the signal persists for 3 more round(s). Alternatives: split the recurring cluster into its own PR, batch the remaining fixes into one push, comment @qwen-code /retry to re-arm a fresh window, or @qwen-code /takeover stop to release takeover. While paused, new feedback and base conflicts stay unhandled.

中文说明

⏸️ AutoFix 已因评审收敛信号暂停:3 轮自动评审连续发出机器可读的不收敛信号(自上次维护者响应以来观察到的信号码;若无响应则自窗口开始:batch-fixes, root-cause-triage)——评审仍在以不降的速率发现新缺陷,而循环在继续扩大 diff,再跑一轮自动修复难以收敛本 PR。维护者在本 PR 上作出回应后循环自动恢复(评论或评审均可,并将作为后续轮次的指引);若信号再持续 3 轮会再次暂停。可选做法:把反复出问题的簇拆成独立 PR、把剩余修复攒成一批一次推送、评论 @qwen-code /retry 重开计数窗口、或评论 @qwen-code /takeover stop 释放接管。暂停期间,新反馈与 base 冲突不会被处理。

@qwen-code-review-bot

qwen-code-review-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

health-deep-with-session

field PR base (before) this PR (after)
activeWorkStaleMs 6 9

— Qwen Code · serve A/B

@qwen-code-review-bot

qwen-code-review-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head f669bd7. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

✅ No screenshot changes against the PR base.

Full-resolution recordings (.webm) are attached to the workflow run.

— Qwen Code · web-shell visuals

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [delay-automatic-review] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [delay-automatic-review] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-review-bot qwen-code-review-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] Prior-round Critical R1-2 (asserted in @doudouOUC's review 5217747523, packages/cli/src/config/settingsSchema.ts:36) — the original inline comment was deleted, so only the reviewer's premise survives and the claimed mechanism could not be t…

Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally (Agent 7 ran per-package vitest suites only).

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": none — the chunk's remaining items were walked clear: agent-types.ts 's new field and all five read sites, the subagent-plan-tool-policy.test.ts title-only c…; "agent reverse-audit (round 1)": mutation-run confirming that deleting the hybrid declarationNames snapshot clause leaves agent-core.skill-gate.test.ts and agent-core.fork-policy.test.ts …; "agent reverse-audit (round 1)": whether getToolExposure(ToolNames.EXEC) is 'code-mode-callable' — inferred from the passing codeModeAllowedToolNames assertions ( exec never appears in a…; "agent reverse-audit (round 2)": did not execute mutation probes (plain registerTool in place of registerPermissionDeferredFactory ; a hybrid search_memory allowlist through getFunctionDe…; "agent reverse-audit (round 2)": did not run npx vitest run src/code-mode/code-mode.test.ts to confirm the suite is green at this commit, so my claim that F1's assertions hold today is derive…, and 2 more.

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

中文说明

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

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

未审查(原文为英文):build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally (Agent 7 ran per-package vitest suites only).

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":none — the chunk's remaining items were walked clear: agent-types.ts 's new field and all five read sites, the subagent-plan-tool-policy.test.ts title-only c…;"agent reverse-audit (round 1)":mutation-run confirming that deleting the hybrid declarationNames snapshot clause leaves agent-core.skill-gate.test.ts and agent-core.fork-policy.test.ts …;"agent reverse-audit (round 1)":whether getToolExposure(ToolNames.EXEC) is 'code-mode-callable' — inferred from the passing codeModeAllowedToolNames assertions ( exec never appears in a…;"agent reverse-audit (round 2)":did not execute mutation probes (plain registerTool in place of registerPermissionDeferredFactory ; a hybrid search_memory allowlist through getFunctionDe…;"agent reverse-audit (round 2)":did not run npx vitest run src/code-mode/code-mode.test.ts to confirm the suite is green at this commit, so my claim that F1's assertions hold today is derive…,另有 2 条。

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

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

Comment on lines +742 to +743
(this.runtimeContext.getToolMode?.() === ToolMode.CodeModeOnly ||
!isHiddenByEagerAllowList(name)) &&

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] R1-1: N03: [certifies-falsely] [new-surface] The new Hybrid-only eager exclusion removes a demoted skill from both the declarations and the exec binding set, but the skill-listing predicate that this same diff rewired (willHaveSkillTool() → toolConfigAllowsSkill(this.toolConfig, hasAgentSkillExecBinding(...)), agent-core.ts:660-664) still answers true through its two non-exec clauses, so in tools.mode: "code_mode" the listing and canInvokeSkill() disagree — the exact #12424 divergence the shared predicate exists to prevent, and the invariant the author's own matrix asserts (expect(gate(core, declared)).toBe(willHaveSkill), agent-core.skill-gate.test.ts:402).

Trigger: tools.mode: "code_mode" (a value this diff adds — ToolMode.CodeMode is new at packages/core/src/tools/code-mode.ts:23) plus an active settings.tools.eager allowlist that omits skill. registerLazyTool turns that omission into status === 'deferred' → registry.registerPermissionDeferredFactory(ToolNames.SKILL, …) (config.ts:12251-12252, skill registered lazily at config.ts:12704-12708), getVisibleTools() is sourced only from settings.tools.visible (config.ts:8322-8331) so it does not contain skill, and getToolExposure('skill') is 'code-mode-callable' (code-mode.ts:39-58 — skill is in neither HIDDEN_TOOLS nor DIRECT_ONLY_TOOLS). Any subagent reaches this with the default config, because SubagentManager builds tools: configuredToolConfig?.tools ?? ['*'] (subagent-manager.ts:1150-1151). Then, traced line by line at this commit: prepareTools() sets isHiddenByEagerAllowList('skill') true (agent-core.ts:709-712 → isPermissionDeferred ∧ isDeferredAndHidden, tool-registry.ts:589 + 1352-1360, alwaysLoad defaults false — SkillTool's super() passes only 7 args, skill.ts:157-166), so the anchored clause drops skill from allowedNames; Hybrid declarationNames ⊆ allowedNames (agent-core.ts:764-773) so skill is not declared, and codeModeAllowedToolNames (agent-core.ts:759-762) excludes it. canInvokeSkill(declared) is therefore false on both routes: declaredToolNames.has(SKILL) false, and the nested route fails its codeModeAllowedToolNames?.includes(SKILL) === true term (agent-core.ts:1854-1862). willHaveSkillTool() is nevertheless true: toolConfigAllowsSkill returns inheritsRegistry || names.includes(SKILL) || reachesThroughExec (subagent-plan-tool-policy.ts:160-164), and inheritsRegistry (names.includes('*')) or names.includes('skill') short-circuits before the exec term that hasAgentSkillExecBinding correctly answers false for. Wrong outcomes at all three consumers of that answer: createChat injec

Witness:

`Source: [probe]` — permission-deferred, unrevealed, non-visible `skill`; hybrid vs Only × four `tools` lists: ``` N03 {"mode":"code_mode","tools":["*"], "willHaveSkillTool":true, "canInvokeSkill":false,"declaredHasSkill":false,"codeModeAllowedHasSkill":false} N03 {"mode":"code_mode","tools":["skill"], "willHaveSkillTool":true, "canInvokeSkill":false,"declaredHasSkill":false} N03 {"mode":"code_mode","tools":["skill","exec"],"willHaveSkillTool":true,"canInvokeSkill":false} N03 {"mode":"code_mode","tools":["exec"], "willHaveSkillTool":false,"canInvokeSkill":false} ← agrees N03 {"mode":"code_mode

Suggested fix: Give the shared predicate the same eager-scope fact prepareTools() uses, so it vetoes every route in Hybrid, not just the exec route: thread a skillEagerHidden (or mode-aware) input into toolConfigAllowsSkill computed as mode === ToolMode.CodeMode && registry.isPermissionDeferred?.(SKILL) === true && registry.isDeferredAndHidden?.(SKILL) === true, return false when it is set, and pass it from all three call sites (agent-core.ts:660-664, subagent-manager.ts:1187-1189, background-agent-resume.ts:157) — fixing only hasAgentSkillExecBinding (as R1-17 proposes) leaves the wildcard and explicit-skill clauses returning true.

The fix must not violate this existing fact: agent-core.skill-gate.test.ts:398-400 — expect(willHaveSkill).toBe(mode === ToolMode.CodeModeOnly || visibility === 'visible');. The fix must keep CodeModeOnly answering true for visibility: 'hidden' (its prepareTools() bypasses the eager filter at agent-core.ts:741, so skill really is bound there) and keep Hybrid answering true for 'visible' and 'revealed', both of which make isDeferredAndHidden false (`!this.revealedDeferred.has(name) && !this.config.getVisibleTools().has(n

Acceptance criterion: packages/core/src/agents/runtime/agent-core.skill-gate.test.ts:341-404 — the it.each([ToolMode.CodeMode, ToolMode.CodeModeOnly] × warm × visibility) matrix already builds a permission-deferred skill and asserts expect(gate(core, declared)).toBe(willHaveSkill), but instantiates only { tools: [ToolNames.EXEC] }. Add { tools: ['*'] } and { tools: [ToolNames.SKILL] } rows to the same matrix: without the fix, mode: ToolMode.CodeMode + visibility: 'hidden' (both warm values) goes R Please prove it by removing the fix and confirming that test goes red.

中文说明

新增的 Hybrid 专用 eager 排除会把被降级的 skill 同时从声明列表和 exec 绑定集合中去掉,但本 PR 改接的 skill 列表判定(willHaveSkillTool() → toolConfigAllowsSkill)仍会通过它的两个非 exec 分支返回 true。因此在 tools.mode: "code_mode" 下,列表判定与执行闸门 canInvokeSkill() 结论相反——这正是该共享判定本应防止的 #12424 分歧。触发条件:hybrid 模式 + 生效的 tools.eager 白名单未包含 skill;子智能体默认 tools: ['*'] 即可命中。后果:createChat 注入 includeAvailableSkillsReminder: true,缓存的 prompt 前缀里出现 <available_skills>,列出该智能体其实没有的工具,模型调用后得到 Tool "skill" not found;SubagentManager 保留 skillsAvailable = true,把 bundled skill 引用作为子智能体无法跟随的指针下发;后台恢复路径重复同样的错误 true。探针实测三行 hybrid 组合分歧、四行 CodeModeOnly 全部一致;打上候选修复后分歧消失且 77 个作者测试仍全绿。另需注意:Direct 模式在未被本 PR 触碰的代码上已有同样分歧,只修 hybrid 会留下一半。

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

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] R1-1: hasAgentSkillExecBinding() credits the exec gateway from mode + session registry only, so in hybrid code_mode — which this diff newly credits (the call site used to pass this.runtimeContext.getToolMode?.() === ToolMode.CodeModeOnly, agent-core.ts diff hunk @@ -646,7 +660,7 @@) — toolConfigAllowsSkill() answers true for a ToolConfig that merely names exec in tools, ignoring the same config's executionAllowedTools and the nestedExecutionAllowedTools field this PR adds. AgentCore.prepareTools()/canInvokeSkill() do honour those bounds, so the shared predicate can now disagree with the gate it exists to match.

Hybrid code_mode, registry holds exec + skill (not permission-deferred) → hasAgentSkillExecBinding() returns true. Give AgentCore the config { tools: [EXEC, READ_FILE], executionAllowedTools: [READ_FILE] } — the shape the fork path produces when fork_tools omits exec while tools: parentToolNames still contains it (buildForkExecutionAllowlist returns {'read_file', …bridges} with no exec, fork-subagent.ts:109-125; resolveForkExecutionAllowedTools passes it through, fork-subagent.ts:69-86). isToolExecutionAllowed(EXEC) is false (executionAllowedTools is defined and omits exec, so the executionAllowedTools === undefined carve-out at agent-core.ts:1972-1981 does not apply), so exec is never declared and codeModeAllowedToolNames never contains skill → canInvokeSkill(declared) is false (agent-core.ts:1852-1863). Yet toolConfigAllowsSkill returns true via reachesThroughExec, because names.includes(ToolNames.EXEC) reads the configured list, not the executable one. Consequences at the two other consumers: SubagentManager.createAgentHeadless sets skillsAvailable = true (subagent-manager.ts:1187-1195) → the subagent's Config keeps a SkillManager (agentManager = skillsAvailable ? sessionManager : null, subagent-manager.ts:1372) → a bundled reference is delivered as a pointer the agent cannot follow; and background-agent-resume.ts:1019 sets includeAvailableSkillsReminder: true → an <available_skills> block advertising skills the agent cannot load. That is the #12424 disagreement this file's own header says the single predicate prevents, and it is the invariant the author's matrix test asserts (expect(gate(core, declared)).toBe(willHaveSkill), agent-core.skill-gate.test.ts:398-400). The same blindness applies to the new nestedExecutionAllowedTools bound ({ tools: [EXEC, READ_FILE, WRITE_FILE], executionAllowedTools: [], nestedExecutionAllowedTools: [READ_FILE] }, the shape asserted at agent-core.skill-gate

Witness:

`A … willHaveSkillTool: true / canInvokeSkill: false / DISAGREES: true` (unpatched) → `A … toolConfigAllowsSkill: false / canInvokeSkill: false / DISAGREES: false` (patched); positive control `D,E,H … DISAGREES: false` on both arms, so the comparator can report agreement. Fix-constraint arm: `77 passed` with the patch applied, i.e. the pinned `{tools:[EXEC], executionAllowedTools:[EXEC]}` case (agent-core.skill-gate.test.ts:331-340) survives. **Three corrections to the finding's trace** (the verdict stands, the reasoning and two of the three consequences do not): 1. *"`isToolExecutionAllowed(E

Suggested fix: Keep the predicate's inputs to what it can actually see in the ToolConfig: in toolConfigAllowsSkill, drop reachesThroughExec when the config's own bounds exclude the route — e.g. const execExecutable = toolConfig.executionAllowedTools === undefined || toolConfig.executionAllowedTools.includes(ToolNames.EXEC); const nestedAdmitsSkill = toolConfig.nestedExecutionAllowedTools === undefined || toolConfig.nestedExecutionAllowedTools.includes(ToolNames.SKILL); and require both alongside execBindingsAvailable && names.includes(ToolNames.EXEC). (Preserve the documented "answers true where it cannot tell" bias for registry facts; this is a config fact it is handed.)

The fix must not violate this existing fact: A fix keyed on executionAllowedTools must not break the nested-binding exec carve-out that keeps skill reachable when exec itself is executable — forNestedBinding && isCodeModeEnabled(...) && configuredAllowlist.includes(ToolNames.EXEC) && getToolExposure(toolName) === 'code-mode-callable' (packages/core/src/agents/runtime/agent-core.ts:1996-2005), pinned by it.each([…, { tools: [ToolNames.EXEC], executionAllowedTools: [ToolNames.EXEC] }])('opens for an executable nested skill: %j') → `exp

Acceptance criterion: Add the two shapes above to the agent-core.skill-gate.test.ts matrix that asserts expect(gate(core, declared)).toBe(willHaveSkill) (the it.each([ToolMode.CodeMode, ToolMode.CodeModeOnly])…'matches the Skill route in $mode…' block, lines 338-401, or a sibling case): { tools: [EXEC, READ_FILE], executionAllowedTools: [READ_FILE] } under ToolMode.CodeMode must yield willHaveSkillTool() === false === gate(...). Both assertions go red without the fix (today willHaveSkillTool() is true Please prove it by removing the fix and confirming that test goes red.

中文说明

hasAgentSkillExecBinding() 只依据模式 + 会话注册表判定 exec 通道,因此在 hybrid code_mode(本 diff 新增支持的取值)下,toolConfigAllowsSkill() 会对一个仅仅在 tools 里写了 exec 的 ToolConfig 返回 true,而忽略同一个 config 上的 executionAllowedTools 和本 PR 新增的 nestedExecutionAllowedTools。探针实测:{tools:[exec,read_file], executionAllowedTools:[read_file]} 等四种组合下列表判定为 true 而闸门为 false;打上候选修复后四种中有四种转为一致,作者 77 个测试仍全绿({tools:['*'], executionAllowedTools:[read_file]} 一种仍分歧,说明修复比问题窄)。实际影响限于 prompt 前缀多出一段不可用的 <available_skills> 以及一次被拒绝的调用,执行闸门本身是正确的,且文件头已把这一方向的误差记为有意选择,故定为 Suggestion。

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

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] R1-1: [certifies-falsely] [new-surface] Still standing at head f669bd76 — re-confirmed this round. The branch's own code is unchanged since round 1 (the only new commit is a merge of main), so this is a re-report under its original id, not a new finding.

In hybrid tools.mode: "code_mode", this clause drops an eager-demoted skill from both the declarations and the nested binding set, but the skill-listing predicate this same diff rewired (willHaveSkillTool() -> toolConfigAllowsSkill(this.toolConfig, hasAgentSkillExecBinding(...)), agent-core.ts:660-664) still answers true through its two non-exec clauses, so the model-visible listing and the canInvokeSkill() gate disagree.

Failure scenario: tools.mode: "code_mode" plus an active settings.tools.eager allowlist that omits skill, and any subagent — including the default one, because SubagentManager builds tools: configuredToolConfig?.tools ?? ['*'] (subagent-manager.ts:1151). toolConfigAllowsSkill returns inheritsRegistry || names.includes(SKILL) || reachesThroughExec (subagent-plan-tool-policy.ts:159-164), and names.includes('*') short-circuits before the exec term that hasAgentSkillExecBinding correctly answers false for. Both consumers of that true then act on it: createChat gets includeAvailableSkillsReminder: true (agent-core.ts:589-594), and environmentContext.ts:589-596 builds an <available_skills> block into the stable cached prefix listing skills the agent has no route to — despite that file's own comment that announcing uninvokable skills wastes tokens. SubagentManager also keeps skillsAvailable = true (subagent-manager.ts:1187-1189). The author's own invariant expect(gate(core, declared)).toBe(willHaveSkill) (agent-core.skill-gate.test.ts:402) is violated; the matrix misses it because it instantiates only { tools: [ToolNames.EXEC] } (:391).

Witness:

Probe (round 1, re-confirmed round 2 at HEAD f669bd76): {"mode":"code_mode","tools":["*"],"willHaveSkillTool":true,"canInvokeSkill":false,"declaredHasSkill":false} {"mode":"code_mode","tools":["skill"],"willHaveSkillTool":true,"canInvokeSkill":false} {"mode":"code_mode","tools":["exec"],"willHaveSkillTool":false,"canInvokeSkill":false} <- agrees

Suggested fix: Give the shared predicate the same eager-scope fact prepareTools() uses, so it vetoes every route rather than only the exec route: thread a skillEagerHidden input into toolConfigAllowsSkill, computed at each call site as registry.isPermissionDeferred?.(SKILL) === true && registry.isDeferredAndHidden?.(SKILL) === true, and return false when it is set — passing it from all three call sites (agent-core.ts:660-664, subagent-manager.ts:1187-1189, background-agent-resume.ts:1019-1021). Guard it on mode so CodeModeOnly is untouched, since its prepareTools() bypasses the eager filter at the anchored line and skill really is bound there. Fixing only hasAgentSkillExecBinding (as ledger R1-28 proposes) leaves the wildcard and explicit-skill clauses returning true.

The fix must not violate this existing fact: agent-core.skill-gate.test.ts:398-400 — expect(willHaveSkill).toBe(mode === ToolMode.CodeModeOnly || visibility === 'visible');. CodeModeOnly must keep answering true for visibility: 'hidden' (its prepareTools() bypasses the eager filter at agent-core.ts:742, so skill really is bound there), and Hybrid must keep answering true for 'visible' and 'revealed', both of which make isDeferredAndHidden false (tool-registry.ts:1352-1361).

Acceptance criterion: agent-core.skill-gate.test.ts:341-403 — the it.each([ToolMode.CodeMode, ToolMode.CodeModeOnly] x warm x visibility) matrix already builds a permission-deferred skill and asserts expect(gate(core, declared)).toBe(willHaveSkill) at :402, but instantiates only { tools: [ToolNames.EXEC] } at :391. Add { tools: ['*'] } and { tools: [ToolNames.SKILL] } rows beside it. Please prove it by removing the fix and confirming that mode: ToolMode.CodeMode + visibility: 'hidden' goes red on both warm values, and that all rows agree once the fix is in place.

中文说明

在混合模式 tools.mode: "code_mode" 下,本行新增的 eager 降级过滤会把被降级的 skill 同时从声明列表和嵌套绑定集合中移除;但本 PR 同时改接的 skill 列表判定 willHaveSkillTool()(改为 toolConfigAllowsSkill(this.toolConfig, hasAgentSkillExecBinding(...)),agent-core.ts:660-664)仍会经由它的两个非 exec 分支返回 true。于是「模型可见的列表」与「实际调用闸门 canInvokeSkill()」结论相反。

触发条件:tools.mode: "code_mode" 且生效的 settings.tools.eager 白名单未包含 skill;任意子智能体即可命中,默认子智能体也会,因为 SubagentManager 构造 tools: configuredToolConfig?.tools ?? ['*'](subagent-manager.ts:1151)。names.includes('*') 会在 exec 分支之前短路,而 exec 分支正是 hasAgentSkillExecBinding 正确返回 false 的地方。

后果:createChat 得到 includeAvailableSkillsReminder: true;environmentContext.ts:589-596 会把 <available_skills> 写入稳定缓存前缀,列出该智能体其实无法调用的技能(该文件自己的注释就写着「宣告模型无法调用的技能是浪费 token」);SubagentManager 同时保留 skillsAvailable = true。作者自己的不变式 expect(gate(core, declared)).toBe(willHaveSkill)(agent-core.skill-gate.test.ts:402)被违反,而该矩阵没有发现,是因为它只构造了 { tools: [ToolNames.EXEC] }(:391)。

修复约束:不得违反 agent-core.skill-gate.test.ts:398-400 的既有断言 —— CodeModeOnly 在 visibility: 'hidden' 时必须仍返回 true(它的 prepareTools() 在 agent-core.ts:742 绕过 eager 过滤,skill 确实已绑定),Hybrid 在 'visible' / 'revealed' 时也必须仍返回 true。

验收标准:在 agent-core.skill-gate.test.ts:391 的矩阵中补上 { tools: ['*'] } 与 { tools: [ToolNames.SKILL] } 两行;去掉修复后,mode: ToolMode.CodeMode + visibility: 'hidden' 应在两个 warm 取值下都变红,打上修复后全部一致。

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

| `exec` | CodeMode and CodeModeOnly | No |
| Direct control | Yes | No |
| Ordinary registered tool | CodeMode only | Yes |
| Hidden bridge (`tool_search`, `tool_call`) | Existing behavior outside strict mode | No |

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] R1-2: N06: The rewritten exposure row conditions the table's only statement about bridge exposure on "strict mode" — a term that exists nowhere in the mode vocabulary, the code, or any other English document — and it groups two tools the code exposes differently.

ToolMode has exactly three values, direct / code_mode / code_mode_only (packages/core/src/tools/code-mode.ts:22-26); no setting, code path, or EN doc defines a "strict mode", and a repo-wide grep over docs/ finds the phrase only on this line (the ZH twin's 严格模式 is glossed only in a different file, docs/design/code-mode.zh-CN.md:104: "tools.mode: "code_mode_only" 选择严格模式"). A user on tools.mode: "code_mode" therefore cannot tell from this table whether hybrid counts as strict, i.e. whether tool_search/tool_call are declared for their session — and the shipped rule splits the pair the row keeps together: tool_search is a top-level direct control under CodeModeOnly (DIRECT_ONLY_TOOLS, code-mode.ts:39-51, admitted by the exposure === 'direct-only' filter in getCodeModeFunctionDeclarations, tool-registry.ts:1126-1131), while tool_call is hidden in every mode (const HIDDEN_TOOLS = new Set<string>(['tool_call']);, code-mode.ts:38). The same file's unchanged Status paragraph states that split correctly ("tool_search is now a top-level direct control … tool_call stays hidden", code-mode-only.md:12-16), so the table and the Status paragraph disagree about whether the two bridge tools share a rule. Every other row in this table names modes explicitly ("CodeMode and CodeModeOnly", "CodeMode only"), making this the one cell a reader cannot resolve to a tools.mode value.

Witness:

`Source: [probe]` sweep — `grep -rn 'strict mode|strict \`|严格模式' **/*.md` over the whole worktree: as a **tool-mode** term the phrase occurs exactly **once** in EN docs (this row) and once in its ZH twin; every other hit is an unrelated domain (TypeScript strict mode, Ajv strict, screen-reader strict, `parseLastEventId`). `ToolMode` has three values (code-mode.ts:22-26) and no setting or code path names one "strict". Mitigation I found and the finder did not weigh: the sibling EN doc this same PR adds glosses it adjectivally — `code-mode.md:22` "the strict `CodeModeOnly` exposure policy" — and

Suggested fix: Replace the cell with mode names and split the pair, e.g. two rows — | tool_search | CodeModeOnly top level; existing Direct/CodeMode behavior | No | and | tool_call | Hidden in every mode | No | — and mirror the change in docs/design/code-mode-only.zh-CN.md:54.

The fix must not violate this existing fact: const HIDDEN_TOOLS = new Set<string>(['tool_call']); and const DIRECT_ONLY_TOOLS = new Set<string>([ToolNames.TOOL_SEARCH, …]) — packages/core/src/tools/code-mode.ts:38-51; the rewritten rows must keep tool_call hidden in every mode and tool_search top-level under CodeModeOnly, matching docs/design/code-mode-only.md:12-16.

Acceptance criterion: N/A (documentation prose; no guard, branch or behaviour to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

改写后的 exposure 表格行把该表唯一一句关于 bridge 暴露的说明限定在 "strict mode" 下——而这个词在模式词表、代码和其余文档中都不存在(真实枚举是 direct / code_mode / code_mode_only)。读者无法把它对应到任何可配置取值。

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

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] R1-2: The new Chinese translation states the opposite of the English original (docs/design/code-mode-only.md:144-148) on Code Mode shell concurrency: EN says "Code Mode Bash calls bypass the read-only command classifier, with the model responsible for keeping dependent calls sequential. Other tools retain their existing concurrency classification"; ZH says the batch is "由现有的只读并发分类器处理" and drops the Bash carve-out entirely.

A reader working from the ZH design concludes that shell calls submitted together inside one exec (await Promise.all([...])) are serialized unless the read-only command checker classifies them as read-only. The shipped behavior is the reverse: packages/core/src/core/coreToolScheduler.ts:1557-1563 returns true for source === 'code_mode' + ToolNames.SHELL + Kind.Execute before isShellCommandReadOnly is ever consulted, so two dependent mutating Bash calls in one batch run in parallel. Someone reasoning about ordering safety, or reviewing a nested-call bug from the ZH doc, starts from a false premise; the ZH doc also contradicts docs/design/code-mode-concurrency.zh-CN.md:20-21 ("Code Mode 中的 Bash 调用跳过只读命令判定"), so the two Chinese designs disagree with each other.

Witness:

driving the real `isToolCallConcurrencySafe` (coreToolScheduler.ts:1546-1564) with the same args, varying only `source`: ``` mutating shell (npm install && git push origin main), source=code_mode -> true mutating shell (npm install && git push origin main), source=model -> false read-only shell (git log --oneline -5), source=code_mode -> true read-only shell (git log --oneline -5), source=model -> true ``` i.e. code-mode Bash is classified concurrency-safe *before* `isShellCommandReadOnly` is consulted — the EN sentence is right, the new ZH sentence is the inverse. ---

Suggested fix: Translate the two missing EN sentences into the ZH paragraph, e.g. replace "…Promise.all 调用会进入同一个 batch,由现有的只读并发分类器处理。" with "…Promise.all 调用会进入同一个 batch。Code Mode 中的 Bash 调用跳过只读命令判定,由模型负责让存在依赖的调用保持串行;其他工具沿用现有的并发分类。"

The fix must not violate this existing fact: packages/core/src/core/coreToolScheduler.ts:1557 — // Code Mode lets the model batch independent shell calls explicitly. guarding if (source === 'code_mode' && canonicalName === ToolNames.SHELL && kind === Kind.Execute) { return true; }; the corrected wording must not reassert read-only classification for code-mode Bash, which docs/design/code-mode-concurrency.zh-CN.md:20 also forbids.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文翻译在 Code Mode shell 并发这一点上与英文原文相反:英文说 "Code Mode Bash calls bypass the read-only command classifier",中文写成"由现有的只读并发分类器处理",恰好把豁免说成了适用。同仓库的 code-mode-concurrency.zh-CN.md:21 与英文一致,可证这是翻译错误。实测 isToolCallConcurrencySafe 对同样的 mutating shell 命令在 source=code_mode 时返回 true、source=model 时返回 false。

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

Comment on lines +87 to +88
property, an exact canonical match wins over rewritten names. If neither is an
exact match, the lexicographically first name wins. The description names the

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] R1-3: N01: The sentence this diff rewrote (pre-diff: "one warning names the omitted collision") claims the exec description names the dropped binding, but in CodeModeOnly with tool_search registered — the shipped default this same diff's new blockquote declares ("Only discovers schemas through top-level tool_search") — buildExecDescription suppresses the collision block entirely. The pre-diff wording was the accurate one; the new one is false for the mode the document is about.

A registry holding mcp__srv__get-data and mcp__srv__get_data normalizes both to mcp__srv__get_data, so planCodeModeBindings keeps the exact match and records { jsName, kept: 'mcp__srv__get_data', omitted: 'mcp__srv__get-data' } (packages/core/src/tools/code-mode.ts:111-124). In a tools.mode: "code_mode_only" session, getCodeModeFunctionDeclarations computes searchAvailable = !!this.getTool(TOOL_SEARCH) && (!allowedNames || allowedNames.has(TOOL_SEARCH)) → true (packages/core/src/tools/tool-registry.ts:1121-1123, passed at :1135), and buildExecDescription sets const searchAvailable = codeModeOnly && (options.searchAvailable ?? false) → true (code-mode.ts:253), so collisionText = (searchAvailable ? [] : plan.collisions) → [] (code-mode.ts:264) and the description ends with no Name collisions: block (code-mode.ts:325). The repo's own test pins that suppression: packages/core/src/code-mode/code-mode.test.ts:723-739 registers exec + tool_search under code_mode_only with hidden-tool/hidden_tool and asserts expect(description).not.toContain('hidden-tool') and not.toContain('hidden_tool'). The only surface that names the drop is debugLogger.warn inside warnCodeModeCollisions (tool-registry.ts:1158-1166) — debug-gated, not model-facing. Concrete cost: the model is never told the binding was dropped in the default Only configuration, and the maintainer triaging "tool X is unreachable through exec / exec called the wrong MCP tool" reads this document as the normative collision contract, goes looking for a missing line in the description generator, and finds code that is behaving exactly as written. The same false sentence ships in the new Chinese twin ("描述会指出被省略的冲突项。", code-mode-only.zh-CN.md:73-74).

Witness:

`Source: [probe]` — CodeModeOnly registry with `exec` + `tool_search` + colliding `get-data`/`get_data` (non-deferred, so deferral filtering cannot explain the absence): `N01-only {"collisions":[{"jsName":"get_data","kept":"get_data","omitted":"get-data"}],"descriptionHasCollisionsBlock":false,"descriptionNamesOmittedTool":false,"declaredNames":["exec","tool_search"]}` Positive controls (the probe *can* see the block): `N01-only-nosearch {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":true}` · `N01-hybrid {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":t

Suggested fix: Scope the claim to the configuration where it holds and name the other surface, e.g. "When search is unavailable in the current scope, the description names the omitted collision; when tool_search is available the collision block is left out of the description and the drop is logged once through the debug logger." Apply the identical correction to docs/design/code-mode-only.zh-CN.md:73-74.

The fix must not violate this existing fact: const collisionText = (searchAvailable ? [] : plan.collisions) — packages/core/src/tools/code-mode.ts:264, with const searchAvailable = codeModeOnly && (options.searchAvailable ?? false); at :253. The corrected sentence must keep the hybrid case true: decorateCodeModeDeclarations passes codeModeOnly: false (packages/core/src/tools/tool-registry.ts:1087-1092), so hybrid descriptions always emit the block.

Acceptance criterion: N/A (documentation prose). The behaviour the corrected sentence must match is already pinned from both sides: packages/core/src/code-mode/code-mode.test.ts:723-739 (CodeModeOnly + tool_search → neither collision name appears) and :563-591 'describes normalized-name collisions on the hybrid surface' (hybrid description contains '- read-file is omitted because it collides with read_file as tools.read_file.'). Please prove it by removing the fix and confirming that test goes red.

中文说明

本次改写后的句子声称 exec 的 description 会点出被丢弃的 binding,但在 CodeModeOnly 下被丢弃的 binding 恰恰不会进入 description;改写前的措辞("one warning names the omitted collision")才是准确的——warnCodeModeCollisions 在所有模式下都会触发。中英文两份设计文档同句同错。

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

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] R1-3: The new ZH version's 状态 section omits the English Status paragraph at docs/design/code-mode-only.md:12-16 ("Partly superseded by Lazy Code Mode: tool_search is now a top-level direct control, and exec omits deferred tool signatures while search is available. The exposure table and the deferred-schema paragraph below describe this MVP; tool_call stays hidden."), and the ZH Sandbox paragraph at line 103 likewise drops EN's clause "whose own scheduler/ACP timeouts remain authoritative" (docs/design/code-mode-only.md:119-121).

The ZH reader never learns that tool_call remains hidden under current behavior, nor that the 暴露策略 table and the deferred-schema paragraph below it describe a superseded MVP rather than shipped behavior — the top blockquote only supersedes "隐藏 tool_search / 始终完整 schema", not the table as a whole. Concretely the ZH table row "隐藏 bridge(tool_search、tool_call)| 严格模式之外沿用既有行为" plus ZH line 76 "CodeModeOnly 会隐藏 tool_search" read as current policy, which the lazy-loading design replaced. At ZH:103 the dropped clause leaves the reader with no statement of which timeout governs while the guest CPU budget and watchdog are paused on a host tool. This violates the repo's own rule in docs/design/README.md: "neither version omits decisions, limitations, acceptance criteria, or follow-up work" and "Do not leave one version with an earlier requirement … that the other version has already answered."

Witness:

`grep -n "Partly superseded|stays hidden|scheduler/ACP timeouts remain authoritative" code-mode-only.md code-mode-only.zh-CN.md` → **3 hits, all in the EN file** (`code-mode-only.md:12`, `:15`, `:120`), zero in the ZH file; and `sed -n '7,11p' code-mode-only.zh-CN.md` → ``` ## 状态 已为 [#10377](…) 实现。 该功能为可选功能,默认关闭。 ``` So the ZH 状态 carries neither the "Partly superseded by Lazy Code Mode … The exposure table and the deferred-schema paragraph below describe this MVP; `tool_call` stays hidden" scope note (EN 12-15) nor, at ZH:103-104, EN's "whose own scheduler/ACP timeouts remain authoritative" (E

Suggested fix: Translate the EN Status paragraph into 状态 (adding the tool_call 仍隐藏 statement and the scope note that the exposure table and deferred-schema paragraph describe the MVP), and add "已注册 host 工具自身的 scheduler/ACP timeout 仍然生效" to the ZH watchdog sentence at line 103.

The fix must not violate this existing fact: docs/design/code-mode-only.md:12-16 is the source text the translation must carry, and docs/design/README.md ("Keep section order and heading levels aligned") requires it land in the existing 状态 section rather than as a new heading.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文版的「状态」小节漏掉了英文 Status 段落(docs/design/code-mode-only.md:12-16)中"Partly superseded by Lazy Code Mode"这一段,导致中英两版结构不同步,违反 AGENTS.md 对双语文档"完整且同步"的要求。

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

Comment on lines +9 to +10
已为 [#10377](https://github.com/QwenLM/qwen-code/issues/10377) 实现。
该功能为可选功能,默认关闭。

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] R1-3: The new ZH version's 状态 section omits the English Status paragraph at docs/design/code-mode-only.md:12-16 ("Partly superseded by Lazy Code Mode: tool_search is now a top-level direct control, and exec omits deferred tool signatures while search is available. The exposure table and the deferred-schema paragraph below describe this MVP; tool_call stays hidden."), and the ZH Sandbox paragraph at line 103 likewise drops EN's clause "whose own scheduler/ACP timeouts remain authoritative" (docs/design/code-mode-only.md:119-121).

The ZH reader never learns that tool_call remains hidden under current behavior, nor that the 暴露策略 table and the deferred-schema paragraph below it describe a superseded MVP rather than shipped behavior — the top blockquote only supersedes "隐藏 tool_search / 始终完整 schema", not the table as a whole. Concretely the ZH table row "隐藏 bridge(tool_search、tool_call)| 严格模式之外沿用既有行为" plus ZH line 76 "CodeModeOnly 会隐藏 tool_search" read as current policy, which the lazy-loading design replaced. At ZH:103 the dropped clause leaves the reader with no statement of which timeout governs while the guest CPU budget and watchdog are paused on a host tool. This violates the repo's own rule in docs/design/README.md: "neither version omits decisions, limitations, acceptance criteria, or follow-up work" and "Do not leave one version with an earlier requirement … that the other version has already answered."

Witness:

`grep -n "Partly superseded|stays hidden|scheduler/ACP timeouts remain authoritative" code-mode-only.md code-mode-only.zh-CN.md` → **3 hits, all in the EN file** (`code-mode-only.md:12`, `:15`, `:120`), zero in the ZH file; and `sed -n '7,11p' code-mode-only.zh-CN.md` → ``` ## 状态 已为 [#10377](…) 实现。 该功能为可选功能,默认关闭。 ``` So the ZH 状态 carries neither the "Partly superseded by Lazy Code Mode … The exposure table and the deferred-schema paragraph below describe this MVP; `tool_call` stays hidden" scope note (EN 12-15) nor, at ZH:103-104, EN's "whose own scheduler/ACP timeouts remain authoritative" (E

Suggested fix: Translate the EN Status paragraph into 状态 (adding the tool_call 仍隐藏 statement and the scope note that the exposure table and deferred-schema paragraph describe the MVP), and add "已注册 host 工具自身的 scheduler/ACP timeout 仍然生效" to the ZH watchdog sentence at line 103.

The fix must not violate this existing fact: docs/design/code-mode-only.md:12-16 is the source text the translation must carry, and docs/design/README.md ("Keep section order and heading levels aligned") requires it land in the existing 状态 section rather than as a new heading.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文版的「状态」小节漏掉了英文 Status 段落(docs/design/code-mode-only.md:12-16)中"Partly superseded by Lazy Code Mode"这一段,导致中英两版结构不同步,违反 AGENTS.md 对双语文档"完整且同步"的要求。

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

Comment on lines +123 to +124
并发链路。guest 的连续 await 会生成连续 batch;`Promise.all` 调用会进入同一个
batch,由现有的只读并发分类器处理。嵌套 request id 包含父 id,并携带

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] R1-2: The new Chinese translation states the opposite of the English original (docs/design/code-mode-only.md:144-148) on Code Mode shell concurrency: EN says "Code Mode Bash calls bypass the read-only command classifier, with the model responsible for keeping dependent calls sequential. Other tools retain their existing concurrency classification"; ZH says the batch is "由现有的只读并发分类器处理" and drops the Bash carve-out entirely.

A reader working from the ZH design concludes that shell calls submitted together inside one exec (await Promise.all([...])) are serialized unless the read-only command checker classifies them as read-only. The shipped behavior is the reverse: packages/core/src/core/coreToolScheduler.ts:1557-1563 returns true for source === 'code_mode' + ToolNames.SHELL + Kind.Execute before isShellCommandReadOnly is ever consulted, so two dependent mutating Bash calls in one batch run in parallel. Someone reasoning about ordering safety, or reviewing a nested-call bug from the ZH doc, starts from a false premise; the ZH doc also contradicts docs/design/code-mode-concurrency.zh-CN.md:20-21 ("Code Mode 中的 Bash 调用跳过只读命令判定"), so the two Chinese designs disagree with each other.

Witness:

driving the real `isToolCallConcurrencySafe` (coreToolScheduler.ts:1546-1564) with the same args, varying only `source`: ``` mutating shell (npm install && git push origin main), source=code_mode -> true mutating shell (npm install && git push origin main), source=model -> false read-only shell (git log --oneline -5), source=code_mode -> true read-only shell (git log --oneline -5), source=model -> true ``` i.e. code-mode Bash is classified concurrency-safe *before* `isShellCommandReadOnly` is consulted — the EN sentence is right, the new ZH sentence is the inverse. ---

Suggested fix: Translate the two missing EN sentences into the ZH paragraph, e.g. replace "…Promise.all 调用会进入同一个 batch,由现有的只读并发分类器处理。" with "…Promise.all 调用会进入同一个 batch。Code Mode 中的 Bash 调用跳过只读命令判定,由模型负责让存在依赖的调用保持串行;其他工具沿用现有的并发分类。"

The fix must not violate this existing fact: packages/core/src/core/coreToolScheduler.ts:1557 — // Code Mode lets the model batch independent shell calls explicitly. guarding if (source === 'code_mode' && canonicalName === ToolNames.SHELL && kind === Kind.Execute) { return true; }; the corrected wording must not reassert read-only classification for code-mode Bash, which docs/design/code-mode-concurrency.zh-CN.md:20 also forbids.

Acceptance criterion: N/A (documentation text; no guard, branch, or behavior to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

新增中文翻译在 Code Mode shell 并发这一点上与英文原文相反:英文说 "Code Mode Bash calls bypass the read-only command classifier",中文写成"由现有的只读并发分类器处理",恰好把豁免说成了适用。同仓库的 code-mode-concurrency.zh-CN.md:21 与英文一致,可证这是翻译错误。实测 isToolCallConcurrencySafe 对同样的 mutating shell 命令在 source=code_mode 时返回 true、source=model 时返回 false。

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

),
),
])(
'uses only exposed tools for image guidance: $declared, code mode $codeModeOnly',

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] R1-30: The it.each title still interpolates only $declared and $codeModeOnly, so the 16 cases this diff adds — whose discriminators are toolMode and allowedNames — collapse onto titles that are already taken by the pre-existing rows.

Five rows now share the title uses only exposed tools for image guidance: read_file,exec, code mode false: the added hybrid-code-mode row that expects ' If details are too small, call tools.zoom_image …', the narrowed-agent row (allowedNames: ['read_file']) that expects '', and the three flatMap rows over declared: ['read_file','exec'] × [[], ['read_file'], ['read_file','zoom_image']] that also expect ''. Four more collide on read_file,exec,tool_search,tool_call, code mode false (one expects the tool_search/tool_call bridge hint, three expect ''). When one of these fails, the reporter prints an ambiguous name — you cannot tell whether the regression is in the reachability computation or in the new ambient-allowlist gate without counting array positions, which is exactly the distinction this diff turns on.

Witness:

not run — probe/mutation via `review scratch-tree` and `base-tree`: unavailable in this environment (repo-local git includeIf resolves to a missing credentials file, so scratch-tree refuses to create a tree; the base build timed out), so the verdict rests on a line-by-line source trace plus repo-wide greps

Suggested fix: Add the new discriminators to the title, e.g. 'uses only exposed tools for image guidance: $declared, code mode $codeModeOnly, tool mode $toolMode, allowed $allowedNames'.

The fix must not violate this existing fact: toolMode?: ToolMode; (fileUtils.test.ts:1187) and allowedNames?: string[]; (fileUtils.test.ts:1191) are optional in the it.each<{…}> row type, and the five pre-existing rows starting at fileUtils.test.ts:1194 supply neither — interpolating $toolMode/$allowedNames renders those rows with no value unless the fix also gives every row both keys.

Acceptance criterion: N/A — a title-only change pins no behaviour; the existing it.each body (asserting text: `Image overview: 20x10; oriented source: 20x10.${hint}` at fileUtils.test.ts:1332-1335) already covers the behaviour. Please prove it by removing the fix and confirming that test goes red.

中文说明

it.each 的标题仍然只插值 $declared 和 $codeModeOnly,因此本 diff 新增的 16 个用例(其区分维度是 toolMode 和 allowedNames)会塌缩到与既有用例相同的标题上,失败时无法从名字定位是哪一例。

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

Comment on lines +1674 to +1676
const ambientAllowedNames = getCurrentCodeModeAllowedNames();
const zoomAvailable =
ambientAllowedNames === 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.

[Suggestion] R1-31: The new guard treats any defined ambient allowlist as "route unknown", but the ambient store is produced by AgentCore.prepareTools() as exactly the agent's exec binding set, so the nested route is the one route it can answer — and it is answered with silence.

Producer/consumer trace. AgentCore.prepareTools() sets this.codeModeAllowedToolNames to allowedNames.filter(name => getToolExposure(name) === 'code-mode-callable') (agent-core.ts:759-762); processFunctionCalls now stamps it on every request, not just exec/tool_search (agent-core.ts:2684-2691, changed by this diff); CoreToolScheduler enters the ALS with it around every execution (coreToolScheduler.ts:5868, 5914), and runWithCodeModeAllowedNames(undefined, …) deliberately does not clear the store, so a nested read_file dispatched from inside exec inherits the parent agent's binding set. Result: a hybrid or CodeModeOnly subagent whose codeModeAllowedToolNames contains zoom_image reads an image, ambientAllowedNames !== undefined, zoomAvailable is false, and zoomHint is '' — the model is never told tools.zoom_image exists even though the very value in scope proves this agent's exec binds it. Pre-diff, a CodeModeOnly agent got that hint (from the session plan); the PR trades a sometimes-wrong hint for no hint at all. The stated reason in the added comment — "An agent's target allowlist does not identify its declared direct, bridge, or exec routes" — is true of the direct and bridge routes (declaredTools comes from the registry's session-wide getFunctionDeclarations(), not the agent's filtered surface) but false of the exec route.

Witness:

not run — probe/mutation via `review scratch-tree` and `base-tree`: unavailable in this environment (repo-local git includeIf resolves to a missing credentials file, so scratch-tree refuses to create a tree; the base build timed out), so the verdict rests on a line-by-line source trace plus repo-wide greps

Suggested fix: Recover the nested route instead of blanket-suppressing: when ambientAllowedNames is defined, set zoomAvailable from useNestedZoom && ambientAllowedNames.includes('zoom_image') (leave the direct and bridge routes suppressed, since declaredTools is a session fact inside an agent).

The fix must not violate this existing fact: The sibling rows must keep passing unchanged: allowedNames: ['read_file'] with declared: ['read_file','exec'] asserts hint: '' (fileUtils.test.ts:1261-1270, "the calling agent's narrowed plan does not [bind it]"), and declared: ['read_file','tool_search','tool_call'] with allowedNames: ['read_file'] asserts hint: '' (fileUtils.test.ts:1271-1283). The fix must therefore key on membership of zoom_image in the ambient set, not merely on the set being defined, and must not resurrec

Acceptance criterion: packages/core/src/utils/fileUtils.test.ts:1285-1298 — the flatMap row declared: ['read_file','exec'] × allowedNames: ['read_file','zoom_image'] currently asserts hint: ''; it must be flipped to ' If details are too small, call tools.zoom_image with coordinates normalized from 0 to 1000.'. Removing the membership check returns it to '' and the row goes red. Please prove it by removing the fix and confirming that test goes red.

中文说明

新增的这道判断把任何已定义的 ambient 白名单都当作"路由未知",但该 ambient store 恰恰是 AgentCore.prepareTools() 产出的、该智能体的 exec 绑定集合,也就是唯一能回答的那条路由——而它被回答以沉默。结果是 ambient 白名单里明明含有 zoom_image 的 hybrid/CodeModeOnly 子智能体读取图片后拿不到任何提示;改动前 CodeModeOnly 智能体是能拿到该提示的。

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

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] R1-31: The new guard treats any defined ambient allowlist as "route unknown", but the ambient store is produced by AgentCore.prepareTools() as exactly the agent's exec binding set, so the nested route is the one route it can answer — and it is answered with silence.

Producer/consumer trace. AgentCore.prepareTools() sets this.codeModeAllowedToolNames to allowedNames.filter(name => getToolExposure(name) === 'code-mode-callable') (agent-core.ts:759-762); processFunctionCalls now stamps it on every request, not just exec/tool_search (agent-core.ts:2684-2691, changed by this diff); CoreToolScheduler enters the ALS with it around every execution (coreToolScheduler.ts:5868, 5914), and runWithCodeModeAllowedNames(undefined, …) deliberately does not clear the store, so a nested read_file dispatched from inside exec inherits the parent agent's binding set. Result: a hybrid or CodeModeOnly subagent whose codeModeAllowedToolNames contains zoom_image reads an image, ambientAllowedNames !== undefined, zoomAvailable is false, and zoomHint is '' — the model is never told tools.zoom_image exists even though the very value in scope proves this agent's exec binds it. Pre-diff, a CodeModeOnly agent got that hint (from the session plan); the PR trades a sometimes-wrong hint for no hint at all. The stated reason in the added comment — "An agent's target allowlist does not identify its declared direct, bridge, or exec routes" — is true of the direct and bridge routes (declaredTools comes from the registry's session-wide getFunctionDeclarations(), not the agent's filtered surface) but false of the exec route.

Witness:

not run — probe/mutation via `review scratch-tree` and `base-tree`: unavailable in this environment (repo-local git includeIf resolves to a missing credentials file, so scratch-tree refuses to create a tree; the base build timed out), so the verdict rests on a line-by-line source trace plus repo-wide greps

Suggested fix: Recover the nested route instead of blanket-suppressing: when ambientAllowedNames is defined, set zoomAvailable from useNestedZoom && ambientAllowedNames.includes('zoom_image') (leave the direct and bridge routes suppressed, since declaredTools is a session fact inside an agent).

The fix must not violate this existing fact: The sibling rows must keep passing unchanged: allowedNames: ['read_file'] with declared: ['read_file','exec'] asserts hint: '' (fileUtils.test.ts:1261-1270, "the calling agent's narrowed plan does not [bind it]"), and declared: ['read_file','tool_search','tool_call'] with allowedNames: ['read_file'] asserts hint: '' (fileUtils.test.ts:1271-1283). The fix must therefore key on membership of zoom_image in the ambient set, not merely on the set being defined, and must not resurrec

Acceptance criterion: packages/core/src/utils/fileUtils.test.ts:1285-1298 — the flatMap row declared: ['read_file','exec'] × allowedNames: ['read_file','zoom_image'] currently asserts hint: ''; it must be flipped to ' If details are too small, call tools.zoom_image with coordinates normalized from 0 to 1000.'. Removing the membership check returns it to '' and the row goes red. Please prove it by removing the fix and confirming that test goes red.

中文说明

新增的这道判断把任何已定义的 ambient 白名单都当作"路由未知",但该 ambient store 恰恰是 AgentCore.prepareTools() 产出的、该智能体的 exec 绑定集合,也就是唯一能回答的那条路由——而它被回答以沉默。结果是 ambient 白名单里明明含有 zoom_image 的 hybrid/CodeModeOnly 子智能体读取图片后拿不到任何提示;改动前 CodeModeOnly 智能体是能拿到该提示的。

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

Comment on lines +1687 to +1688
// An agent's target allowlist does not identify its declared
// direct, bridge, or exec routes. Do not advertise session routes.

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] R1-47: N32: The reachability rule this hunk states is enforced only on the image-overview branch; the omni-delivery branch that returns 48 lines earlier inside the same case 'image' still advertises zoom_image unconditionally, so on an omni-policy session the new gate never executes for images at all.

tools.mode: "code_mode_only" (or "code_mode") plus a subagent whose surface withholds zoom_image, on a model where modalities.image is true and config.isOmniEnabled?.() / omni.isOmniDeliveryActive(config) hold — all model/policy facts, independent of tool mode (fileUtils.ts:1389-1410). processSingleFileContent takes if (omniModule) { return await omniModule.readMediaViaOmniDelivery({…}) } at fileUtils.ts:1638-1650 and never reaches the changed code. packages/core/src/omni/index.ts:1189-1196 then pushes Image <name>: full resolution WxH px. Use zoom_image for a closer look at details. (or the degradation variant Use zoom_image to inspect details — it reads the original file.) with no registry, tool-mode, or ambient-allowlist check. The agent emits a zoom_image call and AgentCore.processFunctionCalls rejects it with Tool "zoom_image" not found. Tools must use the exact names provided. (agent-core.ts:2201-2206) — one wasted turn, which is exactly the cost the deleted #12271 comment and this new gate exist to prevent. The identical agent on a non-omni model gets '' and does not waste the turn, so after this diff whether the invariant holds depends on the session's model policy rather than on the agent's surface.

Witness:

not run — probe/mutation via `review scratch-tree` and `base-tree`: unavailable in this environment (repo-local git includeIf resolves to a missing credentials file, so scratch-tree refuses to create a tree; the base build timed out), so the verdict rests on a line-by-line source trace plus repo-wide greps

Suggested fix: Hoist the route resolution (registry facts + getCurrentCodeModeAllowedNames()) into one helper that returns the hint string, call it before the if (omniModule) early return, and pass the result into readMediaViaOmniDelivery so its two text variants name zoom_image / tools.zoom_image / nothing under the same rule the overview branch now uses.

The fix must not violate this existing fact: packages/core/src/omni/index.ts:1183-1187 — "calling them 'full resolution' would contradict the disclosure pushed right below and steer the model away from zoom_image, the exact remedy for degradation-stripped detail (it reads the original from disk)". Suppression must key on the route being genuinely absent for the calling agent, not on the degradation/disclosure path, or the disclosure and the hint start contradicting each other again.

Acceptance criterion: packages/core/src/omni/index.test.ts:410-464 (adds a resolution + zoom_image hint part for images) currently pins the unconditional text. Add a sibling case that drives the same delivery with the route absent — e.g. wrap in runWithCodeModeAllowedNames(['read_file'], …), or a registry double whose getFunctionDeclarations() omits zoom_image — and assert the pushed part's text does not contain zoom_image. It is red while the omni branch ignores reachability. Please prove it by removing the fix and confirming that test goes red.

中文说明

这段 hunk 所陈述的可达性规则只在 image-overview 分支上被执行;同一个 case 'image' 内提前 48 行 return 的 omni-delivery 分支并不受它约束,形成同族分支的不对称。

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

| `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. |
| `debug` | `boolean` | `false` | Enable debug mode for verbose logging from the CLI process. |
| `maxSessionTurns` | `number` | `-1` (unlimited) | Maximum number of conversation turns before the session automatically terminates. Must be an integer. A turn consists of a user message and an assistant response. |
| `coreTools` | `string[]` | - | Uses the legacy `coreTools` / CLI `--core-tools` allowlist semantics. If specified, only matching core tools are registered for the session. This is the only allowlist-style option that restricts built-in tool registration; a whole-tool `permissions.deny` / `excludeTools` rule (and `tools.disabled` in settings.json) also removes a tool from the registry. `permissions.allow` in settings.json is pure auto-approval and never removes, demotes, or hides a tool (#10075). To keep a tool's schema out of the initial model request, use `tools.eager` in settings.json (requires restart, #9827) — `tool_search`, `tool_call`, `structured_output`, plan-mode lifecycle tools, `task_stop`, `mcp__*` and `computer_use__*` tools are exempt from that allowlist and keep their normal loading; tools demoted this way stay registered and reachable through `tool_search` + `tool_call` while both bridge tools are registered — when either is unregistered (`tools.toolSearch.enabled: false` denies both; a `tool_search` or `tool_call` deny rule, or a `tools.disabled` entry removes one) the demoted tools that remain hidden are absent from top-level declarations and cannot be reached through the bridge for that session, and a warning is written to the CLI process's stderr (SDK forwards it only with piped stderr and effective `debug` logging; an explicit `logLevel` always wins). These bridge and warning rules apply to direct and hybrid code modes. On the session surface in hybrid mode, while `exec` itself is registered (container and SSH execution warn and fall back to direct tools without it), exec retains callable nested bindings; their schemas are included in exec when either bridge tool is unavailable. CodeModeOnly discovers deferred schemas through top-level tool_search and invokes them through exec. It skips deferred preload and startup catalogs; tools.eager reduces the initial exec description. When search is unavailable in the current scope, exec includes all allowed signatures. In Hybrid mode, AgentCore excludes tools still hidden by tools.eager from nested bindings. In both code modes, agent allowlists that do not grant `exec` narrow nested bindings. Inheriting or explicitly granting `exec` keeps all otherwise admitted ordinary code-mode-callable bindings. An execution allowlist that mentions any MCP tool additionally restricts MCP bindings to matching exact names or server patterns. In direct and hybrid modes they stay registered, so a direct call by their own name is still evaluated and approved normally — except tools also listed in `tools.visible`, which are declared upfront, and sessions whose live history contains a direct call to a still-hidden demoted tool, which any tool-set refresh (resume, MCP discovery, the first plan-mode entry in a session, a subagent definition change) re-declares.; to remove a tool entirely, use a whole-tool `excludeTools` / `permissions.deny` rule — a rule with a specifier (such as `'Bash(rm *)'`) only denies matching invocations at runtime. MCP tools are exempt from deny-based removal: hide them with the per-server `excludeTools` / `tools.disabled` filters instead (deny still blocks their calls at runtime). Example: `['read_file', 'edit', 'run_shell_command']`. |

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] R1-48: N33: The rewritten coreTools contract is stored twice, byte-identical, in two files this diff edits — nothing generates one from the other and nothing asserts they agree.

I extracted the last cell of the coreTools row from both files at HEAD and compared: len == 3226 for each and a == b is True. This diff had to apply the same ~1.5 KB rewrite to both (docs/developers/sdk-typescript.md hunk @@ -53,27 +53,27 @@ carries the identical added sentence "On the session surface in hybrid mode…", and the identical removed sentence "These bridge and warning rules apply to direct tool mode."). The next semantic correction — e.g. the hybrid/exec condition in Finding 1, or the canSearchDeferredSchemas = false hardcode on the filtered path at tool-registry.ts:1538 that makes "when either bridge tool is unavailable" wrong for subagents — has to be found and made in both cells or the two published SDK references contradict each other, and an integrator who reads the stale one builds a client against a contract the shipped CLI no longer honours. Nine independently falsifiable statements were added to each copy in this diff and no test pins any of them in either file.

Witness:

``` ARM 1 intact tree (both real files): PASS: coreTools description cells identical (3226 chars) ARM 2 one sentence edited in a /tmp copy (positive control): FAIL: coreTools description cells diverged … first difference at char 1411: A: …always wins). These bridge and warning rules apply to direct and hybrid code modes. On the session surface… B: …always wins). These bridge and warning rules apply to direct code mode only. On the session surface… ARM 3 pre-PR cells replayed from the diff's own '-' lines: FAIL: old-docs-cell.md: 2154 chars / old-readme-cell.md: 2451 chars … first difference at

Suggested fix: Keep one authoritative copy and derive the other (a small generate step, or a <!-- prettier-ignore -->-style include), or — cheapest — add a test that reads both files, extracts the coreTools row's description cell, and asserts the two strings are equal, so drift fails CI instead of shipping.

Acceptance criterion: A new test (e.g. packages/sdk-typescript/src/readme-doc-sync.test.ts) that fails when one cell is edited without the other; it is red the moment either file's coreTools description diverges. Please prove it by removing the fix and confirming that test goes red.

中文说明

改写后的 coreTools 契约在本 diff 编辑的两个文件里逐字节重复存放,既没有由一方生成另一方,也没有任何测试断言二者一致。实测:两文件 22 个共有选项行的 description 全部逐字节相同(coreTools 行为 3226 字符);在 merge base 上二者曾分别为 2154 与 2451 字符、互相矛盾,正是本 PR 手工同步修好的。同一句新增文案在 HEAD 上散布于 6 个文件、11 处手工维护位置(另有 3 处由 CI 固定),而本 diff 手工改了其中 5 个文件。

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

'settings.label.tools.codeModeOnly': '仅代码模式(实验性)',
'settings.description.tools.codeModeOnly':
'普通工具只通过隔离的 exec JavaScript 工具暴露给模型。直接控制类工具仍然可用。在 safe 和 bare 模式下忽略。',
'settings.label.tools.mode': '工具模式(实验性)',

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] R1-32: The row became an enum but the ZH dictionary only translates its label and description — the three option labels have no settings.option.tools.mode.* entries, so a zh-CN user gets the raw English schema labels.

With language zh-CN, open Settings → Tools → 工具模式(实验性). SettingsMessage.tsx:745-757 builds the picker from the daemon descriptor and calls formatSettingOption, which looks up settings.option.tools.mode.direct|code_mode|code_mode_only; none exist (repo-wide grep for settings.option.tools.mode returns zero source hits), so translateSettingText falls back to the served labels — { value: ToolMode.Direct, label: 'Default' } etc. (packages/cli/src/config/settingsSchema.ts:2917-2919, forwarded verbatim by buildSettingsResponse at packages/cli/src/serve/routes/workspace-settings.ts:233). The Chinese panel therefore shows "Default / Code Mode / Code Mode Only" under a fully Chinese label and description, while every other enum in this same ZH map is translated — settings.option.ui.chatWidth.* (messages.ts:192-193), settings.option.review.effort.* (234-237), settings.option.model.reasoningEffort.* (307-311), settings.option.tools.workflowSizeGuideline.* (362-366), settings.option.policy.permissionStrategy.* (377-380). Before this diff the row was a boolean rendered through the already-translated settings.value.on/off, so the untranslated option list is new. "Default" is also the least self-describing of the three for a Chinese reader, since the description calls that mode "Direct".

Witness:

not run — probe/mutation via `review scratch-tree` and `base-tree`: unavailable in this environment (repo-local git includeIf resolves to a missing credentials file, so scratch-tree refuses to create a tree; the base build timed out), so the verdict rests on a line-by-line source trace plus repo-wide greps

Suggested fix: Add the three option keys next to the new label/description, e.g. 'settings.option.tools.mode.direct': '直接(默认)', 'settings.option.tools.mode.code_mode': 'Code Mode', 'settings.option.tools.mode.code_mode_only': 'Code Mode Only' (keeping the two proper-noun mode names as-is if that is the intended house rendering).

The fix must not violate this existing fact: Keys must be spelled exactly settings.option.${setting.key}.${String(value)} (packages/web-shell/client/components/messages/SettingsMessage.tsx:186) with the values the schema ships — { value: ToolMode.Direct, label: 'Default' }, { value: ToolMode.CodeMode, ... }, { value: ToolMode.CodeModeOnly, ... } (packages/cli/src/config/settingsSchema.ts:2917-2919), i.e. direct / code_mode / code_mode_only, not the label text.

Acceptance criterion: A case in packages/web-shell/client/components/messages/SettingsMessage.dom.test.tsx (or a new collocated settings/messages.test.ts) that renders/derives the option list for a tools.mode enum descriptor under the zh-CN dictionary and asserts the Direct option is not the raw schema label 'Default' — equivalently, asserts SETTINGS_MESSAGES_ZH['settings.option.tools.mode.direct'] is defined. Delete the added keys and that assertion must go red; no existing test covers it (`SettingsMessage Please prove it by removing the fix and confirming that test goes red.

中文说明

该行变成了枚举,但中文字典只翻译了它的 label 和 description——三个选项标签没有对应的 settings.option.tools.mode.* 条目,因此 zh-CN 用户在完全中文的标签与描述下会看到原始的英文选项 "Default / Code Mode / Code Mode Only"。同一 diff 已为 CLI 翻译了这三个标签并加了 must-translate 测试,可见 web-shell 这一处属于遗漏而非有意。

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

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] R1-32: The row became an enum but the ZH dictionary only translates its label and description — the three option labels have no settings.option.tools.mode.* entries, so a zh-CN user gets the raw English schema labels.

With language zh-CN, open Settings → Tools → 工具模式(实验性). SettingsMessage.tsx:745-757 builds the picker from the daemon descriptor and calls formatSettingOption, which looks up settings.option.tools.mode.direct|code_mode|code_mode_only; none exist (repo-wide grep for settings.option.tools.mode returns zero source hits), so translateSettingText falls back to the served labels — { value: ToolMode.Direct, label: 'Default' } etc. (packages/cli/src/config/settingsSchema.ts:2917-2919, forwarded verbatim by buildSettingsResponse at packages/cli/src/serve/routes/workspace-settings.ts:233). The Chinese panel therefore shows "Default / Code Mode / Code Mode Only" under a fully Chinese label and description, while every other enum in this same ZH map is translated — settings.option.ui.chatWidth.* (messages.ts:192-193), settings.option.review.effort.* (234-237), settings.option.model.reasoningEffort.* (307-311), settings.option.tools.workflowSizeGuideline.* (362-366), settings.option.policy.permissionStrategy.* (377-380). Before this diff the row was a boolean rendered through the already-translated settings.value.on/off, so the untranslated option list is new. "Default" is also the least self-describing of the three for a Chinese reader, since the description calls that mode "Direct".

Witness:

not run — probe/mutation via `review scratch-tree` and `base-tree`: unavailable in this environment (repo-local git includeIf resolves to a missing credentials file, so scratch-tree refuses to create a tree; the base build timed out), so the verdict rests on a line-by-line source trace plus repo-wide greps

Suggested fix: Add the three option keys next to the new label/description, e.g. 'settings.option.tools.mode.direct': '直接(默认)', 'settings.option.tools.mode.code_mode': 'Code Mode', 'settings.option.tools.mode.code_mode_only': 'Code Mode Only' (keeping the two proper-noun mode names as-is if that is the intended house rendering).

The fix must not violate this existing fact: Keys must be spelled exactly settings.option.${setting.key}.${String(value)} (packages/web-shell/client/components/messages/SettingsMessage.tsx:186) with the values the schema ships — { value: ToolMode.Direct, label: 'Default' }, { value: ToolMode.CodeMode, ... }, { value: ToolMode.CodeModeOnly, ... } (packages/cli/src/config/settingsSchema.ts:2917-2919), i.e. direct / code_mode / code_mode_only, not the label text.

Acceptance criterion: A case in packages/web-shell/client/components/messages/SettingsMessage.dom.test.tsx (or a new collocated settings/messages.test.ts) that renders/derives the option list for a tools.mode enum descriptor under the zh-CN dictionary and asserts the Direct option is not the raw schema label 'Default' — equivalently, asserts SETTINGS_MESSAGES_ZH['settings.option.tools.mode.direct'] is defined. Delete the added keys and that assertion must go red; no existing test covers it (`SettingsMessage Please prove it by removing the fix and confirming that test goes red.

中文说明

该行变成了枚举,但中文字典只翻译了它的 label 和 description——三个选项标签没有对应的 settings.option.tools.mode.* 条目,因此 zh-CN 用户在完全中文的标签与描述下会看到原始的英文选项 "Default / Code Mode / Code Mode Only"。同一 diff 已为 CLI 翻译了这三个标签并加了 must-translate 测试,可见 web-shell 这一处属于遗漏而非有意。

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

@qwen-code-review-bot qwen-code-review-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] Prior-round Critical R1-2 (asserted in @doudouOUC's review 5217747523, packages/cli/src/config/settingsSchema.ts:36) — the original inline comment was deleted, so only the reviewer's premise survives and the claimed mechanism could not be t…

Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally (Agent 7 ran per-package vitest suites only).

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": none — the chunk's remaining items were walked clear: agent-types.ts 's new field and all five read sites, the subagent-plan-tool-policy.test.ts title-only c…; "agent reverse-audit (round 1)": mutation-run confirming that deleting the hybrid declarationNames snapshot clause leaves agent-core.skill-gate.test.ts and agent-core.fork-policy.test.ts …; "agent reverse-audit (round 1)": whether getToolExposure(ToolNames.EXEC) is 'code-mode-callable' — inferred from the passing codeModeAllowedToolNames assertions ( exec never appears in a…; "agent reverse-audit (round 2)": did not execute mutation probes (plain registerTool in place of registerPermissionDeferredFactory ; a hybrid search_memory allowlist through getFunctionDe…; "agent reverse-audit (round 2)": did not run npx vitest run src/code-mode/code-mode.test.ts to confirm the suite is green at this commit, so my claim that F1's assertions hold today is derive…, and 2 more.

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

中文说明

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

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

未审查(原文为英文):build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally (Agent 7 ran per-package vitest suites only).

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":none — the chunk's remaining items were walked clear: agent-types.ts 's new field and all five read sites, the subagent-plan-tool-policy.test.ts title-only c…;"agent reverse-audit (round 1)":mutation-run confirming that deleting the hybrid declarationNames snapshot clause leaves agent-core.skill-gate.test.ts and agent-core.fork-policy.test.ts …;"agent reverse-audit (round 1)":whether getToolExposure(ToolNames.EXEC) is 'code-mode-callable' — inferred from the passing codeModeAllowedToolNames assertions ( exec never appears in a…;"agent reverse-audit (round 2)":did not execute mutation probes (plain registerTool in place of registerPermissionDeferredFactory ; a hybrid search_memory allowlist through getFunctionDe…;"agent reverse-audit (round 2)":did not run npx vitest run src/code-mode/code-mode.test.ts to confirm the suite is green at this commit, so my claim that F1's assertions hold today is derive…,另有 2 条。

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

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

Comment on lines +742 to +743
(this.runtimeContext.getToolMode?.() === ToolMode.CodeModeOnly ||
!isHiddenByEagerAllowList(name)) &&

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] R1-1: N03: [certifies-falsely] [new-surface] The new Hybrid-only eager exclusion removes a demoted skill from both the declarations and the exec binding set, but the skill-listing predicate that this same diff rewired (willHaveSkillTool() → toolConfigAllowsSkill(this.toolConfig, hasAgentSkillExecBinding(...)), agent-core.ts:660-664) still answers true through its two non-exec clauses, so in tools.mode: "code_mode" the listing and canInvokeSkill() disagree — the exact #12424 divergence the shared predicate exists to prevent, and the invariant the author's own matrix asserts (expect(gate(core, declared)).toBe(willHaveSkill), agent-core.skill-gate.test.ts:402).

Trigger: tools.mode: "code_mode" (a value this diff adds — ToolMode.CodeMode is new at packages/core/src/tools/code-mode.ts:23) plus an active settings.tools.eager allowlist that omits skill. registerLazyTool turns that omission into status === 'deferred' → registry.registerPermissionDeferredFactory(ToolNames.SKILL, …) (config.ts:12251-12252, skill registered lazily at config.ts:12704-12708), getVisibleTools() is sourced only from settings.tools.visible (config.ts:8322-8331) so it does not contain skill, and getToolExposure('skill') is 'code-mode-callable' (code-mode.ts:39-58 — skill is in neither HIDDEN_TOOLS nor DIRECT_ONLY_TOOLS). Any subagent reaches this with the default config, because SubagentManager builds tools: configuredToolConfig?.tools ?? ['*'] (subagent-manager.ts:1150-1151). Then, traced line by line at this commit: prepareTools() sets isHiddenByEagerAllowList('skill') true (agent-core.ts:709-712 → isPermissionDeferred ∧ isDeferredAndHidden, tool-registry.ts:589 + 1352-1360, alwaysLoad defaults false — SkillTool's super() passes only 7 args, skill.ts:157-166), so the anchored clause drops skill from allowedNames; Hybrid declarationNames ⊆ allowedNames (agent-core.ts:764-773) so skill is not declared, and codeModeAllowedToolNames (agent-core.ts:759-762) excludes it. canInvokeSkill(declared) is therefore false on both routes: declaredToolNames.has(SKILL) false, and the nested route fails its codeModeAllowedToolNames?.includes(SKILL) === true term (agent-core.ts:1854-1862). willHaveSkillTool() is nevertheless true: toolConfigAllowsSkill returns inheritsRegistry || names.includes(SKILL) || reachesThroughExec (subagent-plan-tool-policy.ts:160-164), and inheritsRegistry (names.includes('*')) or names.includes('skill') short-circuits before the exec term that hasAgentSkillExecBinding correctly answers false for. Wrong outcomes at all three consumers of that answer: createChat injec

Witness:

`Source: [probe]` — permission-deferred, unrevealed, non-visible `skill`; hybrid vs Only × four `tools` lists: ``` N03 {"mode":"code_mode","tools":["*"], "willHaveSkillTool":true, "canInvokeSkill":false,"declaredHasSkill":false,"codeModeAllowedHasSkill":false} N03 {"mode":"code_mode","tools":["skill"], "willHaveSkillTool":true, "canInvokeSkill":false,"declaredHasSkill":false} N03 {"mode":"code_mode","tools":["skill","exec"],"willHaveSkillTool":true,"canInvokeSkill":false} N03 {"mode":"code_mode","tools":["exec"], "willHaveSkillTool":false,"canInvokeSkill":false} ← agrees N03 {"mode":"code_mode

Suggested fix: Give the shared predicate the same eager-scope fact prepareTools() uses, so it vetoes every route in Hybrid, not just the exec route: thread a skillEagerHidden (or mode-aware) input into toolConfigAllowsSkill computed as mode === ToolMode.CodeMode && registry.isPermissionDeferred?.(SKILL) === true && registry.isDeferredAndHidden?.(SKILL) === true, return false when it is set, and pass it from all three call sites (agent-core.ts:660-664, subagent-manager.ts:1187-1189, background-agent-resume.ts:157) — fixing only hasAgentSkillExecBinding (as R1-17 proposes) leaves the wildcard and explicit-skill clauses returning true.

The fix must not violate this existing fact: agent-core.skill-gate.test.ts:398-400 — expect(willHaveSkill).toBe(mode === ToolMode.CodeModeOnly || visibility === 'visible');. The fix must keep CodeModeOnly answering true for visibility: 'hidden' (its prepareTools() bypasses the eager filter at agent-core.ts:741, so skill really is bound there) and keep Hybrid answering true for 'visible' and 'revealed', both of which make isDeferredAndHidden false (`!this.revealedDeferred.has(name) && !this.config.getVisibleTools().has(n

Acceptance criterion: packages/core/src/agents/runtime/agent-core.skill-gate.test.ts:341-404 — the it.each([ToolMode.CodeMode, ToolMode.CodeModeOnly] × warm × visibility) matrix already builds a permission-deferred skill and asserts expect(gate(core, declared)).toBe(willHaveSkill), but instantiates only { tools: [ToolNames.EXEC] }. Add { tools: ['*'] } and { tools: [ToolNames.SKILL] } rows to the same matrix: without the fix, mode: ToolMode.CodeMode + visibility: 'hidden' (both warm values) goes R Please prove it by removing the fix and confirming that test goes red.

中文说明

新增的 Hybrid 专用 eager 排除会把被降级的 skill 同时从声明列表和 exec 绑定集合中去掉,但本 PR 改接的 skill 列表判定(willHaveSkillTool() → toolConfigAllowsSkill)仍会通过它的两个非 exec 分支返回 true。因此在 tools.mode: "code_mode" 下,列表判定与执行闸门 canInvokeSkill() 结论相反——这正是该共享判定本应防止的 #12424 分歧。触发条件:hybrid 模式 + 生效的 tools.eager 白名单未包含 skill;子智能体默认 tools: ['*'] 即可命中。后果:createChat 注入 includeAvailableSkillsReminder: true,缓存的 prompt 前缀里出现 <available_skills>,列出该智能体其实没有的工具,模型调用后得到 Tool "skill" not found;SubagentManager 保留 skillsAvailable = true,把 bundled skill 引用作为子智能体无法跟随的指针下发;后台恢复路径重复同样的错误 true。探针实测三行 hybrid 组合分歧、四行 CodeModeOnly 全部一致;打上候选修复后分歧消失且 77 个作者测试仍全绿。另需注意:Direct 模式在未被本 PR 触碰的代码上已有同样分歧,只修 hybrid 会留下一半。

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

| `exec` | CodeMode and CodeModeOnly | No |
| Direct control | Yes | No |
| Ordinary registered tool | CodeMode only | Yes |
| Hidden bridge (`tool_search`, `tool_call`) | Existing behavior outside strict mode | No |

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] R1-2: N06: The rewritten exposure row conditions the table's only statement about bridge exposure on "strict mode" — a term that exists nowhere in the mode vocabulary, the code, or any other English document — and it groups two tools the code exposes differently.

ToolMode has exactly three values, direct / code_mode / code_mode_only (packages/core/src/tools/code-mode.ts:22-26); no setting, code path, or EN doc defines a "strict mode", and a repo-wide grep over docs/ finds the phrase only on this line (the ZH twin's 严格模式 is glossed only in a different file, docs/design/code-mode.zh-CN.md:104: "tools.mode: "code_mode_only" 选择严格模式"). A user on tools.mode: "code_mode" therefore cannot tell from this table whether hybrid counts as strict, i.e. whether tool_search/tool_call are declared for their session — and the shipped rule splits the pair the row keeps together: tool_search is a top-level direct control under CodeModeOnly (DIRECT_ONLY_TOOLS, code-mode.ts:39-51, admitted by the exposure === 'direct-only' filter in getCodeModeFunctionDeclarations, tool-registry.ts:1126-1131), while tool_call is hidden in every mode (const HIDDEN_TOOLS = new Set<string>(['tool_call']);, code-mode.ts:38). The same file's unchanged Status paragraph states that split correctly ("tool_search is now a top-level direct control … tool_call stays hidden", code-mode-only.md:12-16), so the table and the Status paragraph disagree about whether the two bridge tools share a rule. Every other row in this table names modes explicitly ("CodeMode and CodeModeOnly", "CodeMode only"), making this the one cell a reader cannot resolve to a tools.mode value.

Witness:

`Source: [probe]` sweep — `grep -rn 'strict mode|strict \`|严格模式' **/*.md` over the whole worktree: as a **tool-mode** term the phrase occurs exactly **once** in EN docs (this row) and once in its ZH twin; every other hit is an unrelated domain (TypeScript strict mode, Ajv strict, screen-reader strict, `parseLastEventId`). `ToolMode` has three values (code-mode.ts:22-26) and no setting or code path names one "strict". Mitigation I found and the finder did not weigh: the sibling EN doc this same PR adds glosses it adjectivally — `code-mode.md:22` "the strict `CodeModeOnly` exposure policy" — and

Suggested fix: Replace the cell with mode names and split the pair, e.g. two rows — | tool_search | CodeModeOnly top level; existing Direct/CodeMode behavior | No | and | tool_call | Hidden in every mode | No | — and mirror the change in docs/design/code-mode-only.zh-CN.md:54.

The fix must not violate this existing fact: const HIDDEN_TOOLS = new Set<string>(['tool_call']); and const DIRECT_ONLY_TOOLS = new Set<string>([ToolNames.TOOL_SEARCH, …]) — packages/core/src/tools/code-mode.ts:38-51; the rewritten rows must keep tool_call hidden in every mode and tool_search top-level under CodeModeOnly, matching docs/design/code-mode-only.md:12-16.

Acceptance criterion: N/A (documentation prose; no guard, branch or behaviour to pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

改写后的 exposure 表格行把该表唯一一句关于 bridge 暴露的说明限定在 "strict mode" 下——而这个词在模式词表、代码和其余文档中都不存在(真实枚举是 direct / code_mode / code_mode_only)。读者无法把它对应到任何可配置取值。

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

Comment on lines +87 to +88
property, an exact canonical match wins over rewritten names. If neither is an
exact match, the lexicographically first name wins. The description names the

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] R1-3: N01: The sentence this diff rewrote (pre-diff: "one warning names the omitted collision") claims the exec description names the dropped binding, but in CodeModeOnly with tool_search registered — the shipped default this same diff's new blockquote declares ("Only discovers schemas through top-level tool_search") — buildExecDescription suppresses the collision block entirely. The pre-diff wording was the accurate one; the new one is false for the mode the document is about.

A registry holding mcp__srv__get-data and mcp__srv__get_data normalizes both to mcp__srv__get_data, so planCodeModeBindings keeps the exact match and records { jsName, kept: 'mcp__srv__get_data', omitted: 'mcp__srv__get-data' } (packages/core/src/tools/code-mode.ts:111-124). In a tools.mode: "code_mode_only" session, getCodeModeFunctionDeclarations computes searchAvailable = !!this.getTool(TOOL_SEARCH) && (!allowedNames || allowedNames.has(TOOL_SEARCH)) → true (packages/core/src/tools/tool-registry.ts:1121-1123, passed at :1135), and buildExecDescription sets const searchAvailable = codeModeOnly && (options.searchAvailable ?? false) → true (code-mode.ts:253), so collisionText = (searchAvailable ? [] : plan.collisions) → [] (code-mode.ts:264) and the description ends with no Name collisions: block (code-mode.ts:325). The repo's own test pins that suppression: packages/core/src/code-mode/code-mode.test.ts:723-739 registers exec + tool_search under code_mode_only with hidden-tool/hidden_tool and asserts expect(description).not.toContain('hidden-tool') and not.toContain('hidden_tool'). The only surface that names the drop is debugLogger.warn inside warnCodeModeCollisions (tool-registry.ts:1158-1166) — debug-gated, not model-facing. Concrete cost: the model is never told the binding was dropped in the default Only configuration, and the maintainer triaging "tool X is unreachable through exec / exec called the wrong MCP tool" reads this document as the normative collision contract, goes looking for a missing line in the description generator, and finds code that is behaving exactly as written. The same false sentence ships in the new Chinese twin ("描述会指出被省略的冲突项。", code-mode-only.zh-CN.md:73-74).

Witness:

`Source: [probe]` — CodeModeOnly registry with `exec` + `tool_search` + colliding `get-data`/`get_data` (non-deferred, so deferral filtering cannot explain the absence): `N01-only {"collisions":[{"jsName":"get_data","kept":"get_data","omitted":"get-data"}],"descriptionHasCollisionsBlock":false,"descriptionNamesOmittedTool":false,"declaredNames":["exec","tool_search"]}` Positive controls (the probe *can* see the block): `N01-only-nosearch {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":true}` · `N01-hybrid {"descriptionHasCollisionsBlock":true,"descriptionNamesOmittedTool":t

Suggested fix: Scope the claim to the configuration where it holds and name the other surface, e.g. "When search is unavailable in the current scope, the description names the omitted collision; when tool_search is available the collision block is left out of the description and the drop is logged once through the debug logger." Apply the identical correction to docs/design/code-mode-only.zh-CN.md:73-74.

The fix must not violate this existing fact: const collisionText = (searchAvailable ? [] : plan.collisions) — packages/core/src/tools/code-mode.ts:264, with const searchAvailable = codeModeOnly && (options.searchAvailable ?? false); at :253. The corrected sentence must keep the hybrid case true: decorateCodeModeDeclarations passes codeModeOnly: false (packages/core/src/tools/tool-registry.ts:1087-1092), so hybrid descriptions always emit the block.

Acceptance criterion: N/A (documentation prose). The behaviour the corrected sentence must match is already pinned from both sides: packages/core/src/code-mode/code-mode.test.ts:723-739 (CodeModeOnly + tool_search → neither collision name appears) and :563-591 'describes normalized-name collisions on the hybrid surface' (hybrid description contains '- read-file is omitted because it collides with read_file as tools.read_file.'). Please prove it by removing the fix and confirming that test goes red.

中文说明

本次改写后的句子声称 exec 的 description 会点出被丢弃的 binding,但在 CodeModeOnly 下被丢弃的 binding 恰恰不会进入 description;改写前的措辞("one warning names the omitted collision")才是准确的——warnCodeModeCollisions 在所有模式下都会触发。中英文两份设计文档同句同错。

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

arguments as JavaScript, with ordinary runtime validation still enforced.

This change makes the existing experimental `tools.codeModeOnly` mode load
This change makes the existing experimental `tools.mode: "code_mode_only"` mode load

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] R1-10: N07: This diff renames the setting in the two sentences it touches but leaves "Code Mode" standing as a bare mode name at :43 ("Direct-only and collided tools are excluded from Code Mode discovery.") and :57 ("In Code Mode, an exact-name miss explains that select: requires the registered name…") — a name this same PR now assigns to a different mode (tools.mode: "code_mode", settings label 'Code Mode' at packages/cli/src/config/settingsSchema.ts:2918, and the new docs/design/code-mode.md titled "# Code Mode", which cites this file as the authority that superseded its MVP prose). Both standing sentences describe behavior the code gates on ToolMode.CodeModeOnly alone, so under the naming this PR introduces they are false for the mode they now appear to name.

ToolSearchTool.execute() computes const codeMode = this.config.getToolMode?.() === ToolMode.CodeModeOnly; and const bindings = codeMode ? new Map(…getCodeModeBindingPlan(…).bindings…) : undefined (packages/core/src/tools/tool-search.ts:172-183). In hybrid code_mode, bindings is undefined, so both exec-guidance tails are skipped — '\n\nCall these tools through exec using tools.<jsName>(args) and the required parameters above.' (:498-501) and '\nselect: requires the registered name, including mcp__<server>__<tool> for MCP tools. Search with keywords without select: …, then use the returned jsName in exec.' (:506-508) — and discovery is not restricted to the callable binding plan, so direct-only and collision-omitted tools are not excluded. Concrete cost: a maintainer debugging a hybrid session where select:bogus returns only Not found: bogus reads :57 as a statement about "Code Mode" (= hybrid, per this PR's own naming), concludes discovery guidance is broken, and fixes against the wrong gate; :43 likewise reads as a hybrid permission-boundary claim while hybrid applies no such exclusion. The paragraph's own contrast clause — "Direct mode retains its current response" — leaves hybrid no slot at all, yet hybrid is exactly the case that gets the Direct response. The ZH twins carry the same collision (Code Mode 搜索排除只能直接调用的工具…, Code Mode 的精确名称查找未命中时…).

Witness:

`Source: [probe]` — same registry, same query, two modes: `N07 {"mode":"code_mode_only","query":"select:bogus","hasSelectHintTail":true,"contentHead":"Not found: bogus\nselect: requires the registered name, including mcp__<server>__<tool> for MCP tools. … then use the returned jsName in exec."}` `N07 {"mode":"code_mode","query":"select:bogus","hasSelectHintTail":false,"contentHead":"Not found: bogus"}` So the behaviour :57 attributes to "Code Mode" is Only-only, and hybrid — which this PR names "Code Mode" (`settingsSchema.ts:2918-2921`, option `{ value: ToolMode.CodeMode, label: 'Code Mode' }

Suggested fix: Scope the standing sentences to the mode that implements them — ":43 → Direct-only and collided tools are excluded from CodeModeOnly discovery." and ":57 → In CodeModeOnly, an exact-name miss explains that \select:` requires the registered name…" — and mirror both in lazy-code-mode.zh-CN.md:21/:29 (CodeModeOnly 搜索排除…, CodeModeOnly 的精确名称查找未命中时…). Where the family is genuinely meant, say "both code modes" only for behavior gated on isCodeModeEnabled(…)`.

The fix must not violate this existing fact: const codeMode = this.config.getToolMode?.() === ToolMode.CodeModeOnly; — packages/core/src/tools/tool-search.ts:172. The correction must not widen the sentences to "both code modes": the binding-plan restriction and both exec tails at :498-508 read bindings, which stays undefined in hybrid, and the scheduler's own code-mode call-surface gate is likewise Only-scoped (`this.config.getToolMode?.() === ToolMode.CodeModeOnly && … !isCodeModeToolCallAllowed(canonicalName, reqInfo.source ?? 'mod

Acceptance criterion: N/A (documentation prose; the fix adds no guard, branch, or behavior a test can pin). Please prove it by removing the fix and confirming that test goes red.

中文说明

本 diff 重命名了它改到的两句里的设置项,却在第 43 行(及 :57、中文孪生行)把 "Code Mode" 留作一个裸模式名,而该标签现在已被本 PR 指派给另一个取值 code_mode。

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

| `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. |
| `debug` | `boolean` | `false` | Enable debug mode for verbose logging from the CLI process. |
| `maxSessionTurns` | `number` | `-1` (unlimited) | Maximum number of conversation turns before the session automatically terminates. Must be an integer. A turn consists of a user message and an assistant response. |
| `coreTools` | `string[]` | - | Uses the legacy `coreTools` / CLI `--core-tools` allowlist semantics. If specified, only matching core tools are registered for the session. This is the only allowlist-style option that restricts built-in tool registration; a whole-tool `permissions.deny` / `excludeTools` rule (and `tools.disabled` in settings.json) also removes a tool from the registry. `permissions.allow` in settings.json is pure auto-approval and never removes, demotes, or hides a tool (#10075). To keep a tool's schema out of the initial model request, use `tools.eager` in settings.json (requires restart, #9827) — `tool_search`, `tool_call`, `structured_output`, plan-mode lifecycle tools, `task_stop`, `mcp__*` and `computer_use__*` tools are exempt from that allowlist and keep their normal loading; tools demoted this way stay registered and reachable through `tool_search` + `tool_call` while both bridge tools are registered — when either is unregistered (`tools.toolSearch.enabled: false` denies both; a `tool_search` or `tool_call` deny rule, or a `tools.disabled` entry removes one) the demoted tools that remain hidden are absent from top-level declarations and cannot be reached through the bridge for that session, and a warning is written to the CLI process's stderr (SDK forwards it only with piped stderr and effective `debug` logging; an explicit `logLevel` always wins). These bridge and warning rules apply to direct and hybrid code modes. On the session surface in hybrid mode, while `exec` itself is registered (container and SSH execution warn and fall back to direct tools without it), exec retains callable nested bindings; their schemas are included in exec when either bridge tool is unavailable. CodeModeOnly discovers deferred schemas through top-level tool_search and invokes them through exec. It skips deferred preload and startup catalogs; tools.eager reduces the initial exec description. When search is unavailable in the current scope, exec includes all allowed signatures. In Hybrid mode, AgentCore excludes tools still hidden by tools.eager from nested bindings. In both code modes, agent allowlists that do not grant `exec` narrow nested bindings. Inheriting or explicitly granting `exec` keeps all otherwise admitted ordinary code-mode-callable bindings. An execution allowlist that mentions any MCP tool additionally restricts MCP bindings to matching exact names or server patterns. In direct and hybrid modes they stay registered, so a direct call by their own name is still evaluated and approved normally — except tools also listed in `tools.visible`, which are declared upfront, and sessions whose live history contains a direct call to a still-hidden demoted tool, which any tool-set refresh (resume, MCP discovery, the first plan-mode entry in a session, a subagent definition change) re-declares.; to remove a tool entirely, use a whole-tool `excludeTools` / `permissions.deny` rule — a rule with a specifier (such as `'Bash(rm *)'`) only denies matching invocations at runtime. MCP tools are exempt from deny-based removal: hide them with the per-server `excludeTools` / `tools.disabled` filters instead (deny still blocks their calls at runtime). Example: `['read_file', 'edit', 'run_shell_command']`. |

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] R1-11: N08: The newly documented CodeModeOnly guarantee "It skips deferred preload" rests on a single untested early-return — packages/core/src/core/client.ts:2129 if (this.config.getToolMode?.() === ToolMode.CodeModeOnly) return; — while all three sibling guards in the same method are pinned (client.test.ts:1931 threshold 0, :1937 non-finite threshold, :1954 bridge unavailable). The other half of the same sentence is pinned (tool-registry.test.ts:1152 "getDeferredToolSummary is empty in CodeModeOnly"), so only the preload half is unprotected.

A refactor that consolidates or drops the mode guard in preloadDeferredToolsWithinBudget() (nothing turns red — no test sets getToolMode to ToolMode.CodeModeOnly anywhere in client.test.ts; the only getToolMode mocks are at :3419/:3455/:3490, all ToolMode.CodeMode) lets the budget preload call revealDeferredTool() for every ordinary deferred tool at CodeModeOnly session start. isDeferredAndHidden() then returns false for them (tool-registry.ts:1353-1361 consults revealedDeferred), so planCodeModeBindings stamps deferred: false (code-mode.ts:127), and visibleBindings = plan.bindings.filter((binding) => !searchAvailable || !binding.deferred) (code-mode.ts:252-254) stops filtering them out even though tool_search is available. Their full tools.<jsName>(args: …) signatures and descriptions land in the initial exec declaration — exactly the eager-everything behavior the sentence promises CodeModeOnly skips, and the exec-declaration rewrite / prompt-cache bust that tool-registry.ts:1389-1392 says the empty summary exists to prevent. An SDK integrator who sized their tools.eager allowlist against this documented promise gets the deferred schemas in the first request anyway.

Witness:

two runs. Sweep (coverage): `getToolMode` is mocked three times in `client.test.ts` (:3419, :3455, :3490) and **all three return `ToolMode.CodeMode`**; `ToolMode.CodeModeOnly`/`'code_mode_only'` appears **0** times in that file (the only nearby mocks are a different accessor, `getCodeModeOnly` at :889 and :12634). The sibling guards in the same method *are* pinned — `'skips deferred preload when the threshold is 0'` (:1931), `'…not finite'` (:1937), `'clamps a threshold above 100%'` (:1944), `'…when the bridge is unavailable'` (:1954) — and no other test file touches the client-level guard (`p

Suggested fix: Add one case to the preloadDeferredToolsWithinBudget block in packages/core/src/core/client.test.ts, beside 'skips deferred preload when the threshold is 0' (:1931) and 'skips deferred preload when the bridge is unavailable' (:1954): set mockConfig.getToolMode = vi.fn().mockReturnValue(ToolMode.CodeModeOnly), give the registry both bridge tools and a positive getToolSearchThreshold(), run startChat, and assert the registry preload spy was never called.

The fix must not violate this existing fact: The assertion must target the registry spy, not a reveal side effect: the shared registry stub is preloadDeferredToolsWithinBudget: vi.fn().mockReturnValue(0) (packages/core/src/core/client.test.ts:860) and does not record revealDeferredTool calls for this path. The test must also drive the mode through getToolMode, which is what the guard reads (packages/core/src/core/client.ts:2129), and not through the separately-stubbed getCodeModeOnly: vi.fn().mockReturnValue(false) (`packages/c

Acceptance criterion: That new test — expect(reg.preloadDeferredToolsWithinBudget).not.toHaveBeenCalled() under ToolMode.CodeModeOnly with a positive threshold and both bridge tools registered — goes red if the client.ts:2129 guard is deleted, because the threshold and bridge guards that satisfy the existing three tests are both satisfied in this configuration. Please prove it by removing the fix and confirming that test goes red.

中文说明

新增的 CodeModeOnly 保证"It skips deferred preload"依赖一个没有测试覆盖的提前返回(client.ts:2129),该分支一旦回归,文档承诺就会静默失效。

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

Comment on lines +423 to +424
You may call exec and tools matched by this direct-call allowlist: ${JSON.stringify(executionAllowedTools ?? [])}.
Inside exec, only these exact nested tool names are permitted: ${JSON.stringify(nestedExecutionAllowedTools)}.

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] R1-37: N28: The new sentence asserts a direct-call allowlist unconditionally, but the nested branch is entered for both code modes (isCodeModeEnabled), and under code_mode_only no ordinary tool is declared to the fork and a direct model call to one is hard-denied — so the sentence enumerates tools the child cannot call.

tools.mode: "code_mode_only", a subagent defined with tools: ['exec','read_file','write_file'], launching a plain fork. nestedExecutionAllowedTools is defined (mode is code-mode-enabled and the allowlist frame is set), which also makes requestedExecutionAllowedTools defined for a plain fork for the first time (agent.ts:1922-1928: requestedTools === undefined && nestedExecutionAllowedTools === undefined ? undefined : …) — pre-diff this shape rendered no restriction text at all. The child's declarations come from getFunctionDeclarationsFiltered, which routes CodeModeOnly to getCodeModeFunctionDeclarations(new Set(toolNames)) (tool-registry.ts:1520-1522), keeping only exposure === 'exec' and 'direct-only' tools (tool-registry.ts:1119-1131) — read_file is never declared. If the child nonetheless acts on "You may call exec and tools matched by this direct-call allowlist: ["read_file","write_file"]", coreToolScheduler.ts:3145-3162 rejects it: getToolMode() === ToolMode.CodeModeOnly and isCodeModeToolCallAllowed('read_file','model') is false (exposure code-mode-callable is not exec/direct-only), producing Tool "read_file" is unavailable on this CodeModeOnly call surface. with ToolErrorType.EXECUTION_DENIED and status: 'error'. Cost: a denied call and a wasted turn inside a fork capped at FORK_DEFAULT_MAX_TURNS = 200 (fork-subagent.ts:41), repeated once per file the directive needs, with the authoritative restriction block — not stale boilerplate — as the source of the false belief.

Witness:

probe/grep output recorded by the verifier: — **confirmed (high confidence)** — Suggestion witness: [probe] CodeModeOnly, subagent configured tools: ['exec','read_file','write_file'], plain fork — the real exported builders give defaultExecutionToolNames: ["read_file","write_file"] → executionAllowedTools (direct): ["read_file","write_file"], and the rendered block reads: You may call exec and tools matched by this direct-call allowlist: ["read_file","write_fi

Suggested fix: Make the sentence mode-aware: thread the tool mode into buildChildMessage (the caller already has agentConfig.getToolMode?.()) and, when the mode is code_mode_only, render "You may call exec; ordinary tools are reachable only through it." while keeping the nested line, instead of naming a direct-call allowlist that declares nothing.

The fix must not violate this existing fact: The same sentence must stay true in hybrid — isToolExecutionAllowed returns true for EXEC whenever the mode is code-mode-enabled and executionAllowedTools is defined (packages/core/src/agents/runtime/agent-core.ts:2012-2019: if ((toolName === ToolNames.EXEC && isCodeModeEnabled(this.runtimeContext.getToolMode?.())) || …) { return true; }), and ordinary allowlist names are declared there (tool-registry.ts:1523-1538), so a mode-aware fix must not strip the enumeration for `code_mod

Acceptance criterion: A buildChildMessage case in packages/core/src/tools/agent/agent.test.ts for the CodeModeOnly rendering asserting the message contains Inside exec, only these exact nested tool names are permitted: ["read_file"] and does not contain direct-call allowlist: ["read_file"]; goes red while the sentence is unconditional. Please prove it by removing the fix and confirming that test goes red.

中文说明

新增的这句话无条件地断言存在一个直接调用白名单,但进入该嵌套分支的条件是 isCodeModeEnabled(两种 code 模式都会进),而在 code_mode_only 下并不存在可直接调用的普通工具,提示语因此会误导子智能体。

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

? `\n\nTOOL EXECUTION RESTRICTION:
You may call exec and tools matched by this direct-call allowlist: ${JSON.stringify(executionAllowedTools ?? [])}.
Inside exec, only these exact nested tool names are permitted: ${JSON.stringify(nestedExecutionAllowedTools)}.
Nested permission does not permit direct calls to those tools.`

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] R1-39: N29: In the new nested branch, sentence 3 denies direct calls to the whole nested list while sentence 1 grants direct calls to the direct list — and the two lists overlap by construction, so for any overlapping name the restriction block grants and forbids the same call in consecutive lines.

The PR's own new fixture produces the overlap: agent.test.ts:3605-3621 ('inherit bounded default', a plain fork — no fork_tools) runs inside runWithAgentConfiguredToolAllowlist(['read_file']) + runWithCodeModeAllowedNames(['read_file','write_file']) and asserts executionAllowedTools: ['read_file'] with nestedExecutionAllowedTools: ['read_file','write_file']. buildChildMessage therefore renders: "You may call exec and tools matched by this direct-call allowlist: ["read_file"]. / Inside exec, only these exact nested tool names are permitted: ["read_file","write_file"]. / Nested permission does not permit direct calls to those tools." — "those tools" resolves to ["read_file","write_file"], so the third line withdraws the read_file grant the first line just made. Full coincidence is the common plain-fork case: with requestedTools === undefined, isRequestedByFork returns true for every name (agent.ts:1850-1851), so the direct list (buildParentBoundExecutionAllowlist(defaultExecutionToolNames)) and the nested list (buildParentBoundExecutionAllowlist(getCurrentCodeModeAllowedNames() ?? defaultExecutionToolNames, true)) run the same filters over the same base and come out equal — e.g. both ["read_file","write_file"]. The child resolves the conflict conservatively: it stops calling read_file directly even though isToolExecutionAllowed('read_file') is true and the declaration is present (hybrid), routing every read through an extra exec runtime round trip, or skipping the tool and reporting the directive blocked. That is the same "the child obeys and the grant is discarded" outcome this branch was added to prevent.

Witness:

probe/grep output recorded by the verifier: — **confirmed (high confidence)** — Suggestion witness: [probe] rendered from the built product with the PR's own fixture values (agent.test.ts:3596-3621 asserts direct ['read_file'], nested ['read_file','write_file']): You may call exec and tools matched by this direct-call allowlist: ["read_file"]. Inside exec, only these exact nested tool names are permitted: ["read_file","write_file"]. Nested permission does not

Suggested fix: Scope sentence 3 to the nested-only remainder instead of the whole nested list — e.g. const nestedOnly = nestedExecutionAllowedTools.filter((name) => !(executionAllowedTools ?? []).includes(name)); and emit the denial only when nestedOnly.length > 0, naming that subset; or reword so it cannot conflict: "A nested listing alone does not grant a direct call; direct calls are governed only by the allowlist above."

The fix must not violate this existing fact: packages/core/src/tools/agent/fork-subagent.ts:421-426 — the nested branch is tested before the executionAllowedTools === undefined and length === 0 arms, so a reworded branch must still win for executionAllowedTools: [] (the shape agent.test.ts's 'bounds and persists an explicit exec fork' produces), or the deny-all text at line 430 returns and the nested grant is discarded.

Acceptance criterion: A buildChildMessage case in packages/core/src/tools/agent/agent.test.ts beside the existing shapes at 3089-3104: with executionAllowedTools: ['read_file'] and nestedExecutionAllowedTools: ['read_file','write_file'], assert the rendered restriction does not deny direct calls to read_file while still naming write_file as nested-only; and with executionAllowedTools: ['read_file'], nestedExecutionAllowedTools: ['read_file'], assert no denial sentence is emitted at all. Both go re Please prove it by removing the fix and confirming that test goes red.

中文说明

在新的嵌套分支里,第三句否认对整个嵌套列表的直接调用,而第一句又授予对直接列表的直接调用——两个列表存在重叠时,同一段提示语会自相矛盾。

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

Comment on lines +1435 to +1440
const execTool = this.tools.get(ToolNames.EXEC);
const codeModeBindings =
this.config.getToolMode?.() === ToolMode.CodeMode &&
execTool !== undefined &&
this.isToolAvailable(execTool.name) &&
this.isToolDeclared(execTool.name)

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] R1-42: N30: The budget preload's new decoration gate tests whether exec is registered, available and declared, but the decoration it is estimating for (decorateCodeModeDeclarations, tool-registry.ts:1069) only happens when exec is among the declarations actually returned — so when exec is permission-deferred by a tools.eager allowlist, the estimate charges every candidate for an exec-signature block that will never be sent, and the all-or-nothing preload can refuse reveals the operator's budget would have allowed.

Hybrid session (tools.mode: "code_mode") with tools.eager: ["read_file", "write_file"] — an allowlist that omits exec. exec is not exempt (PermissionManager.isExemptFromEagerAllowList, permission-manager.ts:885-895, exempts structured_output, plan-lifecycle tools, task_stop, tool_call, tool_search, mcp__*, computer_use__* — not exec), so getToolRegistrationStatus returns 'deferred', it is registered through registerPermissionDeferredFactory (config.ts:12252) and lands in permissionDeferred. tool_search/tool_call stay declared (exempt), so the preload runs rather than early-returning. Now isToolDeclared('exec') and isToolAvailable('exec') are both true, so codeModeBindings is built and each deferred candidate is measured as JSON.stringify(augmentDeclarationForCodeMode(tool.schema, binding)) — raw schema plus `\n\nexec tool declaration:\n```ts\ndeclare const tools: { <jsName>(args: <full rendered parameter type>): Promise<CodeModeToolResult>; };\n``` ` (code-mode.ts:219-231 + bindingSignature), which for an MCP tool with a real schema is often as large as the schema itself. But getFunctionDeclarations() filters exec out at tool-registry.ts:1048-1054 (tool.alwaysLoad || !this.isDeferredAndHidden(tool.name)), so decorateCodeModeDeclarations hits its !tools.some(name === EXEC) early return and reveals the raw schemas. Result: with a budget sized to the raw deferred schemas, estimatedTokens > budgetTokens, the preload returns 0, and every deferred tool stays behind the bridge — extra tool_search round trips on a session whose declarations never grew by the amount that was charged. This is the opposite direction from R1-29 (which is an undercount of the exec-side ALL_TOOLS growth when the bridge is present); both live in the same estimate, and a fix for one must not cancel the other.

Witness:

probe/grep output recorded by the verifier: — **confirmed (high confidence)** — Suggestion witness: [probe] hybrid registry, deferred (a shouldDefer MockTool) plus exec registered through registerPermissionDeferredFactory + warmAll(), budget = the raw schema cost: ARM A (exec permission-deferred): exec present in getFunctionDeclarations(): false raw schema cost 37 tokens | charged (decorated) 70 | measured reveal threshold 70 ACTUAL declaration growth on revea

Suggested fix: Make the gate match the condition the decoration actually uses, i.e. that exec will be among the returned declarations: ts const execDeclaredEagerly = execTool !== undefined && this.isToolAvailable(execTool.name) && this.isToolDeclared(execTool.name) && (execTool.alwaysLoad || !this.isDeferredAndHidden(execTool.name)); const codeModeBindings = this.config.getToolMode?.() === ToolMode.CodeMode && execDeclaredEagerly ? new Map(/* unchanged */) : undefined;

The fix must not violate this existing fact: if (!tools.some((tool) => tool.name === ToolNames.EXEC)) { return tools.map((tool) => tool.schema); } — packages/core/src/tools/tool-registry.ts:1069-1071, reached because getFunctionDeclarations()'s filter is tool.alwaysLoad || !this.isDeferredAndHidden(tool.name) (tool-registry.ts:1052-1053). The gate must key on exec's own declared/eager state only — not on the bridge: tool_search and tool_call are exempt from tools.eager (`canonicalName === ToolNames.TOOL_CALL || canonicalName

Acceptance criterion: packages/core/src/tools/tool-registry.test.ts, beside the added counts CodeMode declaration decoration toward the budget (lines 1060-1095): same fixture (toolMode: 'code_mode', a shouldDefer MockTool named deferred, rawBudget = tokensFor(directTool)), but register exec through registry.registerPermissionDeferredFactory('exec', async () => new MockTool({ name: 'exec' })) and await registry.warmAll() (the pattern already used at tool-registry.test.ts:213 and :1340). Assert `codeMod Please prove it by removing the fix and confirming that test goes red.

中文说明

预算预加载新增的 decoration 门控检测的是 exec 是否已注册、可用且已声明,但它要估算的那次 decoration 用的是另一个条件,二者并不等价,估算因此可能在门控通过时并不适用。

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

Comment on lines +1687 to +1688
// An agent's target allowlist does not identify its declared
// direct, bridge, or exec routes. Do not advertise session routes.

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] R1-47: N32: The reachability rule this hunk states is enforced only on the image-overview branch; the omni-delivery branch that returns 48 lines earlier inside the same case 'image' still advertises zoom_image unconditionally, so on an omni-policy session the new gate never executes for images at all.

tools.mode: "code_mode_only" (or "code_mode") plus a subagent whose surface withholds zoom_image, on a model where modalities.image is true and config.isOmniEnabled?.() / omni.isOmniDeliveryActive(config) hold — all model/policy facts, independent of tool mode (fileUtils.ts:1389-1410). processSingleFileContent takes if (omniModule) { return await omniModule.readMediaViaOmniDelivery({…}) } at fileUtils.ts:1638-1650 and never reaches the changed code. packages/core/src/omni/index.ts:1189-1196 then pushes Image <name>: full resolution WxH px. Use zoom_image for a closer look at details. (or the degradation variant Use zoom_image to inspect details — it reads the original file.) with no registry, tool-mode, or ambient-allowlist check. The agent emits a zoom_image call and AgentCore.processFunctionCalls rejects it with Tool "zoom_image" not found. Tools must use the exact names provided. (agent-core.ts:2201-2206) — one wasted turn, which is exactly the cost the deleted #12271 comment and this new gate exist to prevent. The identical agent on a non-omni model gets '' and does not waste the turn, so after this diff whether the invariant holds depends on the session's model policy rather than on the agent's surface.

Witness:

not run — probe/mutation via `review scratch-tree` and `base-tree`: unavailable in this environment (repo-local git includeIf resolves to a missing credentials file, so scratch-tree refuses to create a tree; the base build timed out), so the verdict rests on a line-by-line source trace plus repo-wide greps

Suggested fix: Hoist the route resolution (registry facts + getCurrentCodeModeAllowedNames()) into one helper that returns the hint string, call it before the if (omniModule) early return, and pass the result into readMediaViaOmniDelivery so its two text variants name zoom_image / tools.zoom_image / nothing under the same rule the overview branch now uses.

The fix must not violate this existing fact: packages/core/src/omni/index.ts:1183-1187 — "calling them 'full resolution' would contradict the disclosure pushed right below and steer the model away from zoom_image, the exact remedy for degradation-stripped detail (it reads the original from disk)". Suppression must key on the route being genuinely absent for the calling agent, not on the degradation/disclosure path, or the disclosure and the hint start contradicting each other again.

Acceptance criterion: packages/core/src/omni/index.test.ts:410-464 (adds a resolution + zoom_image hint part for images) currently pins the unconditional text. Add a sibling case that drives the same delivery with the route absent — e.g. wrap in runWithCodeModeAllowedNames(['read_file'], …), or a registry double whose getFunctionDeclarations() omits zoom_image — and assert the pushed part's text does not contain zoom_image. It is red while the omni branch ignores reachability. Please prove it by removing the fix and confirming that test goes red.

中文说明

这段 hunk 所陈述的可达性规则只在 image-overview 分支上被执行;同一个 case 'image' 内提前 48 行 return 的 omni-delivery 分支并不受它约束,形成同族分支的不对称。

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

| `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. |
| `debug` | `boolean` | `false` | Enable debug mode for verbose logging from the CLI process. |
| `maxSessionTurns` | `number` | `-1` (unlimited) | Maximum number of conversation turns before the session automatically terminates. Must be an integer. A turn consists of a user message and an assistant response. |
| `coreTools` | `string[]` | - | Uses the legacy `coreTools` / CLI `--core-tools` allowlist semantics. If specified, only matching core tools are registered for the session. This is the only allowlist-style option that restricts built-in tool registration; a whole-tool `permissions.deny` / `excludeTools` rule (and `tools.disabled` in settings.json) also removes a tool from the registry. `permissions.allow` in settings.json is pure auto-approval and never removes, demotes, or hides a tool (#10075). To keep a tool's schema out of the initial model request, use `tools.eager` in settings.json (requires restart, #9827) — `tool_search`, `tool_call`, `structured_output`, plan-mode lifecycle tools, `task_stop`, `mcp__*` and `computer_use__*` tools are exempt from that allowlist and keep their normal loading; tools demoted this way stay registered and reachable through `tool_search` + `tool_call` while both bridge tools are registered — when either is unregistered (`tools.toolSearch.enabled: false` denies both; a `tool_search` or `tool_call` deny rule, or a `tools.disabled` entry removes one) the demoted tools that remain hidden are absent from top-level declarations and cannot be reached through the bridge for that session, and a warning is written to the CLI process's stderr (SDK forwards it only with piped stderr and effective `debug` logging; an explicit `logLevel` always wins). These bridge and warning rules apply to direct and hybrid code modes. On the session surface in hybrid mode, while `exec` itself is registered (container and SSH execution warn and fall back to direct tools without it), exec retains callable nested bindings; their schemas are included in exec when either bridge tool is unavailable. CodeModeOnly discovers deferred schemas through top-level tool_search and invokes them through exec. It skips deferred preload and startup catalogs; tools.eager reduces the initial exec description. When search is unavailable in the current scope, exec includes all allowed signatures. In Hybrid mode, AgentCore excludes tools still hidden by tools.eager from nested bindings. In both code modes, agent allowlists that do not grant `exec` narrow nested bindings. Inheriting or explicitly granting `exec` keeps all otherwise admitted ordinary code-mode-callable bindings. An execution allowlist that mentions any MCP tool additionally restricts MCP bindings to matching exact names or server patterns. In direct and hybrid modes they stay registered, so a direct call by their own name is still evaluated and approved normally — except tools also listed in `tools.visible`, which are declared upfront, and sessions whose live history contains a direct call to a still-hidden demoted tool, which any tool-set refresh (resume, MCP discovery, the first plan-mode entry in a session, a subagent definition change) re-declares.; to remove a tool entirely, use a whole-tool `excludeTools` / `permissions.deny` rule — a rule with a specifier (such as `'Bash(rm *)'`) only denies matching invocations at runtime. MCP tools are exempt from deny-based removal: hide them with the per-server `excludeTools` / `tools.disabled` filters instead (deny still blocks their calls at runtime). Example: `['read_file', 'edit', 'run_shell_command']`. |

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] R1-48: N33: The rewritten coreTools contract is stored twice, byte-identical, in two files this diff edits — nothing generates one from the other and nothing asserts they agree.

I extracted the last cell of the coreTools row from both files at HEAD and compared: len == 3226 for each and a == b is True. This diff had to apply the same ~1.5 KB rewrite to both (docs/developers/sdk-typescript.md hunk @@ -53,27 +53,27 @@ carries the identical added sentence "On the session surface in hybrid mode…", and the identical removed sentence "These bridge and warning rules apply to direct tool mode."). The next semantic correction — e.g. the hybrid/exec condition in Finding 1, or the canSearchDeferredSchemas = false hardcode on the filtered path at tool-registry.ts:1538 that makes "when either bridge tool is unavailable" wrong for subagents — has to be found and made in both cells or the two published SDK references contradict each other, and an integrator who reads the stale one builds a client against a contract the shipped CLI no longer honours. Nine independently falsifiable statements were added to each copy in this diff and no test pins any of them in either file.

Witness:

``` ARM 1 intact tree (both real files): PASS: coreTools description cells identical (3226 chars) ARM 2 one sentence edited in a /tmp copy (positive control): FAIL: coreTools description cells diverged … first difference at char 1411: A: …always wins). These bridge and warning rules apply to direct and hybrid code modes. On the session surface… B: …always wins). These bridge and warning rules apply to direct code mode only. On the session surface… ARM 3 pre-PR cells replayed from the diff's own '-' lines: FAIL: old-docs-cell.md: 2154 chars / old-readme-cell.md: 2451 chars … first difference at

Suggested fix: Keep one authoritative copy and derive the other (a small generate step, or a <!-- prettier-ignore -->-style include), or — cheapest — add a test that reads both files, extracts the coreTools row's description cell, and asserts the two strings are equal, so drift fails CI instead of shipping.

Acceptance criterion: A new test (e.g. packages/sdk-typescript/src/readme-doc-sync.test.ts) that fails when one cell is edited without the other; it is red the moment either file's coreTools description diverges. Please prove it by removing the fix and confirming that test goes red.

中文说明

改写后的 coreTools 契约在本 diff 编辑的两个文件里逐字节重复存放,既没有由一方生成另一方,也没有任何测试断言二者一致。实测:两文件 22 个共有选项行的 description 全部逐字节相同(coreTools 行为 3226 字符);在 merge base 上二者曾分别为 2154 与 2451 字符、互相矛盾,正是本 PR 手工同步修好的。同一句新增文案在 HEAD 上散布于 6 个文件、11 处手工维护位置(另有 3 处由 CI 固定),而本 diff 手工改了其中 5 个文件。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:
Autofix agent finished without required output file(s): address-summary.md, no-action.md.

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

中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.25.0

@qwen-code-review-bot qwen-code-review-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.

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

  • R1-2 ?:64 — already reported on this PR (round 1 inline thread)
  • R1-3 ?:87 — already reported on this PR (round 1 inline thread)
  • R1-4 ?:9 — already reported on this PR (round 1 inline thread)
  • R1-5 ?:123 — already reported on this PR (round 1 inline thread)
  • R1-6 ?:3 — already reported on this PR (round 1 inline thread)
  • R1-13 ?:581 — already reported on this PR (round 1 inline thread)
  • R1-19 ?:654 — already reported on this PR (round 1 inline thread)
  • R1-20 ?:1089 — already reported on this PR (round 1 inline thread)
  • R1-23 ?:352 — already reported on this PR (round 1 inline thread)
  • R1-25 ?:657 — already reported on this PR (round 1 inline thread)
  • R1-26 ?:950 — already reported on this PR (round 1 inline thread)
  • R1-27 ?:2686 — already reported on this PR (round 1 inline thread)
  • R1-29 ?:468 — already reported on this PR (round 1 inline thread)
  • R1-31 ?:13478 — already reported on this PR (round 1 inline thread)
  • R1-33 ?:1470 — already reported on this PR (round 1 inline thread)
  • R1-37 ?:424 — already reported on this PR (round 1 inline thread)
  • R1-41 ?:1347 — already reported on this PR (round 1 inline thread)

Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally (Agent 7 ran per-package vitest suites only).

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": I did not open buildSubagentContextOverride to check whether the child registry AgentCore.willHaveSkillTool() probes (agent-core.ts:663, this.runtimeContex…; "agent reverse-audit (round 1)": I did not verify whether registerLazy (config.ts:12486-12492, registerExecIfEnabled ) honours --exclude-tools / coreTools for exec ; if it does, the uncon…; "agent reverse-audit (round 2)": whether tools.eager can demote exec itself in hybrid mode — the row's exempt list ( tool_search , tool_call , structured_output , plan-mode lifecycle tool…; "agent reverse-audit (round 1)": the new visible description claim in packages/vscode-ide-companion/schemas/settings.schema.json:1584 ("In Code Mode Only, listed tools have signatures in th…; chunk 5: verifying the tools.visible half of the CodeModeOnly "initial documented bindings" claim in context-cost.md against the exec prompt-assembly code., and 6 more.

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

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

  • ?:103 — [review] The new Chinese version drops the English clause whose…
  • ?:39 — [review] The new effective-mode table claims an omitted…
  • ?:418 — [review] The new tools.mode row states the hybrid nested-binding…
  • ?:56 — [review] The rewritten bullet still states the incomplete-bridge…
  • ?:4471 — [review] The only legacy-setting test sets codeModeOnly alone, so…
  • ?:186 — [review] The rename swept settings.md , context-cost.md ,…
  • ?:47 — [review] This diff makes one ~800-character normative paragraph…
  • ?:626 — [review] The forced-write clause is unconditional, so tools.mode …
  • ?:448 — [review] Billing the detail rows from the declarations themselves…
  • ?:1243 — [review] Inserting the two new Hybrid rows immediately above the…
  • ?:161 — [review] The mcp__offline__read_* arm — the fixture whose only…
  • ?:735 — [review] The mcp__payments__charge half of this assertion pair…
  • ?:972 — [review] None of the nine new hybrid ( ToolMode.CodeMode ) tests…
  • ?:2001 — [review] The third disjunct of the forNestedBinding arm re-runs…
  • ?:2677 — [review] The behaviour this hunk actually adds — the allowlist…
  • ?:122 — [review] The paragraph this diff adds contradicts the two…
  • ?:160 — [review] toolConfigAllowsSkill grew a registry-aware second…
  • ?:208 — [review] The new "invalid mode" case in registers exec in both…
  • ?:351 — [review] These two new assertions bless an exec description that…
  • ?:557 — [review] The new caught-vs-uncaught failure guidance added to…
  • …and 16 more (see the run report)
中文说明

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

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

未审查(原文为英文):build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its suite did not run locally (Agent 7 ran per-package vitest suites only).

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":I did not open buildSubagentContextOverride to check whether the child registry AgentCore.willHaveSkillTool() probes (agent-core.ts:663, this.runtimeContex…;"agent reverse-audit (round 1)":I did not verify whether registerLazy (config.ts:12486-12492, registerExecIfEnabled ) honours --exclude-tools / coreTools for exec ; if it does, the uncon…;"agent reverse-audit (round 2)":whether tools.eager can demote exec itself in hybrid mode — the row's exempt list ( tool_search , tool_call , structured_output , plan-mode lifecycle tool…;"agent reverse-audit (round 1)":the new visible description claim in packages/vscode-ide-companion/schemas/settings.schema.json:1584 ("In Code Mode Only, listed tools have signatures in th…;chunk 5:verifying the tools.visible half of the CodeModeOnly "initial documented bindings" claim in context-cost.md against the exec prompt-assembly code.,另有 6 条。

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

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

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

This branch has not been deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants