feat(plan): add experimental 'plan' approval mode (#16753)

This commit is contained in:
Jerop Kipruto
2026-01-15 17:00:19 -05:00
committed by GitHub
parent 1e8f87fbdf
commit 655ab21d8b
4 changed files with 82 additions and 4 deletions

View File

@@ -1346,6 +1346,10 @@ for that specific session.
- `auto_edit`: Automatically approve edit tools (replace, write_file) while
prompting for others
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
- `plan`: Read-only mode for tool calls (requires experimental planning to
be enabled).
> **Note:** This mode is currently under development and not yet fully
> functional.
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of
`--yolo` for the new unified approach.
- Example: `gemini --approval-mode auto_edit`

View File

@@ -1011,6 +1011,30 @@ describe('Approval mode tool exclusion logic', () => {
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME);
});
it('should exclude all interactive tools in non-interactive mode with plan approval mode', async () => {
process.argv = [
'node',
'script.js',
'--approval-mode',
'plan',
'-p',
'test',
];
const settings = createTestMergedSettings({
experimental: {
plan: true,
},
});
const argv = await parseArguments(createTestMergedSettings());
const config = await loadCliConfig(settings, 'test-session', argv);
const excludedTools = config.getExcludeTools();
expect(excludedTools).toContain(SHELL_TOOL_NAME);
expect(excludedTools).toContain(EDIT_TOOL_NAME);
expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME);
});
it('should exclude no interactive tools in non-interactive mode with legacy yolo flag', async () => {
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
const argv = await parseArguments(createTestMergedSettings());
@@ -1099,7 +1123,7 @@ describe('Approval mode tool exclusion logic', () => {
await expect(
loadCliConfig(settings, 'test-session', invalidArgv as CliArgs),
).rejects.toThrow(
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, default',
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, default',
);
});
});
@@ -2052,6 +2076,42 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
});
it('should set Plan approval mode when --approval-mode=plan is used and experimental.plan is enabled', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
plan: true,
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
});
it('should throw error when --approval-mode=plan is used but experimental.plan is disabled', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
plan: false,
},
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
});
it('should throw error when --approval-mode=plan is used but experimental.plan setting is missing', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
});
// --- Untrusted Folder Scenarios ---
describe('when folder is NOT trusted', () => {
beforeEach(() => {

View File

@@ -143,9 +143,9 @@ export async function parseArguments(
.option('approval-mode', {
type: 'string',
nargs: 1,
choices: ['default', 'auto_edit', 'yolo'],
choices: ['default', 'auto_edit', 'yolo', 'plan'],
description:
'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools)',
'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools), plan (read-only mode)',
})
.option('experimental-acp', {
type: 'boolean',
@@ -492,12 +492,20 @@ export async function loadCliConfig(
case 'auto_edit':
approvalMode = ApprovalMode.AUTO_EDIT;
break;
case 'plan':
if (!(settings.experimental?.plan ?? false)) {
throw new Error(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
}
approvalMode = ApprovalMode.PLAN;
break;
case 'default':
approvalMode = ApprovalMode.DEFAULT;
break;
default:
throw new Error(
`Invalid approval mode: ${argv.approvalMode}. Valid values are: yolo, auto_edit, default`,
`Invalid approval mode: ${argv.approvalMode}. Valid values are: yolo, auto_edit, plan, default`,
);
}
} else {
@@ -578,6 +586,11 @@ export async function loadCliConfig(
);
switch (approvalMode) {
case ApprovalMode.PLAN:
// In plan non-interactive mode, all tools that require approval are excluded.
// TODO(#16625): Replace this default exclusion logic with specific rules for plan mode.
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
break;
case ApprovalMode.DEFAULT:
// In default non-interactive mode, all tools that require approval are excluded.
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));

View File

@@ -46,6 +46,7 @@ export enum ApprovalMode {
DEFAULT = 'default',
AUTO_EDIT = 'autoEdit',
YOLO = 'yolo',
PLAN = 'plan',
}
/**