Skip to content

Pass OTEL_EXPORTER_OTLP_HEADERS env var to mcpg container; remove headers from gateway JSON config#32280

Merged
pelikhan merged 8 commits into
mainfrom
copilot/pass-otel-exporter-otlp-headers
May 15, 2026
Merged

Pass OTEL_EXPORTER_OTLP_HEADERS env var to mcpg container; remove headers from gateway JSON config#32280
pelikhan merged 8 commits into
mainfrom
copilot/pass-otel-exporter-otlp-headers

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 15, 2026

OTLP auth headers were being written into the stdin JSON config pipe as "headers": "${OTEL_EXPORTER_OTLP_HEADERS}", embedding credentials in a pipe where they could be logged or leaked. mcpg never received the env var directly, causing 401s against any collector requiring auth (Sentry, Datadog, Grafana Cloud).

Changes

mcp_setup_generator.go

  • Forward -e OTEL_EXPORTER_OTLP_HEADERS to the mcpg docker container whenever OTLPEndpoint != "", following the same pattern as GITHUB_AW_OTEL_TRACE_ID
  • Register OTEL_EXPORTER_OTLP_HEADERS in buildAddedGatewayEnvVarSet to prevent duplicate -e flags from HTTP MCP env deduplication

mcp_renderer.go

  • Remove the conditional "headers": "${OTEL_EXPORTER_OTLP_HEADERS}" line from the rendered opentelemetry JSON config block — headers are now exclusively container env

JSON schemas (pkg/workflow/schemas/ and docs/public/schemas/)

  • Remove headers property from opentelemetryConfig definition in both copies of mcp-gateway-config.schema.json

docs/src/content/docs/reference/mcp-gateway.md

  • Remove headers from §4.1.3.7 field table; add note directing to OTEL_EXPORTER_OTLP_HEADERS env var
  • Update T-OTEL-006, compliance requirements, Appendix A.6/A.7 examples, and add Version 1.14.0 changelog entry

Tests

  • TestRenderJSONMCPConfig_OTLPGateway: assert "headers" never appears in rendered JSON (regardless of OTLPHeaders value)
  • New TestOTLPHeadersEnvVarPassedToGatewayContainer: compiles a workflow with observability.otlp.headers, asserts -e OTEL_EXPORTER_OTLP_HEADERS appears in the docker command and no "headers" key in JSON config
  • New TestOTLPHeadersEnvVarNotPassedWithoutOTLP: asserts env var is absent when no OTLP is configured

Before (credentials in JSON pipe):

"opentelemetry": {
  "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}",
  "headers": "${OTEL_EXPORTER_OTLP_HEADERS}",   ← removed
  ...
}

After (credentials via container env):

docker run ... -e OTEL_EXPORTER_OTLP_HEADERS ...

Copilot AI and others added 2 commits May 15, 2026 03:54
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
…ve headers from gateway JSON config

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Add OTEL_EXPORTER_OTLP_HEADERS env var to mcpg container for authenticated export Pass OTEL_EXPORTER_OTLP_HEADERS env var to mcpg container; remove headers from gateway JSON config May 15, 2026
Copilot finished work on behalf of lpcox May 15, 2026 04:04
Copilot AI requested a review from lpcox May 15, 2026 04:04
@lpcox lpcox marked this pull request as ready for review May 15, 2026 04:10
Copilot AI review requested due to automatic review settings May 15, 2026 04:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR moves OTLP authentication headers out of the rendered MCP gateway stdin JSON config and into the mcpg container environment (OTEL_EXPORTER_OTLP_HEADERS) to avoid credential exposure and fix auth failures against collectors requiring headers.

Changes:

  • Forward OTEL_EXPORTER_OTLP_HEADERS to the gateway container when OTLP is configured, and dedupe it from other env-flag sources.
  • Stop rendering an opentelemetry.headers field in the gateway JSON config; remove headers from the gateway config JSON schemas.
  • Update docs and add/adjust tests to assert headers never appear in rendered gateway JSON.
Show a summary per file
File Description
pkg/workflow/schemas/mcp-gateway-config.schema.json Removes opentelemetry.headers from the internal gateway config schema.
docs/public/schemas/mcp-gateway-config.schema.json Mirrors the schema change for published docs artifacts.
pkg/workflow/mcp_setup_generator.go Adds -e OTEL_EXPORTER_OTLP_HEADERS when OTLP is enabled; registers it for env dedupe.
pkg/workflow/mcp_renderer.go Removes emission of opentelemetry.headers in rendered gateway JSON config.
pkg/workflow/mcp_renderer_test.go Updates assertions to ensure "headers" never appears in rendered gateway JSON.
pkg/workflow/mcp_setup_generator_test.go Adds coverage to ensure the env var is passed to the container and not rendered into JSON.
docs/src/content/docs/reference/mcp-gateway.md Removes JSON headers field docs and documents env-var-based auth headers.
.changeset/minor-otlp-headers-env-var.md Release note for the breaking change and migration guidance.
.github/workflows/linter-miner.lock.yml Regenerated workflow lock file (but currently still appears inconsistent with the new OTLP headers behavior—see comments).

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 9/9 changed files
  • Comments generated: 4

Comment on lines +737 to 740
esac
DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0')
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.9'

| `serviceName` | string | No | Logical service name reported in the `service.name` resource attribute of all emitted spans. Identifies the gateway in the tracing backend. Defaults to `"mcp-gateway"` when not specified. |

> [!NOTE]
> Authentication headers (e.g., `Authorization: Bearer <token>`) for the OTLP collector MUST be provided via the `OTEL_EXPORTER_OTLP_HEADERS` environment variable, not through the JSON config. This follows the standard [OTel SDK environment variable convention](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options) and keeps credentials out of the stdin config pipe. The format is `key1=value1,key2=value2` per the W3C Baggage header spec. When gh-aw compiles a workflow with `observability.otlp.headers`, the value is automatically forwarded to the gateway container via `-e OTEL_EXPORTER_OTLP_HEADERS`.
Comment on lines +14 to +17
OTLP auth headers (e.g., `Authorization: Bearer <token>`) are security-sensitive. The correct mechanism is the `OTEL_EXPORTER_OTLP_HEADERS` environment variable, which is:
- Isolated inside the container (not visible to processes outside)
- The standard OTel convention used by all OTel SDKs and collectors
- Not embedded in logs or config pipes where it could be inadvertently exposed
Comment thread pkg/workflow/mcp_renderer.go Outdated
@@ -213,13 +213,11 @@ func RenderJSONMCPConfig(
// the run block (RGS-008 compliance). All four fields use ${VARIABLE_NAME} expressions
// expanded by bash from workflow-level env vars.
// Per MCP Gateway Specification §4.1.3.6 and the opentelemetryConfig schema.
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100

Excellent test quality

Metric Value
New/modified tests analyzed 3
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 3 (100%)
Duplicate test clusters 0
Test inflation detected Yes (integration test adding full-pipeline coverage for a small production change)
🚨 Coding-guideline violations None

Test Classification Details

View All Test Classifications
Test File Classification Issues Detected
TestRenderJSONMCPConfig_OTLPGateway (modified) pkg/workflow/mcp_renderer_test.go:515 ✅ Design None — table-driven with 3 rows including no-OTLP edge case
TestOTLPHeadersEnvVarPassedToGatewayContainer (new) pkg/workflow/mcp_setup_generator_test.go:787 ✅ Design None — enforces security invariant: headers via env var, not JSON
TestOTLPHeadersEnvVarNotPassedWithoutOTLP (new) pkg/workflow/mcp_setup_generator_test.go:849 ✅ Design None — negative case; no env var when OTLP not configured

Analysis

TestRenderJSONMCPConfig_OTLPGateway was updated to reflect the behavioral change: the wantHeaders table field is removed and the assertion now verifies that "headers" never appears in the gateway JSON config (regardless of whether headers are provided). This is a clean contract test with three table rows covering endpoint-only, endpoint+headers, and no-OTLP scenarios.

TestOTLPHeadersEnvVarPassedToGatewayContainer is a new integration test that compiles a real workflow and inspects the generated YAML. It verifies the security-critical invariant: OTEL_EXPORTER_OTLP_HEADERS must be passed to the MCP gateway container via -e (not embedded in the JSON config pipe). Multiple complementary assertions cover both what should be present and what must be absent.

TestOTLPHeadersEnvVarNotPassedWithoutOTLP is the corresponding negative case — confirming the env var is not injected when no OTLP is configured, preventing unnecessary credential exposure.

i️ Test Inflation Note

mcp_setup_generator_test.go added 91 lines against mcp_setup_generator.go's 5 lines (+18:1 ratio), triggering the inflation flag. This is expected for integration tests: the production change is a small env-var addition to an existing complex code path, while the tests must compile a complete workflow and inspect the full YAML output. The inflation is justified, not a quality concern.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 3 tests — 1 unit (//go:build !integration), 2 integration (//go:build integration)
  • 🟨 JavaScript: No JavaScript test changes in this PR

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All three tests enforce observable behavioral contracts — particularly the security invariant that OTLP auth credentials travel via the container environment, not the JSON config pipe.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References:

🧪 Test quality analysis by Test Quality Sentinel · ● 6.1M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 90/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All three tests enforce observable behavioral contracts, particularly the security invariant that OTLP auth credentials travel via container env var, not the JSON config pipe.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 6.3M

@@ -515,28 +515,24 @@ func TestRenderJSONMCPConfig_OTLPGateway(t *testing.T) {
name string
otlpEndpoint string
otlpHeaders string
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.

[/tdd] The otlpHeaders field in the test struct is now dead data. Since the renderer no longer uses OTLPHeaders at all, the "OTLP endpoint only" and "OTLP endpoint and headers" test cases are functionally identical — both exercise the same code path and produce the same output. The field should be removed from the struct to make the test's intent clear:

tests := []struct {
    name         string
    otlpEndpoint string
    wantEndpoint bool
}{
    {
        name:         "OTLP endpoint configured",
        otlpEndpoint: "(otel.example.com/redacted)",
        wantEndpoint: true,
    },
    ...
}

Leaving it in suggests there's still some renderer behavior that varies with headers, which is misleading now that headers are exclusively an env var concern.

// Verify OTEL_EXPORTER_OTLP_HEADERS is NOT in the docker command
assert.NotContains(t, yamlStr, "-e OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_HEADERS should NOT be in docker command without OTLP config")
}
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.

[/tdd] Missing test: what happens when OTLP is configured but headers is empty/unset? Currently OTEL_EXPORTER_OTLP_HEADERS is unconditionally forwarded whenever OTLPEndpoint != "", regardless of whether any headers value is set. This is the right behavior (an empty env var is harmless), but a test case explicitly asserting it would pin the contract:

func TestOTLPHeadersEnvVarPassedToGatewayContainerWithoutHeaders(t *testing.T) {
    // observability.otlp configured with endpoint only, no headers
    // assert: -e OTEL_EXPORTER_OTLP_HEADERS still appears in docker command
    // (host env var will simply be empty at runtime — that's fine)
}

Without this test, a future reader could misread the intent of the buildAddedGatewayEnvVarSet registration and think the flag should only be added when headers are non-empty.

@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented May 15, 2026

@copilot address review feedback

@github-actions
Copy link
Copy Markdown
Contributor

Commit pushed: d21da1f

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · ● 6M

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (102 new lines in business directories, threshold 100) but does not have a linked Architecture Decision Record (ADR).

AI has analyzed the PR diff and generated a draft ADR to help you get started:

📄 Draft ADR: docs/adr/32280-pass-otel-headers-as-container-env-var.md

What to do next

  1. Review the draft ADR committed to your branch — it was generated from the PR diff and body
  2. Complete the missing sections — refine the decision rationale, verify the alternatives are real ones you considered, and confirm the consequences match your understanding
  3. Commit the finalized ADR to docs/adr/ on your branch (already created — edit in place)
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-32280: Pass OTEL_EXPORTER_OTLP_HEADERS as a Container Env Var

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. This PR is a credential-handling change with a real security and functional motivation — exactly the kind of decision that benefits from being recorded explicitly. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 32280-...md for PR #32280).

🔎 Why this PR triggered the gate
  • implementation label: not set
  • Net additions in business-logic directories (pkg/, internal/, etc.): 102 lines (threshold: 100)
  • Linked ADR in PR body: none found
  • Existing ADR file matching this PR number: none found before this run

A related but distinct ADR exists (ADR-28524: Object Form for observability.otlp.headers) — that one is about author-facing schema, this one is about transport mechanism. They're complementary, not duplicates.

🔒 This PR should not merge until an ADR is linked in the PR body.

References: §25899764158

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · ● 6M ·

… Baggage reference

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 15, 2026

@copilot address review feedback

Addressed all four review items in commit fix: address review feedback:

  • mcp_renderer.go:215 — Updated spec reference from §4.1.3.6 to §4.1.3.7.
  • docs/reference/mcp-gateway.md:508 — Replaced the "W3C Baggage header spec" reference with a description of the comma-separated key=value format per the OpenTelemetry OTLP exporter specification.
  • .changeset/minor-otlp-headers-env-var.md — Replaced the inaccurate "Isolated inside the container (not visible to processes outside)" bullet with the concrete benefit: credentials are not expanded into the stdin JSON config pipe or workflow logs.
  • linter-miner.lock.yml — Already contains -e OTEL_EXPORTER_OTLP_HEADERS in the MCP_GATEWAY_DOCKER_COMMAND at line 739; no change needed.

Copilot finished work on behalf of lpcox May 15, 2026 04:24
@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented May 15, 2026

@copilot merge main

…orter-otlp-headers

# Conflicts:
#	.github/workflows/linter-miner.lock.yml

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 15, 2026

@copilot merge main

Merged main into this branch. There was one conflict in .github/workflows/linter-miner.lock.yml (the MCP_GATEWAY_DOCKER_COMMAND line) — resolved by keeping our version which includes -e OTEL_EXPORTER_OTLP_HEADERS.

Copilot finished work on behalf of lpcox May 15, 2026 04:31
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot remove the copilot otel configuration to write to a file and the code to upload the otel data

….jsonl upload

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 15, 2026

@copilot remove the copilot otel configuration to write to a file and the code to upload the otel data

Done. Removed in the latest commit:

  • COPILOT_OTEL_FILE_EXPORTER_PATH env var injection from observability_otlp.go (and the now-unused constants import)
  • copilot-otel.jsonl from the artifact upload path list in compiler_yaml_main_job.go
  • CopilotOtelJsonlFilename constant from pkg/constants/job_constants.go
  • Corresponding test assertions updated in observability_otlp_test.go
  • linter-miner.lock.yml regenerated to reflect both removals

Copilot AI requested a review from pelikhan May 15, 2026 04:58
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot update the compiler to remove the step that uploaded the copilot-otel data

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 15, 2026

@copilot update the compiler to remove the step that uploaded the copilot-otel data

Done in the latest commit. Removed the await exportCopilotOtelTraces(core) call and its import from generate_observability_summary.cjs, and deleted export_copilot_otel_traces.cjs and export_copilot_otel_traces.test.cjs since they are no longer referenced anywhere.

@github-actions
Copy link
Copy Markdown
Contributor

@copilot please summarize remaining blockers and propose next steps. (pr-sous-chef run: https://github.com/github/gh-aw/actions/runs/25913427032)

Generated by 👨‍🍳 PR Sous Chef ·

@pelikhan pelikhan merged commit fc42039 into main May 15, 2026
@pelikhan pelikhan deleted the copilot/pass-otel-exporter-otlp-headers branch May 15, 2026 11:45
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.

Pass OTEL_EXPORTER_OTLP_HEADERS env var to mcpg container for authenticated OTel export

4 participants