Show final install path in extension consent dialog and fix isWorkspaceTrusted check (#10830)

This commit is contained in:
Jacob MacDonald
2025-10-10 13:40:13 -07:00
committed by GitHub
parent ae48e964f0
commit bf0f61e656
2 changed files with 55 additions and 31 deletions

View File

@@ -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',

View File

@@ -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) {