2020# 2 validators (pii-scan, audit-log)
2121# global.apl.pdp: 1 Cedar PDP (relationship-based authorization)
2222#
23- # routes: get_compensation → ACT 1 · Workday flow
24- # search_repos → ACT 2 · GitHub flow
25- # send_email → ACT 3 · PII scanner
23+ # Each route names the identities it needs in its `authentication:`
24+ # block and lists its ordered authorization effects under
25+ # `authorization.pre_invocation:`. Plugins otherwise resolve by name +
26+ # hook, so the grouping above is purely for reading.
2627#
27- # Plugins resolve by name + hook + priority, NOT by their order in this
28- # file, so the grouping below is purely for reading.
28+ # Syntax note: this file uses the CPEX v0.2.2 *canonical* APL shape —
29+ # per-route `authentication:` + `authorization:` sibling blocks, no
30+ # `apl:` wrapper, `args:`/`result:` as route-level siblings, and `run(...)`
31+ # to invoke a plugin as an effect (the old `plugin(...)` spelling still
32+ # works as an alias). The pre-0.2.1 `identity:` / `policy:` keys are hard
33+ # errors now.
2934# ────────────────────────────────────────────────────────────────────
3035#
3136# Keycloak runs locally at http://localhost:8081. JWTs are RS256-signed
@@ -40,14 +45,13 @@ plugins:
4045 # ── IDENTITY ─────────────────────────────────────────────────────
4146 # Two JWT resolvers fire on identity.resolve: the human user's token
4247 # (X-User-Token) and the agent's client token (Authorization). Both
43- # verify against the realm JWKS.
48+ # verify against the realm JWKS. Each route lists the ones it needs in
49+ # its `authentication:` block.
4450
4551 # User identity. Carries the human's roles + permissions claims.
4652 - name : jwt-user
4753 kind : identity/jwt
4854 hooks : [identity.resolve]
49- mode : sequential
50- priority : 10
5155 on_error : fail
5256 config :
5357 role : user
@@ -69,8 +73,6 @@ plugins:
6973 - name : jwt-client
7074 kind : identity/jwt
7175 hooks : [identity.resolve]
72- mode : sequential
73- priority : 20
7476 on_error : fail
7577 config :
7678 role : client
@@ -101,9 +103,9 @@ plugins:
101103 - name : workday-oauth
102104 kind : delegator/oauth
103105 hooks : [token.delegate]
104- mode : sequential
105- priority : 30
106- on_error : fail
106+ on_error : fail # delegation failure must BLOCK the call — get_compensation
107+ # has no require(delegation.granted) post-check, so without
108+ # this a failed token exchange would fail open.
107109 capabilities :
108110 - read_inbound_credentials
109111 - write_delegated_tokens
@@ -130,9 +132,9 @@ plugins:
130132 - name : github-oauth
131133 kind : delegator/oauth
132134 hooks : [token.delegate]
133- mode : sequential
134- priority : 31
135- on_error : fail
135+ on_error : fail # delegation failure must BLOCK the call. search_repos also
136+ # has a granted-permissions post-check, but fail-closed here
137+ # keeps a transport/exchange error from ever slipping through.
136138 capabilities :
137139 - read_inbound_credentials
138140 - write_delegated_tokens
@@ -150,7 +152,7 @@ plugins:
150152
151153 # ── VALIDATORS ───────────────────────────────────────────────────
152154 # pii-scan denies on sensitive patterns; audit-log observes and never
153- # blocks. Routes invoke them via plugin (pii-scan) / plugin (audit-log).
155+ # blocks. Routes invoke them via run (pii-scan) / run (audit-log).
154156
155157 # ----------------------------------------------------------------
156158 # Field-level validator: PII scanner.
@@ -161,8 +163,6 @@ plugins:
161163 - name : pii-scan
162164 kind : validator/pii-scan
163165 hooks : [cmf.tool_pre_invoke]
164- mode : sequential
165- priority : 25
166166 on_error : fail
167167 config :
168168 detect :
@@ -177,15 +177,14 @@ plugins:
177177 # client, entity, tool args, and any delegation outcomes. Never
178178 # blocks — observation only. Wired on every CMF entity hook so
179179 # the audit trail covers all tool / prompt / resource traffic.
180+ # Placed LAST in each route's pre_invocation so the record reflects
181+ # the final decision + minted tokens.
180182 - name : audit-log
181183 kind : audit/logger
182184 hooks :
183185 - cmf.tool_pre_invoke
184186 - cmf.prompt_pre_invoke
185187 - cmf.resource_pre_fetch
186- mode : sequential
187- priority : 90 # fires AFTER policy / delegate so the record
188- # reflects the final decision + minted tokens
189188 on_error : ignore # observation failures don't halt traffic
190189 capabilities :
191190 - read_subject
@@ -197,10 +196,6 @@ plugins:
197196 source : cpex-demo-gateway
198197
199198global :
200- identity :
201- - jwt-user
202- - jwt-client
203-
204199 # ----------------------------------------------------------------
205200 # Cedar PDP — declarative authorization for relationship-based
206201 # decisions. The Cedar policies live inline below. APL routes
@@ -254,8 +249,11 @@ routes:
254249 # role/perm predicate + redacts ssn on the wire if missing.
255250 # ----------------------------------------------------------------
256251 - tool : get_compensation
257- apl :
258- policy :
252+ authentication :
253+ - jwt-user
254+ - jwt-client
255+ authorization :
256+ pre_invocation :
259257 - " require(role.hr)"
260258 - " delegate(workday-oauth, target: workday-api, audience: workday-api, permissions: [read_compensation])"
261259 # Taint the session: reading compensation marks this session as
@@ -267,22 +265,22 @@ routes:
267265 - " taint(secret, session)"
268266 # Emit a structured audit record AFTER the policy + delegate
269267 # decisions are in. Records who called what, with which scope.
270- - " plugin (audit-log)"
271- args :
272- # `redact(!perm.view_ssn)` fires when the caller LACKS the
273- # permission. The arg key remains, value becomes "[REDACTED]" —
274- # downstream tool can see "ssn was here but you can't read it."
275- ssn : " str | redact(!perm.view_ssn)"
276- result :
277- # Symmetric guard on the response side. The tool may include
278- # the SSN in its result (it does, when include_ssn=true);
279- # without this rule, a caller with role.hr but NOT
280- # perm.view_ssn (e.g. eve) would receive it. Redacting on the
281- # response closes the loop — the LLM-driven flow doesn't even
282- # require the caller to mention `ssn` in args for the guard
283- # to fire, because the SSN comes back unsolicited in the
284- # upstream tool's response.
285- ssn : " str | redact(!perm.view_ssn)"
268+ - " run (audit-log)"
269+ args :
270+ # `redact(!perm.view_ssn)` fires when the caller LACKS the
271+ # permission. The arg key remains, value becomes "[REDACTED]" —
272+ # downstream tool can see "ssn was here but you can't read it."
273+ ssn : " str | redact(!perm.view_ssn)"
274+ result :
275+ # Symmetric guard on the response side. The tool may include
276+ # the SSN in its result (it does, when include_ssn=true);
277+ # without this rule, a caller with role.hr but NOT
278+ # perm.view_ssn (e.g. eve) would receive it. Redacting on the
279+ # response closes the loop — the LLM-driven flow doesn't even
280+ # require the caller to mention `ssn` in args for the guard
281+ # to fire, because the SSN comes back unsolicited in the
282+ # upstream tool's response.
283+ ssn : " str | redact(!perm.view_ssn)"
286284
287285 # ════════════════════════════════════════════════════════════════
288286 # ACT 2 · GITHUB FLOW · scenarios 04-alice-internal / 05-external / 06-bob
@@ -305,8 +303,11 @@ routes:
305303 # forward — the token never reaches GitHub.
306304 # ----------------------------------------------------------------
307305 - tool : search_repos
308- apl :
309- policy :
306+ authentication :
307+ - jwt-user
308+ - jwt-client
309+ authorization :
310+ pre_invocation :
310311 # Layer 1 — coarse APL gate on group membership.
311312 - " require(team.engineering | team.security)"
312313
@@ -333,7 +334,7 @@ routes:
333334 - " !(delegation.granted.permissions contains 'repo:read:internal'): deny"
334335
335336 # Audit every github call.
336- - " plugin (audit-log)"
337+ - " run (audit-log)"
337338
338339 # ════════════════════════════════════════════════════════════════
339340 # ACT 3 · PII SCANNER · scenario 07-bob-pii-deny
@@ -347,12 +348,15 @@ routes:
347348 # via the audit logger.
348349 # ----------------------------------------------------------------
349350 - tool : send_email
350- apl :
351- policy :
351+ authentication :
352+ - jwt-user
353+ - jwt-client
354+ authorization :
355+ pre_invocation :
352356 - " require(perm.email_send)"
353357 # Walks args.body / args.subject / args.to and denies if any
354358 # carries an SSN-like or credit-card-like pattern.
355- - " plugin (pii-scan)"
359+ - " run (pii-scan)"
356360 # ACT 4 · TAINT PROPAGATION · scenario 08-bob-taint-deny
357361 # Block external email from a session that previously accessed
358362 # secret data (get_compensation taints the session "secret").
@@ -367,4 +371,4 @@ routes:
367371 # yet wired in the pinned FFI — labels surface uniformly under
368372 # `security.labels`). Session scope is what makes it survive to here.
369373 - " security.labels contains \" secret\" : deny('external email blocked: this session accessed secret data', 'session_tainted_secret')"
370- - " plugin (audit-log)"
374+ - " run (audit-log)"
0 commit comments