Skip to main content

shape_abi_v1/
lib.rs

1//! Shape ABI v1
2//!
3//! Stable C ABI for host-loadable Shape capability modules.
4//! Current capability families include data sources and output sinks.
5//!
6//! # Design Principles
7//!
8//! - **Stable C ABI**: Uses `#[repr(C)]` for binary compatibility across Rust versions
9//! - **Self-Describing**: Plugins declare their query parameters and output fields
10//! - **MessagePack Serialization**: Data exchange uses compact binary format
11//! - **Binary Columnar Format**: High-performance direct loading (ABI v2)
12//! - **Platform-Agnostic**: Works on native targets
13//!
14//! # Creating a Data Capability Module
15//!
16//! ```ignore
17//! use shape_abi_v1::*;
18//!
19//! // Define your plugin info
20//! #[no_mangle]
21//! pub extern "C" fn shape_plugin_info() -> *const PluginInfo {
22//!     static INFO: PluginInfo = PluginInfo {
23//!         name: c"my-data-source".as_ptr(),
24//!         version: c"1.0.0".as_ptr(),
25//!         plugin_type: PluginType::DataSource,
26//!         description: c"My custom data source".as_ptr(),
27//!     };
28//!     &INFO
29//! }
30//!
31//! // Optional but recommended: capability manifest
32//! #[no_mangle]
33//! pub extern "C" fn shape_capability_manifest() -> *const CapabilityManifest { ... }
34//!
35//! // Implement the vtable functions...
36//! ```
37
38pub mod binary_builder;
39pub mod binary_format;
40
41use std::ffi::{c_char, c_void};
42
43// ============================================================================
44// Plugin Metadata
45// ============================================================================
46
47/// Plugin metadata returned by `shape_plugin_info()`
48#[repr(C)]
49pub struct PluginInfo {
50    /// Plugin name (null-terminated C string)
51    pub name: *const c_char,
52    /// Plugin version (null-terminated C string, semver format)
53    pub version: *const c_char,
54    /// Type of plugin
55    pub plugin_type: PluginType,
56    /// Human-readable description (null-terminated C string)
57    pub description: *const c_char,
58}
59
60// Safety: PluginInfo contains only const pointers to static strings
61// The strings are never modified through these pointers
62unsafe impl Sync for PluginInfo {}
63
64/// Type of plugin
65#[repr(C)]
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum PluginType {
68    /// Data source that provides time-series data
69    DataSource = 0,
70    /// Output sink for alerts and events
71    OutputSink = 1,
72}
73
74/// Capability family exposed by a plugin/module.
75///
76/// This is intentionally broader than connector-specific concepts so the same
77/// ABI can describe data, sinks, compute kernels, model runtimes, etc.
78#[repr(C)]
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum CapabilityKind {
81    /// Data source/query provider capability.
82    DataSource = 0,
83    /// Output sink capability for alerts/events.
84    OutputSink = 1,
85    /// Generic compute kernel capability.
86    Compute = 2,
87    /// Model/inference runtime capability.
88    Model = 3,
89    /// Language runtime capability for foreign function blocks.
90    LanguageRuntime = 4,
91    /// Catch-all for custom capability families.
92    Custom = 255,
93}
94
95/// Canonical contract name for the built-in data source capability.
96pub const CAPABILITY_DATA_SOURCE: &str = "shape.datasource";
97/// Canonical contract name for the built-in output sink capability.
98pub const CAPABILITY_OUTPUT_SINK: &str = "shape.output_sink";
99/// Canonical contract name for the base module capability.
100pub const CAPABILITY_MODULE: &str = "shape.module";
101/// Canonical contract name for the language runtime capability.
102pub const CAPABILITY_LANGUAGE_RUNTIME: &str = "shape.language_runtime";
103
104/// Declares one capability contract implemented by the plugin.
105#[repr(C)]
106pub struct CapabilityDescriptor {
107    /// Capability family.
108    pub kind: CapabilityKind,
109    /// Contract name (null-terminated C string), e.g. "shape.datasource".
110    pub contract: *const c_char,
111    /// Contract version (null-terminated C string), e.g. "1".
112    pub version: *const c_char,
113    /// Reserved capability flags (set to 0 for now).
114    pub flags: u64,
115}
116
117// Safety: contains only const pointers to static strings.
118unsafe impl Sync for CapabilityDescriptor {}
119
120/// Capability manifest returned by `shape_capability_manifest()`.
121#[repr(C)]
122pub struct CapabilityManifest {
123    /// Array of capability descriptors.
124    pub capabilities: *const CapabilityDescriptor,
125    /// Number of capability descriptors.
126    pub capabilities_len: usize,
127}
128
129// Safety: contains only const pointers to static data.
130unsafe impl Sync for CapabilityManifest {}
131
132// ============================================================================
133// Extension Section Claims
134// ============================================================================
135
136/// Declares a TOML section claimed by an extension.
137///
138/// Extensions use this to declare custom config sections in `shape.toml`
139/// (e.g., `[native-dependencies]`) without coupling domain-specific concepts
140/// into core Shape.
141#[repr(C)]
142pub struct SectionClaim {
143    /// Section name (null-terminated C string), e.g. "native-dependencies"
144    pub name: *const c_char,
145    /// Whether absence of the section is an error (true) or silently ignored (false)
146    pub required: bool,
147}
148
149// Safety: SectionClaim contains only const pointers to static strings
150unsafe impl Sync for SectionClaim {}
151
152/// Manifest of TOML sections claimed by an extension.
153///
154/// Returned by the optional `shape_claimed_sections` export. Extensions that
155/// don't need custom sections simply omit this export (backwards compatible).
156#[repr(C)]
157pub struct SectionsManifest {
158    /// Array of section claims.
159    pub sections: *const SectionClaim,
160    /// Number of section claims.
161    pub sections_len: usize,
162}
163
164// Safety: SectionsManifest contains only const pointers to static data
165unsafe impl Sync for SectionsManifest {}
166
167/// Type signature for optional `shape_claimed_sections` export.
168///
169/// Extensions that need custom TOML sections export this symbol. It is
170/// optional — omitting it is valid and means the extension claims no sections.
171pub type GetClaimedSectionsFn = unsafe extern "C" fn() -> *const SectionsManifest;
172
173// ============================================================================
174// Self-Describing Query Schema
175// ============================================================================
176
177/// Parameter types that a data source can accept in queries
178#[repr(C)]
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum ParamType {
181    /// String value
182    String = 0,
183    /// Numeric value (f64)
184    Number = 1,
185    /// Boolean value
186    Bool = 2,
187    /// Array of strings
188    StringArray = 3,
189    /// Array of numbers
190    NumberArray = 4,
191    /// Nested object with its own schema
192    Object = 5,
193    /// Timestamp (i64 milliseconds since epoch)
194    Timestamp = 6,
195    /// Duration (f64 seconds)
196    Duration = 7,
197}
198
199/// Describes a single query parameter
200///
201/// Plugins use this to declare what parameters they accept,
202/// enabling LSP autocomplete and validation.
203#[repr(C)]
204pub struct QueryParam {
205    /// Parameter name (e.g., "symbol", "device_type", "table")
206    pub name: *const c_char,
207
208    /// Human-readable description
209    pub description: *const c_char,
210
211    /// Parameter type
212    pub param_type: ParamType,
213
214    /// Is this parameter required?
215    pub required: bool,
216
217    /// Default value (MessagePack encoded, null if no default)
218    pub default_value: *const u8,
219    /// Length of default_value bytes
220    pub default_value_len: usize,
221
222    /// For enum-like params: allowed values (MessagePack array, null if any value allowed)
223    pub allowed_values: *const u8,
224    /// Length of allowed_values bytes
225    pub allowed_values_len: usize,
226
227    /// For Object type: nested schema (pointer to QuerySchema, null otherwise)
228    pub nested_schema: *const QuerySchema,
229}
230
231// Safety: QueryParam contains only const pointers to static data
232// The data is never modified through these pointers
233unsafe impl Sync for QueryParam {}
234
235/// Complete schema describing all query parameters for a data source
236#[repr(C)]
237pub struct QuerySchema {
238    /// Array of parameter definitions
239    pub params: *const QueryParam,
240    /// Number of parameters
241    pub params_len: usize,
242
243    /// Example query (MessagePack encoded) for documentation
244    pub example_query: *const u8,
245    /// Length of example_query bytes
246    pub example_query_len: usize,
247}
248
249// Safety: QuerySchema contains only const pointers to static data
250// The data is never modified through these pointers
251unsafe impl Sync for QuerySchema {}
252
253// ============================================================================
254// Self-Describing Output Schema
255// ============================================================================
256
257/// Describes a single output field produced by the data source
258#[repr(C)]
259pub struct OutputField {
260    /// Field name (e.g., "timestamp", "value", "open", "temperature")
261    pub name: *const c_char,
262
263    /// Field type
264    pub field_type: ParamType,
265
266    /// Human-readable description
267    pub description: *const c_char,
268}
269
270// Safety: OutputField contains only const pointers to static strings
271// The data is never modified through these pointers
272unsafe impl Sync for OutputField {}
273
274/// Schema describing output data structure
275#[repr(C)]
276pub struct OutputSchema {
277    /// Array of field definitions
278    pub fields: *const OutputField,
279    /// Number of fields
280    pub fields_len: usize,
281}
282
283// Safety: OutputSchema contains only const pointers to static data
284// The data is never modified through these pointers
285unsafe impl Sync for OutputSchema {}
286
287// ============================================================================
288// Dynamic Schema Discovery (MessagePack-serializable types)
289// ============================================================================
290
291/// Data type for schema columns.
292///
293/// This enum is used in the MessagePack-serialized PluginSchema returned
294/// by the `get_source_schema` vtable function.
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
297#[cfg_attr(feature = "serde", serde(rename_all = "PascalCase"))]
298pub enum DataType {
299    /// Floating-point number
300    Number,
301    /// Integer value
302    Integer,
303    /// String value
304    String,
305    /// Boolean value
306    Boolean,
307    /// Timestamp (Unix milliseconds)
308    Timestamp,
309}
310
311/// Information about a single column in the data source.
312///
313/// This struct is serialized as MessagePack in the response from `get_source_schema`.
314#[derive(Debug, Clone, PartialEq)]
315#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
316pub struct ColumnInfo {
317    /// Column name
318    pub name: std::string::String,
319    /// Column data type
320    pub data_type: DataType,
321}
322
323/// Schema returned by `get_source_schema` for dynamic schema discovery.
324///
325/// This struct is serialized as MessagePack. Example:
326/// ```json
327/// {
328///   "columns": [
329///     { "name": "timestamp", "data_type": "Timestamp" },
330///     { "name": "open", "data_type": "Number" },
331///     { "name": "high", "data_type": "Number" },
332///     { "name": "low", "data_type": "Number" },
333///     { "name": "close", "data_type": "Number" },
334///     { "name": "volume", "data_type": "Integer" }
335///   ],
336///   "timestamp_column": "timestamp"
337/// }
338/// ```
339#[derive(Debug, Clone, PartialEq)]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341pub struct PluginSchema {
342    /// List of columns provided by this source
343    pub columns: Vec<ColumnInfo>,
344    /// Which column contains the timestamp/x-axis data
345    pub timestamp_column: std::string::String,
346}
347
348// ============================================================================
349// Module Capability (shape.module)
350// ============================================================================
351
352/// Schema for one callable module function.
353///
354/// This is serialized as MessagePack by module-capability providers.
355#[derive(Debug, Clone, PartialEq)]
356#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
357pub struct ModuleFunctionSchema {
358    /// Function name as exported in the module namespace.
359    pub name: std::string::String,
360    /// Human-readable description.
361    pub description: std::string::String,
362    /// Parameter type names (for signatures/completions).
363    pub params: Vec<std::string::String>,
364    /// Return type name.
365    pub return_type: Option<std::string::String>,
366}
367
368/// Module-level schema for a `shape.module` capability.
369///
370/// Serialized as MessagePack and returned by `get_module_schema`.
371#[derive(Debug, Clone, PartialEq)]
372#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
373pub struct ModuleSchema {
374    /// Module namespace name (e.g., "duckdb").
375    pub module_name: std::string::String,
376    /// Exported callable functions in this module.
377    pub functions: Vec<ModuleFunctionSchema>,
378}
379
380// ============================================================================
381// Progress Reporting (ABI v2)
382// ============================================================================
383
384/// Progress callback function type for reporting load progress.
385///
386/// Called by plugins during `load_binary` to report progress.
387///
388/// # Arguments
389/// * `phase`: Current phase (0=Connecting, 1=Querying, 2=Fetching, 3=Parsing, 4=Converting)
390/// * `rows_processed`: Number of rows processed so far
391/// * `total_rows`: Total expected rows (0 if unknown)
392/// * `bytes_processed`: Bytes processed so far
393/// * `user_data`: User data passed to `load_binary`
394///
395/// # Returns
396/// * 0: Continue loading
397/// * Non-zero: Cancel the load operation
398pub type ProgressCallbackFn = unsafe extern "C" fn(
399    phase: u8,
400    rows_processed: u64,
401    total_rows: u64,
402    bytes_processed: u64,
403    user_data: *mut c_void,
404) -> i32;
405
406// ============================================================================
407// Data Source Plugin VTable
408// ============================================================================
409
410/// Function pointer types for data source plugins
411#[repr(C)]
412pub struct DataSourceVTable {
413    /// Initialize the data source with configuration.
414    /// `config`: MessagePack-encoded configuration object
415    /// Returns: opaque instance pointer, or null on error
416    pub init: Option<unsafe extern "C" fn(config: *const u8, config_len: usize) -> *mut c_void>,
417
418    /// Get the query schema for this data source.
419    /// Returns a pointer to the QuerySchema struct (must remain valid for plugin lifetime).
420    pub get_query_schema: Option<unsafe extern "C" fn(instance: *mut c_void) -> *const QuerySchema>,
421
422    /// Get the output schema for this data source.
423    /// Returns a pointer to the OutputSchema struct (must remain valid for plugin lifetime).
424    pub get_output_schema:
425        Option<unsafe extern "C" fn(instance: *mut c_void) -> *const OutputSchema>,
426
427    /// Query the data schema for a specific source.
428    ///
429    /// Unlike `get_output_schema` which returns a static schema for the plugin,
430    /// this function returns the dynamic schema for a specific data source.
431    /// This enables schema discovery at runtime.
432    ///
433    /// `source_id`: The source identifier (e.g., table name, symbol, device ID)
434    /// `out_ptr`: Output pointer to MessagePack-encoded PluginSchema
435    /// `out_len`: Output length of the data
436    ///
437    /// The returned PluginSchema (MessagePack) has structure:
438    /// ```json
439    /// {
440    ///   "columns": [
441    ///     { "name": "timestamp", "data_type": "Timestamp" },
442    ///     { "name": "value", "data_type": "Number" }
443    ///   ],
444    ///   "timestamp_column": "timestamp"
445    /// }
446    /// ```
447    ///
448    /// Returns: 0 on success, non-zero error code on failure
449    /// Caller must free the output buffer with `free_buffer`.
450    pub get_source_schema: Option<
451        unsafe extern "C" fn(
452            instance: *mut c_void,
453            source_id: *const u8,
454            source_id_len: usize,
455            out_ptr: *mut *mut u8,
456            out_len: *mut usize,
457        ) -> i32,
458    >,
459
460    /// Validate a query before execution.
461    /// `query`: MessagePack-encoded query parameters
462    /// `out_error`: On error, write error message pointer here (caller must free with `free_string`)
463    /// Returns: 0 on success, non-zero error code on failure
464    pub validate_query: Option<
465        unsafe extern "C" fn(
466            instance: *mut c_void,
467            query: *const u8,
468            query_len: usize,
469            out_error: *mut *mut c_char,
470        ) -> i32,
471    >,
472
473    /// Load historical data (JSON/MessagePack format - legacy).
474    /// `query`: MessagePack-encoded query parameters
475    /// `out_ptr`: Output pointer to MessagePack-encoded Series data
476    /// `out_len`: Output length of the data
477    /// Returns: 0 on success, non-zero error code on failure
478    /// Caller must free the output buffer with `free_buffer`.
479    pub load: Option<
480        unsafe extern "C" fn(
481            instance: *mut c_void,
482            query: *const u8,
483            query_len: usize,
484            out_ptr: *mut *mut u8,
485            out_len: *mut usize,
486        ) -> i32,
487    >,
488
489    /// Load historical data in binary columnar format (ABI v2).
490    ///
491    /// High-performance data loading that bypasses JSON serialization.
492    /// Returns binary data in the format defined by `binary_format` module
493    /// that can be directly mapped to SeriesStorage.
494    ///
495    /// # Arguments
496    /// * `instance`: Plugin instance
497    /// * `query`: MessagePack-encoded query parameters
498    /// * `query_len`: Length of query data
499    /// * `granularity`: Progress reporting granularity (0=Coarse, 1=Fine)
500    /// * `progress_callback`: Optional callback for progress reporting
501    /// * `progress_user_data`: User data passed to progress callback
502    /// * `out_ptr`: Output pointer to binary columnar data
503    /// * `out_len`: Output length of the data
504    ///
505    /// Returns: 0 on success, non-zero error code on failure
506    /// Caller must free the output buffer with `free_buffer`.
507    pub load_binary: Option<
508        unsafe extern "C" fn(
509            instance: *mut c_void,
510            query: *const u8,
511            query_len: usize,
512            granularity: u8,
513            progress_callback: Option<ProgressCallbackFn>,
514            progress_user_data: *mut c_void,
515            out_ptr: *mut *mut u8,
516            out_len: *mut usize,
517        ) -> i32,
518    >,
519
520    /// Subscribe to streaming data.
521    /// `query`: MessagePack-encoded query parameters
522    /// `callback`: Called for each data point (data_ptr, data_len, user_data)
523    /// `callback_data`: User data passed to callback
524    /// Returns: subscription ID on success, 0 on failure
525    pub subscribe: Option<
526        unsafe extern "C" fn(
527            instance: *mut c_void,
528            query: *const u8,
529            query_len: usize,
530            callback: unsafe extern "C" fn(*const u8, usize, *mut c_void),
531            callback_data: *mut c_void,
532        ) -> u64,
533    >,
534
535    /// Unsubscribe from streaming data.
536    /// `subscription_id`: ID returned by `subscribe`
537    /// Returns: 0 on success, non-zero on failure
538    pub unsubscribe:
539        Option<unsafe extern "C" fn(instance: *mut c_void, subscription_id: u64) -> i32>,
540
541    /// Free a buffer allocated by `load`.
542    pub free_buffer: Option<unsafe extern "C" fn(ptr: *mut u8, len: usize)>,
543
544    /// Free an error string allocated by `validate_query`.
545    pub free_string: Option<unsafe extern "C" fn(ptr: *mut c_char)>,
546
547    /// Cleanup and destroy the instance.
548    pub drop: Option<unsafe extern "C" fn(instance: *mut c_void)>,
549}
550
551// ============================================================================
552// Output Sink Plugin VTable
553// ============================================================================
554
555/// Function pointer types for output sink plugins (alerts, webhooks, etc.)
556#[repr(C)]
557pub struct OutputSinkVTable {
558    /// Initialize the output sink with configuration.
559    /// `config`: MessagePack-encoded configuration object
560    /// Returns: opaque instance pointer, or null on error
561    pub init: Option<unsafe extern "C" fn(config: *const u8, config_len: usize) -> *mut c_void>,
562
563    /// Get the tags this sink handles (for routing).
564    /// Returns a MessagePack-encoded array of strings.
565    /// Empty array means sink handles all alerts.
566    pub get_handled_tags: Option<
567        unsafe extern "C" fn(instance: *mut c_void, out_ptr: *mut *mut u8, out_len: *mut usize),
568    >,
569
570    /// Send an alert.
571    /// `alert`: MessagePack-encoded Alert struct
572    /// Returns: 0 on success, non-zero error code on failure
573    pub send: Option<
574        unsafe extern "C" fn(instance: *mut c_void, alert: *const u8, alert_len: usize) -> i32,
575    >,
576
577    /// Flush any pending alerts.
578    /// Returns: 0 on success, non-zero error code on failure
579    pub flush: Option<unsafe extern "C" fn(instance: *mut c_void) -> i32>,
580
581    /// Free a buffer allocated by `get_handled_tags`.
582    pub free_buffer: Option<unsafe extern "C" fn(ptr: *mut u8, len: usize)>,
583
584    /// Cleanup and destroy the instance.
585    pub drop: Option<unsafe extern "C" fn(instance: *mut c_void)>,
586}
587
588// ============================================================================
589// Module Plugin VTable
590// ============================================================================
591
592/// Payload kind returned by `ModuleVTable::invoke_ex`.
593#[repr(u8)]
594#[derive(Debug, Clone, Copy, PartialEq, Eq)]
595pub enum ModuleInvokeResultKind {
596    /// MessagePack-encoded `shape_wire::WireValue` payload.
597    WireValueMsgpack = 0,
598    /// Arrow IPC bytes for a single table result (fast path, no wire envelope).
599    TableArrowIpc = 1,
600}
601
602/// Extended invoke payload for module capability calls.
603#[repr(C)]
604pub struct ModuleInvokeResult {
605    /// Payload encoding kind.
606    pub kind: ModuleInvokeResultKind,
607    /// Pointer to plugin-owned payload bytes.
608    pub payload_ptr: *mut u8,
609    /// Length in bytes of `payload_ptr`.
610    pub payload_len: usize,
611}
612
613impl ModuleInvokeResult {
614    /// Empty invoke result with no payload.
615    pub const fn empty() -> Self {
616        Self {
617            kind: ModuleInvokeResultKind::WireValueMsgpack,
618            payload_ptr: core::ptr::null_mut(),
619            payload_len: 0,
620        }
621    }
622}
623
624/// Function pointer types for the base module capability (`shape.module`).
625#[repr(C)]
626pub struct ModuleVTable {
627    /// Initialize module instance with MessagePack-encoded config.
628    pub init: Option<unsafe extern "C" fn(config: *const u8, config_len: usize) -> *mut c_void>,
629
630    /// Return MessagePack-encoded [`ModuleSchema`].
631    ///
632    /// The caller must free the output buffer with `free_buffer`.
633    pub get_module_schema: Option<
634        unsafe extern "C" fn(
635            instance: *mut c_void,
636            out_ptr: *mut *mut u8,
637            out_len: *mut usize,
638        ) -> i32,
639    >,
640
641    /// Return MessagePack-encoded module artifacts payload.
642    ///
643    /// This is an opaque host-defined payload for bundled Shape modules
644    /// (source and/or precompiled artifacts). ABI keeps this generic.
645    ///
646    /// The caller must free the output buffer with `free_buffer`.
647    pub get_module_artifacts: Option<
648        unsafe extern "C" fn(
649            instance: *mut c_void,
650            out_ptr: *mut *mut u8,
651            out_len: *mut usize,
652        ) -> i32,
653    >,
654
655    /// Invoke a module function with MessagePack-encoded `shape_wire::WireValue` array.
656    ///
657    /// `function` is a UTF-8 function name (bytes).
658    /// `args` is a MessagePack-encoded `Vec<shape_wire::WireValue>` payload.
659    /// On success, `out_ptr/out_len` contain MessagePack-encoded `shape_wire::WireValue`.
660    pub invoke: Option<
661        unsafe extern "C" fn(
662            instance: *mut c_void,
663            function: *const u8,
664            function_len: usize,
665            args: *const u8,
666            args_len: usize,
667            out_ptr: *mut *mut u8,
668            out_len: *mut usize,
669        ) -> i32,
670    >,
671
672    /// Invoke a module function and return a typed payload (`WireValue` or table IPC).
673    ///
674    /// `function` is a UTF-8 function name (bytes).
675    /// `args` is a MessagePack-encoded `Vec<shape_wire::WireValue>` payload.
676    /// On success, `out` must be filled with a valid payload descriptor.
677    pub invoke_ex: Option<
678        unsafe extern "C" fn(
679            instance: *mut c_void,
680            function: *const u8,
681            function_len: usize,
682            args: *const u8,
683            args_len: usize,
684            out: *mut ModuleInvokeResult,
685        ) -> i32,
686    >,
687
688    /// Free a buffer allocated by `get_module_schema`, `invoke`, or `invoke_ex`.
689    pub free_buffer: Option<unsafe extern "C" fn(ptr: *mut u8, len: usize)>,
690
691    /// Cleanup and destroy the instance.
692    pub drop: Option<unsafe extern "C" fn(instance: *mut c_void)>,
693}
694
695// ============================================================================
696// Language Runtime Plugin VTable
697// ============================================================================
698
699/// Error model for a language runtime.
700///
701/// Describes whether a runtime's foreign function calls can fail at runtime
702/// due to the inherent dynamism of the language.
703#[repr(C)]
704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
705pub enum ErrorModel {
706    /// Runtime errors are possible on every call (Python, JS, Ruby).
707    /// Foreign function return types are automatically wrapped in `Result<T>`.
708    Dynamic = 0,
709    /// The language has compile-time type safety. Foreign functions return
710    /// `T` directly; runtime errors are not expected under normal operation.
711    Static = 1,
712}
713
714/// VTable for language runtime plugins (Python, Julia, SQL, etc.).
715///
716/// Language runtimes enable `fn <language> name(...) { body }` blocks in Shape.
717/// The runtime compiles and invokes foreign language code, providing type
718/// marshaling between Shape values and native language objects.
719#[repr(C)]
720pub struct LanguageRuntimeVTable {
721    /// Initialize the runtime with MessagePack-encoded config.
722    /// Returns: opaque instance pointer, or null on error.
723    pub init: Option<unsafe extern "C" fn(config: *const u8, config_len: usize) -> *mut c_void>,
724
725    /// Register Shape type schemas for stub generation (e.g. `.pyi` files).
726    /// `types_msgpack`: MessagePack-encoded `Vec<TypeSchemaExport>`.
727    /// Returns: 0 on success.
728    pub register_types: Option<
729        unsafe extern "C" fn(instance: *mut c_void, types: *const u8, types_len: usize) -> i32,
730    >,
731
732    /// Pre-compile a foreign function body.
733    ///
734    /// * `name`: function name (UTF-8)
735    /// * `source`: dedented body text (UTF-8)
736    /// * `param_names_msgpack`: MessagePack `Vec<String>` of parameter names
737    /// * `param_types_msgpack`: MessagePack `Vec<String>` of Shape type names
738    /// * `return_type`: Shape return type name (UTF-8, empty if none)
739    /// * `is_async`: whether the function was declared `async` in Shape
740    ///
741    /// Returns: opaque compiled function handle, or null on error.
742    /// On error, writes a UTF-8 error message to `out_error` / `out_error_len`
743    /// (caller frees via `free_buffer`).
744    pub compile: Option<
745        unsafe extern "C" fn(
746            instance: *mut c_void,
747            name: *const u8,
748            name_len: usize,
749            source: *const u8,
750            source_len: usize,
751            param_names: *const u8,
752            param_names_len: usize,
753            param_types: *const u8,
754            param_types_len: usize,
755            return_type: *const u8,
756            return_type_len: usize,
757            is_async: bool,
758            out_error: *mut *mut u8,
759            out_error_len: *mut usize,
760        ) -> *mut c_void,
761    >,
762
763    /// Invoke a compiled function with MessagePack-encoded arguments.
764    ///
765    /// `args_msgpack`: MessagePack-encoded argument array.
766    /// On success, writes MessagePack-encoded result to `out_ptr` / `out_len`.
767    /// Returns: 0 on success, non-zero on error.
768    pub invoke: Option<
769        unsafe extern "C" fn(
770            instance: *mut c_void,
771            handle: *mut c_void,
772            args: *const u8,
773            args_len: usize,
774            out_ptr: *mut *mut u8,
775            out_len: *mut usize,
776        ) -> i32,
777    >,
778
779    /// Release a compiled function handle.
780    pub dispose_function: Option<unsafe extern "C" fn(instance: *mut c_void, handle: *mut c_void)>,
781
782    /// Return the language identifier (null-terminated C string, e.g. "python").
783    /// The returned pointer must remain valid for the lifetime of the instance.
784    pub language_id: Option<unsafe extern "C" fn(instance: *mut c_void) -> *const c_char>,
785
786    /// Return MessagePack-encoded `LanguageRuntimeLspConfig`.
787    /// Caller frees via `free_buffer`.
788    pub get_lsp_config: Option<
789        unsafe extern "C" fn(
790            instance: *mut c_void,
791            out_ptr: *mut *mut u8,
792            out_len: *mut usize,
793        ) -> i32,
794    >,
795
796    /// Free a buffer allocated by compile/invoke/get_lsp_config.
797    pub free_buffer: Option<unsafe extern "C" fn(ptr: *mut u8, len: usize)>,
798
799    /// Cleanup and destroy the runtime instance.
800    pub drop: Option<unsafe extern "C" fn(instance: *mut c_void)>,
801
802    /// Error model for this language runtime.
803    ///
804    /// `Dynamic` (0) means every call can fail at runtime — return values are
805    /// automatically wrapped in `Result<T>`.  `Static` (1) means the language
806    /// has compile-time type safety and runtime errors are not expected.
807    ///
808    /// Defaults to `Dynamic` (0) when zero-initialized.
809    pub error_model: ErrorModel,
810}
811
812/// LSP configuration for a language runtime, returned by `get_lsp_config`.
813#[derive(Debug, Clone, PartialEq)]
814#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
815pub struct LanguageRuntimeLspConfig {
816    /// Language identifier (e.g. "python").
817    pub language_id: std::string::String,
818    /// Command to start the child language server.
819    pub server_command: Vec<std::string::String>,
820    /// File extension for virtual documents (e.g. ".py").
821    pub file_extension: std::string::String,
822    /// Extra search paths for the child LSP (e.g. stub directories).
823    pub extra_paths: Vec<std::string::String>,
824}
825
826/// Exported Shape type schema for foreign language runtimes.
827///
828/// Serialized as MessagePack and passed to `register_types()`.
829#[derive(Debug, Clone, PartialEq)]
830#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
831pub struct TypeSchemaExport {
832    /// Type name.
833    pub name: std::string::String,
834    /// Kind of type.
835    pub kind: TypeSchemaExportKind,
836    /// Fields (for struct types).
837    pub fields: Vec<TypeFieldExport>,
838    /// Enum variants (for enum types).
839    pub enum_variants: Option<Vec<EnumVariantExport>>,
840}
841
842/// Kind of exported type schema.
843#[derive(Debug, Clone, Copy, PartialEq, Eq)]
844#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
845pub enum TypeSchemaExportKind {
846    Struct,
847    Enum,
848    Alias,
849}
850
851/// A single field in an exported type schema.
852#[derive(Debug, Clone, PartialEq)]
853#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
854pub struct TypeFieldExport {
855    /// Field name.
856    pub name: std::string::String,
857    /// Shape type name (e.g. "number", "string", "Array<Candle>").
858    pub type_name: std::string::String,
859    /// Whether the field is optional.
860    pub optional: bool,
861    /// Human-readable description.
862    pub description: Option<std::string::String>,
863}
864
865/// A single enum variant in an exported type schema.
866#[derive(Debug, Clone, PartialEq)]
867#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
868pub struct EnumVariantExport {
869    /// Variant name.
870    pub name: std::string::String,
871    /// Payload fields (if any).
872    pub payload_fields: Option<Vec<TypeFieldExport>>,
873}
874
875// ============================================================================
876// Required Plugin Exports
877// ============================================================================
878
879/// Type signature for `shape_plugin_info` export
880pub type GetPluginInfoFn = unsafe extern "C" fn() -> *const PluginInfo;
881
882/// Type signature for `shape_data_source_vtable` export
883pub type GetDataSourceVTableFn = unsafe extern "C" fn() -> *const DataSourceVTable;
884
885/// Type signature for `shape_output_sink_vtable` export
886pub type GetOutputSinkVTableFn = unsafe extern "C" fn() -> *const OutputSinkVTable;
887/// Type signature for `shape_module_vtable` export.
888pub type GetModuleVTableFn = unsafe extern "C" fn() -> *const ModuleVTable;
889/// Type signature for `shape_language_runtime_vtable` export.
890pub type GetLanguageRuntimeVTableFn = unsafe extern "C" fn() -> *const LanguageRuntimeVTable;
891/// Type signature for optional `shape_capability_manifest` export
892pub type GetCapabilityManifestFn = unsafe extern "C" fn() -> *const CapabilityManifest;
893/// Type signature for optional generic `shape_capability_vtable` export
894///
895/// When present, this is preferred over capability-specific symbol names.
896/// `contract` is a UTF-8 byte slice (for example `shape.datasource`).
897/// Return null when the contract is not implemented by this module.
898pub type GetCapabilityVTableFn =
899    unsafe extern "C" fn(contract: *const u8, contract_len: usize) -> *const c_void;
900
901// ============================================================================
902// Error Codes
903// ============================================================================
904
905/// Standard error codes returned by plugin functions
906#[repr(i32)]
907#[derive(Debug, Clone, Copy, PartialEq, Eq)]
908pub enum PluginError {
909    /// Operation succeeded
910    Success = 0,
911    /// Invalid argument
912    InvalidArgument = 1,
913    /// Query validation failed
914    ValidationFailed = 2,
915    /// Connection error
916    ConnectionError = 3,
917    /// Data not found
918    NotFound = 4,
919    /// Timeout
920    Timeout = 5,
921    /// Permission denied
922    PermissionDenied = 6,
923    /// Internal error
924    InternalError = 7,
925    /// Not implemented
926    NotImplemented = 8,
927    /// Resource exhausted
928    ResourceExhausted = 9,
929    /// Plugin not initialized
930    NotInitialized = 10,
931}
932
933// ============================================================================
934// Permission Model (Self-Describing)
935// ============================================================================
936
937use std::collections::BTreeSet;
938use std::fmt;
939
940/// Category of a permission, used for grouping in human-readable displays.
941#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
942#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
943pub enum PermissionCategory {
944    /// Filesystem access (read, write, scoped).
945    Filesystem,
946    /// Network access (connect, listen, scoped).
947    Network,
948    /// System-level capabilities (process, env, time, random).
949    System,
950    /// Sandbox controls (virtual fs, deterministic runtime, output capture).
951    Sandbox,
952}
953
954impl PermissionCategory {
955    /// Human-readable name for this category.
956    pub fn name(&self) -> &'static str {
957        match self {
958            Self::Filesystem => "Filesystem",
959            Self::Network => "Network",
960            Self::System => "System",
961            Self::Sandbox => "Sandbox",
962        }
963    }
964}
965
966impl fmt::Display for PermissionCategory {
967    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
968        f.write_str(self.name())
969    }
970}
971
972/// A single, self-describing permission that a plugin can request.
973///
974/// Each variant carries enough metadata to produce human-readable prompts
975/// (e.g., "Allow plugin X to read the filesystem?").
976///
977/// Permissions are intentionally **not** bitflags — they are named, enumerable,
978/// and carry documentation so that hosts can display meaningful permission
979/// dialogs and plugins can declare exactly what they need.
980#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
981#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
982pub enum Permission {
983    // -- Filesystem --
984    /// Read files and directories.
985    FsRead,
986    /// Write, create, and delete files and directories.
987    FsWrite,
988    /// Filesystem access scoped to specific paths (see `PermissionGrant`).
989    FsScoped,
990
991    // -- Network --
992    /// Open outbound network connections.
993    NetConnect,
994    /// Listen for inbound network connections.
995    NetListen,
996    /// Network access scoped to specific hosts/ports (see `PermissionGrant`).
997    NetScoped,
998
999    // -- System --
1000    /// Spawn child processes.
1001    Process,
1002    /// Read environment variables.
1003    Env,
1004    /// Access wall-clock time.
1005    Time,
1006    /// Access random number generation.
1007    Random,
1008
1009    // -- Sandbox controls --
1010    /// Plugin operates against a virtual filesystem instead of the real one.
1011    Vfs,
1012    /// Plugin runs in a deterministic runtime (fixed time, seeded RNG).
1013    Deterministic,
1014    /// Plugin output is captured for inspection rather than emitted directly.
1015    Capture,
1016    /// Memory usage is limited to a configured ceiling.
1017    MemLimited,
1018    /// Wall-clock execution time is capped.
1019    TimeLimited,
1020    /// Output volume is capped (bytes or records).
1021    OutputLimited,
1022}
1023
1024impl Permission {
1025    /// Short machine-readable name (stable across versions).
1026    pub fn name(&self) -> &'static str {
1027        match self {
1028            Self::FsRead => "fs.read",
1029            Self::FsWrite => "fs.write",
1030            Self::FsScoped => "fs.scoped",
1031            Self::NetConnect => "net.connect",
1032            Self::NetListen => "net.listen",
1033            Self::NetScoped => "net.scoped",
1034            Self::Process => "sys.process",
1035            Self::Env => "sys.env",
1036            Self::Time => "sys.time",
1037            Self::Random => "sys.random",
1038            Self::Vfs => "sandbox.vfs",
1039            Self::Deterministic => "sandbox.deterministic",
1040            Self::Capture => "sandbox.capture",
1041            Self::MemLimited => "sandbox.mem_limited",
1042            Self::TimeLimited => "sandbox.time_limited",
1043            Self::OutputLimited => "sandbox.output_limited",
1044        }
1045    }
1046
1047    /// Human-readable description suitable for permission prompts.
1048    pub fn description(&self) -> &'static str {
1049        match self {
1050            Self::FsRead => "Read files and directories",
1051            Self::FsWrite => "Write, create, and delete files and directories",
1052            Self::FsScoped => "Filesystem access scoped to specific paths",
1053            Self::NetConnect => "Open outbound network connections",
1054            Self::NetListen => "Listen for inbound network connections",
1055            Self::NetScoped => "Network access scoped to specific hosts/ports",
1056            Self::Process => "Spawn child processes",
1057            Self::Env => "Read environment variables",
1058            Self::Time => "Access wall-clock time",
1059            Self::Random => "Access random number generation",
1060            Self::Vfs => "Operate against a virtual filesystem",
1061            Self::Deterministic => "Run in a deterministic runtime (fixed time, seeded RNG)",
1062            Self::Capture => "Output is captured for inspection",
1063            Self::MemLimited => "Memory usage is limited to a configured ceiling",
1064            Self::TimeLimited => "Execution time is capped",
1065            Self::OutputLimited => "Output volume is capped",
1066        }
1067    }
1068
1069    /// Category this permission belongs to.
1070    pub fn category(&self) -> PermissionCategory {
1071        match self {
1072            Self::FsRead | Self::FsWrite | Self::FsScoped => PermissionCategory::Filesystem,
1073            Self::NetConnect | Self::NetListen | Self::NetScoped => PermissionCategory::Network,
1074            Self::Process | Self::Env | Self::Time | Self::Random => PermissionCategory::System,
1075            Self::Vfs
1076            | Self::Deterministic
1077            | Self::Capture
1078            | Self::MemLimited
1079            | Self::TimeLimited
1080            | Self::OutputLimited => PermissionCategory::Sandbox,
1081        }
1082    }
1083
1084    /// All permission variants (useful for enumeration / display).
1085    pub fn all_variants() -> &'static [Permission] {
1086        &[
1087            Self::FsRead,
1088            Self::FsWrite,
1089            Self::FsScoped,
1090            Self::NetConnect,
1091            Self::NetListen,
1092            Self::NetScoped,
1093            Self::Process,
1094            Self::Env,
1095            Self::Time,
1096            Self::Random,
1097            Self::Vfs,
1098            Self::Deterministic,
1099            Self::Capture,
1100            Self::MemLimited,
1101            Self::TimeLimited,
1102            Self::OutputLimited,
1103        ]
1104    }
1105}
1106
1107impl fmt::Display for Permission {
1108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1109        f.write_str(self.name())
1110    }
1111}
1112
1113/// A set of permissions with set-algebraic operations.
1114///
1115/// Backed by a `BTreeSet` so iteration order is deterministic and
1116/// serialization is stable.
1117#[derive(Debug, Clone, PartialEq, Eq)]
1118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1119pub struct PermissionSet {
1120    permissions: BTreeSet<Permission>,
1121}
1122
1123impl Default for PermissionSet {
1124    fn default() -> Self {
1125        Self::pure()
1126    }
1127}
1128
1129impl PermissionSet {
1130    /// Empty permission set (pure computation — no capabilities).
1131    pub fn pure() -> Self {
1132        Self {
1133            permissions: BTreeSet::new(),
1134        }
1135    }
1136
1137    /// Read-only access: filesystem read + env + time.
1138    pub fn readonly() -> Self {
1139        Self {
1140            permissions: [Permission::FsRead, Permission::Env, Permission::Time]
1141                .into_iter()
1142                .collect(),
1143        }
1144    }
1145
1146    /// Full (unrestricted) permissions — every variant.
1147    pub fn full() -> Self {
1148        Self {
1149            permissions: Permission::all_variants().iter().copied().collect(),
1150        }
1151    }
1152
1153    /// Create a set from an iterator of permissions.
1154    pub fn from_iter(iter: impl IntoIterator<Item = Permission>) -> Self {
1155        Self {
1156            permissions: iter.into_iter().collect(),
1157        }
1158    }
1159
1160    /// Add a permission to the set. Returns whether it was newly inserted.
1161    pub fn insert(&mut self, perm: Permission) -> bool {
1162        self.permissions.insert(perm)
1163    }
1164
1165    /// Remove a permission from the set. Returns whether it was present.
1166    pub fn remove(&mut self, perm: &Permission) -> bool {
1167        self.permissions.remove(perm)
1168    }
1169
1170    /// Check whether a specific permission is in the set.
1171    pub fn contains(&self, perm: &Permission) -> bool {
1172        self.permissions.contains(perm)
1173    }
1174
1175    /// True if this set is a subset of `other`.
1176    pub fn is_subset(&self, other: &PermissionSet) -> bool {
1177        self.permissions.is_subset(&other.permissions)
1178    }
1179
1180    /// True if this set is a superset of `other`.
1181    pub fn is_superset(&self, other: &PermissionSet) -> bool {
1182        self.permissions.is_superset(&other.permissions)
1183    }
1184
1185    /// Set union (all permissions from both sets).
1186    pub fn union(&self, other: &PermissionSet) -> PermissionSet {
1187        PermissionSet {
1188            permissions: self
1189                .permissions
1190                .union(&other.permissions)
1191                .copied()
1192                .collect(),
1193        }
1194    }
1195
1196    /// Set intersection (only permissions in both sets).
1197    pub fn intersection(&self, other: &PermissionSet) -> PermissionSet {
1198        PermissionSet {
1199            permissions: self
1200                .permissions
1201                .intersection(&other.permissions)
1202                .copied()
1203                .collect(),
1204        }
1205    }
1206
1207    /// Set difference (permissions in self but not in other).
1208    pub fn difference(&self, other: &PermissionSet) -> PermissionSet {
1209        PermissionSet {
1210            permissions: self
1211                .permissions
1212                .difference(&other.permissions)
1213                .copied()
1214                .collect(),
1215        }
1216    }
1217
1218    /// True when the set is empty (no permissions).
1219    pub fn is_empty(&self) -> bool {
1220        self.permissions.is_empty()
1221    }
1222
1223    /// Number of permissions in the set.
1224    pub fn len(&self) -> usize {
1225        self.permissions.len()
1226    }
1227
1228    /// Iterate over the permissions in deterministic order.
1229    pub fn iter(&self) -> impl Iterator<Item = &Permission> {
1230        self.permissions.iter()
1231    }
1232
1233    /// Return permissions grouped by category.
1234    pub fn by_category(&self) -> std::collections::BTreeMap<PermissionCategory, Vec<Permission>> {
1235        let mut map = std::collections::BTreeMap::new();
1236        for perm in &self.permissions {
1237            map.entry(perm.category())
1238                .or_insert_with(Vec::new)
1239                .push(*perm);
1240        }
1241        map
1242    }
1243}
1244
1245impl fmt::Display for PermissionSet {
1246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1247        let names: Vec<&str> = self.permissions.iter().map(|p| p.name()).collect();
1248        write!(f, "{{{}}}", names.join(", "))
1249    }
1250}
1251
1252impl<const N: usize> From<[Permission; N]> for PermissionSet {
1253    fn from(arr: [Permission; N]) -> Self {
1254        Self {
1255            permissions: arr.into_iter().collect(),
1256        }
1257    }
1258}
1259
1260impl std::iter::FromIterator<Permission> for PermissionSet {
1261    fn from_iter<I: IntoIterator<Item = Permission>>(iter: I) -> Self {
1262        Self {
1263            permissions: iter.into_iter().collect(),
1264        }
1265    }
1266}
1267
1268impl IntoIterator for PermissionSet {
1269    type Item = Permission;
1270    type IntoIter = std::collections::btree_set::IntoIter<Permission>;
1271
1272    fn into_iter(self) -> Self::IntoIter {
1273        self.permissions.into_iter()
1274    }
1275}
1276
1277impl<'a> IntoIterator for &'a PermissionSet {
1278    type Item = &'a Permission;
1279    type IntoIter = std::collections::btree_set::Iter<'a, Permission>;
1280
1281    fn into_iter(self) -> Self::IntoIter {
1282        self.permissions.iter()
1283    }
1284}
1285
1286/// Scope constraints for a permission grant.
1287///
1288/// When attached to a `PermissionGrant`, these constrain *where* or *how much*
1289/// a permission applies. For example, `FsScoped` with `allowed_paths` limits
1290/// filesystem access to specific directories.
1291#[derive(Debug, Clone, PartialEq)]
1292#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1293pub struct ScopeConstraints {
1294    /// Allowed filesystem paths (glob patterns). Only relevant for `FsScoped`.
1295    #[cfg_attr(
1296        feature = "serde",
1297        serde(default, skip_serializing_if = "Vec::is_empty")
1298    )]
1299    pub allowed_paths: Vec<std::string::String>,
1300
1301    /// Allowed network hosts (host:port patterns). Only relevant for `NetScoped`.
1302    #[cfg_attr(
1303        feature = "serde",
1304        serde(default, skip_serializing_if = "Vec::is_empty")
1305    )]
1306    pub allowed_hosts: Vec<std::string::String>,
1307
1308    /// Maximum memory in bytes. Only relevant for `MemLimited`.
1309    #[cfg_attr(
1310        feature = "serde",
1311        serde(default, skip_serializing_if = "Option::is_none")
1312    )]
1313    pub max_memory_bytes: Option<u64>,
1314
1315    /// Maximum execution time in milliseconds. Only relevant for `TimeLimited`.
1316    #[cfg_attr(
1317        feature = "serde",
1318        serde(default, skip_serializing_if = "Option::is_none")
1319    )]
1320    pub max_time_ms: Option<u64>,
1321
1322    /// Maximum output bytes. Only relevant for `OutputLimited`.
1323    #[cfg_attr(
1324        feature = "serde",
1325        serde(default, skip_serializing_if = "Option::is_none")
1326    )]
1327    pub max_output_bytes: Option<u64>,
1328}
1329
1330impl ScopeConstraints {
1331    /// Unconstrained (no limits).
1332    pub fn none() -> Self {
1333        Self {
1334            allowed_paths: Vec::new(),
1335            allowed_hosts: Vec::new(),
1336            max_memory_bytes: None,
1337            max_time_ms: None,
1338            max_output_bytes: None,
1339        }
1340    }
1341}
1342
1343impl Default for ScopeConstraints {
1344    fn default() -> Self {
1345        Self::none()
1346    }
1347}
1348
1349/// A single granted permission with optional scope constraints.
1350#[derive(Debug, Clone, PartialEq)]
1351#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1352pub struct PermissionGrant {
1353    /// The permission being granted.
1354    pub permission: Permission,
1355    /// Optional scope constraints narrowing the grant.
1356    #[cfg_attr(
1357        feature = "serde",
1358        serde(default, skip_serializing_if = "Option::is_none")
1359    )]
1360    pub constraints: Option<ScopeConstraints>,
1361}
1362
1363impl PermissionGrant {
1364    /// Grant a permission without scope constraints.
1365    pub fn unconstrained(permission: Permission) -> Self {
1366        Self {
1367            permission,
1368            constraints: None,
1369        }
1370    }
1371
1372    /// Grant a permission with scope constraints.
1373    pub fn scoped(permission: Permission, constraints: ScopeConstraints) -> Self {
1374        Self {
1375            permission,
1376            constraints: Some(constraints),
1377        }
1378    }
1379}
1380
1381// ============================================================================
1382// Alert Types (for Output Sinks)
1383// ============================================================================
1384
1385/// Alert severity levels
1386#[repr(u8)]
1387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1388pub enum AlertSeverity {
1389    Debug = 0,
1390    Info = 1,
1391    Warning = 2,
1392    Error = 3,
1393    Critical = 4,
1394}
1395
1396/// C-compatible alert structure for serialization reference
1397///
1398/// Actual alerts are MessagePack-encoded with this structure:
1399/// ```json
1400/// {
1401///   "id": "uuid-string",
1402///   "severity": 1,  // AlertSeverity value
1403///   "title": "Alert title",
1404///   "message": "Detailed message",
1405///   "data": { "key": "value" },  // Arbitrary structured data
1406///   "tags": ["tag1", "tag2"],
1407///   "timestamp": 1706054400000  // Unix millis
1408/// }
1409/// ```
1410#[repr(C)]
1411pub struct AlertHeader {
1412    /// Alert severity
1413    pub severity: AlertSeverity,
1414    /// Timestamp in milliseconds since Unix epoch
1415    pub timestamp_ms: i64,
1416}
1417
1418// ============================================================================
1419// Version Checking
1420// ============================================================================
1421
1422/// ABI version for compatibility checking
1423/// ABI version for compatibility checking
1424///
1425/// Version history:
1426/// - v1: Initial release with MessagePack-based load()
1427/// - v2: Added load_binary() for high-performance binary columnar format
1428/// - v3: Added module invoke_ex() typed payloads for table fast-path marshalling
1429pub const ABI_VERSION: u32 = 3;
1430
1431/// Get the ABI version (plugins should export this)
1432pub type GetAbiVersionFn = unsafe extern "C" fn() -> u32;
1433
1434// ============================================================================
1435// Helper Macros (for plugin authors)
1436// ============================================================================
1437
1438/// Macro to define a static QueryParam with const strings
1439#[macro_export]
1440macro_rules! query_param {
1441    (
1442        name: $name:expr,
1443        description: $desc:expr,
1444        param_type: $ptype:expr,
1445        required: $req:expr
1446    ) => {
1447        $crate::QueryParam {
1448            name: concat!($name, "\0").as_ptr() as *const core::ffi::c_char,
1449            description: concat!($desc, "\0").as_ptr() as *const core::ffi::c_char,
1450            param_type: $ptype,
1451            required: $req,
1452            default_value: core::ptr::null(),
1453            default_value_len: 0,
1454            allowed_values: core::ptr::null(),
1455            allowed_values_len: 0,
1456            nested_schema: core::ptr::null(),
1457        }
1458    };
1459}
1460
1461/// Macro to define a static OutputField with const strings
1462#[macro_export]
1463macro_rules! output_field {
1464    (
1465        name: $name:expr,
1466        field_type: $ftype:expr,
1467        description: $desc:expr
1468    ) => {
1469        $crate::OutputField {
1470            name: concat!($name, "\0").as_ptr() as *const core::ffi::c_char,
1471            field_type: $ftype,
1472            description: concat!($desc, "\0").as_ptr() as *const core::ffi::c_char,
1473        }
1474    };
1475}
1476
1477// ============================================================================
1478// Safety Documentation
1479// ============================================================================
1480
1481// # Safety Requirements for Plugin Authors
1482//
1483// 1. All `*const c_char` strings must be null-terminated
1484// 2. All MessagePack buffers must be valid MessagePack data
1485// 3. Instance pointers must be valid for the lifetime of the plugin
1486// 4. Callbacks must not panic across the FFI boundary
1487// 5. Memory allocated by plugin must be freed by plugin's free functions
1488// 6. Schemas must remain valid for the lifetime of the plugin instance
1489
1490// ============================================================================
1491// Tests — Permission Model
1492// ============================================================================
1493
1494#[cfg(test)]
1495mod permission_tests {
1496    use super::*;
1497
1498    // -- Permission enum introspection --
1499
1500    #[test]
1501    fn permission_name_is_dotted() {
1502        for perm in Permission::all_variants() {
1503            let name = perm.name();
1504            assert!(
1505                name.contains('.'),
1506                "Permission name '{}' should contain a dot",
1507                name
1508            );
1509        }
1510    }
1511
1512    #[test]
1513    fn permission_description_is_nonempty() {
1514        for perm in Permission::all_variants() {
1515            assert!(!perm.description().is_empty());
1516        }
1517    }
1518
1519    #[test]
1520    fn permission_category_roundtrip() {
1521        assert_eq!(
1522            Permission::FsRead.category(),
1523            PermissionCategory::Filesystem
1524        );
1525        assert_eq!(
1526            Permission::FsWrite.category(),
1527            PermissionCategory::Filesystem
1528        );
1529        assert_eq!(
1530            Permission::NetConnect.category(),
1531            PermissionCategory::Network
1532        );
1533        assert_eq!(
1534            Permission::NetListen.category(),
1535            PermissionCategory::Network
1536        );
1537        assert_eq!(Permission::Process.category(), PermissionCategory::System);
1538        assert_eq!(Permission::Env.category(), PermissionCategory::System);
1539        assert_eq!(Permission::Time.category(), PermissionCategory::System);
1540        assert_eq!(Permission::Random.category(), PermissionCategory::System);
1541        assert_eq!(Permission::Vfs.category(), PermissionCategory::Sandbox);
1542        assert_eq!(
1543            Permission::Deterministic.category(),
1544            PermissionCategory::Sandbox
1545        );
1546    }
1547
1548    #[test]
1549    fn permission_display() {
1550        assert_eq!(format!("{}", Permission::FsRead), "fs.read");
1551        assert_eq!(format!("{}", Permission::NetConnect), "net.connect");
1552    }
1553
1554    #[test]
1555    fn all_variants_is_exhaustive() {
1556        // If a new variant is added but not listed in all_variants,
1557        // the match in name()/description()/category() will catch it at compile time.
1558        // This test just verifies the count is sane (>= 16 known variants).
1559        assert!(Permission::all_variants().len() >= 16);
1560    }
1561
1562    // -- PermissionSet constructors --
1563
1564    #[test]
1565    fn pure_is_empty() {
1566        let set = PermissionSet::pure();
1567        assert!(set.is_empty());
1568        assert_eq!(set.len(), 0);
1569    }
1570
1571    #[test]
1572    fn readonly_contains_expected() {
1573        let set = PermissionSet::readonly();
1574        assert!(set.contains(&Permission::FsRead));
1575        assert!(set.contains(&Permission::Env));
1576        assert!(set.contains(&Permission::Time));
1577        assert!(!set.contains(&Permission::FsWrite));
1578        assert!(!set.contains(&Permission::NetConnect));
1579        assert_eq!(set.len(), 3);
1580    }
1581
1582    #[test]
1583    fn full_contains_all() {
1584        let set = PermissionSet::full();
1585        for perm in Permission::all_variants() {
1586            assert!(set.contains(perm), "full() missing {:?}", perm);
1587        }
1588    }
1589
1590    // -- Set algebra --
1591
1592    #[test]
1593    fn union_combines() {
1594        let a = PermissionSet::from([Permission::FsRead, Permission::NetConnect]);
1595        let b = PermissionSet::from([Permission::FsWrite, Permission::NetConnect]);
1596        let u = a.union(&b);
1597        assert_eq!(u.len(), 3);
1598        assert!(u.contains(&Permission::FsRead));
1599        assert!(u.contains(&Permission::FsWrite));
1600        assert!(u.contains(&Permission::NetConnect));
1601    }
1602
1603    #[test]
1604    fn intersection_narrows() {
1605        let a = PermissionSet::from([Permission::FsRead, Permission::NetConnect]);
1606        let b = PermissionSet::from([Permission::FsWrite, Permission::NetConnect]);
1607        let i = a.intersection(&b);
1608        assert_eq!(i.len(), 1);
1609        assert!(i.contains(&Permission::NetConnect));
1610    }
1611
1612    #[test]
1613    fn difference_subtracts() {
1614        let a = PermissionSet::from([Permission::FsRead, Permission::FsWrite, Permission::Env]);
1615        let b = PermissionSet::from([Permission::FsWrite]);
1616        let d = a.difference(&b);
1617        assert_eq!(d.len(), 2);
1618        assert!(d.contains(&Permission::FsRead));
1619        assert!(d.contains(&Permission::Env));
1620        assert!(!d.contains(&Permission::FsWrite));
1621    }
1622
1623    #[test]
1624    fn subset_superset() {
1625        let small = PermissionSet::from([Permission::FsRead]);
1626        let big = PermissionSet::from([Permission::FsRead, Permission::FsWrite]);
1627        assert!(small.is_subset(&big));
1628        assert!(!big.is_subset(&small));
1629        assert!(big.is_superset(&small));
1630        assert!(!small.is_superset(&big));
1631    }
1632
1633    #[test]
1634    fn insert_and_remove() {
1635        let mut set = PermissionSet::pure();
1636        assert!(set.insert(Permission::Time));
1637        assert!(!set.insert(Permission::Time)); // duplicate
1638        assert_eq!(set.len(), 1);
1639        assert!(set.remove(&Permission::Time));
1640        assert!(!set.remove(&Permission::Time)); // already removed
1641        assert!(set.is_empty());
1642    }
1643
1644    // -- Display --
1645
1646    #[test]
1647    fn permission_set_display() {
1648        let set = PermissionSet::from([Permission::FsRead, Permission::Env]);
1649        let s = format!("{}", set);
1650        // BTreeSet ordering: FsRead < Env based on Ord derive
1651        assert!(s.starts_with('{'));
1652        assert!(s.ends_with('}'));
1653        assert!(s.contains("fs.read"));
1654        assert!(s.contains("sys.env"));
1655    }
1656
1657    // -- by_category --
1658
1659    #[test]
1660    fn by_category_groups() {
1661        let set = PermissionSet::from([
1662            Permission::FsRead,
1663            Permission::FsWrite,
1664            Permission::NetConnect,
1665            Permission::Time,
1666            Permission::Vfs,
1667        ]);
1668        let cats = set.by_category();
1669        assert_eq!(cats[&PermissionCategory::Filesystem].len(), 2);
1670        assert_eq!(cats[&PermissionCategory::Network].len(), 1);
1671        assert_eq!(cats[&PermissionCategory::System].len(), 1);
1672        assert_eq!(cats[&PermissionCategory::Sandbox].len(), 1);
1673    }
1674
1675    // -- FromIterator / IntoIterator --
1676
1677    #[test]
1678    fn collect_from_iterator() {
1679        let perms = vec![Permission::FsRead, Permission::Env];
1680        let set: PermissionSet = perms.into_iter().collect();
1681        assert_eq!(set.len(), 2);
1682    }
1683
1684    #[test]
1685    fn into_iter_owned() {
1686        let set = PermissionSet::from([Permission::FsRead, Permission::Env]);
1687        let v: Vec<Permission> = set.into_iter().collect();
1688        assert_eq!(v.len(), 2);
1689    }
1690
1691    #[test]
1692    fn into_iter_ref() {
1693        let set = PermissionSet::from([Permission::FsRead, Permission::Env]);
1694        let v: Vec<&Permission> = (&set).into_iter().collect();
1695        assert_eq!(v.len(), 2);
1696    }
1697
1698    #[test]
1699    fn from_array() {
1700        let set = PermissionSet::from([Permission::Process, Permission::Random]);
1701        assert_eq!(set.len(), 2);
1702        assert!(set.contains(&Permission::Process));
1703        assert!(set.contains(&Permission::Random));
1704    }
1705
1706    // -- PermissionGrant --
1707
1708    #[test]
1709    fn unconstrained_grant() {
1710        let g = PermissionGrant::unconstrained(Permission::FsRead);
1711        assert_eq!(g.permission, Permission::FsRead);
1712        assert!(g.constraints.is_none());
1713    }
1714
1715    #[test]
1716    fn scoped_grant_with_paths() {
1717        let c = ScopeConstraints {
1718            allowed_paths: vec!["/tmp/*".into(), "/data/**".into()],
1719            ..Default::default()
1720        };
1721        let g = PermissionGrant::scoped(Permission::FsScoped, c);
1722        assert_eq!(g.permission, Permission::FsScoped);
1723        let sc = g.constraints.unwrap();
1724        assert_eq!(sc.allowed_paths.len(), 2);
1725        assert!(sc.allowed_hosts.is_empty());
1726    }
1727
1728    #[test]
1729    fn scoped_grant_with_limits() {
1730        let c = ScopeConstraints {
1731            max_memory_bytes: Some(1024 * 1024 * 64),
1732            max_time_ms: Some(5000),
1733            max_output_bytes: Some(1024 * 1024),
1734            ..Default::default()
1735        };
1736        let g = PermissionGrant::scoped(Permission::MemLimited, c);
1737        let sc = g.constraints.unwrap();
1738        assert_eq!(sc.max_memory_bytes, Some(64 * 1024 * 1024));
1739        assert_eq!(sc.max_time_ms, Some(5000));
1740    }
1741
1742    // -- PermissionCategory display --
1743
1744    #[test]
1745    fn category_display() {
1746        assert_eq!(format!("{}", PermissionCategory::Filesystem), "Filesystem");
1747        assert_eq!(format!("{}", PermissionCategory::Network), "Network");
1748        assert_eq!(format!("{}", PermissionCategory::System), "System");
1749        assert_eq!(format!("{}", PermissionCategory::Sandbox), "Sandbox");
1750    }
1751
1752    // -- Equality / ordering --
1753
1754    #[test]
1755    fn permission_set_equality() {
1756        let a = PermissionSet::from([Permission::FsRead, Permission::Env]);
1757        let b = PermissionSet::from([Permission::Env, Permission::FsRead]);
1758        assert_eq!(a, b);
1759    }
1760
1761    #[test]
1762    fn permission_ord_is_deterministic() {
1763        // BTreeSet iteration should always be in the same order
1764        let set = PermissionSet::from([Permission::Random, Permission::FsRead, Permission::Vfs]);
1765        let names: Vec<&str> = set.iter().map(|p| p.name()).collect();
1766        let mut sorted = names.clone();
1767        sorted.sort();
1768        // Since BTreeSet uses Ord, the iteration order should already be sorted
1769        // by the derived Ord (which is variant declaration order).
1770        // We just verify it's deterministic by checking two iterations match.
1771        let names2: Vec<&str> = set.iter().map(|p| p.name()).collect();
1772        assert_eq!(names, names2);
1773    }
1774}