feat(policy): map --yolo to allowedTools wildcard policy

This PR maps the `--yolo` flag natively into a wildcard policy array
(`allowedTools: ["*"]`) and removes the concept of `ApprovalMode.YOLO` as a
distinct state in the application, fulfilling issue #11303.

This removes the hardcoded `ApprovalMode.YOLO` state and its associated
UI/bypasses. The `PolicyEngine` now evaluates YOLO purely via data-driven rules.

- Removes `ApprovalMode.YOLO`
- Removes UI toggle (`Ctrl+Y`) and indicators for YOLO
- Removes `yolo.toml`
- Updates A2A server and CLI config logic to translate YOLO into a wildcard tool
- Rewrites policy engine tests to evaluate the wildcard
- Enforces enterprise `disableYoloMode` and `secureModeEnabled` controls
  by actively preventing manual `--allowed-tools=*` bypasses.

Fixes #11303
This commit is contained in:
Spencer
2026-03-19 02:43:14 +00:00
parent 8db2948361
commit 9556a1d620
84 changed files with 521 additions and 1057 deletions
@@ -9,7 +9,6 @@ import {
type Config,
MessageBusType,
ToolConfirmationOutcome,
ApprovalMode,
Scheduler,
type MessageBus,
} from '@google/gemini-cli-core';
@@ -358,7 +357,7 @@ describe('Task Event-Driven Scheduler', () => {
// Enable YOLO mode
const yoloConfig = createMockConfig({
isEventDrivenSchedulerEnabled: () => true,
getApprovalMode: () => ApprovalMode.YOLO,
getAllowedTools: () => ['*'],
}) as Config;
const yoloMessageBus = yoloConfig.messageBus;
+2 -4
View File
@@ -10,7 +10,6 @@ import {
type GeminiClient,
GeminiEventType,
ToolConfirmationOutcome,
ApprovalMode,
getAllMCPServerStatuses,
MCPServerStatus,
isNodeError,
@@ -89,7 +88,8 @@ export class Task {
autoExecute: boolean;
private get isYoloMatch(): boolean {
return (
this.autoExecute || this.config.getApprovalMode() === ApprovalMode.YOLO
this.autoExecute ||
(this.config.getAllowedTools()?.includes('*') ?? false)
);
}
@@ -880,9 +880,7 @@ export class Task {
if (
part.kind !== 'data' ||
!part.data ||
// eslint-disable-next-line no-restricted-syntax
typeof part.data['callId'] !== 'string' ||
// eslint-disable-next-line no-restricted-syntax
typeof part.data['outcome'] !== 'string'
) {
return false;
+5 -18
View File
@@ -19,8 +19,6 @@ import {
AuthType,
isHeadlessMode,
FatalAuthenticationError,
PolicyDecision,
PRIORITY_YOLO_ALLOW_ALL,
} from '@google/gemini-cli-core';
// Mock dependencies
@@ -375,35 +373,24 @@ describe('loadConfig', () => {
});
describe('YOLO mode', () => {
it('should enable YOLO mode and add policy rule when GEMINI_YOLO_MODE is true', async () => {
it('should enable wildcard allowedTools when GEMINI_YOLO_MODE is true', async () => {
vi.stubEnv('GEMINI_YOLO_MODE', 'true');
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
approvalMode: 'yolo',
policyEngineConfig: expect.objectContaining({
rules: expect.arrayContaining([
expect.objectContaining({
decision: PolicyDecision.ALLOW,
priority: PRIORITY_YOLO_ALLOW_ALL,
modes: ['yolo'],
allowRedirection: true,
}),
]),
}),
approvalMode: 'default',
allowedTools: expect.arrayContaining(['*']),
}),
);
});
it('should use default approval mode and empty rules when GEMINI_YOLO_MODE is not true', async () => {
it('should use default approval mode and undefined allowedTools when GEMINI_YOLO_MODE is not true', async () => {
vi.stubEnv('GEMINI_YOLO_MODE', 'false');
await loadConfig(mockSettings, mockExtensionLoader, taskId);
expect(Config).toHaveBeenCalledWith(
expect.objectContaining({
approvalMode: 'default',
policyEngineConfig: expect.objectContaining({
rules: [],
}),
allowedTools: undefined,
}),
);
});
+5 -22
View File
@@ -26,8 +26,6 @@ import {
isHeadlessMode,
FatalAuthenticationError,
isCloudShell,
PolicyDecision,
PRIORITY_YOLO_ALLOW_ALL,
type TelemetryTarget,
type ConfigParameters,
type ExtensionLoader,
@@ -62,11 +60,6 @@ export async function loadConfig(
}
}
const approvalMode =
process.env['GEMINI_YOLO_MODE'] === 'true'
? ApprovalMode.YOLO
: ApprovalMode.DEFAULT;
const configParams: ConfigParameters = {
sessionId: taskId,
clientName: 'a2a-server',
@@ -79,22 +72,12 @@ export async function loadConfig(
coreTools: settings.coreTools || settings.tools?.core || undefined,
excludeTools: settings.excludeTools || settings.tools?.exclude || undefined,
allowedTools: settings.allowedTools || settings.tools?.allowed || undefined,
allowedTools:
process.env['GEMINI_YOLO_MODE'] === 'true'
? [...(settings.allowedTools || settings.tools?.allowed || []), '*']
: settings.allowedTools || settings.tools?.allowed || undefined,
showMemoryUsage: settings.showMemoryUsage || false,
approvalMode,
policyEngineConfig: {
rules:
approvalMode === ApprovalMode.YOLO
? [
{
decision: PolicyDecision.ALLOW,
priority: PRIORITY_YOLO_ALLOW_ALL,
modes: [ApprovalMode.YOLO],
allowRedirection: true,
},
]
: [],
},
approvalMode: ApprovalMode.DEFAULT,
mcpServers: settings.mcpServers,
cwd: workspaceDir,
telemetry: {
+5 -2
View File
@@ -72,6 +72,7 @@ const getToolRegistrySpy = vi.fn().mockReturnValue({
getToolsByServer: vi.fn().mockReturnValue([]),
});
const getApprovalModeSpy = vi.fn();
const getAllowedToolsSpy = vi.fn();
const getShellExecutionConfigSpy = vi.fn();
const getExtensionsSpy = vi.fn();
@@ -83,6 +84,7 @@ vi.mock('../config/config.js', async () => {
const mockConfig = createMockConfig({
getToolRegistry: getToolRegistrySpy,
getApprovalMode: getApprovalModeSpy,
getAllowedTools: getAllowedToolsSpy,
getShellExecutionConfig: getShellExecutionConfigSpy,
getExtensions: getExtensionsSpy,
});
@@ -118,6 +120,7 @@ describe('E2E Tests', () => {
beforeEach(() => {
getApprovalModeSpy.mockReturnValue(ApprovalMode.DEFAULT);
getAllowedToolsSpy.mockReturnValue([]);
});
afterAll(
@@ -406,7 +409,7 @@ describe('E2E Tests', () => {
it('should handle multiple tool calls sequentially in YOLO mode', async () => {
// Set YOLO mode to auto-approve tools and test sequential execution.
getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO);
getAllowedToolsSpy.mockReturnValue(['*']);
// First call yields the tool request
sendMessageStreamSpy.mockImplementationOnce(async function* () {
@@ -697,7 +700,7 @@ describe('E2E Tests', () => {
});
// Set approval mode to yolo
getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO);
getAllowedToolsSpy.mockReturnValue(['*']);
const mockTool = new MockTool({
name: 'test-tool-yolo',
@@ -126,8 +126,8 @@ export function createMockConfig(
mockConfig.getPolicyEngine = vi.fn().mockReturnValue({
check: async () => {
const mode = mockConfig.getApprovalMode();
if (mode === ApprovalMode.YOLO) {
const allowed = mockConfig.getAllowedTools?.() || [];
if (allowed.includes('*')) {
return { decision: PolicyDecision.ALLOW };
}
return { decision: PolicyDecision.ASK_USER };