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}