feat(admin): add support for MCP configuration via admin controls (pt1) (#18223)

This commit is contained in:
Shreya Keshive
2026-02-03 16:19:14 -05:00
committed by GitHub
parent 53027af94c
commit 1fc59484b1
10 changed files with 407 additions and 201 deletions

View File

@@ -2216,8 +2216,11 @@ describe('Settings Loading and Merging', () => {
// 2. Now, set remote admin settings.
loadedSettings.setRemoteAdminSettings({
strictModeDisabled: false,
mcpSetting: { mcpEnabled: false },
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
mcpSetting: { mcpEnabled: false, mcpConfig: {} },
cliFeatureSetting: {
extensionsSetting: { extensionsEnabled: false },
unmanagedCapabilitiesEnabled: false,
},
});
// 3. Verify that remote admin settings take precedence.
@@ -2257,8 +2260,11 @@ describe('Settings Loading and Merging', () => {
const newRemoteSettings = {
strictModeDisabled: false,
mcpSetting: { mcpEnabled: false },
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
mcpSetting: { mcpEnabled: false, mcpConfig: {} },
cliFeatureSetting: {
extensionsSetting: { extensionsEnabled: false },
unmanagedCapabilitiesEnabled: false,
},
};
loadedSettings.setRemoteAdminSettings(newRemoteSettings);
@@ -2269,13 +2275,6 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
// Non-admin settings should remain untouched
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
// Verify that calling setRemoteAdminSettings with partial data overwrites previous remote settings
// and missing properties revert to schema defaults.
loadedSettings.setRemoteAdminSettings({ strictModeDisabled: true });
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false); // Defaulting to false if missing
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false); // Defaulting to false if missing
});
it('should correctly handle undefined remote admin settings', () => {
@@ -2307,84 +2306,6 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
});
it('should correctly handle missing properties in remote admin settings', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const systemSettingsContent = {
admin: {
secureModeEnabled: true,
},
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === getSystemSettingsPath()) {
return JSON.stringify(systemSettingsContent);
}
return '{}';
},
);
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
// Ensure initial state from defaults (as file-based admin settings are ignored)
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
// Set remote settings with only strictModeDisabled (false -> secureModeEnabled: true)
loadedSettings.setRemoteAdminSettings({
strictModeDisabled: false,
});
// Verify secureModeEnabled is updated, others default to false
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
// Set remote settings with only mcpSetting.mcpEnabled
loadedSettings.setRemoteAdminSettings({
mcpSetting: { mcpEnabled: false },
});
// Verify mcpEnabled is updated, others remain defaults (secureModeEnabled defaults to true if strictModeDisabled is missing)
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
// Set remote settings with only cliFeatureSetting.extensionsSetting.extensionsEnabled
loadedSettings.setRemoteAdminSettings({
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
});
// Verify extensionsEnabled is updated, others remain defaults
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
// Verify that missing strictModeDisabled falls back to secureModeEnabled
loadedSettings.setRemoteAdminSettings({
secureModeEnabled: false,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
loadedSettings.setRemoteAdminSettings({
secureModeEnabled: true,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
// Verify strictModeDisabled takes precedence over secureModeEnabled
loadedSettings.setRemoteAdminSettings({
strictModeDisabled: false,
secureModeEnabled: false,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
loadedSettings.setRemoteAdminSettings({
strictModeDisabled: true,
secureModeEnabled: true,
});
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
});
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
const loadedSettings = loadSettings();
loadedSettings.setRemoteAdminSettings({
@@ -2402,51 +2323,6 @@ describe('Settings Loading and Merging', () => {
expect(loadedSettings.merged.admin.skills?.enabled).toBe(false);
});
it('should default mcp.enabled to false if mcpSetting is present but mcpEnabled is undefined', () => {
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
loadedSettings.setRemoteAdminSettings({
mcpSetting: {},
});
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
});
it('should default extensions.enabled to false if extensionsSetting is present but extensionsEnabled is undefined', () => {
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
loadedSettings.setRemoteAdminSettings({
cliFeatureSetting: {
extensionsSetting: {},
},
});
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
});
it('should force secureModeEnabled to true if undefined, overriding schema defaults', () => {
// Mock schema to have secureModeEnabled default to false to verify the override
const originalSchema = getSettingsSchema();
const modifiedSchema = JSON.parse(JSON.stringify(originalSchema));
if (modifiedSchema.admin?.properties?.secureModeEnabled) {
modifiedSchema.admin.properties.secureModeEnabled.default = false;
}
vi.mocked(getSettingsSchema).mockReturnValue(modifiedSchema);
try {
(mockFsExistsSync as Mock).mockReturnValue(true);
(fs.readFileSync as Mock).mockImplementation(() => '{}');
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
// Pass a non-empty object that doesn't have strictModeDisabled
loadedSettings.setRemoteAdminSettings({
mcpSetting: {},
});
// It should be forced to true by the logic (default secure), overriding the mock default of false
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
} finally {
vi.mocked(getSettingsSchema).mockReturnValue(originalSchema);
}
});
it('should handle completely empty remote admin settings response', () => {
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);

View File

@@ -16,7 +16,7 @@ import {
Storage,
coreEvents,
homedir,
type FetchAdminControlsResponse,
type AdminControlsSettings,
} from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/default-light.js';
@@ -348,14 +348,10 @@ export class LoadedSettings {
coreEvents.emitSettingsChanged();
}
setRemoteAdminSettings(remoteSettings: FetchAdminControlsResponse): void {
setRemoteAdminSettings(remoteSettings: AdminControlsSettings): void {
const admin: Settings['admin'] = {};
const {
secureModeEnabled,
strictModeDisabled,
mcpSetting,
cliFeatureSetting,
} = remoteSettings;
const { strictModeDisabled, mcpSetting, cliFeatureSetting } =
remoteSettings;
if (Object.keys(remoteSettings).length === 0) {
this._remoteAdminSettings = { admin };
@@ -363,19 +359,13 @@ export class LoadedSettings {
return;
}
if (strictModeDisabled !== undefined) {
admin.secureModeEnabled = !strictModeDisabled;
} else if (secureModeEnabled !== undefined) {
admin.secureModeEnabled = secureModeEnabled;
} else {
admin.secureModeEnabled = true;
}
admin.mcp = { enabled: mcpSetting?.mcpEnabled ?? false };
admin.secureModeEnabled = !strictModeDisabled;
admin.mcp = { enabled: mcpSetting?.mcpEnabled };
admin.extensions = {
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled ?? false,
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled,
};
admin.skills = {
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled ?? false,
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled,
};
this._remoteAdminSettings = { admin };

View File

@@ -67,7 +67,7 @@ import {
getVersion,
ValidationCancelledError,
ValidationRequiredError,
type FetchAdminControlsResponse,
type AdminControlsSettings,
} from '@google/gemini-cli-core';
import {
initializeApp,
@@ -809,13 +809,13 @@ export function initializeOutputListenersAndFlush() {
}
function setupAdminControlsListener() {
let pendingSettings: FetchAdminControlsResponse | undefined;
let pendingSettings: AdminControlsSettings | undefined;
let config: Config | undefined;
const messageHandler = (msg: unknown) => {
const message = msg as {
type?: string;
settings?: FetchAdminControlsResponse;
settings?: AdminControlsSettings;
};
if (message?.type === 'admin-settings' && message.settings) {
if (config) {

View File

@@ -8,7 +8,7 @@ import { spawn } from 'node:child_process';
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
import {
writeToStderr,
type FetchAdminControlsResponse,
type AdminControlsSettings,
} from '@google/gemini-cli-core';
export async function relaunchOnExitCode(runner: () => Promise<number>) {
@@ -34,7 +34,7 @@ export async function relaunchOnExitCode(runner: () => Promise<number>) {
export async function relaunchAppInChildProcess(
additionalNodeArgs: string[],
additionalScriptArgs: string[],
remoteAdminSettings?: FetchAdminControlsResponse,
remoteAdminSettings?: AdminControlsSettings,
) {
if (process.env['GEMINI_CLI_NO_RELAUNCH']) {
return;
@@ -71,7 +71,7 @@ export async function relaunchAppInChildProcess(
child.on('message', (msg: { type?: string; settings?: unknown }) => {
if (msg.type === 'admin-settings-update' && msg.settings) {
latestAdminSettings = msg.settings as FetchAdminControlsResponse;
latestAdminSettings = msg.settings as AdminControlsSettings;
}
});