mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
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).
This commit is contained in:
1 parent
a554edb433
commit
b8ba258c40
2 files changed
+60
No files matched your search
@@ -2743,6 +2743,45 @@ describe('PermissionManager', () => {
|
||||
expect(await pm.isToolEnabled('structured_output')).toBe(true);
|
||||
});
|
||||
|
||||
it('plan-mode lifecycle tools are exempt from the allowlist (#9827)', async () => {
|
||||
// Under the exact reporter configuration the plan-mode system reminder
|
||||
// still instructs the model to call exit_plan_mode, so the sanctioned
|
||||
// plan-flow tools must stay registered. Same synthetic-system-tool
|
||||
// exemption class as structured_output.
|
||||
pm = new PermissionManager(
|
||||
makeConfig({
|
||||
permissionsAllow: [
|
||||
'ReadFile',
|
||||
'WriteFile',
|
||||
'Edit',
|
||||
'Grep',
|
||||
'Glob',
|
||||
'ListFiles',
|
||||
'Shell',
|
||||
'WebFetch',
|
||||
],
|
||||
}),
|
||||
);
|
||||
pm.initialize();
|
||||
expect(pm.isPermissionsAllowListActive()).toBe(true);
|
||||
expect(await pm.isToolEnabled('exit_plan_mode')).toBe(true);
|
||||
expect(await pm.isToolEnabled('enter_plan_mode')).toBe(true);
|
||||
expect(await pm.isToolEnabled('ask_user_question')).toBe(true);
|
||||
// Unrelated unlisted built-ins stay gated.
|
||||
expect(await pm.isToolEnabled('send_message')).toBe(false);
|
||||
});
|
||||
|
||||
it('a whole-tool deny rule still wins over the plan-mode exemption', async () => {
|
||||
pm = new PermissionManager(
|
||||
makeConfig({
|
||||
permissionsAllow: ['read_file'],
|
||||
permissionsDeny: ['exit_plan_mode'],
|
||||
}),
|
||||
);
|
||||
pm.initialize();
|
||||
expect(await pm.isToolEnabled('exit_plan_mode')).toBe(false);
|
||||
});
|
||||
|
||||
it('session-granted allow rules extend membership but never activate the allowlist', async () => {
|
||||
// No configured allow rules → allowlist must stay inactive even after
|
||||
// a mid-session "Always allow" / skill allowedTools grant, or one
|
||||
|
||||
@@ -632,6 +632,22 @@ export class PermissionManager {
|
||||
'monitor',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Synthetic plan-mode lifecycle tools that must stay registered even under
|
||||
* an active `permissions.allow` registry allowlist. The plan-mode system
|
||||
* reminder instructs the model to present its plan by calling
|
||||
* `exit_plan_mode`, and `enter_plan_mode` / `ask_user_question` are the
|
||||
* sanctioned plan-flow entry and clarification tools; dropping their
|
||||
* schemas makes the plan flow impossible to complete (#9827). They belong
|
||||
* to the same exemption class as `structured_output` and the "synthetic
|
||||
* system tools" the CORE_TOOLS docstring names — deny rules still apply.
|
||||
*/
|
||||
private static readonly PLAN_LIFECYCLE_TOOLS: ReadonlySet<string> = new Set([
|
||||
ToolNames.EXIT_PLAN_MODE,
|
||||
ToolNames.ENTER_PLAN_MODE,
|
||||
ToolNames.ASK_USER_QUESTION,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if a tool is a core tool subject to the coreTools allowlist check.
|
||||
*/
|
||||
@@ -675,9 +691,14 @@ export class PermissionManager {
|
||||
// - `structured_output`: the synthetic terminal contract for
|
||||
// `--json-schema` runs; removing it leaves such runs with no way to
|
||||
// finish (deny rules still apply to it).
|
||||
// - Plan-mode lifecycle tools (`exit_plan_mode` / `enter_plan_mode` /
|
||||
// `ask_user_question`): the plan-mode system reminder tells the model
|
||||
// to call `exit_plan_mode` to present a plan, so their schemas must
|
||||
// reach the model for the sanctioned plan flow to complete (#9827).
|
||||
if (
|
||||
this.permissionsAllowListActive &&
|
||||
canonicalName !== ToolNames.STRUCTURED_OUTPUT &&
|
||||
!PermissionManager.PLAN_LIFECYCLE_TOOLS.has(canonicalName) &&
|
||||
!canonicalName.startsWith('mcp__') &&
|
||||
!this.isCoveredByAllowRule(canonicalName)
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user