feat(admin): Introduce remote admin settings & implement secureModeEnabled/mcpEnabled (#15935)

This commit is contained in:
Shreya Keshive
2026-01-06 16:38:07 -05:00
committed by GitHub
parent 56092bd782
commit 2fe45834dd
9 changed files with 360 additions and 14 deletions
+15
View File
@@ -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 - **Description:** Hooks that execute before tool selection. Can filter or
prioritize available tools dynamically. prioritize available tools dynamically.
- **Default:** `[]` - **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`
<!-- SETTINGS-AUTOGEN:END --> <!-- SETTINGS-AUTOGEN:END -->
#### `mcpServers` #### `mcpServers`
+132 -1
View File
@@ -1069,7 +1069,7 @@ describe('Approval mode tool exclusion logic', () => {
}; };
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow( 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']);
});
});
+26 -10
View File
@@ -505,11 +505,19 @@ export async function loadCliConfig(
} }
// Override approval mode if disableYoloMode is set. // Override approval mode if disableYoloMode is set.
if (settings.security?.disableYoloMode) { if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) {
if (approvalMode === ApprovalMode.YOLO) { 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( 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; approvalMode = ApprovalMode.DEFAULT;
@@ -628,6 +636,8 @@ export async function loadCliConfig(
const ptyInfo = await getPty(); const ptyInfo = await getPty();
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
return new Config({ return new Config({
sessionId, sessionId,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
@@ -646,12 +656,17 @@ export async function loadCliConfig(
excludeTools, excludeTools,
toolDiscoveryCommand: settings.tools?.discoveryCommand, toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand, toolCallCommand: settings.tools?.callCommand,
mcpServerCommand: settings.mcp?.serverCommand, mcpServerCommand: mcpEnabled ? settings.mcp?.serverCommand : undefined,
mcpServers: settings.mcpServers, mcpServers: mcpEnabled ? settings.mcpServers : {},
allowedMcpServers: argv.allowedMcpServerNames ?? settings.mcp?.allowed, mcpEnabled,
blockedMcpServers: argv.allowedMcpServerNames allowedMcpServers: mcpEnabled
? undefined ? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
: settings.mcp?.excluded, : undefined,
blockedMcpServers: mcpEnabled
? argv.allowedMcpServerNames
? undefined
: settings.mcp?.excluded
: undefined,
blockedEnvironmentVariables: blockedEnvironmentVariables:
settings.security?.environmentVariableRedaction?.blocked, settings.security?.environmentVariableRedaction?.blocked,
enableEnvironmentVariableRedaction: enableEnvironmentVariableRedaction:
@@ -660,7 +675,8 @@ export async function loadCliConfig(
geminiMdFileCount: fileCount, geminiMdFileCount: fileCount,
geminiMdFilePaths: filePaths, geminiMdFilePaths: filePaths,
approvalMode, approvalMode,
disableYoloMode: settings.security?.disableYoloMode, disableYoloMode:
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
showMemoryUsage: settings.ui?.showMemoryUsage || false, showMemoryUsage: settings.ui?.showMemoryUsage || false,
accessibility: { accessibility: {
...settings.ui?.accessibility, ...settings.ui?.accessibility,
+68
View File
@@ -1718,6 +1718,74 @@ const SETTINGS_SCHEMA = {
mergeStrategy: MergeStrategy.CONCAT, 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; } as const satisfies SettingsSchema;
export type SettingsSchemaType = typeof SETTINGS_SCHEMA; export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
@@ -102,6 +102,7 @@ describe('BuiltinCommandLoader', () => {
getEnableExtensionReloading: () => false, getEnableExtensionReloading: () => false,
getEnableHooks: () => false, getEnableHooks: () => false,
isSkillsSupportEnabled: vi.fn().mockReturnValue(false), isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
getMcpEnabled: vi.fn().mockReturnValue(true),
getSkillManager: vi.fn().mockReturnValue({ getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue([]), getAllSkills: vi.fn().mockReturnValue([]),
}), }),
@@ -179,6 +180,7 @@ describe('BuiltinCommandLoader', () => {
const mockConfigWithMessageBus = { const mockConfigWithMessageBus = {
...mockConfig, ...mockConfig,
getEnableHooks: () => false, getEnableHooks: () => false,
getMcpEnabled: () => true,
} as unknown as Config; } as unknown as Config;
const loader = new BuiltinCommandLoader(mockConfigWithMessageBus); const loader = new BuiltinCommandLoader(mockConfigWithMessageBus);
const commands = await loader.loadCommands(new AbortController().signal); const commands = await loader.loadCommands(new AbortController().signal);
@@ -198,6 +200,7 @@ describe('BuiltinCommandLoader profile', () => {
getEnableExtensionReloading: () => false, getEnableExtensionReloading: () => false,
getEnableHooks: () => false, getEnableHooks: () => false,
isSkillsSupportEnabled: vi.fn().mockReturnValue(false), isSkillsSupportEnabled: vi.fn().mockReturnValue(false),
getMcpEnabled: vi.fn().mockReturnValue(true),
getSkillManager: vi.fn().mockReturnValue({ getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue([]), getAllSkills: vi.fn().mockReturnValue([]),
}), }),
@@ -6,8 +6,12 @@
import { isDevelopment } from '../utils/installationInfo.js'; import { isDevelopment } from '../utils/installationInfo.js';
import type { ICommandLoader } from './types.js'; import type { ICommandLoader } from './types.js';
import type { SlashCommand } from '../ui/commands/types.js'; import {
import type { Config } from '@google/gemini-cli-core'; 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 { startupProfiler } from '@google/gemini-cli-core';
import { aboutCommand } from '../ui/commands/aboutCommand.js'; import { aboutCommand } from '../ui/commands/aboutCommand.js';
import { authCommand } from '../ui/commands/authCommand.js'; import { authCommand } from '../ui/commands/authCommand.js';
@@ -77,7 +81,25 @@ export class BuiltinCommandLoader implements ICommandLoader {
...(this.config?.getEnableHooks() ? [hooksCommand] : []), ...(this.config?.getEnableHooks() ? [hooksCommand] : []),
await ideCommand(), await ideCommand(),
initCommand, 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<MessageActionReturn> => ({
type: 'message',
messageType: 'error',
content: 'MCP disabled by your admin.',
}),
},
]
: [mcpCommand]),
memoryCommand, memoryCommand,
modelCommand, modelCommand,
...(this.config?.getFolderTrust() ? [permissionsCommand] : []), ...(this.config?.getFolderTrust() ? [permissionsCommand] : []),
+23
View File
@@ -277,3 +277,26 @@ export interface ConversationInteraction {
language?: string; language?: string;
isAgentic?: boolean; 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;
}
+17
View File
@@ -88,6 +88,7 @@ import type { PolicyEngineConfig } from '../policy/types.js';
import { HookSystem } from '../hooks/index.js'; import { HookSystem } from '../hooks/index.js';
import type { UserTierId } from '../code_assist/types.js'; import type { UserTierId } from '../code_assist/types.js';
import type { RetrieveUserQuotaResponse } 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 { getCodeAssistServer } from '../code_assist/codeAssist.js';
import type { Experiments } from '../code_assist/experiments/experiments.js'; import type { Experiments } from '../code_assist/experiments/experiments.js';
import { AgentRegistry } from '../agents/registry.js'; import { AgentRegistry } from '../agents/registry.js';
@@ -356,6 +357,7 @@ export interface ConfigParameters {
disabledSkills?: string[]; disabledSkills?: string[];
experimentalJitContext?: boolean; experimentalJitContext?: boolean;
onModelChange?: (model: string) => void; onModelChange?: (model: string) => void;
mcpEnabled?: boolean;
onReload?: () => Promise<{ disabledSkills?: string[] }>; onReload?: () => Promise<{ disabledSkills?: string[] }>;
} }
@@ -389,6 +391,7 @@ export class Config {
private readonly toolDiscoveryCommand: string | undefined; private readonly toolDiscoveryCommand: string | undefined;
private readonly toolCallCommand: string | undefined; private readonly toolCallCommand: string | undefined;
private readonly mcpServerCommand: string | undefined; private readonly mcpServerCommand: string | undefined;
private readonly mcpEnabled: boolean;
private mcpServers: Record<string, MCPServerConfig> | undefined; private mcpServers: Record<string, MCPServerConfig> | undefined;
private userMemory: string; private userMemory: string;
private geminiMdFileCount: number; private geminiMdFileCount: number;
@@ -491,6 +494,7 @@ export class Config {
private readonly experimentalJitContext: boolean; private readonly experimentalJitContext: boolean;
private contextManager?: ContextManager; private contextManager?: ContextManager;
private terminalBackground: string | undefined = undefined; private terminalBackground: string | undefined = undefined;
private remoteAdminSettings: GeminiCodeAssistSetting | undefined;
constructor(params: ConfigParameters) { constructor(params: ConfigParameters) {
this.sessionId = params.sessionId; this.sessionId = params.sessionId;
@@ -512,6 +516,7 @@ export class Config {
this.toolCallCommand = params.toolCallCommand; this.toolCallCommand = params.toolCallCommand;
this.mcpServerCommand = params.mcpServerCommand; this.mcpServerCommand = params.mcpServerCommand;
this.mcpServers = params.mcpServers; this.mcpServers = params.mcpServers;
this.mcpEnabled = params.mcpEnabled ?? true;
this.allowedMcpServers = params.allowedMcpServers ?? []; this.allowedMcpServers = params.allowedMcpServers ?? [];
this.blockedMcpServers = params.blockedMcpServers ?? []; this.blockedMcpServers = params.blockedMcpServers ?? [];
this.allowedEnvironmentVariables = params.allowedEnvironmentVariables ?? []; this.allowedEnvironmentVariables = params.allowedEnvironmentVariables ?? [];
@@ -894,6 +899,14 @@ export class Config {
return this.terminalBackground; return this.terminalBackground;
} }
getRemoteAdminSettings(): GeminiCodeAssistSetting | undefined {
return this.remoteAdminSettings;
}
setRemoteAdminSettings(settings: GeminiCodeAssistSetting): void {
this.remoteAdminSettings = settings;
}
shouldLoadMemoryFromIncludeDirectories(): boolean { shouldLoadMemoryFromIncludeDirectories(): boolean {
return this.loadMemoryFromIncludeDirectories; return this.loadMemoryFromIncludeDirectories;
} }
@@ -1125,6 +1138,10 @@ export class Config {
return this.mcpServers; return this.mcpServers;
} }
getMcpEnabled(): boolean {
return this.mcpEnabled;
}
getMcpClientManager(): McpClientManager | undefined { getMcpClientManager(): McpClientManager | undefined {
return this.mcpClientManager; return this.mcpClientManager;
} }
+51
View File
@@ -1608,6 +1608,57 @@
"type": "array", "type": "array",
"items": {} "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": { "$defs": {