policy

Embeds the Policy Engine to enforce identity checks and APL rules, exchange tokens, redact fields, track session taint, and emit audit events.
On this page

Embeds the Praxis Policy Engine in-process to enforce multi-source identity, APL route policy, RFC 8693 token exchange, field redaction, session taint, audit emission, and (under body_access: read_write) request / response body rewriting. Content scanning is a host plugin the engine dispatches, not a bundled one.

Requires Cargo feature: policy-engine.

Configuration Notes

Registered under the YAML filter name policy. The policy-engine cargo feature is on by default; --no-default-features leaves the filter out.

A single request can carry multiple identity sources — user JWT in Authorization, agent JWT in X-Agent-Token, workload JWT in X-Workload-Token, etc. Each registered identity plugin reads its own configured header and contributes to a typed Extensions context.

On the body phase, the filter consumes protocol classifier filter metadata (from the praxis-ai package) to dispatch the matching CMF hook chain. APL routes (declared in the policy document) gate the tool/prompt/resource call by role, attribute, or Cedar PDP decision. delegate(...) steps mint audience-scoped tokens (RFC 8693) that the allow path attaches as upstream headers.

Policies with llm: routes authorize the top-level request model through cmf.llm_input, without classifier metadata. Missing, unlisted, and ambiguous models fail closed by default.

body_access: read_write enables the JSON-RPC re-serialization round-trip so APL field mutators (redact(), assign()) rewrite the upstream request body and the downstream response. It also enables cmf.llm_output for non-streaming inference responses. APL field mutators do not rewrite inference bodies.

Response-body hooks run on a small dedicated runtime while the worker waits, for at most twice the engine’s per-plugin timeout (engine_settings.plugin_timeout, so 60 seconds by default). A hook that does not finish in time is aborted and the response fails under the filter’s failure_mode: closed truncates the response, open passes the body through unfiltered.

Outbound policy calls share the proxy’s sub-request limits and circuit breaker, use HTTP/1.1, and keep a separate 1 MiB response ceiling. Calls made by response-body hooks use their own pool on the dispatch runtime, with the same connection limit but no circuit breaker. TLS uses the platform trust store; cluster private CAs and client certificates do not apply. A private destination requires trusted_private_endpoints for a specific host, or allow_private_idp to relax every callout.

An endpoint URL may name an IP address over http, but not over https: an IP carries no SNI, and Pingora peers skip certificate verification entirely when SNI is empty. Use a hostname for https.

Praxis filter configs are flat: the filter’s typed fields sit directly under the - filter: entry alongside the structural keys (name, conditions), not nested under a config: wrapper. See examples/configs/security/policy.yaml for a runnable example.

The referenced YAML is the policy document — plugins, routes, and identity-source declarations. The filter loads it once at construction and rejects misconfigured policy at server startup (fail-fast rather than at first request).

Configuration

FieldTypeRequiredDescription
body_accessread_only | read_writenoBody-access tier. ReadOnly (default) lets APL inspect request and response bodies for routing / policy decisions but discards any mutations. ReadWrite enables the CMF → JSON-RPC re-serialization round-trip so APL field mutators (e.g. args.ssn: redact(!perm.view_ssn)) rewrite the upstream body and response. Pay the round-trip cost only when needed.
config_pathstringyesFilesystem path to the policy document.
init_timeout_secsintegernoMaximum time, in seconds, to wait for PolicyEngine::initialize at filter construction. Identity plugins fetch JWKS over HTTPS during init; a reachable-but-unresponsive identity provider would otherwise hang startup or hot-reload indefinitely. On expiry, filter construction returns an error and the server fails fast. 30s is generous for legitimate cold-cache JWKS fetches over the public internet, while short enough that misbehavior is noticed during the deploy.
max_buffer_bytesintegernoMaximum request or response body size in ReadWrite mode. Also used as the default inference request limit.
allow_private_idpboolnoPermit every policy endpoint a private or loopback address. By default, private DNS answers are skipped and calls with no public answer are rejected, so a private destination needs one of two opt-ins: trusted_private_endpoints to relax a specific host to the RFC 1918 and unique-local ranges, or this flag to relax every callout globally. Proxy upstreams use insecure_options.allow_private_endpoints instead.
trusted_private_endpointsstring[]noHosts the policy engine may reach at a private address. Narrower than allow_private_idp. Only a callout whose host matches an entry reaches a non-public address, and only the RFC 1918 and unique-local (fc00::/7) ranges an in-cluster endpoint resolves to. A listed host still cannot reach loopback, link-local (including cloud metadata), the unspecified address, or any other reserved range. Every unlisted callout stays public-only. Matched on the URL host, case-insensitive, port excluded. An IPv6 address is a bracketed literal such as [fc00::1]. Shared address space (100.64.0.0/10) is deliberately not relaxable, so a pinned endpoint on a cluster that assigns pod addresses there, such as EKS with the VPC CNI secondary-CIDR pattern, is not reachable by a pin. Relaxing that range would need a separate per-host opt-in, off by default.
require_protocol_metadataboolnoFail-closed policy gate for misconfigured chains. When true (default), on_request_body rejects any request that reaches it without mcp.method filter-metadata. The metadata is set by the protocol classifier filter (available in the praxis-ai package), so its absence means either (a) the protocol classifier filter is missing from the chain, or (b) it is ordered AFTER policy instead of before. Either is a misconfiguration that would silently bypass CMF/APL policy. Set to false only when intentionally fronting non-classified traffic through the policy filter for identity-only enforcement (legacy behavior). Only applies to policies with MCP entity routes. Inference routes use their own gates instead. JSON-RPC methods that legitimately carry no entity (e.g. tools/list, initialize, prompts/list) still pass; require_protocol_metadata only rejects when the metadata is missing entirely.
llmLlmOptionsnoInference authorization options.
llm.max_request_bytesintegernoMaximum buffered inference request size, in bytes. Requests over this limit receive HTTP 413.
llm.promote_paramsstring[]noTop-level scalar fields promoted to custom.llm.<name>. A configured list replaces the defaults.
llm.providerstringnoOperator-supplied provider recorded on llm.provider.
llm.require_modelboolnoDeny a request whose body carries no usable top-level model. Enabled by default. When disabled, the request falls through to other policy paths.
llm.require_routeboolnoDeny a model no llm: route selects. Enabled by default. Disable only to admit unlisted models.

Example

filter: policy
config_path: /etc/praxis/policy.yaml
body_access: read_write       # optional; default read_only
require_protocol_metadata: true    # optional; default true
init_timeout_secs: 30         # optional; default 30
max_buffer_bytes: 10485760
llm:
  require_model: true
  require_route: true
  provider: openai