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}