diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index aa00fbe9f2..2c9bdcee51 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..d4b15d760b 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -632,6 +632,77 @@ 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); + loadedSettings.setValue( + SettingScope.System, + 'admin.extensions.enabled', + false, + ); + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings: loadedSettings.merged, + }); + + 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); + loadedSettings.setValue(SettingScope.System, 'admin.mcp.enabled', false); + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings: loadedSettings.merged, + }); + + 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); + loadedSettings.setValue(SettingScope.System, 'admin.mcp.enabled', true); + extensionManager = new ExtensionManager({ + workspaceDir: tempWorkspaceDir, + requestConsent: mockRequestConsent, + requestSetting: mockPromptForSettings, + settings: loadedSettings.merged, + }); + + 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 b99d58239e..af6c4176ec 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -101,6 +101,7 @@ describe('BuiltinCommandLoader', () => { getFolderTrust: vi.fn().mockReturnValue(true), getEnableExtensionReloading: () => false, getEnableHooks: () => false, + getExtensionsEnabled: vi.fn().mockReturnValue(true), isSkillsSupportEnabled: vi.fn().mockReturnValue(false), getMcpEnabled: vi.fn().mockReturnValue(true), getSkillManager: vi.fn().mockReturnValue({ @@ -199,6 +200,7 @@ describe('BuiltinCommandLoader profile', () => { getCheckpointingEnabled: () => false, getEnableExtensionReloading: () => false, getEnableHooks: () => 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 31395c0172..aef44e6210 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?.getEnableHooks() ? [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 10c06950b8..9314a3d0b3 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -356,6 +356,7 @@ export interface ConfigParameters { experimentalJitContext?: boolean; onModelChange?: (model: string) => void; mcpEnabled?: boolean; + extensionsEnabled?: boolean; onReload?: () => Promise<{ disabledSkills?: string[] }>; } @@ -390,6 +391,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; @@ -515,6 +517,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 ?? []; @@ -1140,6 +1143,10 @@ export class Config { return this.mcpEnabled; } + getExtensionsEnabled(): boolean { + return this.extensionsEnabled; + } + getMcpClientManager(): McpClientManager | undefined { return this.mcpClientManager; }