Skip to content

Add governed OTel identity capture for Local agents - #336814

Draft
Ross Wollman (rwoll) wants to merge 3 commits into
mainfrom
rwoll/otel-governed-identity
Draft

Ross Wollman (rwoll) wants to merge 3 commits into
mainfrom
rwoll/otel-governed-identity

Conversation

@rwoll

@rwoll Ross Wollman (rwoll) commented Sep 18, 2026

Copy link
Copy Markdown
Member

Resolves #302930.

Summary

Adds opt-in, independently governed OTel identity capture to the Local harness. This PR now targets main; the managed OTel startup-recovery prerequisite, #336701, has merged.

  • telemetry.capture.identity controls authenticated account attribution (user.name on agent spans), OS account (process.user.name), and machine name (host.name). Capture defaults off and does not enable prompt, response, or tool-content capture.
  • Explicit managed false overrides personal/environment opt-in. The extension keeps omission distinct from denial, and rechecks permission at export/event boundaries so revocation does not wait for a reload.
  • Managed telemetry uses the highest-priority observable whole block (native MDM > server > file), rather than inheriting omitted fields from weaker sources. Server/file object sources preserve empty or unsupported-only blocks; native MDM establishes a block only through observed declared flat keys.
  • Identity is read from the authentication service's current session on top-level, subagent, and inline agent invocations. Account changes take effect after that service refreshes; existing pseudonymous identity is unchanged.
  • Policy Diagnostics explains block selection and preserves suppressed-source provenance without changing effective policy or printing newly surfaced excluded values.

Native Agent Host identity enforcement remains runtime-owned. This does not implement the other five granular content-capture controls, resource-attribute placeholder expansion, or subagent cost accounting. External span content filtering respects the existing captureContent switch; local debug content behavior is unchanged.

Compatibility: An identity-only managed block also replaces personal exporter settings with policy/schema defaults. Configure managed enablement and destination when export should continue. Explicit identity resource attributes require identity permission; they do not bypass default-off behavior or managed false. Known content attributes are filtered from primary OTLP, file, and console span exports when captureContent is false; local debug retention is separate. The follow-up retains these contracts rather than weakening them.

Manual testing

macOS: reuse the temporary-policy script and console OTel collector

Setup

Use the single-script collector from #336701. It accepts OTLP HTTP/JSON, prints traces/logs/metrics without request headers, and creates a temporary machine-wide managed-settings file with guarded cleanup.

  • Use macOS, Node.js 18+, a free port 4318, and an approved disposable test setup. Close other VS Code windows.
  • Use an account/device with no existing managed OTel block. The script refuses to overwrite /Library/Application Support/GitHubCopilot/managed-settings.json. A fresh profile does not isolate machine or account policy. Do not alter an organization's policy to run this test.
  • Run a full editor build containing this PR, including its core policy changes and bundled Copilot extension. Loading only the changed extension into stock Insiders is insufficient for this managed-policy test.
  • Leave personal OTel/DB export disabled and clear OTel environment overrides for the initial positive test. In particular, unset COPILOT_OTEL_ENABLED; setting it to false can prevent policy enablement. Leave github.copilot.chat.otel.captureIdentity at its default null and unset COPILOT_OTEL_CAPTURE_IDENTITY. Sign in, choose Local, and send a harmless request while OTel is still off.

The gist installs the lowest-priority file source. A server telemetry block, even empty, or any observed declared native telemetry key can shadow it. Empty/unknown-only native objects are not observable by the native watcher. No exported telemetry is a failed/inconclusive test, not evidence that identity filtering worked.

Enable identity without content

Download and review otel-restart-test.cjs. In your downloaded copy, replace only its payload declaration with:

const payload = JSON.stringify({
  telemetry: {
    enabled: true,
    endpoint,
    protocol: 'http/json',
    captureContent: false,
    capture: { identity: true },
  },
}, null, 2) + '\n';

Run it in your own terminal:

sudo node otel-restart-test.cjs

Press Enter after Copilot has activated. The script applies the policy eight seconds later and keeps it for 120 seconds. Late enablement can trigger the bounded extension-host recovery from #336701. If active work vetoes recovery, finish that work and reload; do not infer successful export merely from Settings showing the policy.

Once Local is ready, send a harmless request such as Reply with IDENTITY_TEST_OK. Also make a harmless request that actually invokes a subagent; confirm a child invocation occurred before interpreting subagent coverage. Allow approximately ten seconds for export batching.

Expected collector output

Expect POST /v1/traces followed by OTLP JSON. With the gist's unchanged service configuration, identify Local extension output by resource service.name: "copilot-chat" and scopeSpans[].scope.name: "copilot-chat". Its invoke_agent spans also carry copilot_chat.session_id.

The authenticated account name appears as user.name on completed top-level and actual subagent invocations. OS account and host are resource attributes, not necessarily span attributes.

This is an illustrative reduced payload, not a literal transcript or an assertion that each export batch contains all these fields:

{
  "resourceSpans": [{
    "resource": {
      "attributes": [
        { "key": "service.name", "value": { "stringValue": "copilot-chat" } },
        { "key": "process.user.name", "value": { "stringValue": "<OS account>" } },
        { "key": "host.name", "value": { "stringValue": "<machine name>" } }
      ]
    },
    "scopeSpans": [{
      "scope": { "name": "copilot-chat" },
      "spans": [{
        "name": "invoke_agent <agent name>",
        "attributes": [
          { "key": "gen_ai.operation.name", "value": { "stringValue": "invoke_agent" } },
          { "key": "copilot_chat.session_id", "value": { "stringValue": "<session ID>" } },
          { "key": "user.name", "value": { "stringValue": "<authenticated account name>" } }
        ]
      }]
    }]
  }]
}

user.name comes from the current authenticated GitHub session's account.label, not a newly introduced numeric ID or email attribute. Do not count an embedded runtime's span as proof that Local's own instrumentation passed.

With captureContent: false, known external Local span content attributes such as gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, gen_ai.tool.definitions, gen_ai.tool.call.arguments, and gen_ai.tool.call.result must be absent. Prompt/response content such as IDENTITY_TEST_OK must not remain in those fields or the filtered content-event attributes. Events themselves may remain. This is not a claim of universal payload sanitization, and local debug retention is separate. Collector output intentionally contains identifying data in this test; review it before sharing.

Negative and lifecycle cases

Finish each script run and reload/close the editor before starting a fresh test session. Change the downloaded script between runs, never the live managed-settings file; its cleanup deliberately refuses to remove a modified file.

Case Change / action Expected result
Default off Omit capture from the test payload; no personal/environment identity opt-in No automatic user.name, process.user.name, or host.name in Local exports.
Managed denial Use capture: { identity: false }; set personal github.copilot.chat.otel.captureIdentity: true and launch the editor with COPILOT_OTEL_CAPTURE_IDENTITY=true Ordinary Local spans still arrive, but all three governed identity keys are absent, including any explicitly configured identity-resource values.
Omission is not denial Omit capture; launch the fresh editor with COPILOT_OTEL_CAPTURE_IDENTITY=true Identity is present. An absent managed leaf does not block opt-in.
Content independent Keep identity: true, captureContent: false Identity is present without prompt/response/tool content in external spans.
Current account Change the account in the disposable setup, wait for authentication-session refresh, then invoke again Spans read that current session. Once signed-out state is reflected by the authentication service, they omit authenticated user.name.
Policy withdrawal Use the collector-only continuation below after the identity-enabled script removes its own policy Subsequent Local export/event filtering removes identity without a reload. Already exported data cannot be recalled.
Whole-block precedence In an authorized multi-source fixture, a stronger source supplies a declared telemetry key but omits identity, or a server/file object block is empty, while a weaker block enables identity The lower block's identity does not leak through. An omitted identity leaf is not itself an explicit denial of a personal opt-in. The file-only script does not exercise this case.

Cleanup: The script removes only its own unchanged test file after 120 seconds or Ctrl+C, then stops the collector. Reload or close the test editor after each run. If the script is forcibly killed or its file changes, inspect the remaining file manually; do not delete unrelated policy.

Observe withdrawal without losing the collector: let the identity-enabled script finish, confirming Test policy removed; original absence restored. Do not reload the editor. Restart only its exported collector helper, without sudo or policy installation:

node -e 'const { createCollector } = require("./otel-restart-test.cjs"); createCollector().listen(4318, "127.0.0.1", () => console.log("Collector ready; no test policy installed"));'

Wait until the editor reflects policy removal, then send another harmless Local request without a personal/environment opt-in. The existing exporter can continue sending allowed telemetry, but the three governed identity attributes must be absent from new Local output. Ignore pre-removal batches when assessing this case. Stop the collector with Ctrl+C and reload/close the editor afterward. Do not edit the active test policy to simulate revocation.

Validation and remaining draft work

The final follow-up tree, committed as 6cb2923dcef, was validated with the repository's normal runners on Node 24.18.0:

  • 312 extension tests passed across 21 files, including identity, content filtering, configuration replacement/withdrawal, stale configuration, actual tool-loop identity, inline chat, and neighboring OTel suites.
  • 183 combined core results passed (182 tests plus the runner sanity check), covering managed-settings projection/source selection, file/native observation, nullable policy references, account adaptation, and live account-policy replacement/withdrawal.
  • Policy Diagnostics Markdown escaping/rendering check passed.
  • Extension typecheck, changed-source lint, core transpilation, and git diff --check passed.

This supersedes the earlier Node 26 runner and follow-up lint blockers. The native matrix now tests representable declared keys instead of claiming a real native empty-object signal.

Targeted test commands

Use Node 24.18.0. From extensions/copilot:

npm run test:unit -- src/platform/otel src/extension/otel/common/test src/extension/intents/test/node/toolCallingLoopIdentity.spec.ts src/extension/inlineChat2/test/node/inlineChatIntent.spec.ts
./node_modules/.bin/tsc -p tsconfig.json --noEmit

From the repository root, with the required source/build paths present:

npm run transpile-client
npm run test-node -- \
  --run src/vs/platform/policy/test/common/copilotManagedSettings.test.ts \
  --run src/vs/platform/policy/test/common/fileManagedSettingsService.test.ts \
  --run src/vs/platform/policy/test/node/nativeManagedSettingsService.test.ts \
  --run src/vs/platform/configuration/test/common/policyConfiguration.test.ts \
  --run src/vs/workbench/services/accounts/test/browser/managedSettings.test.ts \
  --run src/vs/workbench/services/policies/test/browser/accountPolicyService.test.ts
npm run test-node -- --run src/vs/workbench/test/browser/actions/policyDiagnosticsMarkdown.test.ts

The account and diagnostics suites above ran with the Node runner, not in an Electron/browser session. Exporter cases use recording exporters through the real service/wrapper initialization path, not a live collector.

Still outstanding: live full-editor/OS-policy testing and the manual collector flow above, full Policy Diagnostics UI/transport verification, policy-export generation, and the Git-LFS-dependent generic tool-loop suites. Prior full core typechecking was blocked by missing Electron prerequisites; it was not rerun as a passing check for this follow-up. Targeted passes are not a claim that the full build, live rollout, or all CI checks pass.

Revocation cannot recall already persisted/exported data or cancel a batch already handed to transport. Enabling identity still uses the existing reload/recovery lifecycle.

The PR remains draft. Current agent commits are unsigned; review and a user-signed approval checkpoint remain outstanding.

Keep account and resource identity default-off, enforce explicit managed denial at all export boundaries, and preserve late-policy recovery behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve empty and future-only telemetry block presence through normalization and serialization. Keep unrelated policy keys and sandbox precedence unchanged, and observe native content controls only for source selection.

Add normalization, multi-source, transport, and policy-update regressions. Core execution and follow-up lint remain blocked in the local validation environment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Base automatically changed from rwoll/otel-late-policy-recovery to main September 18, 2026 20:03
@rwoll
Ross Wollman (rwoll) requested a balanced review from Copilot September 18, 2026 20:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Identity can be re-enabled without reload, and tool descriptions can leak while content capture is disabled.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity · 1 Low severity

Open (3)
What changed in this PR

Adds independently governed identity capture to Local Copilot OTel while preserving managed-policy precedence and immediate revocation.

Changes:

  • Adds identity policy projection and whole-block telemetry source selection.
  • Filters identity across OTel spans, resources, logs, metrics, SQLite, and local events.
  • Adds identity attribution, documentation, and regression coverage.
File Description
src/​vs/​workbench/​services/​policies/​test/​browser/​accountPolicyService.test.ts Tests telemetry source replacement.
src/​vs/​workbench/​services/​accounts/​test/​browser/​managedSettings.test.ts Tests server adaptation.
src/​vs/​workbench/​services/​accounts/​browser/​managedSettings.ts Types identity capture responses.
src/​vs/​platform/​policy/​test/​node/​nativeManagedSettingsService.test.ts Tests native capture controls.
src/​vs/​platform/​policy/​test/​common/​fileManagedSettingsService.test.ts Tests telemetry block preservation.
src/​vs/​platform/​policy/​test/​common/​copilotManagedSettings.test.ts Tests projection and precedence.
src/​vs/​platform/​policy/​common/​copilotManagedSettings.ts Implements policy selection and normalization.
src/​vs/​platform/​configuration/​test/​common/​policyConfiguration.test.ts Tests nullable policy references.
src/​vs/​platform/​agentHost/​OTEL.md Documents policy ownership.
src/​vs/​platform/​agentHost/​common/​agentHostStarter.config.contribution.ts Registers the shared policy.
extensions/​copilot/​src/​platform/​otel/​node/​test/​otelIdentity.spec.ts Tests identity filtering.
extensions/​copilot/​src/​platform/​otel/​node/​otelServiceImpl.ts Enforces identity at OTel boundaries.
extensions/​copilot/​src/​platform/​otel/​node/​otelIdentityExporters.ts Adds filtering exporters.
extensions/​copilot/​src/​platform/​otel/​node/​inMemoryOTelService.ts Filters local event delivery.
extensions/​copilot/​src/​platform/​otel/​common/​test/​otelConfigResolution.spec.ts Tests identity resolution.
extensions/​copilot/​src/​platform/​otel/​common/​test/​agentOTelEnv.spec.ts Tests environment isolation.
extensions/​copilot/​src/​platform/​otel/​common/​otelIdentity.ts Adds identity helpers.
extensions/​copilot/​src/​platform/​otel/​common/​otelConfigResolution.ts Resolves policy-aware settings.
extensions/​copilot/​src/​platform/​otel/​common/​otelConfig.ts Adds identity configuration.
extensions/​copilot/​src/​platform/​otel/​common/​genAiAttributes.ts Defines identity attributes.
extensions/​copilot/​src/​platform/​otel/​common/​agentOTelEnv.ts Documents environment boundaries.
extensions/​copilot/​src/​platform/​configuration/​common/​configurationService.ts Registers the nullable setting key.
extensions/​copilot/​src/​extension/​prompt/​node/​searchSubagentToolCallingLoop.ts Supplies authentication state.
extensions/​copilot/​src/​extension/​prompt/​node/​executionSubagentToolCallingLoop.ts Supplies authentication state.
extensions/​copilot/​src/​extension/​prompt/​node/​defaultIntentRequestHandler.ts Supplies authentication state.
extensions/​copilot/​src/​extension/​prompt/​node/​codebaseToolCalling.ts Supplies authentication state.
extensions/​copilot/​src/​extension/​otel/​common/​test/​otelStaleConfigMonitor.spec.ts Tests revocation drift.
extensions/​copilot/​src/​extension/​mcp/​vscode-node/​mcpToolCallingLoop.tsx Supplies authentication state.
extensions/​copilot/​src/​extension/​intents/​test/​node/​toolCallingLoopIdentity.spec.ts Tests invocation attribution.
extensions/​copilot/​src/​extension/​intents/​node/​toolCallingLoop.ts Attributes agent spans.
extensions/​copilot/​src/​extension/​inlineChat2/​node/​inlineChatIntent.ts Attributes inline spans.
extensions/​copilot/​src/​extension/​extension/​vscode-node/​services.ts Adds live permission checks.
extensions/​copilot/​package.json Contributes the identity setting.
extensions/​copilot/​docs/​monitoring/​agent_monitoring.md Documents governed capture.
build/​lib/​policies/​policyData.jsonc Exports policy metadata.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +311 to +312
builder.define(IOTelService, new NodeOTelService(otelConfig, logFn, otelConfig.dbSpanExporter ? otelSqliteStore : undefined,
() => otelConfigResolver.resolve().config.captureIdentity));
Comment on lines +18 to +20
GenAiAttr.INPUT_MESSAGES, GenAiAttr.OUTPUT_MESSAGES, GenAiAttr.SYSTEM_INSTRUCTIONS,
GenAiAttr.TOOL_DEFINITIONS, GenAiAttr.TOOL_CALL_ARGUMENTS, GenAiAttr.TOOL_CALL_RESULT,
CopilotChatAttr.USER_REQUEST, CopilotChatAttr.REASONING_CONTENT, CopilotChatAttr.PROMPT_CONTEXT,
settingProtocol: read<string>('protocol') || undefined,
settingServiceName: read<string>('serviceName') || undefined,
settingResourceAttributes: read<Record<string, string>>('resourceAttributes'),
policyResourceAttributes: hasEnterpriseSettings ? read<Record<string, string>>('resourceAttributes') : undefined,
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Opt-in only] open telemetry should include some PII data in GitHub Copilot in VS Code

2 participants