mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 19:44:30 -07:00
feat(admin): Introduce remote admin settings & implement secureModeEnabled/mcpEnabled (#15935)
This commit is contained in:
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] : []),
|
||||
|
||||
Reference in New Issue
Block a user