drasi-host-sdk 0.6.2

Host-side SDK for loading and interacting with Drasi cdylib plugins
Documentation
# Drasi Host SDK

The Drasi Host SDK provides the host-side counterpart to the [Drasi Plugin SDK](../plugin-sdk/README.md). While the Plugin SDK helps authors **build** cdylib plugins, the Host SDK helps the server **load, validate, and interact** with them at runtime.

## Overview

When the Drasi Server is built with the `dynamic-plugins` feature, it uses this crate to:

1. **Discover** plugin shared libraries (`.so`/`.dylib`/`.dll`) in a directory
2. **Validate** plugin metadata (SDK version, target triple) before initialization
3. **Initialize** plugins by calling their `drasi_plugin_init()` entry point
4. **Wire callbacks** for log routing and lifecycle event capture
5. **Wrap FFI vtables** in proxy types that implement standard DrasiLib traits (`Source`, `Reaction`, `BootstrapProvider`, `SourcePluginDescriptor`, etc.)

The result is that the rest of the server code works with normal Rust trait objects — the FFI boundary is completely hidden behind the proxies.

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│  Host (drasi-server)                                    │
│                                                         │
│   PluginLoader ──► LoadedPlugin                         │
│                      ├── SourcePluginProxy              │
│                      ├── ReactionPluginProxy            │
│                      └── BootstrapPluginProxy           │
│                            │                            │
│                    create_source()                       │
│                            │                            │
│                       SourceProxy ─── impl Source        │
│                                                         │
│   StateStoreVtableBuilder ──► StateStoreVtable ─────┐   │
│   IdentityProviderVtableBuilder ──► IdentityVtable ─┤   │
│   CallbackContext ──► log/lifecycle callbacks ───────┤   │
│                                                      │   │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ FFI boundary ─ ─ ─ ─ ─ ─ ─ ─ ─│─ │
│                                                      ▼   │
│   Plugin (.so / .dylib / .dll)                           │
│     FfiStateStoreProxy ── impl StateStoreProvider        │
│     FfiIdentityProviderProxy ── impl IdentityProvider    │
│     FfiTracingLayer ── forwards logs to host              │
└─────────────────────────────────────────────────────────┘
```

### Data flow

- **Host → Plugin**: The host passes `StateStoreVtable`, `IdentityProviderVtable`, and callback function pointers into the plugin via `FfiRuntimeContext`. The plugin wraps these in proxy types from the Plugin SDK.
- **Plugin → Host**: The plugin returns `SourceVtable`, `ReactionVtable`, and `BootstrapProviderVtable` structs. The Host SDK wraps these in proxy types that implement DrasiLib traits.

## Modules

| Module | Description |
|---|---|
| `loader` | `PluginLoader` and `PluginLoaderConfig` — discovers and loads plugins from a directory |
| `callbacks` | `CallbackContext` and `InstanceCallbackContext` — routes plugin logs and lifecycle events into DrasiLib registries |
| `proxies::source` | `SourceProxy` (wraps `SourceVtable``impl Source`) and `SourcePluginProxy` (wraps `SourcePluginVtable``impl SourcePluginDescriptor`) |
| `proxies::reaction` | `ReactionProxy` (wraps `ReactionVtable``impl Reaction`) and `ReactionPluginProxy` (wraps `ReactionPluginVtable``impl ReactionPluginDescriptor`) |
| `proxies::bootstrap_provider` | `BootstrapProviderProxy` (wraps `BootstrapProviderVtable``impl BootstrapProvider`) and `BootstrapPluginProxy` (wraps `BootstrapPluginVtable``impl BootstrapPluginDescriptor`) |
| `proxies::change_receiver` | `ChangeReceiverProxy` and `BootstrapReceiverProxy` — proxy types for data channel receivers passed to plugins |
| `state_store_bridge` | `StateStoreVtableBuilder` — wraps a host `Arc<dyn StateStoreProvider>` into a `StateStoreVtable` for plugin consumption |
| `identity_bridge` | `IdentityProviderVtableBuilder` — wraps a host `Arc<dyn IdentityProvider>` into an `IdentityProviderVtable` for plugin consumption |

## Usage

### Loading plugins

```rust
use drasi_host_sdk::{PluginLoader, PluginLoaderConfig};

let config = PluginLoaderConfig {
    plugin_dir: PathBuf::from("./plugins"),
    file_patterns: vec![
        "libdrasi_source_*".to_string(),
        "libdrasi_reaction_*".to_string(),
        "libdrasi_bootstrap_*".to_string(),
    ],
};

let loader = PluginLoader::new(config);
let plugins = loader.load_all(
    log_ctx,            // *mut c_void — host callback context
    log_callback,       // LogCallbackFn
    lifecycle_ctx,      // *mut c_void — host callback context
    lifecycle_callback, // LifecycleCallbackFn
)?;

for plugin in plugins {
    // Each LoadedPlugin contains factory proxies
    for source_factory in plugin.source_plugins {
        // source_factory implements SourcePluginDescriptor
        println!("Loaded source plugin: {}", source_factory.kind());
    }
}
```

### Creating component instances

```rust
// SourcePluginProxy implements SourcePluginDescriptor
let source: Box<dyn Source> = source_factory
    .create_source("my-source-1", &config_json, true)
    .await?;

// The returned SourceProxy implements Source — use it normally
source.start().await?;
let status = source.status().await;
```

### Injecting host services

The host can inject a `StateStoreProvider` and `IdentityProvider` into plugins:

```rust
use drasi_host_sdk::{StateStoreVtableBuilder, IdentityProviderVtableBuilder};

// Build FFI vtables from host-side trait objects
let state_store_vtable = StateStoreVtableBuilder::build(my_state_store.clone());
let identity_vtable = IdentityProviderVtableBuilder::build(my_identity_provider.clone());

// These vtables are passed to plugins via FfiRuntimeContext during initialization.
// The plugin wraps them in FfiStateStoreProxy / FfiIdentityProviderProxy.
```

### Callback wiring

```rust
use drasi_host_sdk::CallbackContext;

let ctx = Arc::new(CallbackContext {
    instance_id: "my-instance".to_string(),
    runtime_handle: tokio::runtime::Handle::current(),
    log_registry: log_registry.clone(),
    source_event_history: source_events.clone(),
    reaction_event_history: reaction_events.clone(),
});

// Pass as raw pointer to plugin loader
let raw_ctx = CallbackContext::into_raw(ctx);
```

Plugins route all `log` and `tracing` events through the FFI log callback. The `CallbackContext` dispatches these into the correct DrasiLib `ComponentLogRegistry` keyed by instance and component ID.

## OCI Registry and Signature Verification

The `OciRegistryClient` supports downloading plugins from OCI registries and optional cosign signature verification via `CosignVerifier`:

- Use `OciRegistryClient::with_verifier(config, verifier)` to enable signature verification on download
- `VerificationConfig` allows configuring trusted identities (issuer + subject pattern)
- `download_plugin()` returns `DownloadResult` containing the plugin path and an optional `VerificationResult`
- `PluginMetadata` now includes `git_commit` and `build_timestamp` fields for build provenance

## Plugin Load Sequence

1. **Discovery** — scans the plugin directory for files matching configured glob patterns (e.g., `libdrasi_source_*`), groups them by base name (stripping all known extensions: `.dylib`, `.so`, `.dll`, `.rlib`, `.rmeta`, `.d`)
2. **Extension filtering** — for each plugin group, selects only cdylib files (`.dylib`, `.so`, `.dll`). Non-cdylib Cargo artifacts (`.rlib`, `.d`, `.rmeta`) are silently ignored. If multiple cdylib extensions exist for the same plugin, an error is logged and the plugin is skipped (ambiguous)
3. **Library open**`libloading::Library::new(path)` loads the `.so`/`.dylib`/`.dll`
4. **Metadata validation** — resolves `drasi_plugin_metadata()` symbol, checks SDK version (major.minor match) and target triple
5. **Initialization** — calls `drasi_plugin_init()` which returns an `FfiPluginRegistration` containing vtable arrays and callback setters
6. **Callback wiring** — calls `set_log_callback` and `set_lifecycle_callback` with host context pointers
7. **Proxy extraction** — wraps each `SourcePluginVtable`, `ReactionPluginVtable`, and `BootstrapPluginVtable` in their corresponding proxy types
8. **Library retention** — the `Arc<Library>` is stored in each proxy to keep the shared library loaded for as long as any proxy is alive

### Plugin File Naming

Plugin shared libraries must follow the naming convention `lib<plugin_name>.<ext>` (on Unix) or `<plugin_name>.dll` (on Windows). The loader uses glob patterns to match plugin files:

- **Only cdylib extensions are loaded**: `.dylib` (macOS), `.so` (Linux), `.dll` (Windows)
- **Non-cdylib artifacts are ignored**: `.rlib`, `.rmeta`, `.d` files produced by Cargo alongside the cdylib are silently skipped
- **One cdylib per plugin**: If both `.dylib` and `.so` exist for the same plugin base name, the loader reports an ambiguity error and skips the plugin

## Integration Tests

The host-sdk includes integration tests that load real cdylib plugins and exercise the full pipeline:

```sh
# Prerequisites: build the test plugins as cdylib shared libraries
make build-dynamic-plugins

# Run the integration tests
cargo test -p drasi-host-sdk --test integration_test
```

The tests cover:
- Plugin discovery and loading
- Metadata validation (SDK version, target triple)
- Source/Reaction/Bootstrap factory invocation
- Trait method dispatch through FFI (start, stop, status, subscribe, etc.)
- Log and lifecycle callback routing
- State store and identity provider injection
- Error handling and panic safety

## License

Licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).