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 exchange —
complete(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
- 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.
- 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.
- 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.
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:So
Consent resolvedand 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_resolvedis emitted up to twice, and 2 of 3 sites drop attributionThree emission sites for
EventMCPAuthResolved:mcp_authgate.go:97Awaitgoroutine wakingEmitFromContext(ctx)+ explicitcorrelationID/taskIDmcp_consent_callback.go:296GET /mcp/oauth/callback(via: loopback_callback)Emit()mcp_authgate.go:225POST /mcp/consent(via: consent_endpoint)Emit()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 + unattributedConsent resolved.B. In-process MCP / token egress runs on detached contexts
The egress enforcer attributes via
EmitFromContext(ctx)(runner.go:700), so anegress_allowedis only attributed if the outbound call carries the invocation's correlation on its ctx. These completion-path calls don't:complete(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 unattributedagent-builder…(IdP/token endpoint) egress.doPlatformTokenRequest,FetchAuthorizeURL) and MCP client re-connect / token refresh after resume — made outside the parked invocation's ctx → the unattributedmcp.atlassian.comegress at resume time.C. Startup MCP egress (expected, not a bug)
mcp_server_started+ the firstmcp.atlassian.comegress at11:31:13fire 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) storesTaskID+Sessionbut notCorrelationID. SoPeek(subject, server)can recover the parkedTaskIDbut not the correlation — only site 1 (the waking goroutine's local ctx) has it.Proposed fix
mcp_auth_resolved(bucket A). Make the waking gate (site 1) the single source of truth; drop the resume-side emissions atmcp_consent_callback.go:296andmcp_authgate.go:225(or, if theviadistinction is wanted, move it onto site 1's fields). Eliminates both the duplication and the unattributed copies.CorrelationIDtoauthgate.Specso it travels with the first waiter; at resume (callback +POST /mcp/consent) recover{correlationID, taskID}viaPeekand seed the ctx (coreruntime.WithCorrelationID/WithTaskID) used for the token exchange, platform resolver, and MCP re-resolution, so theiregress_allowedevents attribute to the original invocation.Related
Subprocess egress attribution #338 · egress sequence #341 · TCP egress #337 · auth-required gate #330 · consent delivery #343.