use std::path::{Path, PathBuf};
use std::sync::Arc;
use cpex_hosts_python::extensions::{EXTENSIONS_FIELD, MODIFIED_EXTENSIONS_FIELD};
use cpex_hosts_python::factory::KIND;
use cpex_hosts_python::testing::{
TEMP_DIR_KIND, TempDir, TempDirFactory, prebuild_venv, python_source, scaffold_plugin, skip,
skip_without_python3, worker_delivers_extensions,
};
use ppe::praxis_policy_core::cmf::{ContentPart, Message, MessagePayload, Role, ToolCall};
use ppe::praxis_policy_core::extensions::{Extensions, HttpExtension, SecurityExtension};
use ppe::praxis_policy_core::plugin::PluginConfig;
const FIXTURE_CLASS: &str = "ext_pkg.extensions_plugin.ExtensionsAwarePlugin";
const INBOUND_LABEL: &str = "E2E-INBOUND-LABEL-7b21";
const APPENDED_LABEL: &str = "E2E-APPENDED-LABEL-c93f";
const FIXTURE_PLUGIN: &str = r#"
import os
from cpex.framework.base import Plugin
from cpex.framework.models import PluginResult
INBOUND_LABEL = "E2E-INBOUND-LABEL-7b21"
APPENDED_LABEL = "E2E-APPENDED-LABEL-c93f"
class ExtensionsAwarePlugin(Plugin):
"""Reports the extensions it received, then appends a security label."""
def __init__(self, config):
super().__init__(config)
async def tool_pre_invoke(self, payload, context, extensions):
# A 2-arg plugin would never get here; the framework only forwards
# extensions to hooks whose signature accepts them.
if extensions is None:
verdict = "NO_EXTENSIONS"
else:
labels = set()
if extensions.security is not None:
labels = set(extensions.security.labels or [])
saw_label = INBOUND_LABEL in labels
# Sensitive headers must never arrive over this channel.
#
# The Rust host serializes `request_headers`/`response_headers`, but
# this framework's HttpExtension models a single `headers` field, and
# pydantic drops unknown keys silently — so the slot can arrive
# present-but-empty. Read whichever attribute this build actually
# exposes instead of assuming one: hardcoding the wrong name turns a
# header leak into an AttributeError, which is a crash rather than
# the security assertion this test is here to make.
headers = {}
slot_empty = False
if extensions.http is not None:
for attr in ("request_headers", "headers"):
if hasattr(extensions.http, attr):
headers = dict(getattr(extensions.http, attr) or {})
break
slot_empty = not headers
lowered = {k.lower() for k in headers}
leaked = lowered & {"authorization", "cookie", "x-api-key"}
verdict = "GOT_LABEL" if saw_label else "NO_LABEL"
if leaked:
verdict += f";LEAKED={sorted(leaked)}"
if "x-request-id" in lowered:
verdict += ";GOT_SAFE_HEADER"
elif slot_empty:
# Raw headers are withheld by default, even on a live channel.
verdict += ";HTTP_SLOT_EMPTY"
marker = os.path.join(os.getcwd(), "extensions_verdict.txt")
with open(marker, "w") as f:
f.write(verdict)
# Append a label. `Extensions` is frozen, so modification means a new
# instance via model_copy — the framework's documented pattern.
modified = None
if extensions is not None and extensions.security is not None:
new_labels = list(extensions.security.labels or []) + [APPENDED_LABEL]
new_security = extensions.security.model_copy(update={"labels": new_labels})
modified = extensions.model_copy(update={"security": new_security})
return PluginResult(continue_processing=True, modified_extensions=modified)
"#;
fn require_worker(test_name: &str) -> Option<PathBuf> {
if skip_without_python3(test_name) {
return None;
}
let source = match python_source() {
Ok(source) => source,
Err(reason) => {
skip(test_name, &reason);
return None;
},
};
if !worker_delivers_extensions(&source) {
skip(
test_name,
&format!(
"the cpex source at {} has a worker.py that does not deliver the \
`{EXTENSIONS_FIELD}` field to execute_plugin — the inbound channel has no \
consumer there yet",
source.display()
),
);
return None;
}
Some(source)
}
fn setup(dir: &TempDir, source: &Path, test_name: &str) -> Option<PathBuf> {
let plugin_dir = scaffold_plugin(dir, source, "ext_pkg", "extensions_plugin", FIXTURE_PLUGIN);
match prebuild_venv(&plugin_dir, source, FIXTURE_CLASS, "ext_pkg") {
Ok(()) => Some(plugin_dir),
Err(reason) => {
skip(
test_name,
&format!("could not prepare the plugin venv — {reason}"),
);
None
},
}
}
fn inbound_extensions() -> Extensions {
let mut security = SecurityExtension::default();
security.add_label(INBOUND_LABEL);
let mut http = HttpExtension::default();
http.set_request_header("Authorization", "Bearer must-not-travel");
http.set_request_header("Cookie", "session=must-not-travel");
http.set_request_header("X-API-Key", "must-not-travel");
http.set_request_header("X-Request-Id", "req-e2e-1");
Extensions {
security: Some(Arc::new(security)),
http: Some(Arc::new(http)),
..Default::default()
}
}
const HOOK: &str = "cmf.tool_pre_invoke";
fn tool_call_message() -> MessagePayload {
MessagePayload {
message: Message::with_content(
Role::Assistant,
vec![ContentPart::ToolCall {
content: ToolCall {
tool_call_id: "call-ext-1".into(),
name: "search".into(),
arguments: Default::default(),
namespace: None,
},
}],
),
}
}
fn hooks_dispatch() -> ppe::praxis_policy_core::config::EngineSettings {
ppe::praxis_policy_core::config::EngineSettings {
dispatch: ppe::praxis_policy_core::config::DispatchMode::Hooks,
..Default::default()
}
}
fn plugin_config(capabilities: &[&str]) -> PluginConfig {
PluginConfig {
name: "extensions-aware".into(),
kind: KIND.into(),
hooks: vec![HOOK.into()],
capabilities: capabilities.iter().map(|c| (*c).to_string()).collect(),
version: Some("1.0.0".into()),
config: Some(serde_json::json!({
"class_name": FIXTURE_CLASS,
"requirements_file": "requirements.txt",
"timeout_secs": 300,
})),
..Default::default()
}
}
#[test]
fn the_inbound_task_carries_visible_slots_without_sensitive_headers() {
let mut task = serde_json::json!({"task_type": "load_and_run_hook"});
cpex_hosts_python::extensions::attach_extensions(&mut task, &inbound_extensions())
.expect("the filtered view serializes");
let wire = task
.get(EXTENSIONS_FIELD)
.expect("the extensions field is attached")
.as_object()
.expect("it is an object");
let labels = wire["security"]["labels"]
.as_array()
.expect("labels is an array");
assert!(
labels.iter().any(|l| l.as_str() == Some(INBOUND_LABEL)),
"the inbound label must reach the worker: {labels:?}"
);
let headers = wire["http"]["request_headers"]
.as_object()
.expect("request_headers is an object");
assert!(
headers.is_empty(),
"no unvalidated raw header may cross the process boundary: {headers:?}"
);
let serialized = serde_json::to_string(&task).expect("the task serializes");
assert!(
!serialized.contains("must-not-travel"),
"no sensitive header value may appear anywhere in the task JSON"
);
}
#[test]
fn a_plugin_without_read_labels_gets_no_security_slot() {
let filtered = Extensions {
http: Some(Arc::new(HttpExtension::default())),
..Default::default()
};
let mut task = serde_json::json!({});
cpex_hosts_python::extensions::attach_extensions(&mut task, &filtered)
.expect("serializing a filtered view");
let wire = task[EXTENSIONS_FIELD].as_object().expect("an object");
assert!(
!wire.contains_key("security"),
"a slot the plugin's capabilities excluded must stay excluded"
);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires python3 + a cpex Python checkout (CPEX_PYTHON_SOURCE); run via `make test-python-e2e`"]
async fn an_appended_label_survives_the_merge_when_the_capability_is_declared() {
let test = "an_appended_label_survives_the_merge_when_the_capability_is_declared";
let Some(source) = require_worker(test) else {
return;
};
let dir = TempDir::new();
let Some(plugin_dir) = setup(&dir, &source, test) else {
return;
};
let (verdict, merged) = run_pipeline(
&dir,
&plugin_dir,
&["read_labels", "append_labels", "read_headers"],
)
.await;
assert!(
verdict.starts_with("GOT_LABEL"),
"the 3-arg hook must receive the inbound extensions; got: {verdict}"
);
assert!(
!verdict.contains("LEAKED"),
"no sensitive header may reach the plugin: {verdict}"
);
let security = merged
.security
.as_ref()
.expect("security survives the merge");
assert!(
security.has_label(APPENDED_LABEL),
"the plugin's additive label must survive the monotonic merge"
);
assert!(
security.has_label(INBOUND_LABEL),
"the original label must still be there"
);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires python3 + a cpex Python checkout (CPEX_PYTHON_SOURCE); run via `make test-python-e2e`"]
async fn an_appended_label_is_dropped_without_the_capability() {
let test = "an_appended_label_is_dropped_without_the_capability";
let Some(source) = require_worker(test) else {
return;
};
let dir = TempDir::new();
let Some(plugin_dir) = setup(&dir, &source, test) else {
return;
};
let (verdict, merged) = run_pipeline(&dir, &plugin_dir, &["read_labels", "read_headers"]).await;
assert!(
verdict.starts_with("GOT_LABEL"),
"reading is still allowed by read_labels; got: {verdict}"
);
let security = merged.security.as_ref().expect("security slot present");
assert!(
!security.has_label(APPENDED_LABEL),
"a label append without `append_labels` must not land"
);
assert!(
security.has_label(INBOUND_LABEL),
"and the inbound label is untouched"
);
}
async fn run_pipeline(
dir: &TempDir,
plugin_dir: &Path,
capabilities: &[&str],
) -> (String, Extensions) {
use ppe::praxis_policy_core::config::PolicyConfig;
use ppe::praxis_policy_core::engine::{PolicyEngine, PolicyEngineConfig};
let mut config = plugin_config(capabilities);
config.kind = TEMP_DIR_KIND.into();
let manager = PolicyEngine::new(PolicyEngineConfig::default());
manager.register_factory(
TEMP_DIR_KIND,
Box::new(TempDirFactory {
plugin_dir: plugin_dir.to_path_buf(),
worker_cwd: dir.path().to_path_buf(),
}),
);
manager
.load_config(PolicyConfig {
plugins: vec![config],
engine_settings: hooks_dispatch(),
..Default::default()
})
.expect("the config loads");
manager
.initialize()
.await
.expect("the venv is reused and the worker starts");
let inbound = inbound_extensions();
let (result, _bg) = manager
.invoke_by_name(HOOK, Box::new(tool_call_message()), inbound.clone(), None)
.await;
assert!(
result.errors.is_empty(),
"the pipeline recorded plugin errors: {:?}",
result.errors
);
manager.shutdown().await;
let verdict = std::fs::read_to_string(dir.path().join("extensions_verdict.txt"))
.unwrap_or_else(|e| panic!("the plugin recorded no verdict: {e}"));
let merged = result.modified_extensions.unwrap_or(inbound);
(verdict, merged)
}
mod fake_worker {
use std::sync::Arc;
use async_trait::async_trait;
use ppe::praxis_policy_core::context::PluginContext;
use ppe::praxis_policy_core::error::PluginError;
use ppe::praxis_policy_core::executor::ErasedResultFields;
use ppe::praxis_policy_core::extensions::Extensions;
use ppe::praxis_policy_core::factory::{PluginFactory, PluginInstance};
use ppe::praxis_policy_core::hooks::PluginPayload;
use ppe::praxis_policy_core::plugin::{Plugin, PluginConfig};
use ppe::praxis_policy_core::registry::AnyHookHandler;
use serde_json::Value;
pub const KIND: &str = "fake-worker";
pub struct FakeWorkerPlugin {
cfg: PluginConfig,
}
#[async_trait]
impl Plugin for FakeWorkerPlugin {
fn config(&self) -> &PluginConfig {
&self.cfg
}
}
pub struct FakeWorkerHandler {
pub response: Value,
}
#[async_trait]
impl AnyHookHandler for FakeWorkerHandler {
async fn invoke(
&self,
_payload: &dyn PluginPayload,
extensions: &Extensions,
_ctx: &mut PluginContext,
) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<PluginError>> {
let modified_extensions =
cpex_hosts_python::extensions::owned_from_returned_slot(&self.response, extensions)
.expect("the canned response parses");
Ok(Box::new(ErasedResultFields {
continue_processing: true,
modified_payload: None,
modified_extensions,
violation: None,
}))
}
fn hook_type_name(&self) -> &'static str {
ppe::praxis_policy_core::hooks::lookup_hook_metadata(super::HOOK)
.and_then(|meta| meta.family)
.unwrap_or(super::HOOK)
}
}
pub struct FakeWorkerFactory;
impl PluginFactory for FakeWorkerFactory {
fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>> {
let response = config
.config
.as_ref()
.and_then(|c| c.get("response"))
.cloned()
.unwrap_or(Value::Null);
Ok(PluginInstance {
plugin: Arc::new(FakeWorkerPlugin {
cfg: config.clone(),
}),
handlers: vec![(
super::HOOK,
Arc::new(FakeWorkerHandler { response }) as Arc<dyn AnyHookHandler>,
)],
})
}
}
}
async fn merge_through_executor(
capabilities: &[&str],
response: serde_json::Value,
) -> Option<Extensions> {
use ppe::praxis_policy_core::config::PolicyConfig;
use ppe::praxis_policy_core::engine::{PolicyEngine, PolicyEngineConfig};
let config = PluginConfig {
name: "fake-worker".into(),
kind: fake_worker::KIND.into(),
hooks: vec![HOOK.into()],
capabilities: capabilities.iter().map(|c| (*c).to_string()).collect(),
version: Some("1.0.0".into()),
config: Some(serde_json::json!({"response": response})),
..Default::default()
};
let manager = PolicyEngine::new(PolicyEngineConfig::default());
manager.register_factory(fake_worker::KIND, Box::new(fake_worker::FakeWorkerFactory));
manager
.load_config(PolicyConfig {
plugins: vec![config],
engine_settings: hooks_dispatch(),
..Default::default()
})
.expect("the config loads");
manager.initialize().await.expect("the plugin initializes");
let (result, _bg) = manager
.invoke_by_name(
HOOK,
Box::new(tool_call_message()),
inbound_extensions(),
None,
)
.await;
manager.shutdown().await;
result.modified_extensions
}
#[tokio::test(flavor = "multi_thread")]
async fn a_label_append_is_accepted_with_append_labels() {
let merged = merge_through_executor(
&["read_labels", "append_labels"],
serde_json::json!({"security": {"labels": [INBOUND_LABEL, APPENDED_LABEL]}}),
)
.await
.expect("the append is a real modification");
let security = merged.security.as_ref().expect("security survives");
assert!(
security.has_label(APPENDED_LABEL),
"an additive label change must be accepted by the monotonic tier"
);
assert!(
security.has_label(INBOUND_LABEL),
"the pre-existing label must remain"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_label_removal_is_rejected_by_the_monotonic_tier() {
let merged = merge_through_executor(
&["read_labels", "append_labels"],
serde_json::json!({"security": {"labels": []}}),
)
.await;
if let Some(merged) = merged {
let security = merged.security.as_ref().expect("security present");
assert!(
security.has_label(INBOUND_LABEL),
"a label removal must not strip the pipeline's labels"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_custom_change_is_accepted_as_mutable() {
let merged = merge_through_executor(
&["read_labels"],
serde_json::json!({"custom": {"verdict": "clean"}}),
)
.await
.expect("a custom write is a modification");
let custom = merged.custom.as_ref().expect("custom survives the merge");
assert_eq!(
custom.get("verdict").and_then(|v| v.as_str()),
Some("clean"),
"the mutable tier is accepted as-is"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_forged_immutable_slot_does_not_poison_the_merge() {
let merged = merge_through_executor(
&["read_labels"],
serde_json::json!({
"agent": {"agent_id": "forged"},
"custom": {"verdict": "clean"}
}),
)
.await
.expect("the custom write still counts as a modification");
assert_eq!(
merged
.custom
.as_ref()
.expect("custom survives")
.get("verdict")
.and_then(|v| v.as_str()),
Some("clean"),
"the immutable-tier violation must not take the valid write down with it"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_http_write_is_accepted_only_with_write_headers() {
let with_cap = merge_through_executor(
&["read_headers", "write_headers"],
serde_json::json!({"http": {"request_headers": {"X-Added": "1"}}}),
)
.await
.expect("an authorized header write is a modification");
assert_eq!(
with_cap
.http
.as_ref()
.expect("http survives")
.get_request_header("X-Added"),
Some("1"),
"write_headers authorizes the guarded write"
);
let without_cap = merge_through_executor(
&["read_headers"],
serde_json::json!({"http": {"request_headers": {"X-Added": "1"}}}),
)
.await;
let leaked = without_cap
.as_ref()
.and_then(|m| m.http.as_ref())
.and_then(|h| h.get_request_header("X-Added"));
assert!(
leaked.is_none(),
"without write_headers the guarded write must not land"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_returned_sensitive_header_never_reenters_the_pipeline() {
let merged = merge_through_executor(
&["read_headers", "write_headers"],
serde_json::json!({"http": {"request_headers": {
"Authorization": "Bearer injected",
"X-Fine": "yes"
}}}),
)
.await
.expect("the header write is a modification");
let http = merged.http.as_ref().expect("http survives");
assert!(
http.get_request_header("Authorization").is_none(),
"an injected credential header must be stripped on the way back"
);
assert_eq!(
http.get_request_header("X-Fine"),
Some("yes"),
"the benign header still lands"
);
}
#[test]
fn the_return_field_matches_the_python_plugin_result_model() {
assert_eq!(MODIFIED_EXTENSIONS_FIELD, "modified_extensions");
assert_eq!(EXTENSIONS_FIELD, "extensions");
}