Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,326 changes: 2,375 additions & 951 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"crates/cpex-sdk",
"crates/cpex-builtins",
"crates/cpex-ffi",
"crates/cpex-wasm-host",
"crates/apl-core",
"crates/apl-cmf",
"crates/apl-cpex",
Expand All @@ -27,6 +28,9 @@ members = [
"builtins/session/valkey",
"examples/go-demo/ffi",
]
exclude = [
"crates/cpex-wasm-plugin",
]

# `default-members` controls what `cargo build` / `cargo test` (with no
# `-p` or `--workspace` flag) picks up. `cpex-session-valkey` pulls a redis
Expand Down
21 changes: 11 additions & 10 deletions crates/cpex-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ keywords.workspace = true
categories.workspace = true
rust-version.workspace = true

[features]
default = ["runtime"]
runtime = ["dep:tokio", "dep:tokio-util", "dep:cpex-orchestration", "dep:arc-swap"]

[dependencies]
tokio = { workspace = true }
tokio-util = { workspace = true }
# --- Always-on dependencies (WASM-compatible) ---
serde = { workspace = true }
serde_yaml = { workspace = true }
serde_json = { workspace = true }
Expand All @@ -31,17 +34,15 @@ thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
hashbrown = { workspace = true }
arc-swap = { workspace = true }
wildmatch = { workspace = true }
chrono = { workspace = true }
# Zeroizing wrapper for raw credential material in RawCredentialsExtension.
# `derive` feature pulls the proc-macro so we can `#[derive(Zeroize)]` on
# token-bearing structs in a future slice; for now only the
# `Zeroizing<String>` wrapper is used directly.
zeroize = { version = "1.8", features = ["zeroize_derive"] }
# Shared concurrency primitive used by `executor::run_concurrent_phase`
# (and apl-core's `Effect::Parallel`). Leaf crate, no cycles back here.
cpex-orchestration = { workspace = true }

# --- Runtime-only dependencies (require tokio, not WASM-compatible) ---
tokio = { workspace = true, optional = true }
tokio-util = { workspace = true, optional = true }
arc-swap = { workspace = true, optional = true }
cpex-orchestration = { path = "../cpex-orchestration", optional = true }

[lints]
workspace = true
1 change: 1 addition & 0 deletions crates/cpex-core/src/cmf/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ pub struct MessagePayload {
}

crate::impl_plugin_payload!(MessagePayload);
crate::impl_wasm_payload!(MessagePayload, "cmf.message");

// ---------------------------------------------------------------------------
// CmfHook — Hook Type Definition
Expand Down
2 changes: 2 additions & 0 deletions crates/cpex-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// enabled, conditions on individual plugins are ignored.

use std::collections::{HashMap, HashSet};
#[cfg(feature = "runtime")]
use std::path::Path;

use serde::{Deserialize, Deserializer, Serialize, Serializer};
Expand Down Expand Up @@ -595,6 +596,7 @@ impl StringOrList {
// Config Loading
// ---------------------------------------------------------------------------

#[cfg(feature = "runtime")]
/// Load and parse a CPEX config from a YAML file.
pub fn load_config(path: &Path) -> Result<CpexConfig, Box<PluginError>> {
let content = std::fs::read_to_string(path).map_err(|e| PluginError::Config {
Expand Down
2 changes: 2 additions & 0 deletions crates/cpex-core/src/delegation/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;

#[cfg(feature = "runtime")]
use crate::executor::PipelineResult;
use crate::extensions::raw_credentials::DelegationMode;
use crate::extensions::{
Expand Down Expand Up @@ -358,6 +359,7 @@ impl DelegationPayload {

// -------- Host-side application helpers --------

#[cfg(feature = "runtime")]
/// Pull the resolved `DelegationPayload` out of a `PipelineResult`
/// returned by `mgr.invoke_named::<TokenDelegateHook>(...)`.
/// Returns `None` when the pipeline was denied or when the result's
Expand Down
7 changes: 5 additions & 2 deletions crates/cpex-core/src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@
//
// Hook types are open — hosts define their own using define_hook! alongside the built-ins.

pub mod adapter;
pub mod macros;
pub mod metadata;
pub mod payload;
pub mod trait_def;
pub mod types;

#[cfg(feature = "runtime")]
pub mod adapter;

// Re-export core types at the hooks level
#[cfg(feature = "runtime")]
pub use adapter::TypedHandlerAdapter;
pub use metadata::{
lookup as lookup_hook_metadata, register_hook_metadata, HookMetadata, HookPhase,
};
pub use payload::{Extensions, PluginPayload};
pub use payload::{Extensions, PluginPayload, WasmSerializablePayload};
pub use trait_def::{HookHandler, HookTypeDef, PluginResult};
pub use types::{builtin_hook_types, hook_type_from_str, HookType};
78 changes: 78 additions & 0 deletions crates/cpex-core/src/hooks/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,81 @@ macro_rules! impl_plugin_payload {
}
};
}

// ---------------------------------------------------------------------------
// WasmSerializablePayload — opt-in WASM transport trait
// ---------------------------------------------------------------------------

/// Opt-in trait for payload types that can cross the WASM serialization boundary.
///
/// Implement this (via [`impl_wasm_payload!`]) for any payload type that WASM
/// plugins should be able to receive or return. The type discriminator string
/// is embedded in the WIT `generic-payload` record so the host and guest can
/// agree on which concrete type to deserialize.
///
/// # Example
///
/// ```
/// use serde::{Deserialize, Serialize};
/// use cpex_core::{impl_plugin_payload, impl_wasm_payload};
///
/// #[derive(Debug, Clone, Serialize, Deserialize)]
/// struct ToolInvokePayload {
/// tool_name: String,
/// user: String,
/// }
///
/// impl_plugin_payload!(ToolInvokePayload);
/// impl_wasm_payload!(ToolInvokePayload, "cpex.tool_invoke");
/// ```
pub trait WasmSerializablePayload: PluginPayload {
/// Type discriminator used in the WIT `generic-payload` record.
///
/// Must be unique across all payload types registered with the host.
/// Convention: `"<namespace>.<type>"` (e.g. `"cmf.message"`, `"cpex.tool_invoke"`).
fn payload_type_name() -> &'static str
where
Self: Sized;

/// Serialize this payload to JSON bytes for WASM transport.
fn to_wasm_bytes(&self) -> Result<Vec<u8>, serde_json::Error>;

/// Deserialize a payload from JSON bytes received from WASM.
fn from_wasm_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error>
where
Self: Sized;
}

/// Implements [`WasmSerializablePayload`] for a type that is `Serialize + Deserialize`.
///
/// The type must already implement [`PluginPayload`] (via [`impl_plugin_payload!`]).
///
/// ```
/// use serde::{Deserialize, Serialize};
/// use cpex_core::{impl_plugin_payload, impl_wasm_payload};
///
/// #[derive(Debug, Clone, Serialize, Deserialize)]
/// struct ToolInvokePayload {
/// tool_name: String,
/// user: String,
/// }
///
/// impl_plugin_payload!(ToolInvokePayload);
/// impl_wasm_payload!(ToolInvokePayload, "cpex.tool_invoke");
/// ```
#[macro_export]
macro_rules! impl_wasm_payload {
($ty:ty, $name:literal) => {
impl $crate::hooks::payload::WasmSerializablePayload for $ty {
fn payload_type_name() -> &'static str {
$name
}
fn to_wasm_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec(self)
}
fn from_wasm_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
}
};
}
2 changes: 2 additions & 0 deletions crates/cpex-core/src/identity/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;

#[cfg(feature = "runtime")]
use crate::executor::PipelineResult;
use crate::extensions::{
ClientExtension, DelegationExtension, Extensions, RawCredentialsExtension, SecurityExtension,
Expand Down Expand Up @@ -285,6 +286,7 @@ impl IdentityPayload {

// -------- Host-side application helpers --------

#[cfg(feature = "runtime")]
/// Pull the resolved `IdentityPayload` out of a `PipelineResult`
/// returned by `mgr.invoke_named::<IdentityHook>(...)`. Returns
/// `None` when the pipeline was denied (no `modified_payload`)
Expand Down
14 changes: 11 additions & 3 deletions crates/cpex-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,20 @@ pub mod config;
pub mod context;
pub mod delegation;
pub mod error;
pub mod executor;
pub mod extensions;
pub mod factory;
pub mod hooks;
pub mod identity;
pub mod manager;
pub mod plugin;

// Runtime-only modules — require tokio, task spawning, orchestration.
// Excluded when building for WASM targets (use `default-features = false`).
#[cfg(feature = "runtime")]
pub mod executor;
#[cfg(feature = "runtime")]
pub mod factory;
#[cfg(feature = "runtime")]
pub mod manager;
#[cfg(feature = "runtime")]
pub mod registry;
#[cfg(feature = "runtime")]
pub mod visitor;
18 changes: 18 additions & 0 deletions crates/cpex-wasm-host/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "cpex-wasm-host"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["sync", "macros", "io-util", "rt", "rt-multi-thread", "time", "signal", "net"] }
wasmtime = { version = "45.0", features = ["component-model", "async"] }
wasmtime-wasi = "45.0"
wasmtime-wasi-http = { version = "45.0", features = ["default-send-request"] }
hyper = "1"
anyhow = "1.0"
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
serde_json = { workspace = true }
cpex-core = { path = "../cpex-core" }
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
17 changes: 17 additions & 0 deletions crates/cpex-wasm-host/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.PHONY: build build-plugin run clean

PLUGIN_DIR = ../cpex-wasm-plugin
WASM_DIR = wasm

build-plugin:
$(MAKE) -C $(PLUGIN_DIR) build

build: build-plugin
cargo build --release

run: build
cargo run --release

clean:
cargo clean
rm -f $(WASM_DIR)/plugin.wasm
Loading
Loading