diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 2e2ecbd87f..7ca8d2934d 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -637,6 +637,7 @@ export async function loadCliConfig( const ptyInfo = await getPty(); const mcpEnabled = settings.admin?.mcp?.enabled ?? true; + const extensionsEnabled = settings.admin?.extensions?.enabled ?? true; return new Config({ sessionId, @@ -659,6 +660,7 @@ export async function loadCliConfig( mcpServerCommand: mcpEnabled ? settings.mcp?.serverCommand : undefined, mcpServers: mcpEnabled ? settings.mcpServers : {}, mcpEnabled, + extensionsEnabled, allowedMcpServers: mcpEnabled ? (argv.allowedMcpServerNames ?? settings.mcp?.allowed) : undefined, diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index 3c4ed226c8..998b91529c 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -465,6 +465,12 @@ Would you like to attempt to install via "git clone" instead?`, if (this.loadedExtensions) { throw new Error('Extensions already loaded, only load extensions once.'); } + + if (this.settings.admin?.extensions?.enabled === false) { + this.loadedExtensions = []; + return this.loadedExtensions; + } + const extensionsDir = ExtensionStorage.getUserExtensionsDir(); this.loadedExtensions = []; if (!fs.existsSync(extensionsDir)) { @@ -537,12 +543,16 @@ Would you like to attempt to install via "git clone" instead?`, } if (config.mcpServers) { - config.mcpServers = Object.fromEntries( - Object.entries(config.mcpServers).map(([key, value]) => [ - key, - filterMcpConfig(value), - ]), - ); + if (this.settings.admin?.mcp?.enabled === false) { + config.mcpServers = undefined; + } else { + config.mcpServers = Object.fromEntries( + Object.entries(config.mcpServers).map(([key, value]) => [ + key, + filterMcpConfig(value), + ]), + ); + } } const contextFiles = getContextFileNames(config) diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 0bfa7a0358..1807144e82 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -632,6 +632,79 @@ describe('extension tests', () => { expect(extension).toBeUndefined(); }); + it('should not load any extensions if admin.extensions.enabled is false', async () => { + createExtension({ + extensionsDir: userExtensionsDir, + name: 'test-extension', + version: '1.0.0', + }); + const loadedSettings = loadSettings(tempWorkspaceDir).merged; + (loadedSettings.admin ??= {}).extensions ??= {}; + loadedSettings.admin.extensions.enabled = false; + + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings: loadedSettings, + }); + + const extensions = await extensionManager.loadExtensions(); + expect(extensions).toEqual([]); + }); + + it('should not load mcpServers if admin.mcp.enabled is false', async () => { + createExtension({ + extensionsDir: userExtensionsDir, + name: 'test-extension', + version: '1.0.0', + mcpServers: { + 'test-server': { command: 'echo', args: ['hello'] }, + }, + }); + const loadedSettings = loadSettings(tempWorkspaceDir).merged; + (loadedSettings.admin ??= {}).mcp ??= {}; + loadedSettings.admin.mcp.enabled = false; + + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings: loadedSettings, + }); + + const extensions = await extensionManager.loadExtensions(); + expect(extensions).toHaveLength(1); + expect(extensions[0].mcpServers).toBeUndefined(); + }); + + it('should load mcpServers if admin.mcp.enabled is true', async () => { + createExtension({ + extensionsDir: userExtensionsDir, + name: 'test-extension', + version: '1.0.0', + mcpServers: { + 'test-server': { command: 'echo', args: ['hello'] }, + }, + }); + const loadedSettings = loadSettings(tempWorkspaceDir).merged; + (loadedSettings.admin ??= {}).mcp ??= {}; + loadedSettings.admin.mcp.enabled = true; + + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings: loadedSettings, + }); + + const extensions = await extensionManager.loadExtensions(); + expect(extensions).toHaveLength(1); + expect(extensions[0].mcpServers).toEqual({ + 'test-server': { command: 'echo', args: ['hello'] }, + }); + }); + describe('id generation', () => { it.each([ { diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index 6bebf0b06e..22b7a47ffc 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -102,6 +102,7 @@ describe('BuiltinCommandLoader', () => { getEnableExtensionReloading: () => false, getEnableHooks: () => false, getEnableHooksUI: () => false, + getExtensionsEnabled: vi.fn().mockReturnValue(true), isSkillsSupportEnabled: vi.fn().mockReturnValue(false), getMcpEnabled: vi.fn().mockReturnValue(true), getSkillManager: vi.fn().mockReturnValue({ @@ -201,6 +202,7 @@ describe('BuiltinCommandLoader profile', () => { getEnableExtensionReloading: () => false, getEnableHooks: () => false, getEnableHooksUI: () => false, + getExtensionsEnabled: vi.fn().mockReturnValue(true), isSkillsSupportEnabled: vi.fn().mockReturnValue(false), getMcpEnabled: vi.fn().mockReturnValue(true), getSkillManager: vi.fn().mockReturnValue({ diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index ea72ecdb05..4320217220 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -76,7 +76,24 @@ export class BuiltinCommandLoader implements ICommandLoader { docsCommand, directoryCommand, editorCommand, - extensionsCommand(this.config?.getEnableExtensionReloading()), + ...(this.config?.getExtensionsEnabled() === false + ? [ + { + name: 'extensions', + description: 'Manage extensions', + kind: CommandKind.BUILT_IN, + autoExecute: false, + subCommands: [], + action: async ( + _context: CommandContext, + ): Promise => ({ + type: 'message', + messageType: 'error', + content: 'Extensions are disabled by your admin.', + }), + }, + ] + : [extensionsCommand(this.config?.getEnableExtensionReloading())]), helpCommand, ...(this.config?.getEnableHooksUI() ? [hooksCommand] : []), await ideCommand(), @@ -95,7 +112,7 @@ export class BuiltinCommandLoader implements ICommandLoader { ): Promise => ({ type: 'message', messageType: 'error', - content: 'MCP disabled by your admin.', + content: 'MCP is disabled by your admin.', }), }, ] diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 8e7a8e42cb..01615c1081 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -357,6 +357,7 @@ export interface ConfigParameters { experimentalJitContext?: boolean; onModelChange?: (model: string) => void; mcpEnabled?: boolean; + extensionsEnabled?: boolean; onReload?: () => Promise<{ disabledSkills?: string[] }>; } @@ -391,6 +392,7 @@ export class Config { private readonly toolCallCommand: string | undefined; private readonly mcpServerCommand: string | undefined; private readonly mcpEnabled: boolean; + private readonly extensionsEnabled: boolean; private mcpServers: Record | undefined; private userMemory: string; private geminiMdFileCount: number; @@ -517,6 +519,7 @@ export class Config { this.mcpServerCommand = params.mcpServerCommand; this.mcpServers = params.mcpServers; this.mcpEnabled = params.mcpEnabled ?? true; + this.extensionsEnabled = params.extensionsEnabled ?? true; this.allowedMcpServers = params.allowedMcpServers ?? []; this.blockedMcpServers = params.blockedMcpServers ?? []; this.allowedEnvironmentVariables = params.allowedEnvironmentVariables ?? []; @@ -1143,6 +1146,10 @@ export class Config { return this.mcpEnabled; } + getExtensionsEnabled(): boolean { + return this.extensionsEnabled; + } + getMcpClientManager(): McpClientManager | undefined { return this.mcpClientManager; }