Skip to content

Commit c9f2b51

Browse files
teryltTeryl Taylor
authored andcommitted
feat: initial Rust Core (cpex-core and cpex-sdk) (#13)
* feat: initial revision rust core. Signed-off-by: Teryl Taylor <terylt@ibm.com> * fix: addressed comments in PR. Updated PluginContext to match spec. Signed-off-by: Teryl Taylor <terylt@ibm.com> --------- Signed-off-by: Teryl Taylor <terylt@ibm.com> Co-authored-by: Teryl Taylor <terylt@ibm.com>
1 parent c215d1f commit c9f2b51

20 files changed

Lines changed: 5175 additions & 0 deletions

Cargo.lock

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

Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Location: ./Cargo.toml
2+
# Copyright 2025
3+
# SPDX-License-Identifier: Apache-2.0
4+
# Authors: Teryl Taylor
5+
#
6+
# Workspace root for the CPEX Rust crates.
7+
8+
[workspace]
9+
resolver = "2"
10+
members = [
11+
"crates/cpex-core",
12+
"crates/cpex-sdk",
13+
]
14+
15+
[workspace.package]
16+
version = "0.1.0"
17+
edition = "2021"
18+
license = "Apache-2.0"
19+
authors = ["Teryl Taylor"]
20+
21+
[workspace.dependencies]
22+
tokio = { version = "1", features = ["full"] }
23+
serde = { version = "1", features = ["derive"] }
24+
serde_yaml = "0.9"
25+
serde_json = "1"
26+
async-trait = "0.1"
27+
thiserror = "2"
28+
tracing = "0.1"
29+
uuid = { version = "1", features = ["v4"] }
30+
paste = "1"
31+
futures = "0.3"

crates/README.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# CPEX Rust Core
2+
3+
Phase 1a of the CPEX Rust plugin runtime. Provides the core types, 5-phase executor, and plugin manager for the ContextForge Plugin Extensibility Framework.
4+
5+
## Status
6+
7+
Phase 1a — core runtime functional, no language bindings yet.
8+
9+
- `cpex-core`: Plugin trait, typed hooks, 5-phase executor, plugin manager
10+
- `cpex-sdk`: Lean re-exports for plugin authors
11+
12+
## Prerequisites
13+
14+
### Install Rust
15+
16+
If you don't have Rust installed:
17+
18+
```bash
19+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
20+
```
21+
22+
Follow the prompts, then restart your shell or run:
23+
24+
```bash
25+
source $HOME/.cargo/env
26+
```
27+
28+
Verify the installation:
29+
30+
```bash
31+
rustc --version # should be 1.75+ (we develop on 1.94)
32+
cargo --version
33+
```
34+
35+
### Update an existing installation
36+
37+
```bash
38+
rustup update stable
39+
```
40+
41+
### Build and test
42+
43+
From the repository root:
44+
45+
```bash
46+
# Check that everything compiles
47+
cargo check -p cpex-core -p cpex-sdk
48+
49+
# Run all tests
50+
cargo test -p cpex-core -p cpex-sdk
51+
```
52+
53+
## What It Does
54+
55+
A typed, 5-phase plugin execution framework where:
56+
57+
- **Hooks have typed payloads** — no JSON parsing for native Rust plugins
58+
- **Extensions are separate from payloads** — capability-filtered per plugin, modified independently
59+
- **The framework never clones payloads** — handlers receive borrows, clone only when modifying
60+
- **Plugin configs are trusted**`PluginRef` holds config from the loader, not from the plugin
61+
- **Two invoke paths**`invoke::<H>()` (typed, Rust) and `invoke_by_name()` (dynamic, Python/Go)
62+
63+
## Quick Example
64+
65+
```rust
66+
use std::sync::Arc;
67+
use async_trait::async_trait;
68+
use cpex_core::context::{GlobalContext, PluginContext};
69+
use cpex_core::error::{PluginError, PluginViolation};
70+
use cpex_core::hooks::payload::{Extensions, FilteredExtensions};
71+
use cpex_core::hooks::trait_def::{HookHandler, HookTypeDef, PluginResult};
72+
use cpex_core::manager::PluginManager;
73+
use cpex_core::plugin::{Plugin, PluginConfig, PluginMode, OnError};
74+
75+
// 1. Define a payload
76+
#[derive(Debug, Clone)]
77+
struct ToolCallPayload {
78+
tool_name: String,
79+
include_ssn: bool,
80+
}
81+
cpex_core::impl_plugin_payload!(ToolCallPayload);
82+
83+
// 2. Define a hook type
84+
struct ToolPreInvoke;
85+
impl HookTypeDef for ToolPreInvoke {
86+
type Payload = ToolCallPayload;
87+
type Result = PluginResult<ToolCallPayload>;
88+
const NAME: &'static str = "tool_pre_invoke";
89+
}
90+
91+
// 3. Write a plugin
92+
struct SsnGuard { config: PluginConfig }
93+
94+
#[async_trait]
95+
impl Plugin for SsnGuard {
96+
fn config(&self) -> &PluginConfig { &self.config }
97+
async fn initialize(&self) -> Result<(), PluginError> { Ok(()) }
98+
async fn shutdown(&self) -> Result<(), PluginError> { Ok(()) }
99+
}
100+
101+
impl HookHandler<ToolPreInvoke> for SsnGuard {
102+
fn handle(
103+
&self,
104+
payload: &ToolCallPayload, // borrow — zero cost
105+
_extensions: &FilteredExtensions,
106+
_ctx: &PluginContext,
107+
) -> PluginResult<ToolCallPayload> {
108+
if payload.include_ssn {
109+
PluginResult::deny(PluginViolation::new("ssn_denied", "Requires permission"))
110+
} else {
111+
PluginResult::allow()
112+
}
113+
}
114+
}
115+
116+
// 4. Register and invoke
117+
async fn run() {
118+
let mut manager = PluginManager::default();
119+
120+
let config = PluginConfig {
121+
name: "ssn-guard".into(),
122+
kind: "builtin".into(),
123+
hooks: vec!["tool_pre_invoke".into()],
124+
mode: PluginMode::Sequential,
125+
priority: 10,
126+
on_error: OnError::Fail,
127+
..Default::default()
128+
};
129+
130+
let plugin = Arc::new(SsnGuard { config: config.clone() });
131+
manager.register_handler::<ToolPreInvoke, _>(plugin, config).unwrap();
132+
manager.initialize().await.unwrap();
133+
134+
let payload = ToolCallPayload {
135+
tool_name: "get_compensation".into(),
136+
include_ssn: true,
137+
};
138+
139+
let result = manager
140+
.invoke::<ToolPreInvoke>(payload, Extensions::default(), &GlobalContext::new("req-1"))
141+
.await;
142+
143+
assert!(!result.allowed); // denied — SSN access blocked
144+
}
145+
```
146+
147+
## Crate Structure
148+
149+
```
150+
crates/
151+
├── cpex-core/src/
152+
│ ├── lib.rs — module declarations
153+
│ ├── plugin.rs — Plugin trait (lifecycle), PluginConfig, PluginMode, OnError, PluginCondition
154+
│ ├── error.rs — PluginError, PluginViolation
155+
│ ├── context.rs — GlobalContext, PluginContext
156+
│ ├── hooks/
157+
│ │ ├── payload.rs — PluginPayload trait (object-safe), Extensions, FilteredExtensions
158+
│ │ ├── trait_def.rs — HookTypeDef, HookHandler<H>, PluginResult
159+
│ │ ├── adapter.rs — TypedHandlerAdapter (bridges typed handlers to type-erased dispatch)
160+
│ │ ├── macros.rs — define_hook! macro
161+
│ │ └── types.rs — HookType (string wrapper), hook_names, cmf_hook_names
162+
│ ├── registry.rs — PluginRef (trusted config), PluginRegistry, AnyHookHandler, HookEntry
163+
│ ├── executor.rs — 5-phase engine, PipelineResult, ErasedResultFields
164+
│ ├── manager.rs — PluginManager (register_handler, invoke, invoke_by_name, lifecycle)
165+
│ └── config.rs — (stub — unified YAML parsing, Phase 2)
166+
└── cpex-sdk/src/
167+
└── lib.rs — lean re-exports for plugin authors
168+
```
169+
170+
## 5-Phase Execution Model
171+
172+
```
173+
SEQUENTIAL → TRANSFORM → AUDIT → CONCURRENT → FIRE_AND_FORGET
174+
```
175+
176+
| Phase | Can Block? | Can Modify? | Execution |
177+
|-------|------------|-------------|-----------|
178+
| Sequential | Yes | Yes (clone) | Serial, chained |
179+
| Transform | No | Yes (clone) | Serial, chained |
180+
| Audit | No | No | Serial |
181+
| Concurrent | Yes | No | Parallel |
182+
| FireAndForget | No | No | Background |
183+
184+
All handlers receive `&Payload` (borrow). The framework holds ownership. Modified payloads are returned in `PluginResult::modified_payload` and replace the current payload in the pipeline.
185+
186+
## Key Design Decisions
187+
188+
- **PluginRef trust model** — configs come from the config loader, not from `plugin.config()`. Prevents plugins from tampering with their own priority, mode, or capabilities.
189+
- **Borrow-based handlers** — handlers receive `&Payload`, not owned. Framework never clones. Plugins clone only when modifying. Enforced by Rust's borrow checker at compile time.
190+
- **Single `invoke()` path** — one method on `AnyHookHandler`, not separate `invoke_owned`/`invoke_ref`. Simpler API, same behavior.
191+
- **`PluginPayload` trait** — object-safe base for all payloads. `Box<dyn PluginPayload>` instead of `Box<dyn Any>` — type errors caught at compile time.
192+
- **Extensions separate from payload** — capability-filtered per plugin, modified independently. Extension-only changes don't clone the payload.
193+
194+
## Tests
195+
196+
```bash
197+
cargo test -p cpex-core -p cpex-sdk
198+
```
199+
200+
27 unit tests + 6 doc tests covering: registration, priority ordering, trusted config tamper protection, 5-phase execution, allow/deny/modify results, lifecycle management, typed and dynamic invoke paths.
201+
202+
## What's Next
203+
204+
- **Phase 1b**: `cpex-ffi` + Go bindings (first language binding)
205+
- **Phase 1c**: Conformance test corpus (YAML scenarios, Python + Rust)
206+
- **Phase 2**: Unified YAML config parsing
207+
- **Phase 3**: Full CMF extension types (MonotonicSet, Guarded<T>, MetaExtension, etc.)
208+
209+
See [CPEX Rust Core Proposal](../docs/cpex-rust-core-proposal.md) for the full roadmap.

crates/cpex-core/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Location: ./crates/cpex-core/Cargo.toml
2+
# Copyright 2025
3+
# SPDX-License-Identifier: Apache-2.0
4+
# Authors: Teryl Taylor
5+
#
6+
# CPEX Core — pure Rust plugin runtime with no FFI dependencies.
7+
# Contains the PluginManager, 5-phase executor, hook registry,
8+
# config parser, and all core types.
9+
10+
[package]
11+
name = "cpex-core"
12+
description = "CPEX plugin runtime core — PluginManager, executor, hooks, and config."
13+
version.workspace = true
14+
edition.workspace = true
15+
license.workspace = true
16+
authors.workspace = true
17+
18+
[dependencies]
19+
tokio = { workspace = true }
20+
serde = { workspace = true }
21+
serde_yaml = { workspace = true }
22+
serde_json = { workspace = true }
23+
async-trait = { workspace = true }
24+
thiserror = { workspace = true }
25+
tracing = { workspace = true }
26+
uuid = { workspace = true }
27+
futures = { workspace = true }

crates/cpex-core/src/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Location: ./crates/cpex-core/src/config.rs
2+
// Copyright 2025
3+
// SPDX-License-Identifier: Apache-2.0
4+
// Authors: Teryl Taylor
5+
//
6+
// Unified YAML configuration parsing.
7+
//
8+
// Parses the unified config format that combines global settings,
9+
// plugin declarations, named policy groups, and per-entity routes
10+
// into a single YAML document.
11+
//
12+
// Mirrors the unified config proposal in
13+
// apl-plugins/docs/unified-config-proposal.md.
14+
15+
// TODO: Implement CpexConfig, GlobalConfig, RouteEntry serde models

0 commit comments

Comments
 (0)