fix(core): make permissions.allow restrict the tool schemas sent to the model - #9829
Conversation
…he model (QwenLM#9827) permissions.allow only auto-approved calls; it never gated tool registration, so the outgoing tools array kept every built-in schema even when an allowlist was configured — contradicting the settings docs migration table ("unlisted tools are disabled at registry level") and breaking backends like llama.cpp that compile all tool schemas into a single grammar. - Activate a registry-level allowlist when settings.permissions.allow has at least one valid rule: built-in tools not covered by any allow rule are no longer registered (absent from /tools and the API request). MCP tools and the structured_output contract stay exempt; session-granted rules ("always allow", skill allowedTools) extend membership but never activate the allowlist mid-session. --allowed-tools / SDK allowedTools / legacy tools.allowed keep their pure auto-approval semantics. - Complete the rule alias map so the display names shown by /tools (SendMessage, UpdateGoal, ...) match in allow/deny rules.
|
Re-run gate pass at the current head.
Moving on to code review. 🔍 中文说明在当前 head 上重跑门禁。
进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code review (re-run at this head)Both round-1 holds are resolved — I re-verified at this head, not taken on trust:
The round-1 architecture read still holds (gate at
One standing item — disclosed, not treated as blocking this run: the review lane's R2-2. It still reproduces statically at this head: CI test evidenceThis run is unattended CI — I do not build or run PR code; the evidence below is the PR's own CI fetched via the API. All
中文说明代码审查(在此 head 上重跑)第一轮的两个保留项都已解决 —— 我在当前 head 上重新核实,不是照单全收:
第一轮的架构结论依然成立(在
一个遗留项 —— 公开披露,但本轮不作为阻断:review 通道的 R2-2。 在此 head 上静态仍可复现: CI 测试证据本轮是无人值守 CI —— 我不构建、不运行 PR 代码;以上证据全部来自 PR 自身 CI 的 API 读取。此 head 上所有 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — both round-1 holds are verifiably resolved at this head, and the one known footgun is disclosed, maintainer-accepted, and tracked below rather than fixed here. Stepping back: this is the fix #9827 needed, built to the design I'd have chosen independently — gate at the single What keeps this at 4 rather than 5: the R2-2 interaction ("Always allow" persisting into the very key that now activates the allowlist, collapsing the toolset on next restart) is real and still unfixed — see Stage 2 for the mechanism. It is disclosed, the maintainer approved this exact head with the review lane's finding on record, and I'm treating that as the product decision; a follow-up issue is the one thing I'd like to see filed before merge so the fix-forward is tracked. The skipped integration/matrix legs are by design (merge-queue-only) or fork-lane structural, not evidence against. The bot's earlier 中文说明置信度:4/5 —— 第一轮的两个保留项在此 head 上均已可核实地解决;唯一已知的坑已公开披露、经维护者接受,并在下文登记,而不是在本 PR 内修复。 退一步看:这就是 #9827 需要的修复,也与我独立会选的设计一致 —— 在 没给到 5 的原因:R2-2 交互("Always allow" 恰好持久化到现在会激活白名单的那个键上,导致下次重启工具集坍缩)真实存在且尚未修复 —— 机制见 Stage 2。它已被披露,维护者是在 review 通道的发现仍在记录的情况下批准了这个 head,我把这视为产品决策;我希望合入前能建一个跟进 issue,让后续修复有据可查。被跳过的集成/矩阵作业是设计使然(仅 merge queue)或 fork 通道的结构性原因,不是不利证据。 本 bot 早先在这个 PR 上的 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (downstream of the pre-existing workflow-size guard failure on main) and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (downstream of the pre-existing workflow-size guard failure on main) and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| this.permissionsAllowListActive = parseRules( | ||
| this.config.getRegistryAllowList?.() ?? [], | ||
| ).some((rule) => !rule.invalid); |
There was a problem hiding this comment.
[Suggestion] R1-8: Any syntactically-valid rule activates the allowlist, even one that cannot cover any built-in. parseRule marks only unbalanced-parenthesis rules invalid and passes unknown names through, so: (a) a typo — permissions.allow: ["ReadFle"] — activates, covers nothing, and deregisters the entire built-in toolset with no diagnostic (pre-PR the same typo was an inert auto-approve rule); (b) an MCP-only rule (["mcp__github__create_issue"]) activates even though MCP tools are exempt from the gate, so the only rule present cannot express membership for any tool it gates; (c) neither activation nor the registration skips log anything (the rule warning sits behind QWEN_DEBUG_LOG_FILE), so "all my tools disappeared" has no signal to grep for. The comment above promises to keep "a malformed entry" from gating the toolset, but typos are the commonest malformed-entry class and slip through; the only malformed-rule test is syntactic ('Bash(git commit').
Witness (probe): parseRule('ReadFle').invalid === undefined; a manager with ['ReadFle'] reports active and disables read_file/send_message/run_shell_command; an MCP-only rule activates the same way.
If this semantic is intended, document it in the settings.md row and emit a startup warning naming allow rules that cover no known tool; otherwise require at least one rule resolving to a known tool for activation. Either way, pin the chosen behavior with a test for the lone unknown-name rule.
中文说明
任何语法有效的规则都会激活白名单,即使它覆盖不了任何内置工具。parseRule 只把括号不闭合的规则标记为无效,未知名称原样透传,于是:(a) 拼写错误——permissions.allow: ["ReadFle"]——会激活、什么都不覆盖、并在无任何诊断的情况下注销整个内置工具集(PR 之前同样的拼写错误只是一条无效的自动批准规则);(b) 仅 MCP 的规则(["mcp__github__create_issue"])也会激活,尽管 MCP 工具本身豁免于该门——唯一存在的规则无法为它所门控的任何工具表达成员身份;(c) 激活与注册跳过都不打日志(规则警告藏在 QWEN_DEBUG_LOG_FILE 后面),"我的工具全没了"没有任何可 grep 的信号。上方注释承诺防止"畸形条目"门控整个工具集,但拼写错误是最常见的畸形条目类型却能穿过;唯一的畸形规则测试是语法层的('Bash(git commit')。
见证(探针):parseRule('ReadFle').invalid === undefined;['ReadFle'] 的管理器报告激活且禁用 read_file/send_message/run_shell_command;仅 MCP 规则同样激活。
若这是有意语义,请在 settings.md 对应行写明,并在启动时输出警告点名未覆盖任何已知工具的 allow 规则;否则要求至少一条能解析到已知工具的规则才可激活。无论哪种,用"单独一条未知名称规则"的测试钉住所选行为。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Behavior re-confirmed at the new head (c9b670e): parseRule marks only unbalanced-parenthesis rules invalid, so 'ReadFle' / MCP-only rules parse as valid rules covering nothing, and initialize activates on any non-invalid rule — one typo activates the allowlist and deregisters every uncovered built-in with no user-visible diagnostic. The harm is real (this PR raised a typo's cost from no-op to "toolset gone"), but both remediations are behavior-policy decisions rather than mechanical fixes: requiring ≥1 rule that resolves to a known gated tool changes activation semantics (a deliberately narrow config referencing extension/MCP tools unknown to core would silently stop activating — a fail-open contract change), and warning-only needs a user-visible channel decision (the PM logger sits behind QWEN_DEBUG_LOG_FILE). This is the same root question as the R2-1 thread — what expresses intent to activate the allowlist — so it is consolidated under that maintainer decision instead of being picked unilaterally here. Leaving this thread open to track it.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (downstream of the pre-existing workflow-size guard failure on main) and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 4": none — no check was cut short..
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
docs/users/configuration/settings.md:420 — [review] permissions.allow schema description left stale while settings.md gains the new allowlist semantics (settingsSchema.ts + published VS Code schema JSON)packages/core/src/permissions/rule-parser.ts:172 — [review] of the ~17 newly-aliased tool families only SendMessage/UpdateGoal are exercised by testspackages/core/src/permissions/permission-manager.ts:726 — [review] strippedAllowRules.session bucket in getEffectiveAllowRules() untested; mutant survives all 347 tests (runtime behavior verified correct)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (downstream of the pre-existing workflow-size guard failure on main) and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 4":none — no check was cut short.。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| this.permissionsAllowListActive = parseRules( | ||
| this.config.getRegistryAllowList?.() ?? [], | ||
| ).some((rule) => !rule.invalid); |
There was a problem hiding this comment.
[Suggestion] R1-8: Still stands — the code is unchanged since round 1. Any syntactically-valid rule activates the allowlist, even one that cannot cover any built-in. parseRule marks only unbalanced-parenthesis rules invalid and passes unknown names through, so a typo — permissions.allow: ["Readfile"] or ["bash"] — parses as a VALID rule covering no tool: permissionsAllowListActive becomes true and every built-in tool silently vanishes from the registry and the model request, with no startup warning. Pre-PR the same typo was harmless (it merely failed to auto-approve); this change raises its cost from no-op to "toolset gone". The added guard ("A typo must not gate the whole toolset") covers only syntactic malformation. Consider emitting a user-visible startup warning for entries that resolve to no known built-in or mcp__* pattern, or requiring at least one rule that resolves to a known tool for activation — and logging when the registry allowlist activates and how many tools were gated.
中文说明
仍然成立——代码自上一轮审查以来未变。任何语法合法的规则都会激活白名单,即使它不可能覆盖任何内置工具。parseRule 只把括号不平衡的规则标为 invalid,未知名称原样放行,所以笔误——permissions.allow: ["Readfile"] 或 ["bash"]——会解析为一条不覆盖任何工具的合法规则:permissionsAllowListActive 变为 true,全部内置工具从注册表和模型请求中静默消失,且无任何启动警告。PR 之前同样的笔误是无害的(只是无法自动批准);此改动把它的代价从"无效"提升为"工具集消失"。新增的防护("笔误不能门控整个工具集")只覆盖了语法畸形。建议对解析不到任何已知内置工具或 mcp__* 模式的条目输出用户可见的启动警告,或要求至少一条规则能解析到已知工具才激活——并在白名单激活时记录日志、说明有多少工具被门控。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Behavior re-confirmed at the new head (c9b670e): parseRule marks only unbalanced-parenthesis rules invalid, so 'ReadFle' / MCP-only rules parse as valid rules covering nothing, and initialize activates on any non-invalid rule — one typo activates the allowlist and deregisters every uncovered built-in with no user-visible diagnostic. The harm is real (this PR raised a typo's cost from no-op to "toolset gone"), but both remediations are behavior-policy decisions rather than mechanical fixes: requiring ≥1 rule that resolves to a known gated tool changes activation semantics (a deliberately narrow config referencing extension/MCP tools unknown to core would silently stop activating — a fail-open contract change), and warning-only needs a user-visible channel decision (the PM logger sits behind QWEN_DEBUG_LOG_FILE). This is the same root question as the R2-1 thread — what expresses intent to activate the allowlist — so it is consolidated under that maintainer decision instead of being picked unilaterally here. Leaving this thread open to track it.
| registryAllowList: | ||
| bareMode || safeMode | ||
| ? undefined | ||
| : settings.permissions?.allow?.length | ||
| ? settings.permissions.allow | ||
| : undefined, |
There was a problem hiding this comment.
[Critical] R2-1: A single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family, violating the invariant the PR itself states for the mid-session path. Chain verified at HEAD: the shell confirmation dialog offers "Always allow in this project/user" → shell.ts builds the Bash(npm test) permission rule → coreToolScheduler._handleConfirmationResponseInner → persistPermissionOutcome (permission-helpers.ts:192) → the CLI's onPersistPermissionRule (config.ts:2227-2239) writes permissions.allow into the workspace or user-scope settings file → the next launch feeds that array into registryAllowList here (no filtering of interactive-granted rules) → permissionsAllowListActive becomes true → every uncovered built-in is dropped from the registry. The permissionsAllowListActive JSDoc names the exact harm ("approving one tool would suddenly permission-error every tool not on the list") and guards the in-session state — but the same click, persisted, delivers the identical outcome one restart later, silently, for every subsequent session; "Always allow for this user" writes the user-scope file, so every project opened afterwards boots with the collapsed toolset. Witness (round-2 probe, scratch tree, unmodified PR code — both arms deterministic): fresh project → persist('project','allow','Bash(npm test)') → WORKSPACE_SETTINGS_FILE: {"permissions":{"allow":["Bash(npm test)"]}}; simulated restart → REGISTRY_ALLOW_LIST_AFTER_RESTART: ["Bash(npm test)"], ACTIVE_WITH_ONE_PERSISTED_RULE: true, ENABLED[read_file/edit/write_file/grep_search/glob/list_directory/agent/todo_write/web_fetch]: false, ENABLED[run_shell_command/monitor]: true; flip arm: no persisted rules → ACTIVE_WITH_NO_RULES: false, ENABLED[read_file]: true. Don't let interactive grants silently activate the allowlist: persist always-allow rules under a key that stays a pure auto-approval grant (mirroring how this PR keeps --allowed-tools / tools.allowed out of registryAllowList), or gate activation behind explicit hand-authored intent. At minimum, emit a visible startup notice naming the tools hidden by an active allowlist and warn in the confirmation dialog that persisting the rule restricts the toolset after restart.
中文说明
一次"始终允许"确认选择会持久化到 settings.permissions.allow——正是本 PR 设为注册表白名单激活键的那个键——于是下次重启时整个内置工具集塌缩为那一个被始终允许的工具家族,违反了 PR 自己为会话中路径声明的不变量。已在 HEAD 验证完整链路:shell 确认弹窗提供"在此项目/此用户始终允许" → shell.ts 构造 Bash(npm test) 权限规则 → coreToolScheduler._handleConfirmationResponseInner → persistPermissionOutcome(permission-helpers.ts:192)→ CLI 的 onPersistPermissionRule(config.ts:2227-2239)把 permissions.allow 写入工作区或用户级设置文件 → 下次启动把该数组原样喂给此处的 registryAllowList(交互式授予的规则没有任何过滤)→ permissionsAllowListActive 变为 true → 每个未被覆盖的内置工具被逐出注册表。permissionsAllowListActive 的 JSDoc 明确写出了这一危害("批准一个工具就会让列表外的所有工具突然权限报错")并防护了会话内状态——但同一次点击持久化之后,下一次启动就会静默产生完全相同的结果,且影响之后每个会话;"此用户始终允许"写的是用户级设置文件,此后打开的每个项目都以塌缩的工具集启动。见证(本轮探针,隔离树,未改动的 PR 代码——两臂均确定性):新项目 → 持久化 'Bash(npm test)' → 设置文件写入该规则;模拟重启 → 白名单激活、read_file/edit/write_file/grep_search/glob/list_directory/agent/todo_write/web_fetch 全部 false,仅 run_shell_command/monitor 为 true;翻转臂:无持久化规则 → 未激活、read_file 为 true。建议不要让交互式授予静默激活白名单:把"始终允许"规则持久化到一个保持纯自动批准语义的键(参照本 PR 刻意把 --allowed-tools / tools.allowed 排除在 registryAllowList 之外的做法),或把激活限定为显式手写意图。至少应在启动时输出可见提示、列出被激活白名单隐藏的工具,并在确认弹窗中警告:持久化该规则会在重启后收缩工具集。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Confirmed real at HEAD — the full chain reproduces (persist Bash(npm test) via "Always allow" -> next start registryAllowList=["Bash(npm test)"], allowlist active, read_file/edit/grep_search/glob/... all isToolEnabled=false). This directly violates the mid-session invariant the PR documents for itself, one restart later and silently.
I'm escalating rather than fixing because the only correct fixes are a data-semantics decision that shouldn't be made unilaterally inside this PR. Interactively-persisted rules and hand-authored rules are indistinguishable once both live in settings.permissions.allow, so there is no small, correct way to keep an interactive grant from activating the allowlist without choosing one of:
- Persist "Always allow" grants under a separate key that stays a pure auto-approval grant (mirrors how this PR keeps
--allowed-tools/tools.allowedout ofregistryAllowList). Cost: new settings key + load-path merge into the auto-approval path + migration for rules users already accumulated underpermissions.allow. - Record provenance metadata on persisted rules and gate activation on hand-authored intent. Cost: settings-schema change.
- Mitigation only (startup notice naming the hidden tools + confirmation-dialog warning). Cheap, but does not stop the toolset collapse.
My lean is (1) since it matches the grant-vs-allowlist split this PR already establishes, but it changes the on-disk settings format and the public permissions contract, so it needs a maintainer sign-off. Which direction do you want? Leaving this thread open until then.
There was a problem hiding this comment.
Already escalated in the round-2 thread — see reply 3840449009: the chain is confirmed real at HEAD (persisting an "Always allow" rule writes settings.permissions.allow, which activates the allowlist on next restart and collapses the toolset to that one family). The remediation requires a maintainer settings data-format decision — persist interactive grants under a separate non-activating key, or gate allowlist activation behind explicit hand-authored intent — so it is not being fixed in this review round. Leaving this thread open to track that decision.
There was a problem hiding this comment.
Still maintainer-gated this round — no code change on this surface (the round-6 R6-1 fix in c9b670e only touches the tool_search exemption). Note: round 6's R1-8 finding reduces to the same root question — what activates the permissions.allow registry allowlist — so both threads are consolidated under the decision laid out in reply 3840449009 (separate non-activating grant key vs. provenance-gated activation vs. mitigation-only). Leaving this thread open for maintainer sign-off.
…wenLM#9827) isLsToolEnabled() only read tools.listDirectory.enabled and the coreTools allowlist, so an explicitly allowlisted list_directory passed PermissionManager.isToolEnabled() but was never registered — absent from /tools and the model request, with calls failing TOOL_NOT_REGISTERED. This broke the documented tools.core -> permissions.allow migration equivalence for exactly this tool. Consult getRegistryAllowList() with the same coverage semantics the registry gate uses (toolMatchesRuleToolName, so Read / ListFiles / specifier forms all count).
…ist (QwenLM#9827) The permissions.allow registry gate covered exit_plan_mode / enter_plan_mode / ask_user_question, so the exact reporter configuration unregistered them. The plan-mode system reminder still instructs the model to present its plan by calling exit_plan_mode, whose schema is then never sent, so the sanctioned plan flow cannot complete. Exempt the three plan-mode lifecycle tools alongside structured_output (same synthetic- system-tool class the CORE_TOOLS docstring names; deny rules still apply).
) The JSDoc added for QueryOptions.allowedTools (and the coreTools block) claimed the SDK allowedTools param activates the registry allowlist and hides unlisted built-in schemas. It does not: ProcessTransport maps it to the CLI --allowed-tools flag, and this PR's CLI wiring builds registryAllowList only from settings.permissions.allow. Reword both JSDoc blocks and the two hand-maintained SDK doc pages (sdk-typescript.md, sdk-typescript/README.md) to the shipped contract: allowedTools stays a pure auto-approval grant; only permissions.allow in settings.json (requires restart) activates the registry allowlist.
…wenLM#9827) The permissions.allow registry-allowlist exemption list named only MCP tools and the structured_output contract. Add the plan-mode lifecycle tools (exit_plan_mode / enter_plan_mode / ask_user_question) exempted in b8ba258 so the documented exemption set matches the gate.
|
Closeout for Fixed (3 findings, 8 threads):
Escalated (1, deliberately unresolved):
Untouched this round (cap-4): Criticals R1-2/R1-4/R1-5/R1-6 and Suggestions R1-8..R1-12 (18 threads) — queued for a follow-up round. One non-force push |
…oped registration (QwenLM#9827)
|
Cap-4 round 2 on the four standing Criticals — all verified real at head
Verification at pushed head: permission-manager 355, tool-registry 52, skill-utils 9 (416/416 green); Still open: R2-1 (Always-allow persistence activating the allowlist — settings data-format design call, escalated with evidence on record) + 5 Suggestions (R1-8/9/10/11/12, queued for a future cap round). |
…LM#9827) A tool covered only by a permissions.ask rule was silently deregistered whenever the permissions.allow registry allowlist was active: allow ["ReadFile"] + ask ["Shell"] hid the whole shell family from the model, so the documented "always require user confirmation" silently became "tool unavailable" and the ask rule could never fire. Ask rules express "this tool must stay usable, with confirmation", so they now count toward registry membership (frozen at startup for the same restart-scoped monotonicity as allow rules).
The wiring tests only covered the safe-mode half of registryAllowList: bareMode || safeMode ? undefined : ... — a mutant dropping the bareMode guard survived the suite and would activate the allowlist from settings while bare mode strips those same rules from the merged allow set, leaving the bare registry's minimal toolset ungated. Mirror the safe-mode test for --bare.
…wenLM#9827) An allowlist-miss rejection surfaced as "Qwen Code requires permission to use X, but that permission was declined" citing a deny rule that does not exist (findMatchingDenyRule finds nothing) and never mentioning permissions.allow. When no deny rule matched and the registry allowlist is active, emit a distinct message pointing at the real config knob.
…enLM#9827) TOOL_NAME_ALIASES hand-maintains the canonical/display-name mappings that tool-names.ts declares; nothing enforced the sync, so a tool added to tool-names.ts without an alias entry would compile, pass every test, and silently never match a permission rule — the exact QwenLM#9827 bug class, now with higher stakes since a missed entry also breaks allowlist coverage. Walk every ToolNames/ToolDisplayNames pair and assert it round-trips through resolveToolName.
|
Closeout — cap round on the standing round-1 Suggestions (push
Verification: permission-manager 404/404, cli wiring 5/5, core+cli typecheck clean, eslint clean on all touched files. Remaining unresolved: R1-8 ×2 + R2-1. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (downstream of the pre-existing workflow-size guard failure on main) and its suite did not run locally.
Not explored to full depth (tool budget reached): chunk 7: running packages/core unit tests to execute the new test live — the review worktree has no node_modules / dist , and a full npm ci + build exceeds this rev….
Convergence: round 3 posted 16 inline comment(s), 15 of them reported for the first time; the previous round posted 14 (2 new). Findings keep coming back to the same files: packages/sdk-typescript/src/types/types.ts (findings in rounds 1, 2; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts:2221: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). No commit since the round-2 anchor touched the file; mechanism re-read at HEAD: onPersistPermissionRule (config.ts:2227-2239) appends the interactive grant to permissions.allow, and the registryAllowList wiring consumes that key unfiltered — no startup notice, no dialog warning. Author confirmed real at HEAD and escalated for a maintainer settings data-format decision; no fix landed this round (original thread: comment 3840142514). Not anchorable in this round's incremental diff because the file is unchanged.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (downstream of the pre-existing workflow-size guard failure on main) and its suite did not run locally。
未探索到全部深度(达到工具调用预算):chunk 7:running packages/core unit tests to execute the new test live — the review worktree has no node_modules / dist , and a full npm ci + build exceeds this rev…。
收敛情况:第 3 轮发布了 16 条行内评论,其中 15 条是首次提出;上一轮发布了 14 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/sdk-typescript/src/types/types.ts(第 1、2 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts:2221: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). No commit since the round-2 anchor touched the file; mechanism re-read at HEAD: onPersistPermissionRule (config.ts:2227-2239) appends the interactive grant to permissions.allow, and the registryAllowList wiring consumes that key unfiltered — no startup notice, no dialog warning. Author confirmed real at HEAD and escalated for a maintainer settings data-format decision; no fix landed this round (original thread: comment 3840142514). Not anchorable in this round's incremental diff because the file is unchanged.
— qwen3.8-max via Qwen Code /review (v0.22.0)
| // Only `settings.permissions.allow` rules activate it (see the | ||
| // `permissionsAllowListActive` field). Requiring at least one VALID | ||
| // rule keeps a malformed entry from gating the entire toolset. | ||
| this.permissionsAllowListActive = parseRules( |
There was a problem hiding this comment.
[Suggestion] R1-8: Still stands — any syntactically-valid rule activates the allowlist, even one that cannot cover any built-in. parseRule marks only unbalanced-parenthesis rules invalid and passes unknown tool names through, so a tool-name typo in settings — permissions.allow: ["ReadFiel"], a more common typo class than a missing parenthesis — activates the allowlist while covering nothing: every non-exempt built-in disappears from the registry, the exact "whole toolset gated" outcome the comment above this check ("Requiring at least one VALID rule keeps a malformed entry from gating the entire toolset") claims to prevent. The malformed-rule test added this round pins only the unbalanced-parenthesis class. Either filter rules that resolve to no known tool out of activation (e.g. require a resolveToolName hit in initialize()), or narrow the comment to "a syntactically malformed rule" so the next maintainer doesn't read a broader guarantee than exists.
中文说明
仍然成立——任何语法合法的规则都会激活白名单,即使它不可能覆盖任何内置工具。parseRule 仅把括号不配对的规则标为 invalid,未知工具名原样通过;因此 settings 中的工具名笔误——permissions.allow: ["ReadFiel"](比缺括号更常见的笔误类型)——会激活白名单却什么都不覆盖:所有未豁免的内置工具从注册表消失,恰是此检查上方注释("要求至少一条合法规则可防止单个格式错误条目门控整个工具集")声称要防止的结果。本轮新增的格式错误规则测试只固定了括号不配对这一类。建议在激活条件中过滤无法解析到已知工具的规则(例如在 initialize() 中要求 resolveToolName 命中),或把注释收窄为"语法格式错误的规则",避免后续维护者读到比实现更宽的保证。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Behavior re-confirmed at the new head (c9b670e): parseRule marks only unbalanced-parenthesis rules invalid, so 'ReadFle' / MCP-only rules parse as valid rules covering nothing, and initialize activates on any non-invalid rule — one typo activates the allowlist and deregisters every uncovered built-in with no user-visible diagnostic. The harm is real (this PR raised a typo's cost from no-op to "toolset gone"), but both remediations are behavior-policy decisions rather than mechanical fixes: requiring ≥1 rule that resolves to a known gated tool changes activation semantics (a deliberately narrow config referencing extension/MCP tools unknown to core would silently stop activating — a fail-open contract change), and warning-only needs a user-visible channel decision (the PM logger sits behind QWEN_DEBUG_LOG_FILE). This is the same root question as the R2-1 thread — what expresses intent to activate the allowlist — so it is consolidated under that maintainer decision instead of being picked unilaterally here. Leaving this thread open to track it.
…rage is unknown (QwenLM#9827) The optional isCoveredByAllowOrAskRule call's : true fallback routed shim-mediated rejections of COVERED tools into the new allowlist-attribution message, contradicting the comment above it ('they keep the pre-QwenLM#9827 message meanwhile'). Both production shims (memory-scoped-agent-config.ts, skillReviewAgentPlanner.ts) Pick a partial interface without isCoveredByAllowOrAskRule, so for them the ternary always took the allowlist arm — telling the user a covered tool 'is not covered by any permissions.allow rule' when a different gate (e.g. the legacy coreTools allowlist) rejected it. Flip the fallback to false so unknown coverage stays on the pre-QwenLM#9827 declined message, and update the shim test to pin that message instead of the allowlist one.
…nLM#9827) The suite pins ask rules counting toward allowlist membership, but nothing pins the complementary activation boundary: no test constructed a PermissionManager with only permissionsAsk (no permissionsAllow) and asserted the allowlist stays inactive. Current behavior is correct; this guards against a future edit folding ask rules into activation, which would turn an ask-only posture (permissions.ask: ["Shell"], no allow rules — a natural 'always confirm shell' config) into an active allowlist that deregisters every unlisted built-in. The nearest existing test ('no allow rules → allowlist inactive') uses no rules at all and would still pass.
|
Closeout pass (worker 2): fixed 4 findings, pushed to the fork branch in one non-force push Handled (4):
Verification: full runs of the three directly-hit files — permission-manager.test.ts (408 passed), config.test.ts (573 passed), coreToolScheduler.test.ts (376 passed, 4 failed). The 4 failures ("Plan shell routing") fail identically at the pre-fix base commit Deferred (8, out of this worker's scope): R1-8 ×3 (PRRT_kwDOPB-92c6bi3hm / ...bjjqm / ...boZJz — escalated product decision), R2-1 (...bjjqw — design decision), R3-7 (...boZJl), R3-9 (...boZJu), R4-5 (...bxjv6), R3-15 (...bxjv9). Left unresolved, untouched. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- untested deny-rule message branch of the scheduler's permission-error path — already reported (comment 3841988333, R3-7; author deferred to a follow-up round)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/core/src/permissions/permission-manager.test.ts:128 — [review] resolveToolName exhaustiveness guard does not walk the legacy-rename maps (ToolNamesMigration / ToolDisplayNamesMigration)packages/core/src/permissions/permission-manager.test.ts:3128 — [review] no test pins that a malformed rule alongside valid rules still activates the allowlist (the .some() complement)
Convergence: round 6 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/core/src/permissions/permission-manager.ts (findings in round 5; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). Mechanism re-read at HEAD 6c2694b: onPersistPermissionRule (packages/cli/src/config/config.ts:2216-2227) still appends the interactive grant to permissions.${ruleType}, and the registryAllowList wiring (2205-2210) consumes settings.permissions.allow unfiltered — no startup notice, no dialog warning; contradicts the PR's own "mid-session grants can never activate it" invariant (the grant is mid-session; the activation arrives one restart later, silently). Author confirmed real at HEAD and escalated for a maintainer settings data-format decision (persist interactive grants under a separate non-activating key, or gate activation behind explicit hand-authored intent); no fix landed (original thread: comment 3840142514). Not anchorable — the mechanism lines sit outside this round's incremental diff hunks.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 6 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/core/src/permissions/permission-manager.ts(第 5 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). Mechanism re-read at HEAD 6c2694b: onPersistPermissionRule (packages/cli/src/config/config.ts:2216-2227) still appends the interactive grant to permissions.${ruleType}, and the registryAllowList wiring (2205-2210) consumes settings.permissions.allow unfiltered — no startup notice, no dialog warning; contradicts the PR's own "mid-session grants can never activate it" invariant (the grant is mid-session; the activation arrives one restart later, silently). Author confirmed real at HEAD and escalated for a maintainer settings data-format decision (persist interactive grants under a separate non-activating key, or gate activation behind explicit hand-authored intent); no fix landed (original thread: comment 3840142514). Not anchorable — the mechanism lines sit outside this round's incremental diff hunks.
— qwen3.8-max via Qwen Code /review (v0.22.0)
QwenLM#9827) Under a narrow active allowlist, tool_search itself was gated out of the registry. Without ToolSearch, client.ts resolveDeferredToolsForReminder eagerly force-reveals every registered deferred tool (all mcp__* and the deferred computer_use__* family) into the eager model request, and preloadDeferredToolsWithinBudget early-returns — inverting the schema-shrink goal into maximal schema bloat for exactly the deferred families the other exemptions preserve for ToolSearch discoverability. Pre-QwenLM#9827 tool_search always bypassed the legacy coreTools gate as a non-core tool.
|
Round 6 closeout — new head: c9b670e (fork branch).
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
docs/users/configuration/settings.md:421 (+5 locations) — [review] exemption enumeration on five doc/JSDoc surfaces omits tool_search (settings.md, sdk-typescript.md, sdk README, types.ts, config.ts registryAllowList JSDoc)packages/core/src/permissions/permission-manager.test.ts:3023 — [review] new test comment cites renamed-away symbol isCoveredByAllowRule (actual predicate: isCoveredByAllowOrAskRule)packages/core/src/config/config.ts:7188 — [probe] added comment falsely claims PermissionManager.initialize tolerates non-string settings entries (parseRules throws; probe: parseRules([123]) → TypeError)
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). Mechanism re-read at HEAD c9b670e: onPersistPermissionRule (packages/cli/src/config/config.ts:2216-2227) still appends the interactive grant to permissions.${ruleType}, and the registryAllowList wiring (2205-2210) consumes settings.permissions.allow unfiltered — no startup notice, no dialog warning; contradicts the PR's own "mid-session grants can never activate it" invariant (the grant is mid-session; the activation arrives one restart later, silently). Author confirmed real at HEAD and escalated for a maintainer settings data-format decision (persist interactive grants under a separate non-activating key, or gate activation behind explicit hand-authored intent); no fix landed (original thread: comment 3840142514). Not anchorable — the mechanism lines sit outside this round's incremental diff hunks.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). Mechanism re-read at HEAD c9b670e: onPersistPermissionRule (packages/cli/src/config/config.ts:2216-2227) still appends the interactive grant to permissions.${ruleType}, and the registryAllowList wiring (2205-2210) consumes settings.permissions.allow unfiltered — no startup notice, no dialog warning; contradicts the PR's own "mid-session grants can never activate it" invariant (the grant is mid-session; the activation arrives one restart later, silently). Author confirmed real at HEAD and escalated for a maintainer settings data-format decision (persist interactive grants under a separate non-activating key, or gate activation behind explicit hand-authored intent); no fix landed (original thread: comment 3840142514). Not anchorable — the mechanism lines sit outside this round's incremental diff hunks.
— qwen3.8-max via Qwen Code /review (v0.22.0)
…ssion message (QwenLM#9827) The three-way message branch in CoreToolScheduler covers the allowlist-miss arm and the generic fallback arm, but every findMatchingDenyRule mock returned undefined, so the deny-rule arm — whose position FIRST in the if/else-if chain is what makes a real denial cite the matching rule instead of the allowlist attribution — had no scheduler-level coverage. Add two tests where findMatchingDenyRule returns a matching rule: one with the allowlist arm armed (active allowlist + uncovered tool) pinning the if/else-if ordering, one without an active allowlist pinning the deny arm over the generic declined fallback. Mutation-checked: disabling the deny arm fails both tests. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…nLM#9827) The discovery-gate test built its PermissionManager with EMPTY ask/deny lists, so the gate's two documented sibling semantics were unpinned: settings.md says a whole-tool deny rule removes a discovered tool from the registry even under an active allowlist, and an ask rule keeps a discovered tool registered ("always require confirmation" must never silently become "tool unavailable"). Add two discovery-gate tests with deny-covered and ask-covered PermissionManager configurations: the denied tool is also allow-covered so only the deny branch of isToolEnabled can reject it, and the ask test carries an uncovered control tool proving the gate is active in the same run. Mutation-checked: ignoring deny decisions fails the deny test only; dropping ask coverage from isCoveredByAllowOrAskRule fails the ask test only. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Local maintainer verification —
|
| cell | config | tools on wire |
|---|---|---|
| base / none | — | 27 |
| head / none | — | 27 — set identical to base |
merged into live main / none |
— | 28 = + report_findings (#9794, new since merge base) |
base + reporter's 8 permissions.allow rules |
settings | 27 — unchanged: bug reproduced (red control) |
| head + same rules | settings | 13 — the fix |
| merged + same rules | settings | 13 — report_findings correctly stripped |
head + --allowed-tools ReadFile |
flag | 27 — flag never activates the allowlist |
head + --exclude-tools send_message,update_goal |
flag | 25 = − exactly those two |
| head + ask-only rule | settings | 27 — ask rules never activate |
| head + deny via display names ×3 | settings | 24 = − exactly those three |
| base + deny via display names ×3 | settings | 27 — unchanged: alias-map bug reproduced (red control) |
base / head + tools.disabled:["read_file"] |
settings | 26 on both arms — symmetric positive control proving the settings file is read on both arms |
Grammar breakers (the #9827 pain): with the reporter's allow rules, base sends all four of send_message, update_goal, loop_wakeup, read_mcp_resource; head sends none of them.
The 13 head sends = the PR body's promised 10 + task_stop + tool_search (exempted by the PR's own commits) + list_directory (opt-in tool activated by the ListFiles rule). See corrections below.
Corrections to the description (not code defects)
- The body's "After … sends 10 schemas" under-counts: this environment sends 13, and the extra three are exactly the PR's own documented exemptions/opt-in. Mechanism behaves as specified; only the number in the Reviewer Test Plan is stale.
- The "63 tool schemas" baseline is environment-dependent (27 built-ins in a minimal headless env — no
computer_use__*, no interactive-only tools). The A/B holds on identical environments, so conclusions are unaffected.
Suite is load-bearing (vacuity check)
- Base's own
permission-manager.test.tson base sources: 332/332 green. - Head's rewritten file (410 tests) on base sources: 52 red / 358 green — 37 behavioral
AssertionErrorfailures (e.g.deny via display name removes the tool from the registry → expected true to be false) + 15 missing-API failures (isPermissionsAllowListActive is not a function, expected when reverting wholesale). Behavioral, not import-breakage.
Targeted gates
- Core changed-file suites (6 files incl.
permission-manager,config,tool-registry): 1472/1472 green. - CLI
config.test.ts: 355/355 green. - Typecheck: zero errors under
packages/. The 349 root-wide errors are all inintegrations/external-context's client, root-caused byTS2307 @qwen-code/webui/daemon-react-sdkmissing under this round's--cli-onlybuild scope — a harness artifact, not the PR. Liveness control: a planted type error was reported by the gate and removed.
Interaction with concurrent main
git merge-tree into live main (c3d9279) is conflict-free; main's only file overlap (config.ts, adding report_findings) composes correctly on the measured merged tree — new tool joins the default set and is stripped under the allowlist.
Not covered
Interactive TUI / /tools rendering; a real llama.cpp backend (wire shape proven, not the backend's grammar compilation itself); MCP-server tool registration paths and the computer_use__* family (unit-suite only, absent from this env); per-commit attribution across the 31 commits (aggregate merge-ref, like CI); full-build root-wide typecheck.
Evidence
中文摘要
结论:merge-ready(34/34 断言全过,0 失败)。 在隔离容器(node:22-bookworm,与 CI verify lane 同源)对 base / PR 合并树 / 试合并 live main 三棵树做真实 A/B:录制式 OpenAI 兼容端点抓取 CLI 首个请求的 tools 数组。核心结论:base 上配置报告者的 permissions.allow 八条规则后 wire 仍是全量 27 个工具(bug 复现,红色对照);PR 树上同样配置只剩 13 个 —— 四个 grammar 破坏者(send_message/update_goal/loop_wakeup/read_mcp_resource)全部消失。13 = PR 描述承诺的 10 + task_stop/tool_search(PR 自己的豁免提交)+ list_directory(ListFiles 规则激活的 opt-in 工具)—— 属于对 PR 描述计数的勘误,不是代码缺陷。回归保护全部成立:无 permissions 时两树集合完全一致;--allowed-tools 不激活白名单;--exclude-tools 精确排除;ask-only 规则不激活;display-name deny 在 PR 树精确移除 3 个工具、在 base 上完全无效(alias 表修复的红色对照);tools.disabled 对称阳性对照两树都生效(证明 settings 确实被读取)。试合并 live main:新工具 report_findings 正常加入默认集(27→28),白名单下同样被剥离,无合并冲突。负载证明:head 的新 permission-manager 套件跑在 base 源码上 52 红 / 358 绿(37 个行为性断言失败 + 15 个新 API 缺失),base 自身套件 332/332 全绿。门禁:core 改动文件 1472/1472、cli config 355/355 全绿;typecheck 的 349 个错误全部位于 --cli-only 构建跳过的 webui 依赖 client(验证环境构建范围所致),packages/ 下零错误,planted-error 对照证明门是活的。未覆盖:TUI 交互、真实 llama.cpp 后端(wire 形状已证,非 grammar 编译本身)、MCP/computer_use 注册路径(仅单测覆盖)、逐提交归因、完整构建下的全仓 typecheck。
…wenLM#9827) Four surfaces said the permissions.allow registry allowlist activates "when at least one allow rule is configured", but PermissionManager.initialize computes activation as at least one VALID rule from settings.permissions.allow only (getRegistryAllowList): a malformed entry never activates it, and auto-approval-only sources such as the --allowed-tools CLI flag / the SDK allowedTools parameter never do either. Reword settings.md, the SDK docs, the sdk-typescript README and the coreTools JSDoc to the exact predicate, and complete their exemption lists with task_stop and tool_search, which isToolEnabled exempts but the docs did not name. Docs-only. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Closeout — cap round on the standing Suggestions (single non-force fork push
Verification: tool-registry 56/56; coreToolScheduler 378 pass (4 pre-existing Plan-shell-routing failures reproduce identically at base — environmental); core typecheck clean. Threads: 4 cap threads replied with fix SHAs + resolved. Remaining 4 unresolved = the R1-8 ×3 product decision + R2-1 design decision ledger — escalated previously, left for maintainer direction. Fork lane: /triage structurally skipped (same-repo guard); push auto-triggered the fork review lane. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — test-efficacy probe inconclusive (probe runner tripped vitest's missing-dist guard; 0 mutants and 0 hunks probed; direct suite runs of both changed test files are green).
Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:
packages/core/src/tools/tool-registry.test.ts:1037 — [review] three new discovery-gate tests triplicate the PermissionManager + spawn-mock boilerplate that the makeConfig factory in permission-manager.test.ts already providespackages/core/src/core/coreToolScheduler.test.ts:3780 — [probe] the isPermissionsAllowListActive() conjunct in the scheduler's allowlist-miss arm is unpinned — the deletion mutant survives all six new tests (probe flipped both ways)
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). Mechanism re-read at HEAD 5e63280: onPersistPermissionRule (packages/cli/src/config/config.ts:2216-2227) still appends the interactive grant to permissions.${ruleType}, and the registryAllowList wiring (2205-2210) consumes settings.permissions.allow unfiltered — no startup notice, no dialog warning; contradicts the PR's own "mid-session grants can never activate it" invariant (the grant is mid-session; the activation arrives one restart later, silently). Witness: round-2 chain verified at HEAD (persist Bash(npm test) via "Always allow" -> next start registryAllowList=["Bash(npm test)"], allowlist active, read_file/edit/grep_search/glob all isToolEnabled=false); this round's incremental delta does not touch the file. Author confirmed real and escalated for a maintainer settings data-format decision (persist interactive grants under a separate non-activating key, or gate allowlist activation behind explicit hand-authored intent); no fix landed (original thread: comment 3840142514). Not anchorable — the mechanism lines sit outside this round's incremental diff hunks.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:build-and-test — test-efficacy probe inconclusive (probe runner tripped vitest's missing-dist guard; 0 mutants and 0 hunks probed; direct suite runs of both changed test files are green)。
收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
[Critical] R2-2 Still stands — packages/cli/src/config/config.ts: a single "Always allow" confirmation choice persists into settings.permissions.allow — the exact key this PR makes the registry-allowlist activator — so the next restart collapses the entire built-in toolset to the one always-allowed tool family ("Always allow for this user" writes the user-scope file, so every project opened afterwards boots collapsed). Mechanism re-read at HEAD 5e63280: onPersistPermissionRule (packages/cli/src/config/config.ts:2216-2227) still appends the interactive grant to permissions.${ruleType}, and the registryAllowList wiring (2205-2210) consumes settings.permissions.allow unfiltered — no startup notice, no dialog warning; contradicts the PR's own "mid-session grants can never activate it" invariant (the grant is mid-session; the activation arrives one restart later, silently). Witness: round-2 chain verified at HEAD (persist Bash(npm test) via "Always allow" -> next start registryAllowList=["Bash(npm test)"], allowlist active, read_file/edit/grep_search/glob all isToolEnabled=false); this round's incremental delta does not touch the file. Author confirmed real and escalated for a maintainer settings data-format decision (persist interactive grants under a separate non-activating key, or gate allowlist activation behind explicit hand-authored intent); no fix landed (original thread: comment 3840142514). Not anchorable — the mechanism lines sit outside this round's incremental diff hunks.
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 18 passed · 1 failed · 19 total Flakiness gate: 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:18 通过 · 1 失败 · 19 总计 抖动门: Verification reportPR #9829 — deep verificationVerdict: 中文摘要
Central claim + A/BCentral claim: with Wire oracle: each cell runs the built bundle headless (
Witness: CorrectionsThe body's "10 schemas" evidence is stale, not wrong-in-spirit. The Evidence block predates three later commits in this PR (the FindingsF1 — alias map misses
|
| mutation | expectation | result |
|---|---|---|
| none (baseline, with F1 scratch fix) | 411/411 green | 411 passed |
| none (as-shipped) | alias tests red | 409 passed / 2 failed = F1 |
M1: delete allowlist branch in isToolEnabled |
#9827 suite fails | 14 failed / 16 passed / 381 skipped — behavioral (expected true to be false on isToolEnabled('send_message')) |
M2: delete only tool_search exemption clause |
its pinned test fails | exactly 1 failed / 410 skipped |
| M3: revert scheduler denial-message to pre-#9827 | allowlist-miss test fails | 1 failed / 5 passed (the other 5 pin behavior the old code also satisfied — deny-wins and legacy fallback) |
No vacuous rows: every guard the PR introduces is killed by its own test when removed, and M2/M3 show the kills are fine-grained, not collateral.
Targeted gates
packages/core(config, tool-registry, coreToolScheduler, memory-scoped-agent-config, skillReviewAgentPlanner): 1067/1067 passed (5 files).packages/clisrc/config/config.test.ts(registry-allowlist wiring incl. bare-mode strip): 355/355 passed.npm run typecheck(all workspaces incl. integration-tests): exit 0.permission-manager.test.ts: 411/411 with the F1 scratch fix; 409/411 as-shipped (the 2 fails are F1).
Not covered
- Per-commit attribution: shallow checkout exposes only 2 of the 33 commits; the aggregate
HEAD^1..HEADdiff was verified instead. - MCP /
structured_output/ plan-lifecycle exemptions at wire level: no MCP server and no plan-mode trigger in this sandbox; these exemptions are pinned only by the unit suite (M1 kills them when the branch is removed). - The reporter's 63-tool environment: this container registers 28 built-ins (
computer_use__*, image_gen etc. absent). The mechanism is config-driven and identical; the absolute counts differ. - TUI
/toolsrendering: not screenshotted; it reads the same registry the request layer reads. - eslint/prettier: not re-run here (PR's CI covers them); typecheck was run.
- The PR's "11 of 15 new tests red on original sources" claim: not re-litigated; M1's revert of the final code kills 14/30 in the final suite, which supersedes it.
Methodology
Environment: CI verify container (node:22-bookworm, 64 cores), merge-ref checkout at depth 2; npm ci + npm run build pre-ran at HEAD. Base arm: scratch worktree at b449a9536a with a full npm ci (PR leaves package.json/lockfile untouched, so the dependency tree is a clean control); internal workspace links asserted via readlink -f node_modules/@qwen-code/qwen-code-core → base tree before trusting any base cell. Head arm: the prebuilt dist/cli.js at the merge ref. Wire oracle: recording mock OpenAI server storing each request body verbatim; assertions compare sorted tool-name sets (ab-assertions.mjs, 10/10 pass). Mutation and gate runs executed with vitest per package; raw logs, harness scripts, and per-cell request captures live in this artifact directory (logs/, *.mjs). All mutation edits were reverted; final tree is pristine (git status empty).
Flakiness gate log
rounds=5 files=7 skipped=0
file packages/cli/src/config/config.test.ts: (cd packages/cli) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/core/coreToolScheduler.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/coreToolScheduler.test.ts
file packages/core/src/memory/memory-scoped-agent-config.test.ts: (cd packages/core) npx --no-install vitest run ./src/memory/memory-scoped-agent-config.test.ts
file packages/core/src/memory/skillReviewAgentPlanner.test.ts: (cd packages/core) npx --no-install vitest run ./src/memory/skillReviewAgentPlanner.test.ts
file packages/core/src/permissions/permission-manager.test.ts: (cd packages/core) npx --no-install vitest run ./src/permissions/permission-manager.test.ts
file packages/core/src/tools/tool-registry.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/tool-registry.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/config/config.test.ts: PPPPP
packages/core/src/config/config.test.ts: PPPPP
packages/core/src/core/coreToolScheduler.test.ts: PPPPP
packages/core/src/memory/memory-scoped-agent-config.test.ts: PPPPP
packages/core/src/memory/skillReviewAgentPlanner.test.ts: PPPPP
packages/core/src/permissions/permission-manager.test.ts: FFFFF
packages/core/src/tools/tool-registry.test.ts: PPPPP
verdict: consistent-fail
summary: 1 of 7 changed test file(s) failed identically in every round — deterministic, so CI owns that signal
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/core/coreToolScheduler.test.ts: P (exit 0)
round 1 · packages/core/src/memory/memory-scoped-agent-config.test.ts: P (exit 0)
round 1 · packages/core/src/memory/skillReviewAgentPlanner.test.ts: P (exit 0)
round 1 · packages/core/src/permissions/permission-manager.test.ts: F (exit 1)
--- output tail · round 1 · packages/core/src/permissions/permission-manager.test.ts ---
�[39m
�[32m✓�[39m PermissionManager.findMatchingDenyRule�[2m > �[22mreturns the raw deny rule string when context matches�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager.findMatchingDenyRule�[2m > �[22mreturns undefined when no deny rule matches�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager.findMatchingDenyRule�[2m > �[22mmatches session deny rules�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager.findMatchingDenyRule�[2m > �[22mreturns undefined for non-denied tool�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager.findMatchingDenyRule�[2m > �[22mmatches bare tool deny rule�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager.findMatchingDenyRule�[2m > �[22mmatches a deny rule through a symlinked path�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mstrips Bash interpreter wildcards and stashes them�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mstrips bare tool-level Bash allow�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mstrips Agent / Skill any-allow rules�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mis idempotent — second strip returns the same stash without re-removal�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mrestoreDangerousRules reattaches stripped rules to their original scope�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mnever strips deny rules — user intent for deny is honored�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mauto-strips on initialize when approvalMode is "auto"�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — strip/restore for AUTO mode�[2m > �[22mdoes NOT auto-strip when approvalMode is the default�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mdeny rule matches a write after `cd` into a subdir�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mdeny rule matches a write through a `bash -lc` wrapper after `cd`�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mask rule matches a write through nested shell wrappers�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mallow rule on the same shell command does NOT downgrade a virtual-op deny�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mordinary writes after `cd` into project subdirs stay unmatched by self-mod rules�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mdoes not treat canonical-only allow matches as relevant�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mhasRelevantRules sees protected writes after sibling shell-wrapper segments�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mhasRelevantRules sees protected writes after `cd` before compound recursion�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mhasMatchingAskRule sees writes after `cd` into a subdir�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mescalates dynamic-cd writes when path-specific deny rules may apply�[32m 2�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — compound shell write attribution�[2m > �[22mpreserves wildcard deny rules for dynamic-cd writes�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — toolParams end-to-end�[2m > �[22mevaluate respects allow rule with param matcher�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — toolParams end-to-end�[2m > �[22mevaluate denies when param matcher does not match�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — toolParams end-to-end�[2m > �[22mfindMatchingDenyRule matches deny rule with param matcher�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — toolParams end-to-end�[2m > �[22mfindMatchingDenyRule returns undefined when param does not match�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — toolParams end-to-end�[2m > �[22mhasRelevantRules returns true when param matcher rule exists�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — toolParams end-to-end�[2m > �[22mhasMatchingAskRule returns true when param matcher ask rule matches�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m PermissionManager — toolParams end-to-end�[2m > �[22mcase-insensitive param matching: deny rule blocks different casing�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m matchesRule — param matcher type guards�[2m > �[22mrejects boolean param values�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m matchesRule — param matcher type guards�[2m > �[22mrejects null param values�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m matchesRule — param matcher type guards�[2m > �[22mrejects undefined param values�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m matchesRule — param matcher type guards�[2m > �[22mrejects object param values�[32m 0�[2mms�[22m�[39m
�[32m✓�[39m matchesRule — param matcher type guards�[2m > �[22maccepts number param values via coercion�[32m 0�[2mms�[22m�[39m
�[31m⎯⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Tests 2 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m
�[41m�[1m FAIL �[22m�[49m src/permissions/permission-manager.test.ts�[2m > �[22mresolveToolName exhaustiveness (#9827)�[2m > �[22mcovers 'REPORT_FINDINGS' ('ReportFindings' -> 'report_findings')
�[31m�[1mAssertionError�[22m: expected 'ReportFindings' to be 'report_findings' // Object.is equality�[39m
Expected: �[32m"�[7mr�[27meport�[7m_f�[27mindings"�[39m
Received: �[31m"�[7mR�[27meport�[7mF�[27mindings"�[39m
�[36m �[2m❯�[22m src/permissions/permission-manager.test.ts:�[2m124:44�[22m�[39m
�[90m122| �[39m // The /tools display name — the spelling users copy into rules —
�[90m123| �[39m // must resolve to the canonical tool.
�[90m124| �[39m expect(resolveToolName(displayName)).toBe(canonicalName);
�[90m | �[39m �[31m^�[39m
�[90m125| �[39m },
�[90m126| �[39m );
�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯�[22m�[39m
�[41m�[1m FAIL �[22m�[49m src/permissions/permission-manager.test.ts�[2m > �[22mresolveToolName exhaustiveness (#9827)�[2m > �[22mregisters every canonical tool name in the alias map
�[31m�[1mAssertionError�[22m: expected undefined to be 'report_findings' // Object.is equality�[39m
�[32m- Expected:�[39m
"report_findings"
�[31m+ Received:�[39m
undefined
�[36m �[2m❯�[22m src/permissions/permission-manager.test.ts:�[2m130:48�[22m�[39m
�[90m128| �[39m it('registers every canonical tool name in the alias map', () => {
�[90m129| �[39m for (const canonicalName of Object.values(ToolNames)) {
�[90m130| �[39m expect(TOOL_NAME_ALIASES[canonicalName]).toBe(canonicalName);
�[90m | �[39m �[31m^�[39m
�[90m131| �[39m }
�[90m132| �[39m });
�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯�[22m�[39m
�[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[90m
...truncated -- full content in the run artifacts.
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Released in v0.22.2. |






What this PR does
Makes
permissions.allowact as the registry-level allowlist the settings docs promise: when at least one allow rule is configured insettings.permissions.allow, built-in tools not covered by any allow rule are no longer registered — so they disappear from/toolsand, more importantly, their schemas are never included in thetoolsarray sent to the model. Previouslypermissions.allowonly auto-approved matching calls; registration was never gated, so the full built-in tool set was always sent regardless of the allowlist.Also completes the permission-rule alias map so rules written with the display names shown by
/tools(SendMessage,UpdateGoal,LoopWakeup, ...) actually match — before this change such rules silently never matched anything, which is why the reporter'spermissions.denyattempts had no effect while--exclude-tools(wire names) worked.Compatibility decisions, deliberately conservative:
settings.permissions.allowactivates the allowlist.--allowed-tools, the SDKallowedToolsparam, and the legacytools.allowedkey keep their documented pure auto-approval semantics, and rules granted mid-session ("Always allow", skillallowedToolsgrants) extend allowlist membership but can never activate it — so approving one tool mid-session can never permission-error the rest of the running session.structured_outputcontract (--json-schematerminal contract) are exempt from the allowlist, matching the existing--core-toolsexemptions.Bash(npm test)registers the shell tool); meta-category semantics are identical to runtime rule matching (Readcovers grep/glob/...,Bashcoversmonitor).Why it's needed
Fixes #9827. The settings docs migration table promises
tools.core→permissions.allowwith "unlisted tools are disabled at registry level", but the implementation never did that. For reporters using constrained-decoding backends (llama.cpp / Docker Model Runner), which compile every tool schema into one GBNF grammar, the always-fulltoolsarray is fatal: built-in schemas with largemaxLengthparams (send_message.message→ 65536,loop_wakeup.prompt→ 10000,update_goal.reason→ 8000,read_mcp_resource.uri→ 4096) exceed llama.cpp's repetition threshold and grammar compilation fails, breaking all tool calling for the session. There was no settings-level way to avoid sending those schemas (--exclude-toolsworked,permissions.allow/permissions.denywith display names did not).Reviewer Test Plan
How to verify
Reproduced end-to-end by pointing the CLI at a recording OpenAI-compatible mock server and capturing the outgoing
toolsarray, with the reporter's exact settings (permissions.allow: ["ReadFile", "WriteFile", "Edit", "Grep", "Glob", "ListFiles", "Shell", "WebFetch"]):main): first chat request carries 63 tool schemas —send_message,update_goal,get_goal,loop_wakeup,read_mcp_resource,agent,skill,todo_write, allcomputer_use__*, etc. — despite the allowlist. Bug reproduced.read_file,write_file,edit,notebook_edit,grep_search,glob,zoom_image,run_shell_command,web_fetch, plusmonitor, whichShellrules cover on purpose so the shell can't be bypassed). The reporter's grammar-breaking tools are all gone.permissionsconfigured the request still carries all 63 tools;--allowed-tools ReadFilealone also keeps all 63 (auto-approve only);--exclude-tools send_message,update_goalstill removes exactly those (61 left).Unit tests: the PR replaces the old "permissionsAllow is not a whitelist" test with the new documented semantic and adds a dedicated suite (
permissions.allow registry allowlist (#9827)inpermission-manager.test.ts: activation, membership, specifier/meta-category coverage, deny precedence, MCP + structured_output exemptions, session-rule safety, AUTO-mode-stripped rules, coreTools combination,--allowed-toolsnon-activation, malformed-rule safety), registry-level registration tests in coreconfig.test.ts, and CLI wiring tests asserting onlysettings.permissions.allowbecomes the registry allowlist. On the original sources 11 of the 15 new permission-manager tests fail (red), with the fix all pass.Evidence (Before & After)
Captured request payloads (mock server log):
Tested on
Environment (optional)
Node v24.19.0, built CLI (
node scripts/build.js --cli-only) run headless against a local recording mock OpenAI endpoint; targeted vitest suites inpackages/coreandpackages/cli;tsc --noEmitfor core/cli/sdk-typescript; eslint + prettier clean.Risk & Scope
settings.permissions.allowpurely for auto-approval will, after restart, see unlisted built-in tools disappear from the model's toolset. That is the behavior the docs migration table promises (and what this issue asks for), but it is a behavior change; release notes should call it out. Activation is restart-scoped and requires a settings-sourced allow rule, so mid-session approvals and--allowed-tools/SDK users are unaffected.includeTools/excludeToolsandtools.disabledcover it); the deadmigrateLegacyPermissions()code inpackages/cli/src/config/settings.ts; interactive TUI verification (request-layer change, no UI surface beyond/tools, which reads the same registry).settings.permissions.allow; for users with it, the documented allowlist semantic now applies (restart required).settings.tools.disabledremains the knob for hiding individual tools without an allowlist.Linked Issues
Fixes #9827
Related: #982 (original
tools.coreallowlist request)中文说明
这个 PR 做了什么
让
permissions.allow真正成为 settings 文档承诺的注册表级白名单:只要settings.permissions.allow配置了至少一条规则,未被任何 allow 规则覆盖的内置工具就不再注册——既不会出现在/tools,其 schema 也不会进入发给模型的tools数组。此前permissions.allow只做自动批准,从不约束注册,所以无论白名单怎么配,请求里始终带上全部内置工具。同时补全了权限规则的别名表:用
/tools显示的展示名(SendMessage、UpdateGoal、LoopWakeup等)写的规则现在能真正匹配——改动前这类规则会静默失配,这正是报告者permissions.deny无效、而--exclude-tools(wire 名)有效的原因。兼容性上刻意保守:
settings.permissions.allow会激活白名单。--allowed-tools、SDK 的allowedTools参数、遗留tools.allowed键保持文档承诺的纯自动批准语义;会话中临时授予的规则("Always allow"、skill 的allowedTools)只扩展白名单成员、永远不会激活白名单——避免会话中途批准一个工具就把其余工具全部权限报错。--json-schema的合成structured_output契约工具豁免,与现有--core-tools的豁免一致。Bash(npm test)会注册 shell 工具);元类别语义与运行时规则匹配一致(Read覆盖 grep/glob/…,Bash覆盖monitor)。为什么需要
修复 #9827。settings 文档迁移表承诺
tools.core→permissions.allow且"未列出的工具在注册表级禁用",但实现从未如此。对使用受限解码后端(llama.cpp / Docker Model Runner,把所有工具 schema 编成单一 GBNF grammar)的用户,全量tools数组是致命的:send_message.message(65536)、loop_wakeup.prompt(10000)、update_goal.reason(8000)、read_mcp_resource.uri(4096)等大maxLength参数超出 llama.cpp 重复上限,grammar 编译直接失败,整个会话的工具调用全挂。此前没有任何 settings 层面的办法避免发送这些 schema(--exclude-tools有效,permissions.allow/permissions.deny用展示名则无效)。测试方式
端到端复现:用本地录像式 mock OpenAI 端点抓取出站
tools数组,配置与报告者完全一致。修复前首个请求携带 63 个工具(含全部 grammar 破坏者);修复后同样配置只剩 10 个(仅 allow 规则覆盖的工具;monitor因Shell元类别有意保留,防止绕过 shell)。回归保护:不配置任何 permissions 时仍发送全部 63 个工具;仅
--allowed-tools ReadFile也保持 63 个(纯自动批准);--exclude-tools send_message,update_goal仍精确排除(61 个)。单测:新增权限管理器白名单专项套件、core 注册层测试、CLI 接线测试。在原始代码上 15 个新测试中 11 个失败(红),应用修复后全部通过;相关目标套件 1680+ 用例全绿;core/cli/sdk typecheck、eslint、prettier 均通过。
主要风险:此前把
settings.permissions.allow纯当自动批准用的用户,重启后会发现未列出的内置工具从模型工具集中消失。这是文档承诺的行为(也是本 issue 的诉求),但属于行为变更,建议 release notes 提示。激活仅限重启后生效、且仅 settings 来源的 allow 规则触发,会话中途批准与--allowed-tools/SDK 用户不受影响。未覆盖:MCP 工具过滤(有意豁免,走各 server 的 includeTools/excludeTools 与 tools.disabled);settings.ts 中的死代码
migrateLegacyPermissions();TUI 交互验证(本改动在请求层,/tools与请求读同一注册表,天然一致)。无迁移负担:未配置
settings.permissions.allow的用户行为完全不变。