cortex_sdk/lib.rs
1//! # Cortex SDK
2//!
3//! The official Rust SDK for Cortex's trusted native plugin boundary.
4//!
5//! This crate defines the public plugin surface with **zero dependency on
6//! Cortex internals**. The runtime loads trusted native plugins through a
7//! stable C-compatible ABI and bridges these traits to its own turn runtime,
8//! command surface, and transport layer.
9//!
10//! Process-isolated JSON plugins do **not** need this crate. They are defined
11//! through `manifest.toml` plus a child-process command. Use `cortex-sdk` when
12//! you are building a trusted in-process native plugin that exports
13//! `cortex_plugin_init`.
14//!
15//! SDK release cadence is independent from Cortex runtime releases. Choose the
16//! SDK version by the native ABI/DTO surface your plugin needs, not by the
17//! latest Cortex runtime patch. Runtime compatibility is declared in the
18//! plugin `manifest.toml` `cortex_version` field, which is the minimum Cortex
19//! runtime version the plugin supports.
20//!
21//! ## Architecture
22//!
23//! ```text
24//! ┌──────────────┐ dlopen ┌──────────────────┐
25//! │ cortex-runtime│ ──────────────▶ │ your plugin.so │
26//! │ (daemon) │ │ cortex-sdk only │
27//! └──────┬───────┘ FFI call └────────┬─────────┘
28//! │ cortex_plugin_init() │
29//! ▼ ▼
30//! ToolRegistry ◀─── register ─── MultiToolPlugin
31//! ├─ plugin_info()
32//! └─ create_tools()
33//! ├─ Tool A
34//! └─ Tool B
35//! ```
36//!
37//! Plugins are compiled as `cdylib` shared libraries. The runtime calls
38//! `cortex_plugin_init`, receives a C-compatible function table, then asks that
39//! table for plugin metadata, tool descriptors, and tool execution results.
40//! Rust trait objects stay inside the plugin; they never cross the
41//! dynamic-library boundary.
42//!
43//! The SDK exposes a runtime-aware execution surface:
44//!
45//! - [`InvocationContext`] gives tools stable metadata such as session id,
46//! canonical actor, transport/source, and foreground/background scope
47//! - [`ToolRuntime`] lets tools emit progress updates and observer text back
48//! to the parent turn
49//! - [`ToolCapabilities`] lets tools declare whether they emit runtime signals
50//! and whether they are background-safe
51//! - [`Attachment`] and [`ToolResult::with_media`] let tools return structured
52//! image, audio, video, or file outputs without depending on Cortex internals
53//! ## Native Plugin Quick Start
54//!
55//! **Cargo.toml:**
56//!
57//! ```toml
58//! [package]
59//! name = "cortex-plugin-native-hello"
60//! version = "0.1.0"
61//! edition = "2024"
62//! publish = false
63//!
64//! [lib]
65//! crate-type = ["cdylib", "rlib"]
66//!
67//! [dependencies]
68//! cortex-sdk = "1.6.11"
69//! serde_json = "1"
70//! ```
71//!
72//! **src/lib.rs:**
73//!
74//! ```text
75//! use cortex_sdk::prelude::*;
76//!
77//! #[derive(Default)]
78//! struct MyPlugin;
79//!
80//! impl MultiToolPlugin for MyPlugin {
81//! fn plugin_info(&self) -> PluginInfo {
82//! PluginInfo {
83//! name: "my-plugin".into(),
84//! version: env!("CARGO_PKG_VERSION").into(),
85//! description: "My custom tools for Cortex".into(),
86//! }
87//! }
88//!
89//! fn create_tools(&self) -> Vec<Box<dyn Tool>> {
90//! vec![Box::new(WordCountTool)]
91//! }
92//! }
93//!
94//! struct WordCountTool;
95//!
96//! impl Tool for WordCountTool {
97//! fn name(&self) -> &'static str { "word_count" }
98//!
99//! fn description(&self) -> &'static str {
100//! "Count words in a text string. Use when the user asks for word \
101//! counts, statistics, or text length metrics."
102//! }
103//!
104//! fn input_schema(&self) -> serde_json::Value {
105//! serde_json::json!({
106//! "type": "object",
107//! "properties": {
108//! "text": {
109//! "type": "string",
110//! "description": "The text to count words in"
111//! }
112//! },
113//! "required": ["text"]
114//! })
115//! }
116//!
117//! fn execute(&self, input: serde_json::Value) -> Result<ToolResult, ToolError> {
118//! let text = input["text"]
119//! .as_str()
120//! .ok_or_else(|| ToolError::InvalidInput("missing 'text' field".into()))?;
121//! let count = text.split_whitespace().count();
122//! Ok(ToolResult::success(format!("{count} words")))
123//! }
124//! }
125//!
126//! cortex_sdk::export_plugin!(MyPlugin);
127//! ```
128//!
129//! Tools that need runtime context can override
130//! [`Tool::execute_with_runtime`] instead of only [`Tool::execute`].
131//!
132//! **manifest.toml:**
133//!
134//! ```toml
135//! name = "native-hello"
136//! version = "0.1.0"
137//! description = "Example trusted native Cortex plugin"
138//! cortex_version = "1.6.11"
139//! trust = "trusted_native"
140//!
141//! [capabilities]
142//! provides = ["tools"]
143//! secrets = false
144//!
145//! [sandbox]
146//! level = "trusted_in_process"
147//!
148//! [native]
149//! library = "lib/libcortex_plugin_native_hello.so"
150//! isolation = "trusted_in_process"
151//! abi_version = 1
152//! ```
153//!
154//! ## Build, Sign, Pack, Publish
155//!
156//! ```bash
157//! cargo build --release
158//! cortex plugin review .
159//! cortex plugin test .
160//! cortex plugin keygen ~/.config/cortex/plugin-signing/example-dev.ed25519
161//! cortex plugin sign . --key ~/.config/cortex/plugin-signing/example-dev.ed25519 --publisher example.dev
162//! cortex plugin pack .
163//! sha256sum cortex-plugin-native-hello-v0.1.0-linux-amd64.cpx > cortex-plugin-native-hello-v0.1.0-linux-amd64.cpx.sha256
164//! ```
165//!
166//! Upload the `.cpx` and `.sha256` files to a GitHub Release. Users install the
167//! package by repository name and restart the daemon so the native library is
168//! loaded:
169//!
170//! ```bash
171//! cortex plugin install owner/cortex-plugin-native-hello@0.1.0
172//! cortex restart
173//! ```
174//!
175//! Installing or replacing a trusted native shared library still requires a
176//! daemon restart so the new code is loaded. Process-isolated plugin manifest
177//! changes hot-apply without that restart.
178//!
179//! ## Plugin Lifecycle
180//!
181//! 1. **Load** — `dlopen` at daemon startup
182//! 2. **Create** — runtime calls `export_plugin!`-generated stable ABI init
183//! 3. **Register** — [`MultiToolPlugin::create_tools`] is called once; each
184//! [`Tool`] is registered in the global tool registry
185//! 4. **Execute** — the LLM invokes tools by name during turns; the runtime
186//! calls [`Tool::execute`] with JSON parameters
187//! 5. **Retain** — the library handle is held for the daemon's lifetime;
188//! `Drop` runs only at shutdown
189//!
190//! ## Tool Design Guidelines
191//!
192//! - **`name`**: lowercase with underscores (`word_count`, not `WordCount`).
193//! Must be unique across all tools in the registry.
194//! - **`description`**: written for the LLM — explain what the tool does,
195//! when to use it, and when *not* to use it. The LLM reads this to decide
196//! whether to call the tool.
197//! - **`input_schema`**: a [JSON Schema](https://json-schema.org/) object
198//! describing the parameters. The LLM generates JSON matching this schema.
199//! - **`execute`**: receives the LLM-generated JSON. Return
200//! [`ToolResult::success`] for normal output or [`ToolResult::error`] for
201//! recoverable errors the LLM should see. Return [`ToolError`] only for
202//! unrecoverable failures (invalid input, missing deps).
203//! - **Media output**: attach files with [`ToolResult::with_media`]. Cortex
204//! delivers attachments through the active transport; plugins should not call
205//! channel-specific APIs directly.
206//! - **`execute_with_runtime`**: use this when the tool needs invocation
207//! metadata or wants to emit progress / observer updates during execution.
208//! - **`timeout_secs`**: optional per-tool timeout override. If `None`, the
209//! global `[turn].tool_timeout_secs` applies.
210
211use serde::{Deserialize, Serialize};
212pub use serde_json;
213
214mod effects;
215mod native;
216pub mod prelude;
217mod tool;
218
219pub use effects::{
220 DryRunSupport, EffectConfirmation, EffectReversibility, ToolCapabilities, ToolEffect,
221 ToolEffectKind, ToolRuntime,
222};
223pub use native::{
224 CortexBuffer, CortexHostApi, CortexPluginApi, NativePluginState, cortex_buffer_free,
225 native_plugin_drop, native_plugin_info, native_tool_count, native_tool_descriptor,
226 native_tool_execute,
227};
228pub use tool::{MultiToolPlugin, PluginInfo, Tool, ToolError, ToolResult};
229
230/// Version of the SDK crate used by native plugin builds.
231pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
232
233/// Stable native ABI version for trusted in-process plugins.
234///
235/// The runtime never exchanges Rust trait objects across the dynamic-library
236/// boundary. It loads a C-compatible function table through `cortex_plugin_init`
237/// and moves structured values as UTF-8 JSON buffers.
238pub const NATIVE_ABI_VERSION: u32 = 1;
239
240/// Stable multimedia attachment DTO exposed to plugins.
241///
242/// This type intentionally lives in `cortex-sdk` instead of depending on
243/// Cortex internal crates, so plugin authors only need the SDK.
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct Attachment {
246 /// High-level type: `"image"`, `"audio"`, `"video"`, `"file"`.
247 pub media_type: String,
248 /// MIME type, for example `"image/png"` or `"audio/mpeg"`.
249 pub mime_type: String,
250 /// Local file path or remote URL readable by the runtime transport.
251 pub url: String,
252 /// Optional caption or description.
253 pub caption: Option<String>,
254 /// File size in bytes, if known.
255 pub size: Option<u64>,
256}
257
258/// Whether a tool invocation belongs to a user-visible foreground turn or a
259/// background maintenance execution.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
261#[serde(rename_all = "snake_case")]
262pub enum ExecutionScope {
263 #[default]
264 Foreground,
265 Background,
266}
267
268/// Stable runtime metadata exposed to plugin tools during execution.
269///
270/// This intentionally exposes the execution surface, not Cortex internals.
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct InvocationContext {
273 /// Tool name being invoked.
274 pub tool_name: String,
275 /// Active session id when available.
276 pub session_id: Option<String>,
277 /// Canonical actor identity when available.
278 pub actor: Option<String>,
279 /// Transport or invocation source (`http`, `rpc`, `telegram`, `heartbeat`, ...).
280 pub source: Option<String>,
281 /// Whether this invocation belongs to a foreground or background execution.
282 pub execution_scope: ExecutionScope,
283}
284
285impl InvocationContext {
286 #[must_use]
287 pub fn is_background(&self) -> bool {
288 self.execution_scope == ExecutionScope::Background
289 }
290
291 #[must_use]
292 pub fn is_foreground(&self) -> bool {
293 self.execution_scope == ExecutionScope::Foreground
294 }
295}
296
297// ── Export Macro ────────────────────────────────────────────
298
299/// Generate the stable native ABI entry point for a [`MultiToolPlugin`].
300///
301/// This macro expands to an `extern "C"` function named `cortex_plugin_init`
302/// that fills a C-compatible function table. The plugin type must implement
303/// [`Default`].
304///
305/// # Usage
306///
307/// `cortex_sdk::export_plugin!(MyPlugin);`
308///
309/// # Expansion
310///
311/// The macro constructs the Rust plugin internally and exposes it through the
312/// stable native ABI table. Rust trait objects never cross the dynamic-library
313/// boundary.
314#[macro_export]
315macro_rules! export_plugin {
316 ($plugin_type:ty) => {
317 #[unsafe(no_mangle)]
318 pub unsafe extern "C" fn cortex_plugin_init(
319 host: *const $crate::CortexHostApi,
320 out_plugin: *mut $crate::CortexPluginApi,
321 ) -> i32 {
322 if host.is_null() || out_plugin.is_null() {
323 return -1;
324 }
325 let host = unsafe { &*host };
326 if host.abi_version != $crate::NATIVE_ABI_VERSION {
327 return -2;
328 }
329 let plugin: Box<dyn $crate::MultiToolPlugin> = Box::new(<$plugin_type>::default());
330 let state = Box::new($crate::NativePluginState::new(plugin));
331 unsafe {
332 *out_plugin = $crate::CortexPluginApi {
333 abi_version: $crate::NATIVE_ABI_VERSION,
334 plugin: Box::into_raw(state).cast(),
335 plugin_info: Some($crate::native_plugin_info),
336 tool_count: Some($crate::native_tool_count),
337 tool_descriptor: Some($crate::native_tool_descriptor),
338 tool_execute: Some($crate::native_tool_execute),
339 plugin_drop: Some($crate::native_plugin_drop),
340 buffer_free: Some($crate::cortex_buffer_free),
341 };
342 }
343 0
344 }
345 };
346}