diff --git a/packages/cli/src/acp-integration/acpAgent.test.ts b/packages/cli/src/acp-integration/acpAgent.test.ts index 74d71ed63e..57b494e9f9 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 40b9ac636f..3ce805ae28 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 ac919a6eac..bae1bb0d8f 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 8b4ae8fb42..d985704ef5 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 (