feat: Enable praxis policy engine to honor MCP response codes, optionally enable CIBA human-in-the-loop flows.#820
Conversation
precedence, identity_gate tests, mcp vocabulary, fmt Signed-off-by: Teryl Taylor <terylt@ibm.com>
praxis-bot
left a comment
There was a problem hiding this comment.
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.
|
I pushed changes to address the comments made by @alexsnaps and @praxis-bot. |
Sounds good. Resolve the conflicts, then mark this |
… 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>
2ceab2c to
bbe3273
Compare
|
Rebased the PR and fixed the merge conflict. |
praxis-bot
left a comment
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
[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",
);
Summary
Adds human-in-the-loop (HIL) elicitation support to the
policyfilter. 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/cibaplugin — the policy engine already models arequire_approval(...)step; this PR lets its "pending" outcome surface correctly over the wire and be resumed.How it works
require_approval(...). The elicitation plugin dispatches the approval (CIBA) and the step returns pending.HTTP 200with a JSON-RPC error-32120(elicitation.pending, "awaiting approval — retry with this id") and an elicitation bundle inerror.data(elicitation_id,approver,expires_at,channel). This is distinct from a flat deny (-32001), so clients can tell "pending" from "denied."-32120again.Changes
error.rs— the JSON-RPC error envelope now honorsPluginViolation.proto_error_code(so a pending elicitation surfaces as-32120rather than being collapsed to the generic-32001) and passes the violation's structureddetailsthrougherror.data(the elicitation bundle). +2 unit tests.filter.rs—identity_gateskips the early identity gate when the body phase already stashedResolvedIdentity. When a downstream pre-read runson_request_bodybefore this header phase and strips the inbound identity headers, re-resolving here would spuriously reject an already-authorized request; the body phase is authoritative.mcp.method/mcp.namefrom 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 cpexelicitation-cibafeature.Testing
cargo test -p praxis-proxy-filter --features cpex-policy-engine→ 1627 + 2 new unit tests, 58 doctests, all green.demos/authpolicy-transpiler(pure L7, no classifier) — all 5 CEL checks pass (unchanged behavior).demos/cpexHIL — 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 genericpolicyfilter 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
elicitation-cibafeature (enabled here) if wanting to use CIBA.praxis-ai; because this branch addsmax_batch_sizetoJsonRpcConfig(from the policy-http-extensions work now inmain), praxis-ai must be built with itspraxis-mainfeature. That's a demo-repo build concern, not part of this filter change.