Module 4: Effects & sequencing#
You are in the CPEX tutorial. Runs without the IdP.
Goal: compose several effects in an ordered pipeline that halts on the first denial, run a side-effecting plugin (audit), and emit your own machine-readable denial code.
The problem#
Real routes do more than one thing. They audit the attempt, apply a business rule, then check authorization, in a specific order, where a denial stops everything after it but not the side effects before it. You need to see and control that ordering.
Build it#
pre_invocation is an ordered list. From policies/m04.yaml:
plugins:
- name: audit-log
kind: tutorial/audit-line
hooks: [cmf.tool_pre_invoke]
capabilities: [read_subject, read_meta]
routes:
- tool: send_email
authorization:
pre_invocation:
- "run(audit-log)"
- "args.external == true: deny('outbound email to external recipients is blocked', 'email.external_blocked')"
- "require(authenticated)"CPEX ships no audit plugin of its own, because the record’s format and destination are what each deployment replaces. The audit-log plugin here has kind: tutorial/audit-line, a small host plugin in src/audit.rs. It writes one JSON line per call to stderr with the entity, the subject, the tool name, and the argument keys. It leaves argument values out on purpose, since they can carry PII or secrets. The host registers its factory before loading the policy. From examples/m04_effects.rs:
let engine = Arc::new(PolicyEngine::default());
cpex::install_builtins(&engine);
cpex_tutorial::install_http_transport(&engine);
cpex_tutorial::audit::register(&engine);
engine
.load_config_yaml(POLICY)
.expect("policy m04.yaml should load");
engine.initialize().await.expect("initialize");Policy reaches it by name with run(audit-log), like any other plugin. Module 9 shows how to build one.
Three effects, top to bottom:
run(audit-log)records the attempt and always allows, so the pipeline continues. This is a side effect.- A guard,
<predicate>: deny('<reason>', '<code>').deny(...)produces a denial with a code you choose, so downstream systems can branch on it. require(authenticated)is reached only if the guard passed. Halt-on-deny means a denial at step 2 skips step 3.
Run it#
cargo run -p cpex-tutorial --example m04_effects▸ send_email to an external recipient (custom deny guard halts)
{"ts":1791421465,"plugin":"audit-log","entity":{"type":"tool","name":"send_email"},"tool_call":{"name":"send_email","arg_keys":["external","subject","to"]}}
✗ DENIED [email.external_blocked] outbound email to external recipients is blocked
▸ send_email to an internal recipient (guard passes, auth check halts)
{"ts":1791421465,"plugin":"audit-log","entity":{"type":"tool","name":"send_email"},"tool_call":{"name":"send_email","arg_keys":["external","subject","to"]}}
✗ DENIED [routes.tool:send_email.pre_invocation[2]] access deniedThe caller is anonymous, so the audit line has no subject. The ts value is a Unix timestamp and differs on your run. The audit line (stderr) fires on both calls, because it runs before the denials. The external call halts at the custom guard with email.external_blocked. The internal call gets past the guard to require(authenticated) and halts there. Same route, different stopping point, because the order is policy.
Try it#
- Reorder. Move
require(authenticated)to the top and re-run. Expect: both calls deny with the authentication code, and the external-recipient guard never runs. Ordering changed the outcome and the reason. - Change the code. Edit the
deny(...)code string toemail.blocked_v2. Expect: the external call’s reason code changes, nothing else does. - Drop audit. Remove the
run(audit-log)line and re-run. Expect: same allow and deny outcomes, but no audit line on stderr. The side effect is gone.
Checkpoint#
Why does the audit line appear even when the call is denied?
What decides the denial’s reason code?
Go deeper#
- Effects & Sequencing for the full effect set, halt-on-deny, and
on_allow/on_denyreactions. - Builtins for the bundled plugins.
- Module 9 for how a host plugin like
tutorial/audit-lineis built.
Next#
Module 5: Delegating decisions: hand a decision to CEL or Cedar and let CPEX enforce the verdict.