This document defines the contract between plugins and the Execution Engine for ABI v2.0.0. It specifies:
- Required plugin entry points (FFI functions)
- Context structure and services
- Error codes and handling
- Lifecycle events
- Version compatibility
- Current Version: v2.0.0
- Stability: Stable (no breaking changes planned)
- Release Date: 2024-02-20
- License: MIT OR Apache-2.0
Every plugin built for Execution Engine ABI v2 must export the following FFI functions. All functions must use #[no_mangle] and extern "C" calling convention.
Called when the plugin is loaded by the execution engine.
Responsibilities:
- Initialize plugin state
- Validate the provided context
- Register services with the service registry
- Start background tasks if needed
Parameters:
context: Pointer to the plugin context (valid for the plugin's lifetime)
Returns:
PluginResultV2::Success(0) if initialization succeededPluginResultV2::Error(-1) for initialization failuresPluginResultV2::InvalidRequest(-2) if context is invalid
Example:
#[no_mangle]
pub extern "C" fn plugin_init_v2(context: *const PluginContextV2) -> PluginResultV2 {
if context.is_null() {
return PluginResultV2::InvalidRequest;
}
unsafe {
// Store context for later use
PLUGIN_CONTEXT = Some(context);
// Initialize plugin state
if let Err(e) = initialize_plugin_state() {
return PluginResultV2::Error;
}
}
PluginResultV2::Success
}Called when the plugin is unloaded or the engine is shutting down.
Responsibilities:
- Gracefully shut down background tasks
- Release resources
- Persist state if necessary
- Close connections
Parameters:
context: Pointer to the plugin context
Returns:
PluginResultV2::Successif shutdown succeededPluginResultV2::Errorif shutdown failed
Example:
#[no_mangle]
pub extern "C" fn plugin_shutdown_v2(context: *const PluginContextV2) -> PluginResultV2 {
unsafe {
PLUGIN_CONTEXT = None;
}
// Gracefully shut down
PluginResultV2::Success
}Returns static information about the plugin.
Responsibilities:
- Provide plugin metadata
- Declare capabilities and permissions
- Specify version and dependencies
- Define configuration schema
Returns:
- Pointer to a static
PluginInfoV2structure - Must never return NULL
Stability: This function is called frequently and must be extremely fast (typically just returning a static pointer).
Example:
static PLUGIN_INFO: PluginInfoV2 = PluginInfoV2 {
name: "my-plugin\0".as_ptr() as *const c_char,
version: "1.0.0\0".as_ptr() as *const c_char,
// ... other fields ...
};
#[no_mangle]
pub extern "C" fn plugin_get_info_v2() -> *const PluginInfoV2 {
&PLUGIN_INFO
}4. plugin_handle_request_v2(request: *mut PluginRequestV2, response: *mut PluginResponseV2) -> PluginResultV2
Handles service requests from other plugins or the engine.
Responsibilities:
- Parse the request
- Invoke appropriate handler
- Populate the response
- Handle errors gracefully
Parameters:
request: Mutable pointer to the request (plugin may modify for scratch space)response: Mutable pointer to the response (must be filled with results)
Returns:
PluginResultV2::Successif request was handledPluginResultV2::Errorfor processing errorsPluginResultV2::InvalidRequestfor malformed requestsPluginResultV2::Timeoutif processing took too long
Example:
#[no_mangle]
pub extern "C" fn plugin_handle_request_v2(
request: *mut PluginRequestV2,
response: *mut PluginResponseV2,
) -> PluginResultV2 {
if request.is_null() || response.is_null() {
return PluginResultV2::InvalidRequest;
}
unsafe {
// Handle request and populate response
// ...
}
PluginResultV2::Success
}Called before the plugin is reloaded to allow state migration.
Responsibilities:
- Prepare state for migration to new version
- Serialize state to persistent storage if needed
- Validate that hot reload is safe
Parameters:
context: Pointer to the plugin context
Returns:
PluginResultV2::Successif hot reload is safePluginResultV2::Errorif hot reload cannot proceedPluginResultV2::NotImplementedif hot reload is not supported
Example:
#[no_mangle]
pub extern "C" fn plugin_prepare_hot_reload_v2(context: *const PluginContextV2) -> PluginResultV2 {
// Save any persistent state before reload
PluginResultV2::Success
}6. plugin_init_from_state_v2(context: *const PluginContextV2, state: *const c_char) -> PluginResultV2
Called after hot reload to restore the plugin state.
Responsibilities:
- Restore plugin state from serialized data
- Validate restored state
- Resume operations
Parameters:
context: Pointer to the plugin contextstate: C-string containing serialized state
Returns:
PluginResultV2::Successif state was restoredPluginResultV2::Errorif state is corrupt or incompatiblePluginResultV2::NotImplementedif hot reload is not supported
Example:
#[no_mangle]
pub extern "C" fn plugin_init_from_state_v2(
context: *const PluginContextV2,
state: *const c_char,
) -> PluginResultV2 {
if state.is_null() {
return PluginResultV2::InvalidRequest;
}
// Restore state
PluginResultV2::Success
}Plugins may implement these entry points for enhanced functionality:
Returns a JSON Schema describing the plugin's configuration.
Returns:
- Pointer to a C-string containing valid JSON Schema
- Must never return NULL (return empty schema
{}if no config needed)
Returns MCP (Model Context Protocol) tool definitions for AI agents.
Returns:
- Array of MCP tool schemas
- Each tool defines input/output parameters and capabilities
The PluginContextV2 structure provides access to engine services:
struct PluginContextV2 {
// Service discovery
ServiceRegistry* service_registry;
// Logging
Logger* logger;
// Configuration
ConfigManager* config_manager;
// Secrets
SecretsProvider* secrets_provider;
// RPC/networking
RPCClient* rpc_client;
// Job queue
JobQueue* job_queue;
// Permissions
PermissionsManager* permissions_manager;
// Plugin ID and metadata
const char* plugin_id;
const char* plugin_name;
const char* plugin_version;
};Logging Example:
unsafe {
if let Some(context) = PLUGIN_CONTEXT {
let logger = (*context).logger;
let msg = CString::new("Plugin initialized").unwrap();
(logger.log)(context, PluginLogLevel::Info, msg.as_ptr());
}
}Service Registry Example:
unsafe {
if let Some(context) = PLUGIN_CONTEXT {
let registry = (*context).service_registry;
let service_name = CString::new("my-service").unwrap();
(registry.register)(context, service_name.as_ptr(), handler_fn);
}
}Plugins should return appropriate PluginResultV2 codes:
| Value | Name | Meaning |
|---|---|---|
| 0 | Success | Operation completed successfully |
| -1 | Error | Generic error (details in logs) |
| -2 | InvalidRequest | Request was malformed or invalid |
| -3 | ServiceUnavailable | Required service not available |
| -4 | PermissionDenied | Insufficient permissions for operation |
| -5 | NotImplemented | Feature not implemented by plugin |
| -6 | Timeout | Operation exceeded time limit |
| -7 | ResourceExhausted | Out of memory or other resources |
Plugins built for ABI v2.0.0 will work with future v2.x releases that:
- Add new optional services to the context
- Add new optional entry points
- Maintain existing service signatures
Plugins built for older ABI versions:
- Will NOT work with ABI v2.x (breaking change requires major version bump)
- Must be recompiled for the target ABI version
Plugins can check the ABI version at runtime:
unsafe {
if let Some(context) = PLUGIN_CONTEXT {
// Use version information to adjust behavior
let version = CStr::from_ptr((*context).plugin_version);
}
}- Engine loads plugin binary using dynamic linking
- Engine calls
plugin_init_v2()to initialize - Engine caches result of
plugin_get_info_v2() - Plugin is ready to handle requests
- Engine stops sending new requests
- Engine waits for in-flight requests to complete (with timeout)
- Engine calls
plugin_shutdown_v2() - Engine unloads plugin binary
- Engine calls
plugin_prepare_hot_reload_v2()on old version - Engine unloads old version
- Engine loads new version binary
- Engine calls
plugin_init_from_state_v2()with saved state - Engine switches requests to new version
- Strings from Engine: Plugin must not free
- Strings from Plugin: Engine will free using the plugin's allocator
- Structures: Pass by pointer (ownership determined by convention)
- Buffers: Plugin is responsible for allocated buffers until returned to engine
Plugins should use standard C malloc/free:
extern "C" {
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
}The engine will use the same allocator.
All plugins MUST validate:
- Pointer validity before dereference
- String null-termination
- Buffer bounds
- Data type invariants
Plugins should respect:
- CPU usage limits (will be enforced by scheduler)
- Memory limits (will trigger OOM killer if exceeded)
- Network bandwidth limits
- Concurrent request limits
FFI boundaries require unsafe blocks. Minimize unsafe code and document:
- Why the code is safe
- What invariants must hold
- Caller responsibilities
#[test]
fn test_plugin_exports() {
// Verify all required symbols are exported
let lib = dlopen("target/release/my_plugin.so", RTLD_NOW).unwrap();
let init: extern "C" fn(*const PluginContextV2) -> PluginResultV2 =
dlsym(&lib, "plugin_init_v2").unwrap().transmute();
let info: extern "C" fn() -> *const PluginInfoV2 =
dlsym(&lib, "plugin_get_info_v2").unwrap().transmute();
// ... more checks
}Use the provided test framework in the core crate to test your plugin with a live execution engine.