diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 5e50764d2b..047a0eff31 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -933,6 +933,21 @@ their corresponding top-level category object in your `settings.json` file. - **Description:** Hooks that execute before tool selection. Can filter or prioritize available tools dynamically. - **Default:** `[]` + +#### `admin` + +- **`admin.secureModeEnabled`** (boolean): + - **Description:** If true, disallows yolo mode from being used. + - **Default:** `false` + +- **`admin.extensions.enabled`** (boolean): + - **Description:** If false, disallows extensions from being installed or + used. + - **Default:** `true` + +- **`admin.mcp.enabled`** (boolean): + - **Description:** If false, disallows MCP servers from being used. + - **Default:** `true` #### `mcpServers` diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 3d5b45df80..465ed90bca 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1069,7 +1069,7 @@ describe('Approval mode tool exclusion logic', () => { }; await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow( - 'Cannot start in YOLO mode when it is disabled by settings', + 'Cannot start in YOLO mode since it is disabled by your admin', ); }); @@ -2412,3 +2412,134 @@ describe('Policy Engine Integration in loadCliConfig', () => { ); }); }); + +describe('loadCliConfig secureModeEnabled', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); + vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); + vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]); + vi.mocked(isWorkspaceTrusted).mockReturnValue({ + isTrusted: true, + source: undefined, + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it('should throw an error if YOLO mode is attempted when secureModeEnabled is true', async () => { + process.argv = ['node', 'script.js', '--yolo']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + admin: { + secureModeEnabled: true, + }, + }; + + await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow( + 'Cannot start in YOLO mode since it is disabled by your admin', + ); + }); + + it('should throw an error if approval-mode=yolo is attempted when secureModeEnabled is true', async () => { + process.argv = ['node', 'script.js', '--approval-mode=yolo']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + admin: { + secureModeEnabled: true, + }, + }; + + await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow( + 'Cannot start in YOLO mode since it is disabled by your admin', + ); + }); + + it('should set disableYoloMode to true when secureModeEnabled is true', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + admin: { + secureModeEnabled: true, + }, + }; + const config = await loadCliConfig(settings, 'test-session', argv); + expect(config.isYoloModeDisabled()).toBe(true); + }); +}); + +describe('loadCliConfig mcpEnabled', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); + vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); + vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + const mcpSettings = { + mcp: { + serverCommand: 'mcp-server', + allowed: ['serverA'], + excluded: ['serverB'], + }, + mcpServers: { serverA: { url: 'http://a' } }, + }; + + it('should enable MCP by default', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { ...mcpSettings }; + const config = await loadCliConfig(settings, 'test-session', argv); + expect(config.getMcpEnabled()).toBe(true); + expect(config.getMcpServerCommand()).toBe('mcp-server'); + expect(config.getMcpServers()).toEqual({ serverA: { url: 'http://a' } }); + expect(config.getAllowedMcpServers()).toEqual(['serverA']); + expect(config.getBlockedMcpServers()).toEqual(['serverB']); + }); + + it('should disable MCP when mcpEnabled is false', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + ...mcpSettings, + admin: { + mcp: { + enabled: false, + }, + }, + }; + const config = await loadCliConfig(settings, 'test-session', argv); + expect(config.getMcpEnabled()).toBe(false); + expect(config.getMcpServerCommand()).toBeUndefined(); + expect(config.getMcpServers()).toEqual({}); + expect(config.getAllowedMcpServers()).toEqual([]); + expect(config.getBlockedMcpServers()).toEqual([]); + }); + + it('should enable MCP when mcpEnabled is true', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + ...mcpSettings, + admin: { + mcp: { + enabled: true, + }, + }, + }; + const config = await loadCliConfig(settings, 'test-session', argv); + expect(config.getMcpEnabled()).toBe(true); + expect(config.getMcpServerCommand()).toBe('mcp-server'); + expect(config.getMcpServers()).toEqual({ serverA: { url: 'http://a' } }); + expect(config.getAllowedMcpServers()).toEqual(['serverA']); + expect(config.getBlockedMcpServers()).toEqual(['serverB']); + }); +}); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 8f233f77fa..aa00fbe9f2 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -505,11 +505,19 @@ export async function loadCliConfig( } // Override approval mode if disableYoloMode is set. - if (settings.security?.disableYoloMode) { + if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) { if (approvalMode === ApprovalMode.YOLO) { - debugLogger.error('YOLO mode is disabled by the "disableYolo" setting.'); + if (settings.admin?.secureModeEnabled) { + debugLogger.error( + 'YOLO mode is disabled by "secureModeEnabled" setting.', + ); + } else { + debugLogger.error( + 'YOLO mode is disabled by the "disableYolo" setting.', + ); + } throw new FatalConfigError( - 'Cannot start in YOLO mode when it is disabled by settings', + 'Cannot start in YOLO mode since it is disabled by your admin', ); } approvalMode = ApprovalMode.DEFAULT; @@ -628,6 +636,8 @@ export async function loadCliConfig( const ptyInfo = await getPty(); + const mcpEnabled = settings.admin?.mcp?.enabled ?? true; + return new Config({ sessionId, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, @@ -646,12 +656,17 @@ export async function loadCliConfig( excludeTools, toolDiscoveryCommand: settings.tools?.discoveryCommand, toolCallCommand: settings.tools?.callCommand, - mcpServerCommand: settings.mcp?.serverCommand, - mcpServers: settings.mcpServers, - allowedMcpServers: argv.allowedMcpServerNames ?? settings.mcp?.allowed, - blockedMcpServers: argv.allowedMcpServerNames - ? undefined - : settings.mcp?.excluded, + mcpServerCommand: mcpEnabled ? settings.mcp?.serverCommand : undefined, + mcpServers: mcpEnabled ? settings.mcpServers : {}, + mcpEnabled, + allowedMcpServers: mcpEnabled + ? (argv.allowedMcpServerNames ?? settings.mcp?.allowed) + : undefined, + blockedMcpServers: mcpEnabled + ? argv.allowedMcpServerNames + ? undefined + : settings.mcp?.excluded + : undefined, blockedEnvironmentVariables: settings.security?.environmentVariableRedaction?.blocked, enableEnvironmentVariableRedaction: @@ -660,7 +675,8 @@ export async function loadCliConfig( geminiMdFileCount: fileCount, geminiMdFilePaths: filePaths, approvalMode, - disableYoloMode: settings.security?.disableYoloMode, + disableYoloMode: + settings.security?.disableYoloMode || settings.admin?.secureModeEnabled, showMemoryUsage: settings.ui?.showMemoryUsage || false, accessibility: { ...settings.ui?.accessibility, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index ba5f9895cd..ee5a8e71c3 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1718,6 +1718,74 @@ const SETTINGS_SCHEMA = { mergeStrategy: MergeStrategy.CONCAT, }, }, + + admin: { + type: 'object', + label: 'Admin', + category: 'Admin', + requiresRestart: false, + default: {}, + description: 'Settings configured remotely by enterprise admins.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + properties: { + secureModeEnabled: { + type: 'boolean', + label: 'Secure Mode Enabled', + category: 'Admin', + requiresRestart: false, + default: false, + description: 'If true, disallows yolo mode from being used.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + }, + extensions: { + type: 'object', + label: 'Extensions Settings', + category: 'Admin', + requiresRestart: false, + default: {}, + description: 'Extensions-specific admin settings.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + properties: { + enabled: { + type: 'boolean', + label: 'Extensions Enabled', + category: 'Admin', + requiresRestart: false, + default: true, + description: + 'If false, disallows extensions from being installed or used.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + }, + }, + }, + mcp: { + type: 'object', + label: 'MCP Settings', + category: 'Admin', + requiresRestart: false, + default: {}, + description: 'MCP-specific admin settings.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + properties: { + enabled: { + type: 'boolean', + label: 'MCP Enabled', + category: 'Admin', + requiresRestart: false, + default: true, + description: 'If false, disallows MCP servers from being used.', + showInDialog: false, + mergeStrategy: MergeStrategy.REPLACE, + }, + }, + }, + }, + }, } as const satisfies SettingsSchema; export type SettingsSchemaType = typeof SETTINGS_SCHEMA; diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index 4d8fe6773d..b99d58239e 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -102,6 +102,7 @@ describe('BuiltinCommandLoader', () => { getEnableExtensionReloading: () => false, getEnableHooks: () => false, isSkillsSupportEnabled: vi.fn().mockReturnValue(false), + getMcpEnabled: vi.fn().mockReturnValue(true), getSkillManager: vi.fn().mockReturnValue({ getAllSkills: vi.fn().mockReturnValue([]), }), @@ -179,6 +180,7 @@ describe('BuiltinCommandLoader', () => { const mockConfigWithMessageBus = { ...mockConfig, getEnableHooks: () => false, + getMcpEnabled: () => true, } as unknown as Config; const loader = new BuiltinCommandLoader(mockConfigWithMessageBus); const commands = await loader.loadCommands(new AbortController().signal); @@ -198,6 +200,7 @@ describe('BuiltinCommandLoader profile', () => { getEnableExtensionReloading: () => false, getEnableHooks: () => false, isSkillsSupportEnabled: vi.fn().mockReturnValue(false), + getMcpEnabled: vi.fn().mockReturnValue(true), getSkillManager: vi.fn().mockReturnValue({ getAllSkills: vi.fn().mockReturnValue([]), }), diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 6978322bbf..31395c0172 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -6,8 +6,12 @@ import { isDevelopment } from '../utils/installationInfo.js'; import type { ICommandLoader } from './types.js'; -import type { SlashCommand } from '../ui/commands/types.js'; -import type { Config } from '@google/gemini-cli-core'; +import { + CommandKind, + type SlashCommand, + type CommandContext, +} from '../ui/commands/types.js'; +import type { MessageActionReturn, Config } from '@google/gemini-cli-core'; import { startupProfiler } from '@google/gemini-cli-core'; import { aboutCommand } from '../ui/commands/aboutCommand.js'; import { authCommand } from '../ui/commands/authCommand.js'; @@ -77,7 +81,25 @@ export class BuiltinCommandLoader implements ICommandLoader { ...(this.config?.getEnableHooks() ? [hooksCommand] : []), await ideCommand(), initCommand, - mcpCommand, + ...(this.config?.getMcpEnabled() === false + ? [ + { + name: 'mcp', + description: + 'Manage configured Model Context Protocol (MCP) servers', + kind: CommandKind.BUILT_IN, + autoExecute: false, + subCommands: [], + action: async ( + _context: CommandContext, + ): Promise => ({ + type: 'message', + messageType: 'error', + content: 'MCP disabled by your admin.', + }), + }, + ] + : [mcpCommand]), memoryCommand, modelCommand, ...(this.config?.getFolderTrust() ? [permissionsCommand] : []), diff --git a/packages/core/src/code_assist/types.ts b/packages/core/src/code_assist/types.ts index 824f6ff530..3fd81d465b 100644 --- a/packages/core/src/code_assist/types.ts +++ b/packages/core/src/code_assist/types.ts @@ -277,3 +277,26 @@ export interface ConversationInteraction { language?: string; isAgentic?: boolean; } + +export interface GeminiCodeAssistSetting { + secureModeEnabled?: boolean; + mcpSetting?: McpSetting; + cliFeatureSetting?: CliFeatureSetting; +} + +export interface McpSetting { + mcpEnabled?: boolean; + allowedMcpConfigs?: McpConfig[]; +} + +export interface McpConfig { + mcpServer?: string; +} + +export interface CliFeatureSetting { + extensionsSetting?: ExtensionsSetting; +} + +export interface ExtensionsSetting { + extensionsEnabled?: boolean; +} diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 616743acda..5859de2133 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -88,6 +88,7 @@ import type { PolicyEngineConfig } from '../policy/types.js'; import { HookSystem } from '../hooks/index.js'; import type { UserTierId } from '../code_assist/types.js'; import type { RetrieveUserQuotaResponse } from '../code_assist/types.js'; +import type { GeminiCodeAssistSetting } from '../code_assist/types.js'; import { getCodeAssistServer } from '../code_assist/codeAssist.js'; import type { Experiments } from '../code_assist/experiments/experiments.js'; import { AgentRegistry } from '../agents/registry.js'; @@ -356,6 +357,7 @@ export interface ConfigParameters { disabledSkills?: string[]; experimentalJitContext?: boolean; onModelChange?: (model: string) => void; + mcpEnabled?: boolean; onReload?: () => Promise<{ disabledSkills?: string[] }>; } @@ -389,6 +391,7 @@ export class Config { private readonly toolDiscoveryCommand: string | undefined; private readonly toolCallCommand: string | undefined; private readonly mcpServerCommand: string | undefined; + private readonly mcpEnabled: boolean; private mcpServers: Record | undefined; private userMemory: string; private geminiMdFileCount: number; @@ -491,6 +494,7 @@ export class Config { private readonly experimentalJitContext: boolean; private contextManager?: ContextManager; private terminalBackground: string | undefined = undefined; + private remoteAdminSettings: GeminiCodeAssistSetting | undefined; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -512,6 +516,7 @@ export class Config { this.toolCallCommand = params.toolCallCommand; this.mcpServerCommand = params.mcpServerCommand; this.mcpServers = params.mcpServers; + this.mcpEnabled = params.mcpEnabled ?? true; this.allowedMcpServers = params.allowedMcpServers ?? []; this.blockedMcpServers = params.blockedMcpServers ?? []; this.allowedEnvironmentVariables = params.allowedEnvironmentVariables ?? []; @@ -894,6 +899,14 @@ export class Config { return this.terminalBackground; } + getRemoteAdminSettings(): GeminiCodeAssistSetting | undefined { + return this.remoteAdminSettings; + } + + setRemoteAdminSettings(settings: GeminiCodeAssistSetting): void { + this.remoteAdminSettings = settings; + } + shouldLoadMemoryFromIncludeDirectories(): boolean { return this.loadMemoryFromIncludeDirectories; } @@ -1125,6 +1138,10 @@ export class Config { return this.mcpServers; } + getMcpEnabled(): boolean { + return this.mcpEnabled; + } + getMcpClientManager(): McpClientManager | undefined { return this.mcpClientManager; } diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index cbf96f738d..f7f134d2c9 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1608,6 +1608,57 @@ "type": "array", "items": {} } + }, + "admin": { + "title": "Admin", + "description": "Settings configured remotely by enterprise admins.", + "markdownDescription": "Settings configured remotely by enterprise admins.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`", + "default": {}, + "type": "object", + "properties": { + "secureModeEnabled": { + "title": "Secure Mode Enabled", + "description": "If true, disallows yolo mode from being used.", + "markdownDescription": "If true, disallows yolo mode from being used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `false`", + "default": false, + "type": "boolean" + }, + "extensions": { + "title": "Extensions Settings", + "description": "Extensions-specific admin settings.", + "markdownDescription": "Extensions-specific admin settings.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`", + "default": {}, + "type": "object", + "properties": { + "enabled": { + "title": "Extensions Enabled", + "description": "If false, disallows extensions from being installed or used.", + "markdownDescription": "If false, disallows extensions from being installed or used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `true`", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "mcp": { + "title": "MCP Settings", + "description": "MCP-specific admin settings.", + "markdownDescription": "MCP-specific admin settings.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `{}`", + "default": {}, + "type": "object", + "properties": { + "enabled": { + "title": "MCP Enabled", + "description": "If false, disallows MCP servers from being used.", + "markdownDescription": "If false, disallows MCP servers from being used.\n\n- Category: `Admin`\n- Requires restart: `no`\n- Default: `true`", + "default": true, + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "$defs": {