feat: add --approval-mode parameter (#6024)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
parent
11377915db
commit
8d6eb8c322
|
@ -422,6 +422,13 @@ Arguments passed directly when running the CLI can override other configurations
|
||||||
- Displays the current memory usage.
|
- Displays the current memory usage.
|
||||||
- **`--yolo`**:
|
- **`--yolo`**:
|
||||||
- Enables YOLO mode, which automatically approves all tool calls.
|
- Enables YOLO mode, which automatically approves all tool calls.
|
||||||
|
- **`--approval-mode <mode>`**:
|
||||||
|
- Sets the approval mode for tool calls. Available modes:
|
||||||
|
- `default`: Prompt for approval on each tool call (default behavior)
|
||||||
|
- `auto_edit`: Automatically approve edit tools (replace, write_file) while prompting for others
|
||||||
|
- `yolo`: Automatically approve all tool calls (equivalent to `--yolo`)
|
||||||
|
- Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach.
|
||||||
|
- Example: `gemini --approval-mode auto_edit`
|
||||||
- **`--telemetry`**:
|
- **`--telemetry`**:
|
||||||
- Enables [telemetry](../telemetry.md).
|
- Enables [telemetry](../telemetry.md).
|
||||||
- **`--telemetry-target`**:
|
- **`--telemetry-target`**:
|
||||||
|
@ -517,7 +524,7 @@ Sandboxing is disabled by default, but you can enable it in a few ways:
|
||||||
|
|
||||||
- Using `--sandbox` or `-s` flag.
|
- Using `--sandbox` or `-s` flag.
|
||||||
- Setting `GEMINI_SANDBOX` environment variable.
|
- Setting `GEMINI_SANDBOX` environment variable.
|
||||||
- Sandbox is enabled in `--yolo` mode by default.
|
- Sandbox is enabled when using `--yolo` or `--approval-mode=yolo` by default.
|
||||||
|
|
||||||
By default, it uses a pre-built `gemini-cli-sandbox` Docker image.
|
By default, it uses a pre-built `gemini-cli-sandbox` Docker image.
|
||||||
|
|
||||||
|
|
|
@ -261,4 +261,149 @@ describe('Configuration Integration Tests', () => {
|
||||||
expect(config.getExtensionContextFilePaths()).toEqual(contextFiles);
|
expect(config.getExtensionContextFilePaths()).toEqual(contextFiles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Approval Mode Integration Tests', () => {
|
||||||
|
let parseArguments: typeof import('./config').parseArguments;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Import the argument parsing function for integration testing
|
||||||
|
const { parseArguments: parseArgs } = await import('./config');
|
||||||
|
parseArguments = parseArgs;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse --approval-mode=auto_edit correctly through the full argument parsing flow', async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--approval-mode',
|
||||||
|
'auto_edit',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
];
|
||||||
|
|
||||||
|
const argv = await parseArguments();
|
||||||
|
|
||||||
|
// Verify that the argument was parsed correctly
|
||||||
|
expect(argv.approvalMode).toBe('auto_edit');
|
||||||
|
expect(argv.prompt).toBe('test');
|
||||||
|
expect(argv.yolo).toBe(false);
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse --approval-mode=yolo correctly through the full argument parsing flow', async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--approval-mode',
|
||||||
|
'yolo',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
];
|
||||||
|
|
||||||
|
const argv = await parseArguments();
|
||||||
|
|
||||||
|
expect(argv.approvalMode).toBe('yolo');
|
||||||
|
expect(argv.prompt).toBe('test');
|
||||||
|
expect(argv.yolo).toBe(false); // Should NOT be set when using --approval-mode
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse --approval-mode=default correctly through the full argument parsing flow', async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--approval-mode',
|
||||||
|
'default',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
];
|
||||||
|
|
||||||
|
const argv = await parseArguments();
|
||||||
|
|
||||||
|
expect(argv.approvalMode).toBe('default');
|
||||||
|
expect(argv.prompt).toBe('test');
|
||||||
|
expect(argv.yolo).toBe(false);
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse legacy --yolo flag correctly', async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
||||||
|
|
||||||
|
const argv = await parseArguments();
|
||||||
|
|
||||||
|
expect(argv.yolo).toBe(true);
|
||||||
|
expect(argv.approvalMode).toBeUndefined(); // Should NOT be set when using --yolo
|
||||||
|
expect(argv.prompt).toBe('test');
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid approval mode values during argument parsing', async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.argv = ['node', 'script.js', '--approval-mode', 'invalid_mode'];
|
||||||
|
|
||||||
|
// Should throw during argument parsing due to yargs validation
|
||||||
|
await expect(parseArguments()).rejects.toThrow();
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject conflicting --yolo and --approval-mode flags', async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--yolo',
|
||||||
|
'--approval-mode',
|
||||||
|
'default',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Should throw during argument parsing due to conflict validation
|
||||||
|
await expect(parseArguments()).rejects.toThrow();
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle backward compatibility with mixed scenarios', async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test that no approval mode arguments defaults to no flags set
|
||||||
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
|
|
||||||
|
const argv = await parseArguments();
|
||||||
|
|
||||||
|
expect(argv.approvalMode).toBeUndefined();
|
||||||
|
expect(argv.yolo).toBe(false);
|
||||||
|
expect(argv.prompt).toBe('test');
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -156,6 +156,93 @@ describe('parseArguments', () => {
|
||||||
expect(argv.promptInteractive).toBe('interactive prompt');
|
expect(argv.promptInteractive).toBe('interactive prompt');
|
||||||
expect(argv.prompt).toBeUndefined();
|
expect(argv.prompt).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error when both --yolo and --approval-mode are used together', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--yolo',
|
||||||
|
'--approval-mode',
|
||||||
|
'default',
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||||
|
throw new Error('process.exit called');
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockConsoleError = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||||
|
|
||||||
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockExit.mockRestore();
|
||||||
|
mockConsoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when using short flags -y and --approval-mode together', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '-y', '--approval-mode', 'yolo'];
|
||||||
|
|
||||||
|
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||||
|
throw new Error('process.exit called');
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockConsoleError = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||||
|
|
||||||
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockExit.mockRestore();
|
||||||
|
mockConsoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow --approval-mode without --yolo', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
expect(argv.approvalMode).toBe('auto_edit');
|
||||||
|
expect(argv.yolo).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow --yolo without --approval-mode', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--yolo'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
expect(argv.yolo).toBe(true);
|
||||||
|
expect(argv.approvalMode).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid --approval-mode values', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--approval-mode', 'invalid'];
|
||||||
|
|
||||||
|
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||||
|
throw new Error('process.exit called');
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockConsoleError = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
await expect(parseArguments()).rejects.toThrow('process.exit called');
|
||||||
|
|
||||||
|
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Invalid values:'),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockExit.mockRestore();
|
||||||
|
mockConsoleError.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadCliConfig', () => {
|
describe('loadCliConfig', () => {
|
||||||
|
@ -760,6 +847,211 @@ describe('mergeExcludeTools', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Approval mode tool exclusion logic', () => {
|
||||||
|
const originalIsTTY = process.stdin.isTTY;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.stdin.isTTY = false; // Ensure non-interactive mode
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.stdin.isTTY = originalIsTTY;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude all interactive tools in non-interactive mode with default approval mode', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings,
|
||||||
|
extensions,
|
||||||
|
'test-session',
|
||||||
|
argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludedTools = config.getExcludeTools();
|
||||||
|
expect(excludedTools).toContain(ShellTool.Name);
|
||||||
|
expect(excludedTools).toContain(EditTool.Name);
|
||||||
|
expect(excludedTools).toContain(WriteFileTool.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude all interactive tools in non-interactive mode with explicit default approval mode', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--approval-mode',
|
||||||
|
'default',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings,
|
||||||
|
extensions,
|
||||||
|
'test-session',
|
||||||
|
argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludedTools = config.getExcludeTools();
|
||||||
|
expect(excludedTools).toContain(ShellTool.Name);
|
||||||
|
expect(excludedTools).toContain(EditTool.Name);
|
||||||
|
expect(excludedTools).toContain(WriteFileTool.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude only shell tools in non-interactive mode with auto_edit approval mode', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--approval-mode',
|
||||||
|
'auto_edit',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings,
|
||||||
|
extensions,
|
||||||
|
'test-session',
|
||||||
|
argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludedTools = config.getExcludeTools();
|
||||||
|
expect(excludedTools).toContain(ShellTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(EditTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(WriteFileTool.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude no interactive tools in non-interactive mode with yolo approval mode', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--approval-mode',
|
||||||
|
'yolo',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings,
|
||||||
|
extensions,
|
||||||
|
'test-session',
|
||||||
|
argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludedTools = config.getExcludeTools();
|
||||||
|
expect(excludedTools).not.toContain(ShellTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(EditTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(WriteFileTool.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();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings,
|
||||||
|
extensions,
|
||||||
|
'test-session',
|
||||||
|
argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludedTools = config.getExcludeTools();
|
||||||
|
expect(excludedTools).not.toContain(ShellTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(EditTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(WriteFileTool.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not exclude interactive tools in interactive mode regardless of approval mode', async () => {
|
||||||
|
process.stdin.isTTY = true; // Interactive mode
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ args: ['node', 'script.js'] }, // default
|
||||||
|
{ args: ['node', 'script.js', '--approval-mode', 'default'] },
|
||||||
|
{ args: ['node', 'script.js', '--approval-mode', 'auto_edit'] },
|
||||||
|
{ args: ['node', 'script.js', '--approval-mode', 'yolo'] },
|
||||||
|
{ args: ['node', 'script.js', '--yolo'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
process.argv = testCase.args;
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = {};
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings,
|
||||||
|
extensions,
|
||||||
|
'test-session',
|
||||||
|
argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludedTools = config.getExcludeTools();
|
||||||
|
expect(excludedTools).not.toContain(ShellTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(EditTool.Name);
|
||||||
|
expect(excludedTools).not.toContain(WriteFileTool.Name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge approval mode exclusions with settings exclusions in auto_edit mode', async () => {
|
||||||
|
process.argv = [
|
||||||
|
'node',
|
||||||
|
'script.js',
|
||||||
|
'--approval-mode',
|
||||||
|
'auto_edit',
|
||||||
|
'-p',
|
||||||
|
'test',
|
||||||
|
];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const settings: Settings = { excludeTools: ['custom_tool'] };
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
const config = await loadCliConfig(
|
||||||
|
settings,
|
||||||
|
extensions,
|
||||||
|
'test-session',
|
||||||
|
argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludedTools = config.getExcludeTools();
|
||||||
|
expect(excludedTools).toContain('custom_tool'); // From settings
|
||||||
|
expect(excludedTools).toContain(ShellTool.Name); // From approval mode
|
||||||
|
expect(excludedTools).not.toContain(EditTool.Name); // Should be allowed in auto_edit
|
||||||
|
expect(excludedTools).not.toContain(WriteFileTool.Name); // Should be allowed in auto_edit
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error for invalid approval mode values in loadCliConfig', async () => {
|
||||||
|
// Create a mock argv with an invalid approval mode that bypasses argument parsing validation
|
||||||
|
const invalidArgv: Partial<CliArgs> & { approvalMode: string } = {
|
||||||
|
approvalMode: 'invalid_mode',
|
||||||
|
promptInteractive: '',
|
||||||
|
prompt: '',
|
||||||
|
yolo: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings: Settings = {};
|
||||||
|
const extensions: Extension[] = [];
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
loadCliConfig(settings, extensions, 'test-session', invalidArgv),
|
||||||
|
).rejects.toThrow(
|
||||||
|
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, default',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('loadCliConfig with allowed-mcp-server-names', () => {
|
describe('loadCliConfig with allowed-mcp-server-names', () => {
|
||||||
const originalArgv = process.argv;
|
const originalArgv = process.argv;
|
||||||
const originalEnv = { ...process.env };
|
const originalEnv = { ...process.env };
|
||||||
|
@ -1327,3 +1619,80 @@ describe('loadCliConfig interactive', () => {
|
||||||
expect(config.isInteractive()).toBe(false);
|
expect(config.isInteractive()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loadCliConfig approval mode', () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||||
|
process.env.GEMINI_API_KEY = 'test-api-key';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
process.env = originalEnv;
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default to DEFAULT approval mode when no flags are set', async () => {
|
||||||
|
process.argv = ['node', 'script.js'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set YOLO approval mode when --yolo flag is used', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--yolo'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set YOLO approval mode when -y flag is used', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '-y'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--approval-mode', 'default'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--approval-mode', 'yolo'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => {
|
||||||
|
// Note: This test documents the intended behavior, but in practice the validation
|
||||||
|
// prevents both flags from being used together
|
||||||
|
process.argv = ['node', 'script.js', '--approval-mode', 'default'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
// Manually set yolo to true to simulate what would happen if validation didn't prevent it
|
||||||
|
argv.yolo = true;
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to --yolo behavior when --approval-mode is not set', async () => {
|
||||||
|
process.argv = ['node', 'script.js', '--yolo'];
|
||||||
|
const argv = await parseArguments();
|
||||||
|
const config = await loadCliConfig({}, [], 'test-session', argv);
|
||||||
|
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -57,6 +57,7 @@ export interface CliArgs {
|
||||||
showMemoryUsage: boolean | undefined;
|
showMemoryUsage: boolean | undefined;
|
||||||
show_memory_usage: boolean | undefined;
|
show_memory_usage: boolean | undefined;
|
||||||
yolo: boolean | undefined;
|
yolo: boolean | undefined;
|
||||||
|
approvalMode: string | undefined;
|
||||||
telemetry: boolean | undefined;
|
telemetry: boolean | undefined;
|
||||||
checkpointing: boolean | undefined;
|
checkpointing: boolean | undefined;
|
||||||
telemetryTarget: string | undefined;
|
telemetryTarget: string | undefined;
|
||||||
|
@ -147,6 +148,12 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||||
'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?',
|
'Automatically accept all actions (aka YOLO mode, see https://www.youtube.com/watch?v=xvFZjo5PgG0 for more details)?',
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
.option('approval-mode', {
|
||||||
|
type: 'string',
|
||||||
|
choices: ['default', 'auto_edit', 'yolo'],
|
||||||
|
description:
|
||||||
|
'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools)',
|
||||||
|
})
|
||||||
.option('telemetry', {
|
.option('telemetry', {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description:
|
description:
|
||||||
|
@ -219,6 +226,11 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||||
'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
|
'Cannot use both --prompt (-p) and --prompt-interactive (-i) together',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (argv.yolo && argv.approvalMode) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.',
|
||||||
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
@ -356,20 +368,59 @@ export async function loadCliConfig(
|
||||||
|
|
||||||
let mcpServers = mergeMcpServers(settings, activeExtensions);
|
let mcpServers = mergeMcpServers(settings, activeExtensions);
|
||||||
const question = argv.promptInteractive || argv.prompt || '';
|
const question = argv.promptInteractive || argv.prompt || '';
|
||||||
const approvalMode =
|
|
||||||
argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
|
// Determine approval mode with backward compatibility
|
||||||
|
let approvalMode: ApprovalMode;
|
||||||
|
if (argv.approvalMode) {
|
||||||
|
// New --approval-mode flag takes precedence
|
||||||
|
switch (argv.approvalMode) {
|
||||||
|
case 'yolo':
|
||||||
|
approvalMode = ApprovalMode.YOLO;
|
||||||
|
break;
|
||||||
|
case 'auto_edit':
|
||||||
|
approvalMode = ApprovalMode.AUTO_EDIT;
|
||||||
|
break;
|
||||||
|
case 'default':
|
||||||
|
approvalMode = ApprovalMode.DEFAULT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Invalid approval mode: ${argv.approvalMode}. Valid values are: yolo, auto_edit, default`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to legacy --yolo flag behavior
|
||||||
|
approvalMode =
|
||||||
|
argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
const interactive =
|
const interactive =
|
||||||
!!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
|
!!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
|
||||||
// In non-interactive and non-yolo mode, exclude interactive built in tools.
|
// In non-interactive mode, exclude tools that require a prompt.
|
||||||
const extraExcludes =
|
const extraExcludes: string[] = [];
|
||||||
!interactive && approvalMode !== ApprovalMode.YOLO
|
if (!interactive) {
|
||||||
? [ShellTool.Name, EditTool.Name, WriteFileTool.Name]
|
switch (approvalMode) {
|
||||||
: undefined;
|
case ApprovalMode.DEFAULT:
|
||||||
|
// In default non-interactive mode, all tools that require approval are excluded.
|
||||||
|
extraExcludes.push(ShellTool.Name, EditTool.Name, WriteFileTool.Name);
|
||||||
|
break;
|
||||||
|
case ApprovalMode.AUTO_EDIT:
|
||||||
|
// In auto-edit non-interactive mode, only tools that still require a prompt are excluded.
|
||||||
|
extraExcludes.push(ShellTool.Name);
|
||||||
|
break;
|
||||||
|
case ApprovalMode.YOLO:
|
||||||
|
// No extra excludes for YOLO mode.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// This should never happen due to validation earlier, but satisfies the linter
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const excludeTools = mergeExcludeTools(
|
const excludeTools = mergeExcludeTools(
|
||||||
settings,
|
settings,
|
||||||
activeExtensions,
|
activeExtensions,
|
||||||
extraExcludes,
|
extraExcludes.length > 0 ? extraExcludes : undefined,
|
||||||
);
|
);
|
||||||
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
|
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue