Skip to content

refactor!: adopt PPE as the CPEX core for 0.3.0 - #190

Merged
araujof merged 36 commits into
mainfrom
refactor/adopt-ppe-core
Oct 14, 2026
Merged

araujof merged 36 commits into
mainfrom
refactor/adopt-ppe-core

Conversation

@araujof

@araujof araujof commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Summary

CPEX 0.3.0 runs on the praxis-policy engine (PPE) 0.4.1 from crates.io instead of its own engine crates. The engine core and builtins are removed from this repo. CPEX keeps what PPE does not have: the Python plugin host, the C FFI, the Python and Go bindings, the biscuit delegator, the tutorial, and standalone docs. All of them move to PPE's API and policy format.

  • Requirements: docs/brainstorms/ppe-core-adoption-requirements.md
  • Plan: docs/plans/2026-10-07-001-refactor-adopt-ppe-core-plan.md
  • Migration guide: docs/content/docs/migration-0.3.md
  • Changelog: CHANGELOG.md [0.3.0]

Key decisions

  • Dependency: PPE is pinned exactly (=0.4.1) from crates.io. The removed crates stay at 0.2.3 on crates.io.
  • API names: a clean break, PluginManager becomes PolicyEngine on every surface. Python keeps PluginManager as an alias.
  • C ABI: version 3. It adds cpex_apl_install(handle, flags), cpex_refresh_secrets and cpex_secrets_last_success.
  • Policy format: the document follows PPE exactly. There is no shim for old keys; the migration guide maps each one.
  • Builtins in the bindings: they ship every non-experimental PPE builtin. OPA, CIBA, api-key and Vault are new; pii-scanner and audit-logger are gone.
  • Outbound HTTP:
    • The bindings install PPE's HTTP transport.
    • Private and loopback destinations are blocked by default, with a per-binding opt-in.
    • Vault uses the same transport.
  • Legacy Python worker: the Python plugin host translates cmf.* payloads for 0.1.x workers.
    • Post hooks refuse messages with content the plugin cannot see.
    • Pre hooks pass sibling parts through.
  • Edition: Rust edition 2024 with resolver 3.

Verification

  • Parity: tests/parity replays 68 recorded 0.2.3 decisions. Every allow/deny decision matches, and the violation codes match apart from the rule-path change below. The 14 requests with field differences are listed with their cause in tests/parity/intended-differences.yaml:

    • rule-path deny codes drop .apl.
    • OAuth denial reasons no longer forward the IdP description
    • JWKS is refetched once on an unknown key id
    • identity runs only on routes with authentication:
  • Valkey: labels stored by 0.2.3 read back unchanged.

  • Acceptance suites: Python, Go and the Python host each have one, covering identity, delegation, Cedar, OPA, session labels and Valkey shared across engines.

  • Run locally on macOS:

    • cargo test --workspace, clippy -D warnings and fmt
    • make publish-dry for all four publishable crates
    • Go -race
    • Python bindings, 67 tests
    • Python host e2e against the 0.1.x worker, no skips
    • parity with a real Valkey
    • tutorial offline checks
  • Not run locally:

    • Linux and musl FFI builds, and the Linux Go job
    • the Keycloak and SPIRE tutorial modules, because port 8081 was taken locally

    CI covers the first two. The tutorial workflow covers the Keycloak and SPIRE modules.

Known follow-ups

  • Elicitation: an approval is not bound to the requesting subject. This is present in 0.2.3 and PPE 0.4.1 and is documented as a known limitation. Needs an upstream PPE issue.
  • PPE audit sinks: they cannot be used alongside routes under policy dispatch (upstream).
  • identity-jwt: a blocked private JWKS reports auth.jwks_unavailable. A distinct egress code would need an upstream change.
  • Go LoadConfig: errors return codes only, and the message goes to the tracing log. A "last error" FFI call would be additive.
  • HTTP routes: only the Rust surface can invoke them; the FFI and Python have no HTTP payload type.
  • Biscuit: there is no public factory, so it cannot be loaded by kind: from YAML (same as 0.2.3).

Post-Deploy Monitoring & Validation

This is a library release, with no service to deploy. After tagging 0.3.0:

  • Release workflow: all four crates publish in order (cpex-hosts-python, cpex-plugin-delegator-biscuit, cpex, cpex-python). The FFI artifact workflow builds all five targets and reports FFI_ABI=3.

  • Healthy signals:

    • crates.io shows 0.3.0 for the four crates
    • Go consumers pinning go/cpex/v0.3.0 link against the matching FFI artifact
  • Failure signals and rollback:

    • publish stops partway: rerun the tag, which skips versions already published
    • FFI artifacts missing for a target: yank and fix forward

    0.2.3 stays available for anyone who needs to roll back.

  • Owner: the release maintainer, for the first week after the tag.

araujof added 30 commits October 7, 2026 15:15
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
@araujof
araujof marked this pull request as ready for review October 9, 2026 00:54
@araujof araujof added the enhancement New feature or request label Oct 9, 2026
@araujof araujof added this to CPEX Oct 9, 2026
@araujof araujof added this to the 0.3.0 milestone Oct 9, 2026
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>

@terylt terylt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I think things look good. I'd recommend the following.

Findings

1. The APL double-visitor defect is mitigated once per surface instead of upstream

The plan's Context correctly reports that install_builtins calls register_apl each time and that visitor registration appends, so a second install adds a second APL visitor. Both halves check out: install_builtins discards the returned visitor handle (crates/ppe/src/lib.rs:283) and register_visitor is an unconditional push with no dedup (crates/ppe-core/src/engine.rs:1403).

The mitigation is a per-handle installed flag in the FFI (U5) and a separate idempotency guard in the Python bindings (U8). That is the same upstream defect papered over once per binding, and it leaves A1, the Rust embedder, with no protection at all: a Rust host calling install_builtins twice still doubles the visitor, and nothing CPEX ships can prevent that. Because install_builtins throws the handle away, a host cannot even detect the condition.

The plan already has the channel for this. U2 establishes an upstreaming disposition process, and Key Technical Decisions commits to shipping any needed PPE fix as a 0.4.x patch from a release branch. This is exactly that kind of fix (dedup in register_visitor, or an idempotent install_builtins), and it is not routed there.

What I would do: file it as a PPE issue, keep the binding guards as defence in depth, and note in the migration guide that double install is a host error until the upstream fix lands.

2. Deleting the parity harness makes the plan's main control unfalsifiable

U1 says the capture harness location is "chosen during implementation; it is deleted or retargeted in U11". Parity is the one control standing between a 466-file engine swap and a silent behaviour change, and U11's verification is a diff against snapshots that can no longer be regenerated. When a difference appears, there is no way to distinguish a port bug from a bad snapshot without rebuilding the harness from the 0.2.3 tag.

What I would do: keep the harness in tests/parity/, pinned so it runs against the 0.2.3 tag, rather than deleting it. The cost is a directory that does not compile against 0.3.0 and has to be run out of tree; the benefit is that the control can be re-run.

3. Nothing asserts the hook rename table is total

U11 replays U1's request sets through "the reviewed legacy-to-PPE hook rename table". A missing or wrong row in that table converts a real behaviour change into a passing parity result, which is the precise failure mode parity exists to catch. The plan treats the table as reviewed, not as checked.

What I would do: assert in the parity test that every hook name appearing in any captured request has exactly one row, and that every PPE dispatched name reachable by the fixture set is either a target or explicitly listed as unused. Two assertions, and the table stops being load-bearing on review attention alone.

4. U3 does not apply the plan's own test-reconciliation learning

Institutional Learnings records "reconcile tests by executed count and list ignored tests explicitly". U3 deletes seven engine crates and nine builtins, and with them every test those crates carried, yet its verification is only that the workspace resolves and cargo build -p cpex / cargo test -p cpex pass. R19's "existing suites pass" is ambiguous across a deletion that large: a suite that no longer exists trivially passes.

What I would do: record the before and after executed-test counts across the cut-over, name the deleted suites as now covered upstream in PPE, and list the ignored tests on both sides. This is the step most likely to lose coverage without anyone noticing, and the plan already knows the technique.

5. The private-destination opt-in is a single global boolean with documentation as its only control

U12 states the consequence plainly, which is good: the switch permits loopback, RFC 1918, link-local including cloud metadata, and CGNAT, for every outbound builtin at once. But the surface is one constructor keyword argument, one Go option and one FFI flag, and the tutorial and demos turn it on. Those are the configurations integrators copy.

I checked whether a narrower knob exists upstream: it does not. HyperTransport's only egress control is with_allow_private_destinations (crates/ppe/src/http_hyper.rs:216); there is no per-destination allow-list. So a tighter switch is a PPE feature request, not something CPEX could have built, and it is correctly outside the plan's "no PPE feature work beyond upstreaming fixes CPEX needs" boundary.

What I would do: add it to the deferred list as a PPE issue (an allow-list of private hosts, so a JWKS on localhost does not also open cloud metadata), and in the meantime emit a startup warning when the flag is on, so an operator who inherited a demo config sees it in logs rather than only in docs.

6. The out-of-repo Python worker SDK is a release gate with no risk row

"Whether the out-of-process Python worker SDK (outside this repo, pointed to by CPEX_PYTHON_SOURCE) registers handlers by legacy hook names" sits in Deferred to Implementation, resolved in U4. If the answer is yes, every deployed Python plugin breaks on upgrade and the fix lands in a repository this plan does not cover, with its own release to coordinate against the 0.3.0 tag.

The risk table has a row for Python plugins silently losing credentials, which is the in-repo half of the same hazard. The out-of-repo half is larger and absent.

What I would do: give it a risk row with the coordinated-release gate named, next to the credentials row.

7. No abort criterion

One integration branch, one cut-over commit, sixteen crates deleted. The first risk row's mitigation reads "Research found no gaps; any found becomes a PPE 0.4.x patch from a release branch, gating the 0.3.0 tag". The first clause is confidence rather than mitigation, and the second covers small gaps only. The plan never states what happens if U11 surfaces a difference that can be neither fixed nor accepted as intended.

What I would do: one sentence naming the stop condition and who decides, since the branch is the rollback unit and that only works while it is unmerged.

Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
@araujof

araujof commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks, @terylt. I addressed the CPEX-side changes. The 0.2.3 capture harness is retained and pinned, the hook mapping is checked, and the test counts, ignored tests and upstream coverage are recorded. I also added the startup warning, SDK release gate and abort condition.

CPEX now uses PPE 0.5.0, and the tests pass. I opened #208 for duplicate APL installation and #209 for a private-host allow-list. We can handle those in a separate PPE PR. Until then, the binding guards stay in place, and the migration guide calls out the once-per-engine requirement for Rust hosts.

@araujof
araujof requested a review from terylt October 13, 2026 23:44
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>

@terylt terylt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@araujof
araujof merged commit 3262096 into main Oct 14, 2026
16 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in CPEX Oct 14, 2026
@araujof
araujof deleted the refactor/adopt-ppe-core branch October 14, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants