Use Cases
On this page
Each use case runs end to end in the Praxis demo. PPE runs inside an AI gateway backed by an IdP and a mock MCP backend. Each snippet comes from the live configuration, and each scenario has an executable script.
The demo realizes the running scenario: one agent, three callers, three kinds of backend reached over MCP. Identity decides the outcome.
| Persona | Identity | Result |
|---|---|---|
| Bob | HR, view_ssn | Full compensation record, SSN included |
| Eve | HR, no view_ssn | Same record, SSN redacted on the wire |
| Alice | Engineering | Denied HR tools; allowed internal repos, denied external |
Praxis is an AI-native proxy built
around a filter chain. PPE ships as its policy filter (the
policy-engine feature), so the gateway parses MCP JSON-RPC, runs the full
policy pass, and only then forwards a scoped request upstream:
The wiring is two files:
praxis.yaml
declares the listener and filter chain, and
policy.yaml
holds everything PPE: identity plugins, delegators, validators, routes, and the
PDP policy. The use cases below are that one config, taken apart.
Watch it run
The recording below shows an interactive session against the gateway, driven by an LLM agent, covering an allow with token exchange, on-the-wire redaction, Session Taint, a CEL policy decision, and a human-in-the-loop manager approval, with the governing policy shown alongside each step.
1. Identity-aware tool access
Who may call this tool at all? The get_compensation route opens with an
attribute gate, and the attributes come from verified JWTs, not from anything
the agent claims:
routes:
- tool: get_compensation
authorization:
pre_invocation:
- "require(role.hr)"
Bob and Eve pass (HR role in their tokens). Alice is denied with a JSON-RPC
error envelope before the request ever leaves the gateway; the backend never
sees it. The demo resolves two identities per request, the human
(X-User-Token) and the client (Authorization), each validated by its own
identity/jwt plugin against Keycloak.
Run it:
scenarios/01-bob-allow.sh,
scenarios/02-alice-deny.sh.
More: Identity.
2. On-the-wire redaction
This is the same-request-different-data outcome from the Overview, now running in a real gateway. Bob and Eve send the byte-for-byte same request; the field pipeline rewrites Eve’s response body inside the proxy, after the tool returns and before the agent sees it:
result:
ssn: "str | redact(!perm.view_ssn)"
The tool does not implement this, cannot get it wrong, and cannot be talked out of it. The novelty here over the Overview is where it happens: in the proxy’s response path, so an unmodified MCP backend gets per-caller redaction for free.
Run it:
scenarios/03-eve-redact.sh.
More: field pipelines in Effects.
3. Credential custody and token exchange
The backend should never hold or even see the user’s IdP credential. Before forwarding, the route exchanges Bob’s token for a fresh one scoped to exactly this backend (RFC 8693, via Keycloak):
- "delegate(workday-oauth, target: workday-api, audience: workday-api, permissions: [read_compensation])"
What reaches workday-api is a short-lived token with audience: workday-api
and only read_compensation. A leak at the backend leaks that, not Bob’s
session. The search_repos route goes one step further and verifies the grant
before trusting it:
- "!(delegation.granted.permissions contains 'repo:read:internal'): deny"
Run it:
scenarios/01-bob-allow.sh
plus
verify-token-exchange.sh.
More: Delegation.
4. Cross-tool data-flow control
The classic exfiltration path: read something sensitive, then send it somewhere. Content filters miss it when the outbound message is clean. PPE instead taints the session at the read and gates the send on the label:
# get_compensation
- "taint(secret, session)"
# send_email
- "security.labels contains \"secret\": deny('external email blocked: this session accessed secret data', 'session_tainted_secret')"
Once a session touches compensation data, its emails are blocked, even with a spotless body. The label lives in PPE’s session store (Valkey in the demo), keyed by a hash of subject and session id, so it survives gateway restarts and cannot cross principals: Eve tainting a session id does not poison Bob’s use of the same id.
Run it:
scenarios/08-bob-taint-deny.sh,
scenarios/09-cross-principal-taint-isolation.sh.
More: Session Taint.
5. PII guardrails on arguments
Session taint is state-based; this control is content-based, and they complement each other. A validator plugin scans tool arguments for sensitive patterns and denies before dispatch:
- name: pii-scan
kind: validator/pii-scan
hooks: [cmf.tool_pre_invoke]
config:
detect:
- { kind: ssn }
- { kind: credit_card }
mode: deny
Bob pasting an SSN into an email body gets a deny, and the audit record of the
attempt is still written: the route runs run(audit-log) before
run(pii-scan), so observation happens before the gate blocks. Flip mode: deny to audit to shadow-test the scanner against real traffic first (see
Patterns).
Run it:
scenarios/07-bob-pii-deny.sh.
More: Builtins.
6. Human-in-the-loop approval
Some actions should not happen on the caller’s authority alone.
adjust_compensation changes a salary, so anything over $10,000 requires the
requester’s manager to sign off, out-of-band, before the tool runs:
routes:
- tool: adjust_compensation
authorization:
pre_invocation:
- "require(role.hr)"
- when: "args.amount > 10000"
do:
- "require_approval(manager-approver, from: claim.manager,
channel: \"ciba\",
scope: \"args.amount <= 25000\",
purpose: \"Approve a compensation adjustment\",
timeout: 24h)"
- "run(audit-log)"
The gateway never blocks. It suspends the call, answers the agent with JSON-RPC
-32120 and an elicitation id, and drives an OIDC CIBA backchannel request to
Keycloak, which pushes the prompt to the manager’s device. The scope binds the
approval to the live amount, so a sign-off cannot be replayed against a larger
change.
The agent needs no approval protocol; it sees “retry later” and, later, a result. In the demo’s chat client the conversation continues until the approval lands and the result cuts back in.
Run it:
scenarios/10-bob-adjust-under-threshold.sh,
scenarios/11-bob-adjust-approval.sh.
More: Elicitation.
7. Pluggable policy decisions
Attribute gates answer “does the caller have this role?”. Relationship questions (“may this principal read this resource?”) go to a PDP, and the demo ships the same decision in two dialects. Cedar, as a versioned policy set:
- cedar:
action: 'Action::"read"'
resource:
type: Repo
id: ${args.repo_name}
attributes:
visibility: ${args.visibility}
@id("engineering-internal-repos")
permit(
principal,
action == Action::"read",
resource is Repo
) when {
principal.roles.contains("engineer") &&
resource.visibility == "internal"
};
CEL, as an inline predicate on the route:
- cel:
expr: |
(has(role.engineer) && role.engineer && args.visibility == "internal")
|| (has(role.security) && role.security)
on_deny:
- "deny('engineering may read internal repos only; security may read any', 'cel.policy_denied')"
Same route, same outcome, different authoring model. Cedar suits
versioned or signed policy sets with an entity model, CEL a
self-contained predicate with no external policy store, and Rego a team
that already writes it: the opa decision point embeds the evaluator,
so there is no sidecar to run. All three compile into one binary and
the config selects which runs.
They are held to each other by a differential test suite. Given the same attributes and equivalent policy intent, the three must agree across their shared boolean, integer, string, and string-set subset. A new disagreement fails the build; documented semantic differences are allowlisted rather than discovered in production.
Run it:
scenarios/04-alice-internal-allow.sh,
scenarios/05-alice-external-cedar-deny.sh,
then again with GATEWAY_CONFIG=praxis-cel.yaml. More: PDP
Integration.
Run it yourself
The whole demo is one command from
demos/policy-engine
(Docker plus a Rust toolchain):
./restart.sh # build the gateway, bring up Keycloak + backend + valkey
./walkthrough.sh # narrated tour of the core scenarios
Eleven scripted scenarios cover every control above, and agent/chat.py --persona bob drives the same gateway from an LLM chat agent, approvals
included.
The same scenario runs with PPE embedded as a Rust crate inside the host process rather than behind a gateway. See Deployment for what changes between the two placements.
Related documentation
- APL: define routes, phases, predicates, and field pipelines.
- Effects and Sequencing: order controls and halt on denial.
- PDP Integration: invoke Cedar, CEL, or OPA from APL.