Skip to main content

neomind_extension_sdk/
lib.rs

1//! NeoMind Extension SDK
2//!
3//! A unified SDK for developing NeoMind extensions that can be compiled
4//! for both Native and WASM targets.
5//!
6//! # Features
7//!
8//! - Unified trait system for Native and WASM
9//! - Automatic FFI export generation
10//! - Helper macros for common patterns
11//! - Type-safe metric and command definitions
12//! - Single-source IPC boundary types
13//!
14//! # Architecture (V2 - Process Isolation)
15//!
16//! All extensions run in isolated processes by default:
17//!
18//! ```text
19//! ┌─────────────────────────────────────────────────────────────┐
20//! │                   NeoMind Main Process                       │
21//! │  - UnifiedExtensionService manages all extensions           │
22//! │  - IPC communication via stdin/stdout                       │
23//! └─────────────────────────────────────────────────────────────┘
24//!                               │
25//!                               ▼
26//! ┌─────────────────────────────────────────────────────────────┐
27//! │                  Extension Runner Process                    │
28//! │  - Your extension runs here in isolation                    │
29//! │  - Native: loaded via FFI                                   │
30//! │  - WASM: executed via wasmtime                              │
31//! │  - Crashes don't affect main process                        │
32//! └─────────────────────────────────────────────────────────────┘
33//! ```
34//!
35//! # ABI Stability
36//!
37//! The IPC boundary types in `ipc_types` are the stable protocol between
38//! extensions and the main process. Extensions compiled against older SDK
39//! versions will continue to work because:
40//!
41//! 1. Types are serialized as JSON over IPC
42//! 2. Only the JSON format matters, not the implementation
43//! 3. New fields use `#[serde(default)]` for forward compatibility
44//!
45//! # Quick Start
46//!
47//! ```rust,ignore
48//! use neomind_extension_sdk::prelude::*;
49//!
50//! // Define your extension struct
51//! pub struct MyExtension {
52//!     counter: std::sync::atomic::AtomicI64,
53//! }
54//!
55//! impl MyExtension {
56//!     pub fn new() -> Self {
57//!         Self {
58//!             counter: std::sync::atomic::AtomicI64::new(0),
59//!         }
60//!     }
61//! }
62//!
63//! // Implement the Extension trait
64//! #[async_trait]
65//! impl Extension for MyExtension {
66//!     fn metadata(&self) -> &ExtensionMetadata {
67//!         static META: ExtensionMetadata = ExtensionMetadata::new(
68//!             "my-extension",
69//!             "My Extension",
70//!             "1.0.0",
71//!         );
72//!         &META
73//!     }
74//!
75//!     fn metrics(&self) -> Vec<MetricDescriptor> {
76//!         vec![
77//!             MetricDescriptor::new("counter", "Counter", MetricDataType::Integer)
78//!                 .with_unit("count")
79//!         ]
80//!     }
81//!
82//!     fn commands(&self) -> Vec<ExtensionCommand> {
83//!         vec![
84//!             CommandBuilder::new("increment")
85//!                 .display_name("Increment")
86//!                 .param(
87//!                     ParamBuilder::new("amount", MetricDataType::Integer)
88//!                         .display_name("Amount")
89//!                         .default(MetricValue::Integer(1))
90//!                         .build()
91//!                 )
92//!                 .build()
93//!         ]
94//!     }
95//!
96//!     async fn execute_command(&self, command: &str, args: &serde_json::Value) -> Result<serde_json::Value> {
97//!         match command {
98//!             "increment" => {
99//!                 let amount = args.get("amount").and_then(|v| v.as_i64()).unwrap_or(1);
100//!                 let new_value = self.counter.fetch_add(amount, std::sync::atomic::Ordering::SeqCst) + amount;
101//!                 Ok(serde_json::json!({ "counter": new_value }))
102//!             }
103//!             _ => Err(ExtensionError::CommandNotFound(command.to_string())),
104//!         }
105//!     }
106//!
107//!     fn produce_metrics(&self) -> Result<Vec<ExtensionMetricValue>> {
108//!         Ok(vec![
109//!             ExtensionMetricValue::new(
110//!                 "counter",
111//!                 MetricValue::Integer(self.counter.load(std::sync::atomic::Ordering::SeqCst))
112//!             )
113//!         ])
114//!     }
115//! }
116//!
117//! // Export FFI functions
118//! neomind_export!(MyExtension);
119//! ```
120
121// ============================================================================
122// IPC Boundary Types (Stable - for IPC serialization)
123// ============================================================================
124
125mod ipc_types;
126
127/// Stable IPC boundary types for extension communication.
128pub mod ipc {
129    pub use crate::ipc_types::*;
130}
131
132// ============================================================================
133// Host API (Extension trait + capabilities + streaming)
134// ============================================================================
135
136mod host;
137
138// ============================================================================
139// Re-exports from ipc_types (Core Types)
140// ============================================================================
141
142pub use ipc_types::{
143    BatchCommand,
144    BatchResult,
145    BatchResultsVec,
146    CExtensionMetadata,
147    CommandDefinition,
148    CommandDescriptor,
149    ErrorKind,
150    ExtensionCommand,
151    ExtensionDescriptor,
152    ExtensionError,
153    ExtensionMetadata,
154    ExtensionMetricValue,
155    ExtensionRuntimeState,
156    ExtensionStats,
157    IpcFrame,
158    // IPC Protocol Types (for process isolation)
159    IpcMessage,
160    IpcResponse,
161    MetricDataType,
162    MetricDescriptor,
163    MetricValue,
164    ParamMetricValue,
165    ParameterDefinition,
166    ParameterGroup,
167    PushOutputData,
168    PushOutputMessage,
169    Result,
170    StreamClientInfo,
171    StreamDataChunk,
172    ValidationRule,
173    ABI_VERSION,
174};
175
176// Alias for backward compatibility
177pub type MetricDefinition = MetricDescriptor;
178
179// ============================================================================
180// Re-exports from host (Extension trait + capabilities)
181// ============================================================================
182
183pub use host::Extension;
184
185pub use host::{
186    send_push_output,
187
188    set_native_capability_bridge,
189    set_push_output_writer,
190    AvailableCapabilities,
191    CapabilityError,
192    CapabilityManifest,
193    ClientInfo,
194
195    DataChunk,
196    // Event system
197    EventFilter,
198    EventSubscription,
199    // Capability system
200    ExtensionCapability,
201    ExtensionCapabilityProvider,
202    ExtensionContext,
203    ExtensionContextConfig,
204    FlowControl,
205    // Push mode
206    PushOutputWriterFn,
207    SessionStats,
208    StreamCapability,
209    StreamDataType,
210    // Streaming types
211    StreamDirection,
212    StreamError,
213    StreamMode,
214    StreamResult,
215    StreamSession,
216};
217
218// CapabilityContext requires tokio (not available on wasm32)
219#[cfg(not(target_arch = "wasm32"))]
220pub use host::CapabilityContext;
221
222/// Capability name constants - re-exported from host module
223pub mod capability_constants {
224    pub use crate::host::capabilities::*;
225}
226
227// Native-only FFI types
228#[cfg(not(target_arch = "wasm32"))]
229pub use host::{NativeCapabilityFreeFn, NativeCapabilityInvokeFn};
230
231// ============================================================================
232// WASM-specific Types and Extension Trait
233// ============================================================================
234
235#[cfg(target_arch = "wasm32")]
236mod wasm_types {
237    pub use crate::extension::{
238        SdkCommandDefinition as ExtensionCommand, SdkExtensionError as ExtensionError,
239        SdkExtensionMetadata as ExtensionMetadata, SdkExtensionMetricValue as ExtensionMetricValue,
240        SdkMetricDataType as MetricDataType, SdkMetricDefinition as MetricDescriptor,
241        SdkMetricValue as ParamMetricValue, SdkParameterDefinition as ParameterDefinition,
242        SdkParameterGroup as ParameterGroup,
243    };
244
245    pub type Result<T> = std::result::Result<T, crate::extension::SdkExtensionError>;
246    pub const ABI_VERSION: u32 = 3;
247
248    /// Simplified StreamCapability for WASM
249    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
250    pub struct StreamCapability {
251        pub direction: StreamDirection,
252        pub mode: StreamMode,
253        pub max_chunk_size: usize,
254        pub preferred_chunk_size: usize,
255        pub max_concurrent_sessions: usize,
256    }
257
258    impl Default for StreamCapability {
259        fn default() -> Self {
260            Self {
261                direction: StreamDirection::None,
262                mode: StreamMode::Push,
263                max_chunk_size: 0,
264                preferred_chunk_size: 0,
265                max_concurrent_sessions: 0,
266            }
267        }
268    }
269
270    /// Stream direction (WASM version)
271    #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
272    #[serde(rename_all = "lowercase")]
273    pub enum StreamDirection {
274        None,
275        Input,
276        Output,
277        Duplex,
278    }
279
280    /// Stream mode (WASM version)
281    #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
282    #[serde(rename_all = "lowercase")]
283    pub enum StreamMode {
284        Stateless,
285        Stateful,
286        Push,
287        Pull,
288    }
289}
290
291#[cfg(target_arch = "wasm32")]
292pub use wasm_types::*;
293
294// Re-export pollster for WASM target (used by macros)
295#[cfg(target_arch = "wasm32")]
296pub use pollster;
297
298// ============================================================================
299// Utility Re-exports
300// ============================================================================
301
302pub use async_trait::async_trait;
303pub use serde_json::{json, Value};
304
305// ============================================================================
306// Extension Types Module
307// ============================================================================
308
309mod extension;
310pub use extension::*;
311
312pub use extension::{
313    ComponentSize, FrontendComponent, FrontendComponentType, FrontendManifest,
314    FrontendManifestBuilder, I18nConfig,
315};
316
317// ============================================================================
318// Additional Modules
319// ============================================================================
320
321mod macros;
322pub mod prelude;
323
324#[cfg(not(target_arch = "wasm32"))]
325pub mod native;
326
327#[cfg(target_arch = "wasm32")]
328pub mod wasm;
329
330pub mod capabilities;
331pub mod utils;
332
333// ============================================================================
334// SDK Constants
335// ============================================================================
336
337/// SDK version
338pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
339
340/// ABI version for the unified SDK
341pub const SDK_ABI_VERSION: u32 = 3;
342
343/// Minimum NeoMind core version required
344pub const MIN_NEOMIND_VERSION: &str = "0.5.0";
345
346// ============================================================================
347// Builder Types
348// ============================================================================
349
350/// Helper type for building metric descriptors
351#[derive(Debug, Clone)]
352pub struct MetricBuilder {
353    metric: MetricDescriptor,
354}
355
356impl MetricBuilder {
357    pub fn new(name: impl Into<String>, display_name: impl Into<String>) -> Self {
358        Self {
359            metric: MetricDescriptor {
360                name: name.into(),
361                display_name: display_name.into(),
362                data_type: MetricDataType::String,
363                unit: String::new(),
364                min: None,
365                max: None,
366                required: false,
367            },
368        }
369    }
370
371    pub fn data_type(mut self, data_type: MetricDataType) -> Self {
372        self.metric.data_type = data_type;
373        self
374    }
375
376    pub fn float(self) -> Self {
377        self.data_type(MetricDataType::Float)
378    }
379
380    pub fn integer(self) -> Self {
381        self.data_type(MetricDataType::Integer)
382    }
383
384    pub fn boolean(self) -> Self {
385        self.data_type(MetricDataType::Boolean)
386    }
387
388    pub fn string(self) -> Self {
389        self.data_type(MetricDataType::String)
390    }
391
392    pub fn enum_type(self, options: Vec<String>) -> Self {
393        self.data_type(MetricDataType::Enum { options })
394    }
395
396    pub fn unit(mut self, unit: impl Into<String>) -> Self {
397        self.metric.unit = unit.into();
398        self
399    }
400
401    pub fn min(mut self, min: f64) -> Self {
402        self.metric.min = Some(min);
403        self
404    }
405
406    pub fn max(mut self, max: f64) -> Self {
407        self.metric.max = Some(max);
408        self
409    }
410
411    pub fn required(mut self) -> Self {
412        self.metric.required = true;
413        self
414    }
415
416    pub fn build(self) -> MetricDescriptor {
417        self.metric
418    }
419}
420
421/// Helper type for building command definitions
422#[derive(Debug, Clone)]
423pub struct CommandBuilder {
424    command: ExtensionCommand,
425}
426
427impl CommandBuilder {
428    pub fn new(name: impl Into<String>) -> Self {
429        Self {
430            command: ExtensionCommand {
431                name: name.into(),
432                display_name: String::new(),
433                description: String::new(),
434                payload_template: String::new(),
435                parameters: Vec::new(),
436                fixed_values: std::collections::HashMap::new(),
437                samples: Vec::new(),
438                parameter_groups: Vec::new(),
439            },
440        }
441    }
442
443    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
444        self.command.display_name = display_name.into();
445        self
446    }
447
448    pub fn description(mut self, description: impl Into<String>) -> Self {
449        self.command.description = description.into();
450        self
451    }
452
453    pub fn param(mut self, param: ParameterDefinition) -> Self {
454        self.command.parameters.push(param);
455        self
456    }
457
458    pub fn param_simple(
459        mut self,
460        name: impl Into<String>,
461        display_name: impl Into<String>,
462        data_type: MetricDataType,
463    ) -> Self {
464        self.command.parameters.push(ParameterDefinition {
465            name: name.into(),
466            display_name: display_name.into(),
467            description: String::new(),
468            param_type: data_type,
469            required: true,
470            default_value: None,
471            min: None,
472            max: None,
473            options: Vec::new(),
474        });
475        self
476    }
477
478    pub fn param_optional(
479        mut self,
480        name: impl Into<String>,
481        display_name: impl Into<String>,
482        data_type: MetricDataType,
483    ) -> Self {
484        self.command.parameters.push(ParameterDefinition {
485            name: name.into(),
486            display_name: display_name.into(),
487            description: String::new(),
488            param_type: data_type,
489            required: false,
490            default_value: None,
491            min: None,
492            max: None,
493            options: Vec::new(),
494        });
495        self
496    }
497
498    pub fn param_with_default(
499        mut self,
500        name: impl Into<String>,
501        display_name: impl Into<String>,
502        data_type: MetricDataType,
503        default: MetricValue,
504    ) -> Self {
505        self.command.parameters.push(ParameterDefinition {
506            name: name.into(),
507            display_name: display_name.into(),
508            description: String::new(),
509            param_type: data_type,
510            required: false,
511            default_value: Some(default),
512            min: None,
513            max: None,
514            options: Vec::new(),
515        });
516        self
517    }
518
519    pub fn sample(mut self, sample: serde_json::Value) -> Self {
520        self.command.samples.push(sample);
521        self
522    }
523
524    pub fn build(self) -> ExtensionCommand {
525        self.command
526    }
527}
528
529/// Helper type for building parameter definitions
530#[derive(Debug, Clone)]
531pub struct ParamBuilder {
532    param: ParameterDefinition,
533}
534
535impl ParamBuilder {
536    pub fn new(name: impl Into<String>, data_type: MetricDataType) -> Self {
537        Self {
538            param: ParameterDefinition {
539                name: name.into(),
540                display_name: String::new(),
541                description: String::new(),
542                param_type: data_type,
543                required: true,
544                default_value: None,
545                min: None,
546                max: None,
547                options: Vec::new(),
548            },
549        }
550    }
551
552    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
553        self.param.display_name = display_name.into();
554        self
555    }
556
557    pub fn description(mut self, description: impl Into<String>) -> Self {
558        self.param.description = description.into();
559        self
560    }
561
562    pub fn optional(mut self) -> Self {
563        self.param.required = false;
564        self
565    }
566
567    pub fn required(mut self) -> Self {
568        self.param.required = true;
569        self
570    }
571
572    pub fn default(mut self, value: MetricValue) -> Self {
573        self.param.default_value = Some(value);
574        self.param.required = false;
575        self
576    }
577
578    pub fn min(mut self, min: f64) -> Self {
579        self.param.min = Some(min);
580        self
581    }
582
583    pub fn max(mut self, max: f64) -> Self {
584        self.param.max = Some(max);
585        self
586    }
587
588    pub fn options(mut self, options: Vec<String>) -> Self {
589        self.param.options = options;
590        self
591    }
592
593    pub fn build(self) -> ParameterDefinition {
594        self.param
595    }
596}
597
598// ============================================================================
599// Static Helper Macros
600// ============================================================================
601
602/// Create a static ExtensionMetadata
603#[macro_export]
604macro_rules! static_metadata {
605    ($id:literal, $name:literal, $version:literal) => {{
606        static META: $crate::ExtensionMetadata =
607            $crate::ExtensionMetadata::new($id, $name, $version);
608        &META
609    }};
610}
611
612/// Create a static slice of metrics
613#[macro_export]
614macro_rules! static_metrics {
615    ($($metric:expr),* $(,)?) => {{
616        static METRICS: &[$crate::MetricDescriptor] = &[$($metric),*];
617        METRICS
618    }};
619}
620
621/// Create a static slice of commands
622#[macro_export]
623macro_rules! static_commands {
624    ($($cmd:expr),* $(,)?) => {{
625        static COMMANDS: &[$crate::ExtensionCommand] = &[$($cmd),*];
626        COMMANDS
627    }};
628}
629
630// ============================================================================
631// Tests
632// ============================================================================
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn test_capability_constants() {
640        assert_eq!(
641            capability_constants::DEVICE_METRICS_READ,
642            "device_metrics_read"
643        );
644        assert_eq!(
645            capability_constants::DEVICE_METRICS_WRITE,
646            "device_metrics_write"
647        );
648        assert_eq!(capability_constants::DEVICE_CONTROL, "device_control");
649        assert_eq!(capability_constants::STORAGE_QUERY, "storage_query");
650        assert_eq!(capability_constants::EVENT_PUBLISH, "event_publish");
651        assert_eq!(capability_constants::EVENT_SUBSCRIBE, "event_subscribe");
652        assert_eq!(capability_constants::TELEMETRY_HISTORY, "telemetry_history");
653        assert_eq!(capability_constants::METRICS_AGGREGATE, "metrics_aggregate");
654        assert_eq!(capability_constants::EXTENSION_CALL, "extension_call");
655        assert_eq!(capability_constants::AGENT_TRIGGER, "agent_trigger");
656        assert_eq!(capability_constants::RULE_TRIGGER, "rule_trigger");
657    }
658
659    #[test]
660    fn test_metric_builder() {
661        let metric = MetricBuilder::new("test", "Test Metric")
662            .float()
663            .unit("°C")
664            .min(-40.0)
665            .max(100.0)
666            .required()
667            .build();
668
669        assert_eq!(metric.name, "test");
670        assert_eq!(metric.display_name, "Test Metric");
671        assert_eq!(metric.data_type, MetricDataType::Float);
672        assert_eq!(metric.unit, "°C");
673        assert_eq!(metric.min, Some(-40.0));
674        assert_eq!(metric.max, Some(100.0));
675        assert!(metric.required);
676    }
677
678    #[test]
679    fn test_extension_metadata() {
680        let meta = ExtensionMetadata::new("test-ext", "Test Extension", "1.0.0")
681            .with_description("A test extension")
682            .with_author("Test Author");
683
684        assert_eq!(meta.id, "test-ext");
685        assert_eq!(meta.name, "Test Extension");
686        assert_eq!(meta.version, "1.0.0");
687        assert_eq!(meta.description, Some("A test extension".to_string()));
688        assert_eq!(meta.author, Some("Test Author".to_string()));
689    }
690
691    #[test]
692    fn test_abi_version() {
693        assert_eq!(ABI_VERSION, 3);
694        assert_eq!(SDK_ABI_VERSION, 3);
695    }
696}