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

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
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`
<!-- SETTINGS-AUTOGEN:END -->
#### `mcpServers`

View File

@@ -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']);
});
});

View File

@@ -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,

View File

@@ -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;

View File

@@ -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([]),
}),

View File

@@ -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<MessageActionReturn> => ({
type: 'message',
messageType: 'error',
content: 'MCP disabled by your admin.',
}),
},
]
: [mcpCommand]),
memoryCommand,
modelCommand,
...(this.config?.getFolderTrust() ? [permissionsCommand] : []),

View File

@@ -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;
}

View File

@@ -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<string, MCPServerConfig> | 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;
}

View File

@@ -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": {