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
+132 -1
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']);
});
});
+26 -10
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,
+68
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;
@@ -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([]),
}),
@@ -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] : []),