Skip to content

MCP consent/egress audit events lose invocation attribution (unattributed correlation_id/task_id) #366

Description

@initializ-mk

Sibling to #338 (subprocess egress attribution) and #341 (egress sequence numbers). Even with invocation IDs on subprocess egress, the MCP delegated-consent path emits several audit events with no correlation_id/task_id, so they surface as "unattributed" instead of grouping under the invocation that caused them.

Observed (Atlassian write, one Slack invocation d3446f5b…)

The invocation shows an attributed Consent resolved + Egress allowed mcp.atlassian.com, but an "Other · unattributed" bucket holds duplicates/siblings of the same flow:

Other · unattributed events (7)
  MCP server started
  Egress allowed  mcp.atlassian.com          (11:31:13 — startup)
  Egress allowed  agent-builder…svc           (11:34:34)
  Egress allowed  mcp.atlassian.com  ×2        (11:34:53)
  Consent resolved                            (11:34:53)   ← also appears attributed in the invocation
  Egress allowed  agent-builder…svc           (11:34:53)

So Consent resolved and the consent-path egress are double-counted: one attributed copy under the invocation, one unattributed copy in "Other."

Root cause — three buckets

A. mcp_auth_resolved is emitted up to twice, and 2 of 3 sites drop attribution

Three emission sites for EventMCPAuthResolved:

Site Path Emit Attributed?
mcp_authgate.go:97 the parked Await goroutine waking EmitFromContext(ctx) + explicit correlationID/taskID ✅ yes
mcp_consent_callback.go:296 loopback GET /mcp/oauth/callback (via: loopback_callback) plain Emit() ❌ no
mcp_authgate.go:225 POST /mcp/consent (via: consent_endpoint) plain Emit() ❌ no

On every resume the waking gate (site 1) already emits an attributed mcp_auth_resolved; the resume channel (site 2 standalone / site 3 managed+Slack) emits a second, unattributed copy on a different request (the browser callback / the platform POST), which has none of the parked invocation's context. → the duplicated + unattributed Consent resolved.

B. In-process MCP / token egress runs on detached contexts

The egress enforcer attributes via EmitFromContext(ctx) (runner.go:700), so an egress_allowed is only attributed if the outbound call carries the invocation's correlation on its ctx. These completion-path calls don't:

  • Loopback callback token exchangecomplete(req.Context(), …) (mcp_consent_callback.go:287) runs the code→token HTTP call on the browser callback request context (auth-exempt, no invocation correlation) → the unattributed agent-builder… (IdP/token endpoint) egress.
  • Platform token resolver (doPlatformTokenRequest, FetchAuthorizeURL) and MCP client re-connect / token refresh after resume — made outside the parked invocation's ctx → the unattributed mcp.atlassian.com egress at resume time.

C. Startup MCP egress (expected, not a bug)

mcp_server_started + the first mcp.atlassian.com egress at 11:31:13 fire before any invocation exists, so they are legitimately unattributed. (Optional: give the UI a distinct "system/startup" bucket rather than "Other.")

Why the resume side can't attribute today

The gate Spec (carried by the first waiter, authgate.go:73-82) stores TaskID + Session but not CorrelationID. So Peek(subject, server) can recover the parked TaskID but not the correlation — only site 1 (the waking goroutine's local ctx) has it.

Proposed fix

  1. De-duplicate mcp_auth_resolved (bucket A). Make the waking gate (site 1) the single source of truth; drop the resume-side emissions at mcp_consent_callback.go:296 and mcp_authgate.go:225 (or, if the via distinction is wanted, move it onto site 1's fields). Eliminates both the duplication and the unattributed copies.
  2. Carry correlation on the gate + seed completion contexts (bucket B). Add CorrelationID to authgate.Spec so it travels with the first waiter; at resume (callback + POST /mcp/consent) recover {correlationID, taskID} via Peek and seed the ctx (coreruntime.WithCorrelationID/WithTaskID) used for the token exchange, platform resolver, and MCP re-resolution, so their egress_allowed events attribute to the original invocation.
  3. Leave startup egress unattributed (bucket C); optionally bucket it as "system" in the Security UI.

Related

Subprocess egress attribution #338 · egress sequence #341 · TCP egress #337 · auth-required gate #330 · consent delivery #343.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingforge-cliAffects the forge-cli command-line tool (init, run, build, mcp commands)platformInitializ Platform (governance control plane) responsibility

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions