fix(core): allow reading saved plan files without a confirmation prompt (#7678)

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 <noreply@anthropic.com>
This commit is contained in:
Nothing Chan 2026-07-25 10:43:23 +08:00 committed by GitHub
parent a76a0feedb
commit 331d58c652
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 30 additions and 0 deletions

View file

@ -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']),
];
}

View file

@ -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(),
];

View file

@ -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'),

View file

@ -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 (