Skip to content

feat: Enable praxis policy engine to honor MCP response codes, optionally enable CIBA human-in-the-loop flows.#820

Open
terylt wants to merge 3 commits into
praxis-proxy:mainfrom
terylt:feat/hil_policy_http
Open

feat: Enable praxis policy engine to honor MCP response codes, optionally enable CIBA human-in-the-loop flows.#820
terylt wants to merge 3 commits into
praxis-proxy:mainfrom
terylt:feat/hil_policy_http

Conversation

@terylt

@terylt terylt commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds human-in-the-loop (HIL) elicitation support to the policy filter. A policy can now suspend a request pending an out-of-band approval (e.g. a manager sign-off over OIDC CIBA) instead of allowing or denying it outright. The gateway returns a distinct JSON-RPC error the client can retry against; once the approval resolves, the same call proceeds.

This is the Praxis-side plumbing for the CPEX elicitation/ciba plugin — the policy engine already models a require_approval(...) step; this PR lets its "pending" outcome surface correctly over the wire and be resumed.

How it works

  1. A tool call hits a route whose policy runs require_approval(...). The elicitation plugin dispatches the approval (CIBA) and the step returns pending.
  2. The gateway responds HTTP 200 with a JSON-RPC error -32120 (elicitation.pending, "awaiting approval — retry with this id") and an elicitation bundle in error.data (elicitation_id, approver, expires_at, channel). This is distinct from a flat deny (-32001), so clients can tell "pending" from "denied."
  3. The client retries the same call carrying the elicitation id. The policy takes the check path instead of re-dispatching; while still pending it returns -32120 again.
  4. Once the approver responds, the retry resolves — an approval runs the tool, a denial denies it.

Changes

  • error.rs — the JSON-RPC error envelope now honors PluginViolation.proto_error_code (so a pending elicitation surfaces as -32120 rather than being collapsed to the generic -32001) and passes the violation's structured details through error.data (the elicitation bundle). +2 unit tests.
  • filter.rs
    • identity_gate skips the early identity gate when the body phase already stashed ResolvedIdentity. When a downstream pre-read runs on_request_body before this header phase and strips the inbound identity headers, re-resolving here would spuriously reject an already-authorized request; the body phase is authoritative.
    • CMF dispatch reads mcp.method / mcp.name from classifier metadata (see Note below).
  • json_rpc.rs / common_message_format.rs — match MCP-native JSON-RPC method names (tools/call, prompts/get, resources/read) for entity/content resolution.
  • Cargo.toml — enable the cpex elicitation-ciba feature.

Testing

  • cargo test -p praxis-proxy-filter --features cpex-policy-engine1627 + 2 new unit tests, 58 doctests, all green.
  • End-to-end, both demos on this branch:
    • demos/authpolicy-transpiler (pure L7, no classifier) — all 5 CEL checks pass (unchanged behavior).
    • demos/cpex HIL — suspend (-32120), under-threshold apply, and approve → retry → applied all verified against Keycloak CIBA.

Note / follow-up: protocol vocabulary

The metadata reads (mcp.method) and method values (tools/call, …) currently bind this filter to MCP's naming, because that's what the praxis-ai MCP classifier emits. This works but couples the generic policy filter to one protocol.

A protocol-neutral design (so e.g. A2A can route through the same policy) would have the classifier emit a normalized entity descriptor — that's a classifier-side change, tracked separately. Flagging here so the coupling is a conscious, reviewable choice rather than an accident.

Dependencies

  • Requires the cpex elicitation-ciba feature (enabled here) if wanting to use CIBA.
  • The HIL demo gateway additionally composes praxis-ai; because this branch adds max_batch_size to JsonRpcConfig (from the policy-http-extensions work now in main), praxis-ai must be built with its praxis-main feature. That's a demo-repo build concern, not part of this filter change.

@terylt
terylt requested a review from a team July 17, 2026 17:58
@terylt
terylt requested a review from shaneutt as a code owner July 17, 2026 17:58
@terylt terylt added the skip/pr-conventions Skip conventions checks for PRs label Jul 17, 2026
@araujof araujof added area/security enhancement New feature or request labels Jul 18, 2026
terylt added a commit to terylt/praxis that referenced this pull request Jul 18, 2026
  precedence, identity_gate tests, mcp vocabulary, fmt

Signed-off-by: Teryl Taylor <terylt@ibm.com>
@terylt
terylt requested a review from alexsnaps July 20, 2026 17:24
Comment thread filter/src/builtins/http/security/policy/error.rs

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Clean, well-scoped PR. The metadata key rename (protocol.* -> mcp.*), MCP-native method names (tools/call, prompts/get, resources/read), and the elicitation envelope enhancement are all consistent across every file. The identity_gate skip logic is sound -- ResolvedIdentity is only constructible within the policy module, so the bypass is safe. The anti-shadowing defense (violation key inserted last) is correct and tested. Two medium findings below.

Severity Count
Critical 0
Large 0
Medium 2

Reviewed with praxis-bot.

Comment thread filter/src/builtins/http/security/policy/error.rs
Comment thread filter/src/builtins/http/security/policy/error.rs
@shaneutt shaneutt self-assigned this Jul 22, 2026
@shaneutt shaneutt added this to the v0.5.0 milestone Jul 22, 2026
@shaneutt shaneutt moved this from Next to Review in AI Gateway - Model Serving Jul 22, 2026
@praxis-bot
praxis-bot marked this pull request as draft July 23, 2026 19:46
@praxis-bot praxis-bot added the holding-pattern Waiting for discussions or contributor updates in order to proceed label Jul 23, 2026
@terylt

terylt commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

I pushed changes to address the comments made by @alexsnaps and @praxis-bot.

@shaneutt

Copy link
Copy Markdown
Member

I pushed changes to address the comments made by @alexsnaps and @praxis-bot.

Sounds good. Resolve the conflicts, then mark this Ready for review and I'll swing back through 👍

terylt added 3 commits July 24, 2026 09:48
… response code, match native MCP json-rpc method.

Signed-off-by: Teryl Taylor <terylt@ibm.com>
  precedence, identity_gate tests, mcp vocabulary, fmt

Signed-off-by: Teryl Taylor <terylt@ibm.com>
…added denial comments to mod.rs as per review.

Signed-off-by: Teryl Taylor <terylt@ibm.com>
@terylt
terylt force-pushed the feat/hil_policy_http branch from 2ceab2c to bbe3273 Compare July 24, 2026 16:03
@terylt terylt removed the holding-pattern Waiting for discussions or contributor updates in order to proceed label Jul 24, 2026
@terylt

terylt commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Rebased the PR and fixed the merge conflict.

@terylt
terylt marked this pull request as ready for review July 24, 2026 16:33

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (ready-for-review pass)

Previously reviewed as a draft on 2026-07-21. The PR has since been marked ready for review (2026-07-24). Full re-read of all changed files against current main.

Previous findings

# File Status
1 error.rs line 140 -- docstring example outdated Addressed. The JSON example now shows "...": "<violation details>" in data, and the prose below documents variable code and details map.
2 error.rs line 250 -- inline comments in test bodies Addressed. All assertions in pending_violation_surfaces_proto_code_and_details carry message strings.

New findings

Severity Count
Critical 0
Large 0
Medium 1

See inline comment.

Re-reviewed with praxis-bot.

let v = PluginViolation::new("apl.policy", "denied");
let env = envelope(&v);
assert_eq!(env["error"]["code"], GATEWAY_DENIED_CODE);
assert_eq!(env["error"]["data"]["violation"], "apl.policy");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] plain_violation_uses_default_deny_code (here, 2 bare assertions) and details_cannot_shadow_the_canonical_violation_key (line 295, 1 bare assertion) omit message strings on their assert_eq! calls. The workspace lint missing_assert_message = "deny" (Cargo.toml line 249) will reject these under cargo clippy. The sibling test pending_violation_surfaces_proto_code_and_details already does it correctly -- same pattern applies here.

assert_eq!(
    env["error"]["code"], GATEWAY_DENIED_CODE,
    "plain violation must use the default deny code",
);
assert_eq!(
    env["error"]["data"]["violation"], "apl.policy",
    "violation code must appear in data",
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/security enhancement New feature or request skip/pr-conventions Skip conventions checks for PRs

Projects

Status: Review

Development

Successfully merging this pull request may close these issues.

5 participants