diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index e59bda2131..ee91582d6e 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -116,7 +116,10 @@ describe('extension tests', () => { fs.mkdirSync(userExtensionsDir, { recursive: true }); vi.mocked(os.homedir).mockReturnValue(tempHomeDir); - vi.mocked(isWorkspaceTrusted).mockReturnValue(true); + vi.mocked(isWorkspaceTrusted).mockReturnValue({ + isTrusted: true, + source: undefined, + }); vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir); vi.mocked(execSync).mockClear(); Object.values(mockGit).forEach((fn) => fn.mockReset()); @@ -285,8 +288,8 @@ describe('extension tests', () => { }); it('should resolve environment variables in extension configuration', () => { - process.env.TEST_API_KEY = 'test-api-key-123'; - process.env.TEST_DB_URL = 'postgresql://localhost:5432/testdb'; + process.env['TEST_API_KEY'] = 'test-api-key-123'; + process.env['TEST_DB_URL'] = 'postgresql://localhost:5432/testdb'; try { const userExtensionsDir = path.join( @@ -331,14 +334,14 @@ describe('extension tests', () => { const serverConfig = extension.mcpServers?.['test-server']; expect(serverConfig).toBeDefined(); expect(serverConfig?.env).toBeDefined(); - expect(serverConfig?.env?.API_KEY).toBe('test-api-key-123'); - expect(serverConfig?.env?.DATABASE_URL).toBe( + expect(serverConfig?.env?.['API_KEY']).toBe('test-api-key-123'); + expect(serverConfig?.env?.['DATABASE_URL']).toBe( 'postgresql://localhost:5432/testdb', ); - expect(serverConfig?.env?.STATIC_VALUE).toBe('no-substitution'); + expect(serverConfig?.env?.['STATIC_VALUE']).toBe('no-substitution'); } finally { - delete process.env.TEST_API_KEY; - delete process.env.TEST_DB_URL; + delete process.env['TEST_API_KEY']; + delete process.env['TEST_DB_URL']; } }); @@ -380,8 +383,8 @@ describe('extension tests', () => { const extension = extensions[0]; const serverConfig = extension.mcpServers!['test-server']; expect(serverConfig.env).toBeDefined(); - expect(serverConfig.env!.MISSING_VAR).toBe('$UNDEFINED_ENV_VAR'); - expect(serverConfig.env!.MISSING_VAR_BRACES).toBe('${ALSO_UNDEFINED}'); + expect(serverConfig.env!['MISSING_VAR']).toBe('$UNDEFINED_ENV_VAR'); + expect(serverConfig.env!['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}'); }); it('should skip extensions with invalid JSON and log a warning', () => { @@ -1015,7 +1018,7 @@ This extension will run the following MCP servers: await expect( installExtension( { source: sourceExtDir, type: 'local' }, - async () => true, + async (_) => true, ), ).rejects.toThrow('Invalid extension name: "bad_name"'); }); @@ -1134,25 +1137,34 @@ This extension will run the following MCP servers: describe('folder trust', () => { it('refuses to install extensions from untrusted folders', async () => { - vi.mocked(isWorkspaceTrusted).mockReturnValue(false); + vi.mocked(isWorkspaceTrusted).mockReturnValue({ + isTrusted: false, + source: undefined, + }); const ext1Path = createExtension({ extensionsDir: workspaceExtensionsDir, name: 'ext1', version: '1.0.0', }); - const failed = await performWorkspaceExtensionMigration([ - loadExtension({ - extensionDir: ext1Path, - workspaceDir: tempWorkspaceDir, - })!, - ]); + const failed = await performWorkspaceExtensionMigration( + [ + loadExtension({ + extensionDir: ext1Path, + workspaceDir: tempWorkspaceDir, + })!, + ], + async () => true, + ); expect(failed).toEqual(['ext1']); }); it('does not copy extensions to the user dir', async () => { - vi.mocked(isWorkspaceTrusted).mockReturnValue(false); + vi.mocked(isWorkspaceTrusted).mockReturnValue({ + isTrusted: false, + source: undefined, + }); const ext1Path = createExtension({ extensionsDir: workspaceExtensionsDir, name: 'ext1', @@ -1178,7 +1190,10 @@ This extension will run the following MCP servers: }); it('does not load any extensions in the workspace config', async () => { - vi.mocked(isWorkspaceTrusted).mockReturnValue(false); + vi.mocked(isWorkspaceTrusted).mockReturnValue({ + isTrusted: false, + source: undefined, + }); const ext1Path = createExtension({ extensionsDir: workspaceExtensionsDir, name: 'ext1', diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 6aced07430..5230036582 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -27,7 +27,10 @@ import * as path from 'node:path'; import * as os from 'node:os'; import { SettingScope, loadSettings } from '../config/settings.js'; import { getErrorMessage } from '../utils/errors.js'; -import { recursivelyHydrateStrings } from './extensions/variables.js'; +import { + recursivelyHydrateStrings, + type JsonObject, +} from './extensions/variables.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { resolveEnvVarsInObject } from '../utils/envVarResolver.js'; import { randomUUID } from 'node:crypto'; @@ -156,7 +159,7 @@ export function loadExtensions( const allExtensions = [...loadUserExtensions()]; if ( - (isWorkspaceTrusted(settings) ?? true) && + isWorkspaceTrusted(settings).isTrusted && // Default management setting to true !(settings.experimental?.extensionManagement ?? true) ) { @@ -435,7 +438,7 @@ export async function installExtension( try { const settings = loadSettings(cwd).merged; - if (!isWorkspaceTrusted(settings)) { + if (!isWorkspaceTrusted(settings).isTrusted) { throw new Error( `Could not install extension from untrusted folder at ${installMetadata.source}`, ); @@ -646,17 +649,23 @@ export function loadExtensionConfig( } try { const configContent = fs.readFileSync(configFilePath, 'utf-8'); - const config = recursivelyHydrateStrings(JSON.parse(configContent), { - extensionPath: extensionDir, - workspacePath: workspaceDir, - '/': path.sep, - pathSeparator: path.sep, - }) as unknown as ExtensionConfig; - if (!config.name || !config.version) { + const rawConfig = JSON.parse(configContent) as ExtensionConfig; + if (!rawConfig.name || !rawConfig.version) { throw new Error( - `Invalid configuration in ${configFilePath}: missing ${!config.name ? '"name"' : '"version"'}`, + `Invalid configuration in ${configFilePath}: missing ${!rawConfig.name ? '"name"' : '"version"'}`, ); } + const installDir = new ExtensionStorage(rawConfig.name).getExtensionDir(); + const config = recursivelyHydrateStrings( + rawConfig as unknown as JsonObject, + { + extensionPath: installDir, + workspacePath: workspaceDir, + '/': path.sep, + pathSeparator: path.sep, + }, + ) as unknown as ExtensionConfig; + validateName(config.name); return config; } catch (e) {