Module 1: Hello, enforcement#
You are in the CPEX tutorial. No IdP needed for this module.
Goal: stand up the smallest possible CPEX enforcement point and see a route allow one call and deny another, with no application logic making the decision.
The problem#
You have tools an agent can call. Some should be gated, some open. You do not want that decision scattered through handler code, where it drifts and is hard to audit. You want it in one place, declarative, at the boundary.
Build it#
The host is three lines plus loading a policy. From examples/tutorial/examples/m01_hello.rs:
let mgr = Arc::new(PluginManager::default());
cpex::install_builtins(&mgr); // register the bundled plugins + APL visitor
mgr.load_config_yaml(POLICY).unwrap(); // load policies/m01.yaml
mgr.initialize().await.unwrap();The policy (policies/m01.yaml) defines two routes:
routes:
- tool: get_compensation
authorization:
pre_invocation:
- "require(authenticated)" # denies an anonymous caller
- tool: search_repos
authorization:
pre_invocation: [] # no rule, so openEach call goes through mediate(), the harness wrapper around the loop a host owns: resolve identity, run policy, call the backend, run policy on the result. It is harness code, not a CPEX API. Module 9 opens it up.
Run it#
cargo run -p cpex-tutorial --example m01_hello▸ anonymous → get_compensation (route requires authentication)
✗ DENIED [routes.tool:get_compensation.apl.pre_invocation[0]] access denied
▸ anonymous → search_repos (route has no rule)
✓ ALLOWED {"visibility":"public","repositories":[{"name":"brand-site","visibility":"public"}]}Same anonymous caller, same host code. The route decided the outcome, and the denial names the exact rule that failed.
Try it#
- Change the failing predicate. In
examples/tutorial/policies/m01.yaml, changerequire(authenticated)torequire(role.hr)and re-run. Expect:get_compensationstill denies, because the anonymous caller has no role either. The displayed reason is the same generic, position-based code (...pre_invocation[0]] access denied); what changed is which predicate rejected the call, not the visible text. - Open the gated route. Delete the
require(authenticated)line (leavepre_invocation: []) and re-run. Expect: both calls allow. - Gate the open route.
get_compensationalready denies; add- "require(authenticated)"undersearch_repostoo, leavingget_compensationunchanged, and re-run. Expect: both calls now deny.
Reset any time with git checkout -- examples/tutorial/policies.
Checkpoint#
Why did get_compensation deny when the Rust code never checked anything?
What makes search_repos allow?
Go deeper#
- APL: routes and phases for the full route model.
- Quick Start for the same shape in prose.
Next#
Module 2: Who’s calling?: give callers a real identity so require(role.hr) has something to check. Start the IdP first.