Skip to content

Commit 821160f

Browse files
committed
build(cpex): bump CPEX to v0.2.2 and migrate hr demo policy to canonical APL syntax
Bumps the CPEX dependency from v0.2.0-alpha.2 to v0.2.2: * CPEX_FFI_VERSION -> v0.2.2 (FFI ABI unchanged, still 2) * github.com/contextforge-org/cpex/go/cpex v0.2.2 in authlib + the authbridge-cpex module (go.mod/go.sum) v0.2.1 renamed the APL authz/authn config keys (breaking): the old 'identity:' and 'policy:' keys are hard load errors now. Migrate the hr-cpex demo policy to the v0.2.2 canonical shape: * routes drop the 'apl:' wrapper -> per-route 'authentication:' + 'authorization.pre_invocation:', with 'args:'/'result:' as siblings * global.identity -> per-route authentication groups * plugin(...) -> run(...) Cedar PDP (cedar-direct), delegate(..., permissions:), taint(), redact(), and inline ': deny(...)' guards are unchanged. The require()-derived deny codes shift from '..._apl_policy_0_' to '..._apl_pre_invocation_0_'; scenarios 02 and 06 expected-code notes updated to match. Also refreshed the cpex plugin + demo READMEs to the canonical syntax. Validated end-to-end on kind-kagenti: rebuilt authbridge-cpex linking the v0.2.2 musl FFI, sidecar parsed the canonical policy cleanly, all 9 demo scenarios pass with identical authorization outcomes. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
1 parent 26740cf commit 821160f

10 files changed

Lines changed: 93 additions & 81 deletions

File tree

authbridge/authlib/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/kagenti/kagenti-extensions/authbridge/authlib
33
go 1.26.4
44

55
require (
6-
github.com/contextforge-org/cpex/go/cpex v0.2.0-alpha.2
6+
github.com/contextforge-org/cpex/go/cpex v0.2.2
77
github.com/envoyproxy/go-control-plane/envoy v1.37.0
88
github.com/fsnotify/fsnotify v1.10.1
99
github.com/gobwas/glob v0.2.3

authbridge/authlib/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI
5858
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
5959
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
6060
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
61-
github.com/contextforge-org/cpex/go/cpex v0.2.0-alpha.2 h1:Ddpq+JFjnA7G4VUiHJPP2520GPmIv2XmikUV2n/gRd0=
62-
github.com/contextforge-org/cpex/go/cpex v0.2.0-alpha.2/go.mod h1:SuDkdY76asy9MP/72YKqhc2FBcFZ9t5PKe3CeXSO4/g=
61+
github.com/contextforge-org/cpex/go/cpex v0.2.2 h1:rC0HciJa19nEz9LcjmgwyjoRF3EUFidNpAT6eYu1fhA=
62+
github.com/contextforge-org/cpex/go/cpex v0.2.2/go.mod h1:SuDkdY76asy9MP/72YKqhc2FBcFZ9t5PKe3CeXSO4/g=
6363
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6464
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6565
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=

authbridge/authlib/plugins/cpex/README.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ so resolved credentials reach delegation in CPEX memory.
8484
## CPEX plugins
8585

8686
APL routes pull from a toolbox of named CPEX sub-plugins declared in the CPEX
87-
YAML. Routes invoke them by name through APL steps: `plugin(pii-scan)`,
87+
YAML. Routes invoke them by name through APL steps: `run(pii-scan)`,
8888
`delegate(workday-oauth, ...)`, `cedar: { ... }`. See the HR demo for examples.
8989

9090
## CPEX APL
@@ -96,24 +96,30 @@ sequencing, over the common CMF vocabulary. A route binds a policy to one entity
9696
```yaml
9797
routes:
9898
- tool: get_compensation
99-
apl:
100-
policy:
99+
authentication:
100+
- jwt-user # identity plugins that resolve the caller
101+
- jwt-client
102+
authorization:
103+
pre_invocation:
101104
- "require(role.hr)"
102105
- "delegate(workday-oauth, target: workday-api, audience: workday-api, permissions: [read_compensation])"
103106
- "taint(secret, session)" # session label persists across calls
104-
- "plugin(audit-log)"
105-
args:
106-
ssn: "str | redact(!perm.view_ssn)" # redact request arg when caller lacks the perm
107-
result:
108-
ssn: "str | redact(!perm.view_ssn)" # redact the response field too
107+
- "run(audit-log)"
108+
args:
109+
ssn: "str | redact(!perm.view_ssn)" # redact request arg when caller lacks the perm
110+
result:
111+
ssn: "str | redact(!perm.view_ssn)" # redact the response field too
109112

110113
- tool: send_email
111-
apl:
112-
policy:
114+
authentication:
115+
- jwt-user
116+
- jwt-client
117+
authorization:
118+
pre_invocation:
113119
- "require(perm.email_send)"
114-
- "plugin(pii-scan)"
120+
- "run(pii-scan)"
115121
- "security.labels contains \"secret\": deny('external email blocked', 'session_tainted_secret')"
116-
- "plugin(audit-log)"
122+
- "run(audit-log)"
117123
```
118124
119125
`get_compensation` taints the session with `secret`. The label persists in the
@@ -127,17 +133,17 @@ APL steps can call out to external policy decision points and act on their
127133
verdict with the same effect vocabulary:
128134

129135
```yaml
130-
policy:
136+
pre_invocation:
131137
- require(authenticated)
132138
- opa("http://opa:8181/v1/data/hr/compensation/deny"):
133139
on_deny:
134140
- deny
135141
- taint(compensation_violation, session)
136-
- plugin(audit-log)
142+
- run(audit-log)
137143
- cedar:
138144
action: "Jans::Action::Read"
139145
resource_type: "Jans::CompensationRecord"
140-
on_deny: [deny, plugin(compliance_alert)]
146+
on_deny: [deny, run(compliance_alert)]
141147
- authzen("https://authz.corp.com/access/v1/evaluation"):
142148
on_deny: [deny]
143149
```
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.2.0-alpha.2
1+
v0.2.2

authbridge/cmd/authbridge-cpex/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require github.com/kagenti/kagenti-extensions/authbridge/authlib v0.0.0-00010101
66

77
require (
88
github.com/Microsoft/go-winio v0.6.2 // indirect
9-
github.com/contextforge-org/cpex/go/cpex v0.2.0-alpha.2 // indirect
9+
github.com/contextforge-org/cpex/go/cpex v0.2.2 // indirect
1010
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1111
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
1212
github.com/fsnotify/fsnotify v1.10.1 // indirect

authbridge/cmd/authbridge-cpex/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcO
88
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
99
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
1010
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
11-
github.com/contextforge-org/cpex/go/cpex v0.2.0-alpha.2 h1:Ddpq+JFjnA7G4VUiHJPP2520GPmIv2XmikUV2n/gRd0=
12-
github.com/contextforge-org/cpex/go/cpex v0.2.0-alpha.2/go.mod h1:SuDkdY76asy9MP/72YKqhc2FBcFZ9t5PKe3CeXSO4/g=
11+
github.com/contextforge-org/cpex/go/cpex v0.2.2 h1:rC0HciJa19nEz9LcjmgwyjoRF3EUFidNpAT6eYu1fhA=
12+
github.com/contextforge-org/cpex/go/cpex v0.2.2/go.mod h1:SuDkdY76asy9MP/72YKqhc2FBcFZ9t5PKe3CeXSO4/g=
1313
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1414
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1515
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=

authbridge/demos/hr-cpex/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ compose three things a plain PDP does not:
3737
cheap predicates first and the PDP only for requests that clear them.
3838
- **Explicit effects.** Authorization is more than allow or deny. APL
3939
expresses effects as first-class steps: `redact(...)` rewrites a
40-
field on the wire, `plugin(pii-scan)` runs a content guardrail,
41-
`plugin(audit-log)` records the outcome. The decision and what to do
40+
field on the wire, `run(pii-scan)` runs a content guardrail,
41+
`run(audit-log)` records the outcome. The decision and what to do
4242
about it live in the same place.
4343
- **Actions as decisions.** `delegate(...)` mints a downstream-scoped
4444
token (RFC 8693 token exchange) as a policy step, and a post-check
@@ -328,9 +328,11 @@ permission directly on the user token. APL gates on a flat predicate
328328
and redacts the SSN on the wire when the permission is missing:
329329

330330
```text
331-
require(role.hr)
332-
delegate(workday-oauth, target: workday-api, permissions: [read_compensation])
333-
plugin(audit-log)
331+
authentication: [jwt-user, jwt-client]
332+
authorization.pre_invocation:
333+
require(role.hr)
334+
delegate(workday-oauth, target: workday-api, permissions: [read_compensation])
335+
run(audit-log)
334336
args.ssn -> str | redact(!perm.view_ssn)
335337
result.ssn -> str | redact(!perm.view_ssn)
336338
```

authbridge/demos/hr-cpex/k8s/cpex-policy.yaml

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
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

199198
global:
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)"

authbridge/demos/hr-cpex/scenarios/02-alice-deny.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ set -euo pipefail
1414
source "$(dirname "$0")/_lib.sh"
1515

1616
step "Alice (engineer) → get_compensation"
17-
note "Expected: HTTP 200 + JSON-RPC error frame, error.data.error=cpex.routes_tool_get_compensation_apl_policy_0_"
17+
note "Expected: HTTP 200 + JSON-RPC error frame, error.data.error=cpex.routes_tool_get_compensation_apl_pre_invocation_0_"
1818
note "Triggered by: require(role.hr) deny BEFORE delegation runs"
1919
note "Expected upstream: no inbound request (gateway short-circuited)"
2020

authbridge/demos/hr-cpex/scenarios/06-bob-apl-deny.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
#
1515
# Result: HTTP 200 with an MCP JSON-RPC 2.0 error frame; error.data.error =
1616
# the cpex.* form of the apl.policy step index that failed
17-
# (cpex.routes_tool_search_repos_apl_policy_0_).
17+
# (cpex.routes_tool_search_repos_apl_pre_invocation_0_).
1818

1919
set -euo pipefail
2020
source "$(dirname "$0")/_lib.sh"
2121

2222
step "Bob (HR) → search_repos (gateway short-circuits at the APL gate)"
23-
note "Expected: HTTP 200 + JSON-RPC error frame, error.data.error=cpex.routes_tool_search_repos_apl_policy_0_"
23+
note "Expected: HTTP 200 + JSON-RPC error frame, error.data.error=cpex.routes_tool_search_repos_apl_pre_invocation_0_"
2424
note "Triggered by: require(team.engineering | team.security) — Bob is team.hr"
2525
note "Expected: Cedar never runs; IdP never called"
2626

0 commit comments

Comments
 (0)