Skip to content

Commit 448aef2

Browse files
authored
Merge branch 'dev' into feat/plugin-wasm-e2e
2 parents e53e023 + b9d31de commit 448aef2

6 files changed

Lines changed: 1158 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ jobs:
110110
- name: Checkout the cpex Python framework
111111
uses: actions/checkout@v7
112112
with:
113-
ref: "feat/python_plugin_compat_0.1.x"
113+
ref: "0.1.x"
114114
path: cpex-python
115115
fetch-depth: 1
116116
- name: Install Python

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ cpex-pdp-cedar-direct = { path = "builtins/pdps/cedar-direct", v
147147
cpex-pdp-cel = { path = "builtins/pdps/cel", version = "0.2.2" }
148148
cpex-pdp-opa = { path = "builtins/pdps/opa", version = "0.2.2" }
149149
cpex-session-valkey = { path = "builtins/session/valkey", version = "0.2.2" }
150+
cpex-hosts-python = { path = "crates/cpex-hosts-python", version = "0.2.2", default-features = false }
150151

151152
# Size-first release profile. The FFI artifact (libcpex_ffi.a) is linked
152153
# statically into host binaries, so its compiled size flows straight into

crates/cpex/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,22 @@ cedar = ["cpex-builtins/cedar-direct"]
6262
cel = ["cpex-builtins/cel"]
6363
valkey = ["cpex-builtins/valkey"]
6464

65+
# Out-of-process host for existing Python CPEX plugins. Not part of
66+
# `builtins` / `full`: those are the in-process bundled extension set, while
67+
# this pulls a host that spawns a `worker.py` subprocess per plugin.
68+
python-host = ["dep:cpex-hosts-python"]
69+
6570
[dependencies]
6671
# Host runtime — always present, this is the point of the facade.
6772
cpex-core = { workspace = true }
6873
apl-core = { workspace = true }
6974
apl-cmf = { workspace = true }
7075
apl-cpex = { workspace = true }
7176

77+
# Python plugin host — runs each Python plugin out-of-process, in its own
78+
# virtualenv, from the Rust PluginManager. Present only with `python-host`.
79+
cpex-hosts-python = { workspace = true, optional = true, default-features = false }
80+
7281
# Bundled extension set — present only when a builtins feature is enabled.
7382
# `default-features = false` so the granular plugin features compose; an
7483
# enabling feature turns on exactly the cpex-builtins features it names.

crates/cpex/src/lib.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
//!
2727
//! By default it is the **engine only**: no builtin plugins are compiled in.
2828
//! The bundled extension set lives in [`cpex-builtins`](cpex_builtins) and is
29-
//! pulled in only when a builtins feature is enabled.
29+
//! pulled in only when a builtins feature is enabled. The out-of-process
30+
//! Python plugin host is separate again, behind `python-host`.
3031
//!
3132
//! # Usage
3233
//!
@@ -61,6 +62,27 @@
6162
//! `audit`, `cedar`, `cel`, `valkey`). When any builtins feature is on, the
6263
//! registration helpers and the concrete factory types are re-exported here
6364
//! from [`cpex-builtins`](cpex_builtins).
65+
//!
66+
//! `python-host` is orthogonal to all of those, and is in neither `builtins`
67+
//! nor `full`. It pulls
68+
//! [`cpex-hosts-python`](cpex_hosts_python), which runs existing Python CPEX
69+
//! plugins out-of-process — one cached virtualenv and one `worker.py`
70+
//! subprocess per plugin — and re-exports [`IsolatedVenvFactory`] and
71+
//! [`ISOLATED_VENV_KIND`] here. Unlike the builtins there is no
72+
//! `install_*` helper: register the factory yourself, because the `kind` is
73+
//! one host serving arbitrarily many Python plugins.
74+
//!
75+
//! ```ignore
76+
//! use cpex::{IsolatedVenvFactory, PluginManager, ISOLATED_VENV_KIND};
77+
//! use cpex::cpex_core::factory::PluginFactoryRegistry;
78+
//!
79+
//! let mut factories = PluginFactoryRegistry::new();
80+
//! factories.register(ISOLATED_VENV_KIND, Box::new(IsolatedVenvFactory));
81+
//! let mgr = PluginManager::from_config(config, &factories)?;
82+
//! // `initialize()` builds each venv and launches its worker — a cold pip
83+
//! // install is measured in minutes, so do it at startup, not on demand.
84+
//! mgr.initialize().await?;
85+
//! ```
6486
6587
// Whole-crate re-exports for advanced use (types not surfaced below).
6688
pub use {apl_cmf, apl_core, apl_cpex, cpex_core};
@@ -78,6 +100,9 @@ pub use cpex_wasm_host;
78100
// The whole aggregator, for advanced use.
79101
#[cfg(feature = "cpex-builtins")]
80102
pub use cpex_builtins;
103+
// The whole Python host crate, for advanced use (venv and worker internals).
104+
#[cfg(feature = "python-host")]
105+
pub use cpex_hosts_python;
81106

82107
// Registration helpers — delegated to cpex-builtins, keeping the facade's
83108
// historical names (`register_builtin_plugins`, `builtin_pdp_factories`).
@@ -103,6 +128,11 @@ pub use cpex_builtins::{OAuthDelegatorFactory, OAUTH_KIND};
103128
pub use cpex_builtins::{PiiScannerFactory, PII_KIND};
104129
#[cfg(feature = "valkey")]
105130
pub use cpex_builtins::{ValkeyConfig, ValkeySessionStoreFactory, VALKEY_KIND};
131+
// The Python host's `KIND` is renamed on re-export: bare `KIND` at the facade
132+
// root says nothing about which plugin kind it is, and the builtins above all
133+
// use a prefixed const.
134+
#[cfg(feature = "python-host")]
135+
pub use cpex_hosts_python::{IsolatedVenvFactory, KIND as ISOLATED_VENV_KIND};
106136

107137
#[cfg(all(test, feature = "cpex-builtins"))]
108138
mod tests {
@@ -115,3 +145,42 @@ mod tests {
115145
install_builtins(&mgr);
116146
}
117147
}
148+
#[cfg(all(test, feature = "python-host"))]
149+
mod python_host_tests {
150+
use super::*;
151+
use cpex_core::config::parse_config;
152+
use cpex_core::factory::PluginFactoryRegistry;
153+
154+
/// A config with one `isolated_venv` plugin, mirroring the YAML shape an
155+
/// operator writes. No `plugin_dirs`: the host always resolves
156+
/// `<project root>/plugins` — see `plugin::DEFAULT_PLUGIN_DIR`.
157+
fn minimal_config_yaml() -> &'static str {
158+
r#"
159+
plugins:
160+
- name: pii-filter
161+
kind: isolated_venv
162+
hooks: [tool_pre_invoke]
163+
config:
164+
class_name: my_pkg.filters.PiiFilter
165+
"#
166+
}
167+
168+
/// The facade's re-exported `IsolatedVenvFactory` and kind const are
169+
/// wired up well enough to instantiate a plugin from config. This stops
170+
/// at `from_config` deliberately — `initialize()` is what builds the venv
171+
/// and spawns `worker.py`, which needs a real interpreter and a real
172+
/// package, so it belongs in cpex-hosts-python's integration tests.
173+
#[test]
174+
fn from_config_instantiates_the_python_host() {
175+
let config = parse_config(minimal_config_yaml()).expect("valid YAML");
176+
177+
let mut factories = PluginFactoryRegistry::new();
178+
factories.register(ISOLATED_VENV_KIND, Box::new(IsolatedVenvFactory));
179+
180+
let mgr = PluginManager::from_config(config, &factories)
181+
.expect("isolated_venv factory is registered, so instantiation succeeds");
182+
183+
assert_eq!(mgr.plugin_count(), 1);
184+
assert!(mgr.has_hooks_for("tool_pre_invoke"));
185+
}
186+
}

0 commit comments

Comments
 (0)