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