mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-14 08:14:19 +00:00
fix: enforce plan mode restrictions in ACP sessions (issue #1806)
- Add plan mode enforcement in Session.runTool to block write tools - Align ACP behavior with CoreToolScheduler plan mode logic - Add test case to verify write tools are blocked in plan mode - Fixes #1806
This commit is contained in:
parent
fb9f3fb4dc
commit
785d0ef5b7
2 changed files with 107 additions and 0 deletions
|
|
@ -648,6 +648,101 @@ function setupAcpTest(
|
|||
}
|
||||
});
|
||||
|
||||
it('blocks write tools in plan mode (issue #1806)', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('acp plan mode enforcement');
|
||||
|
||||
const toolCallEvents: Array<{
|
||||
toolName: string;
|
||||
status: string;
|
||||
error?: string;
|
||||
}> = [];
|
||||
|
||||
const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig, {
|
||||
permissionHandler: () => ({ optionId: 'proceed_once' }),
|
||||
});
|
||||
|
||||
try {
|
||||
await sendRequest('initialize', {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
||||
});
|
||||
await sendRequest('authenticate', { methodId: 'openai' });
|
||||
|
||||
const newSession = (await sendRequest('session/new', {
|
||||
cwd: rig.testDir!,
|
||||
mcpServers: [],
|
||||
})) as { sessionId: string };
|
||||
|
||||
// Set mode to 'plan'
|
||||
const setModeResult = (await sendRequest('session/set_mode', {
|
||||
sessionId: newSession.sessionId,
|
||||
modeId: 'plan',
|
||||
})) as { modeId: string };
|
||||
expect(setModeResult.modeId).toBe('plan');
|
||||
|
||||
// Try to create a file - this should be blocked by plan mode
|
||||
const promptResult = await sendRequest('session/prompt', {
|
||||
sessionId: newSession.sessionId,
|
||||
prompt: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Create a file called test.txt with content "Hello World"',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(promptResult).toBeDefined();
|
||||
|
||||
// Give time for tool calls to be processed
|
||||
await delay(2000);
|
||||
|
||||
// Collect tool call events from session updates
|
||||
sessionUpdates.forEach((update) => {
|
||||
if (update.update?.sessionUpdate === 'tool_call_update') {
|
||||
const toolUpdate = update.update as {
|
||||
sessionUpdate: string;
|
||||
toolName?: string;
|
||||
status?: string;
|
||||
error?: { message?: string };
|
||||
};
|
||||
if (toolUpdate.toolName) {
|
||||
toolCallEvents.push({
|
||||
toolName: toolUpdate.toolName,
|
||||
status: toolUpdate.status ?? 'unknown',
|
||||
error: toolUpdate.error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that if write_file was attempted, it was blocked
|
||||
const writeFileEvents = toolCallEvents.filter(
|
||||
(e) => e.toolName === 'write_file',
|
||||
);
|
||||
|
||||
// If the LLM tried to call write_file in plan mode, it should have been blocked
|
||||
if (writeFileEvents.length > 0) {
|
||||
const blockedEvent = writeFileEvents.find(
|
||||
(e) => e.status === 'error' && e.error?.includes('Plan mode'),
|
||||
);
|
||||
expect(blockedEvent).toBeDefined();
|
||||
expect(blockedEvent?.error).toContain('Plan mode is active');
|
||||
}
|
||||
|
||||
// Verify the file was NOT created
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const testFilePath = path.join(rig.testDir!, 'test.txt');
|
||||
const fileExists = fs.existsSync(testFilePath);
|
||||
expect(fileExists).toBe(false);
|
||||
} catch (e) {
|
||||
if (stderr.length) console.error('Agent stderr:', stderr.join(''));
|
||||
throw e;
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('receives usage metadata in agent_message_chunk updates', async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('acp usage metadata');
|
||||
|
|
|
|||
|
|
@ -516,6 +516,18 @@ export class Session implements SessionContext {
|
|||
? await invocation.shouldConfirmExecute(abortSignal)
|
||||
: false;
|
||||
|
||||
// Check for plan mode enforcement - block non-read-only tools
|
||||
const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN;
|
||||
if (isPlanMode && !isExitPlanModeTool && confirmationDetails) {
|
||||
// In plan mode, block any tool that requires confirmation (write operations)
|
||||
return errorResponse(
|
||||
new Error(
|
||||
`Plan mode is active. The tool "${fc.name}" cannot be executed because it modifies the system. ` +
|
||||
'Please use the exit_plan_mode tool to present your plan and exit plan mode before making changes.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (confirmationDetails) {
|
||||
const content: acp.ToolCallContent[] = [];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue