qwen-code/docs/developers
易良 f2e593c26b
fix(core): make permissions.allow restrict the tool schemas sent to the model (#9829)
* fix(core): make permissions.allow restrict the tool schemas sent to the model (#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.

* fix(core): honor permissions.allow in the list_directory opt-in gate (#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).

* fix(core): keep plan-mode lifecycle tools registered under the allowlist (#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).

* docs(sdk): correct allowedTools registry-allowlist contract (#9827)

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.

* docs(settings): note plan-mode lifecycle exemption in the allowlist (#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
b8ba258c40 so the documented exemption set matches the gate.

* fix(core): exempt the computer_use__* family from the registry allowlist (#9827)

* fix(core): gate command-discovered tools through the registry allowlist (#9827)

* fix(core): make registry-allowlist membership monotonic within the session (#9827)

* fix(core): narrow the skill allowedTools grant contract to restart-scoped registration (#9827)

* fix(core): count ask rules toward registry-allowlist membership (#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).

* docs(settings): note that ask rules keep tools registered under allowlist (#9827)

* test(cli): pin registry-allowlist strip in bare mode (#9827)

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.

* fix(core): attribute registry-allowlist misses to permissions.allow (#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.

* test(core): pin resolveToolName coverage of every ToolNames entry (#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 #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.

* fix(core): expose isPermissionsAllowListActive on scoped PM shims (#9827)

* fix(core): honour ask-only list_directory coverage in the opt-in gate (#9827)

* docs: align registry-allowlist contract wording across docs and JSDoc (#9827)

* docs: scope settings.md removal and whole-tool-deny claims precisely (#9827)

* fix(core): count merged allow coverage in the list_directory opt-in gate (#9827)

isLsToolEnabled() scanned only the settings-sourced getRegistryAllowList() for allow coverage while PermissionManager.isToolEnabled() counts the merged allow set (settings + --allowed-tools + SDK allowedTools + legacy tools.allowed). Under an active allowlist, list_directory covered only by a merged rule passed isToolEnabled but was never offered to registerLazy — it vanished from /tools and the model request while calls failed TOOL_NOT_REGISTERED. Count the merged allow set for coverage (activation still requires a valid settings rule) and filter empty/whitespace-only entries from activation exactly like PermissionManager.initialize's parseRules does, so a degenerate [""] entry cannot activate the gate here while the permission system reports the allowlist inactive.

* test(core): pin activation source and merged-allow coverage of the list_directory gate (#9827)

Every existing isLsToolEnabled test fed the identical array as both allow and registryAllowList, so the settings-only vs merged-allow distinction was unpinned and the R4-1 divergence shipped uncovered. Add three cases shaped like the CLI wiring: coverage by a merged (non-settings) allow rule under an active allowlist registers the tool; merged-only coverage with no settings rule does not activate the allowlist; an empty settings entry ([""]) does not activate it either.

* fix(core): attribute scheduler denials to permissions.allow only for uncovered tools (#9827)

The allowlist-miss message fired for any disabled tool with no matching deny rule while the allowlist is active — including tools rejected by the legacy coreTools gate that ARE covered by an allow rule, where 'not covered by any permissions.allow rule' is wrong and the remediation a no-op. Expose isCoveredByAllowOrAskRule on PermissionManager and take the allowlist branch only when the tool is genuinely uncovered; covered tools fall back to the generic declined message. The optional call keeps scoped PermissionManager shims (installed via 'as unknown as PermissionManager') from throwing until they grow the delegation.

* test(core): pin the covered-tool fallback for scheduler denial messages (#9827)

Add a scheduler-level case where the allowlist is active, no deny rule matches, and the disabled tool IS covered by an allow rule (the legacy coreTools gate shape): the message must be the generic declined one, not the permissions.allow attribution. Also make the existing allowlist-miss stub explicit about coverage.

* fix(core): register request_shutdown in the permission rule alias map (#9827)

Merging origin/main brought ToolNames.REQUEST_SHUTDOWN (#9806) but no TOOL_NAME_ALIASES entry, which the resolveToolName exhaustiveness test added on this branch pins. Map request_shutdown / RequestShutdown so permission rules can address the tool.

* fix(core): guard the list_directory allowlist gate against non-string rules (#9827)

isLsToolEnabled()'s activation check and coverage scan called raw.trim() / parseRule(raw) directly while PermissionManager.initialize computes the same thing through parseRules, whose r && r.trim() filter skips falsy entries. Settings load performs no element-type validation (the schema declares only type: array), so a stray null in settings.permissions.allow/ask — or in the legacy tools.allowed key riding the merged coverage set — threw TypeError during createToolRegistry and crashed startup while PermissionManager.initialize tolerated the same settings file. Mirror the parseRules guard with a typeof check in both the activation check and the coverage predicate, and pin both arms (tolerated entries still activate/cover, a [null]-only list keeps the gate closed).

* fix(core): exempt task_stop from the permissions.allow registry gate (#9827)

task_stop satisfies the PR's own two written exemption criteria but was missing from the set: it is shouldDefer=true (task-stop.ts), the exact deferred-schema property the computer_use__* exemption cites, and it is advertised to the model by a registered tool's copy — run_shell_command's schema says to use task_stop to stop a background command (and not to use broad process-name kills), and the background-promotion result instructs task_stop({ task_id }) verbatim. Under the reporter configuration the suite pins, run_shell_command stays listed while task_stop was gated out, so the sanctioned stop flow failed. Add the exemption and pin it next to the plan-mode exemption tests, including that a whole-tool deny rule still wins via the existing evaluate pass.

* fix(core): keep shim denials on the pre-#9827 message when coverage is unknown (#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-#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-#9827 declined message, and update the shim test to pin that message instead of the allowlist one.

* test(core): pin that ask-only rules never activate the allowlist (#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.

* fix(core): exempt tool_search from the permissions.allow registry gate (#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-#9827 tool_search always bypassed the legacy coreTools gate as a
non-core tool.

* test(core): pin the deny-rule arm's precedence in the scheduler permission message (#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>

* test(core): pin deny/ask sibling semantics at the discovery gate (#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>

* docs: match the allowlist activation wording to the real predicate (#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>

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-25 11:57:24 +00:00
..
daemon fix(serve): Repair persisted session lifecycle (#9626) 2026-08-25 02:24:23 +00:00
daemon-client-adapters docs(serve): Close multi-workspace hardening gaps (#7019) 2026-07-16 17:33:53 +00:00
daemon-ui fix(sdk): route unrecognized diagnostics onto a bounded transcript sidechannel (#9202) 2026-08-19 14:38:00 +00:00
development feat(telemetry): link daemon HTTP request spans to inbound W3C traceparent (#9391) 2026-08-19 12:21:49 +00:00
examples docs(serve): Close multi-workspace hardening gaps (#7019) 2026-07-16 17:33:53 +00:00
tools docs(agent): clarify parameter preconditions (#9580) 2026-08-24 02:27:09 +00:00
_meta.ts docs(daemon): Refresh daemon docs in English (#5144) 2026-06-15 22:38:01 +08:00
architecture.md feat(desktop): remove packages/desktop after OpenWork fork; keep the Tauri upgrade bridge (#9085) 2026-08-25 05:26:11 +00:00
channel-plugins.md docs(channels): Document loops and proactive delivery (#7628) 2026-07-26 07:32:18 +00:00
contributing.md revert: remove local PR verification gate (#7031) 2026-07-16 11:24:38 +00:00
qwen-serve-protocol.md fix(serve): Repair persisted session lifecycle (#9626) 2026-08-25 02:24:23 +00:00
roadmap.md docs: standardize GitHub Actions capitalization (#6367) 2026-07-06 06:55:07 +00:00
sdk-java.md feat(core): make list_directory opt-in (disabled by default) (#9424) 2026-08-21 07:24:05 +00:00
sdk-python.md fix(sdk): support "auto" permission mode (#9003) 2026-08-22 14:25:40 +00:00
sdk-typescript.md fix(core): make permissions.allow restrict the tool schemas sent to the model (#9829) 2026-08-25 11:57:24 +00:00