Skip to main content

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}