From 1f5c0e18d8530fe6ea18a28e0b2b6ecf6c619a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=B3=E4=BF=A1?= Date: Fri, 24 Jul 2026 22:24:35 +0800 Subject: [PATCH] fix(core): allow reading saved plan files without a confirmation prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default plans dir (~/.qwen/plans) sits outside the workspace and was in none of ReadFileTool's permission-free roots, so reading a saved plan back landed on an ask confirmation — popped at exactly the moment the user approved the plan and told the agent to start coding, and resolvable as a denial in non-interactive/ACP flows. With the approved-plan pointer (#7197) the saved file is the model's only recovery route for the plan text, so the read must not stall on a prompt. Adds config.getPlansDir() to ReadFileTool.getDefaultPermission's allowedRoots and to AcpAgent.buildAcpLocalReadRoots (per the SYNC comment). The dir holds only session plan files; sibling ~/.qwen files such as settings.json stay confirmation-gated, pinned by a new test. Refs #6237 Co-Authored-By: Claude Fable 5 --- .../cli/src/acp-integration/acpAgent.test.ts | 2 ++ packages/cli/src/acp-integration/acpAgent.ts | 3 +++ packages/core/src/tools/read-file.test.ts | 19 +++++++++++++++++++ packages/core/src/tools/read-file.ts | 6 ++++++ 4 files changed, 30 insertions(+) diff --git a/packages/cli/src/acp-integration/acpAgent.test.ts b/packages/cli/src/acp-integration/acpAgent.test.ts index 74d71ed63e0..57b494e9f9e 100644 --- a/packages/cli/src/acp-integration/acpAgent.test.ts +++ b/packages/cli/src/acp-integration/acpAgent.test.ts @@ -2497,6 +2497,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => { getSessionService: vi.fn(() => new SessionService('/tmp')), hasSessionWriteOwnership: vi.fn().mockReturnValue(false), getSessionRuntimeBaseDir: vi.fn().mockReturnValue('/runtime-a'), + getPlansDir: vi.fn().mockReturnValue('/home/test/.qwen/plans'), setFileSystemService: vi.fn(), getHookSystem: vi.fn().mockReturnValue(undefined), getDisableAllHooks: vi.fn().mockReturnValue(true), @@ -2515,6 +2516,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => { '/tmp/user-memory', '/home/test/.qwen/skills', '/tmp/qwen-extensions', + '/home/test/.qwen/plans', ...(process.platform === 'win32' ? [] : ['/tmp']), ]; } diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index 40b9ac636f8..3ce805ae286 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -542,6 +542,9 @@ function buildAcpLocalReadRoots(config: Config): string[] { getUserAutoMemoryRoot(), ...config.storage.getUserSkillsDirs(), Storage.getUserExtensionsDir(), + // Saved plan files (see ReadFileTool.getDefaultPermission for why the + // plans dir must be readable without a confirmation prompt). + config.getPlansDir(), ...defaultAcpOnlyLocalReadRoots(), ...parseAcpLocalReadRootsEnv(), ]; diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index ac919a6eac1..bae1bb0d8f9 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -104,6 +104,7 @@ describe('ReadFileTool', () => { getProjectDir: () => path.join(tempRootDir, '.project'), getUserSkillsDirs: () => [path.join(os.homedir(), '.qwen', 'skills')], }, + getPlansDir: () => path.join(os.homedir(), '.qwen', 'plans'), getTruncateToolOutputThreshold: () => 2500, getTruncateToolOutputLines: () => 500, getContentGeneratorConfig: () => ({ @@ -324,6 +325,24 @@ describe('ReadFileTool', () => { expect(permission).toBe('allow'); }); + it('should return allow for saved plan files under the plans directory', async () => { + const params: ReadFileToolParams = { + file_path: path.join(os.homedir(), '.qwen', 'plans', 'session-1.md'), + }; + const invocation = tool.build(params); + const permission = await invocation.getDefaultPermission(); + expect(permission).toBe('allow'); + }); + + it('should still return ask for ~/.qwen files outside the plans directory', async () => { + const params: ReadFileToolParams = { + file_path: path.join(os.homedir(), '.qwen', 'settings.json'), + }; + const invocation = tool.build(params); + const permission = await invocation.getDefaultPermission(); + expect(permission).toBe('ask'); + }); + it('should return ask for paths directly under the OS temp directory', async () => { const params: ReadFileToolParams = { file_path: path.join(os.tmpdir(), 'pr-review-context.md'), diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 8b4ae8fb422..d985704ef5f 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -135,6 +135,12 @@ class ReadFileToolInvocation extends BaseToolInvocation< Storage.getGlobalTempDir(), ...this.config.storage.getUserSkillsDirs(), Storage.getUserExtensionsDir(), + // Approved plans are persisted here (default ~/.qwen/plans, outside + // the workspace) and after approval nothing re-injects the plan text, + // so the saved file is the model's only recovery route — reading it + // back must not stall on a confirmation prompt. The dir holds only + // session plan files, never credentials or settings. + this.config.getPlansDir(), ]; if (