Skip to main content

agentgateway + AxonFlow Integration

Prerequisites: AxonFlow Enterprise ≥ 9.7.0 (Getting Started), agentgateway v1.3.1+, the axonflow-gateway-adapters binary (built from the AxonFlow Enterprise source tree — see below).

Governance coverage
  • Interception point: agentgateway's three native external-policy seams — mcpGuardrails/ExtMcp (MCP method layer), extAuthz (Envoy ext_authz, HTTP headers), and extProc (Envoy ext_proc, HTTP bodies) — each calling the AxonFlow axonflow-gateway-adapters gRPC service, which translates to Decision Mode (/api/v1/decide + the engine's check-input/check-output endpoints).
  • Governs (can block): MCP tools/call (and other MCP methods) on both request and response; LLM/HTTP requests and responses via ext_proc; HTTP requests via ext_authz. Prompt and tool content is policy-checked and can be denied or engine-redacted. Exception: an ext_proc leg configured for streaming completions (responseBodyMode: none, adapter opt-in) governs the request only — the streamed response body is not scanned.
  • Fail-closed on both sides: agentgateway's seams default to failClosed/deny, and the adapters block on an unreachable PDP (request plane configurable; the response plane is unconditionally fail-closed).

Why pair agentgateway with AxonFlow

agentgateway (a Linux Foundation project) is a high-performance data plane for agentic traffic: MCP multiplexing, A2A, LLM provider routing, JWT validation, rate limiting — and its own inline guardrails. It is a Policy Enforcement Point.

What a data plane structurally does not carry is the regulated-market decision layer, and that is the layer AxonFlow provides as the Policy Decision Point its callouts consult:

  • Regulated-market PII decisioning — Indonesia NIK (checksum-validated, not regex-guessed), Singapore NRIC, Aadhaar/PAN, and per-category, per-tenant detection posture — decided centrally, not per-gateway-config.
  • Durable, tamper-evident audit lineage — every gateway callout produces a canonical decision record with decision_id, trace_id/correlation_id, and plane attribution, chained across the request and response planes and exportable as compliance evidence. A gateway log line disappears with the pod; the decision trail does not.
  • Compliance frameworks as policy — OJK / UU PDP (Indonesia), RBI & SEBI (India), MAS (Singapore), EU AI Act lineage — with cross-border transfer bases recorded on the decision.
  • Human-in-the-loop verdictsneeds_approval is a first-class PDP verdict, not a config branch.
  • One policy plane for every enforcement point — the same policies govern your SDKs, plugins (Claude Code, Cursor, Codex, Claude Desktop), and gateways; changing a policy changes them everywhere, with one audit trail.

The division of labor: agentgateway owns the data path; AxonFlow owns the decision and the evidence. AxonFlow never sits inline as a proxy — the gateway consults it at each governance touchpoint below and keeps forwarding traffic itself.

Architecture

┌────────────────────────── data plane ──────────────────────────┐
MCP / LLM │ agentgateway │ MCP servers /
clients ──▶│ mcpGuardrails (ExtMcp) extAuthz extProc │──▶ LLM providers /
│ │ gRPC │ gRPC │ gRPC │ HTTP upstreams
└────────┼───────────────────┼───────────────┼──────────────────┘
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ axonflow-gateway-adapters (gRPC) │ ← thin protocol translator,
│ ExtMcp · ext_authz · ext_proc (one binary) │ zero policy/redaction logic
└────────────────────────┬────────────────────────┘
▼ HTTPS
┌─────────────────────────────────────────────────┐
│ AxonFlow PDP — /api/v1/decide, │ ← verdicts, engine redaction,
│ /api/v1/mcp/check-input, …/check-output │ canonical audit lineage
└─────────────────────────────────────────────────┘

The adapters are deliberately thin: they translate wire formats and never make a policy or redaction decision themselves. A redacted payload is always the engine's bytes (the obligation two-touch flow). Each seam tells the PDP what it is able to do, so the PDP only ever asks it for work it can actually perform — see Seam capabilities.

The three seams

agentgateway seamLayerBlocksMutates contentAxonFlow contract
mcpGuardrails (ExtMcp gRPC)MCP methods (tools/call, …)✅ params (request) + result (response)decide (stage tool) + engine check-input / check-output
extAuthz (Envoy ext_authz)HTTP request (headers + optional bounded body)headers onlydecide
extProc (Envoy ext_proc)HTTP request + response bodies✅ both bodiesdecide + engine check-input / check-output

Choosing a seam: govern MCP traffic with ExtMcp; govern LLM/HTTP legs with ext_proc when content may need redaction; use ext_authz for pure allow/deny gating. ext_authz is headers-only — it cannot mask content — so when a policy calls for redacting a request body on that seam, the PDP applies your organization's obligation-fallback posture instead of the adapter blocking locally.

Seam capabilities and the obligation-fallback posture

Requires platform ≥ 9.11.0 and adapters ≥ 9.11.0.

Each seam advertises what it can do on every decision request (fulfillment_capabilities):

SeamAdvertisesCan mask the content it forwards?
mcpGuardrails (ExtMcp)request_body_redaction✅ rewrites tool params
extProc — request with a bodyrequest_body_redaction✅ rewrites the body
extProcbodyless requestrequest_header_mutation❌ the only content is the request line, and ext_proc cannot rewrite :path/:method
extAuthzrequest_header_mutation❌ headers only

Capability is a property of the call path, not the seam: extProc is body-capable when it has a body to rewrite and headers-only on a bodyless request, so a PII-bearing path (e.g. GET /users?nik=…) on that seam takes the fallback posture below, exactly as ext_authz does.

The PDP emits an obligation only to a seam that can discharge it. When a redaction would land on a seam that cannot mask (ext_authz), the PDP suppresses the obligation and applies your organization's obligation-fallback posture:

PostureVerdictContentAudit trail
log (default)allowforwarded unmaskedcanonical audit_logs row records the suppressed redaction + the detected categories
blockdenynot forwardedcanonical audit_logs row records the deny + what was suppressed

log is the default because it matches what the posture that produced the obligation already asked for: an organization on PII_ACTION=redact has said "mask it, don't block it" — it values continuity — so a seam that cannot mask degrades to detect-and-audit rather than to an outage. You always get the audit record; what you lose on this seam is the masking. Choose block if you require that content a policy wanted masked never reaches the backend unmasked.

Set it per organization (admin, sso:configure):

# block: deny instead of forwarding unmasked on a seam that cannot redact
curl -X PUT https://portal.example.com/api/settings/detection-posture/obligation_fallback \
-H 'content-type: application/json' -b "$SESSION" -d '{"action":"block"}'

# revert to the default (log)
curl -X DELETE https://portal.example.com/api/settings/detection-posture/obligation_fallback \
-b "$SESSION"

Only block and log are accepted for this category — redact is precisely what the seam cannot do, and warn has no enforcement distinct from log.

The posture applies only to an obligation that was suppressed, so it never affects a seam that can redact: with block set, an ext_proc or ExtMcp leg still masks and forwards exactly as before.

Alert on axonflow_decision_obligation_fallbacks_total{obligation, action, stage, origin} — a non-zero rate means a seam is receiving content it cannot govern (for example a headers-only leg fronting an LLM that is being sent PII).

Upgrade the platform and the adapters together

A ≥ 9.11.0 adapter against a ≤ 9.10.0 platform is the one unsafe combination to leave in place: the older platform does not understand fulfillment_capabilities, so it still emits the obligation to ext_authz. The adapter then fails closed with a 403 and an ERROR log naming the version mismatch — safe, but every PII-bearing request on that seam is blocked. The reverse (an older adapter against a ≥ 9.11.0 platform) is unaffected: it advertises nothing, reads as a legacy caller, and behaves exactly as it did before.

Run the adapters

The axonflow-gateway-adapters binary serves all three seams on one gRPC listener (plus standard gRPC health). It is an Enterprise component, available two ways:

Prebuilt image (recommended). Enterprise customers pull the multi-arch (linux/amd64 + linux/arm64) image — pin it by the digest from your welcome bundle:

Authenticate to the registry first with the read:packages token from your welcome bundle:

echo "$AXONFLOW_GHCR_TOKEN" | docker login ghcr.io -u <username-from-welcome-bundle> --password-stdin
docker run -d --name axonflow-gateway-adapters \
-p 9090:9090 \
-e AXONFLOW_ENDPOINT=https://your-axonflow-agent:8080 \
-e AXONFLOW_ORG_ID=your-org \
-e AXONFLOW_LICENSE_KEY=your-license-key \
-e AXONFLOW_TENANT_ID=your-tenant \
-e AXONFLOW_GATEWAY_ID=agentgateway-prod \
ghcr.io/getaxonflow/axonflow-gateway-adapters:v9.19.0
# production: pin by digest instead of tag —
# ghcr.io/getaxonflow/axonflow-gateway-adapters@sha256:<GATEWAY_ADAPTERS_DIGEST from your welcome bundle>

The image runs as a non-root user and bundles grpc_health_probe for exec-based health checks (the container HEALTHCHECK uses it; on ECS, point the container health check at CMD-SHELL grpc_health_probe -addr=:9090). The adapter refuses to start without AXONFLOW_ENDPOINT rather than serving ungoverned.

From source. Enterprise customers with source access can build the static binary directly:

# from the axonflow-enterprise checkout
cd ee && go build ./platform/agent/gateway_adapters/cmd/axonflow-gateway-adapters
AXONFLOW_ENDPOINT=https://your-axonflow-agent:8080 \
AXONFLOW_ORG_ID=your-org \
AXONFLOW_LICENSE_KEY=your-license-key \
AXONFLOW_TENANT_ID=your-tenant \
AXONFLOW_GATEWAY_ID=agentgateway-prod \
GATEWAY_ADAPTERS_LISTEN=:9090 \
axonflow-gateway-adapters
VariableDefaultPurpose
AXONFLOW_ENDPOINT— (required)PDP base URL
AXONFLOW_ORG_ID / AXONFLOW_LICENSE_KEYHTTP Basic PDP credentials (together)
AXONFLOW_TENANT_IDtenant scope on every decision
AXONFLOW_GATEWAY_IDagentgatewaycaller_identity.gateway_id on the audit row
AXONFLOW_DEFAULT_STAGEllmdecide stage for the HTTP seams (llm|tool|agent); ExtMcp always uses tool
AXONFLOW_FAIL_MODEclosedrequest-plane posture on PDP-unreachable (closed|open)
AXONFLOW_REQUEST_TIMEOUT10sengine-call bound (circuit posture)
AXONFLOW_CONNECTOR_TAGagentgatewayconnector_type origin tag on fulfillment audit rows
AXONFLOW_MAX_BODY_BYTES8388608 (8 MB)scannable-payload bound; larger payloads fail closed
AXONFLOW_BREAKER_THRESHOLD / AXONFLOW_BREAKER_COOLDOWN5 / 30sconsecutive PDP transport failures that open the circuit, and how long it stays open
AXONFLOW_TRUST_IDENTITY_HEADERSfalseforward client X-User-Email/X-Session-Id on the response-governance call; on platform ≥ 9.9.0 the engine attributes audit rows from them when the same flag is also set on the agent (attribution only — never a verdict; earlier platforms ignore them). Enable ONLY when a hop upstream of agentgateway re-sets them from a validated source (see the trust boundary below)
AXONFLOW_EXTPROC_RESPONSE_GOVERNANCEbufferedwhich ext_proc response body modes are accepted: buffered requires every leg to hand the adapter the whole response, so every response is scanned; off also accepts responseBodyMode: none legs, which run with the response ungoverned — the streaming (SSE) seam. Any other value refuses to boot
GATEWAY_ADAPTERS_LISTEN:9090gRPC listen address

Configure agentgateway

MCP governance (ExtMcp)

binds:
- port: 3000
listeners:
- routes:
- backends:
- mcp:
targets:
- name: your-mcp-server
mcp: { host: http://mcp-backend:3001/mcp }
policies:
mcpGuardrails:
processors:
- kind: remote
methods:
"tools/call": full # request + response phases
host: axonflow-adapters:9090
failureMode: failClosed # the default — keep it

A denied tools/call surfaces to the MCP client as JSON-RPC error -32001 whose data payload carries the AxonFlow decision_id, verdict, and reasons — quotable evidence, straight from the wire.

LLM leg (ext_proc)

- port: 3001
listeners:
- routes:
- backends:
- ai:
name: openai
provider:
openAI: { model: gpt-4o-mini }
policies:
backendAuth:
key: $OPENAI_API_KEY # gateway-held provider credential
extProc:
host: axonflow-adapters:9090
failureMode: failClosed
processingOptions:
requestHeaderMode: send
responseHeaderMode: send
requestBodyMode: buffered # REQUIRED — always; see the mode matrix below
responseBodyMode: buffered # governed response; `none` = streaming, opt-in

Set both body modes explicitly: agentgateway defaults both directions to fullDuplexStreamed, which the adapter rejects fail-closed.

backendAuth matters twice: agentgateway's openAI provider does not inject a key by itself, and holding the provider credential at the gateway frees the client's own Authorization header to carry the end-user JWT the adapter forwards for audit attribution.

The adapter buffers each body, decides on the extracted prompt (OpenAI-shaped bodies get model + last user message; anything else is gated whole), fulfills redaction obligations through the engine against the full body, and governs the response body through the engine's response-governance endpoint before the client sees it. To override the decide stage per route, use the gateway-config metadataContext (clients cannot influence it):

extProc:
# …
metadataContext:
axonflow: { stage: '"tool"' } # CEL string literal

ext_authz routes use the axonflow-stage context extension shown below instead. Stage overrides come only from these gateway-controlled channels — the adapter deliberately ignores any client-sent stage header, so the governed party can never pick its own policy layer.

Streaming completions (SSE) — prompt redaction without buffering the response

Requires adapters ≥ 9.11.0.

Buffering a response body is what lets the adapter scan it — and it is also what destroys an SSE stream: the client gets its first token only after the provider has finished. For streaming chat (stream: true) the seam therefore splits the two directions: the request is still buffered, decided, and engine-redacted in full before the provider sees it, while the response streams through untouched.

extProc:
host: axonflow-adapters:9090
failureMode: failClosed
processingOptions:
requestHeaderMode: send
responseHeaderMode: send
requestBodyMode: buffered # the prompt is still decided + redacted in full
responseBodyMode: none # the completion streams; NOT scanned (see below)

and on the adapter:

AXONFLOW_EXTPROC_RESPONSE_GOVERNANCE=off # exact string; anything else refuses to boot

The response body is not governed on such a leg. No response scan, no response redaction, no response-plane block — if the model emits sensitive content, the client receives it. That is the trade this mode makes, which is why it takes the adapter-side opt-in: without AXONFLOW_EXTPROC_RESPONSE_GOVERNANCE=off, a responseBodyMode: none leg is rejected fail-closed, so an edit to gateway YAML alone can never switch response governance off. The opt-in is process-wide — once set, any ext_proc route on that adapter may run ungoverned-response by advertising none; run a separate adapter process for legs that must keep response governance. The posture is logged at adapter startup and once per ungoverned stream. When responses must be governed, use responseBodyMode: buffered (non-streaming legs) or govern the traffic at the MCP seam.

The request plane is identical on a streaming leg: a deny still blocks before the provider is called (the client gets the 403, no tokens are spent, nothing streams), AXONFLOW_FAIL_MODE behaves the same, and the AXONFLOW_MAX_BODY_BYTES bound applies unchanged. Note the meaning of AXONFLOW_FAIL_MODE=open here: the engine performs the redaction, so with the PDP unreachable a fail-open leg forwards the prompt unredacted. Choose closed (the default) if prompts must never reach the provider unmasked.

ext_proc body-mode matrix

The adapter validates the advertised modes per direction and rejects anything else fail-closed with an error naming the fix:

requestBodyMode \ responseBodyModebufferednonebufferedPartial / streamed / fullDuplexStreamed
buffered✅ governed both ways (default posture)only with AXONFLOW_EXTPROC_RESPONSE_GOVERNANCE=off — response ungoverned❌ rejected
anything else❌ rejected❌ rejected❌ rejected

(streamed is not expressible in agentgateway v1.3.1 config, but this table is the adapter's validation matrix and the adapter also faces raw Envoy, whose STREAMED mode is likewise rejected fail-closed.)

The request direction is never negotiable: the adapter decides on the full prompt or not at all. Partial response modes stay rejected in both postures — scanning a fragment of a response and calling it governed would be a false claim of coverage; a leg that cannot buffer its response says so honestly with none.

Streaming legs declare their capabilities per call path like every other leg (platform ≥ 9.11.0): the request-body path advertises request_body_redaction (it rewrites the prompt), while a bodyless request on the same leg advertises request_header_mutation — a deliberately truthful "I can set headers, I cannot mask content" declaration, kept non-empty because an empty declaration would read as a legacy caller. See the seam-capability section above for how the PDP uses these.

Upgrading from ≤ 9.10.x: earlier adapters accepted responseBodyMode: none silently (the response plane simply never ran). From 9.11.0 that combination requires the opt-in — a leg already running none fails closed after the upgrade until you set AXONFLOW_EXTPROC_RESPONSE_GOVERNANCE=off on the adapter. This is deliberate: an ungoverned response plane is now an explicit, logged decision instead of a config side effect. Platform version does not matter for this feature — the mode gate is adapter-side (a ≥ 9.11.0 adapter streams fine against a 9.9.0+ platform).

Sizing for large prompts (history + RAG context)

Conversation history plus retrieved context routinely exceeds 64 KB. Two different limits apply, on different seams:

  • ext_authz buffers at most includeRequestBody.maxRequestBytes (the reference config uses 65536); a larger request is rejected by the gateway with 413 before the adapter is ever called, and AXONFLOW_FAIL_MODE cannot help (it is pre-callout). If you gate LLM routes with ext_authz, raise maxRequestBytes accordingly, and decide the oversize posture explicitly: Envoy's ext_authz buffer settings support allowing an over-limit request to continue without the body (partial-body allow) — configure that only if an uninspected request is acceptable on that route.
  • ext_proc (this seam) has no gateway-side cap in the reference config; the bound is the adapter's AXONFLOW_MAX_BODY_BYTES (default 8 MB), which fails closed when exceeded. A 130 KB history+RAG prompt streams through this seam redacted, no tuning required.

HTTP allow/deny (ext_authz)

- port: 3002
listeners:
- routes:
- backends:
- host: your-upstream:8080
policies:
extAuthz:
host: axonflow-adapters:9090
failureMode: deny # the default — keep it
includeRequestBody: { maxRequestBytes: 65536 }
protocol:
grpc:
context:
axonflow-stage: llm # optional per-route stage override

Denials return a direct 403 JSON response with decision_id, trace_id, verdict, and reasons; allows stamp x-axonflow-decision-id / x-axonflow-trace-id onto the upstream request and expose the decision to later CEL filters as extauthz.* dynamic metadata.

End-user identity and audit attribution

agentgateway forwards the inbound request headers with each callout. The adapters propagate:

  • Authorization: Bearer <jwt> → the decision's user_token — this is the attribution channel: the PDP resolves and attributes it (an invalid token is denied), on both the decide and response-governance calls;
  • traceparent → trace stitching across gateway layers (trace correlation).

So the audit row names the actual end user behind the gateway, not just the gateway service account — the attribution regulators ask for.

Trust boundary details:

  • X-User-Email/X-Session-Id are trust-gated at both ends, ignored by default. The adapter refuses to forward them unless AXONFLOW_TRUST_IDENTITY_HEADERS=true, because they are client-assertable and agentgateway applies route header modifiers after the ext_proc callout — no gateway config could strip a forged value before the adapter sees it. On the engine side, platforms ≥ 9.9.0 honor the forwarded headers for audit attribution only (audit_logs.user_email / session_id — never a verdict, authz decision, or tenant resolution) under the same AXONFLOW_TRUST_IDENTITY_HEADERS flag set on the agent — one deployment decision, set it in both places. Platforms ≤ 9.8.x derive audit identity from the validated user_token and ignore these headers on the decide/check-output planes. Opt in only when a hop upstream of agentgateway strips the inbound headers and re-sets them from a validated source (e.g. a jwtAuth claim). See Per-developer identity — the trust gate.
  • ExtMcp response-plane rows attribute to the gateway. agentgateway's ExtMcp response callout structurally carries no headers, so MCP response-plane governance rows attribute to the gateway credentials; the request-plane row for the same call still names the end user. The reference config additionally suppresses the identity headers at the ExtMcp seam (requestHeaders: {disallowed: [x-user-email, x-session-id]}) as defense-in-depth.

Failure posture

FailureRequest planeResponse plane
PDP unreachable / 5xx / circuit openblock (default) or forward with AXONFLOW_FAIL_MODE=openalways block
PDP rejects the call (4xx)always blockalways block
Redaction obligation unfulfillable (engine error, redactor didn't run, unforwardable output)always blockalways block
Redaction lands on a call path that structurally cannot mask (ext_authz; ext_proc on a bodyless request)not a failure — the PDP decides: obligation-fallback posture (log → allow + audit, block → deny)n/a
Payload exceeds the scannable bound (8 MB default)always blockalways block
Response body arrives on a leg that advertised responseBodyMode: nonen/aalways block — the gateway contradicted its own advertisement, so the response cannot be proven scanned

On an opted-in streaming leg the response column does not apply — there is no response plane on that leg by explicit configuration, and the request column is unchanged.

Both layers default fail-closed independently — agentgateway's failClosed/deny covers a dead adapter; the adapter's posture covers a dead PDP. Response-plane governance additionally requires the engine's redaction_evaluated attestation (platform ≥ 9.7.0), so "the redactor wasn't running" is indistinguishable from a block, never from an allow.

Coexistence with agentgateway's native guardrails

agentgateway ships inline promptGuard regex/webhook guardrails; keep using them for cheap structural checks if you like. Route regulated decisions — PII with legal definitions (NIK, NRIC, Aadhaar), approval workflows, anything you must later evidence to OJK/RBI/SEBI/EU-AI-Act auditors — through the AxonFlow seams, where every decision lands in the compliance-grade trail. The two compose: the seams run as ordered processors alongside native policies.

Verification

The integration ships with a runtime E2E harness (runtime-e2e/2886_agentgateway_pep_adapters/ in the platform repo) that drives a real agentgateway binary, a real MCP server, and a real LLM provider through all three seams against a live PDP — allow, deny, engine-redaction, and fail-closed on every seam. Its config template is the canonical reference config.

See also