docs: Plugin architecture proposal#342
Conversation
Proposes an extensible filter pipeline for AuthBridge with: - Single Filter interface (OnRequest/OnResponse on shared Context) - Mutations directly on context (headers, body, values) - Mode-agnostic pipeline with thin adapters per deployment mode - Registry-based compiled-in plugin loading (v1) - Protocol parsing as a composable filter (not a special layer) - Tighten-only policy (plugins cannot weaken built-in security) - Praxis alignment for potential collaboration Signed-off-by: Hai Huang <hai@us.ibm.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
- Rename "filter" to "plugin" throughout — AuthBridge extensions are security-aware plugins, not generic stream filters - Add "Current Architecture" section grounding the proposal in actual codebase (Auth struct, existing adapter pattern, func type precedent) - Add AgentIdentity to Context — dual SPIFFE/OAuth identity gives plugins security context no external proxy can provide - Add ResolvedRoute to Context for destination-aware policy decisions - Add "Tighten-Only Enforcement" section with concrete mechanisms (required plugins, protected registry, read-only claims) - Add "Observability by Default" section — auto-instrumented spans and metrics per plugin invocation - Add "Identity-Aware Protocol Policy" showing tool-level authorization via MCP parser + tool-policy plugin composition - Add concrete Praxis integration models (A/B/C) with recommendation - Sharpen migration path with real before/after code from ext_proc listener and mapping table from existing Auth fields to built-in plugins - Clarify body access as hard mode constraint (ext_authz: never, ext_proc: requires BUFFERED config, proxy: always) - Add WASM to future loading mechanisms, multi-turn sessions to open questions Signed-off-by: Hai Huang <huang195@gmail.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
- Remove "Observability by Default" section — too prescriptive about implementation details for an architecture proposal - Make AgentIdentity generic: WorkloadID + TrustDomain instead of SPIFFEID. Works with SPIFFE, k8s SA, or any workload identity scheme. - Reframe Praxis section: position as Envoy replacement where AuthBridge benefits indirectly (universal body access, pre-parsed protocol metadata, simpler adapter layer) rather than peer integration models - Update tool-policy example to use trust_domain instead of SPIFFE pattern Signed-off-by: Hai Huang <huang195@gmail.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
|
This is a great start, and something that I think we can help build on a deliver. Many of the requirements listed here are things we've worked through with CPEX, and learned what works well and what doesn't work. Here is what I would suggest at a high level, and then I'll give a few more comments. Let us write up a proposal / spec for how to deliver CPEX to provide the plugin capabilities for AuthBridge and share it early next week. This will mirror what we already did for Praxis and Mellea and ContextForge. Given the rewrite in Rust, we can support AuthBridge / Go using Rust + C ABI + Go (cgo). Some things we can then work through are the placement of the hooks (ingress and egress, return response, etc.) and things like the payload and how to configure it. For things like the The other thing this will provide is robust prioritized, parallel, and sequential pipelines with varying semantics (fail on error, fire-and-forget, async versus blocking, etc.). This is all stuff you'll get for free. Let's see how we can provide these capabilities for automatically. Smaller, comments:
|
|
Thanks Ian — this is really helpful feedback, and the timing is great. A few responses:
Policy language — Great point about moving beyond custom YAML rules. Since we're already in the Kuadrant ecosystem (MCP Gateway uses Kuadrant), one option we're exploring is integrating with Authorino for policy evaluation. AuthBridge's role would be enriching the authorization context with protocol-level data (tool name, arguments, method) that Authorino can't extract on its own, then delegating the policy decision via ext_authz. This would give us OPA/Rego, pattern-matching, and SpiceDB support through existing infrastructure. Plugin interface and CPEX integration — Our current thinking is to keep the initial pipeline implementation in Go to match the rest of the AuthBridge codebase and keep the build simple. That said, we want to design the Plugin interface to be open: type Plugin interface {
Name() string
Capabilities() PluginCapabilities
OnRequest(ctx *Context) Action
OnResponse(ctx *Context) Action
}If CPEX provides a bridge implementation behind this interface — bringing the Rust runtime, scheduling, and CMF — that would work well. The interface is the contract, and CPEX would be a first-class way to implement it. This is similar to how CPEX adapts to Praxis and Mellea's extension models. We'd love to see how this could look concretely. Other points:
Would love to see the CPEX spec for AuthBridge when it's ready next week. Having a concrete proposal to compare against will help us find the right integration points. |
|
@huang195 Thanks for sharing the initial proposal! Building on your draft and @imolloy's feedback on CPEX integration, I put together a concrete spec that wires CPEX into AuthBridge as the hook system and plugin runtime: In short, the spec defines typed hooks across the AuthBridge lifecycle (startup, inbound JWT validation, outbound token exchange, and cross-cutting audit), with CPEX embedded in-process via Go bindings to the Rust core. Plugins declare capabilities that gate which Extensions they can access (Security, Http, Delegation, etc.), and all policy results compose through a tighten-only monoid; plugins can deny but never override a deny to allow. It also addresses several points from the discussion: typed extension slots instead of I'd be happy to team up and refine it to cover any missing requirements, adjust scope, and incorporate feedback from others. |
|
@araujof Thanks for putting this together — the spec is thorough and clearly builds on lessons learned from the Praxis and Mellea integrations. We've been thinking about how CPEX fits into AuthBridge's architecture, and we'd like to explore an approach where CPEX runs as a plugin within AuthBridge's Go pipeline rather than replacing it. The idea: Why this direction:
What CPEX can do from this position:
What would stay as native Go plugins:
Lifecycle hooks: We are open to adding Questions for discussion:
Would be great to discuss this in more detail — happy to set up time if that is easier than async. |
| 5. **Declare what you need** — body access is opt-in per plugin; the pipeline | ||
| only buffers the body when at least one plugin requests it | ||
| 6. **Identity-first** — every plugin receives the agent's own identity and | ||
| the caller's validated claims, enabling security decisions no external |
There was a problem hiding this comment.
How will outbound plugins become aware of the caller's validated claims? Don't we need to solve #182 first?
There was a problem hiding this comment.
Yeah, that's a good point. Per the issue you linked, the easiest path forward is to propagate the token from inbound path to outbound, so it can be used to correlate sessions. Other suggestions are welcome.
There was a problem hiding this comment.
FYI: Some services use multiple credentials. For example, a bookstore checkout service might use the user's credentials for billing and shipping, but use the bookstores credentials to generate advertisements for similar books. In that scenario, maybe there is NO incoming request and NO incoming token.
|
Thanks @huang195! The design direction you described using a "cpex-bridge" makes a lot of sense and aligns well with the spec, and I think we can converge cleanly. It would be great to find time to brainstorm next steps in more detail. A couple of areas worth digging into are (1) how we define the payloads and extensions (metadata) created at the predefined AuthBridge hook call sites before invoking the CPEX plugin pipeline, and (2) the wire protocol for handling results (e.g., continuation, payload mutation, abort semantics). Agreement on the layered modelYour framing maps well to the assumptions in the spec: AuthBridge's "cpex-bridge" request flow could look like the following at each hook point (sketch): // 1. AuthBridge constructs typed payload + extensions from call-site context
payload := &InboundPayload{...}
extensions := &cpex.Extensions{
Meta: &cpex.MetaExtension{
EntityType: "request",
EntityName: req.URL.Path,
Tags: routeTags,
},
Security: &cpex.SecurityExtension{
Agent: &cpex.AgentIdentity{
ClientID: cfg.ClientID,
WorkloadID: cfg.SpiffeID,
TrustDomain: cfg.TrustDomain,
},
Token: authHeader, // capability-gated: only plugins with read_token see this
},
Http: &cpex.HttpExtension{
Method: req.Method,
Path: req.URL.Path,
Host: req.Host,
RequestHeaders: req.Header,
},
}
// 2. Invoke CPEX: the PluginManager dispatches to all registered plugins
// (guardrails, policy engines, audit logger, etc.)
result, bg, err := mgr.Invoke("inbound.pre_validation", payload, extensions, ctxTable)
if err != nil {
return abortWithError(err)
}
// 3. Enforce tighten-only: a deny from any plugin is final
if !result.ContinueProcessing {
return denyRequest(result.Violation)
}
// 4. Apply mutations (if payload was modified by a transform-mode plugin)
if result.ModifiedPayload != nil {
applyPayloadMutations(result.ModifiedPayload)
}
// 5. Thread context table into the next hook for state continuity
ctxTable = result.ContextTable
// 6. Background tasks run outside latency budget
defer bg.Wait()Answering your questionsDoes CPEX work well as an embedded sub-pipeline?Yes, this is the designed integration model. The Go bindings proposal (
CPEX handles everything inside the invoke boundary (route resolution, phase ordering, parallel dispatch, capability gating, context management). AuthBridge owns everything outside (lifecycle, payload construction, result enforcement, tighten-only semantics). The FFI boundary What does the Go-side integration surface look like?The API is minimal: // Lifecycle, initializes the plugin manager from configuration, loads and registers plugins to hook points
mgr, err := cpex.NewPluginManagerFromFile("plugins.yaml")
mgr.Initialize()
defer mgr.Shutdown()
// Invocation (typed, deserializes result payload into P)
result, bg, err := cpex.Invoke[InboundPayload](mgr,
"inbound.post_validation", payload, extensions, ctxTable)How do two capability-gating layers compose?The spec already relies on CPEX's capability model for all extension access. The Go pipeline doesn't implement a parallel gating mechanism; it constructs the full Where the Go layer adds value is in controlling what it puts into Extensions in the first place. For example, What this gives us concretelyThe layered model unlocks several capabilities:
Next stepsI'd suggest we:
|
|
Thanks Fred — good to see alignment on the layered model. On extensions mapping: we've gone through CPEX's extension types and not everything is relevant to AuthBridge (e.g., CompletionExtension, LLMExtension, FrameworkExtension are application-level concerns we don't handle at the proxy layer). But for the ones that are relevant, we're defining our own Go structs that keep the same shape to make the translation layer at the bridge boundary as thin as possible:
The Go pipeline populates these before calling into the CPEX bridge. Because the shapes align, the bridge adapter should be a straightforward field-to-field mapping. Agree on starting small for next steps. |
mrsabath
left a comment
There was a problem hiding this comment.
Well-structured plugin architecture proposal. All codebase references verified against current main — the Auth struct composition, ext_proc listener line count (~170 → actual 174), and directory structure all check out. The design principles (single interface, tighten-only enforcement, mode-agnostic adapters) are sound, and the active CPEX integration discussion shows good community convergence.
Areas reviewed: Documentation accuracy, codebase reference verification, security (no secrets), commit conventions
Commits: 3 commits, all signed-off ✓
CI: All checks passing ✓
| "path", ctx.Path, | ||
| "agent", ctx.Agent.ClientID, | ||
| "caller", ctx.Claims.Subject) | ||
| } |
There was a problem hiding this comment.
suggestion: ctx.Response is referenced here, but the Context struct definition (line 87) has no Response field — it only defines request-side fields (Method, Host, Path, Headers, Body) plus identity fields (Agent, Claims, Route) and Values.
Consider either:
- Adding a
Response *ResponseInfofield to theContextstruct definition, or - Reworking this example to use fields that exist on the proposed Context (e.g.,
ctx.Values["response_status"])
Minor inconsistency — doesn't block the proposal.
| forward proxy listeners are similar. The adapter pattern already exists in | ||
| the codebase — this proposal formalizes it and opens it to extension. | ||
|
|
||
| The `ActorTokenSource` and `AudienceDeriver` function types are the existing |
There was a problem hiding this comment.
praise: Good grounding — the "~170 lines" claim for ext_proc checks out (actual: 174 lines) and the Auth struct composition matches current main. Anchoring proposals in verifiable codebase facts makes the doc credible and reviewable.
|
More addressing the broader discussion: (1) Since there’s been the push in previous solutions on BBR - I think it’s worth addressing in this earlier design and discussion stage how this solution here differentiates and what use cases it enables to justify the complexity Specifically https://github.com/kubernetes-sigs/gateway-api-inference-extension/tree/main/pkg/bbr/framework has the
(2) For the request -> response ordering, if I’m understanding correctly, currently the example Go pipeline should end up with: Request: jwt-validation -> mcp-parser -> guardrail through cpex-bridge -> token-exchange Particularly on the guardrail through cpex-bridge -> mcp-parser OnResponse runs, if a tool call response had to be parsed for a guardrail, should each guardrail be expected to work on (unmarshal, etc) pctx.Body and not a parsed MCP tool result, or likely risk duplicating mcp-parser logic? The current extension metadata structs also seem fairly catered towards requests. Some consistency could be created through the cpex bridge eventually, but not account for the plugins generally - is this the intended experience? |
|
Thanks Evaline — both good points. (1) Differentiation from BBR They operate at different layers. BBR is inference routing — model selection, body transformation, load balancing. AuthBridge's pipeline is auth-layer: JWT validation, RFC 8693 token exchange, SPIFFE delegation chains, and protocol-level authorization (is this agent allowed to call tool X with these scopes?). BBR doesn't handle workload identity, per-hop token exchange with audience narrowing, or delegation chain tracking. The two could coexist — BBR at the inference gateway, AuthBridge at the agent-to-agent auth boundary. We should make this layering explicit in the doc. (2) Response ordering and guardrails You're right — strict LIFO creates a problem. If guardrails run OnResponse before mcp-parser, they'd have to re-parse MCP tool results from raw body bytes. The plan is to support explicit response ordering separate from request ordering:
The extension metadata also needs response-side variants — tool result, status, output content — so guardrails work with |
|
Thanks @huang195! I had a couple more questions on the response ordering details
With the protocol plugins being associated with their parsers, is there going to be a way for plugin writers to ‘declare’ what protocols their plugins should work with - mcp, a2a, potentially both (if possible) since that would affect what parsers should be expected to run first?
And also related to the above, overall, how should a persona configuring the AuthBridge plugins know how to do the plugin ordering? Will they need deep knowledge of each plugin or be able to quickly inspect some info (specifically is it enough to just be based on the extension.x.result input to the plugin?) |
|
@huang195 we finished the first version of CPEX Go. I created a short specification of the implemented public API in this document. It contains a summary of the consumption API, and includes examples so you can see how it works. Our next step is to agree on the interface for the AuthBridge plugin and data types. Once we have that, we will refactor our initial spec targeting |
|
closing this PR as it's easier just to iterate on the current code instead of over design. |
Summary
Adds
docs/plugin-architecture.md— a design proposal for an extensible plugin pipeline in AuthBridge.Key design decisions
PluginwithOnRequest(ctx)/OnResponse(ctx)— no layered abstractionsAgentIdentity(SPIFFE ID + client_id) alongside callerClaims— security decisions no external proxy can makectx.Headers/ctx.Bodydirectly, return Continue or Rejectctx.Values, enabling tool-level authorization via compositionWhat changed in v2
Authstruct, real listener code, real migration seamsDiscussion points
Assisted-By: Claude (Anthropic AI) noreply@anthropic.com