Quick Start

Stand up PPE and run the scenario: a get_employee route that authorizes by role and redacts a field by permission.
On this page

Stand up PPE and run the scenario: a get_employee route that authorizes by role and redacts a field by permission.

You need Rust 1.92 or newer (install with rustup). The toolchain is pinned in the repository, so cargo build picks the right one.

1. Add PPE

praxis-policy = { version = "0.4", features = ["builtins", "http-hyper"] }

builtins compiles in every stable bundled extension: JWT and API key identity, OAuth delegation, CIBA elicitation, the Cedar, CEL and OPA decision points, the Valkey session store, and the Vault secret provider. For a smaller build, name a subset instead: features = ["jwt", "cedar"]. See Builtins.

The default build is the engine alone. http-hyper is separate from builtins because it is the one piece a host commonly already owns: it supplies the bundled outbound HTTP transport, and a host with its own HTTP client injects that instead (step 2).

2. Register the runtime

Create the engine, register the enabled builtin factories, and install the APL config visitor:

use std::sync::Arc;
use praxis_policy::PolicyEngine;

let engine = Arc::new(PolicyEngine::default());

// Registers every enabled builtin factory and installs the APL visitor.
praxis_policy::install_builtins(&engine);

Without the builtins feature there is nothing to register, so install the APL visitor yourself:

use std::sync::Arc;
use praxis_policy::{
    AplOptions, DispatchCache, MemorySessionStore, PolicyEngine, register_apl,
};

let engine = Arc::new(PolicyEngine::default());
register_apl(
    &engine,
    AplOptions {
        dispatch_cache: Arc::new(DispatchCache::new()),
        session_store: Arc::new(MemorySessionStore::new()),
        pdps: Vec::new(),
        pdp_factories: Vec::new(),
        session_store_factories: Vec::new(),
        base_capabilities: None,
    },
);

A host with plugins of its own registers each one alongside this. See Plugins and the Execution Pipeline.

Then install the outbound HTTP transport. PPE performs no HTTP of its own, so a plugin that fetches JWKS, exchanges a token, or dispatches a CIBA prompt has nowhere to send its request until a host supplies one:

// The bundled transport, from the `http-hyper` feature.
praxis_policy::install_default_http_transport(&engine);

A host that already has an HTTP client calls engine.set_http_transport(my_transport) instead, so the process keeps one connection pool and one TLS trust store. Either call goes before initialize(), and a plugin that reaches outward must also declare the perform_http capability. Builtins covers both, and the error each mistake produces.

3. Write the policy

routes: is a list, one entry per operation. This route matches the get_employee tool, authorizes by role, and redacts on the wire by permission:

routes:
  - tool: get_employee
    args:
      employee_id: "str"
    authorization:
      pre_invocation:
        - "require(authenticated)"
        - "require(role.hr)"
    result:
      ssn: "str | redact(!perm.view_ssn)"
      salary: "int | redact(!role.hr)"
      employee_id: "str | mask(4)"

require(authenticated) and require(role.hr) read attributes resolved from the caller’s verified token. Identity covers how those attributes get there; for now, an identity plugin such as identity/jwt resolves the subject and roles before policy runs.

4. Load and run

engine.load_config_yaml(policy)?;
engine.initialize().await?;

Loading is where mistakes surface. An unknown key fails and names its replacement, an unrecognized plugin kind fails because no factory registered it, and under the default dispatch: policy a declared plugin that no policy reaches fails by name. A configuration that loads is one where every key does something.

The repository carries two runnable programs under crates/ppe-core/examples/:

cargo run -p praxis-policy-core --example plugin_demo
cargo run -p praxis-policy-core --example cmf_capabilities_demo

Both run in dispatch: hooks mode and show the plugin and hook machinery rather than APL policy. Their README explains what each one demonstrates.

What the policy produces

  • An HR caller with view_ssn receives the full record.
  • An HR caller without view_ssn receives the record with ssn redacted before it leaves PPE.
  • A non-HR caller is denied at require(role.hr), and the call never reaches the backend.

Next