From bea10f45f6787291eb0a1a38a291b58204f19e26 Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Mon, 14 Sep 2026 08:02:44 -0700 Subject: [PATCH 1/2] configuration: stabilize managed Copilot OTel startup Expose the policy layer through the extension host's internal configuration inspection result so built-in extensions can distinguish managed values from defaults. Fall back to the early core Copilot OTel policy owners when extension policy references have not registered yet.\n\nFixes #336102\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../extension/vscode-node/services.ts | 14 ++---- .../src/platform/otel/common/otelConfig.ts | 38 +++++++++++++++ .../otel/common/test/otelConfig.spec.ts | 48 ++++++++++++++++++- .../api/common/extHostConfiguration.ts | 2 + .../test/browser/extHostConfiguration.test.ts | 36 ++++++++++++-- 5 files changed, 122 insertions(+), 16 deletions(-) diff --git a/extensions/copilot/src/extension/extension/vscode-node/services.ts b/extensions/copilot/src/extension/extension/vscode-node/services.ts index 9271573284f753..8852de78c1c737 100644 --- a/extensions/copilot/src/extension/extension/vscode-node/services.ts +++ b/extensions/copilot/src/extension/extension/vscode-node/services.ts @@ -57,7 +57,7 @@ import { IFetcherService } from '../../../platform/networking/common/fetcherServ import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService'; import { ChatWebSocketManager, IChatWebSocketManager } from '../../../platform/networking/node/chatWebSocketManager'; import { FetcherService } from '../../../platform/networking/vscode-node/fetcherServiceImpl'; -import { resolveOTelConfig } from '../../../platform/otel/common/otelConfig'; +import { readOTelPolicyConfig, resolveOTelConfig } from '../../../platform/otel/common/otelConfig'; import { IOTelService } from '../../../platform/otel/common/otelService'; import { InMemoryOTelService } from '../../../platform/otel/node/inMemoryOTelService'; import { IOTelSqliteStore, OTelSqliteStore } from '../../../platform/otel/node/sqlite/otelSqliteStore'; @@ -295,7 +295,7 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio // OTel service — resolve config from env + settings, create appropriate impl const otelSettings = workspace.getConfiguration('github.copilot.chat.otel'); - const policyValue = (key: string): T | undefined => (otelSettings.inspect(key) as { policyValue?: T } | undefined)?.policyValue; + const coreOtelSettings = workspace.getConfiguration('chat.agentHost.otel'); const otelConfig = resolveOTelConfig({ env: process.env, settingEnabled: otelSettings.get('enabled'), @@ -306,18 +306,10 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio settingOutfile: otelSettings.get('outfile') || undefined, settingDbSpanExporter: otelSettings.get('dbSpanExporter.enabled'), settingProtocol: otelSettings.get('protocol') || undefined, - policyEnabled: policyValue('enabled'), - policyExporterType: policyValue<'otlp-grpc' | 'otlp-http' | 'console' | 'file'>('exporterType'), - policyOtlpEndpoint: policyValue('otlpEndpoint'), - policyCaptureContent: policyValue('captureContent'), - policyOutfile: policyValue('outfile'), - policyProtocol: policyValue('protocol'), settingServiceName: otelSettings.get('serviceName') || undefined, - policyServiceName: policyValue('serviceName'), settingResourceAttributes: otelSettings.get>('resourceAttributes'), - policyResourceAttributes: policyValue>('resourceAttributes'), settingHeaders: otelSettings.get>('headers'), - policyHeaders: policyValue>('headers'), + ...readOTelPolicyConfig(otelSettings, coreOtelSettings), extensionVersion: extensionContext.extension.packageJSON.version ?? '0.0.0', sessionId: env.sessionId, }); diff --git a/extensions/copilot/src/platform/otel/common/otelConfig.ts b/extensions/copilot/src/platform/otel/common/otelConfig.ts index cc1a62c108618a..3d07217341e47a 100644 --- a/extensions/copilot/src/platform/otel/common/otelConfig.ts +++ b/extensions/copilot/src/platform/otel/common/otelConfig.ts @@ -118,6 +118,44 @@ export interface OTelConfigInput { vscodeTelemetryLevel?: string; } +interface OTelPolicyConfiguration { + inspect(section: string): unknown; +} + +type OTelPolicyConfig = Pick; + +/** + * Reads extension-owned policy references, falling back to their core policy owners because + * core settings are registered before extension configuration contributes its references. + */ +export function readOTelPolicyConfig(extensionSettings: OTelPolicyConfiguration, coreSettings: OTelPolicyConfiguration): OTelPolicyConfig { + const policyValue = (key: string, coreKey = key): T | undefined => { + const extensionValue = (extensionSettings.inspect(key) as { policyValue?: T } | undefined)?.policyValue; + return extensionValue ?? (coreSettings.inspect(coreKey) as { policyValue?: T } | undefined)?.policyValue; + }; + + return { + policyEnabled: policyValue('enabled'), + policyExporterType: policyValue('exporterType'), + policyOtlpEndpoint: policyValue('otlpEndpoint'), + policyCaptureContent: policyValue('captureContent'), + policyOutfile: policyValue('outfile'), + policyProtocol: policyValue('protocol', 'otlpProtocol'), + policyServiceName: policyValue('serviceName'), + policyResourceAttributes: policyValue>('resourceAttributes'), + policyHeaders: policyValue>('headers'), + }; +} + /** * Resolve OTel configuration with layered precedence: * 1. Enterprise policy values from managed settings (highest) diff --git a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts index 7d40811da9ae8a..fb7acadc30c3aa 100644 --- a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, it } from 'vitest'; -import { resolveOTelConfig, type OTelConfigInput } from '../otelConfig'; +import { readOTelPolicyConfig, resolveOTelConfig, type OTelConfigInput } from '../otelConfig'; function makeInput(overrides: Partial = {}): OTelConfigInput { return { @@ -114,6 +114,52 @@ describe('resolveOTelConfig', () => { }); }); + describe('readOTelPolicyConfig', () => { + + it('falls back to early core policy values when extension policy references are not registered yet', () => { + const coreValues = { + enabled: true, + exporterType: 'otlp-http', + otlpEndpoint: 'https://collector.example.com', + captureContent: true, + outfile: '', + otlpProtocol: 'http/json', + serviceName: 'github-copilot', + resourceAttributes: { deployment: 'managed' }, + headers: { authorization: 'core' }, + }; + const configuration = (values: Record) => ({ + inspect: (key: string) => ({ policyValue: values[key] as T | undefined }), + }); + + expect(readOTelPolicyConfig(configuration({}), configuration(coreValues))).toEqual({ + policyEnabled: true, + policyExporterType: 'otlp-http', + policyOtlpEndpoint: 'https://collector.example.com', + policyCaptureContent: true, + policyOutfile: '', + policyProtocol: 'http/json', + policyServiceName: 'github-copilot', + policyResourceAttributes: { deployment: 'managed' }, + policyHeaders: { authorization: 'core' }, + }); + }); + + it('prefers extension policy values including false and empty values', () => { + const configuration = (values: Record) => ({ + inspect: (key: string) => ({ policyValue: values[key] as T | undefined }), + }); + + expect(readOTelPolicyConfig( + configuration({ enabled: false, outfile: '' }), + configuration({ enabled: true, outfile: 'core.jsonl' }) + )).toMatchObject({ + policyEnabled: false, + policyOutfile: '', + }); + }); + }); + it('merges resource attributes with precedence policy > env > setting', () => { const config = resolveOTelConfig(makeInput({ settingResourceAttributes: { fromSetting: 'setting', shared: 'setting' }, diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index d583f3f65a989f..9fffbdb0176708 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -36,6 +36,7 @@ function lookUp(tree: unknown, key: string) { export type ConfigurationInspect = { key: string; + policyValue?: T; defaultValue?: T; globalLocalValue?: T; globalRemoteValue?: T; @@ -269,6 +270,7 @@ export class ExtHostConfigProvider { return { key, + policyValue: deepClone(config.policy?.value), defaultValue: deepClone(config.policy?.value ?? config.default?.value), globalLocalValue: deepClone(config.userLocal?.value), globalRemoteValue: deepClone(config.userRemote?.value), diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index bb5dc1ad70feb0..c2e7b34eb34d11 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -35,17 +35,19 @@ suite('ExtHostConfiguration', function () { return new ExtHostWorkspace(new TestRPCProtocol(), new class extends mock() { }, new class extends mock() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, new NullLogService(), new class extends mock() { }); } - function createExtHostConfiguration(contents: any = Object.create(null), shape?: MainThreadConfigurationShape) { + function createExtHostConfiguration(contents: any = Object.create(null), shape?: MainThreadConfigurationShape, policyContents?: any) { if (!shape) { shape = new class extends mock() { }; } - return new ExtHostConfigProvider(shape, createExtHostWorkspace(), createConfigurationData(contents), new NullLogService()); + return new ExtHostConfigProvider(shape, createExtHostWorkspace(), createConfigurationData(contents, policyContents), new NullLogService()); } - function createConfigurationData(contents: any): IConfigurationInitData { + function createConfigurationData(contents: any, policyContents?: any): IConfigurationInitData { return { defaults: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), - policy: ConfigurationModel.createEmptyModel(new NullLogService()), + policy: policyContents === undefined + ? ConfigurationModel.createEmptyModel(new NullLogService()) + : new ConfigurationModel(policyContents, [], [], undefined, new NullLogService()), application: ConfigurationModel.createEmptyModel(new NullLogService()), userLocal: new ConfigurationModel(contents, [], [], undefined, new NullLogService()), userRemote: ConfigurationModel.createEmptyModel(new NullLogService()), @@ -66,6 +68,32 @@ suite('ExtHostConfiguration', function () { } }); + test('inspect exposes policy value separately from the effective default value', function () { + const configuration = createExtHostConfiguration( + { setting: { enabled: false } }, + undefined, + { setting: { enabled: true } } + ); + + assert.deepStrictEqual(configuration.getConfiguration('setting').inspect('enabled'), { + key: 'setting.enabled', + policyValue: true, + defaultValue: true, + globalLocalValue: false, + globalRemoteValue: undefined, + globalValue: false, + workspaceValue: undefined, + workspaceFolderValue: undefined, + defaultLanguageValue: undefined, + globalLocalLanguageValue: undefined, + globalRemoteLanguageValue: undefined, + globalLanguageValue: undefined, + workspaceLanguageValue: undefined, + workspaceFolderLanguageValue: undefined, + languageIds: [] + }); + }); + assert.strictEqual(extHostConfig.getConfiguration('search.exclude')['**/node_modules'], true); assert.strictEqual(extHostConfig.getConfiguration('search.exclude').get('**/node_modules'), true); assert.strictEqual(extHostConfig.getConfiguration('search').get('exclude')['**/node_modules'], true); From 8bdc188b1312f425737d78365b906a452e56053c Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Mon, 14 Sep 2026 09:06:47 -0700 Subject: [PATCH 2/2] test: register policy inspection coverage at suite scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/browser/extHostConfiguration.test.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts index c2e7b34eb34d11..3cc420c72e112c 100644 --- a/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts +++ b/src/vs/workbench/api/test/browser/extHostConfiguration.test.ts @@ -68,32 +68,6 @@ suite('ExtHostConfiguration', function () { } }); - test('inspect exposes policy value separately from the effective default value', function () { - const configuration = createExtHostConfiguration( - { setting: { enabled: false } }, - undefined, - { setting: { enabled: true } } - ); - - assert.deepStrictEqual(configuration.getConfiguration('setting').inspect('enabled'), { - key: 'setting.enabled', - policyValue: true, - defaultValue: true, - globalLocalValue: false, - globalRemoteValue: undefined, - globalValue: false, - workspaceValue: undefined, - workspaceFolderValue: undefined, - defaultLanguageValue: undefined, - globalLocalLanguageValue: undefined, - globalRemoteLanguageValue: undefined, - globalLanguageValue: undefined, - workspaceLanguageValue: undefined, - workspaceFolderLanguageValue: undefined, - languageIds: [] - }); - }); - assert.strictEqual(extHostConfig.getConfiguration('search.exclude')['**/node_modules'], true); assert.strictEqual(extHostConfig.getConfiguration('search.exclude').get('**/node_modules'), true); assert.strictEqual(extHostConfig.getConfiguration('search').get('exclude')['**/node_modules'], true); @@ -102,6 +76,32 @@ suite('ExtHostConfiguration', function () { assert.strictEqual(extHostConfig.getConfiguration('search').has('exclude.**/node_modules'), true); }); + test('inspect exposes policy value separately from the effective default value', function () { + const configuration = createExtHostConfiguration( + { setting: { enabled: false } }, + undefined, + { setting: { enabled: true } } + ); + + assert.deepStrictEqual(configuration.getConfiguration('setting').inspect('enabled'), { + key: 'setting.enabled', + policyValue: true, + defaultValue: true, + globalLocalValue: false, + globalRemoteValue: undefined, + globalValue: false, + workspaceValue: undefined, + workspaceFolderValue: undefined, + defaultLanguageValue: undefined, + globalLocalLanguageValue: undefined, + globalRemoteLanguageValue: undefined, + globalLanguageValue: undefined, + workspaceLanguageValue: undefined, + workspaceFolderLanguageValue: undefined, + languageIds: [] + }); + }); + test('has/get', () => { const all = createExtHostConfiguration({