This guide maps the legacy pure-Python cpex package (at ./cpex/) to the
new Rust-backed package (at ./bindings/python/).
# Before (legacy)
from cpex.framework import PluginManager
from cpex.framework.models import PluginResult
# After (Rust-backed)
from cpex import PluginManager, PipelineResult| Concept | Legacy (./cpex/) |
Rust-backed (bindings/python/) |
|---|---|---|
| Import | from cpex.framework import PluginManager |
from cpex import PluginManager |
| Construction | PluginManager() (Borg singleton) |
PluginManager(config_path) (explicit config) |
| Initialize | await manager.initialize() |
await manager.initialize() |
| Shutdown | await manager.shutdown() |
await manager.shutdown() |
| Invoke | result, violations = await manager.invoke_hook(hook, payload, context) (2-tuple) |
result = await manager.invoke_hook(hook, payload) (single result) |
| Result type | (PluginResult, list[Violation]) 2-tuple |
PipelineResult single object |
continue_processing |
result.continue_processing |
result.continue_processing |
| Violation | Returned as second element of tuple | result.violation dict (or None) |
| Plugin errors | result.errors / violations_as_exceptions |
result.errors list of dicts |
| Payload type | Pydantic models | Plain dict (JSON-compatible) |
| Config | PLUGINS_* env vars |
YAML config file path at construction |
| Extensions | GlobalContext |
extensions kwarg dict |
| Context table | context kwarg (Pydantic model) |
context_table kwarg dict |
Hook names follow the engine (cmf.tool_pre_invoke, identity.resolve,
token.delegate, etc.). A name removed in 0.3 (tool_pre_invoke,
identity_resolve, prompt_pre_fetch and the rest) raises ValueError
naming its replacement, because it would reach no handler and come back
allowed. The Rust-backed package picks the payload type from the hook
name: cmf.* hooks use typed MessagePayload, identity.resolve uses
typed IdentityPayload, and all other hooks use GenericPayload. For the
two typed cases, a dict that doesn't match the schema raises ValueError.
An identity.resolve payload may set only source, source_header,
headers, client_host and client_port; one that sets a resolver
output field (subject, raw_claims and the like) raises ValueError.
The result's modified_payload does not echo headers.
The Rust-backed package uses plain Python dicts that are converted via
direct PyObject ↔ serde_json traversal. There is no Pydantic validation
layer. Dict keys must be strings; values must be JSON-compatible types
(bool, int, float, str, None, list, dict). Nesting deeper
than 128 levels raises ValueError.
For cmf.* hooks the dict must match the MessagePayload schema:
payload = {
"message": {
"role": "user",
"content": [{"type": "text", "text": "Hello"}],
}
}| Field | Type | Notes |
|---|---|---|
continue_processing |
bool |
False when a plugin denied |
violation |
dict | None |
Populated on deny; keys: code, reason, description, details |
errors |
list[dict] |
Per-plugin errors from on_error: ignore plugins |
modified_payload |
dict | None |
Payload after transform-phase modifications |
modified_extensions |
dict | None |
Extensions after modifications |
metadata |
dict | None |
Optional telemetry metadata |
context_table |
dict |
Per-plugin state for stateful plugins |
| Scenario | Exception |
|---|---|
| Missing / unreadable config file | ValueError |
| Malformed YAML | ValueError |
| Payload conversion failure | ValueError |
| Nesting > 128 levels | ValueError |
| Plugin execution error | RuntimeError |
| Wall-clock timeout exceeded | TimeoutError |
Policy denials do not raise — they surface as
result.continue_processing == False with result.violation populated.
The legacy framework's fire_and_forget plugins behave the same way:
they run asynchronously and their side effects are only guaranteed
after await manager.shutdown(). If you need to assert audit-log
side effects in tests, always call shutdown first.
The following features from the legacy package are not available in v1 of the Rust-backed package:
- Delegation (
token_delegate, nowtoken.delegate): inbound tokens are never serialized, so a delegator cannot exchange them for a call made from Python.identity.resolve(formerlyidentity_resolve) does take a typedIdentityPayloaddict; see the README. - Dual-mode / backend selection: this package is always Rust-backed. There is no environment variable to switch between backends.
The Rust-backed cpex package and the legacy cpex package share the
same top-level import name. They must never be installed in the same
virtualenv. Always install the Rust-backed package in its own venv:
python -m venv .venv-cpex-rust
source .venv-cpex-rust/bin/activate
pip install maturin
cd bindings/python && maturin develop