Context
Follow-up to #1670 / PR #1765, which added a harness-specific extra passthrough to MCPDependency so keys like Claude Code's remote-MCP oauth block (clientId / callbackPort) survive apm.yml -> generated .mcp.json rendering instead of being silently dropped.
PR #1765 ships the bare-key ergonomics: any unknown top-level key on a self-defined MCP dependency is auto-captured into extra (matching the documented oauth: examples). It also adds a modeled-field-name denylist (a passthrough value can never shadow/redirect name/transport/command/url/headers/env/... or the Codex http_headers alias) so the concrete injection edge is closed.
The deferred tightening
The supply-chain-security review lens flagged that auto-capturing bare unknown keys is fail-open: a transitive dependency package's apm.yml is parsed straight through from_dict, and the prior contract dropped unknown keys (fail-closed) whereas the new contract preserves them. The denylist closes the high-severity shadow/redirect edge, but the residual posture question remains: should passthrough require an explicit extra: block and drop bare unknown keys with a warning (fail-closed), so only keys the author deliberately nests under extra: flow through?
This was deliberately sequenced out of #1765 by maintainer decision (Option B + denylist) because it is a user-facing authoring-contract change (the documented examples use bare top-level oauth:), i.e. a breaking change that needs a deprecation path rather than a silent flip.
Proposed scope (this issue)
- Introduce explicit
extra: as the only sanctioned passthrough surface.
- Deprecation window: keep capturing bare unknown keys, but emit a deprecation warning instructing authors to nest them under
extra:. After the window, drop bare unknown keys (restore fail-closed drop-and-warn).
- Rewrite the three doc examples (
install-mcp-servers.md, reference/manifest-schema.md, apm-usage/dependencies.md) to nest oauth: under extra:.
- Update
test_mcp_from_dict_unknown_keys.py expectations accordingly.
Also captured here (deferred from #1765, non-blocking)
- Per-harness scoping:
extra: { copilot: {...}, codex: {...} } so a Claude-specific block is not broadcast to every target.
- Secret-shaped-value scan: warn when an
extra value looks like a token/secret being written to a world-readable config.
to_dict collision debug-warn: emit a debug log when an extra key shadows a known field during re-serialization.
Why not now
Per the PR #1765 maintainer decision, #1765 keeps bare-key ergonomics + denylist (closes the concrete trust-boundary edge) and defers this fail-closed contract tightening here so the posture change is explicit, deprecation-gated, and reviewable on its own.
/cc the #1670 thread.
Context
Follow-up to #1670 / PR #1765, which added a harness-specific
extrapassthrough toMCPDependencyso keys like Claude Code's remote-MCPoauthblock (clientId/callbackPort) surviveapm.yml-> generated.mcp.jsonrendering instead of being silently dropped.PR #1765 ships the bare-key ergonomics: any unknown top-level key on a self-defined MCP dependency is auto-captured into
extra(matching the documentedoauth:examples). It also adds a modeled-field-name denylist (a passthrough value can never shadow/redirectname/transport/command/url/headers/env/... or the Codexhttp_headersalias) so the concrete injection edge is closed.The deferred tightening
The supply-chain-security review lens flagged that auto-capturing bare unknown keys is fail-open: a transitive dependency package's
apm.ymlis parsed straight throughfrom_dict, and the prior contract dropped unknown keys (fail-closed) whereas the new contract preserves them. The denylist closes the high-severity shadow/redirect edge, but the residual posture question remains: should passthrough require an explicitextra:block and drop bare unknown keys with a warning (fail-closed), so only keys the author deliberately nests underextra:flow through?This was deliberately sequenced out of #1765 by maintainer decision (Option B + denylist) because it is a user-facing authoring-contract change (the documented examples use bare top-level
oauth:), i.e. a breaking change that needs a deprecation path rather than a silent flip.Proposed scope (this issue)
extra:as the only sanctioned passthrough surface.extra:. After the window, drop bare unknown keys (restore fail-closed drop-and-warn).install-mcp-servers.md,reference/manifest-schema.md,apm-usage/dependencies.md) to nestoauth:underextra:.test_mcp_from_dict_unknown_keys.pyexpectations accordingly.Also captured here (deferred from #1765, non-blocking)
extra: { copilot: {...}, codex: {...} }so a Claude-specific block is not broadcast to every target.extravalue looks like a token/secret being written to a world-readable config.to_dictcollision debug-warn: emit a debug log when anextrakey shadows a known field during re-serialization.Why not now
Per the PR #1765 maintainer decision, #1765 keeps bare-key ergonomics + denylist (closes the concrete trust-boundary edge) and defers this fail-closed contract tightening here so the posture change is explicit, deprecation-gated, and reviewable on its own.
/cc the #1670 thread.