Skip to content

IBAC: nil-pointer panic on outbound-first traffic; needs MCP housekeeping bypass #442

Description

@kellyaa

Summary

The IBAC plugin panics on every outbound request that arrives before any inbound A2A request has populated a session bucket. The panic blocks agent startup whenever the agent's first action is an outbound call (e.g., MCP initialize against an MCP server).

Two related bugs:

  1. Nil-pointer dereferenceIBAC.OnRequest calls pctx.Session.LastIntent() at authlib/plugins/ibac/plugin.go:252 without checking that pctx.Session is non-nil. pipeline.Context.Session is documented as nullable (authlib/pipeline/context.go:111 — "nil unless session tracking is enabled"), and the forward-proxy listener only assigns it when Sessions.ActiveSession() != "" (authlib/listener/forwardproxy/server.go:205-209).

  2. Lifecycle mismatch with outbound-first traffic — Even with a nil guard, IBAC has nothing useful to say about MCP protocol housekeeping (initialize, tools/list) that fires before any user intent has been declared. Currently every such request would be denied as no_intent, which makes IBAC unusable for any agent whose runtime opens an MCP connection at startup.

Reproduction

Pipeline (proxy-sidecar / authbridge-envoy:v0.6.0-alpha.4):

inbound:  [a2a-parser, jwt-validation]
outbound: [token-exchange, inference-parser, mcp-parser, ibac]

Agent does an MCP initialize to its tool server at startup, before serving any A2A traffic. Sidecar logs:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=...]

goroutine N [running]:
github.com/kagenti/kagenti-extensions/authbridge/authlib/pipeline.(*SessionView).LastIntent(...)
        /app/authlib/pipeline/session.go:336
github.com/kagenti/kagenti-extensions/authbridge/authlib/plugins/ibac.(*IBAC).OnRequest(0x..., ...)
        /app/authlib/plugins/ibac/plugin.go:252 +0x158

The agent never recovers — every retry of initialize panics the same way, so the MCP transport never opens.

Root Cause

Bug 1 — nil deref

plugin.go:248-252:

// 5. Pull the user's most recent declared intent. nil here means
//    either a2a-parser isn't in the inbound chain, or no user
//    message has been received yet — both are operator-error /
//    suspicious states. Fail closed.
intent := pctx.Session.LastIntent()

The comment treats "nil here" as covering both states, but it only handles LastIntent() == nil. When pctx.Session is itself nil (no inbound A2A has been processed yet on this pod), the dereference panics before the nil-intent check can run.

forwardproxy/server.go:205-209:

if s.Sessions != nil {
    if aid := s.Sessions.ActiveSession(); aid != "" {
        pctx.Session = s.Sessions.View(aid)
    }
}

ActiveSession() returns "" until an inbound A2A request seeds the active bucket — so pctx.Session legitimately stays nil for outbound-first traffic. The nullability is by design; the IBAC plugin just doesn't honor it.

Bug 2 — lifecycle mismatch

The single-tenant correlation model (session/store.go:285 ActiveSession(), comment at :309: "Assumes single-tenant, no concurrent conversations per pod") assumes outbound calls always follow an inbound A2A. That isn't true for:

  • MCP initialize / tools/list — connection setup that happens at agent startup
  • Any agent that performs warmup calls (LLM keepalive, tool discovery) before serving traffic
  • Background reconciliation loops

Even with the nil guard fixed, denying these as no_intent is wrong: there's no user-facing action to judge. They are protocol housekeeping.

Suggested Fix

Bug 1 (small, mechanical)

Add a nil guard at plugin.go:252. Either fold into the existing no_intent deny:

var intent *pipeline.SessionEvent
if pctx.Session != nil {
    intent = pctx.Session.LastIntent()
}
intentText := extractIntentText(intent)
if intentText == "" {
    // existing no_intent deny path
}

…or treat nil Session as a distinct reason (no_session) so dashboards can tell "session tracking off / first request" apart from "user spoke but said nothing useful".

Bug 2 (design)

Two options, not mutually exclusive:

a) Bypass MCP protocol-housekeeping methods. When mcp-parser populated pctx.Extensions.MCP and mcp.Method is one of initialize, notifications/*, tools/list, prompts/list, resources/list, IBAC should pctx.Skip("mcp_housekeeping") and continue. Only tools/call (and arguably prompts/get / resources/read) carry an actionable intent. This is analogous to the existing inference_bypass for the agent's own LLM reasoning.

b) Treat empty/nil Session on outbound as no_session_yet rather than no_intent, and decide policy via a config flag (e.g. deny_without_session: true default, false allows housekeeping through). This is more general but pushes the policy choice onto the operator.

Option (a) is the lower-risk fix — it matches the existing protocol-aware bypass shape (inference_bypass) and doesn't require new config surface.

Workarounds (today)

  • Add the MCP service host to bypass_hosts in the IBAC config — unblocks startup, but means real tools/call traffic to that host is also unjudged. Half-defeats the demo's purpose.
  • Pin to an authbridge build prior to IBAC's introduction — disables the feature.

Environment

  • Image: ghcr.io/kagenti/kagenti-extensions/authbridge-envoy:v0.6.0-alpha.4
  • Mode: proxy-sidecar (operator post-binary-split, container name authbridge-proxy)
  • Pipeline: as shown above
  • Workload: workload-harness/exgentic_a2a_runner agent connecting to an MCP tool server at startup

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions