Skip to content

test(mcp): end-to-end Kuadrant MCP Gateway authorization acceptance for the Responses agentic loop - #1833

Draft
leseb wants to merge 15 commits into
praxis-proxy:mainfrom
leseb:leseb/mcp-gw-kuadrant-fix
Draft

leseb wants to merge 15 commits into
praxis-proxy:mainfrom
leseb:leseb/mcp-gw-kuadrant-fix

Conversation

@leseb

@leseb leseb commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

End-to-end acceptance for issue #1673: stand up a real Kuadrant MCP Gateway and
prove per-user MCP tool authorization (discovery filtering + per-call authorization)
through Praxis's OpenAI Responses agentic loop.

This is acceptance of already-delivered courier behavior — there are no Praxis
product-code changes
(git diff --stat touches only tests/, examples/, a
Makefile target, examples/README.md, and a new CI workflow).

The gateway runs as a minimal, no-full-Kubernetes container stack (proven feasible
by spike): static Envoy → ext_proc(MCP router) → ext_authz(Authorino, backed by a
bare etcd+kube-apiserver, no operator) → broker → fake MCP backend, plus a
static-JWKS IdP. Praxis's own rmcp client drives it unchanged.

What's proven (18 cases, buffered + streaming, all green locally)

Matrix item Result
1 authorized user discovers + invokes tool discovered, agentic loop dispatches, backend exec==1
2 unauthorized user tool filtered from discovery, exec==0
3a Praxis won't dispatch an undiscovered tool no mcp_call, exec==0
3b gateway denies a discovered-but-forbidden call 403 → mcp_call.error, exec==0 (open-discovery variant forces the call to the gateway)
4a invalid Bearer forwarded opaquely, rejected at gateway (502), exec==0
4 missing credential slot fails closed at Praxis before any dial
4b invalid forwarded x-mcp-authorized explicitly uncovered in Topology A (Authorino mints its own) — documented, not falsely "rejected"
5 approval does not bypass authz require_approval:never still denied; an approved resume re-dials the gateway and is still denied
6a/6b live-policy revocation revoke between discovery/dispatch and between approval-create/resume; denial confirmed (polls the decision, never races a sleep)
7 same forwarded identity reaches discovery + dispatch one-way digest parity observed at gateway ingress; raw credentials never logged

Topology A: the per-user OAuth Bearer is the enforcement carrier (gateway Authorino
validates it, mints the discovery wristband, authorizes each call); the
x-mcp-authorized courier is asserted separately for correctness.

How to run

make test-kuadrant-acceptance   # builds praxis-ai (full,store-sqlite), stacks up, runs the matrix, tears down

Needs a container engine (CONTAINER_ENGINE, podman/docker), kubectl, uv, openssl.
See tests/integration/kuadrant/README.md.

CI

New gated lane .github/workflows/kuadrant.yaml, modeled on postgres.yaml: path-filtered
(only MCP-courier / harness / example-config changes trigger it), merge_group changes
gate, pinned image tags, auto-registered as a required check by ci-status.yaml.

Follow-ups (deferred, non-blocking)

  • Add an explicit kubectl setup step to the lane (ubuntu-24.04 ships it today).
  • trap-based teardown around the whole make target for local robustness.
  • Confirm the setup-rust composite caches cargo (30-min timeout has headroom).

Draft: opening for CI signal on the new lane.

Closes #1673

@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

leseb added 14 commits October 27, 2026 15:06
Promote the proven spike into a committed harness that stands up the minimal
no-full-Kubernetes real Kuadrant MCP Gateway stack (Envoy -> ext_proc router ->
ext_authz Authorino on a bare etcd+kube-apiserver -> broker -> fake MCP backend,
plus a static-JWKS IdP) on network kuadrant-accept and emits .stack-env for the
acceptance driver. Engine-parametric (CONTAINER_ENGINE: podman/docker).

Part of issue praxis-proxy#1673 acceptance (no Praxis product-code changes).

Signed-off-by: Sébastien Han <seb@redhat.com>
Add examples/configs/openai/responses/mcp-kuadrant-gateway.yaml (Topology A: the
per-user OAuth Bearer is the enforcement carrier; gateway Authorino mints the
wristband and authorizes). Add a config-validity functional test that proves the
example loads and its pipeline builds, and sync examples/README.md.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
Add the Python acceptance driver (stack/scripted_model/praxis fixtures + raw-httpx
Responses helpers) and prove item 1: userA (tool:get_forecast) discovers
weather_get_forecast, the agentic loop dispatches it through the real gateway, and
the backend executes exactly once. Promote the deterministic scripted model.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
…-dispatch

Item 2: userB (no roles) sees weather_get_forecast filtered out of discovery, no
dispatch. Item 3a: even when the model force-emits a call to a tool userB never
discovered, Praxis does not dispatch it to the gateway (exec==0). Add an
input-keyed force-call mode to the scripted model.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
Add switch_authconfig.sh (applies an AuthConfig variant and waits for Authorino to
reconcile). Item 3b: under the open-discovery policy userB discovers the tool, but
the gateway's per-call authz denies the invocation (403 -> mcp_call.error) and the
backend never executes — the 'discovery is not enforcement' proof.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
Teach the scripted model to emit Responses SSE when the request is streaming, and
add streaming equivalents for items 1/3a/3b. Each streaming denial asserts a valid
terminal SSE lifecycle, the surfaced tool error, and zero backend executions.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
Add `make test-kuadrant-acceptance` (build, stack up, run the Responses-loop
matrix, tear down) and .github/workflows/kuadrant.yaml, a path-gated lane modeled on
postgres.yaml (changes/merge_group gate; auto-registered by ci-status.yaml).
Measured ~2min warm locally; budget ~8-12min cold on CI (30min timeout).

Completes increment 1 (items 1-3 buffered+streaming). Part of issue praxis-proxy#1673.

Signed-off-by: Sébastien Han <seb@redhat.com>
…gress

Re-add the x-mcp-authorized courier slot (callout_credentials assertions +
authorization_assertion on resolve+dispatch); every request now forwards both the
per-user Bearer and the wristband. Add a two-stage Envoy Lua ingress observer
(capture one-way djb2 digests before the router strips x-mcp-authorized, log them
labeled by the router-set x-mcp-method) and item 7: the same Bearer and wristband
digests reach both discovery and dispatch. No raw credentials are logged.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
4a: a tampered per-user Bearer is forwarded opaquely (reaches the gateway, confirmed
at the ingress observer) and rejected there, surfaced as a 502 connector-resolve
failure; the backend never executes. Missing bearer slot fails closed AT PRAXIS
(no gateway dial). 4b documents that an invalid forwarded x-mcp-authorized is NOT an
enforcement point in Topology A (Authorino mints its own) — explicitly uncovered.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
Add a store-enabled praxis fixture (file-backed SQLite armed before dispatch) and
item 5: require_approval:never still denies an unauthorized call, and an *approved*
resume re-dials the gateway and is still denied (exec==0), buffered and streaming.
Requires praxis-ai built with store-sqlite.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
…proval resume

Add the revoked AuthConfig variant (denies userA+get_forecast at the per-call rule,
discovery unchanged) and item 6: revoke between discovery and dispatch (6a) and
between approval creation and resume (6b), buffered and streaming. Each confirms the
Authorino reconcile took effect (polls the decision, never races a sleep) before
asserting the denial + zero execution. Also fix a flaky --tamper that could emit a
still-valid signature, and refresh the runtime note.

Completes the issue praxis-proxy#1673 acceptance matrix.

Signed-off-by: Sébastien Han <seb@redhat.com>
Address the fresh-reviewer Minor findings worth landing: move the item-6a streaming
AuthConfig switch inside try so failures always restore 'filtered' (test isolation);
poll the Envoy ingress log (_ingress_since) for items 4a/7 to avoid a log-flush race;
strengthen item 3a to assert NO mcp_call item at all (proves client-side
non-dispatch, not merely an unsuccessful call); and document the deliberate djb2 (not
SHA-256) ingress fingerprint. Suite 18/18.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
Wrap a long assert! per the pinned nightly rustfmt (nightly-2026-03-28) so the CI
fmt check is clean.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
Rebase onto main: 'full' is now just 'standard' (post-praxis-proxy#1718 evolution), so the
acceptance binary and the example test must enable openai-all explicitly. Build the
binary with 'full,openai-all,store-sqlite', gate the config-validity test module on
openai-agentic-loop + openai-mcp-tools (the config uses the agentic loop), and refresh
the README note. Full 18-case suite green against rebased main.

Part of issue praxis-proxy#1673 acceptance.

Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb
leseb force-pushed the leseb/mcp-gw-kuadrant-fix branch from 6765b1c to 97782d5 Compare October 27, 2026 14:16
Signed-off-by: Sébastien Han <seb@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(mcp): end-to-end MCP Gateway authorization acceptance for the Responses agentic loop

1 participant