From 1ae83f24a519a2bbd10b567fc4652e71bae6fd9a Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:07:36 -0700 Subject: [PATCH 01/26] docs: add enterprise OTel managed-settings policy plan High-level plan for VS Code enterprise control of Copilot agent-host OTel export via the cross-client telemetry managed-settings schema (matches CLI ManagedTelemetrySettings, copilot-agent-runtime #10735). Covers schema, ownership, precedence, security, delivery channels, suppressions, and touch points. --- .../agent_monitoring_managed_settings.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md new file mode 100644 index 00000000000000..be49277af0e98d --- /dev/null +++ b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md @@ -0,0 +1,95 @@ +# Enterprise OTel Managed-Settings Policy — High-Level Spec (VS Code) + +Goal: let an enterprise centrally control Copilot agent-host OpenTelemetry export via +**Copilot managed settings**, surfaced as VS Code policies. When the enterprise sets a +value it wins over the user's own setting / env vars and is locked in the UI. + +This must match the **cross-client `telemetry` contract** already shipped in the CLI +(copilot-agent-runtime PR #10735, `ManagedTelemetrySettings`) under the managed-settings +governance initiative (github/copilot-agent-runtime#9930). VS Code is the second client of +the same schema — keep keys and semantics identical. + +## Scope — `telemetry` managed-settings schema (canonical, 8 fields) + +Nested `telemetry` block in the managed-settings response: + +- `telemetry.enabled` — enables OTel; users cannot disable export when true. +- `telemetry.endpoint` — OTLP collector endpoint (⇄ `OTEL_EXPORTER_OTLP_ENDPOINT`). +- `telemetry.protocol` — free-form string (⇄ `OTEL_EXPORTER_OTLP_PROTOCOL`); `http/json` / + `http/protobuf` (`grpc` accepted for forward-compat, falls back to default). No enum. +- `telemetry.headers` — auth/routing headers (⇄ `OTEL_EXPORTER_OTLP_HEADERS`). **Secret. Never logged.** +- `telemetry.resourceAttributes` — extra resource attributes (⇄ `OTEL_RESOURCE_ATTRIBUTES`). +- `telemetry.serviceName` — overrides `service.name` (⇄ `OTEL_SERVICE_NAME`). +- `telemetry.captureContent` — capture prompts/responses/tool args. Default false. +- `telemetry.lockCaptureContent` — prevents the user from enabling content capture themselves. + +## Ownership + +- **Owner:** the `chat.agentHost.otel.*` settings carry the `policy:` definitions. +- **Reference:** the `github.copilot.chat.otel.*` (extension) settings point at the same + policies (`policyReference`), so a single managed value applies to **both** setting surfaces. + +## Precedence & enforcement + +Enterprise policy > env vars > user settings > defaults (managed wins over env). + +- `protocol` maps to the agent-host exporter/wire protocol; managed value wins, incl. per-signal env overrides. +- `captureContent`: explicit value wins; otherwise `lockCaptureContent: true` forces it off. +- **Per-key managed-wins for `headers` / `resourceAttributes`**: a managed key overrides the + same key from user env (`OTEL_EXPORTER_OTLP_HEADERS` / `OTEL_RESOURCE_ATTRIBUTES`), but + env-only keys are preserved. `endpoint` is hard-locked. + +## Security (carried from the CLI implementation — must replicate) + +- **Secrets stay out of `process.env`.** `headers`, `resourceAttributes`, `serviceName`, + `endpoint` are passed out-of-band (explicit params), never written to env, so secret + headers don't leak and aren't inherited by spawned subprocesses. +- **Subprocess env sanitization.** Strip `OTEL_*` from env of spawned shells/subprocesses so + users can't override enterprise telemetry config downstream. +- **Logging.** Only header / resource-attribute *names* may be logged, never values. + +## Channels — all three already wired in VS Code (no new infra) + +Server, MDM, and file-based managed settings **all already exist** and converge through one +shared pipeline, so supporting all three for `telemetry` is free once the keys are registered: + +- **Server:** `/copilot_internal/managed_settings` → `adaptManagedSettings()` → `normalizeManagedSettings()` + ([managedSettings.ts](src/vs/workbench/services/accounts/browser/managedSettings.ts), [defaultAccount.ts](src/vs/workbench/services/accounts/browser/defaultAccount.ts)). +- **MDM:** Windows registry `SOFTWARE\Policies\GitHubCopilot` + macOS `com.github.copilot` via + `@vscode/policy-watcher` ([nativeManagedSettingsService.ts](src/vs/platform/policy/node/nativeManagedSettingsService.ts)). +- **File:** `managed-settings.json` on well-known Win/macOS/Linux paths via `IFileService` + ([fileManagedSettingsService.ts](src/vs/platform/policy/common/fileManagedSettingsService.ts)). + +All three feed `AccountPolicyService.getPolicyData()`, which calls `selectManagedSettings()` and +the shared normalizer in [copilotManagedSettings.ts](src/vs/platform/policy/common/copilotManagedSettings.ts). + +**Channel precedence: `server > MDM > file`** (single winner, never merged) — richer than the +CLI's `server ?? mdm`. Main-process wiring is in [main.ts](src/vs/code/electron-main/main.ts). + +## Explicit non-goals / suppressions (consistent with CLI — none of these are in its schema) + +- **outfile** — no policy. When the enterprise sets an endpoint/protocol, local file export + is forced off so data can't be diverted to disk. +- **dbSpanExporter** — **not** under enterprise policy; the user can still enable it. + (NB: the earlier VS Code branch wrongly nested `dbSpanExporter.enabled` under `telemetry` — + remove it to match the CLI contract.) +- **maxAttributeSizeChars** — suppressed for now (may be added later). + +## Touch points (where work lands, no detail) + +1. **Register keys** in [copilotManagedSettings.ts](src/vs/platform/policy/common/copilotManagedSettings.ts): + the `telemetry.*` constants + the nested `telemetry` object in the `STRUCTURED_MANAGED_SETTINGS` + table. Scalars (`enabled`/`endpoint`/`protocol`/`captureContent`/`lockCaptureContent`/`serviceName`) + flatten trivially, but **`headers` and `resourceAttributes` are `Record` maps and + MUST use the structured-key JSON-encode path** (like `enabledPlugins` / `extraKnownMarketplaces`), + not the scalar dot-path flattener. Validate block shape (URL, protocol values, map values). +2. `policy:` definitions on the owning `chat.agentHost.otel.*` settings + policy catalog entries + (`value(policyData)` callbacks + `managedSettings: { telemetry: { type: 'object' } }`). +3. `policyReference` on the `github.copilot.chat.otel.*` extension settings. +4. Plumb policy values into the agent-host launch path + extension OTel config resolver: + apply precedence, keep secrets out of env, do per-key header/attr merge, sanitize `OTEL_*` + from subprocess env. +5. Tests + docs. + +No new channel infrastructure is required — server/MDM/file are already unified; the cost is +one structured-key registration + the per-setting policy blocks. From b2ca1ff9d5a6a8a134839382efc74fc5fc63d301 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:23:01 -0700 Subject: [PATCH 02/26] docs: note policyReference type-match constraint and structured-encode location --- .../docs/monitoring/agent_monitoring_managed_settings.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md index be49277af0e98d..c9c190807ad658 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md @@ -28,6 +28,10 @@ Nested `telemetry` block in the managed-settings response: - **Owner:** the `chat.agentHost.otel.*` settings carry the `policy:` definitions. - **Reference:** the `github.copilot.chat.otel.*` (extension) settings point at the same policies (`policyReference`), so a single managed value applies to **both** setting surfaces. +- **Constraint:** `policyReference` maps one policy to two settings, so the owning and referencing + settings **must have identical types**. Confirmed OK — `chat.agentHost.otel.*` and + `github.copilot.chat.otel.*` already share the same setting types. (`policyReference` is a + newly-introduced pattern; extensible if the type-match rule needs to loosen.) ## Precedence & enforcement @@ -79,7 +83,8 @@ CLI's `server ?? mdm`. Main-process wiring is in [main.ts](src/vs/code/electron- 1. **Register keys** in [copilotManagedSettings.ts](src/vs/platform/policy/common/copilotManagedSettings.ts): the `telemetry.*` constants + the nested `telemetry` object in the `STRUCTURED_MANAGED_SETTINGS` - table. Scalars (`enabled`/`endpoint`/`protocol`/`captureContent`/`lockCaptureContent`/`serviceName`) + table. **All managed-settings deserialization/mapping stays in that table** (one `encode` row per + structured key). Scalars (`enabled`/`endpoint`/`protocol`/`captureContent`/`lockCaptureContent`/`serviceName`) flatten trivially, but **`headers` and `resourceAttributes` are `Record` maps and MUST use the structured-key JSON-encode path** (like `enabledPlugins` / `extraKnownMarketplaces`), not the scalar dot-path flattener. Validate block shape (URL, protocol values, map values). From cb4f2a8bf5c60c476ef6b3c7acc20ebf4c3ada25 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:37:55 -0700 Subject: [PATCH 03/26] feat: add enterprise OTel telemetry managed-settings keys and response shape --- .../policy/common/copilotManagedSettings.ts | 24 +++++++++++++++++++ .../accounts/browser/managedSettings.ts | 7 ++++++ 2 files changed, 31 insertions(+) diff --git a/src/vs/platform/policy/common/copilotManagedSettings.ts b/src/vs/platform/policy/common/copilotManagedSettings.ts index a63c6c10d1639f..1df41c267a50f9 100644 --- a/src/vs/platform/policy/common/copilotManagedSettings.ts +++ b/src/vs/platform/policy/common/copilotManagedSettings.ts @@ -38,6 +38,30 @@ export const COPILOT_STRICT_MARKETPLACES_KEY = 'strictKnownMarketplaces'; /** Managed-settings key for the default chat model (carried as a plain string: `auto`, a model family name, or a full model id). */ export const COPILOT_MODEL_KEY = 'model'; +/** + * Enterprise OTel managed-settings keys. These are the scalar leaves of the canonical + * `telemetry` block from the cross-client managed-settings schema (see the CLI + * `ManagedTelemetrySettings`); they flatten to dot-path bag keys via + * {@link normalizeManagedSettings}, so no {@link STRUCTURED_MANAGED_SETTINGS} entry is needed. + * The `telemetry.headers` / `telemetry.resourceAttributes` map fields are intentionally not + * carried yet — they require a dedicated owner setting plus structured-key handling. + */ + +/** Managed-settings key for enterprise OTel enablement. */ +export const COPILOT_OTEL_ENABLED_KEY = 'telemetry.enabled'; + +/** Managed-settings key for the enterprise OTLP collector endpoint. */ +export const COPILOT_OTEL_ENDPOINT_KEY = 'telemetry.endpoint'; + +/** Managed-settings key for the enterprise OTLP protocol (`http/json`, `http/protobuf`, or `grpc`). */ +export const COPILOT_OTEL_PROTOCOL_KEY = 'telemetry.protocol'; + +/** Managed-settings key for enterprise OTel content capture. */ +export const COPILOT_OTEL_CAPTURE_CONTENT_KEY = 'telemetry.captureContent'; + +/** Managed-settings key that prevents users from enabling OTel content capture themselves. */ +export const COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY = 'telemetry.lockCaptureContent'; + const managedSettingValueCallbacks = new Map ManagedSettingValue | undefined>(); /** diff --git a/src/vs/workbench/services/accounts/browser/managedSettings.ts b/src/vs/workbench/services/accounts/browser/managedSettings.ts index babea8116cdc5a..8dcefc297df98c 100644 --- a/src/vs/workbench/services/accounts/browser/managedSettings.ts +++ b/src/vs/workbench/services/accounts/browser/managedSettings.ts @@ -28,6 +28,13 @@ export interface IManagedSettingsResponse { | { readonly source: 'git'; readonly url: string; readonly ref?: string }; }>; readonly strictKnownMarketplaces?: readonly unknown[]; + readonly telemetry?: { + readonly enabled?: boolean; + readonly endpoint?: string; + readonly protocol?: 'grpc' | 'http/protobuf' | 'http/json'; + readonly captureContent?: boolean; + readonly lockCaptureContent?: boolean; + }; /** Any unknown keys in the response are accepted for forward compatibility. */ readonly [key: string]: unknown; } From afd5338eaa8c5bdad6519b80217c3649fa7fda01 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:37:55 -0700 Subject: [PATCH 04/26] feat: gate agent-host OTel settings with enterprise managed-settings policies --- .../agentHostStarter.config.contribution.ts | 127 ++++++++++++++++++ .../platform/agentHost/common/agentService.ts | 31 +++++ .../electron-main/electronAgentHostStarter.ts | 12 +- .../agentHost/node/nodeAgentHostStarter.ts | 12 +- 4 files changed, 178 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts index 33700df0716b60..9f4dcbf0bcad1f 100644 --- a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from '../../../nls.js'; +import { IPolicyData } from '../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../base/common/policy.js'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; +import { COPILOT_OTEL_CAPTURE_CONTENT_KEY, COPILOT_OTEL_ENABLED_KEY, COPILOT_OTEL_ENDPOINT_KEY, COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY, COPILOT_OTEL_PROTOCOL_KEY, managedSettingValue } from '../../policy/common/copilotManagedSettings.js'; import product from '../../product/common/product.js'; import { Registry } from '../../registry/common/platform.js'; import { @@ -39,6 +41,40 @@ import { // (renderer registration for the settings UI). const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + +// Custom managed-settings resolvers for the enterprise OTel policies. The simple pass-through +// keys use `managedSettingValue(KEY)`; these three combine or transform the managed value: +// - protocol: the schema's OTLP protocol string maps onto the agent-host exporter type. +// - captureContent: explicit boolean wins; otherwise `lockCaptureContent` forces it off. +// - outfile: when the enterprise mandates an OTLP endpoint/protocol, local file export is +// suppressed so spans can't be diverted to disk. +function managedOTelProtocolValue(policyData: IPolicyData): string | undefined { + const protocol = policyData.managedSettings?.[COPILOT_OTEL_PROTOCOL_KEY]; + if (protocol === 'grpc') { + return 'otlp-grpc'; + } + if (protocol === 'http/protobuf' || protocol === 'http/json') { + return 'otlp-http'; + } + return undefined; +} + +function managedOTelCaptureContentValue(policyData: IPolicyData): boolean | undefined { + const captureContent = policyData.managedSettings?.[COPILOT_OTEL_CAPTURE_CONTENT_KEY]; + if (typeof captureContent === 'boolean') { + return captureContent; + } + return policyData.managedSettings?.[COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY] === true ? false : undefined; +} + +function managedOTelOutfileValue(policyData: IPolicyData): string | undefined { + const managedSettings = policyData.managedSettings; + if (managedSettings?.[COPILOT_OTEL_ENDPOINT_KEY] !== undefined || managedSettings?.[COPILOT_OTEL_PROTOCOL_KEY] !== undefined) { + return ''; + } + return undefined; +} + configurationRegistry.registerConfiguration({ id: 'chatAgentHostStarter', title: nls.localize('chatAgentHostStarterConfigurationTitle', "Chat Agent Host Starter"), @@ -118,6 +154,23 @@ configurationRegistry.registerConfiguration({ default: false, scope: ConfigurationScope.APPLICATION, tags: ['experimental', 'advanced'], + // Owns `CopilotOtelEnabled`; the copilot-chat setting `github.copilot.chat.otel.enabled` + // attaches to it via a `policyReference` in the extension's package.json. + policy: { + name: 'CopilotOtelEnabled', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedSettingValue(COPILOT_OTEL_ENABLED_KEY), + managedSettings: { + [COPILOT_OTEL_ENABLED_KEY]: { type: 'boolean' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.enabled.policy', + value: nls.localize('chat.agentHost.otel.enabled.policy', "Controls whether Copilot OpenTelemetry export is enabled. When managed, users cannot override the enterprise value."), + } + }, + }, }, [AgentHostOTelExporterTypeSettingId]: { type: 'string', @@ -126,6 +179,29 @@ configurationRegistry.registerConfiguration({ default: 'otlp-http', scope: ConfigurationScope.APPLICATION, tags: ['experimental', 'advanced'], + // Owns `CopilotOtelProtocol`; the managed `telemetry.protocol` string is mapped onto + // the exporter type (`grpc` -> `otlp-grpc`, `http/*` -> `otlp-http`). + policy: { + name: 'CopilotOtelProtocol', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedOTelProtocolValue, + managedSettings: { + [COPILOT_OTEL_PROTOCOL_KEY]: { type: 'string' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.protocol.policy', + value: nls.localize('chat.agentHost.otel.protocol.policy', "Controls the enterprise-managed OTLP protocol for Copilot OpenTelemetry export."), + }, + enumDescriptions: [ + { key: 'chat.agentHost.otel.protocol.policy.otlpHttp', value: nls.localize('chat.agentHost.otel.protocol.policy.otlpHttp', "Use OTLP over HTTP."), }, + { key: 'chat.agentHost.otel.protocol.policy.otlpGrpc', value: nls.localize('chat.agentHost.otel.protocol.policy.otlpGrpc', "Use OTLP over gRPC."), }, + { key: 'chat.agentHost.otel.protocol.policy.console', value: nls.localize('chat.agentHost.otel.protocol.policy.console', "Console exporter is not selected by enterprise managed settings."), }, + { key: 'chat.agentHost.otel.protocol.policy.file', value: nls.localize('chat.agentHost.otel.protocol.policy.file', "File exporter is not selected by enterprise managed settings."), }, + ], + }, + }, }, [AgentHostOTelOtlpEndpointSettingId]: { type: 'string', @@ -133,6 +209,22 @@ configurationRegistry.registerConfiguration({ default: '', scope: ConfigurationScope.APPLICATION, tags: ['experimental', 'advanced'], + // Owns `CopilotOtelEndpoint`. + policy: { + name: 'CopilotOtelEndpoint', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedSettingValue(COPILOT_OTEL_ENDPOINT_KEY), + managedSettings: { + [COPILOT_OTEL_ENDPOINT_KEY]: { type: 'string' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.otlpEndpoint.policy', + value: nls.localize('chat.agentHost.otel.otlpEndpoint.policy', "Controls the enterprise-managed OTLP collector endpoint for Copilot OpenTelemetry export."), + } + }, + }, }, [AgentHostOTelCaptureContentSettingId]: { type: 'boolean', @@ -140,6 +232,24 @@ configurationRegistry.registerConfiguration({ default: false, scope: ConfigurationScope.APPLICATION, tags: ['experimental', 'advanced'], + // Owns `CopilotOtelCaptureContent`; explicit managed value wins, otherwise + // `telemetry.lockCaptureContent` forces capture off. + policy: { + name: 'CopilotOtelCaptureContent', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedOTelCaptureContentValue, + managedSettings: { + [COPILOT_OTEL_CAPTURE_CONTENT_KEY]: { type: 'boolean' }, + [COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY]: { type: 'boolean' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.captureContent.policy', + value: nls.localize('chat.agentHost.otel.captureContent.policy', "Controls whether Copilot OpenTelemetry export captures prompt, response, and tool content."), + } + }, + }, }, [AgentHostOTelOutfileSettingId]: { type: 'string', @@ -147,6 +257,23 @@ configurationRegistry.registerConfiguration({ default: '', scope: ConfigurationScope.APPLICATION, tags: ['experimental', 'advanced'], + // Owns `CopilotOtelOutfile`; suppresses local file export when the enterprise mandates an OTLP sink. + policy: { + name: 'CopilotOtelOutfile', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedOTelOutfileValue, + managedSettings: { + [COPILOT_OTEL_ENDPOINT_KEY]: { type: 'string' }, + [COPILOT_OTEL_PROTOCOL_KEY]: { type: 'string' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.outfile.policy', + value: nls.localize('chat.agentHost.otel.outfile.policy', "Prevents local file export when enterprise-managed Copilot OpenTelemetry export is configured."), + } + }, + }, }, [AgentHostOTelDbSpanExporterEnabledSettingId]: { type: 'boolean', diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 48e60ab8056fdf..2fb482e041762c 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -345,6 +345,7 @@ export interface IAgentHostOTelSettings { export function buildAgentHostOTelEnv( settings: IAgentHostOTelSettings, inheritedEnv: Readonly>, + policySettings: IAgentHostOTelSettings = {}, ): Record { const out: Record = {}; const setIfMissing = (key: string, value: string | undefined): void => { @@ -353,6 +354,13 @@ export function buildAgentHostOTelEnv( } out[key] = value; }; + // Enterprise policy wins over inherited env (managed settings cannot be overridden by a + // user-set env var), unlike user settings which yield to env via `setIfMissing`. + const setPolicy = (key: string, value: string | undefined): void => { + if (value !== undefined) { + out[key] = value; + } + }; if (settings.enabled) { setIfMissing(AgentHostOTelEnvVars.Enabled, 'true'); } @@ -365,6 +373,29 @@ export function buildAgentHostOTelEnv( if (settings.dbSpanExporterEnabled) { setIfMissing(AgentHostOTelEnvVars.DbSpanExporterEnabled, 'true'); } + + if (policySettings.enabled !== undefined) { + setPolicy(AgentHostOTelEnvVars.Enabled, policySettings.enabled ? 'true' : 'false'); + if (!policySettings.enabled) { + setPolicy(AgentHostOTelEnvVars.OtlpEndpoint, ''); + setPolicy(AgentHostOTelEnvVars.OtlpEndpointAlt, ''); + setPolicy(AgentHostOTelEnvVars.FilePath, ''); + } + } + if (policySettings.exporterType !== undefined) { + setPolicy(AgentHostOTelEnvVars.ExporterType, policySettings.exporterType); + setPolicy(AgentHostOTelEnvVars.FilePath, ''); + } + if (policySettings.otlpEndpoint !== undefined) { + setPolicy(AgentHostOTelEnvVars.OtlpEndpoint, policySettings.otlpEndpoint); + setPolicy(AgentHostOTelEnvVars.FilePath, ''); + } + if (policySettings.outfile !== undefined) { + setPolicy(AgentHostOTelEnvVars.FilePath, policySettings.outfile); + } + if (policySettings.captureContent !== undefined) { + setPolicy(AgentHostOTelEnvVars.CaptureContent, policySettings.captureContent ? 'true' : 'false'); + } return out; } diff --git a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts index 054f96ca48969c..caa0defaea0b98 100644 --- a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts +++ b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts @@ -79,7 +79,9 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt // Translate `chat.agentHost.otel.*` settings into the env vars consumed by // the agent host process. Any value already present on `process.env` wins - // (developer override) — see `buildAgentHostOTelEnv` for the precedence. + // for user settings, while enterprise policy values win over inherited env — + // see `buildAgentHostOTelEnv` for the precedence. + const policyValue = (key: string): T | undefined => this._configurationService.inspect(key).policyValue; const otelEnv = buildAgentHostOTelEnv({ enabled: this._configurationService.getValue(AgentHostOTelEnabledSettingId), exporterType: this._configurationService.getValue(AgentHostOTelExporterTypeSettingId), @@ -87,7 +89,13 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt captureContent: this._configurationService.getValue(AgentHostOTelCaptureContentSettingId), outfile: this._configurationService.getValue(AgentHostOTelOutfileSettingId), dbSpanExporterEnabled: this._configurationService.getValue(AgentHostOTelDbSpanExporterEnabledSettingId), - }, process.env); + }, process.env, { + enabled: policyValue(AgentHostOTelEnabledSettingId), + exporterType: policyValue(AgentHostOTelExporterTypeSettingId), + otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), + captureContent: policyValue(AgentHostOTelCaptureContentSettingId), + outfile: policyValue(AgentHostOTelOutfileSettingId), + }); const args = [ '--logsPath', this._environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, diff --git a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts index 8016d0f293cbc8..39a806f732f75c 100644 --- a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts +++ b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts @@ -88,7 +88,9 @@ export class NodeAgentHostStarter extends Disposable implements IAgentHostStarte // Translate `chat.agentHost.otel.*` settings into the env vars consumed by // the agent host process. Any value already present on `process.env` wins - // (developer override) — see `buildAgentHostOTelEnv`. + // for user settings, while enterprise policy values win over inherited env — + // see `buildAgentHostOTelEnv`. + const policyValue = (key: string): T | undefined => this._configurationService.inspect(key).policyValue; const otelEnv = buildAgentHostOTelEnv({ enabled: this._configurationService.getValue(AgentHostOTelEnabledSettingId), exporterType: this._configurationService.getValue(AgentHostOTelExporterTypeSettingId), @@ -96,7 +98,13 @@ export class NodeAgentHostStarter extends Disposable implements IAgentHostStarte captureContent: this._configurationService.getValue(AgentHostOTelCaptureContentSettingId), outfile: this._configurationService.getValue(AgentHostOTelOutfileSettingId), dbSpanExporterEnabled: this._configurationService.getValue(AgentHostOTelDbSpanExporterEnabledSettingId), - }, process.env); + }, process.env, { + enabled: policyValue(AgentHostOTelEnabledSettingId), + exporterType: policyValue(AgentHostOTelExporterTypeSettingId), + otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), + captureContent: policyValue(AgentHostOTelCaptureContentSettingId), + outfile: policyValue(AgentHostOTelOutfileSettingId), + }); Object.assign(env, otelEnv); // Forward WebSocket server configuration to the child process via env vars From 74333cf4da89490f3d19e575e445842c08d9944b Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:39:43 -0700 Subject: [PATCH 05/26] feat: apply enterprise OTel policy precedence in copilot-chat extension --- extensions/copilot/package.json | 15 ++++ .../extension/vscode-node/services.ts | 6 ++ .../src/platform/otel/common/otelConfig.ts | 71 +++++++++++++------ 3 files changed, 69 insertions(+), 23 deletions(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 71e0bfae0cb6af..c92e5211f0d3d7 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -5273,6 +5273,9 @@ "type": "boolean", "default": false, "scope": "application", + "policyReference": { + "name": "CopilotOtelEnabled" + }, "markdownDescription": "Enable OpenTelemetry trace/metric/log emission for Copilot Chat operations. Configurable in user settings only. Env var `COPILOT_OTEL_ENABLED` takes precedence. Requires window reload.", "tags": [ "advanced" @@ -5288,6 +5291,9 @@ ], "default": "otlp-http", "scope": "application", + "policyReference": { + "name": "CopilotOtelProtocol" + }, "markdownDescription": "OTel exporter type for Copilot Chat telemetry. Configurable in user settings only. Requires window reload.", "tags": [ "advanced" @@ -5297,6 +5303,9 @@ "type": "string", "default": "http://localhost:4318", "scope": "application", + "policyReference": { + "name": "CopilotOtelEndpoint" + }, "markdownDescription": "OTLP collector endpoint URL for Copilot Chat OTel data. Configurable in user settings only. Env var `OTEL_EXPORTER_OTLP_ENDPOINT` takes precedence. Requires window reload.", "tags": [ "advanced" @@ -5306,6 +5315,9 @@ "type": "boolean", "default": false, "scope": "application", + "policyReference": { + "name": "CopilotOtelCaptureContent" + }, "markdownDescription": "Capture input/output messages, system instructions, and tool definitions in OTel telemetry. **Contains potentially sensitive data.** Configurable in user settings only. Env var `COPILOT_OTEL_CAPTURE_CONTENT` takes precedence. Requires window reload.", "tags": [ "advanced" @@ -5325,6 +5337,9 @@ "type": "string", "default": "", "scope": "application", + "policyReference": { + "name": "CopilotOtelOutfile" + }, "markdownDescription": "File path for file-based OTel exporter output (JSON-lines). When set, overrides exporter type to `file`. Configurable in user settings only. Requires window reload.", "tags": [ "advanced" diff --git a/extensions/copilot/src/extension/extension/vscode-node/services.ts b/extensions/copilot/src/extension/extension/vscode-node/services.ts index 6d304454c86d99..8a6d19d1b35699 100644 --- a/extensions/copilot/src/extension/extension/vscode-node/services.ts +++ b/extensions/copilot/src/extension/extension/vscode-node/services.ts @@ -289,6 +289,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 otelConfig = resolveOTelConfig({ env: process.env, settingEnabled: otelSettings.get('enabled'), @@ -298,6 +299,11 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio settingMaxAttributeSizeChars: otelSettings.get('maxAttributeSizeChars'), settingOutfile: otelSettings.get('outfile') || undefined, settingDbSpanExporter: otelSettings.get('dbSpanExporter.enabled'), + policyEnabled: policyValue('enabled'), + policyExporterType: policyValue<'otlp-grpc' | 'otlp-http' | 'console' | 'file'>('exporterType'), + policyOtlpEndpoint: policyValue('otlpEndpoint'), + policyCaptureContent: policyValue('captureContent'), + policyOutfile: policyValue('outfile'), 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 e2aaefe269a496..bb7141e4b2eabd 100644 --- a/extensions/copilot/src/platform/otel/common/otelConfig.ts +++ b/extensions/copilot/src/platform/otel/common/otelConfig.ts @@ -5,7 +5,7 @@ export type OTelExporterType = 'otlp-grpc' | 'otlp-http' | 'console' | 'file'; -export type OTelEnabledVia = 'envVar' | 'setting' | 'otlpEndpointEnvVar' | 'dbSpanExporterOnly' | 'disabled'; +export type OTelEnabledVia = 'policy' | 'envVar' | 'setting' | 'otlpEndpointEnvVar' | 'dbSpanExporterOnly' | 'disabled'; /** Default OTLP endpoint used when no env var or setting overrides it. */ export const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4318'; @@ -87,6 +87,11 @@ export interface OTelConfigInput { settingMaxAttributeSizeChars?: number; settingOutfile?: string; settingDbSpanExporter?: boolean; + policyEnabled?: boolean; + policyExporterType?: OTelExporterType; + policyOtlpEndpoint?: string; + policyCaptureContent?: boolean; + policyOutfile?: string; extensionVersion: string; sessionId: string; vscodeTelemetryLevel?: string; @@ -94,10 +99,11 @@ export interface OTelConfigInput { /** * Resolve OTel configuration with layered precedence: - * 1. COPILOT_OTEL_* env vars (highest) - * 2. OTEL_EXPORTER_OTLP_* standard env vars - * 3. VS Code settings - * 4. Defaults (lowest) + * 1. Enterprise policy values from managed settings (highest) + * 2. COPILOT_OTEL_* env vars + * 3. OTEL_EXPORTER_OTLP_* standard env vars + * 4. VS Code settings + * 5. Defaults (lowest) */ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { const { env } = input; @@ -110,17 +116,20 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { // SQLite DB span exporter: setting > default(false) const dbSpanExporter = input.settingDbSpanExporter ?? false; - // Determine if enabled: env > setting > dbSpanExporter > default(false) + const policyMandatesOtlp = input.policyOtlpEndpoint !== undefined || input.policyExporterType !== undefined; + const policyEndpointEnables = input.policyOtlpEndpoint !== undefined ? true : undefined; + + // Determine if enabled: policy > env > setting > policy endpoint > dbSpanExporter > default(false) // When dbSpanExporter is on, OTel must be enabled for the SDK pipeline to work. - const enabled = (envBool(env['COPILOT_OTEL_ENABLED']) + const enabledSignal = input.policyEnabled + ?? envBool(env['COPILOT_OTEL_ENABLED']) ?? input.settingEnabled - ?? (!!env['OTEL_EXPORTER_OTLP_ENDPOINT'])) - || dbSpanExporter; + ?? policyEndpointEnables + ?? (!!env['OTEL_EXPORTER_OTLP_ENDPOINT']); + const enabled = input.policyEnabled === false ? false : enabledSignal || dbSpanExporter; - // OTel was explicitly enabled if the user/env turned it on, not just dbSpanExporter - const enabledExplicitly = (envBool(env['COPILOT_OTEL_ENABLED']) - ?? input.settingEnabled - ?? (!!env['OTEL_EXPORTER_OTLP_ENDPOINT'])) === true; + // OTel was explicitly enabled if policy/user/env turned it on, not just dbSpanExporter + const enabledExplicitly = enabled && enabledSignal === true; if (!enabled) { return createDisabledConfig(input); @@ -128,7 +137,9 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { // Determine how OTel was enabled for telemetry tracking let enabledVia: OTelEnabledVia; - if (envBool(env['COPILOT_OTEL_ENABLED']) === true) { + if (input.policyEnabled === true || (input.policyEnabled === undefined && input.policyOtlpEndpoint !== undefined)) { + enabledVia = 'policy'; + } else if (envBool(env['COPILOT_OTEL_ENABLED']) === true) { enabledVia = 'envVar'; } else if (input.settingEnabled === true) { enabledVia = 'setting'; @@ -138,23 +149,36 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { enabledVia = 'dbSpanExporterOnly'; } - // Protocol: env > inferred from exporter type > default - const rawProtocol = env['OTEL_EXPORTER_OTLP_PROTOCOL'] ?? env['COPILOT_OTEL_PROTOCOL']; + // Protocol: policy > env > default + const rawProtocol = input.policyExporterType === 'otlp-grpc' + ? 'grpc' + : input.policyExporterType === 'otlp-http' + ? 'http' + : env['OTEL_EXPORTER_OTLP_PROTOCOL'] ?? env['COPILOT_OTEL_PROTOCOL']; const protocol: 'grpc' | 'http' = rawProtocol === 'grpc' ? 'grpc' : 'http'; - // Endpoint: COPILOT_OTEL env > OTEL env > setting > default - const rawEndpoint = env['COPILOT_OTEL_ENDPOINT'] + // Endpoint: policy > COPILOT_OTEL env > OTEL env > setting > default + const rawEndpoint = input.policyOtlpEndpoint + ?? env['COPILOT_OTEL_ENDPOINT'] ?? env['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? input.settingOtlpEndpoint ?? DEFAULT_OTLP_ENDPOINT; const otlpEndpoint = parseOtlpEndpoint(rawEndpoint, protocol) ?? DEFAULT_OTLP_ENDPOINT; - // File exporter path - const fileExporterPath = env['COPILOT_OTEL_FILE_EXPORTER_PATH'] ?? input.settingOutfile; + // File exporter path. Enterprise OTLP policy suppresses file export diversion. + const fileExporterPath = input.policyOutfile !== undefined + ? input.policyOutfile || undefined + : policyMandatesOtlp + ? undefined + : env['COPILOT_OTEL_FILE_EXPORTER_PATH'] ?? input.settingOutfile; // Exporter type let exporterType: OTelExporterType; - if (fileExporterPath) { + if (input.policyExporterType) { + exporterType = input.policyExporterType; + } else if (policyMandatesOtlp) { + exporterType = 'otlp-http'; + } else if (fileExporterPath) { exporterType = 'file'; } else if (input.settingExporterType) { exporterType = input.settingExporterType; @@ -162,8 +186,9 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { exporterType = protocol === 'grpc' ? 'otlp-grpc' : 'otlp-http'; } - // Content capture - const captureContent = envBool(env['COPILOT_OTEL_CAPTURE_CONTENT']) + // Content capture: policy > env > setting > default(false) + const captureContent = input.policyCaptureContent + ?? envBool(env['COPILOT_OTEL_CAPTURE_CONTENT']) ?? input.settingCaptureContent ?? false; From 42a2f6b5b59ce64dd91158482b947fa7ed97f54f Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:41:53 -0700 Subject: [PATCH 06/26] test: cover enterprise OTel policy precedence in resolveOTelConfig --- .../otel/common/test/otelConfig.spec.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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 ff05f5c74b1944..ac082fd08d4b4d 100644 --- a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts @@ -318,4 +318,52 @@ describe('resolveOTelConfig', () => { expect(config.maxAttributeSizeChars).toBe(0); }); }); + + describe('enterprise policy precedence', () => { + it('policy enabled wins over a disabling user setting', () => { + const config = resolveOTelConfig(makeInput({ + settingEnabled: false, + policyEnabled: true, + })); + expect(config.enabled).toBe(true); + expect(config.enabledVia).toBe('policy'); + }); + + it('policy disabled forces OTel off even when env enables it', () => { + const config = resolveOTelConfig(makeInput({ + env: { 'COPILOT_OTEL_ENABLED': 'true' }, + policyEnabled: false, + })); + expect(config.enabled).toBe(false); + }); + + it('policy endpoint wins over env endpoint and enables OTel', () => { + const config = resolveOTelConfig(makeInput({ + env: { 'OTEL_EXPORTER_OTLP_ENDPOINT': 'http://user:4318' }, + policyOtlpEndpoint: 'http://enterprise:4318', + })); + expect(config.enabled).toBe(true); + expect(config.otlpEndpoint).toBe('http://enterprise:4318/'); + expect(config.enabledVia).toBe('policy'); + }); + + it('policy exporter type maps and suppresses file export', () => { + const config = resolveOTelConfig(makeInput({ + settingOutfile: '/tmp/spans.jsonl', + policyEnabled: true, + policyExporterType: 'otlp-http', + })); + expect(config.exporterType).toBe('otlp-http'); + expect(config.fileExporterPath).toBeUndefined(); + }); + + it('policy captureContent overrides the user setting', () => { + const config = resolveOTelConfig(makeInput({ + policyEnabled: true, + settingCaptureContent: true, + policyCaptureContent: false, + })); + expect(config.captureContent).toBe(false); + }); + }); }); From a3930b873ef761c5ba9cca520123f9b8e02eb2cf Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:42:40 -0700 Subject: [PATCH 07/26] build: export OTel managed-settings policies to policy catalog --- build/lib/policies/policyData.jsonc | 138 +++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 12 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index b619ea557fe9b6..b6d52bf752fbe0 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -143,6 +143,24 @@ "default": true, "included": true }, + { + "key": "chat.agentHost.claudeAgent.enabled", + "name": "Claude3PIntegration", + "category": "InteractiveSession", + "minimumVersion": "1.113", + "localization": { + "description": { + "key": "chat.agentHost.claudeAgent.enabled.policy", + "value": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic Claude Agent SDK directly in the editor. Uses your existing Copilot subscription." + } + }, + "type": "boolean", + "default": true, + "included": true, + "referencedSettings": [ + "github.copilot.chat.claudeAgent.enabled" + ] + }, { "key": "chat.agentHost.codexAgent.enabled", "name": "Codex3PIntegration", @@ -159,38 +177,134 @@ "included": true }, { - "key": "chat.defaultModel", - "name": "ChatDefaultModel", + "key": "chat.agentHost.otel.enabled", + "name": "CopilotOtelEnabled", "category": "InteractiveSession", "minimumVersion": "1.127", "localization": { "description": { - "key": "chat.defaultModel.policy", - "value": "Sets the default chat model for new conversations. Accepts \"auto\", a model family name (such as \"opus\" or \"gemini\"), or a full model id. Users can still switch the model within a conversation." + "key": "chat.agentHost.otel.enabled.policy", + "value": "Controls whether Copilot OpenTelemetry export is enabled. When managed, users cannot override the enterprise value." + } + }, + "type": "boolean", + "default": false, + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.enabled" + ] + }, + { + "key": "chat.agentHost.otel.exporterType", + "name": "CopilotOtelProtocol", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.protocol.policy", + "value": "Controls the enterprise-managed OTLP protocol for Copilot OpenTelemetry export." + }, + "enumDescriptions": [ + { + "key": "chat.agentHost.otel.protocol.policy.otlpHttp", + "value": "Use OTLP over HTTP." + }, + { + "key": "chat.agentHost.otel.protocol.policy.otlpGrpc", + "value": "Use OTLP over gRPC." + }, + { + "key": "chat.agentHost.otel.protocol.policy.console", + "value": "Console exporter is not selected by enterprise managed settings." + }, + { + "key": "chat.agentHost.otel.protocol.policy.file", + "value": "File exporter is not selected by enterprise managed settings." + } + ] + }, + "type": "string", + "default": "otlp-http", + "enum": [ + "otlp-http", + "otlp-grpc", + "console", + "file" + ], + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.exporterType" + ] + }, + { + "key": "chat.agentHost.otel.otlpEndpoint", + "name": "CopilotOtelEndpoint", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.otlpEndpoint.policy", + "value": "Controls the enterprise-managed OTLP collector endpoint for Copilot OpenTelemetry export." } }, "type": "string", "default": "", - "included": true + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.otlpEndpoint" + ] }, { - "key": "chat.agentHost.claudeAgent.enabled", - "name": "Claude3PIntegration", + "key": "chat.agentHost.otel.captureContent", + "name": "CopilotOtelCaptureContent", "category": "InteractiveSession", - "minimumVersion": "1.113", + "minimumVersion": "1.127", "localization": { "description": { - "key": "chat.agentHost.claudeAgent.enabled.policy", - "value": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic Claude Agent SDK directly in the editor. Uses your existing Copilot subscription." + "key": "chat.agentHost.otel.captureContent.policy", + "value": "Controls whether Copilot OpenTelemetry export captures prompt, response, and tool content." } }, "type": "boolean", - "default": true, + "default": false, "included": true, "referencedSettings": [ - "github.copilot.chat.claudeAgent.enabled" + "github.copilot.chat.otel.captureContent" + ] + }, + { + "key": "chat.agentHost.otel.outfile", + "name": "CopilotOtelOutfile", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.outfile.policy", + "value": "Prevents local file export when enterprise-managed Copilot OpenTelemetry export is configured." + } + }, + "type": "string", + "default": "", + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.outfile" ] }, + { + "key": "chat.defaultModel", + "name": "ChatDefaultModel", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.defaultModel.policy", + "value": "Sets the default chat model for new conversations. Accepts \"auto\", a model family name (such as \"opus\" or \"gemini\"), or a full model id. Users can still switch the model within a conversation." + } + }, + "type": "string", + "default": "", + "included": true + }, { "key": "chat.tools.global.autoApprove", "name": "ChatToolsAutoApprove", From d0ba9fcdc4a12b9d9c1d47cb057e830b4c9fe5b5 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 11:43:03 -0700 Subject: [PATCH 08/26] docs: add OTel managed-settings sprint plan with completion notes --- ...gent_monitoring_managed_settings.sprint.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md new file mode 100644 index 00000000000000..a878b48318ff93 --- /dev/null +++ b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md @@ -0,0 +1,49 @@ +# OTel Managed-Settings Policy — Sprint Plan + +Implements the plan in [agent_monitoring_managed_settings.md](./agent_monitoring_managed_settings.md), +following the `add-policy` skill (`.github/skills/add-policy/`). + +## Scope decision + +Two tiers in the canonical `telemetry` schema: + +- **Scalar fields** — flatten to dot-path bag keys automatically (`flattenManagedSettings`), + no `STRUCTURED_MANAGED_SETTINGS` change needed, and map onto **existing** `chat.agentHost.otel.*` + settings. **In scope this sprint.** +- **Map fields** (`headers`, `resourceAttributes`) + `serviceName` — need brand-new owner settings + + (for the maps) structured-nested-key support in the normalizer + secret-safe env plumbing. + **Deferred** (see Hiccups). + +## Tasks (prioritized) + +1. **Keys** — add `telemetry.*` constants in `copilotManagedSettings.ts`. +2. **Response shape** — add `telemetry` block to `IManagedSettingsResponse` (`managedSettings.ts`). +3. **Owner policies** — attach `policy:` to `chat.agentHost.otel.{enabled,exporterType,otlpEndpoint,captureContent,outfile}` in `agentHostStarter.config.contribution.ts`. +4. **Env precedence** — `buildAgentHostOTelEnv` gains a `policySettings` param (policy overwrites env); both starters pass policy values. +5. **Extension policyReference** — point `github.copilot.chat.otel.{enabled,exporterType,otlpEndpoint,captureContent,outfile}` at the owner policies (`package.json`). +6. **Extension precedence** — `otelConfig.ts` policy > env > setting; `services.ts` plumbs `policyValue`. +7. **Validate** — `npm run typecheck-client`, tests. + +## Status + +- [x] 1 Keys +- [x] 2 Response shape +- [x] 3 Owner policies +- [x] 4 Env precedence +- [x] 5 Extension policyReference +- [x] 6 Extension precedence +- [x] 7 Validate (`typecheck-client` clean; `otelConfig.spec` 40/40; `export-policy-data` → 41 policies, 6 references) + +## Hiccups & Notes + +- **Deferred `headers` / `resourceAttributes` / `serviceName`.** These have no existing + `chat.agentHost.otel.*` owner setting, the two maps need structured-nested-key support in + `normalizeManagedSettings` (the current `STRUCTURED_MANAGED_SETTINGS` table only handles + top-level keys), and `headers` needs secret-safe out-of-env plumbing. Follow-up: add the three + owner settings + structured map handling + secret env injection, then `policyReference` them. +- **`dbSpanExporter` left user-controlled** (no policy) per the plan — diverges from the earlier + branch which wrongly nested it under `telemetry`. +- **Scalars need no structured-table change.** `telemetry.{enabled,endpoint,protocol,captureContent,lockCaptureContent}` + flatten to dot-path bag keys automatically via `flattenManagedSettings`. +- **`export-policy-data` works offline-ish.** It logged `Failed to fetch .../copilot_internal/user` + (no mock policy server running) but still exported the catalog from the distro `product.json`. From 4cf2f3e5cfb637f2228215804fc86836620c5754 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 13:21:29 -0700 Subject: [PATCH 09/26] feat: honor managed OTLP wire protocol (protobuf/json) in agent host env --- .../agentHostStarter.config.contribution.ts | 27 +++++++++++++ .../platform/agentHost/common/agentService.ts | 17 ++++++++ .../electron-main/electronAgentHostStarter.ts | 3 +- .../agentHost/node/nodeAgentHostStarter.ts | 3 +- .../test/common/agentService.test.ts | 39 ++++++++++++++++++- 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts index 9f4dcbf0bcad1f..fa25ffd716f800 100644 --- a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts @@ -21,6 +21,7 @@ import { AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, + AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, } from './agentService.js'; @@ -203,6 +204,32 @@ configurationRegistry.registerConfiguration({ }, }, }, + [AgentHostOTelOtlpProtocolSettingId]: { + type: 'string', + markdownDescription: nls.localize('chat.agentHost.otel.otlpProtocol', "Enterprise-managed OTLP wire protocol (`http/json`, `http/protobuf`, or `grpc`) for Copilot OpenTelemetry export. Policy-only: there is no user-facing setting; it carries the managed `telemetry.protocol` so the agent host's `OTEL_EXPORTER_OTLP_PROTOCOL` distinguishes protobuf from json."), + default: '', + scope: ConfigurationScope.APPLICATION, + // Policy-only delivery slot — no user-writable surface (mirrors `chat.plugins.extraMarketplaces`). + included: false, + tags: ['experimental', 'advanced'], + // Owns `CopilotOtelOtlpProtocol`; passes the raw managed `telemetry.protocol` through so the + // starters can set `OTEL_EXPORTER_OTLP_PROTOCOL` (the `exporterType` policy only carries transport). + policy: { + name: 'CopilotOtelOtlpProtocol', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedSettingValue(COPILOT_OTEL_PROTOCOL_KEY), + managedSettings: { + [COPILOT_OTEL_PROTOCOL_KEY]: { type: 'string' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.otlpProtocol.policy', + value: nls.localize('chat.agentHost.otel.otlpProtocol.policy', "Controls the enterprise-managed OTLP wire protocol (protobuf vs JSON) for Copilot OpenTelemetry export."), + } + }, + }, + }, [AgentHostOTelOtlpEndpointSettingId]: { type: 'string', markdownDescription: nls.localize('chat.agentHost.otel.otlpEndpoint', "OTLP endpoint URL when exporter type is `otlp-http` or `otlp-grpc`. Configurable in user settings only. Sets `OTEL_EXPORTER_OTLP_ENDPOINT` inside the agent host process."), diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 2fb482e041762c..70f441f60d4cc9 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -282,6 +282,13 @@ export const AgentHostCodexAgentBinaryArgsEnvVar = 'VSCODE_AGENT_HOST_CODEX_APP_ export const AgentHostOTelEnabledSettingId = 'chat.agentHost.otel.enabled'; /** Exporter type for the SDK's OTel pipeline. One of: `otlp-http`, `otlp-grpc`, `console`, `file`. */ export const AgentHostOTelExporterTypeSettingId = 'chat.agentHost.otel.exporterType'; +/** + * OTLP wire protocol (`http/json`, `http/protobuf`, `grpc`). Policy-only delivery slot (no user UI): + * carries the enterprise-managed `telemetry.protocol` so it can be threaded into the agent host's + * `OTEL_EXPORTER_OTLP_PROTOCOL` env, which the runtime needs to distinguish protobuf from json + * (the `exporterType` setting only models transport, not the HTTP wire encoding). + */ +export const AgentHostOTelOtlpProtocolSettingId = 'chat.agentHost.otel.otlpProtocol'; /** OTLP endpoint URL when `exporterType` is `otlp-http` or `otlp-grpc`. */ export const AgentHostOTelOtlpEndpointSettingId = 'chat.agentHost.otel.otlpEndpoint'; /** Whether to include prompt/response content in span attributes (privacy-sensitive). */ @@ -314,6 +321,8 @@ export const AgentHostOTelEnvVars = Object.freeze({ OtlpEndpoint: 'OTEL_EXPORTER_OTLP_ENDPOINT', OtlpEndpointAlt: 'COPILOT_OTEL_ENDPOINT', OtlpProtocol: 'OTEL_EXPORTER_OTLP_PROTOCOL', + OtlpTracesProtocol: 'OTEL_EXPORTER_OTLP_TRACES_PROTOCOL', + OtlpMetricsProtocol: 'OTEL_EXPORTER_OTLP_METRICS_PROTOCOL', OtlpHeaders: 'OTEL_EXPORTER_OTLP_HEADERS', CaptureContent: 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT', FilePath: 'COPILOT_OTEL_FILE_EXPORTER_PATH', @@ -328,6 +337,7 @@ export const AgentHostOTelEnvVars = Object.freeze({ export interface IAgentHostOTelSettings { readonly enabled?: boolean; readonly exporterType?: string; + readonly otlpProtocol?: string; readonly otlpEndpoint?: string; readonly captureContent?: boolean; readonly outfile?: string; @@ -386,6 +396,13 @@ export function buildAgentHostOTelEnv( setPolicy(AgentHostOTelEnvVars.ExporterType, policySettings.exporterType); setPolicy(AgentHostOTelEnvVars.FilePath, ''); } + if (policySettings.otlpProtocol !== undefined && policySettings.otlpProtocol !== '') { + // Mirror the CLI: thread the managed protocol into the generic AND per-signal protocol + // env vars so it wins over any user-provided OTEL_EXPORTER_OTLP_{,TRACES_,METRICS_}PROTOCOL. + setPolicy(AgentHostOTelEnvVars.OtlpProtocol, policySettings.otlpProtocol); + setPolicy(AgentHostOTelEnvVars.OtlpTracesProtocol, policySettings.otlpProtocol); + setPolicy(AgentHostOTelEnvVars.OtlpMetricsProtocol, policySettings.otlpProtocol); + } if (policySettings.otlpEndpoint !== undefined) { setPolicy(AgentHostOTelEnvVars.OtlpEndpoint, policySettings.otlpEndpoint); setPolicy(AgentHostOTelEnvVars.FilePath, ''); diff --git a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts index caa0defaea0b98..23a621f99346b4 100644 --- a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts +++ b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts @@ -19,7 +19,7 @@ import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js'; import { UtilityProcess } from '../../utilityProcess/electron-main/utilityProcess.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; import { deepClone } from '../../../base/common/objects.js'; import '../common/agentHost.config.contribution.js'; import '../common/agentHostStarter.config.contribution.js'; @@ -92,6 +92,7 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt }, process.env, { enabled: policyValue(AgentHostOTelEnabledSettingId), exporterType: policyValue(AgentHostOTelExporterTypeSettingId), + otlpProtocol: policyValue(AgentHostOTelOtlpProtocolSettingId), otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), diff --git a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts index 39a806f732f75c..2fad289e4d9a54 100644 --- a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts +++ b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts @@ -13,7 +13,7 @@ import { parseAgentHostDebugPort } from '../../environment/node/environmentServi import { ILogService } from '../../log/common/log.js'; import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; import '../common/agentHostStarter.config.contribution.js'; /** @@ -101,6 +101,7 @@ export class NodeAgentHostStarter extends Disposable implements IAgentHostStarte }, process.env, { enabled: policyValue(AgentHostOTelEnabledSettingId), exporterType: policyValue(AgentHostOTelExporterTypeSettingId), + otlpProtocol: policyValue(AgentHostOTelOtlpProtocolSettingId), otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), diff --git a/src/vs/platform/agentHost/test/common/agentService.test.ts b/src/vs/platform/agentHost/test/common/agentService.test.ts index cd29d5fdfc9f92..f3840cba9fb36b 100644 --- a/src/vs/platform/agentHost/test/common/agentService.test.ts +++ b/src/vs/platform/agentHost/test/common/agentService.test.ts @@ -6,7 +6,7 @@ import assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { AgentSession, isAgentEnabled } from '../../common/agentService.js'; +import { AgentSession, AgentHostOTelEnvVars, buildAgentHostOTelEnv, isAgentEnabled } from '../../common/agentService.js'; suite('AgentSession namespace', () => { @@ -66,3 +66,40 @@ suite('isAgentEnabled', () => { }); } }); + +suite('buildAgentHostOTelEnv', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('enterprise policy wins over inherited env', () => { + const env = buildAgentHostOTelEnv( + { enabled: false }, + { [AgentHostOTelEnvVars.OtlpEndpoint]: 'http://user:4318' }, + { enabled: true, otlpEndpoint: 'http://enterprise:4318' }, + ); + assert.strictEqual(env[AgentHostOTelEnvVars.Enabled], 'true'); + assert.strictEqual(env[AgentHostOTelEnvVars.OtlpEndpoint], 'http://enterprise:4318'); + }); + + test('managed protocol sets the generic and per-signal protocol env vars', () => { + const env = buildAgentHostOTelEnv( + {}, + { [AgentHostOTelEnvVars.OtlpProtocol]: 'http/json' }, + { otlpProtocol: 'http/protobuf' }, + ); + assert.strictEqual(env[AgentHostOTelEnvVars.OtlpProtocol], 'http/protobuf'); + assert.strictEqual(env[AgentHostOTelEnvVars.OtlpTracesProtocol], 'http/protobuf'); + assert.strictEqual(env[AgentHostOTelEnvVars.OtlpMetricsProtocol], 'http/protobuf'); + }); + + test('policy-disabled blanks endpoint and file export', () => { + const env = buildAgentHostOTelEnv( + { enabled: true, otlpEndpoint: 'http://user:4318' }, + {}, + { enabled: false }, + ); + assert.strictEqual(env[AgentHostOTelEnvVars.Enabled], 'false'); + assert.strictEqual(env[AgentHostOTelEnvVars.OtlpEndpoint], ''); + assert.strictEqual(env[AgentHostOTelEnvVars.FilePath], ''); + }); +}); From 7f142afb2615f99308f9223b90d93e74c0573a0c Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 13:21:29 -0700 Subject: [PATCH 10/26] build: export CopilotOtelOtlpProtocol policy to catalog --- build/lib/policies/policyData.jsonc | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index b6d52bf752fbe0..2735937eb01579 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -38,6 +38,21 @@ } ], "policies": [ + { + "key": "chat.agentHost.otel.otlpProtocol", + "name": "CopilotOtelOtlpProtocol", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.otlpProtocol.policy", + "value": "Controls the enterprise-managed OTLP wire protocol (protobuf vs JSON) for Copilot OpenTelemetry export." + } + }, + "type": "string", + "default": "", + "included": false + }, { "key": "mcp.enterpriseManagedAuth.idp", "name": "McpEnterpriseManagedAuthIdp", @@ -189,10 +204,7 @@ }, "type": "boolean", "default": false, - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.enabled" - ] + "included": true }, { "key": "chat.agentHost.otel.exporterType", @@ -231,10 +243,7 @@ "console", "file" ], - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.exporterType" - ] + "included": true }, { "key": "chat.agentHost.otel.otlpEndpoint", @@ -249,10 +258,7 @@ }, "type": "string", "default": "", - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.otlpEndpoint" - ] + "included": true }, { "key": "chat.agentHost.otel.captureContent", @@ -267,10 +273,7 @@ }, "type": "boolean", "default": false, - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.captureContent" - ] + "included": true }, { "key": "chat.agentHost.otel.outfile", @@ -285,10 +288,7 @@ }, "type": "string", "default": "", - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.outfile" - ] + "included": true }, { "key": "chat.defaultModel", From bdbe1f07602faa13f5e0c2b8af1ed483fa39d97b Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 13:38:08 -0700 Subject: [PATCH 11/26] feat: honor managed OTLP protobuf wire protocol in copilot-chat extension --- extensions/copilot/package-lock.json | 220 ++++++++++++++++++ extensions/copilot/package.json | 26 ++- .../extension/vscode-node/services.ts | 2 + .../src/platform/otel/common/agentOTelEnv.ts | 2 +- .../src/platform/otel/common/otelConfig.ts | 24 +- .../otel/common/test/otelConfig.spec.ts | 25 ++ .../src/platform/otel/node/otelServiceImpl.ts | 19 ++ 7 files changed, 312 insertions(+), 6 deletions(-) diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 218506a19a8de7..53f42cb984ce1b 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -22,10 +22,13 @@ "@opentelemetry/api-logs": "^0.212.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0", "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.214.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0", "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.212.0", "@opentelemetry/sdk-metrics": "^2.5.1", @@ -4226,6 +4229,105 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.214.0.tgz", + "integrity": "sha512-IWAVvCO1TlpotRjFmhQFz9RSfQy5BsLtDRBtptSrXZRwfyRPpuql/RMe5zdmu0Gxl3ERDFwOzOqkf3bwy7Jzcw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-logs": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", + "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", + "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { "version": "0.214.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.214.0.tgz", @@ -4329,6 +4431,57 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.214.0.tgz", + "integrity": "sha512-pJIcghFGhx3VSCgP5U+yZx+OMNj0t+ttnhC8IjL5Wza7vWIczctF6t3AGcVQffi2dEqX+ZHANoBwoPR8y6RMKA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-metrics": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { "version": "0.214.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.214.0.tgz", @@ -4465,6 +4618,73 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.214.0.tgz", + "integrity": "sha512-ON0spYWb2yAdQ9b+ItNyK0c6qdtcs+0eVR4YFJkhJL7agfT8sHFg0e5EesauSRiTHPZHiDobI92k77q0lwAmqg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", + "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/instrumentation": { "version": "0.41.2", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index c92e5211f0d3d7..c7b96f208b0925 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4328,7 +4328,10 @@ "github.copilot.chat.tools.grepSearch.outputFormat": { "type": "string", "default": "tag", - "enum": ["grep", "tag"], + "enum": [ + "grep", + "tag" + ], "markdownDescription": "%github.copilot.chat.tools.grepSearch.outputFormat%", "tags": [ "experimental", @@ -5299,6 +5302,24 @@ "advanced" ] }, + "github.copilot.chat.otel.protocol": { + "type": "string", + "enum": [ + "", + "http/json", + "http/protobuf", + "grpc" + ], + "default": "", + "scope": "application", + "policyReference": { + "name": "CopilotOtelOtlpProtocol" + }, + "markdownDescription": "OTLP wire protocol for Copilot Chat OTel data, mirroring `OTEL_EXPORTER_OTLP_PROTOCOL`. `http/protobuf` selects the protobuf-over-HTTP exporter; the default (empty) uses `http/json`. Configurable in user settings only. Requires window reload.", + "tags": [ + "advanced" + ] + }, "github.copilot.chat.otel.otlpEndpoint": { "type": "string", "default": "http://localhost:4318", @@ -7113,10 +7134,13 @@ "@opentelemetry/api-logs": "^0.212.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0", "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.214.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0", "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.212.0", "@opentelemetry/sdk-metrics": "^2.5.1", diff --git a/extensions/copilot/src/extension/extension/vscode-node/services.ts b/extensions/copilot/src/extension/extension/vscode-node/services.ts index 8a6d19d1b35699..0404592a3d8c9a 100644 --- a/extensions/copilot/src/extension/extension/vscode-node/services.ts +++ b/extensions/copilot/src/extension/extension/vscode-node/services.ts @@ -299,11 +299,13 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio settingMaxAttributeSizeChars: otelSettings.get('maxAttributeSizeChars'), 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'), extensionVersion: extensionContext.extension.packageJSON.version ?? '0.0.0', sessionId: env.sessionId, }); diff --git a/extensions/copilot/src/platform/otel/common/agentOTelEnv.ts b/extensions/copilot/src/platform/otel/common/agentOTelEnv.ts index 1b66ca61083766..1a6676d38f1e8b 100644 --- a/extensions/copilot/src/platform/otel/common/agentOTelEnv.ts +++ b/extensions/copilot/src/platform/otel/common/agentOTelEnv.ts @@ -75,7 +75,7 @@ export function deriveClaudeOTelEnv(config: OTelConfig, env: Record env > default + // Protocol (transport): policy > env > default const rawProtocol = input.policyExporterType === 'otlp-grpc' ? 'grpc' : input.policyExporterType === 'otlp-http' @@ -157,6 +161,18 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { : env['OTEL_EXPORTER_OTLP_PROTOCOL'] ?? env['COPILOT_OTEL_PROTOCOL']; const protocol: 'grpc' | 'http' = rawProtocol === 'grpc' ? 'grpc' : 'http'; + // Wire protocol (json vs protobuf within http): policy > env > setting > default(http/json). + // grpc transport always reports 'grpc'. + const rawWireProtocol = input.policyProtocol + ?? env['OTEL_EXPORTER_OTLP_PROTOCOL'] + ?? env['COPILOT_OTEL_PROTOCOL'] + ?? input.settingProtocol; + const otlpProtocol: OTelConfig['otlpProtocol'] = protocol === 'grpc' + ? 'grpc' + : rawWireProtocol === 'http/protobuf' + ? 'http/protobuf' + : 'http/json'; + // Endpoint: policy > COPILOT_OTEL env > OTEL env > setting > default const rawEndpoint = input.policyOtlpEndpoint ?? env['COPILOT_OTEL_ENDPOINT'] @@ -219,7 +235,7 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { enabledVia, exporterType, otlpEndpoint, - otlpProtocol: protocol, + otlpProtocol, captureContent, maxAttributeSizeChars: maxAttributeSizeChars < 0 ? 0 : maxAttributeSizeChars, fileExporterPath, @@ -240,7 +256,7 @@ function createDisabledConfig(input: OTelConfigInput): OTelConfig { enabledVia: 'disabled' as const, exporterType: 'otlp-http' as const, otlpEndpoint: '', - otlpProtocol: 'http' as const, + otlpProtocol: 'http/json' as const, captureContent: false, maxAttributeSizeChars: 0, dbSpanExporter: false, 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 ac082fd08d4b4d..7a4f77edaaa528 100644 --- a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts @@ -365,5 +365,30 @@ describe('resolveOTelConfig', () => { })); expect(config.captureContent).toBe(false); }); + + it('policy protocol http/protobuf selects the protobuf wire encoding', () => { + const config = resolveOTelConfig(makeInput({ + policyEnabled: true, + policyProtocol: 'http/protobuf', + })); + expect(config.otlpProtocol).toBe('http/protobuf'); + }); + + it('policy protocol http/json keeps the json wire encoding', () => { + const config = resolveOTelConfig(makeInput({ + policyEnabled: true, + policyProtocol: 'http/json', + })); + expect(config.otlpProtocol).toBe('http/json'); + }); + + it('grpc exporter reports grpc regardless of wire protocol', () => { + const config = resolveOTelConfig(makeInput({ + policyEnabled: true, + policyExporterType: 'otlp-grpc', + policyProtocol: 'http/protobuf', + })); + expect(config.otlpProtocol).toBe('grpc'); + }); }); }); diff --git a/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts b/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts index 47036221a8b97c..9530260f4adaf9 100644 --- a/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts +++ b/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts @@ -249,6 +249,25 @@ export class NodeOTelService implements IOTelService { }; } + // otlp-http with protobuf wire encoding + if (config.otlpProtocol === 'http/protobuf' && !dbOnlyMode) { + const [ + { OTLPTraceExporter }, + { OTLPLogExporter }, + { OTLPMetricExporter }, + ] = await Promise.all([ + import('@opentelemetry/exporter-trace-otlp-proto'), + import('@opentelemetry/exporter-logs-otlp-proto'), + import('@opentelemetry/exporter-metrics-otlp-proto'), + ]); + const base = config.otlpEndpoint.replace(/\/$/, ''); + return { + spanExporter: new OTLPTraceExporter({ url: `${base}/v1/traces` }), + logExporter: new OTLPLogExporter({ url: `${base}/v1/logs` }), + metricExporter: new OTLPMetricExporter({ url: `${base}/v1/metrics` }), + }; + } + // Default: otlp-http (or noop when in db-only mode) if (dbOnlyMode) { const metricsSDK = await import('@opentelemetry/sdk-metrics'); From 401a8ca8f3d0d05a6d19f1fa947d451b82187799 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 13:38:09 -0700 Subject: [PATCH 12/26] build: link otel.protocol reference in policy catalog --- build/lib/policies/policyData.jsonc | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index 2735937eb01579..aa3aa32b0c61fb 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -51,7 +51,10 @@ }, "type": "string", "default": "", - "included": false + "included": false, + "referencedSettings": [ + "github.copilot.chat.otel.protocol" + ] }, { "key": "mcp.enterpriseManagedAuth.idp", @@ -204,7 +207,10 @@ }, "type": "boolean", "default": false, - "included": true + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.enabled" + ] }, { "key": "chat.agentHost.otel.exporterType", @@ -243,7 +249,10 @@ "console", "file" ], - "included": true + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.exporterType" + ] }, { "key": "chat.agentHost.otel.otlpEndpoint", @@ -258,7 +267,10 @@ }, "type": "string", "default": "", - "included": true + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.otlpEndpoint" + ] }, { "key": "chat.agentHost.otel.captureContent", @@ -273,7 +285,10 @@ }, "type": "boolean", "default": false, - "included": true + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.captureContent" + ] }, { "key": "chat.agentHost.otel.outfile", @@ -288,7 +303,10 @@ }, "type": "string", "default": "", - "included": true + "included": true, + "referencedSettings": [ + "github.copilot.chat.otel.outfile" + ] }, { "key": "chat.defaultModel", From 4be6140abd7088e4b2eb75932192141008843176 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 13:39:17 -0700 Subject: [PATCH 13/26] docs: update OTel managed-settings plan/sprint for protocol parity --- .../agent_monitoring_managed_settings.md | 15 +++++++++++++- ...gent_monitoring_managed_settings.sprint.md | 20 +++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md index c9c190807ad658..974f50b20d068e 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md @@ -23,6 +23,14 @@ Nested `telemetry` block in the managed-settings response: - `telemetry.captureContent` — capture prompts/responses/tool args. Default false. - `telemetry.lockCaptureContent` — prevents the user from enabling content capture themselves. +## Implementation status + +Shipped (see [agent_monitoring_managed_settings.sprint.md](./agent_monitoring_managed_settings.sprint.md)): +`enabled`, `endpoint`, `protocol` (full `http/json` vs `http/protobuf` vs `grpc`), `captureContent`, +`lockCaptureContent` — on both the agent-host owner settings and the `github.copilot.chat.otel.*` +references. **Deferred:** `headers`, `resourceAttributes`, `serviceName` (need new owner settings, +structured-nested-key support for the maps, and secret-safe env plumbing). + ## Ownership - **Owner:** the `chat.agentHost.otel.*` settings carry the `policy:` definitions. @@ -37,7 +45,12 @@ Nested `telemetry` block in the managed-settings response: Enterprise policy > env vars > user settings > defaults (managed wins over env). -- `protocol` maps to the agent-host exporter/wire protocol; managed value wins, incl. per-signal env overrides. +- `protocol` is two axes: **transport** (maps to the agent-host `exporterType`: `grpc`→`otlp-grpc`, + `http/*`→`otlp-http`) and **wire encoding** (`http/json` vs `http/protobuf`). The raw value is + threaded through a dedicated policy-only slot so it sets `OTEL_EXPORTER_OTLP_PROTOCOL` (+ per-signal) + on the agent host and selects the `-proto` vs `-http` exporter in the extension. Managed value wins, + incl. per-signal env overrides. (The runtime defaults to `http/json` when unset, so the wire axis + must be propagated, not just mapped to `exporterType`.) - `captureContent`: explicit value wins; otherwise `lockCaptureContent: true` forces it off. - **Per-key managed-wins for `headers` / `resourceAttributes`**: a managed key overrides the same key from user env (`OTEL_EXPORTER_OTLP_HEADERS` / `OTEL_RESOURCE_ATTRIBUTES`), but diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md index a878b48318ff93..e7b63d27206f88 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md @@ -22,7 +22,14 @@ Two tiers in the canonical `telemetry` schema: 4. **Env precedence** — `buildAgentHostOTelEnv` gains a `policySettings` param (policy overwrites env); both starters pass policy values. 5. **Extension policyReference** — point `github.copilot.chat.otel.{enabled,exporterType,otlpEndpoint,captureContent,outfile}` at the owner policies (`package.json`). 6. **Extension precedence** — `otelConfig.ts` policy > env > setting; `services.ts` plumbs `policyValue`. -7. **Validate** — `npm run typecheck-client`, tests. +7. **OTLP wire protocol parity** — honor `http/json` vs `http/protobuf` end-to-end: + - Agent host: hidden policy-only `chat.agentHost.otel.otlpProtocol` slot (`CopilotOtelOtlpProtocol`) → + sets `OTEL_EXPORTER_OTLP_PROTOCOL` (+ per-signal) on the agent-host env (the runtime defaults to + `http/json`, so an unset protocol silently dropped the enterprise's protobuf choice). + - Extension: add the `@opentelemetry/exporter-{trace,logs,metrics}-otlp-proto` deps, widen + `OTelConfig.otlpProtocol` to `grpc | http/json | http/protobuf`, select the `-proto` exporter, + and add `github.copilot.chat.otel.protocol` (referencing `CopilotOtelOtlpProtocol`). +8. **Validate** — typecheck, specs, `export-policy-data`. ## Status @@ -32,7 +39,8 @@ Two tiers in the canonical `telemetry` schema: - [x] 4 Env precedence - [x] 5 Extension policyReference - [x] 6 Extension precedence -- [x] 7 Validate (`typecheck-client` clean; `otelConfig.spec` 40/40; `export-policy-data` → 41 policies, 6 references) +- [x] 7 OTLP wire protocol parity (agent host + extension `-proto`) +- [x] 8 Validate (`typecheck-client` clean; agentService + `otelConfig` specs green; `export-policy-data` → 42 policies, 7 references) ## Hiccups & Notes @@ -41,9 +49,13 @@ Two tiers in the canonical `telemetry` schema: `normalizeManagedSettings` (the current `STRUCTURED_MANAGED_SETTINGS` table only handles top-level keys), and `headers` needs secret-safe out-of-env plumbing. Follow-up: add the three owner settings + structured map handling + secret env injection, then `policyReference` them. +- **OTLP protocol was lossy at first.** Mapping `telemetry.protocol` only onto the `exporterType` + enum (`otlp-http`) dropped the protobuf-vs-json axis; the agent-host runtime then defaulted to + `http/json`. Fixed by threading the raw protocol through a dedicated slot on both surfaces. +- **Extension can't do protobuf-HTTP without a dep.** Its exporter only shipped `-grpc` + `-http` + (JSON); added the `-proto` siblings. The earlier `cgmanifest` worry was unfounded — ordinary npm + registry deps aren't tracked there (its `-grpc`/`-http` siblings have no cgmanifest entry either). - **`dbSpanExporter` left user-controlled** (no policy) per the plan — diverges from the earlier branch which wrongly nested it under `telemetry`. - **Scalars need no structured-table change.** `telemetry.{enabled,endpoint,protocol,captureContent,lockCaptureContent}` flatten to dot-path bag keys automatically via `flattenManagedSettings`. -- **`export-policy-data` works offline-ish.** It logged `Failed to fetch .../copilot_internal/user` - (no mock policy server running) but still exported the catalog from the distro `product.json`. From 276c471c05467cbd0b6677da3b7c72726542994f Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 14:40:55 -0700 Subject: [PATCH 14/26] fix: deliver managed OTel policy to the desktop agent host The managed-settings policy (AccountPolicyService) is applied only in the renderer's MultiplexPolicyService, so the main-process config service that ElectronAgentHostStarter reads returns policyValue===undefined for the chat.agentHost.otel.* keys. The agent host was therefore spawned without the managed OTel env (endpoint/protocol/enabled) and never exported telemetry. Thread the renderer-resolved policy to the starter over the existing renderer->main connection seam (the renderer is already the gate for policy-resolved agent-host decisions main cannot observe): - agentService: AgentHostOTelPolicyIpcChannel + readAgentHostOTelPolicySettings() + sanitizeAgentHostOTelPolicySettings(). - localAgentHostService: forward the resolved policy before acquirePort (FIFO ordering lands it before the lazy spawn). - electronAgentHostStarter: cache the renderer policy and use it as buildAgentHostOTelEnv policySettings, falling back to main-process policy. Additive/optional: no managed policy -> unchanged behavior. Verified e2e (agent host env got endpoint 4318 + http/protobuf; Aspire service.name github-copilot). --- .../platform/agentHost/common/agentService.ts | 53 ++++++++++++ .../electron-browser/localAgentHostService.ts | 10 ++- .../electron-main/electronAgentHostStarter.ts | 43 ++++++++-- .../test/common/agentService.test.ts | 83 ++++++++++++++++++- 4 files changed, 178 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 70f441f60d4cc9..44a11966d06a8f 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -344,6 +344,59 @@ export interface IAgentHostOTelSettings { readonly dbSpanExporterEnabled?: boolean; } +/** + * IPC channel (renderer -> main) the desktop agent-host path uses to hand the + * enterprise-resolved `chat.agentHost.otel.*` policy to `ElectronAgentHostStarter`. + * + * The main-process configuration service does NOT include the renderer-only + * `AccountPolicyService` (managed settings: server / native-MDM / file channels), so a + * starter running in the main process sees `policyValue === undefined` for these keys. + * The renderer — whose policy layer does include managed settings — forwards the resolved + * values here just before requesting the agent-host connection, so the host is spawned with + * the managed OTel env. See {@link readAgentHostOTelPolicySettings}. + */ +export const AgentHostOTelPolicyIpcChannel = 'vscode:agentHostOTelPolicy'; + +/** + * Resolve the enterprise-policy values for the `chat.agentHost.otel.*` settings from a + * configuration service whose policy layer includes managed settings (i.e. the renderer's). + * Each field is `undefined` when no policy is set. Intended as the `policySettings` argument + * of {@link buildAgentHostOTelEnv}. + */ +export function readAgentHostOTelPolicySettings(configurationService: IConfigurationService): IAgentHostOTelSettings { + const policyValue = (key: string): T | undefined => configurationService.inspect(key).policyValue; + return { + enabled: policyValue(AgentHostOTelEnabledSettingId), + exporterType: policyValue(AgentHostOTelExporterTypeSettingId), + otlpProtocol: policyValue(AgentHostOTelOtlpProtocolSettingId), + otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), + captureContent: policyValue(AgentHostOTelCaptureContentSettingId), + outfile: policyValue(AgentHostOTelOutfileSettingId), + }; +} + +/** + * Validate/normalize an {@link IAgentHostOTelSettings} received over IPC, keeping only + * well-typed fields. Defends the main process against a malformed payload before the values + * are turned into agent-host process env vars. + */ +export function sanitizeAgentHostOTelPolicySettings(raw: unknown): IAgentHostOTelSettings { + if (!raw || typeof raw !== 'object') { + return {}; + } + const record = raw as Record; + const asString = (value: unknown): string | undefined => typeof value === 'string' ? value : undefined; + const asBoolean = (value: unknown): boolean | undefined => typeof value === 'boolean' ? value : undefined; + return { + enabled: asBoolean(record.enabled), + exporterType: asString(record.exporterType), + otlpProtocol: asString(record.otlpProtocol), + otlpEndpoint: asString(record.otlpEndpoint), + captureContent: asBoolean(record.captureContent), + outfile: asString(record.outfile), + }; +} + /** * Build the env-var overlay for the agent host process from user settings and * inherited env. Settings are translated to env vars, but if the same env var is diff --git a/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts b/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts index c3c8a58bf8cc80..3290ee83fa4c75 100644 --- a/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts +++ b/src/vs/platform/agentHost/electron-browser/localAgentHostService.ts @@ -11,11 +11,12 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { getDelayedChannel, ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; import { Client as MessagePortClient } from '../../../base/parts/ipc/common/ipc.mp.js'; import { acquirePort } from '../../../base/parts/ipc/electron-browser/ipc.mp.js'; +import { ipcRenderer } from '../../../base/parts/sandbox/electron-browser/globals.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; import { ILogService } from '../../log/common/log.js'; -import { AgentHostAhpJsonlLoggingSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostIpcChannels, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService, isAgentHostEnabled, IMcpNotification } from '../common/agentService.js'; +import { AgentHostAhpJsonlLoggingSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostIpcChannels, IAgentCreateChatOptions, IAgentCreateSessionConfig, IAgentHostInspectInfo, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService, isAgentHostEnabled, IMcpNotification, AgentHostOTelPolicyIpcChannel, readAgentHostOTelPolicySettings } from '../common/agentService.js'; import { AhpJsonlLogger } from '../common/ahpJsonlLogger.js'; import { wrapAgentServiceWithAhpLogging } from './localAhpJsonlLogging.js'; import { AgentSubscriptionManager, isActionEnvelopeRelevantToSubscriptionUris, type IActiveSubscriptionInfo, type IAgentSubscription } from '../common/state/agentSubscription.js'; @@ -148,6 +149,13 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos private async _connect(): Promise { this._logService.info('[AgentHost:renderer] Acquiring MessagePort to agent host...'); + // Forward the enterprise-resolved OTel policy to the main-process starter BEFORE + // requesting the connection. The main config service does not include the renderer-only + // managed-settings policy (`AccountPolicyService`), so without this the agent host would + // be spawned missing managed OTel settings (endpoint/protocol/enabled). Sent first so it + // is processed (FIFO per sender) before the starter spawns the host on the connection + // request. See `AgentHostOTelPolicyIpcChannel`. + ipcRenderer.send(AgentHostOTelPolicyIpcChannel, readAgentHostOTelPolicySettings(this._configurationService)); const port = await acquirePort('vscode:createAgentHostMessageChannel', 'vscode:createAgentHostMessageChannelResult'); this._logService.info('[AgentHost:renderer] MessagePort acquired, creating client...'); diff --git a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts index 23a621f99346b4..ffd79aab0c13b3 100644 --- a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts +++ b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts @@ -19,7 +19,7 @@ import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js'; import { UtilityProcess } from '../../utilityProcess/electron-main/utilityProcess.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelPolicyIpcChannel, buildAgentHostOTelEnv, buildAgentSdkEnv, IAgentHostOTelSettings, sanitizeAgentHostOTelPolicySettings } from '../common/agentService.js'; import { deepClone } from '../../../base/common/objects.js'; import '../common/agentHost.config.contribution.js'; import '../common/agentHostStarter.config.contribution.js'; @@ -34,6 +34,15 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt private readonly _onWillShutdown = this._register(new Emitter()); readonly onWillShutdown = this._onWillShutdown.event; + /** + * Enterprise OTel policy forwarded by the renderer (see `AgentHostOTelPolicyIpcChannel`). + * The main-process config service lacks the managed-settings (`AccountPolicyService`) policy + * layer, so the renderer — which has it — sends the resolved values here before requesting + * the connection that lazily spawns the host. Used as the `policySettings` of + * `buildAgentHostOTelEnv` in `start()`, falling back to main-process policy when absent. + */ + private _otelPolicyFromRenderer: IAgentHostOTelSettings | undefined = undefined; + constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IEnvironmentMainService private readonly _environmentMainService: IEnvironmentMainService, @@ -44,6 +53,16 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt this._register(this._lifecycleMainService.onWillShutdown(() => this._onWillShutdown.fire())); + // Capture the enterprise OTel policy the renderer forwards before it requests a + // connection (FIFO per sender ensures this lands before the spawn in `start()`). + const onOTelPolicy = (_e: IpcMainEvent, policy: unknown) => { + this._otelPolicyFromRenderer = sanitizeAgentHostOTelPolicySettings(policy); + }; + validatedIpcMain.on(AgentHostOTelPolicyIpcChannel, onOTelPolicy); + this._register(toDisposable(() => { + validatedIpcMain.removeListener(AgentHostOTelPolicyIpcChannel, onOTelPolicy); + })); + // Listen for new windows to establish a direct MessagePort connection to the agent host const onWindowConnection = (e: IpcMainEvent, nonce: string) => this._onWindowConnection(e, nonce); validatedIpcMain.on('vscode:createAgentHostMessageChannel', onWindowConnection); @@ -81,7 +100,20 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt // the agent host process. Any value already present on `process.env` wins // for user settings, while enterprise policy values win over inherited env — // see `buildAgentHostOTelEnv` for the precedence. + // + // Policy source: prefer the renderer-forwarded policy (its config service + // includes the managed-settings `AccountPolicyService` layer that the main + // process cannot see); fall back to the main-process policy for the keys it + // can resolve (e.g. native MDM via the policy channel). const policyValue = (key: string): T | undefined => this._configurationService.inspect(key).policyValue; + const policySettings: IAgentHostOTelSettings = this._otelPolicyFromRenderer ?? { + enabled: policyValue(AgentHostOTelEnabledSettingId), + exporterType: policyValue(AgentHostOTelExporterTypeSettingId), + otlpProtocol: policyValue(AgentHostOTelOtlpProtocolSettingId), + otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), + captureContent: policyValue(AgentHostOTelCaptureContentSettingId), + outfile: policyValue(AgentHostOTelOutfileSettingId), + }; const otelEnv = buildAgentHostOTelEnv({ enabled: this._configurationService.getValue(AgentHostOTelEnabledSettingId), exporterType: this._configurationService.getValue(AgentHostOTelExporterTypeSettingId), @@ -89,14 +121,7 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt captureContent: this._configurationService.getValue(AgentHostOTelCaptureContentSettingId), outfile: this._configurationService.getValue(AgentHostOTelOutfileSettingId), dbSpanExporterEnabled: this._configurationService.getValue(AgentHostOTelDbSpanExporterEnabledSettingId), - }, process.env, { - enabled: policyValue(AgentHostOTelEnabledSettingId), - exporterType: policyValue(AgentHostOTelExporterTypeSettingId), - otlpProtocol: policyValue(AgentHostOTelOtlpProtocolSettingId), - otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), - captureContent: policyValue(AgentHostOTelCaptureContentSettingId), - outfile: policyValue(AgentHostOTelOutfileSettingId), - }); + }, process.env, policySettings); const args = [ '--logsPath', this._environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, diff --git a/src/vs/platform/agentHost/test/common/agentService.test.ts b/src/vs/platform/agentHost/test/common/agentService.test.ts index f3840cba9fb36b..609ba0d6158e7d 100644 --- a/src/vs/platform/agentHost/test/common/agentService.test.ts +++ b/src/vs/platform/agentHost/test/common/agentService.test.ts @@ -6,7 +6,8 @@ import assert from 'assert'; import { URI } from '../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { AgentSession, AgentHostOTelEnvVars, buildAgentHostOTelEnv, isAgentEnabled } from '../../common/agentService.js'; +import { IConfigurationService } from '../../../configuration/common/configuration.js'; +import { AgentSession, AgentHostOTelEnvVars, buildAgentHostOTelEnv, isAgentEnabled, readAgentHostOTelPolicySettings, sanitizeAgentHostOTelPolicySettings } from '../../common/agentService.js'; suite('AgentSession namespace', () => { @@ -103,3 +104,83 @@ suite('buildAgentHostOTelEnv', () => { assert.strictEqual(env[AgentHostOTelEnvVars.FilePath], ''); }); }); + +suite('readAgentHostOTelPolicySettings', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + function fakeConfig(policy: Record): IConfigurationService { + return { + inspect: (key: string) => ({ policyValue: policy[key] as T | undefined }), + } as unknown as IConfigurationService; + } + + test('maps the policy value of every otel key', () => { + const cfg = fakeConfig({ + 'chat.agentHost.otel.enabled': true, + 'chat.agentHost.otel.exporterType': 'otlp-http', + 'chat.agentHost.otel.otlpProtocol': 'http/protobuf', + 'chat.agentHost.otel.otlpEndpoint': 'http://localhost:4318', + 'chat.agentHost.otel.captureContent': false, + 'chat.agentHost.otel.outfile': '/tmp/o.jsonl', + }); + assert.deepStrictEqual(readAgentHostOTelPolicySettings(cfg), { + enabled: true, + exporterType: 'otlp-http', + otlpProtocol: 'http/protobuf', + otlpEndpoint: 'http://localhost:4318', + captureContent: false, + outfile: '/tmp/o.jsonl', + }); + }); + + test('absent policy yields an all-undefined snapshot', () => { + assert.deepStrictEqual(readAgentHostOTelPolicySettings(fakeConfig({})), { + enabled: undefined, + exporterType: undefined, + otlpProtocol: undefined, + otlpEndpoint: undefined, + captureContent: undefined, + outfile: undefined, + }); + }); +}); + +suite('sanitizeAgentHostOTelPolicySettings', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('keeps well-typed fields and drops unknown/mistyped ones', () => { + assert.deepStrictEqual( + sanitizeAgentHostOTelPolicySettings({ + enabled: true, + exporterType: 'otlp-http', + otlpProtocol: 'http/protobuf', + otlpEndpoint: 'http://localhost:4318', + captureContent: false, + outfile: '/tmp/o.jsonl', + bogus: 123, + }), + { + enabled: true, + exporterType: 'otlp-http', + otlpProtocol: 'http/protobuf', + otlpEndpoint: 'http://localhost:4318', + captureContent: false, + outfile: '/tmp/o.jsonl', + }, + ); + }); + + test('mistyped fields are dropped to undefined', () => { + assert.deepStrictEqual( + sanitizeAgentHostOTelPolicySettings({ enabled: 'yes', otlpEndpoint: 42, captureContent: 1 }), + { enabled: undefined, exporterType: undefined, otlpProtocol: undefined, otlpEndpoint: undefined, captureContent: undefined, outfile: undefined }, + ); + }); + + test('non-object input yields an empty policy', () => { + assert.deepStrictEqual(sanitizeAgentHostOTelPolicySettings(null), {}); + assert.deepStrictEqual(sanitizeAgentHostOTelPolicySettings('x'), {}); + }); +}); From 0fdb82fd27ed4c871c2b92960377a950ccea7075 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 15:27:46 -0700 Subject: [PATCH 15/26] docs: revise OTel managed-settings sprint plan for headers/resourceAttributes/serviceName Records the runtime spike: the headless agent host resolves OTel from env only and doesn't self-fetch managed telemetry, but build_resource reads OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES env. Revised plan delivers serviceName + resourceAttributes to both surfaces (env for the host, programmatic for the extension) and headers to the extension only; agent-host headers stay deferred (env would leak the token to tool subprocesses). --- ...gent_monitoring_managed_settings.sprint.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md index e7b63d27206f88..9f37df83ca42b2 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md @@ -59,3 +59,62 @@ Two tiers in the canonical `telemetry` schema: branch which wrongly nested it under `telemetry`. - **Scalars need no structured-table change.** `telemetry.{enabled,endpoint,protocol,captureContent,lockCaptureContent}` flatten to dot-path bag keys automatically via `flattenManagedSettings`. + +- **Desktop agent host wasn't receiving managed OTel policy (fixed).** `AccountPolicyService` + (server / native-MDM / file managed settings) is added to the policy service only in the + **renderer** (`desktop.main.ts` `MultiplexPolicyService([policyChannel, accountPolicy])`). The + agent-host starter (`ElectronAgentHostStarter`) runs in **electron-main**, whose config service + lacks that layer, so `inspect(key).policyValue` for `chat.agentHost.otel.*` was always `undefined` + and the host spawned with no managed OTel env (endpoint/protocol/enabled) — the extension worked + because the extension host mirrors the renderer config. Fix threads the renderer-resolved policy to + the starter over the existing renderer→main connection seam (`AgentHostOTelPolicyIpcChannel`; + `readAgentHostOTelPolicySettings` / `sanitizeAgentHostOTelPolicySettings`; renderer sends before + `acquirePort`, starter uses it as `buildAgentHostOTelEnv` `policySettings`, falling back to + main-process policy). Verified e2e: agent host env got `4318` + `http/protobuf`; Aspire + `service.name=github-copilot`. + +## Follow-ups (not in this sprint) + +- **Deferred map/serviceName fields** — `headers` / `resourceAttributes` / `serviceName`. Spike + (against the CLI runtime source) revised the plan — see **Spike: where the agent host gets OTel + config** below. Net: deliver them VS Code-only (no runtime change) via the env vars the runtime's + `build_resource` already reads (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`) plus the + extension's own exporter; **agent-host `headers` stay deferred** (secret-in-env leaks to tool + subprocesses — needs the runtime's `applyManagedTelemetry`). +- **Agent-host OTel env is fixed at spawn; no re-apply on later policy change.** The agent host is a + singleton utility process whose OTel env is computed once in `start()`. If managed OTel policy + changes (or first syncs) **after** the host has already spawned, the running host keeps the stale + env — same class of problem as the extension requiring a "Reload Window". Follow-up: detect a + managed-OTel-policy change in the renderer and either (a) re-spawn the agent host, or (b) surface a + restart affordance, so the new policy takes effect without a full quit/relaunch. Today the host + must first spawn *after* policy sync to pick up managed OTel settings. + +## Spike: where the agent host gets OTel config (for `headers`/`resourceAttributes`/`serviceName`) + +Traced through the CLI runtime (`copilot-agent-runtime`): + +- The headless/agent-host runtime resolves OTel from **env only** (`OtelLifecycle` → `resolveOtelConfig`). + Its env reader (`readOtelEnv`) maps only the scalar `COPILOT_OTEL_*`/`OTEL_*` vars — it does **not** + read headers/resourceAttributes/serviceName into the config object. +- Those three only flow through the **structured** path: `mergeManagedOtelConfig(managed, env)` → + `OtelLifecycle.applyManagedTelemetry(managed)` (headers stamped out-of-env by `ManagedHeaderClient`). + That path is invoked **interactive-CLI-only** (`expectManagedTelemetry` gated on `!isNonInteractiveMode`; + TUI fetches via `app.tsx onAuthChange → fetchManagedSettings`). The SDK *does* expose + `applyManagedTelemetry`/`expectManagedTelemetry` on `LocalSessionHost`, but the headless agent-host + entry never calls them and nothing wires VS Code's block in. +- **However**, the runtime's `build_resource` (otel_sdk.rs) reads standard env as resource precedence: + managed `service_name`/`resource_attributes` (1) → `OTEL_RESOURCE_ATTRIBUTES` (2) → + `OTEL_SERVICE_NAME` (3) → default. So VS Code **can** deliver `serviceName`/`resourceAttributes` to the + agent host via those env vars without any runtime change. `headers` via `OTEL_EXPORTER_OTLP_HEADERS` + env would also be read — but that's a secret that leaks into every tool subprocess the host spawns. + +### Revised plan (VS Code-only, no runtime change) + +| Field | Extension (`copilot-chat`) | Agent host (`github-copilot`) | +| --- | --- | --- | +| `serviceName` | programmatic (own resource) | env `OTEL_SERVICE_NAME` | +| `resourceAttributes` | programmatic | env `OTEL_RESOURCE_ATTRIBUTES` | +| `headers` (secret) | programmatic, out-of-env | **deferred** — needs runtime `applyManagedTelemetry` (env path leaks the token to tool subprocesses) | + +Sequencing (separate commits): (1) `serviceName`; (2) `resourceAttributes` (needs nested-key support in +`STRUCTURED_MANAGED_SETTINGS`); (3) `headers` extension-only. Agent-host `headers` tracked under the runtime follow-up. From ee15deed1c44369e140f32ee5b76509f1d8983e4 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 15:44:01 -0700 Subject: [PATCH 16/26] feat: deliver managed OTel serviceName to both surfaces --- build/lib/policies/policyData.jsonc | 18 +++++++++++ extensions/copilot/package.json | 12 +++++++ .../extension/vscode-node/services.ts | 2 ++ .../src/platform/otel/common/otelConfig.ts | 11 +++++-- .../otel/common/test/otelConfig.spec.ts | 31 +++++++++++++++++++ .../agentHostStarter.config.contribution.ts | 29 ++++++++++++++++- .../platform/agentHost/common/agentService.ts | 10 ++++++ .../electron-main/electronAgentHostStarter.ts | 3 +- .../agentHost/node/nodeAgentHostStarter.ts | 3 +- .../test/common/agentService.test.ts | 26 +++++++++++++++- .../policy/common/copilotManagedSettings.ts | 7 +++-- .../accounts/browser/managedSettings.ts | 1 + 12 files changed, 145 insertions(+), 8 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index aa3aa32b0c61fb..dd7f0a79ae8d17 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -56,6 +56,24 @@ "github.copilot.chat.otel.protocol" ] }, + { + "key": "chat.agentHost.otel.serviceName", + "name": "CopilotOtelServiceName", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.serviceName.policy", + "value": "Controls the enterprise-managed OTel `service.name` resource attribute for Copilot OpenTelemetry export." + } + }, + "type": "string", + "default": "", + "included": false, + "referencedSettings": [ + "github.copilot.chat.otel.serviceName" + ] + }, { "key": "mcp.enterpriseManagedAuth.idp", "name": "McpEnterpriseManagedAuthIdp", diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index c7b96f208b0925..a94d7a5fc66756 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -5344,6 +5344,18 @@ "advanced" ] }, + "github.copilot.chat.otel.serviceName": { + "type": "string", + "default": "", + "scope": "application", + "policyReference": { + "name": "CopilotOtelServiceName" + }, + "markdownDescription": "OTel `service.name` resource attribute for Copilot Chat OTel data. Configurable in user settings only. Env var `OTEL_SERVICE_NAME` takes precedence over the setting; enterprise policy takes precedence over both. Requires window reload.", + "tags": [ + "advanced" + ] + }, "github.copilot.chat.otel.maxAttributeSizeChars": { "type": "integer", "default": 0, diff --git a/extensions/copilot/src/extension/extension/vscode-node/services.ts b/extensions/copilot/src/extension/extension/vscode-node/services.ts index 0404592a3d8c9a..0a9c4bd3963e5f 100644 --- a/extensions/copilot/src/extension/extension/vscode-node/services.ts +++ b/extensions/copilot/src/extension/extension/vscode-node/services.ts @@ -306,6 +306,8 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio policyCaptureContent: policyValue('captureContent'), policyOutfile: policyValue('outfile'), policyProtocol: policyValue('protocol'), + settingServiceName: otelSettings.get('serviceName') || undefined, + policyServiceName: policyValue('serviceName'), 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 bb4d482a9d7d58..6ffde6a1a0459b 100644 --- a/extensions/copilot/src/platform/otel/common/otelConfig.ts +++ b/extensions/copilot/src/platform/otel/common/otelConfig.ts @@ -96,6 +96,10 @@ export interface OTelConfigInput { policyOutfile?: string; /** Enterprise-managed OTLP wire protocol (raw `telemetry.protocol`). */ policyProtocol?: string; + /** Service name from VS Code setting (`github.copilot.chat.otel.serviceName`). */ + settingServiceName?: string; + /** Enterprise-managed `service.name` (raw `telemetry.serviceName`). */ + policyServiceName?: string; extensionVersion: string; sessionId: string; vscodeTelemetryLevel?: string; @@ -223,8 +227,11 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { // HTTP instrumentation const httpInstrumentation = envBool(env['COPILOT_OTEL_HTTP_INSTRUMENTATION']) ?? false; - // Service name - const serviceName = env['OTEL_SERVICE_NAME'] ?? 'copilot-chat'; + // Service name: policy > env > setting > default. Empty values fall through. + const serviceName = (input.policyServiceName || undefined) + ?? env['OTEL_SERVICE_NAME'] + ?? (input.settingServiceName || undefined) + ?? 'copilot-chat'; // Resource attributes const resourceAttributes = parseResourceAttributes(env['OTEL_RESOURCE_ATTRIBUTES']); 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 7a4f77edaaa528..be6911ba3dd23a 100644 --- a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts @@ -155,6 +155,37 @@ describe('resolveOTelConfig', () => { expect(config.serviceName).toBe('my-service'); }); + it('resolves service name from setting when env is absent', () => { + const config = resolveOTelConfig(makeInput({ + settingEnabled: true, + settingServiceName: 'setting-service', + })); + expect(config.serviceName).toBe('setting-service'); + }); + + it('prefers OTEL_SERVICE_NAME env over the setting', () => { + const config = resolveOTelConfig(makeInput({ + settingServiceName: 'setting-service', + env: { + 'COPILOT_OTEL_ENABLED': 'true', + 'OTEL_SERVICE_NAME': 'env-service', + }, + })); + expect(config.serviceName).toBe('env-service'); + }); + + it('enterprise policy service name wins over env and setting', () => { + const config = resolveOTelConfig(makeInput({ + policyServiceName: 'policy-service', + settingServiceName: 'setting-service', + env: { + 'COPILOT_OTEL_ENABLED': 'true', + 'OTEL_SERVICE_NAME': 'env-service', + }, + })); + expect(config.serviceName).toBe('policy-service'); + }); + it('returns frozen config objects', () => { const enabled = resolveOTelConfig(makeInput({ settingEnabled: true })); const disabled = resolveOTelConfig(makeInput()); diff --git a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts index fa25ffd716f800..f038f7041e7931 100644 --- a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts @@ -7,7 +7,7 @@ import * as nls from '../../../nls.js'; import { IPolicyData } from '../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../base/common/policy.js'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; -import { COPILOT_OTEL_CAPTURE_CONTENT_KEY, COPILOT_OTEL_ENABLED_KEY, COPILOT_OTEL_ENDPOINT_KEY, COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY, COPILOT_OTEL_PROTOCOL_KEY, managedSettingValue } from '../../policy/common/copilotManagedSettings.js'; +import { COPILOT_OTEL_CAPTURE_CONTENT_KEY, COPILOT_OTEL_ENABLED_KEY, COPILOT_OTEL_ENDPOINT_KEY, COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY, COPILOT_OTEL_PROTOCOL_KEY, COPILOT_OTEL_SERVICE_NAME_KEY, managedSettingValue } from '../../policy/common/copilotManagedSettings.js'; import product from '../../product/common/product.js'; import { Registry } from '../../registry/common/platform.js'; import { @@ -23,6 +23,7 @@ import { AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, + AgentHostOTelServiceNameSettingId, } from './agentService.js'; // Settings consumed by the agent host starter (`electronAgentHostStarter.ts` @@ -309,5 +310,31 @@ configurationRegistry.registerConfiguration({ scope: ConfigurationScope.APPLICATION, tags: ['experimental', 'advanced'], }, + [AgentHostOTelServiceNameSettingId]: { + type: 'string', + markdownDescription: nls.localize('chat.agentHost.otel.serviceName', "Enterprise-managed OTel `service.name` resource attribute for Copilot OpenTelemetry export. Policy-only: there is no user-facing setting; it carries the managed `telemetry.serviceName` so the agent host's `OTEL_SERVICE_NAME` identifies spans from this deployment."), + default: '', + scope: ConfigurationScope.APPLICATION, + // Policy-only delivery slot — no user-writable surface (mirrors `chat.agentHost.otel.otlpProtocol`). + included: false, + tags: ['experimental', 'advanced'], + // Owns `CopilotOtelServiceName`; passes the raw managed `telemetry.serviceName` through so the + // starters can set `OTEL_SERVICE_NAME` on the agent host process. + policy: { + name: 'CopilotOtelServiceName', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedSettingValue(COPILOT_OTEL_SERVICE_NAME_KEY), + managedSettings: { + [COPILOT_OTEL_SERVICE_NAME_KEY]: { type: 'string' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.serviceName.policy', + value: nls.localize('chat.agentHost.otel.serviceName.policy', "Controls the enterprise-managed OTel `service.name` resource attribute for Copilot OpenTelemetry export."), + } + }, + }, + }, } }); diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 44a11966d06a8f..2403970763e48d 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -295,6 +295,8 @@ export const AgentHostOTelOtlpEndpointSettingId = 'chat.agentHost.otel.otlpEndpo export const AgentHostOTelCaptureContentSettingId = 'chat.agentHost.otel.captureContent'; /** Output path when `exporterType` is `file`. */ export const AgentHostOTelOutfileSettingId = 'chat.agentHost.otel.outfile'; +/** Policy-only delivery slot for the enterprise-managed OTel `service.name` (no user UI). */ +export const AgentHostOTelServiceNameSettingId = 'chat.agentHost.otel.serviceName'; /** When true, ALL spans are persisted to a local SQLite store regardless of `exporterType`. */ export const AgentHostOTelDbSpanExporterEnabledSettingId = 'chat.agentHost.otel.dbSpanExporter.enabled'; @@ -327,6 +329,7 @@ export const AgentHostOTelEnvVars = Object.freeze({ CaptureContent: 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT', FilePath: 'COPILOT_OTEL_FILE_EXPORTER_PATH', SourceName: 'COPILOT_OTEL_SOURCE_NAME', + ServiceName: 'OTEL_SERVICE_NAME', DbSpanExporterEnabled: 'COPILOT_OTEL_DB_SPAN_EXPORTER_ENABLED', } as const); @@ -341,6 +344,7 @@ export interface IAgentHostOTelSettings { readonly otlpEndpoint?: string; readonly captureContent?: boolean; readonly outfile?: string; + readonly serviceName?: string; readonly dbSpanExporterEnabled?: boolean; } @@ -372,6 +376,7 @@ export function readAgentHostOTelPolicySettings(configurationService: IConfigura otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), + serviceName: policyValue(AgentHostOTelServiceNameSettingId), }; } @@ -394,6 +399,7 @@ export function sanitizeAgentHostOTelPolicySettings(raw: unknown): IAgentHostOTe otlpEndpoint: asString(record.otlpEndpoint), captureContent: asBoolean(record.captureContent), outfile: asString(record.outfile), + serviceName: asString(record.serviceName), }; } @@ -429,6 +435,7 @@ export function buildAgentHostOTelEnv( } setIfMissing(AgentHostOTelEnvVars.ExporterType, settings.exporterType); setIfMissing(AgentHostOTelEnvVars.OtlpEndpoint, settings.otlpEndpoint); + setIfMissing(AgentHostOTelEnvVars.ServiceName, settings.serviceName); setIfMissing(AgentHostOTelEnvVars.FilePath, settings.outfile); if (settings.captureContent !== undefined) { setIfMissing(AgentHostOTelEnvVars.CaptureContent, settings.captureContent ? 'true' : 'false'); @@ -466,6 +473,9 @@ export function buildAgentHostOTelEnv( if (policySettings.captureContent !== undefined) { setPolicy(AgentHostOTelEnvVars.CaptureContent, policySettings.captureContent ? 'true' : 'false'); } + if (policySettings.serviceName !== undefined && policySettings.serviceName !== '') { + setPolicy(AgentHostOTelEnvVars.ServiceName, policySettings.serviceName); + } return out; } diff --git a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts index ffd79aab0c13b3..3de8c7a9fc9a55 100644 --- a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts +++ b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts @@ -19,7 +19,7 @@ import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js'; import { UtilityProcess } from '../../utilityProcess/electron-main/utilityProcess.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelPolicyIpcChannel, buildAgentHostOTelEnv, buildAgentSdkEnv, IAgentHostOTelSettings, sanitizeAgentHostOTelPolicySettings } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelServiceNameSettingId, AgentHostOTelPolicyIpcChannel, buildAgentHostOTelEnv, buildAgentSdkEnv, IAgentHostOTelSettings, sanitizeAgentHostOTelPolicySettings } from '../common/agentService.js'; import { deepClone } from '../../../base/common/objects.js'; import '../common/agentHost.config.contribution.js'; import '../common/agentHostStarter.config.contribution.js'; @@ -113,6 +113,7 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), + serviceName: policyValue(AgentHostOTelServiceNameSettingId), }; const otelEnv = buildAgentHostOTelEnv({ enabled: this._configurationService.getValue(AgentHostOTelEnabledSettingId), diff --git a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts index 2fad289e4d9a54..6fd460c78c25fe 100644 --- a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts +++ b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts @@ -13,7 +13,7 @@ import { parseAgentHostDebugPort } from '../../environment/node/environmentServi import { ILogService } from '../../log/common/log.js'; import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelServiceNameSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; import '../common/agentHostStarter.config.contribution.js'; /** @@ -105,6 +105,7 @@ export class NodeAgentHostStarter extends Disposable implements IAgentHostStarte otlpEndpoint: policyValue(AgentHostOTelOtlpEndpointSettingId), captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), + serviceName: policyValue(AgentHostOTelServiceNameSettingId), }); Object.assign(env, otelEnv); diff --git a/src/vs/platform/agentHost/test/common/agentService.test.ts b/src/vs/platform/agentHost/test/common/agentService.test.ts index 609ba0d6158e7d..5378c24e8cb920 100644 --- a/src/vs/platform/agentHost/test/common/agentService.test.ts +++ b/src/vs/platform/agentHost/test/common/agentService.test.ts @@ -103,6 +103,25 @@ suite('buildAgentHostOTelEnv', () => { assert.strictEqual(env[AgentHostOTelEnvVars.OtlpEndpoint], ''); assert.strictEqual(env[AgentHostOTelEnvVars.FilePath], ''); }); + + test('managed service name wins over inherited env', () => { + const env = buildAgentHostOTelEnv( + { serviceName: 'user-service' }, + { [AgentHostOTelEnvVars.ServiceName]: 'env-service' }, + { serviceName: 'enterprise-service' }, + ); + assert.strictEqual(env[AgentHostOTelEnvVars.ServiceName], 'enterprise-service'); + }); + + test('empty managed service name emits no override', () => { + const env = buildAgentHostOTelEnv( + {}, + { [AgentHostOTelEnvVars.ServiceName]: 'env-service' }, + { serviceName: '' }, + ); + // The builder returns only overrides; leaving the key out preserves the inherited env value. + assert.strictEqual(env[AgentHostOTelEnvVars.ServiceName], undefined); + }); }); suite('readAgentHostOTelPolicySettings', () => { @@ -123,6 +142,7 @@ suite('readAgentHostOTelPolicySettings', () => { 'chat.agentHost.otel.otlpEndpoint': 'http://localhost:4318', 'chat.agentHost.otel.captureContent': false, 'chat.agentHost.otel.outfile': '/tmp/o.jsonl', + 'chat.agentHost.otel.serviceName': 'my-service', }); assert.deepStrictEqual(readAgentHostOTelPolicySettings(cfg), { enabled: true, @@ -131,6 +151,7 @@ suite('readAgentHostOTelPolicySettings', () => { otlpEndpoint: 'http://localhost:4318', captureContent: false, outfile: '/tmp/o.jsonl', + serviceName: 'my-service', }); }); @@ -142,6 +163,7 @@ suite('readAgentHostOTelPolicySettings', () => { otlpEndpoint: undefined, captureContent: undefined, outfile: undefined, + serviceName: undefined, }); }); }); @@ -159,6 +181,7 @@ suite('sanitizeAgentHostOTelPolicySettings', () => { otlpEndpoint: 'http://localhost:4318', captureContent: false, outfile: '/tmp/o.jsonl', + serviceName: 'my-service', bogus: 123, }), { @@ -168,6 +191,7 @@ suite('sanitizeAgentHostOTelPolicySettings', () => { otlpEndpoint: 'http://localhost:4318', captureContent: false, outfile: '/tmp/o.jsonl', + serviceName: 'my-service', }, ); }); @@ -175,7 +199,7 @@ suite('sanitizeAgentHostOTelPolicySettings', () => { test('mistyped fields are dropped to undefined', () => { assert.deepStrictEqual( sanitizeAgentHostOTelPolicySettings({ enabled: 'yes', otlpEndpoint: 42, captureContent: 1 }), - { enabled: undefined, exporterType: undefined, otlpProtocol: undefined, otlpEndpoint: undefined, captureContent: undefined, outfile: undefined }, + { enabled: undefined, exporterType: undefined, otlpProtocol: undefined, otlpEndpoint: undefined, captureContent: undefined, outfile: undefined, serviceName: undefined }, ); }); diff --git a/src/vs/platform/policy/common/copilotManagedSettings.ts b/src/vs/platform/policy/common/copilotManagedSettings.ts index 1df41c267a50f9..2d7868aa004360 100644 --- a/src/vs/platform/policy/common/copilotManagedSettings.ts +++ b/src/vs/platform/policy/common/copilotManagedSettings.ts @@ -43,8 +43,8 @@ export const COPILOT_MODEL_KEY = 'model'; * `telemetry` block from the cross-client managed-settings schema (see the CLI * `ManagedTelemetrySettings`); they flatten to dot-path bag keys via * {@link normalizeManagedSettings}, so no {@link STRUCTURED_MANAGED_SETTINGS} entry is needed. - * The `telemetry.headers` / `telemetry.resourceAttributes` map fields are intentionally not - * carried yet — they require a dedicated owner setting plus structured-key handling. + * The `telemetry.headers` / `telemetry.resourceAttributes` map fields require structured-key + * handling (see {@link STRUCTURED_MANAGED_SETTINGS}); `telemetry.serviceName` is a scalar. */ /** Managed-settings key for enterprise OTel enablement. */ @@ -62,6 +62,9 @@ export const COPILOT_OTEL_CAPTURE_CONTENT_KEY = 'telemetry.captureContent'; /** Managed-settings key that prevents users from enabling OTel content capture themselves. */ export const COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY = 'telemetry.lockCaptureContent'; +/** Managed-settings key for the OTel `service.name` resource attribute. */ +export const COPILOT_OTEL_SERVICE_NAME_KEY = 'telemetry.serviceName'; + const managedSettingValueCallbacks = new Map ManagedSettingValue | undefined>(); /** diff --git a/src/vs/workbench/services/accounts/browser/managedSettings.ts b/src/vs/workbench/services/accounts/browser/managedSettings.ts index 8dcefc297df98c..3e5894c808964c 100644 --- a/src/vs/workbench/services/accounts/browser/managedSettings.ts +++ b/src/vs/workbench/services/accounts/browser/managedSettings.ts @@ -34,6 +34,7 @@ export interface IManagedSettingsResponse { readonly protocol?: 'grpc' | 'http/protobuf' | 'http/json'; readonly captureContent?: boolean; readonly lockCaptureContent?: boolean; + readonly serviceName?: string; }; /** Any unknown keys in the response are accepted for forward compatibility. */ readonly [key: string]: unknown; From fce8c4fea16d33bd847e1b37018b8929ff1185a8 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 15:54:59 -0700 Subject: [PATCH 17/26] feat: deliver managed OTel resourceAttributes to both surfaces --- build/lib/policies/policyData.jsonc | 18 +++++ extensions/copilot/package.json | 15 ++++ .../extension/vscode-node/services.ts | 2 + .../src/platform/otel/common/otelConfig.ts | 12 +++- .../otel/common/test/otelConfig.spec.ts | 17 +++++ .../agentHostStarter.config.contribution.ts | 29 +++++++- .../platform/agentHost/common/agentService.ts | 38 ++++++++++ .../electron-main/electronAgentHostStarter.ts | 3 +- .../agentHost/node/nodeAgentHostStarter.ts | 3 +- .../test/common/agentService.test.ts | 25 ++++++- .../policy/common/copilotManagedSettings.ts | 72 +++++++++++++++++-- .../accounts/browser/managedSettings.ts | 1 + .../test/browser/managedSettings.test.ts | 16 +++++ 13 files changed, 239 insertions(+), 12 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index dd7f0a79ae8d17..565d6b2be28a39 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -74,6 +74,24 @@ "github.copilot.chat.otel.serviceName" ] }, + { + "key": "chat.agentHost.otel.resourceAttributes", + "name": "CopilotOtelResourceAttributes", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.resourceAttributes.policy", + "value": "Controls the enterprise-managed OTel resource attributes for Copilot OpenTelemetry export." + } + }, + "type": "object", + "default": {}, + "included": false, + "referencedSettings": [ + "github.copilot.chat.otel.resourceAttributes" + ] + }, { "key": "mcp.enterpriseManagedAuth.idp", "name": "McpEnterpriseManagedAuthIdp", diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index a94d7a5fc66756..f9e39393710191 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -5356,6 +5356,21 @@ "advanced" ] }, + "github.copilot.chat.otel.resourceAttributes": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": {}, + "scope": "application", + "policyReference": { + "name": "CopilotOtelResourceAttributes" + }, + "markdownDescription": "Additional OTel resource attributes for Copilot Chat OTel data, as a `{ \"key\": \"value\" }` map. Configurable in user settings only. Merged per-key with `OTEL_RESOURCE_ATTRIBUTES` env (env wins over the setting); enterprise policy wins over both. Requires window reload.", + "tags": [ + "advanced" + ] + }, "github.copilot.chat.otel.maxAttributeSizeChars": { "type": "integer", "default": 0, diff --git a/extensions/copilot/src/extension/extension/vscode-node/services.ts b/extensions/copilot/src/extension/extension/vscode-node/services.ts index 0a9c4bd3963e5f..fd736cd1e25121 100644 --- a/extensions/copilot/src/extension/extension/vscode-node/services.ts +++ b/extensions/copilot/src/extension/extension/vscode-node/services.ts @@ -308,6 +308,8 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio policyProtocol: policyValue('protocol'), settingServiceName: otelSettings.get('serviceName') || undefined, policyServiceName: policyValue('serviceName'), + settingResourceAttributes: otelSettings.get>('resourceAttributes'), + policyResourceAttributes: policyValue>('resourceAttributes'), 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 6ffde6a1a0459b..057d0f93563026 100644 --- a/extensions/copilot/src/platform/otel/common/otelConfig.ts +++ b/extensions/copilot/src/platform/otel/common/otelConfig.ts @@ -100,6 +100,10 @@ export interface OTelConfigInput { settingServiceName?: string; /** Enterprise-managed `service.name` (raw `telemetry.serviceName`). */ policyServiceName?: string; + /** Resource attributes from VS Code setting (`github.copilot.chat.otel.resourceAttributes`). */ + settingResourceAttributes?: Record; + /** Enterprise-managed resource attributes (raw `telemetry.resourceAttributes`). */ + policyResourceAttributes?: Record; extensionVersion: string; sessionId: string; vscodeTelemetryLevel?: string; @@ -233,8 +237,12 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { ?? (input.settingServiceName || undefined) ?? 'copilot-chat'; - // Resource attributes - const resourceAttributes = parseResourceAttributes(env['OTEL_RESOURCE_ATTRIBUTES']); + // Resource attributes: merged per-key with precedence policy > env > setting. + const resourceAttributes = { + ...(input.settingResourceAttributes ?? {}), + ...parseResourceAttributes(env['OTEL_RESOURCE_ATTRIBUTES']), + ...(input.policyResourceAttributes ?? {}), + }; return Object.freeze({ enabled: true, 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 be6911ba3dd23a..4ef3cdf4ce5b8a 100644 --- a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts @@ -114,6 +114,23 @@ describe('resolveOTelConfig', () => { }); }); + it('merges resource attributes with precedence policy > env > setting', () => { + const config = resolveOTelConfig(makeInput({ + settingResourceAttributes: { fromSetting: 'setting', shared: 'setting' }, + policyResourceAttributes: { fromPolicy: 'policy', shared: 'policy' }, + env: { + 'COPILOT_OTEL_ENABLED': 'true', + 'OTEL_RESOURCE_ATTRIBUTES': 'fromEnv=env,shared=env', + }, + })); + expect(config.resourceAttributes).toEqual({ + fromSetting: 'setting', + fromEnv: 'env', + fromPolicy: 'policy', + shared: 'policy', + }); + }); + it('uses grpc protocol when OTEL_EXPORTER_OTLP_PROTOCOL=grpc', () => { const config = resolveOTelConfig(makeInput({ env: { diff --git a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts index f038f7041e7931..bab7c67d36464d 100644 --- a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts @@ -7,7 +7,7 @@ import * as nls from '../../../nls.js'; import { IPolicyData } from '../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../base/common/policy.js'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; -import { COPILOT_OTEL_CAPTURE_CONTENT_KEY, COPILOT_OTEL_ENABLED_KEY, COPILOT_OTEL_ENDPOINT_KEY, COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY, COPILOT_OTEL_PROTOCOL_KEY, COPILOT_OTEL_SERVICE_NAME_KEY, managedSettingValue } from '../../policy/common/copilotManagedSettings.js'; +import { COPILOT_OTEL_CAPTURE_CONTENT_KEY, COPILOT_OTEL_ENABLED_KEY, COPILOT_OTEL_ENDPOINT_KEY, COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY, COPILOT_OTEL_PROTOCOL_KEY, COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY, COPILOT_OTEL_SERVICE_NAME_KEY, managedSettingValue } from '../../policy/common/copilotManagedSettings.js'; import product from '../../product/common/product.js'; import { Registry } from '../../registry/common/platform.js'; import { @@ -23,6 +23,7 @@ import { AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, + AgentHostOTelResourceAttributesSettingId, AgentHostOTelServiceNameSettingId, } from './agentService.js'; @@ -336,5 +337,31 @@ configurationRegistry.registerConfiguration({ }, }, }, + [AgentHostOTelResourceAttributesSettingId]: { + // Policy-only delivery slot — no user-writable surface (mirrors `chat.plugins.extraMarketplaces`). + // Carried as a `{ [key]: string }` object; the starters serialize it into `OTEL_RESOURCE_ATTRIBUTES`. + type: 'object', + additionalProperties: { type: ['string'] as ['string'] }, + default: {}, + scope: ConfigurationScope.APPLICATION, + included: false, + tags: ['experimental', 'advanced'], + markdownDescription: nls.localize('chat.agentHost.otel.resourceAttributes', "Enterprise-managed OTel resource attributes for Copilot OpenTelemetry export. Policy-only: there is no user-facing setting; it carries the managed `telemetry.resourceAttributes` map so the agent host's `OTEL_RESOURCE_ATTRIBUTES` includes the deployment's attributes."), + policy: { + name: 'CopilotOtelResourceAttributes', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedSettingValue(COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY), + managedSettings: { + [COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY]: { type: 'string' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.resourceAttributes.policy', + value: nls.localize('chat.agentHost.otel.resourceAttributes.policy', "Controls the enterprise-managed OTel resource attributes for Copilot OpenTelemetry export."), + } + }, + }, + }, } }); diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 2403970763e48d..92a81d9ed85aa2 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -297,6 +297,8 @@ export const AgentHostOTelCaptureContentSettingId = 'chat.agentHost.otel.capture export const AgentHostOTelOutfileSettingId = 'chat.agentHost.otel.outfile'; /** Policy-only delivery slot for the enterprise-managed OTel `service.name` (no user UI). */ export const AgentHostOTelServiceNameSettingId = 'chat.agentHost.otel.serviceName'; +/** Policy-only delivery slot for enterprise-managed OTel resource attributes (no user UI). */ +export const AgentHostOTelResourceAttributesSettingId = 'chat.agentHost.otel.resourceAttributes'; /** When true, ALL spans are persisted to a local SQLite store regardless of `exporterType`. */ export const AgentHostOTelDbSpanExporterEnabledSettingId = 'chat.agentHost.otel.dbSpanExporter.enabled'; @@ -330,6 +332,7 @@ export const AgentHostOTelEnvVars = Object.freeze({ FilePath: 'COPILOT_OTEL_FILE_EXPORTER_PATH', SourceName: 'COPILOT_OTEL_SOURCE_NAME', ServiceName: 'OTEL_SERVICE_NAME', + ResourceAttributes: 'OTEL_RESOURCE_ATTRIBUTES', DbSpanExporterEnabled: 'COPILOT_OTEL_DB_SPAN_EXPORTER_ENABLED', } as const); @@ -345,6 +348,7 @@ export interface IAgentHostOTelSettings { readonly captureContent?: boolean; readonly outfile?: string; readonly serviceName?: string; + readonly resourceAttributes?: Record; readonly dbSpanExporterEnabled?: boolean; } @@ -377,6 +381,7 @@ export function readAgentHostOTelPolicySettings(configurationService: IConfigura captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), serviceName: policyValue(AgentHostOTelServiceNameSettingId), + resourceAttributes: policyValue>(AgentHostOTelResourceAttributesSettingId), }; } @@ -392,6 +397,18 @@ export function sanitizeAgentHostOTelPolicySettings(raw: unknown): IAgentHostOTe const record = raw as Record; const asString = (value: unknown): string | undefined => typeof value === 'string' ? value : undefined; const asBoolean = (value: unknown): boolean | undefined => typeof value === 'boolean' ? value : undefined; + const asStringRecord = (value: unknown): Record | undefined => { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return undefined; + } + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + if (typeof v === 'string') { + out[k] = v; + } + } + return out; + }; return { enabled: asBoolean(record.enabled), exporterType: asString(record.exporterType), @@ -400,9 +417,25 @@ export function sanitizeAgentHostOTelPolicySettings(raw: unknown): IAgentHostOTe captureContent: asBoolean(record.captureContent), outfile: asString(record.outfile), serviceName: asString(record.serviceName), + resourceAttributes: asStringRecord(record.resourceAttributes), }; } +/** + * Serialize an OTel resource-attribute map into the `OTEL_RESOURCE_ATTRIBUTES` env-var format + * (`key1=value1,key2=value2`, W3C Baggage style). Returns `undefined` for an empty/absent map so + * callers can skip emitting the env var. Empty keys and non-string values are dropped. + */ +function serializeResourceAttributes(attributes: Record | undefined): string | undefined { + if (!attributes) { + return undefined; + } + const parts = Object.entries(attributes) + .filter(([key, value]) => key !== '' && typeof value === 'string') + .map(([key, value]) => `${key}=${value}`); + return parts.length > 0 ? parts.join(',') : undefined; +} + /** * Build the env-var overlay for the agent host process from user settings and * inherited env. Settings are translated to env vars, but if the same env var is @@ -436,6 +469,7 @@ export function buildAgentHostOTelEnv( setIfMissing(AgentHostOTelEnvVars.ExporterType, settings.exporterType); setIfMissing(AgentHostOTelEnvVars.OtlpEndpoint, settings.otlpEndpoint); setIfMissing(AgentHostOTelEnvVars.ServiceName, settings.serviceName); + setIfMissing(AgentHostOTelEnvVars.ResourceAttributes, serializeResourceAttributes(settings.resourceAttributes)); setIfMissing(AgentHostOTelEnvVars.FilePath, settings.outfile); if (settings.captureContent !== undefined) { setIfMissing(AgentHostOTelEnvVars.CaptureContent, settings.captureContent ? 'true' : 'false'); @@ -476,6 +510,10 @@ export function buildAgentHostOTelEnv( if (policySettings.serviceName !== undefined && policySettings.serviceName !== '') { setPolicy(AgentHostOTelEnvVars.ServiceName, policySettings.serviceName); } + const policyResourceAttributes = serializeResourceAttributes(policySettings.resourceAttributes); + if (policyResourceAttributes !== undefined) { + setPolicy(AgentHostOTelEnvVars.ResourceAttributes, policyResourceAttributes); + } return out; } diff --git a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts index 3de8c7a9fc9a55..d633d2d9041d04 100644 --- a/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts +++ b/src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts @@ -19,7 +19,7 @@ import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js'; import { UtilityProcess } from '../../utilityProcess/electron-main/utilityProcess.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelServiceNameSettingId, AgentHostOTelPolicyIpcChannel, buildAgentHostOTelEnv, buildAgentSdkEnv, IAgentHostOTelSettings, sanitizeAgentHostOTelPolicySettings } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelResourceAttributesSettingId, AgentHostOTelServiceNameSettingId, AgentHostOTelPolicyIpcChannel, buildAgentHostOTelEnv, buildAgentSdkEnv, IAgentHostOTelSettings, sanitizeAgentHostOTelPolicySettings } from '../common/agentService.js'; import { deepClone } from '../../../base/common/objects.js'; import '../common/agentHost.config.contribution.js'; import '../common/agentHostStarter.config.contribution.js'; @@ -114,6 +114,7 @@ export class ElectronAgentHostStarter extends Disposable implements IAgentHostSt captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), serviceName: policyValue(AgentHostOTelServiceNameSettingId), + resourceAttributes: policyValue>(AgentHostOTelResourceAttributesSettingId), }; const otelEnv = buildAgentHostOTelEnv({ enabled: this._configurationService.getValue(AgentHostOTelEnabledSettingId), diff --git a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts index 6fd460c78c25fe..a145f36451b5a0 100644 --- a/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts +++ b/src/vs/platform/agentHost/node/nodeAgentHostStarter.ts @@ -13,7 +13,7 @@ import { parseAgentHostDebugPort } from '../../environment/node/environmentServi import { ILogService } from '../../log/common/log.js'; import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; import { IAgentHostConnection, IAgentHostStarter } from '../common/agent.js'; -import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelServiceNameSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; +import { AgentHostClaudeAgentEnabledSettingId, AgentHostCodexAgentBinaryArgsSettingId, AgentHostCodexAgentEnabledSettingId, AgentHostCodexAgentSdkRootSettingId, AgentHostCodexAgentCodexHomeSettingId, AgentHostOTelCaptureContentSettingId, AgentHostOTelDbSpanExporterEnabledSettingId, AgentHostOTelEnabledSettingId, AgentHostOTelExporterTypeSettingId, AgentHostOTelOtlpEndpointSettingId, AgentHostOTelOtlpProtocolSettingId, AgentHostOTelOutfileSettingId, AgentHostOTelResourceAttributesSettingId, AgentHostOTelServiceNameSettingId, buildAgentHostOTelEnv, buildAgentSdkEnv } from '../common/agentService.js'; import '../common/agentHostStarter.config.contribution.js'; /** @@ -106,6 +106,7 @@ export class NodeAgentHostStarter extends Disposable implements IAgentHostStarte captureContent: policyValue(AgentHostOTelCaptureContentSettingId), outfile: policyValue(AgentHostOTelOutfileSettingId), serviceName: policyValue(AgentHostOTelServiceNameSettingId), + resourceAttributes: policyValue>(AgentHostOTelResourceAttributesSettingId), }); Object.assign(env, otelEnv); diff --git a/src/vs/platform/agentHost/test/common/agentService.test.ts b/src/vs/platform/agentHost/test/common/agentService.test.ts index 5378c24e8cb920..54ae6218bb3fb2 100644 --- a/src/vs/platform/agentHost/test/common/agentService.test.ts +++ b/src/vs/platform/agentHost/test/common/agentService.test.ts @@ -122,6 +122,24 @@ suite('buildAgentHostOTelEnv', () => { // The builder returns only overrides; leaving the key out preserves the inherited env value. assert.strictEqual(env[AgentHostOTelEnvVars.ServiceName], undefined); }); + + test('managed resource attributes serialize into OTEL_RESOURCE_ATTRIBUTES', () => { + const env = buildAgentHostOTelEnv( + {}, + { [AgentHostOTelEnvVars.ResourceAttributes]: 'service.namespace=env' }, + { resourceAttributes: { 'deployment.environment': 'prod', 'service.namespace': 'acme' } }, + ); + assert.strictEqual(env[AgentHostOTelEnvVars.ResourceAttributes], 'deployment.environment=prod,service.namespace=acme'); + }); + + test('empty managed resource attributes emit no override', () => { + const env = buildAgentHostOTelEnv( + {}, + { [AgentHostOTelEnvVars.ResourceAttributes]: 'service.namespace=env' }, + { resourceAttributes: {} }, + ); + assert.strictEqual(env[AgentHostOTelEnvVars.ResourceAttributes], undefined); + }); }); suite('readAgentHostOTelPolicySettings', () => { @@ -143,6 +161,7 @@ suite('readAgentHostOTelPolicySettings', () => { 'chat.agentHost.otel.captureContent': false, 'chat.agentHost.otel.outfile': '/tmp/o.jsonl', 'chat.agentHost.otel.serviceName': 'my-service', + 'chat.agentHost.otel.resourceAttributes': { 'service.namespace': 'acme' }, }); assert.deepStrictEqual(readAgentHostOTelPolicySettings(cfg), { enabled: true, @@ -152,6 +171,7 @@ suite('readAgentHostOTelPolicySettings', () => { captureContent: false, outfile: '/tmp/o.jsonl', serviceName: 'my-service', + resourceAttributes: { 'service.namespace': 'acme' }, }); }); @@ -164,6 +184,7 @@ suite('readAgentHostOTelPolicySettings', () => { captureContent: undefined, outfile: undefined, serviceName: undefined, + resourceAttributes: undefined, }); }); }); @@ -182,6 +203,7 @@ suite('sanitizeAgentHostOTelPolicySettings', () => { captureContent: false, outfile: '/tmp/o.jsonl', serviceName: 'my-service', + resourceAttributes: { 'service.namespace': 'acme', dropped: 7 }, bogus: 123, }), { @@ -192,6 +214,7 @@ suite('sanitizeAgentHostOTelPolicySettings', () => { captureContent: false, outfile: '/tmp/o.jsonl', serviceName: 'my-service', + resourceAttributes: { 'service.namespace': 'acme' }, }, ); }); @@ -199,7 +222,7 @@ suite('sanitizeAgentHostOTelPolicySettings', () => { test('mistyped fields are dropped to undefined', () => { assert.deepStrictEqual( sanitizeAgentHostOTelPolicySettings({ enabled: 'yes', otlpEndpoint: 42, captureContent: 1 }), - { enabled: undefined, exporterType: undefined, otlpProtocol: undefined, otlpEndpoint: undefined, captureContent: undefined, outfile: undefined, serviceName: undefined }, + { enabled: undefined, exporterType: undefined, otlpProtocol: undefined, otlpEndpoint: undefined, captureContent: undefined, outfile: undefined, serviceName: undefined, resourceAttributes: undefined }, ); }); diff --git a/src/vs/platform/policy/common/copilotManagedSettings.ts b/src/vs/platform/policy/common/copilotManagedSettings.ts index 2d7868aa004360..47d8921b9a0e4e 100644 --- a/src/vs/platform/policy/common/copilotManagedSettings.ts +++ b/src/vs/platform/policy/common/copilotManagedSettings.ts @@ -43,8 +43,8 @@ export const COPILOT_MODEL_KEY = 'model'; * `telemetry` block from the cross-client managed-settings schema (see the CLI * `ManagedTelemetrySettings`); they flatten to dot-path bag keys via * {@link normalizeManagedSettings}, so no {@link STRUCTURED_MANAGED_SETTINGS} entry is needed. - * The `telemetry.headers` / `telemetry.resourceAttributes` map fields require structured-key - * handling (see {@link STRUCTURED_MANAGED_SETTINGS}); `telemetry.serviceName` is a scalar. + * The `telemetry.resourceAttributes` map field is structured (a {@link STRUCTURED_MANAGED_SETTINGS} + * row carries it as a JSON-encoded object under a nested key); `telemetry.serviceName` is a scalar. */ /** Managed-settings key for enterprise OTel enablement. */ @@ -65,6 +65,9 @@ export const COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY = 'telemetry.lockCaptureConte /** Managed-settings key for the OTel `service.name` resource attribute. */ export const COPILOT_OTEL_SERVICE_NAME_KEY = 'telemetry.serviceName'; +/** Managed-settings key for additional OTel resource attributes (a `{ [k]: string }` map). */ +export const COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY = 'telemetry.resourceAttributes'; + const managedSettingValueCallbacks = new Map ManagedSettingValue | undefined>(); /** @@ -284,8 +287,64 @@ const STRUCTURED_MANAGED_SETTINGS: readonly IStructuredManagedSetting[] = [ key: COPILOT_EXTRA_MARKETPLACES_KEY, encode: (value, onWarn) => extraKnownMarketplacesToConfigDict(normalizeExtraKnownMarketplaces(value, onWarn)), }, + { + // Nested under `telemetry`; carried as a JSON-encoded `{ [k]: string }` map. Non-string + // primitive values are coerced to strings; non-primitive values are dropped. + key: COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY, + encode: value => { + if (!isObject(value)) { + return undefined; + } + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + if (isString(v)) { + out[k] = v; + } else if (typeof v === 'number' || typeof v === 'boolean') { + out[k] = String(v); + } + } + return out; + }, + }, ]; +/** + * Read a (possibly nested) dot-separated key from a parsed managed-settings object, e.g. + * `telemetry.resourceAttributes`. Returns `undefined` if any path segment is missing or not an + * object. Single-segment keys behave like a plain property read. + */ +function readNestedManagedKey(obj: Record, dottedKey: string): unknown { + let current: unknown = obj; + for (const segment of dottedKey.split('.')) { + if (!isObject(current)) { + return undefined; + } + current = (current as Record)[segment]; + } + return current; +} + +/** + * Return a copy of `obj` with the (possibly nested) dot-separated key removed, cloning only the + * objects along the touched path so the original (and any shared sub-objects) stay untouched. The + * spread-then-`delete` shape matches a destructuring rest: it copies own enumerable keys (including + * an own `__proto__`) without triggering the inherited `__proto__` setter. + */ +function withNestedManagedKeyDeleted(obj: Record, dottedKey: string): Record { + const dot = dottedKey.indexOf('.'); + if (dot === -1) { + const clone = { ...obj }; + delete clone[dottedKey]; + return clone; + } + const head = dottedKey.slice(0, dot); + const child = obj[head]; + if (!isObject(child)) { + return obj; + } + return { ...obj, [head]: withNestedManagedKeyDeleted(child as Record, dottedKey.slice(dot + 1)) }; +} + /** * Normalize a parsed managed-settings object (from the server `managed_settings` API, a file on * disk, or any other source using the managed-settings schema) into the canonical @@ -309,16 +368,17 @@ const STRUCTURED_MANAGED_SETTINGS: readonly IStructuredManagedSetting[] = [ export function normalizeManagedSettings(parsed: Record, onWarn?: (msg: string) => void): ManagedSettingsData { // Spread + delete (not for..in + assignment) so the scalar remainder keeps exact `{ ...rest }` // semantics: it never triggers the inherited `__proto__` setter for a source-sent own - // `__proto__` key, matching a destructuring rest. - const scalarRest: Record = { ...parsed }; + // `__proto__` key, matching a destructuring rest. Structured keys may be nested (e.g. + // `telemetry.resourceAttributes`), so removal clones only the touched path. + let scalarRest: Record = { ...parsed }; for (const setting of STRUCTURED_MANAGED_SETTINGS) { - delete scalarRest[setting.key]; + scalarRest = withNestedManagedKeyDeleted(scalarRest, setting.key); } const result: Record = { ...flattenManagedSettings(scalarRest) }; for (const setting of STRUCTURED_MANAGED_SETTINGS) { - const encoded = setting.encode(parsed[setting.key], onWarn); + const encoded = setting.encode(readNestedManagedKey(parsed, setting.key), onWarn); if (encoded !== undefined) { result[setting.key] = JSON.stringify(encoded); } diff --git a/src/vs/workbench/services/accounts/browser/managedSettings.ts b/src/vs/workbench/services/accounts/browser/managedSettings.ts index 3e5894c808964c..3c842f92fbba4f 100644 --- a/src/vs/workbench/services/accounts/browser/managedSettings.ts +++ b/src/vs/workbench/services/accounts/browser/managedSettings.ts @@ -35,6 +35,7 @@ export interface IManagedSettingsResponse { readonly captureContent?: boolean; readonly lockCaptureContent?: boolean; readonly serviceName?: string; + readonly resourceAttributes?: Record; }; /** Any unknown keys in the response are accepted for forward compatibility. */ readonly [key: string]: unknown; diff --git a/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts b/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts index e3f70c93498ecd..11e1aac7d0c944 100644 --- a/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts +++ b/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts @@ -57,6 +57,22 @@ suite('adaptManagedSettings', () => { }); }); + test('flattens scalar telemetry leaves and carries resourceAttributes as a single JSON key', () => { + assert.deepStrictEqual(adaptManagedSettings({ + telemetry: { + enabled: true, + serviceName: 'acme-copilot', + resourceAttributes: { 'deployment.environment': 'prod', 'service.namespace': 'acme' }, + }, + }), { + managedSettings: { + 'telemetry.enabled': true, + 'telemetry.serviceName': 'acme-copilot', + 'telemetry.resourceAttributes': '{"deployment.environment":"prod","service.namespace":"acme"}', + }, + }); + }); + test('encodes github marketplaces as a { name: shorthand } JSON dict', () => { assert.deepStrictEqual(adaptManagedSettings({ extraKnownMarketplaces: { From bb524011b6d5727819d7798d0688cf2551a3811a Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 16:02:53 -0700 Subject: [PATCH 18/26] feat: deliver managed OTel headers to the Copilot Chat extension --- build/lib/policies/policyData.jsonc | 18 +++++++ extensions/copilot/package.json | 15 ++++++ .../extension/vscode-node/services.ts | 2 + .../src/platform/otel/common/otelConfig.ts | 19 ++++++++ .../otel/common/test/otelConfig.spec.ts | 17 +++++++ .../src/platform/otel/node/otelServiceImpl.ts | 14 +++--- .../agentHostStarter.config.contribution.ts | 30 +++++++++++- .../policy/common/copilotManagedSettings.ts | 48 ++++++++++++------- .../accounts/browser/managedSettings.ts | 1 + .../test/browser/managedSettings.test.ts | 2 + 10 files changed, 142 insertions(+), 24 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index 565d6b2be28a39..faf419d71a3700 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -92,6 +92,24 @@ "github.copilot.chat.otel.resourceAttributes" ] }, + { + "key": "chat.agentHost.otel.headers", + "name": "CopilotOtelHeaders", + "category": "InteractiveSession", + "minimumVersion": "1.127", + "localization": { + "description": { + "key": "chat.agentHost.otel.headers.policy", + "value": "Controls the enterprise-managed OTLP exporter headers for Copilot OpenTelemetry export." + } + }, + "type": "object", + "default": {}, + "included": false, + "referencedSettings": [ + "github.copilot.chat.otel.headers" + ] + }, { "key": "mcp.enterpriseManagedAuth.idp", "name": "McpEnterpriseManagedAuthIdp", diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index f9e39393710191..92f21eaaba320a 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -5371,6 +5371,21 @@ "advanced" ] }, + "github.copilot.chat.otel.headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": {}, + "scope": "application", + "policyReference": { + "name": "CopilotOtelHeaders" + }, + "markdownDescription": "Extra OTLP exporter headers (e.g. auth tokens) for Copilot Chat OTel data, as a `{ \"key\": \"value\" }` map. Applied directly to the OTLP exporter, not via environment variables. Configurable in user settings only. Merged per-key with `OTEL_EXPORTER_OTLP_HEADERS` env (env wins over the setting); enterprise policy wins over both. **Contains potentially sensitive credentials.** Requires window reload.", + "tags": [ + "advanced" + ] + }, "github.copilot.chat.otel.maxAttributeSizeChars": { "type": "integer", "default": 0, diff --git a/extensions/copilot/src/extension/extension/vscode-node/services.ts b/extensions/copilot/src/extension/extension/vscode-node/services.ts index fd736cd1e25121..15ca799eec7c00 100644 --- a/extensions/copilot/src/extension/extension/vscode-node/services.ts +++ b/extensions/copilot/src/extension/extension/vscode-node/services.ts @@ -310,6 +310,8 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio policyServiceName: policyValue('serviceName'), settingResourceAttributes: otelSettings.get>('resourceAttributes'), policyResourceAttributes: policyValue>('resourceAttributes'), + settingHeaders: otelSettings.get>('headers'), + policyHeaders: policyValue>('headers'), 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 057d0f93563026..43961d0e744b40 100644 --- a/extensions/copilot/src/platform/otel/common/otelConfig.ts +++ b/extensions/copilot/src/platform/otel/common/otelConfig.ts @@ -37,6 +37,11 @@ export interface OTelConfig { readonly serviceVersion: string; readonly sessionId: string; readonly resourceAttributes: Record; + /** + * Extra OTLP request headers (e.g. auth tokens) applied directly to the exporter. Carried + * out-of-band from process env so secrets never leak into spawned tool subprocesses. + */ + readonly headers: Record; } /** @@ -104,6 +109,10 @@ export interface OTelConfigInput { settingResourceAttributes?: Record; /** Enterprise-managed resource attributes (raw `telemetry.resourceAttributes`). */ policyResourceAttributes?: Record; + /** OTLP headers from VS Code setting (`github.copilot.chat.otel.headers`). */ + settingHeaders?: Record; + /** Enterprise-managed OTLP headers (raw `telemetry.headers`). */ + policyHeaders?: Record; extensionVersion: string; sessionId: string; vscodeTelemetryLevel?: string; @@ -244,6 +253,14 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { ...(input.policyResourceAttributes ?? {}), }; + // OTLP headers: merged per-key with precedence policy > env > setting. Same `k=v,k2=v2` format + // as resource attributes (`OTEL_EXPORTER_OTLP_HEADERS`). + const headers = { + ...(input.settingHeaders ?? {}), + ...parseResourceAttributes(env['OTEL_EXPORTER_OTLP_HEADERS']), + ...(input.policyHeaders ?? {}), + }; + return Object.freeze({ enabled: true, enabledExplicitly, @@ -261,6 +278,7 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { serviceVersion: input.extensionVersion, sessionId: input.sessionId, resourceAttributes, + headers, }); } @@ -281,6 +299,7 @@ function createDisabledConfig(input: OTelConfigInput): OTelConfig { serviceVersion: input.extensionVersion, sessionId: input.sessionId, resourceAttributes: {}, + headers: {}, }); } 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 4ef3cdf4ce5b8a..9031fd00d97702 100644 --- a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts @@ -131,6 +131,23 @@ describe('resolveOTelConfig', () => { }); }); + it('merges OTLP headers with precedence policy > env > setting', () => { + const config = resolveOTelConfig(makeInput({ + settingHeaders: { fromSetting: 'setting', shared: 'setting' }, + policyHeaders: { fromPolicy: 'policy', shared: 'policy' }, + env: { + 'COPILOT_OTEL_ENABLED': 'true', + 'OTEL_EXPORTER_OTLP_HEADERS': 'fromEnv=env,shared=env', + }, + })); + expect(config.headers).toEqual({ + fromSetting: 'setting', + fromEnv: 'env', + fromPolicy: 'policy', + shared: 'policy', + }); + }); + it('uses grpc protocol when OTEL_EXPORTER_OTLP_PROTOCOL=grpc', () => { const config = resolveOTelConfig(makeInput({ env: { diff --git a/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts b/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts index 9530260f4adaf9..d6272b5d8bbb83 100644 --- a/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts +++ b/extensions/copilot/src/platform/otel/node/otelServiceImpl.ts @@ -241,7 +241,7 @@ export class NodeOTelService implements IOTelService { import('@opentelemetry/exporter-logs-otlp-grpc'), import('@opentelemetry/exporter-metrics-otlp-grpc'), ]); - const opts = { url: config.otlpEndpoint }; + const opts = { url: config.otlpEndpoint, headers: config.headers }; return { spanExporter: new OTLPTraceExporter(opts), logExporter: new OTLPLogExporter(opts), @@ -262,9 +262,9 @@ export class NodeOTelService implements IOTelService { ]); const base = config.otlpEndpoint.replace(/\/$/, ''); return { - spanExporter: new OTLPTraceExporter({ url: `${base}/v1/traces` }), - logExporter: new OTLPLogExporter({ url: `${base}/v1/logs` }), - metricExporter: new OTLPMetricExporter({ url: `${base}/v1/metrics` }), + spanExporter: new OTLPTraceExporter({ url: `${base}/v1/traces`, headers: config.headers }), + logExporter: new OTLPLogExporter({ url: `${base}/v1/logs`, headers: config.headers }), + metricExporter: new OTLPMetricExporter({ url: `${base}/v1/metrics`, headers: config.headers }), }; } @@ -289,9 +289,9 @@ export class NodeOTelService implements IOTelService { ]); const base = config.otlpEndpoint.replace(/\/$/, ''); return { - spanExporter: new OTLPTraceExporter({ url: `${base}/v1/traces` }), - logExporter: new OTLPLogExporter({ url: `${base}/v1/logs` }), - metricExporter: new OTLPMetricExporter({ url: `${base}/v1/metrics` }), + spanExporter: new OTLPTraceExporter({ url: `${base}/v1/traces`, headers: config.headers }), + logExporter: new OTLPLogExporter({ url: `${base}/v1/logs`, headers: config.headers }), + metricExporter: new OTLPMetricExporter({ url: `${base}/v1/metrics`, headers: config.headers }), }; } diff --git a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts index bab7c67d36464d..0beea5266a08e0 100644 --- a/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts +++ b/src/vs/platform/agentHost/common/agentHostStarter.config.contribution.ts @@ -7,7 +7,7 @@ import * as nls from '../../../nls.js'; import { IPolicyData } from '../../../base/common/defaultAccount.js'; import { PolicyCategory } from '../../../base/common/policy.js'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; -import { COPILOT_OTEL_CAPTURE_CONTENT_KEY, COPILOT_OTEL_ENABLED_KEY, COPILOT_OTEL_ENDPOINT_KEY, COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY, COPILOT_OTEL_PROTOCOL_KEY, COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY, COPILOT_OTEL_SERVICE_NAME_KEY, managedSettingValue } from '../../policy/common/copilotManagedSettings.js'; +import { COPILOT_OTEL_CAPTURE_CONTENT_KEY, COPILOT_OTEL_ENABLED_KEY, COPILOT_OTEL_ENDPOINT_KEY, COPILOT_OTEL_HEADERS_KEY, COPILOT_OTEL_LOCK_CAPTURE_CONTENT_KEY, COPILOT_OTEL_PROTOCOL_KEY, COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY, COPILOT_OTEL_SERVICE_NAME_KEY, managedSettingValue } from '../../policy/common/copilotManagedSettings.js'; import product from '../../product/common/product.js'; import { Registry } from '../../registry/common/platform.js'; import { @@ -363,5 +363,33 @@ configurationRegistry.registerConfiguration({ }, }, }, + // Extension-only policy delivery slot for managed OTLP exporter headers (e.g. auth tokens). + // Deliberately NOT delivered to the agent host: headers would have to travel via env vars, + // which the agent host leaks into the tool subprocesses it spawns, exposing the secret. The + // Copilot Chat extension applies these headers directly to its OTLP exporter instead. + ['chat.agentHost.otel.headers']: { + type: 'object', + additionalProperties: { type: ['string'] as ['string'] }, + default: {}, + scope: ConfigurationScope.APPLICATION, + included: false, + tags: ['experimental', 'advanced'], + markdownDescription: nls.localize('chat.agentHost.otel.headers', "Enterprise-managed OTLP exporter headers (e.g. auth tokens) for Copilot OpenTelemetry export. Policy-only and extension-only: applied directly to the Copilot Chat extension's OTLP exporter, never delivered to the agent host process."), + policy: { + name: 'CopilotOtelHeaders', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.127', + value: managedSettingValue(COPILOT_OTEL_HEADERS_KEY), + managedSettings: { + [COPILOT_OTEL_HEADERS_KEY]: { type: 'string' }, + }, + localization: { + description: { + key: 'chat.agentHost.otel.headers.policy', + value: nls.localize('chat.agentHost.otel.headers.policy', "Controls the enterprise-managed OTLP exporter headers for Copilot OpenTelemetry export."), + } + }, + }, + }, } }); diff --git a/src/vs/platform/policy/common/copilotManagedSettings.ts b/src/vs/platform/policy/common/copilotManagedSettings.ts index 47d8921b9a0e4e..381c4308e44e28 100644 --- a/src/vs/platform/policy/common/copilotManagedSettings.ts +++ b/src/vs/platform/policy/common/copilotManagedSettings.ts @@ -43,8 +43,9 @@ export const COPILOT_MODEL_KEY = 'model'; * `telemetry` block from the cross-client managed-settings schema (see the CLI * `ManagedTelemetrySettings`); they flatten to dot-path bag keys via * {@link normalizeManagedSettings}, so no {@link STRUCTURED_MANAGED_SETTINGS} entry is needed. - * The `telemetry.resourceAttributes` map field is structured (a {@link STRUCTURED_MANAGED_SETTINGS} - * row carries it as a JSON-encoded object under a nested key); `telemetry.serviceName` is a scalar. + * The `telemetry.resourceAttributes` and `telemetry.headers` map fields are structured + * ({@link STRUCTURED_MANAGED_SETTINGS} rows carry them as JSON-encoded objects under their nested + * keys); `telemetry.serviceName` is a scalar. */ /** Managed-settings key for enterprise OTel enablement. */ @@ -68,6 +69,9 @@ export const COPILOT_OTEL_SERVICE_NAME_KEY = 'telemetry.serviceName'; /** Managed-settings key for additional OTel resource attributes (a `{ [k]: string }` map). */ export const COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY = 'telemetry.resourceAttributes'; +/** Managed-settings key for extra OTLP exporter headers (a `{ [k]: string }` map). */ +export const COPILOT_OTEL_HEADERS_KEY = 'telemetry.headers'; + const managedSettingValueCallbacks = new Map ManagedSettingValue | undefined>(); /** @@ -274,6 +278,26 @@ interface IStructuredManagedSetting { readonly encode: (value: unknown, onWarn?: (msg: string) => void) => unknown; } +/** + * Encode a managed-settings value into a canonical `{ [k]: string }` map: keeps string values + * as-is and coerces number/boolean values to strings; drops keys with non-primitive values. + * Returns `undefined` for a non-object input so the structured key is omitted. + */ +function encodeStringMap(value: unknown): Record | undefined { + if (!isObject(value)) { + return undefined; + } + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + if (isString(v)) { + out[k] = v; + } else if (typeof v === 'number' || typeof v === 'boolean') { + out[k] = String(v); + } + } + return out; +} + const STRUCTURED_MANAGED_SETTINGS: readonly IStructuredManagedSetting[] = [ { key: COPILOT_ENABLED_PLUGINS_KEY, @@ -291,20 +315,12 @@ const STRUCTURED_MANAGED_SETTINGS: readonly IStructuredManagedSetting[] = [ // Nested under `telemetry`; carried as a JSON-encoded `{ [k]: string }` map. Non-string // primitive values are coerced to strings; non-primitive values are dropped. key: COPILOT_OTEL_RESOURCE_ATTRIBUTES_KEY, - encode: value => { - if (!isObject(value)) { - return undefined; - } - const out: Record = {}; - for (const [k, v] of Object.entries(value)) { - if (isString(v)) { - out[k] = v; - } else if (typeof v === 'number' || typeof v === 'boolean') { - out[k] = String(v); - } - } - return out; - }, + encode: encodeStringMap, + }, + { + // Nested under `telemetry`; carried as a JSON-encoded `{ [k]: string }` map of OTLP headers. + key: COPILOT_OTEL_HEADERS_KEY, + encode: encodeStringMap, }, ]; diff --git a/src/vs/workbench/services/accounts/browser/managedSettings.ts b/src/vs/workbench/services/accounts/browser/managedSettings.ts index 3c842f92fbba4f..5a9d276e0c69d5 100644 --- a/src/vs/workbench/services/accounts/browser/managedSettings.ts +++ b/src/vs/workbench/services/accounts/browser/managedSettings.ts @@ -36,6 +36,7 @@ export interface IManagedSettingsResponse { readonly lockCaptureContent?: boolean; readonly serviceName?: string; readonly resourceAttributes?: Record; + readonly headers?: Record; }; /** Any unknown keys in the response are accepted for forward compatibility. */ readonly [key: string]: unknown; diff --git a/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts b/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts index 11e1aac7d0c944..b2fbb792777f18 100644 --- a/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts +++ b/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts @@ -63,12 +63,14 @@ suite('adaptManagedSettings', () => { enabled: true, serviceName: 'acme-copilot', resourceAttributes: { 'deployment.environment': 'prod', 'service.namespace': 'acme' }, + headers: { 'x-api-key': 'secret' }, }, }), { managedSettings: { 'telemetry.enabled': true, 'telemetry.serviceName': 'acme-copilot', 'telemetry.resourceAttributes': '{"deployment.environment":"prod","service.namespace":"acme"}', + 'telemetry.headers': '{"x-api-key":"secret"}', }, }); }); From 6410d9e8f53c9b5c73428ec6b7029ac4c1dedd25 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 16:04:32 -0700 Subject: [PATCH 19/26] docs: record serviceName/resourceAttributes/headers delivery in sprint --- .../agent_monitoring_managed_settings.sprint.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md index 9f37df83ca42b2..7fe2b21626aeb7 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md @@ -118,3 +118,19 @@ Traced through the CLI runtime (`copilot-agent-runtime`): Sequencing (separate commits): (1) `serviceName`; (2) `resourceAttributes` (needs nested-key support in `STRUCTURED_MANAGED_SETTINGS`); (3) `headers` extension-only. Agent-host `headers` tracked under the runtime follow-up. + +### Implemented (this follow-up) + +All three delivered VS-Code-only, one commit each: + +- [x] **`serviceName`** — scalar key `telemetry.serviceName`; hidden policy-only agent-host slot + `CopilotOtelServiceName` → `OTEL_SERVICE_NAME`; extension setting `github.copilot.chat.otel.serviceName` + (policy > env > setting > `copilot-chat`). +- [x] **`resourceAttributes`** — structured nested-map key `telemetry.resourceAttributes` (added + nested read/delete + `encodeStringMap` to `STRUCTURED_MANAGED_SETTINGS`); hidden agent-host object slot + `CopilotOtelResourceAttributes`, serialized to `OTEL_RESOURCE_ATTRIBUTES=k=v,…`; extension setting + merges per-key (policy > env > setting). +- [x] **`headers`** — structured nested-map key `telemetry.headers`; **extension-only** object slot + `CopilotOtelHeaders` applied directly to the OTLP exporter (`headers:` on each exporter), never via env. + **Agent-host `headers` remain deferred** (env path leaks the secret to tool subprocesses). + From 65bcc23a6ddab2de6a20771044f442ae193bc218 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 16:58:47 -0700 Subject: [PATCH 20/26] docs: remove internal OTel managed-settings planning notes --- .../agent_monitoring_managed_settings.md | 113 --------------- ...gent_monitoring_managed_settings.sprint.md | 136 ------------------ 2 files changed, 249 deletions(-) delete mode 100644 extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md delete mode 100644 extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md deleted file mode 100644 index 974f50b20d068e..00000000000000 --- a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.md +++ /dev/null @@ -1,113 +0,0 @@ -# Enterprise OTel Managed-Settings Policy — High-Level Spec (VS Code) - -Goal: let an enterprise centrally control Copilot agent-host OpenTelemetry export via -**Copilot managed settings**, surfaced as VS Code policies. When the enterprise sets a -value it wins over the user's own setting / env vars and is locked in the UI. - -This must match the **cross-client `telemetry` contract** already shipped in the CLI -(copilot-agent-runtime PR #10735, `ManagedTelemetrySettings`) under the managed-settings -governance initiative (github/copilot-agent-runtime#9930). VS Code is the second client of -the same schema — keep keys and semantics identical. - -## Scope — `telemetry` managed-settings schema (canonical, 8 fields) - -Nested `telemetry` block in the managed-settings response: - -- `telemetry.enabled` — enables OTel; users cannot disable export when true. -- `telemetry.endpoint` — OTLP collector endpoint (⇄ `OTEL_EXPORTER_OTLP_ENDPOINT`). -- `telemetry.protocol` — free-form string (⇄ `OTEL_EXPORTER_OTLP_PROTOCOL`); `http/json` / - `http/protobuf` (`grpc` accepted for forward-compat, falls back to default). No enum. -- `telemetry.headers` — auth/routing headers (⇄ `OTEL_EXPORTER_OTLP_HEADERS`). **Secret. Never logged.** -- `telemetry.resourceAttributes` — extra resource attributes (⇄ `OTEL_RESOURCE_ATTRIBUTES`). -- `telemetry.serviceName` — overrides `service.name` (⇄ `OTEL_SERVICE_NAME`). -- `telemetry.captureContent` — capture prompts/responses/tool args. Default false. -- `telemetry.lockCaptureContent` — prevents the user from enabling content capture themselves. - -## Implementation status - -Shipped (see [agent_monitoring_managed_settings.sprint.md](./agent_monitoring_managed_settings.sprint.md)): -`enabled`, `endpoint`, `protocol` (full `http/json` vs `http/protobuf` vs `grpc`), `captureContent`, -`lockCaptureContent` — on both the agent-host owner settings and the `github.copilot.chat.otel.*` -references. **Deferred:** `headers`, `resourceAttributes`, `serviceName` (need new owner settings, -structured-nested-key support for the maps, and secret-safe env plumbing). - -## Ownership - -- **Owner:** the `chat.agentHost.otel.*` settings carry the `policy:` definitions. -- **Reference:** the `github.copilot.chat.otel.*` (extension) settings point at the same - policies (`policyReference`), so a single managed value applies to **both** setting surfaces. -- **Constraint:** `policyReference` maps one policy to two settings, so the owning and referencing - settings **must have identical types**. Confirmed OK — `chat.agentHost.otel.*` and - `github.copilot.chat.otel.*` already share the same setting types. (`policyReference` is a - newly-introduced pattern; extensible if the type-match rule needs to loosen.) - -## Precedence & enforcement - -Enterprise policy > env vars > user settings > defaults (managed wins over env). - -- `protocol` is two axes: **transport** (maps to the agent-host `exporterType`: `grpc`→`otlp-grpc`, - `http/*`→`otlp-http`) and **wire encoding** (`http/json` vs `http/protobuf`). The raw value is - threaded through a dedicated policy-only slot so it sets `OTEL_EXPORTER_OTLP_PROTOCOL` (+ per-signal) - on the agent host and selects the `-proto` vs `-http` exporter in the extension. Managed value wins, - incl. per-signal env overrides. (The runtime defaults to `http/json` when unset, so the wire axis - must be propagated, not just mapped to `exporterType`.) -- `captureContent`: explicit value wins; otherwise `lockCaptureContent: true` forces it off. -- **Per-key managed-wins for `headers` / `resourceAttributes`**: a managed key overrides the - same key from user env (`OTEL_EXPORTER_OTLP_HEADERS` / `OTEL_RESOURCE_ATTRIBUTES`), but - env-only keys are preserved. `endpoint` is hard-locked. - -## Security (carried from the CLI implementation — must replicate) - -- **Secrets stay out of `process.env`.** `headers`, `resourceAttributes`, `serviceName`, - `endpoint` are passed out-of-band (explicit params), never written to env, so secret - headers don't leak and aren't inherited by spawned subprocesses. -- **Subprocess env sanitization.** Strip `OTEL_*` from env of spawned shells/subprocesses so - users can't override enterprise telemetry config downstream. -- **Logging.** Only header / resource-attribute *names* may be logged, never values. - -## Channels — all three already wired in VS Code (no new infra) - -Server, MDM, and file-based managed settings **all already exist** and converge through one -shared pipeline, so supporting all three for `telemetry` is free once the keys are registered: - -- **Server:** `/copilot_internal/managed_settings` → `adaptManagedSettings()` → `normalizeManagedSettings()` - ([managedSettings.ts](src/vs/workbench/services/accounts/browser/managedSettings.ts), [defaultAccount.ts](src/vs/workbench/services/accounts/browser/defaultAccount.ts)). -- **MDM:** Windows registry `SOFTWARE\Policies\GitHubCopilot` + macOS `com.github.copilot` via - `@vscode/policy-watcher` ([nativeManagedSettingsService.ts](src/vs/platform/policy/node/nativeManagedSettingsService.ts)). -- **File:** `managed-settings.json` on well-known Win/macOS/Linux paths via `IFileService` - ([fileManagedSettingsService.ts](src/vs/platform/policy/common/fileManagedSettingsService.ts)). - -All three feed `AccountPolicyService.getPolicyData()`, which calls `selectManagedSettings()` and -the shared normalizer in [copilotManagedSettings.ts](src/vs/platform/policy/common/copilotManagedSettings.ts). - -**Channel precedence: `server > MDM > file`** (single winner, never merged) — richer than the -CLI's `server ?? mdm`. Main-process wiring is in [main.ts](src/vs/code/electron-main/main.ts). - -## Explicit non-goals / suppressions (consistent with CLI — none of these are in its schema) - -- **outfile** — no policy. When the enterprise sets an endpoint/protocol, local file export - is forced off so data can't be diverted to disk. -- **dbSpanExporter** — **not** under enterprise policy; the user can still enable it. - (NB: the earlier VS Code branch wrongly nested `dbSpanExporter.enabled` under `telemetry` — - remove it to match the CLI contract.) -- **maxAttributeSizeChars** — suppressed for now (may be added later). - -## Touch points (where work lands, no detail) - -1. **Register keys** in [copilotManagedSettings.ts](src/vs/platform/policy/common/copilotManagedSettings.ts): - the `telemetry.*` constants + the nested `telemetry` object in the `STRUCTURED_MANAGED_SETTINGS` - table. **All managed-settings deserialization/mapping stays in that table** (one `encode` row per - structured key). Scalars (`enabled`/`endpoint`/`protocol`/`captureContent`/`lockCaptureContent`/`serviceName`) - flatten trivially, but **`headers` and `resourceAttributes` are `Record` maps and - MUST use the structured-key JSON-encode path** (like `enabledPlugins` / `extraKnownMarketplaces`), - not the scalar dot-path flattener. Validate block shape (URL, protocol values, map values). -2. `policy:` definitions on the owning `chat.agentHost.otel.*` settings + policy catalog entries - (`value(policyData)` callbacks + `managedSettings: { telemetry: { type: 'object' } }`). -3. `policyReference` on the `github.copilot.chat.otel.*` extension settings. -4. Plumb policy values into the agent-host launch path + extension OTel config resolver: - apply precedence, keep secrets out of env, do per-key header/attr merge, sanitize `OTEL_*` - from subprocess env. -5. Tests + docs. - -No new channel infrastructure is required — server/MDM/file are already unified; the cost is -one structured-key registration + the per-setting policy blocks. diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md b/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md deleted file mode 100644 index 7fe2b21626aeb7..00000000000000 --- a/extensions/copilot/docs/monitoring/agent_monitoring_managed_settings.sprint.md +++ /dev/null @@ -1,136 +0,0 @@ -# OTel Managed-Settings Policy — Sprint Plan - -Implements the plan in [agent_monitoring_managed_settings.md](./agent_monitoring_managed_settings.md), -following the `add-policy` skill (`.github/skills/add-policy/`). - -## Scope decision - -Two tiers in the canonical `telemetry` schema: - -- **Scalar fields** — flatten to dot-path bag keys automatically (`flattenManagedSettings`), - no `STRUCTURED_MANAGED_SETTINGS` change needed, and map onto **existing** `chat.agentHost.otel.*` - settings. **In scope this sprint.** -- **Map fields** (`headers`, `resourceAttributes`) + `serviceName` — need brand-new owner settings - + (for the maps) structured-nested-key support in the normalizer + secret-safe env plumbing. - **Deferred** (see Hiccups). - -## Tasks (prioritized) - -1. **Keys** — add `telemetry.*` constants in `copilotManagedSettings.ts`. -2. **Response shape** — add `telemetry` block to `IManagedSettingsResponse` (`managedSettings.ts`). -3. **Owner policies** — attach `policy:` to `chat.agentHost.otel.{enabled,exporterType,otlpEndpoint,captureContent,outfile}` in `agentHostStarter.config.contribution.ts`. -4. **Env precedence** — `buildAgentHostOTelEnv` gains a `policySettings` param (policy overwrites env); both starters pass policy values. -5. **Extension policyReference** — point `github.copilot.chat.otel.{enabled,exporterType,otlpEndpoint,captureContent,outfile}` at the owner policies (`package.json`). -6. **Extension precedence** — `otelConfig.ts` policy > env > setting; `services.ts` plumbs `policyValue`. -7. **OTLP wire protocol parity** — honor `http/json` vs `http/protobuf` end-to-end: - - Agent host: hidden policy-only `chat.agentHost.otel.otlpProtocol` slot (`CopilotOtelOtlpProtocol`) → - sets `OTEL_EXPORTER_OTLP_PROTOCOL` (+ per-signal) on the agent-host env (the runtime defaults to - `http/json`, so an unset protocol silently dropped the enterprise's protobuf choice). - - Extension: add the `@opentelemetry/exporter-{trace,logs,metrics}-otlp-proto` deps, widen - `OTelConfig.otlpProtocol` to `grpc | http/json | http/protobuf`, select the `-proto` exporter, - and add `github.copilot.chat.otel.protocol` (referencing `CopilotOtelOtlpProtocol`). -8. **Validate** — typecheck, specs, `export-policy-data`. - -## Status - -- [x] 1 Keys -- [x] 2 Response shape -- [x] 3 Owner policies -- [x] 4 Env precedence -- [x] 5 Extension policyReference -- [x] 6 Extension precedence -- [x] 7 OTLP wire protocol parity (agent host + extension `-proto`) -- [x] 8 Validate (`typecheck-client` clean; agentService + `otelConfig` specs green; `export-policy-data` → 42 policies, 7 references) - -## Hiccups & Notes - -- **Deferred `headers` / `resourceAttributes` / `serviceName`.** These have no existing - `chat.agentHost.otel.*` owner setting, the two maps need structured-nested-key support in - `normalizeManagedSettings` (the current `STRUCTURED_MANAGED_SETTINGS` table only handles - top-level keys), and `headers` needs secret-safe out-of-env plumbing. Follow-up: add the three - owner settings + structured map handling + secret env injection, then `policyReference` them. -- **OTLP protocol was lossy at first.** Mapping `telemetry.protocol` only onto the `exporterType` - enum (`otlp-http`) dropped the protobuf-vs-json axis; the agent-host runtime then defaulted to - `http/json`. Fixed by threading the raw protocol through a dedicated slot on both surfaces. -- **Extension can't do protobuf-HTTP without a dep.** Its exporter only shipped `-grpc` + `-http` - (JSON); added the `-proto` siblings. The earlier `cgmanifest` worry was unfounded — ordinary npm - registry deps aren't tracked there (its `-grpc`/`-http` siblings have no cgmanifest entry either). -- **`dbSpanExporter` left user-controlled** (no policy) per the plan — diverges from the earlier - branch which wrongly nested it under `telemetry`. -- **Scalars need no structured-table change.** `telemetry.{enabled,endpoint,protocol,captureContent,lockCaptureContent}` - flatten to dot-path bag keys automatically via `flattenManagedSettings`. - -- **Desktop agent host wasn't receiving managed OTel policy (fixed).** `AccountPolicyService` - (server / native-MDM / file managed settings) is added to the policy service only in the - **renderer** (`desktop.main.ts` `MultiplexPolicyService([policyChannel, accountPolicy])`). The - agent-host starter (`ElectronAgentHostStarter`) runs in **electron-main**, whose config service - lacks that layer, so `inspect(key).policyValue` for `chat.agentHost.otel.*` was always `undefined` - and the host spawned with no managed OTel env (endpoint/protocol/enabled) — the extension worked - because the extension host mirrors the renderer config. Fix threads the renderer-resolved policy to - the starter over the existing renderer→main connection seam (`AgentHostOTelPolicyIpcChannel`; - `readAgentHostOTelPolicySettings` / `sanitizeAgentHostOTelPolicySettings`; renderer sends before - `acquirePort`, starter uses it as `buildAgentHostOTelEnv` `policySettings`, falling back to - main-process policy). Verified e2e: agent host env got `4318` + `http/protobuf`; Aspire - `service.name=github-copilot`. - -## Follow-ups (not in this sprint) - -- **Deferred map/serviceName fields** — `headers` / `resourceAttributes` / `serviceName`. Spike - (against the CLI runtime source) revised the plan — see **Spike: where the agent host gets OTel - config** below. Net: deliver them VS Code-only (no runtime change) via the env vars the runtime's - `build_resource` already reads (`OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`) plus the - extension's own exporter; **agent-host `headers` stay deferred** (secret-in-env leaks to tool - subprocesses — needs the runtime's `applyManagedTelemetry`). -- **Agent-host OTel env is fixed at spawn; no re-apply on later policy change.** The agent host is a - singleton utility process whose OTel env is computed once in `start()`. If managed OTel policy - changes (or first syncs) **after** the host has already spawned, the running host keeps the stale - env — same class of problem as the extension requiring a "Reload Window". Follow-up: detect a - managed-OTel-policy change in the renderer and either (a) re-spawn the agent host, or (b) surface a - restart affordance, so the new policy takes effect without a full quit/relaunch. Today the host - must first spawn *after* policy sync to pick up managed OTel settings. - -## Spike: where the agent host gets OTel config (for `headers`/`resourceAttributes`/`serviceName`) - -Traced through the CLI runtime (`copilot-agent-runtime`): - -- The headless/agent-host runtime resolves OTel from **env only** (`OtelLifecycle` → `resolveOtelConfig`). - Its env reader (`readOtelEnv`) maps only the scalar `COPILOT_OTEL_*`/`OTEL_*` vars — it does **not** - read headers/resourceAttributes/serviceName into the config object. -- Those three only flow through the **structured** path: `mergeManagedOtelConfig(managed, env)` → - `OtelLifecycle.applyManagedTelemetry(managed)` (headers stamped out-of-env by `ManagedHeaderClient`). - That path is invoked **interactive-CLI-only** (`expectManagedTelemetry` gated on `!isNonInteractiveMode`; - TUI fetches via `app.tsx onAuthChange → fetchManagedSettings`). The SDK *does* expose - `applyManagedTelemetry`/`expectManagedTelemetry` on `LocalSessionHost`, but the headless agent-host - entry never calls them and nothing wires VS Code's block in. -- **However**, the runtime's `build_resource` (otel_sdk.rs) reads standard env as resource precedence: - managed `service_name`/`resource_attributes` (1) → `OTEL_RESOURCE_ATTRIBUTES` (2) → - `OTEL_SERVICE_NAME` (3) → default. So VS Code **can** deliver `serviceName`/`resourceAttributes` to the - agent host via those env vars without any runtime change. `headers` via `OTEL_EXPORTER_OTLP_HEADERS` - env would also be read — but that's a secret that leaks into every tool subprocess the host spawns. - -### Revised plan (VS Code-only, no runtime change) - -| Field | Extension (`copilot-chat`) | Agent host (`github-copilot`) | -| --- | --- | --- | -| `serviceName` | programmatic (own resource) | env `OTEL_SERVICE_NAME` | -| `resourceAttributes` | programmatic | env `OTEL_RESOURCE_ATTRIBUTES` | -| `headers` (secret) | programmatic, out-of-env | **deferred** — needs runtime `applyManagedTelemetry` (env path leaks the token to tool subprocesses) | - -Sequencing (separate commits): (1) `serviceName`; (2) `resourceAttributes` (needs nested-key support in -`STRUCTURED_MANAGED_SETTINGS`); (3) `headers` extension-only. Agent-host `headers` tracked under the runtime follow-up. - -### Implemented (this follow-up) - -All three delivered VS-Code-only, one commit each: - -- [x] **`serviceName`** — scalar key `telemetry.serviceName`; hidden policy-only agent-host slot - `CopilotOtelServiceName` → `OTEL_SERVICE_NAME`; extension setting `github.copilot.chat.otel.serviceName` - (policy > env > setting > `copilot-chat`). -- [x] **`resourceAttributes`** — structured nested-map key `telemetry.resourceAttributes` (added - nested read/delete + `encodeStringMap` to `STRUCTURED_MANAGED_SETTINGS`); hidden agent-host object slot - `CopilotOtelResourceAttributes`, serialized to `OTEL_RESOURCE_ATTRIBUTES=k=v,…`; extension setting - merges per-key (policy > env > setting). -- [x] **`headers`** — structured nested-map key `telemetry.headers`; **extension-only** object slot - `CopilotOtelHeaders` applied directly to the OTLP exporter (`headers:` on each exporter), never via env. - **Agent-host `headers` remain deferred** (env path leaks the secret to tool subprocesses). - From 649467ab6d55d11b1601ccd45fbf6e9014dcffbb Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 17:15:10 -0700 Subject: [PATCH 21/26] fix: align agentOTelEnv test config with widened otlpProtocol type --- .../copilot/src/platform/otel/common/test/agentOTelEnv.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/copilot/src/platform/otel/common/test/agentOTelEnv.spec.ts b/extensions/copilot/src/platform/otel/common/test/agentOTelEnv.spec.ts index d463cfa61596ba..8aa8da6303f44c 100644 --- a/extensions/copilot/src/platform/otel/common/test/agentOTelEnv.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/agentOTelEnv.spec.ts @@ -14,7 +14,7 @@ function makeConfig(overrides: Partial = {}): OTelConfig { enabledVia: 'setting', exporterType: 'otlp-http', otlpEndpoint: 'http://localhost:4318', - otlpProtocol: 'http', + otlpProtocol: 'http/json', captureContent: false, maxAttributeSizeChars: 0, dbSpanExporter: false, @@ -24,6 +24,7 @@ function makeConfig(overrides: Partial = {}): OTelConfig { serviceVersion: '1.0.0', sessionId: 'test-session', resourceAttributes: {}, + headers: {}, ...overrides, }; } From e9a25536d6298cd845378c1612aad00780580428 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 17:21:27 -0700 Subject: [PATCH 22/26] =?UTF-8?q?refactor:=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20policy=20precedence=20docs,=20grpc=20transport=20in?= =?UTF-8?q?ference,=20prototype-pollution=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/monitoring/agent_monitoring_arch.md | 12 ++++++++---- extensions/copilot/package.json | 12 ++++++------ .../copilot/src/platform/otel/common/otelConfig.ts | 5 +++-- .../platform/otel/common/test/otelConfig.spec.ts | 12 ++++++++++++ src/vs/platform/agentHost/common/agentService.ts | 3 +++ .../agentHost/test/common/agentService.test.ts | 9 +++++++++ .../policy/common/copilotManagedSettings.ts | 3 +++ .../accounts/test/browser/managedSettings.test.ts | 13 ++++++++++++- 8 files changed, 56 insertions(+), 13 deletions(-) diff --git a/extensions/copilot/docs/monitoring/agent_monitoring_arch.md b/extensions/copilot/docs/monitoring/agent_monitoring_arch.md index 09c99b26de8ed2..2eb3bc29fbac80 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring_arch.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring_arch.md @@ -209,10 +209,13 @@ Both export to the same OTLP endpoint. Bridge processor sits on Provider B, forw `resolveOTelConfig()` implements layered precedence: -1. `COPILOT_OTEL_*` env vars (highest) -2. `OTEL_EXPORTER_OTLP_*` standard env vars -3. VS Code settings (`github.copilot.chat.otel.*`) -4. Defaults (lowest) +1. Enterprise managed settings (policy) — highest; overrides env vars +2. `COPILOT_OTEL_*` env vars +3. `OTEL_EXPORTER_OTLP_*` standard env vars +4. VS Code settings (`github.copilot.chat.otel.*`) +5. Defaults (lowest) + +The OTLP wire protocol distinguishes `http/json` (default) from `http/protobuf`; `grpc` is selected by the gRPC exporter type. Kill switch: `telemetry.telemetryLevel === 'off'` → all OTel disabled. @@ -222,6 +225,7 @@ The resolved config records *how* OTel was enabled in `OTelConfig.enabledVia` (u | `enabledVia` | Trigger | |---|---| +| `policy` | Enterprise managed settings enable OTel (`telemetry.enabled` or a managed endpoint) | | `envVar` | `COPILOT_OTEL_ENABLED=true` | | `setting` | `github.copilot.chat.otel.enabled` is `true` | | `otlpEndpointEnvVar` | `OTEL_EXPORTER_OTLP_ENDPOINT` is set without an explicit enable | diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 75e469d938e3dc..ff481106e8bac3 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -5282,7 +5282,7 @@ "policyReference": { "name": "CopilotOtelEnabled" }, - "markdownDescription": "Enable OpenTelemetry trace/metric/log emission for Copilot Chat operations. Configurable in user settings only. Env var `COPILOT_OTEL_ENABLED` takes precedence. Requires window reload.", + "markdownDescription": "Enable OpenTelemetry trace/metric/log emission for Copilot Chat operations. Precedence: enterprise policy > env var `COPILOT_OTEL_ENABLED` > user setting. Requires window reload.", "tags": [ "advanced" ] @@ -5300,7 +5300,7 @@ "policyReference": { "name": "CopilotOtelProtocol" }, - "markdownDescription": "OTel exporter type for Copilot Chat telemetry. Configurable in user settings only. Requires window reload.", + "markdownDescription": "OTel exporter type for Copilot Chat telemetry. Configurable in user settings or managed by enterprise policy (policy takes precedence). Requires window reload.", "tags": [ "advanced" ] @@ -5318,7 +5318,7 @@ "policyReference": { "name": "CopilotOtelOtlpProtocol" }, - "markdownDescription": "OTLP wire protocol for Copilot Chat OTel data, mirroring `OTEL_EXPORTER_OTLP_PROTOCOL`. `http/protobuf` selects the protobuf-over-HTTP exporter; the default (empty) uses `http/json`. Configurable in user settings only. Requires window reload.", + "markdownDescription": "OTLP wire protocol for Copilot Chat OTel data, mirroring `OTEL_EXPORTER_OTLP_PROTOCOL`. `http/protobuf` selects the protobuf-over-HTTP exporter; the default (empty) uses `http/json`. Precedence: enterprise policy > env var > user setting. Requires window reload.", "tags": [ "advanced" ] @@ -5330,7 +5330,7 @@ "policyReference": { "name": "CopilotOtelEndpoint" }, - "markdownDescription": "OTLP collector endpoint URL for Copilot Chat OTel data. Configurable in user settings only. Env var `OTEL_EXPORTER_OTLP_ENDPOINT` takes precedence. Requires window reload.", + "markdownDescription": "OTLP collector endpoint URL for Copilot Chat OTel data. Precedence: enterprise policy > env var `OTEL_EXPORTER_OTLP_ENDPOINT` > user setting. Requires window reload.", "tags": [ "advanced" ] @@ -5342,7 +5342,7 @@ "policyReference": { "name": "CopilotOtelCaptureContent" }, - "markdownDescription": "Capture input/output messages, system instructions, and tool definitions in OTel telemetry. **Contains potentially sensitive data.** Configurable in user settings only. Env var `COPILOT_OTEL_CAPTURE_CONTENT` takes precedence. Requires window reload.", + "markdownDescription": "Capture input/output messages, system instructions, and tool definitions in OTel telemetry. **Contains potentially sensitive data.** Precedence: enterprise policy > env var `COPILOT_OTEL_CAPTURE_CONTENT` > user setting. Requires window reload.", "tags": [ "advanced" ] @@ -5406,7 +5406,7 @@ "policyReference": { "name": "CopilotOtelOutfile" }, - "markdownDescription": "File path for file-based OTel exporter output (JSON-lines). When set, overrides exporter type to `file`. Configurable in user settings only. Requires window reload.", + "markdownDescription": "File path for file-based OTel exporter output (JSON-lines). When set, overrides exporter type to `file`. Configurable in user settings or managed by enterprise policy (policy takes precedence). Requires window reload.", "tags": [ "advanced" ] diff --git a/extensions/copilot/src/platform/otel/common/otelConfig.ts b/extensions/copilot/src/platform/otel/common/otelConfig.ts index 43961d0e744b40..cc1a62c108618a 100644 --- a/extensions/copilot/src/platform/otel/common/otelConfig.ts +++ b/extensions/copilot/src/platform/otel/common/otelConfig.ts @@ -170,12 +170,13 @@ export function resolveOTelConfig(input: OTelConfigInput): OTelConfig { enabledVia = 'dbSpanExporterOnly'; } - // Protocol (transport): policy > env > default + // Protocol (transport): policy > env > setting exporter type > default const rawProtocol = input.policyExporterType === 'otlp-grpc' ? 'grpc' : input.policyExporterType === 'otlp-http' ? 'http' - : env['OTEL_EXPORTER_OTLP_PROTOCOL'] ?? env['COPILOT_OTEL_PROTOCOL']; + : (env['OTEL_EXPORTER_OTLP_PROTOCOL'] ?? env['COPILOT_OTEL_PROTOCOL'] + ?? (input.settingExporterType === 'otlp-grpc' ? 'grpc' : input.settingExporterType === 'otlp-http' ? 'http' : undefined)); const protocol: 'grpc' | 'http' = rawProtocol === 'grpc' ? 'grpc' : 'http'; // Wire protocol (json vs protobuf within http): policy > env > setting > default(http/json). 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 9031fd00d97702..7d40811da9ae8a 100644 --- a/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts +++ b/extensions/copilot/src/platform/otel/common/test/otelConfig.spec.ts @@ -162,6 +162,18 @@ describe('resolveOTelConfig', () => { expect(config.otlpEndpoint).toBe('http://collector:4317'); }); + it('infers grpc transport from settingExporterType when no env/policy protocol is set', () => { + const config = resolveOTelConfig(makeInput({ + settingEnabled: true, + settingExporterType: 'otlp-grpc', + settingOtlpEndpoint: 'http://collector:4317/some/path', + })); + expect(config.otlpProtocol).toBe('grpc'); + expect(config.exporterType).toBe('otlp-grpc'); + // gRPC parsing keeps only the origin + expect(config.otlpEndpoint).toBe('http://collector:4317'); + }); + it('preserves service version and session id', () => { const config = resolveOTelConfig(makeInput({ settingEnabled: true, diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index e0299f7cf5b5a5..3cebbdccd4641e 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -403,6 +403,9 @@ export function sanitizeAgentHostOTelPolicySettings(raw: unknown): IAgentHostOTe } const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { + if (k === '__proto__' || k === 'constructor' || k === 'prototype') { + continue; // defend the IPC boundary against prototype pollution + } if (typeof v === 'string') { out[k] = v; } diff --git a/src/vs/platform/agentHost/test/common/agentService.test.ts b/src/vs/platform/agentHost/test/common/agentService.test.ts index 54ae6218bb3fb2..8545e9f22084b7 100644 --- a/src/vs/platform/agentHost/test/common/agentService.test.ts +++ b/src/vs/platform/agentHost/test/common/agentService.test.ts @@ -230,4 +230,13 @@ suite('sanitizeAgentHostOTelPolicySettings', () => { assert.deepStrictEqual(sanitizeAgentHostOTelPolicySettings(null), {}); assert.deepStrictEqual(sanitizeAgentHostOTelPolicySettings('x'), {}); }); + + test('resourceAttributes drop prototype-pollution keys', () => { + // JSON.parse yields an OWN enumerable `__proto__` data property; the sanitizer must not + // copy it onto the result (which would trigger the prototype setter). + const raw = JSON.parse('{"resourceAttributes":{"__proto__":"polluted","constructor":"x","service.namespace":"acme"}}'); + const result = sanitizeAgentHostOTelPolicySettings(raw); + assert.deepStrictEqual(result.resourceAttributes, { 'service.namespace': 'acme' }); + assert.strictEqual(({} as Record).polluted, undefined); + }); }); diff --git a/src/vs/platform/policy/common/copilotManagedSettings.ts b/src/vs/platform/policy/common/copilotManagedSettings.ts index 381c4308e44e28..a0b91f1c5c7d68 100644 --- a/src/vs/platform/policy/common/copilotManagedSettings.ts +++ b/src/vs/platform/policy/common/copilotManagedSettings.ts @@ -289,6 +289,9 @@ function encodeStringMap(value: unknown): Record | undefined { } const out: Record = {}; for (const [k, v] of Object.entries(value)) { + if (k === '__proto__' || k === 'constructor' || k === 'prototype') { + continue; // defend the shared normalizer against prototype pollution + } if (isString(v)) { out[k] = v; } else if (typeof v === 'number' || typeof v === 'boolean') { diff --git a/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts b/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts index b2fbb792777f18..771e945684e092 100644 --- a/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts +++ b/src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts @@ -57,7 +57,7 @@ suite('adaptManagedSettings', () => { }); }); - test('flattens scalar telemetry leaves and carries resourceAttributes as a single JSON key', () => { + test('flattens scalar telemetry leaves and carries resourceAttributes and headers as single JSON keys', () => { assert.deepStrictEqual(adaptManagedSettings({ telemetry: { enabled: true, @@ -207,4 +207,15 @@ suite('adaptManagedSettings', () => { managedSettings: {}, }); }); + + test('resilience: telemetry map keys that could pollute the prototype are dropped', () => { + // JSON.parse yields an OWN enumerable `__proto__` data property on the nested map. + const response = JSON.parse('{"telemetry":{"resourceAttributes":{"__proto__":"polluted","constructor":"x","service.namespace":"acme"}}}') as IManagedSettingsResponse; + assert.deepStrictEqual(adaptManagedSettings(response), { + managedSettings: { + 'telemetry.resourceAttributes': '{"service.namespace":"acme"}', + }, + }); + assert.strictEqual(({} as Record).polluted, undefined); + }); }); From 7b0180b3c3a3db23d1acbab92e1a9bfff4db9f86 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 17:25:32 -0700 Subject: [PATCH 23/26] docs: update OTel user + agent-host docs for managed-settings precedence and new settings --- .../copilot/docs/monitoring/agent_monitoring.md | 13 +++++++++---- src/vs/platform/agentHost/OTEL.md | 10 ++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/extensions/copilot/docs/monitoring/agent_monitoring.md b/extensions/copilot/docs/monitoring/agent_monitoring.md index 60e0422bbb3ab9..76de7393f8aa48 100644 --- a/extensions/copilot/docs/monitoring/agent_monitoring.md +++ b/extensions/copilot/docs/monitoring/agent_monitoring.md @@ -36,7 +36,7 @@ Open **Settings** (`Ctrl+,`) and add: } ``` -> **Note:** You can also use environment variables instead of VS Code settings (see [Configuration](#configuration)). Environment variables always take precedence. +> **Note:** You can also use environment variables instead of VS Code settings (see [Configuration](#configuration)). Precedence is **enterprise policy > environment variables > settings**. ### 3. Generate Telemetry @@ -70,13 +70,17 @@ Open **Settings** (`Ctrl+,`) and search for `copilot otel`: | `github.copilot.chat.otel.exporterType` | string | `"otlp-http"` | `otlp-http`, `otlp-grpc`, `console`, or `file` | | `github.copilot.chat.otel.otlpEndpoint` | string | `"http://localhost:4318"` | OTLP collector endpoint | | `github.copilot.chat.otel.captureContent` | boolean | `false` | Capture full prompt/response content | +| `github.copilot.chat.otel.protocol` | string | `""` | OTLP wire protocol: `http/json` (default), `http/protobuf`, or `grpc` | +| `github.copilot.chat.otel.serviceName` | string | `""` | Override the `service.name` resource attribute | +| `github.copilot.chat.otel.resourceAttributes` | object | `{}` | Extra resource attributes (`{ "key": "value" }`) | +| `github.copilot.chat.otel.headers` | object | `{}` | Extra OTLP exporter headers, applied directly to the exporter (`{ "key": "value" }`) | | `github.copilot.chat.otel.maxAttributeSizeChars` | integer | `0` | Max characters per OTel content attribute (prompts, tool args/results, hook input/output). `0` (the default) disables truncation so backends with no per-attribute limit get full payloads. Set to a positive value to match your backend's per-attribute size limit — consult your backend's documentation. The value counts JavaScript string characters (UTF-16 code units); for non-ASCII content one character can be multiple UTF-8 bytes on the wire. | | `github.copilot.chat.otel.outfile` | string | `""` | File path for JSON-lines output | | `github.copilot.chat.otel.dbSpanExporter.enabled` | boolean | `false` | Persist OTel spans to a local SQLite database for the **Chat: Export Agent Traces DB** command. Implicitly enables OTel. | ### Environment Variables -Environment variables **always take precedence** over VS Code settings. +Environment variables take precedence over VS Code settings, and **enterprise managed settings (policy) take precedence over both** — admins can centrally mandate any `github.copilot.chat.otel.*` value. | Variable | Default | Description | |---|---|---| @@ -98,6 +102,7 @@ Environment variables **always take precedence** over VS Code settings. OTel is **off by default** with zero overhead. It activates when: +- enterprise policy enables it (managed `telemetry.enabled` or a managed endpoint), or - `COPILOT_OTEL_ENABLED=true`, or - `OTEL_EXPORTER_OTLP_ENDPOINT` is set, or - `github.copilot.chat.otel.enabled` is `true`, or @@ -471,7 +476,7 @@ All signals carry: | Attribute | Value | |---|---| -| `service.name` | `copilot-chat` (configurable via `OTEL_SERVICE_NAME`) | +| `service.name` | `copilot-chat` (override via the `github.copilot.chat.otel.serviceName` setting, `OTEL_SERVICE_NAME`, or enterprise policy) | | `service.version` | Extension version | | `session.id` | Unique per VS Code window | @@ -541,7 +546,7 @@ Content is captured in full with no truncation. } ``` -> **Note:** Authentication headers are only configurable via the `OTEL_EXPORTER_OTLP_HEADERS` environment variable (e.g., `Authorization=Bearer your-token`). See [Environment Variables](#environment-variables). +> **Note:** Authentication headers can be set via the `github.copilot.chat.otel.headers` setting (a `{ "key": "value" }` map applied directly to the exporter) or the `OTEL_EXPORTER_OTLP_HEADERS` environment variable, and can be mandated by enterprise policy. See [Environment Variables](#environment-variables). **File-based output (offline / CI):** diff --git a/src/vs/platform/agentHost/OTEL.md b/src/vs/platform/agentHost/OTEL.md index 726f922a98608f..60cd77310aefd8 100644 --- a/src/vs/platform/agentHost/OTEL.md +++ b/src/vs/platform/agentHost/OTEL.md @@ -77,8 +77,10 @@ The workbench-side starter translates the settings above into the following env | `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `chat.agentHost.otel.captureContent` | | | `COPILOT_OTEL_FILE_EXPORTER_PATH` | `chat.agentHost.otel.outfile` | | | `COPILOT_OTEL_DB_SPAN_EXPORTER_ENABLED` | `chat.agentHost.otel.dbSpanExporter.enabled` | | -| `OTEL_EXPORTER_OTLP_PROTOCOL` | (inherited) | `grpc` selects gRPC; any other value uses HTTP. | -| `OTEL_EXPORTER_OTLP_HEADERS` | (inherited) | Auth headers (e.g., `Authorization=Bearer …`). | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | (inherited or enterprise policy) | `grpc` selects gRPC; any other value uses HTTP. Set from the managed `telemetry.protocol` when configured. | +| `OTEL_SERVICE_NAME` | (inherited or enterprise policy) | `service.name` resource attribute; set from the managed `telemetry.serviceName`. | +| `OTEL_RESOURCE_ATTRIBUTES` | (inherited or enterprise policy) | Extra resource attributes (`k=v,k2=v2`); set from the managed `telemetry.resourceAttributes`. | +| `OTEL_EXPORTER_OTLP_HEADERS` | (inherited) | Auth headers (e.g., `Authorization=Bearer …`). **Not** delivered from managed settings — env delivery would leak the secret to tool subprocesses; managed headers apply to the Copilot Chat extension only. | > **Activation timing.** Env vars are bound at agent host **spawn time**. Changing a setting while the agent host is already running has no effect until the host respawns — restart VS Code or reload the window if you change these settings mid-session. @@ -134,7 +136,7 @@ src/vs/platform/otel/ ## Settings → Env Var Translation -`buildAgentHostOTelEnv()` ([common/agentService.ts](common/agentService.ts)) is the single translation point. The starter (`electronAgentHostStarter.ts` / `nodeAgentHostStarter.ts`) reads settings, calls `buildAgentHostOTelEnv(settings, parentEnv)`, and merges the result into the spawned process's environment. Parent-env values always win. +`buildAgentHostOTelEnv()` ([common/agentService.ts](common/agentService.ts)) is the single translation point. The starter (`electronAgentHostStarter.ts` / `nodeAgentHostStarter.ts`) reads settings, calls `buildAgentHostOTelEnv(settings, parentEnv)`, and merges the result into the spawned process's environment. Parent-env values win over the local `chat.agentHost.otel.*` settings (developer override); **enterprise managed-policy values win over parent env**. | Setting | Env var | |---|---| @@ -145,7 +147,7 @@ src/vs/platform/otel/ | `chat.agentHost.otel.outfile` | `COPILOT_OTEL_FILE_EXPORTER_PATH` | | `chat.agentHost.otel.dbSpanExporter.enabled` | `COPILOT_OTEL_DB_SPAN_EXPORTER_ENABLED` | -`OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_EXPORTER_OTLP_HEADERS`, and `OTEL_RESOURCE_ATTRIBUTES` flow via env inheritance — they are not translated from settings. +`OTEL_EXPORTER_OTLP_HEADERS` flows via env inheritance only. `OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_SERVICE_NAME`, and `OTEL_RESOURCE_ATTRIBUTES` are not translated from the local `chat.agentHost.otel.*` settings, but **enterprise managed settings (policy)** can set them on the spawned host: the renderer forwards the resolved policy to the starter, and managed values win over inherited env. `readAgentHostOTelEnv()` ([node/otel/agentHostOTelService.ts](node/otel/agentHostOTelService.ts)) is the inverse: it reads `process.env` inside the agent host and produces the `ResolvedConfig` that drives mode selection and outbound forwarding. From e5ec5abcc6ac93f8baa9123088661acf5eef962b Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 17:37:54 -0700 Subject: [PATCH 24/26] fix: register otel protocol/serviceName/resourceAttributes/headers config keys to match package.json --- .../src/platform/configuration/common/configurationService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 01c2a48ee08836..e0d94d59300ae4 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -728,8 +728,12 @@ export namespace ConfigKey { // OTel settings export const OTelEnabled = defineSetting('chat.otel.enabled', ConfigType.Simple, false); export const OTelExporterType = defineSetting('chat.otel.exporterType', ConfigType.Simple, 'otlp-http'); + export const OTelProtocol = defineSetting('chat.otel.protocol', ConfigType.Simple, ''); export const OTelOtlpEndpoint = defineSetting('chat.otel.otlpEndpoint', ConfigType.Simple, 'http://localhost:4318'); export const OTelCaptureContent = defineSetting('chat.otel.captureContent', ConfigType.Simple, false); + export const OTelServiceName = defineSetting('chat.otel.serviceName', ConfigType.Simple, ''); + export const OTelResourceAttributes = defineSetting>('chat.otel.resourceAttributes', ConfigType.Simple, {}); + export const OTelHeaders = defineSetting>('chat.otel.headers', ConfigType.Simple, {}); export const OTelMaxAttributeSizeChars = defineSetting('chat.otel.maxAttributeSizeChars', ConfigType.Simple, 0); export const OTelOutfile = defineSetting('chat.otel.outfile', ConfigType.Simple, ''); export const OTelDbSpanExporter = defineSetting('chat.otel.dbSpanExporter.enabled', ConfigType.Simple, false); From 0ce1f55d29aeb22b4440827301c9562da8539692 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 22:15:11 -0700 Subject: [PATCH 25/26] fix: regenerate policy data to match fixture-based export The checked-in policyData.jsonc was generated with the Copilot extension loaded, which injected referencedSettings linking the OTel extension settings to the core CopilotOtel* policies. The PolicyExport integration test regenerates using the test fixture distro with no user extensions, so those referencedSettings are absent. Regenerate in an isolated environment to match the fixture-based export. --- build/lib/policies/policyData.jsonc | 45 ++++++----------------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index faf419d71a3700..c5095409403719 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -51,10 +51,7 @@ }, "type": "string", "default": "", - "included": false, - "referencedSettings": [ - "github.copilot.chat.otel.protocol" - ] + "included": false }, { "key": "chat.agentHost.otel.serviceName", @@ -69,10 +66,7 @@ }, "type": "string", "default": "", - "included": false, - "referencedSettings": [ - "github.copilot.chat.otel.serviceName" - ] + "included": false }, { "key": "chat.agentHost.otel.resourceAttributes", @@ -87,10 +81,7 @@ }, "type": "object", "default": {}, - "included": false, - "referencedSettings": [ - "github.copilot.chat.otel.resourceAttributes" - ] + "included": false }, { "key": "chat.agentHost.otel.headers", @@ -105,10 +96,7 @@ }, "type": "object", "default": {}, - "included": false, - "referencedSettings": [ - "github.copilot.chat.otel.headers" - ] + "included": false }, { "key": "mcp.enterpriseManagedAuth.idp", @@ -261,10 +249,7 @@ }, "type": "boolean", "default": false, - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.enabled" - ] + "included": true }, { "key": "chat.agentHost.otel.exporterType", @@ -303,10 +288,7 @@ "console", "file" ], - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.exporterType" - ] + "included": true }, { "key": "chat.agentHost.otel.otlpEndpoint", @@ -321,10 +303,7 @@ }, "type": "string", "default": "", - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.otlpEndpoint" - ] + "included": true }, { "key": "chat.agentHost.otel.captureContent", @@ -339,10 +318,7 @@ }, "type": "boolean", "default": false, - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.captureContent" - ] + "included": true }, { "key": "chat.agentHost.otel.outfile", @@ -357,10 +333,7 @@ }, "type": "string", "default": "", - "included": true, - "referencedSettings": [ - "github.copilot.chat.otel.outfile" - ] + "included": true }, { "key": "chat.defaultModel", From 997c3786fa28c54cddeeeda49e14f38ec3c7eea9 Mon Sep 17 00:00:00 2001 From: Zhichao Li Date: Fri, 26 Jun 2026 22:18:59 -0700 Subject: [PATCH 26/26] docs(add-policy): warn that policyData must be regenerated in a clean env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plain 'npm run export-policy-data' loads dev-profile extensions, injecting referencedSettings the fixture-based PolicyExport integration test won't produce — failing CI in a way that is not reproducible locally. Document the isolated regeneration command. --- .github/skills/add-policy/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/skills/add-policy/SKILL.md b/.github/skills/add-policy/SKILL.md index 32a34f41d6d85a..1b9a1260794026 100644 --- a/.github/skills/add-policy/SKILL.md +++ b/.github/skills/add-policy/SKILL.md @@ -328,4 +328,5 @@ Search the codebase for `policy:` to find all the examples of different policy c ## Learnings * Never hand-edit `build/lib/policies/policyData.jsonc` (its header explicitly forbids it). If `npm run export-policy-data` is failing, fix the script — don't patch the JSON. Common cause: running it in the wrong working directory (e.g. main repo instead of a worktree), which silently exports the wrong source tree. +* **Regenerate `policyData.jsonc` in a clean environment, or the `PolicyExport` integration test will fail in CI.** `referencedSettings` is only captured for references **loaded at export time**. A plain `npm run export-policy-data` loads your **dev-profile extensions** (e.g. the Copilot extension), which injects `referencedSettings` onto core policies that the test's **fixture-based** export (clean profile, no user extensions) won't produce — so the checked-in file ends up with extra `referencedSettings` and CI fails. This is **not reproducible locally** because the test reuses your default extensions dir. Regenerate the way the test exports: `DISTRO_PRODUCT_JSON= ./scripts/code.sh --export-policy-data="$PWD/build/lib/policies/policyData.jsonc" --user-data-dir="$(mktemp -d)" --extensions-dir="$(mktemp -d)"` (with `VSCODE_SKIP_PRELAUNCH=1`). * Document **behavior and business-logic expectations**, not copy-pasted implementation. Reproducing internal code (e.g. the `getPolicyData()` merge body) in the skill rots the moment the source changes and adds no information beyond the source itself. State the contract in prose (e.g. "server-delivered managed settings win over native MDM; the two layers are never merged") and point to the source for the implementation. Reserve code blocks for the **author-facing API contract** a contributor must follow — how to *declare* a `policy` / `managedSettings` / `value` callback — not for restating runtime plumbing.