mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
feat(admin): add support for MCP configuration via admin controls (pt1) (#18223)
This commit is contained in:
@@ -123,7 +123,7 @@ describe('loadConfig', () => {
|
|||||||
|
|
||||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||||
|
|
||||||
expect(Config).toHaveBeenCalledWith(
|
expect(Config).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
disableYoloMode: !mockAdminSettings.strictModeDisabled,
|
disableYoloMode: !mockAdminSettings.strictModeDisabled,
|
||||||
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
||||||
@@ -144,11 +144,11 @@ describe('loadConfig', () => {
|
|||||||
|
|
||||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||||
|
|
||||||
expect(Config).toHaveBeenCalledWith(
|
expect(Config).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
disableYoloMode: !false,
|
disableYoloMode: !false,
|
||||||
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
||||||
extensionsEnabled: false,
|
extensionsEnabled: undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -159,7 +159,7 @@ describe('loadConfig', () => {
|
|||||||
|
|
||||||
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
await loadConfig(mockSettings, mockExtensionLoader, taskId);
|
||||||
|
|
||||||
expect(Config).toHaveBeenCalledWith(expect.objectContaining({}));
|
expect(Config).toHaveBeenLastCalledWith(expect.objectContaining({}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch admin controls using the code assist server when available', async () => {
|
it('should fetch admin controls using the code assist server when available', async () => {
|
||||||
@@ -182,11 +182,11 @@ describe('loadConfig', () => {
|
|||||||
mockCodeAssistServer,
|
mockCodeAssistServer,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
expect(Config).toHaveBeenCalledWith(
|
expect(Config).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
disableYoloMode: !mockAdminSettings.strictModeDisabled,
|
disableYoloMode: !mockAdminSettings.strictModeDisabled,
|
||||||
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
mcpEnabled: mockAdminSettings.mcpSetting?.mcpEnabled,
|
||||||
extensionsEnabled: false,
|
extensionsEnabled: undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -157,14 +157,10 @@ export async function loadConfig(
|
|||||||
// If NONE are present, disregard admin settings entirely, and pass the
|
// If NONE are present, disregard admin settings entirely, and pass the
|
||||||
// final config as is.
|
// final config as is.
|
||||||
if (Object.keys(adminSettings).length !== 0) {
|
if (Object.keys(adminSettings).length !== 0) {
|
||||||
finalConfigParams.disableYoloMode = !(
|
finalConfigParams.disableYoloMode = !adminSettings.strictModeDisabled;
|
||||||
adminSettings.strictModeDisabled ?? false
|
finalConfigParams.mcpEnabled = adminSettings.mcpSetting?.mcpEnabled;
|
||||||
);
|
|
||||||
finalConfigParams.mcpEnabled =
|
|
||||||
adminSettings.mcpSetting?.mcpEnabled ?? false;
|
|
||||||
finalConfigParams.extensionsEnabled =
|
finalConfigParams.extensionsEnabled =
|
||||||
adminSettings.cliFeatureSetting?.extensionsSetting?.extensionsEnabled ??
|
adminSettings.cliFeatureSetting?.extensionsSetting?.extensionsEnabled;
|
||||||
false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2216,8 +2216,11 @@ describe('Settings Loading and Merging', () => {
|
|||||||
// 2. Now, set remote admin settings.
|
// 2. Now, set remote admin settings.
|
||||||
loadedSettings.setRemoteAdminSettings({
|
loadedSettings.setRemoteAdminSettings({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
mcpSetting: { mcpEnabled: false },
|
mcpSetting: { mcpEnabled: false, mcpConfig: {} },
|
||||||
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Verify that remote admin settings take precedence.
|
// 3. Verify that remote admin settings take precedence.
|
||||||
@@ -2257,8 +2260,11 @@ describe('Settings Loading and Merging', () => {
|
|||||||
|
|
||||||
const newRemoteSettings = {
|
const newRemoteSettings = {
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
mcpSetting: { mcpEnabled: false },
|
mcpSetting: { mcpEnabled: false, mcpConfig: {} },
|
||||||
cliFeatureSetting: { extensionsSetting: { extensionsEnabled: false } },
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
loadedSettings.setRemoteAdminSettings(newRemoteSettings);
|
loadedSettings.setRemoteAdminSettings(newRemoteSettings);
|
||||||
@@ -2269,13 +2275,6 @@ describe('Settings Loading and Merging', () => {
|
|||||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||||
// Non-admin settings should remain untouched
|
// Non-admin settings should remain untouched
|
||||||
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
|
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', () => {
|
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);
|
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', () => {
|
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
|
||||||
const loadedSettings = loadSettings();
|
const loadedSettings = loadSettings();
|
||||||
loadedSettings.setRemoteAdminSettings({
|
loadedSettings.setRemoteAdminSettings({
|
||||||
@@ -2402,51 +2323,6 @@ describe('Settings Loading and Merging', () => {
|
|||||||
expect(loadedSettings.merged.admin.skills?.enabled).toBe(false);
|
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', () => {
|
it('should handle completely empty remote admin settings response', () => {
|
||||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
Storage,
|
Storage,
|
||||||
coreEvents,
|
coreEvents,
|
||||||
homedir,
|
homedir,
|
||||||
type FetchAdminControlsResponse,
|
type AdminControlsSettings,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import stripJsonComments from 'strip-json-comments';
|
import stripJsonComments from 'strip-json-comments';
|
||||||
import { DefaultLight } from '../ui/themes/default-light.js';
|
import { DefaultLight } from '../ui/themes/default-light.js';
|
||||||
@@ -348,14 +348,10 @@ export class LoadedSettings {
|
|||||||
coreEvents.emitSettingsChanged();
|
coreEvents.emitSettingsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
setRemoteAdminSettings(remoteSettings: FetchAdminControlsResponse): void {
|
setRemoteAdminSettings(remoteSettings: AdminControlsSettings): void {
|
||||||
const admin: Settings['admin'] = {};
|
const admin: Settings['admin'] = {};
|
||||||
const {
|
const { strictModeDisabled, mcpSetting, cliFeatureSetting } =
|
||||||
secureModeEnabled,
|
remoteSettings;
|
||||||
strictModeDisabled,
|
|
||||||
mcpSetting,
|
|
||||||
cliFeatureSetting,
|
|
||||||
} = remoteSettings;
|
|
||||||
|
|
||||||
if (Object.keys(remoteSettings).length === 0) {
|
if (Object.keys(remoteSettings).length === 0) {
|
||||||
this._remoteAdminSettings = { admin };
|
this._remoteAdminSettings = { admin };
|
||||||
@@ -363,19 +359,13 @@ export class LoadedSettings {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strictModeDisabled !== undefined) {
|
admin.secureModeEnabled = !strictModeDisabled;
|
||||||
admin.secureModeEnabled = !strictModeDisabled;
|
admin.mcp = { enabled: mcpSetting?.mcpEnabled };
|
||||||
} else if (secureModeEnabled !== undefined) {
|
|
||||||
admin.secureModeEnabled = secureModeEnabled;
|
|
||||||
} else {
|
|
||||||
admin.secureModeEnabled = true;
|
|
||||||
}
|
|
||||||
admin.mcp = { enabled: mcpSetting?.mcpEnabled ?? false };
|
|
||||||
admin.extensions = {
|
admin.extensions = {
|
||||||
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled ?? false,
|
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled,
|
||||||
};
|
};
|
||||||
admin.skills = {
|
admin.skills = {
|
||||||
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled ?? false,
|
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._remoteAdminSettings = { admin };
|
this._remoteAdminSettings = { admin };
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ import {
|
|||||||
getVersion,
|
getVersion,
|
||||||
ValidationCancelledError,
|
ValidationCancelledError,
|
||||||
ValidationRequiredError,
|
ValidationRequiredError,
|
||||||
type FetchAdminControlsResponse,
|
type AdminControlsSettings,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
initializeApp,
|
initializeApp,
|
||||||
@@ -809,13 +809,13 @@ export function initializeOutputListenersAndFlush() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupAdminControlsListener() {
|
function setupAdminControlsListener() {
|
||||||
let pendingSettings: FetchAdminControlsResponse | undefined;
|
let pendingSettings: AdminControlsSettings | undefined;
|
||||||
let config: Config | undefined;
|
let config: Config | undefined;
|
||||||
|
|
||||||
const messageHandler = (msg: unknown) => {
|
const messageHandler = (msg: unknown) => {
|
||||||
const message = msg as {
|
const message = msg as {
|
||||||
type?: string;
|
type?: string;
|
||||||
settings?: FetchAdminControlsResponse;
|
settings?: AdminControlsSettings;
|
||||||
};
|
};
|
||||||
if (message?.type === 'admin-settings' && message.settings) {
|
if (message?.type === 'admin-settings' && message.settings) {
|
||||||
if (config) {
|
if (config) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { spawn } from 'node:child_process';
|
|||||||
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
|
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
|
||||||
import {
|
import {
|
||||||
writeToStderr,
|
writeToStderr,
|
||||||
type FetchAdminControlsResponse,
|
type AdminControlsSettings,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export async function relaunchOnExitCode(runner: () => Promise<number>) {
|
export async function relaunchOnExitCode(runner: () => Promise<number>) {
|
||||||
@@ -34,7 +34,7 @@ export async function relaunchOnExitCode(runner: () => Promise<number>) {
|
|||||||
export async function relaunchAppInChildProcess(
|
export async function relaunchAppInChildProcess(
|
||||||
additionalNodeArgs: string[],
|
additionalNodeArgs: string[],
|
||||||
additionalScriptArgs: string[],
|
additionalScriptArgs: string[],
|
||||||
remoteAdminSettings?: FetchAdminControlsResponse,
|
remoteAdminSettings?: AdminControlsSettings,
|
||||||
) {
|
) {
|
||||||
if (process.env['GEMINI_CLI_NO_RELAUNCH']) {
|
if (process.env['GEMINI_CLI_NO_RELAUNCH']) {
|
||||||
return;
|
return;
|
||||||
@@ -71,7 +71,7 @@ export async function relaunchAppInChildProcess(
|
|||||||
|
|
||||||
child.on('message', (msg: { type?: string; settings?: unknown }) => {
|
child.on('message', (msg: { type?: string; settings?: unknown }) => {
|
||||||
if (msg.type === 'admin-settings-update' && msg.settings) {
|
if (msg.type === 'admin-settings-update' && msg.settings) {
|
||||||
latestAdminSettings = msg.settings as FetchAdminControlsResponse;
|
latestAdminSettings = msg.settings as AdminControlsSettings;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isDeepStrictEqual } from 'node:util';
|
||||||
import {
|
import {
|
||||||
describe,
|
describe,
|
||||||
it,
|
it,
|
||||||
@@ -23,6 +24,10 @@ import {
|
|||||||
import type { CodeAssistServer } from '../server.js';
|
import type { CodeAssistServer } from '../server.js';
|
||||||
import type { Config } from '../../config/config.js';
|
import type { Config } from '../../config/config.js';
|
||||||
import { getCodeAssistServer } from '../codeAssist.js';
|
import { getCodeAssistServer } from '../codeAssist.js';
|
||||||
|
import type {
|
||||||
|
FetchAdminControlsResponse,
|
||||||
|
AdminControlsSettings,
|
||||||
|
} from '../types.js';
|
||||||
|
|
||||||
vi.mock('../codeAssist.js', () => ({
|
vi.mock('../codeAssist.js', () => ({
|
||||||
getCodeAssistServer: vi.fn(),
|
getCodeAssistServer: vi.fn(),
|
||||||
@@ -50,37 +55,243 @@ describe('Admin Controls', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('sanitizeAdminSettings', () => {
|
describe('sanitizeAdminSettings', () => {
|
||||||
it('should strip unknown fields', () => {
|
it('should strip unknown fields and pass through mcpConfigJson when valid', () => {
|
||||||
|
const mcpConfig = {
|
||||||
|
mcpServers: {
|
||||||
|
'server-1': {
|
||||||
|
url: 'http://example.com',
|
||||||
|
type: 'sse' as const,
|
||||||
|
trust: true,
|
||||||
|
includeTools: ['tool1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const input = {
|
const input = {
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
extraField: 'should be removed',
|
extraField: 'should be removed',
|
||||||
mcpSetting: {
|
mcpSetting: {
|
||||||
mcpEnabled: false,
|
mcpEnabled: true,
|
||||||
|
mcpConfigJson: JSON.stringify(mcpConfig),
|
||||||
unknownMcpField: 'remove me',
|
unknownMcpField: 'remove me',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const result = sanitizeAdminSettings(
|
||||||
|
input as unknown as FetchAdminControlsResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
strictModeDisabled: false,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore mcpConfigJson if it is invalid JSON', () => {
|
||||||
|
const input: FetchAdminControlsResponse = {
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfigJson: '{ invalid json }',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = sanitizeAdminSettings(input);
|
||||||
|
expect(result.mcpSetting).toEqual({
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfig: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore mcpConfigJson if it does not match schema', () => {
|
||||||
|
const invalidConfig = {
|
||||||
|
mcpServers: {
|
||||||
|
'server-1': {
|
||||||
|
url: 123, // should be string
|
||||||
|
type: 'invalid-type', // should be sse or http
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const input: FetchAdminControlsResponse = {
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfigJson: JSON.stringify(invalidConfig),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = sanitizeAdminSettings(input);
|
||||||
|
expect(result.mcpSetting).toEqual({
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfig: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply default values when fields are missing', () => {
|
||||||
|
const input = {};
|
||||||
|
const result = sanitizeAdminSettings(input as FetchAdminControlsResponse);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
strictModeDisabled: false,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: false,
|
||||||
|
mcpConfig: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default mcpEnabled to false if mcpSetting is present but mcpEnabled is undefined', () => {
|
||||||
|
const input = { mcpSetting: {} };
|
||||||
|
const result = sanitizeAdminSettings(input as FetchAdminControlsResponse);
|
||||||
|
expect(result.mcpSetting?.mcpEnabled).toBe(false);
|
||||||
|
expect(result.mcpSetting?.mcpConfig).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default extensionsEnabled to false if extensionsSetting is present but extensionsEnabled is undefined', () => {
|
||||||
|
const input = {
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = sanitizeAdminSettings(input as FetchAdminControlsResponse);
|
||||||
|
expect(
|
||||||
|
result.cliFeatureSetting?.extensionsSetting?.extensionsEnabled,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default unmanagedCapabilitiesEnabled to false if cliFeatureSetting is present but unmanagedCapabilitiesEnabled is undefined', () => {
|
||||||
|
const input = {
|
||||||
|
cliFeatureSetting: {},
|
||||||
|
};
|
||||||
|
const result = sanitizeAdminSettings(input as FetchAdminControlsResponse);
|
||||||
|
expect(result.cliFeatureSetting?.unmanagedCapabilitiesEnabled).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reflect explicit values', () => {
|
||||||
|
const input: FetchAdminControlsResponse = {
|
||||||
|
strictModeDisabled: true,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: true },
|
||||||
|
unmanagedCapabilitiesEnabled: true,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const result = sanitizeAdminSettings(input);
|
const result = sanitizeAdminSettings(input);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: true,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: true },
|
||||||
|
unmanagedCapabilitiesEnabled: true,
|
||||||
|
},
|
||||||
mcpSetting: {
|
mcpSetting: {
|
||||||
mcpEnabled: false,
|
mcpEnabled: true,
|
||||||
|
mcpConfig: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Explicitly check that unknown fields are gone
|
|
||||||
expect((result as Record<string, unknown>)['extraField']).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve valid nested fields', () => {
|
it('should prioritize strictModeDisabled over secureModeEnabled', () => {
|
||||||
const input = {
|
const input: FetchAdminControlsResponse = {
|
||||||
cliFeatureSetting: {
|
strictModeDisabled: true,
|
||||||
extensionsSetting: {
|
secureModeEnabled: true, // Should be ignored because strictModeDisabled takes precedence for backwards compatibility if both exist (though usually they shouldn't)
|
||||||
extensionsEnabled: true,
|
};
|
||||||
|
|
||||||
|
const result = sanitizeAdminSettings(input);
|
||||||
|
expect(result.strictModeDisabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use secureModeEnabled if strictModeDisabled is undefined', () => {
|
||||||
|
const input: FetchAdminControlsResponse = {
|
||||||
|
secureModeEnabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = sanitizeAdminSettings(input);
|
||||||
|
expect(result.strictModeDisabled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDeepStrictEqual verification', () => {
|
||||||
|
it('should consider AdminControlsSettings with different key orders as equal', () => {
|
||||||
|
const settings1: AdminControlsSettings = {
|
||||||
|
strictModeDisabled: false,
|
||||||
|
mcpSetting: { mcpEnabled: true },
|
||||||
|
cliFeatureSetting: { unmanagedCapabilitiesEnabled: true },
|
||||||
|
};
|
||||||
|
const settings2: AdminControlsSettings = {
|
||||||
|
cliFeatureSetting: { unmanagedCapabilitiesEnabled: true },
|
||||||
|
mcpSetting: { mcpEnabled: true },
|
||||||
|
strictModeDisabled: false,
|
||||||
|
};
|
||||||
|
expect(isDeepStrictEqual(settings1, settings2)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should consider nested settings objects with different key orders as equal', () => {
|
||||||
|
const settings1: AdminControlsSettings = {
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfig: {
|
||||||
|
mcpServers: {
|
||||||
|
server1: { url: 'url', type: 'sse' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(sanitizeAdminSettings(input)).toEqual(input);
|
|
||||||
|
// Order swapped in mcpConfig and mcpServers items
|
||||||
|
const settings2: AdminControlsSettings = {
|
||||||
|
mcpSetting: {
|
||||||
|
mcpConfig: {
|
||||||
|
mcpServers: {
|
||||||
|
server1: { type: 'sse', url: 'url' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mcpEnabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(isDeepStrictEqual(settings1, settings2)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should consider arrays in options as order-independent and equal if shuffled after sanitization', () => {
|
||||||
|
const mcpConfig1 = {
|
||||||
|
mcpServers: {
|
||||||
|
server1: { includeTools: ['a', 'b'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mcpConfig2 = {
|
||||||
|
mcpServers: {
|
||||||
|
server1: { includeTools: ['b', 'a'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings1 = sanitizeAdminSettings({
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfigJson: JSON.stringify(mcpConfig1),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const settings2 = sanitizeAdminSettings({
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: true,
|
||||||
|
mcpConfigJson: JSON.stringify(mcpConfig2),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isDeepStrictEqual(settings1, settings2)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,7 +323,14 @@ describe('Admin Controls', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use cachedSettings and start polling if provided', async () => {
|
it('should use cachedSettings and start polling if provided', async () => {
|
||||||
const cachedSettings = { strictModeDisabled: false };
|
const cachedSettings = {
|
||||||
|
strictModeDisabled: false,
|
||||||
|
mcpSetting: { mcpEnabled: false, mcpConfig: {} },
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
const result = await fetchAdminControls(
|
const result = await fetchAdminControls(
|
||||||
mockServer,
|
mockServer,
|
||||||
cachedSettings,
|
cachedSettings,
|
||||||
@@ -153,7 +371,17 @@ describe('Admin Controls', () => {
|
|||||||
true,
|
true,
|
||||||
mockOnSettingsChanged,
|
mockOnSettingsChanged,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(serverResponse);
|
expect(result).toEqual({
|
||||||
|
strictModeDisabled: false,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: false,
|
||||||
|
mcpConfig: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,7 +437,17 @@ describe('Admin Controls', () => {
|
|||||||
true,
|
true,
|
||||||
mockOnSettingsChanged,
|
mockOnSettingsChanged,
|
||||||
);
|
);
|
||||||
expect(result).toEqual({ strictModeDisabled: false });
|
expect(result).toEqual({
|
||||||
|
strictModeDisabled: false,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: false,
|
||||||
|
mcpConfig: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
(result as Record<string, unknown>)['unknownField'],
|
(result as Record<string, unknown>)['unknownField'],
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
@@ -279,7 +517,17 @@ describe('Admin Controls', () => {
|
|||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue(serverResponse);
|
||||||
|
|
||||||
const result = await fetchAdminControlsOnce(mockServer, true);
|
const result = await fetchAdminControlsOnce(mockServer, true);
|
||||||
expect(result).toEqual({ strictModeDisabled: true });
|
expect(result).toEqual({
|
||||||
|
strictModeDisabled: true,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: false,
|
||||||
|
mcpConfig: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -337,6 +585,14 @@ describe('Admin Controls', () => {
|
|||||||
|
|
||||||
expect(mockOnSettingsChanged).toHaveBeenCalledWith({
|
expect(mockOnSettingsChanged).toHaveBeenCalledWith({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: false,
|
||||||
|
mcpConfig: {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,7 +618,6 @@ describe('Admin Controls', () => {
|
|||||||
expect(mockOnSettingsChanged).not.toHaveBeenCalled();
|
expect(mockOnSettingsChanged).not.toHaveBeenCalled();
|
||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(2);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should continue polling after a fetch error', async () => {
|
it('should continue polling after a fetch error', async () => {
|
||||||
// Initial fetch is successful
|
// Initial fetch is successful
|
||||||
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
(mockServer.fetchAdminControls as Mock).mockResolvedValue({
|
||||||
@@ -392,6 +647,14 @@ describe('Admin Controls', () => {
|
|||||||
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(3);
|
expect(mockServer.fetchAdminControls).toHaveBeenCalledTimes(3);
|
||||||
expect(mockOnSettingsChanged).toHaveBeenCalledWith({
|
expect(mockOnSettingsChanged).toHaveBeenCalledWith({
|
||||||
strictModeDisabled: false,
|
strictModeDisabled: false,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
extensionsSetting: { extensionsEnabled: false },
|
||||||
|
unmanagedCapabilitiesEnabled: false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: false,
|
||||||
|
mcpConfig: {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,21 +10,74 @@ import { isDeepStrictEqual } from 'node:util';
|
|||||||
import {
|
import {
|
||||||
type FetchAdminControlsResponse,
|
type FetchAdminControlsResponse,
|
||||||
FetchAdminControlsResponseSchema,
|
FetchAdminControlsResponseSchema,
|
||||||
|
McpConfigDefinitionSchema,
|
||||||
|
type AdminControlsSettings,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { getCodeAssistServer } from '../codeAssist.js';
|
import { getCodeAssistServer } from '../codeAssist.js';
|
||||||
import type { Config } from '../../config/config.js';
|
import type { Config } from '../../config/config.js';
|
||||||
|
|
||||||
let pollingInterval: NodeJS.Timeout | undefined;
|
let pollingInterval: NodeJS.Timeout | undefined;
|
||||||
let currentSettings: FetchAdminControlsResponse | undefined;
|
let currentSettings: AdminControlsSettings | undefined;
|
||||||
|
|
||||||
export function sanitizeAdminSettings(
|
export function sanitizeAdminSettings(
|
||||||
settings: FetchAdminControlsResponse,
|
settings: FetchAdminControlsResponse,
|
||||||
): FetchAdminControlsResponse {
|
): AdminControlsSettings {
|
||||||
const result = FetchAdminControlsResponseSchema.safeParse(settings);
|
const result = FetchAdminControlsResponseSchema.safeParse(settings);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return result.data;
|
const sanitized = result.data;
|
||||||
|
let mcpConfig;
|
||||||
|
|
||||||
|
if (sanitized.mcpSetting?.mcpConfigJson) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(sanitized.mcpSetting.mcpConfigJson);
|
||||||
|
const validationResult = McpConfigDefinitionSchema.safeParse(parsed);
|
||||||
|
|
||||||
|
if (validationResult.success) {
|
||||||
|
mcpConfig = validationResult.data;
|
||||||
|
// Sort include/exclude tools for stable comparison
|
||||||
|
if (mcpConfig.mcpServers) {
|
||||||
|
for (const server of Object.values(mcpConfig.mcpServers)) {
|
||||||
|
if (server.includeTools) {
|
||||||
|
server.includeTools.sort();
|
||||||
|
}
|
||||||
|
if (server.excludeTools) {
|
||||||
|
server.excludeTools.sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore parsing errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply defaults (secureModeEnabled is supported for backward compatibility)
|
||||||
|
let strictModeDisabled = false;
|
||||||
|
if (sanitized.strictModeDisabled !== undefined) {
|
||||||
|
strictModeDisabled = sanitized.strictModeDisabled;
|
||||||
|
} else if (sanitized.secureModeEnabled !== undefined) {
|
||||||
|
strictModeDisabled = !sanitized.secureModeEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
strictModeDisabled,
|
||||||
|
cliFeatureSetting: {
|
||||||
|
...sanitized.cliFeatureSetting,
|
||||||
|
extensionsSetting: {
|
||||||
|
extensionsEnabled:
|
||||||
|
sanitized.cliFeatureSetting?.extensionsSetting?.extensionsEnabled ??
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
unmanagedCapabilitiesEnabled:
|
||||||
|
sanitized.cliFeatureSetting?.unmanagedCapabilitiesEnabled ?? false,
|
||||||
|
},
|
||||||
|
mcpSetting: {
|
||||||
|
mcpEnabled: sanitized.mcpSetting?.mcpEnabled ?? false,
|
||||||
|
mcpConfig: mcpConfig ?? {},
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGaxiosError(error: unknown): error is { status: number } {
|
function isGaxiosError(error: unknown): error is { status: number } {
|
||||||
@@ -48,10 +101,10 @@ function isGaxiosError(error: unknown): error is { status: number } {
|
|||||||
*/
|
*/
|
||||||
export async function fetchAdminControls(
|
export async function fetchAdminControls(
|
||||||
server: CodeAssistServer | undefined,
|
server: CodeAssistServer | undefined,
|
||||||
cachedSettings: FetchAdminControlsResponse | undefined,
|
cachedSettings: AdminControlsSettings | undefined,
|
||||||
adminControlsEnabled: boolean,
|
adminControlsEnabled: boolean,
|
||||||
onSettingsChanged: (settings: FetchAdminControlsResponse) => void,
|
onSettingsChanged: (settings: AdminControlsSettings) => void,
|
||||||
): Promise<FetchAdminControlsResponse> {
|
): Promise<AdminControlsSettings> {
|
||||||
if (!server || !server.projectId || !adminControlsEnabled) {
|
if (!server || !server.projectId || !adminControlsEnabled) {
|
||||||
stopAdminControlsPolling();
|
stopAdminControlsPolling();
|
||||||
currentSettings = undefined;
|
currentSettings = undefined;
|
||||||
@@ -129,7 +182,7 @@ export async function fetchAdminControlsOnce(
|
|||||||
function startAdminControlsPolling(
|
function startAdminControlsPolling(
|
||||||
server: CodeAssistServer,
|
server: CodeAssistServer,
|
||||||
project: string,
|
project: string,
|
||||||
onSettingsChanged: (settings: FetchAdminControlsResponse) => void,
|
onSettingsChanged: (settings: AdminControlsSettings) => void,
|
||||||
) {
|
) {
|
||||||
stopAdminControlsPolling();
|
stopAdminControlsPolling();
|
||||||
|
|
||||||
|
|||||||
@@ -311,11 +311,39 @@ const CliFeatureSettingSchema = z.object({
|
|||||||
unmanagedCapabilitiesEnabled: z.boolean().optional(),
|
unmanagedCapabilitiesEnabled: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const McpServerConfigSchema = z.object({
|
||||||
|
url: z.string().optional(),
|
||||||
|
type: z.enum(['sse', 'http']).optional(),
|
||||||
|
trust: z.boolean().optional(),
|
||||||
|
includeTools: z.array(z.string()).optional(),
|
||||||
|
excludeTools: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const McpConfigDefinitionSchema = z.object({
|
||||||
|
mcpServers: z.record(McpServerConfigSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type McpConfigDefinition = z.infer<typeof McpConfigDefinitionSchema>;
|
||||||
|
|
||||||
const McpSettingSchema = z.object({
|
const McpSettingSchema = z.object({
|
||||||
mcpEnabled: z.boolean().optional(),
|
mcpEnabled: z.boolean().optional(),
|
||||||
overrideMcpConfigJson: z.string().optional(),
|
mcpConfigJson: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Schema for internal application use (parsed mcpConfig)
|
||||||
|
export const AdminControlsSettingsSchema = z.object({
|
||||||
|
strictModeDisabled: z.boolean().optional(),
|
||||||
|
mcpSetting: z
|
||||||
|
.object({
|
||||||
|
mcpEnabled: z.boolean().optional(),
|
||||||
|
mcpConfig: McpConfigDefinitionSchema.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
cliFeatureSetting: CliFeatureSettingSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AdminControlsSettings = z.infer<typeof AdminControlsSettingsSchema>;
|
||||||
|
|
||||||
export const FetchAdminControlsResponseSchema = z.object({
|
export const FetchAdminControlsResponseSchema = z.object({
|
||||||
// TODO: deprecate once backend stops sending this field
|
// TODO: deprecate once backend stops sending this field
|
||||||
secureModeEnabled: z.boolean().optional(),
|
secureModeEnabled: z.boolean().optional(),
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ import { ApprovalMode, 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 { FetchAdminControlsResponse } from '../code_assist/types.js';
|
import type { AdminControlsSettings } 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';
|
||||||
@@ -623,7 +623,7 @@ export class Config {
|
|||||||
private readonly planEnabled: boolean;
|
private readonly planEnabled: boolean;
|
||||||
private contextManager?: ContextManager;
|
private contextManager?: ContextManager;
|
||||||
private terminalBackground: string | undefined = undefined;
|
private terminalBackground: string | undefined = undefined;
|
||||||
private remoteAdminSettings: FetchAdminControlsResponse | undefined;
|
private remoteAdminSettings: AdminControlsSettings | undefined;
|
||||||
private latestApiRequest: GenerateContentParameters | undefined;
|
private latestApiRequest: GenerateContentParameters | undefined;
|
||||||
private lastModeSwitchTime: number = Date.now();
|
private lastModeSwitchTime: number = Date.now();
|
||||||
|
|
||||||
@@ -1025,7 +1025,7 @@ export class Config {
|
|||||||
codeAssistServer,
|
codeAssistServer,
|
||||||
this.getRemoteAdminSettings(),
|
this.getRemoteAdminSettings(),
|
||||||
adminControlsEnabled,
|
adminControlsEnabled,
|
||||||
(newSettings: FetchAdminControlsResponse) => {
|
(newSettings: AdminControlsSettings) => {
|
||||||
this.setRemoteAdminSettings(newSettings);
|
this.setRemoteAdminSettings(newSettings);
|
||||||
coreEvents.emitAdminSettingsChanged();
|
coreEvents.emitAdminSettingsChanged();
|
||||||
},
|
},
|
||||||
@@ -1094,11 +1094,11 @@ export class Config {
|
|||||||
this.latestApiRequest = req;
|
this.latestApiRequest = req;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRemoteAdminSettings(): FetchAdminControlsResponse | undefined {
|
getRemoteAdminSettings(): AdminControlsSettings | undefined {
|
||||||
return this.remoteAdminSettings;
|
return this.remoteAdminSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRemoteAdminSettings(settings: FetchAdminControlsResponse): void {
|
setRemoteAdminSettings(settings: AdminControlsSettings): void {
|
||||||
this.remoteAdminSettings = settings;
|
this.remoteAdminSettings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user