# Plugin System Pinakes is very powerful on its own, but with a goal as ambitious as "to be the last media management system you will ever need" I recognize the need for a plugin system; not everything belongs in the core of Pinakes. Thus, Pinakes supports WASM-based plugins for extending media type support, metadata extraction, thumbnail generation, search, event handling, and theming. Plugins run in a sandboxed wasmtime runtime with capability-based security. ## How It Works A plugin is a directory containing: ```plaintext my-plugin/ plugin.toml Manifest declaring name, version, capabilities my_plugin.wasm Compiled WASM binary (wasm32-wasi target) ``` The server discovers plugins in configured directories, validates their manifests, checks capabilities against the security policy, compiles the WASM module, and registers the plugin. Plugins are then invoked through exported functions when the system needs their functionality. ## Plugin Kinds A plugin can implement one or more of these roles: | Kind | Purpose | | --------------------- | -------------------------------------------------- | | `media_type` | Register new file formats (extensions, MIME types) | | `metadata_extractor` | Extract metadata from files of specific types | | `thumbnail_generator` | Generate thumbnails for specific media types | | `search_backend` | Provide alternative search implementations | | `event_handler` | React to system events (imports, deletes, etc.) | | `theme_provider` | Supply UI themes | | `general` | General-purpose (uses host functions freely) | Declare kinds in `plugin.toml`: ```toml [plugin] name = "heif-support" version = "1.0.0" api_version = "1.0" kind = ["media_type", "metadata_extractor", "thumbnail_generator"] ``` ## Writing a Plugin ### 1. Create a Rust library ```bash cargo new --lib my-plugin ``` In `Cargo.toml`: ```toml [lib] crate-type = ["cdylib"] [dependencies] pinakes-plugin-api = { path = "../path/to/pinakes/crates/pinakes-plugin-api" } serde = { version = "1", features = ["derive"] } serde_json = "1" ``` ### 2. Implement the Plugin trait ```rust use pinakes_plugin_api::{Plugin, PluginContext, PluginMetadata}; pub struct MyPlugin; impl Plugin for MyPlugin { fn metadata(&self) -> PluginMetadata { PluginMetadata { name: "my-plugin".into(), version: "1.0.0".into(), description: "My custom plugin".into(), author: "Your Name".into(), } } fn initialize(&mut self, _ctx: &PluginContext) -> Result<(), String> { Ok(()) } fn shutdown(&mut self) -> Result<(), String> { Ok(()) } } ``` Then implement whichever additional traits your plugin needs (`MediaTypeProvider`, `MetadataExtractor`, `ThumbnailGenerator`, etc.). ### 3. Build to WASM ```bash rustup target add wasm32-wasi cargo build --target wasm32-wasi --release # Optional: strip debug info to reduce binary size wasm-tools strip target/wasm32-wasi/release/my_plugin.wasm -o target/wasm32-wasi/release/my_plugin.wasm ``` ### 4. Create the manifest ```toml [plugin] name = "my-plugin" version = "1.0.0" api_version = "1.0" description = "A custom plugin" author = "Your Name" kind = ["metadata_extractor"] [plugin.binary] wasm = "my_plugin.wasm" [capabilities] network = false [capabilities.filesystem] read = ["/media"] write = [] [capabilities.resources] max_memory_bytes = 67108864 max_cpu_time_ms = 5000 ``` ### 5. Install Place the plugin directory in one of the configured plugin directories, or use the API: ```bash # Install from local path curl -X POST http://localhost:3000/api/v1/plugins/install \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"source": "/path/to/my-plugin"}' # Install from HTTPS URL (downloads and extracts tar.gz) curl -X POST http://localhost:3000/api/v1/plugins/install \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"source": "https://example.com/my-plugin-1.0.0.tar.gz"}' ``` ## Host Functions Plugins can call these host functions from within WASM: ### `host_log(level, ptr, len)` Log a message. Levels: 0=error, 1=warn, 2=info, 3+=debug. ### `host_read_file(path_ptr, path_len) -> i32` Read a file into the exchange buffer. Returns file size on success, `-1` on IO error, `-2` if the path is outside the plugin's allowed read paths. ### `host_write_file(path_ptr, path_len, data_ptr, data_len) -> i32` Write data to a file. Returns `0` on success, `-1` on IO error, `-2` if the path is outside the plugin's allowed write paths. ### `host_http_request(url_ptr, url_len) -> i32` Make an HTTP GET request. Returns response size on success (body in exchange buffer), `-1` on error, `-2` if network access is disabled. ### `host_get_config(key_ptr, key_len) -> i32` Read a plugin configuration value. Returns JSON value length on success (in exchange buffer), `-1` if key not found. ### `host_get_buffer(dest_ptr, dest_len) -> i32` Copy the exchange buffer into WASM memory. Returns bytes copied. Use this after `host_read_file`, `host_http_request`, or `host_get_config` to retrieve data. ## Security Model ### Capabilities Every plugin declares what it needs in `plugin.toml`. At load time, the `CapabilityEnforcer` validates these against the system policy: - **Filesystem**: Read and write paths must be within allowed directories. Requests for paths outside the allowlist are rejected at load time (the plugin won't load) and at runtime (the host function returns `-2`). - **Network**: Must be explicitly enabled. Even when enabled, each `host_http_request` call checks the flag. Domain whitelisting is supported in the capability model but not yet enforced at the host function level. - **Resources**: Maximum memory (default 512MB) and CPU time (default 60s) are enforced via wasmtime's fuel metering system. Each function invocation gets a fresh `Store` with a fuel budget proportional to the configured CPU time limit. ### Isolation - Each `call_function` invocation creates a fresh wasmtime `Store`, so plugins cannot retain state between calls via the WASM runtime (they can use the filesystem or exchange buffer for persistence). - WASM's linear memory model prevents plugins from accessing host memory. - The wasmtime stack is limited to 1MB. ### Unsigned Plugins The `allow_unsigned` configuration option (default `true` in development) permits loading plugins without signature verification. For production deployments, set this to `false` and sign your plugins. Note: signature verification is defined in the configuration but not yet implemented in the loader. This is a known gap. ## Plugin API Endpoints All plugin endpoints require Admin role. | Method | Path | Description | | -------- | ----------------------------- | ------------------------ | | `GET` | `/api/v1/plugins` | List all plugins | | `GET` | `/api/v1/plugins/:id` | Get plugin details | | `POST` | `/api/v1/plugins/install` | Install from path or URL | | `DELETE` | `/api/v1/plugins/:id` | Uninstall a plugin | | `POST` | `/api/v1/plugins/:id/enable` | Enable a disabled plugin | | `POST` | `/api/v1/plugins/:id/disable` | Disable a plugin | | `POST` | `/api/v1/plugins/:id/reload` | Hot-reload a plugin | ## Plugin Lifecycle 1. **Discovery**: On startup, the plugin manager walks configured plugin directories looking for `plugin.toml` files. 2. **Validation**: The manifest is parsed, and capabilities are checked against the security policy. If validation fails, the plugin is skipped with a warning. 3. **Loading**: The WASM binary is compiled via wasmtime. The `initialize` export is called if present. 4. **Registration**: The plugin is added to the in-memory registry and the database. 5. **Invocation**: When the system needs plugin functionality (e.g., extracting metadata from an unknown file type), it calls the appropriate exported function via `WasmPlugin::call_function()`. 6. **Shutdown**: On server shutdown or plugin disable, the `shutdown` export is called if present. 7. **Hot-reload**: The `/plugins/:id/reload` endpoint reloads the WASM binary from disk without restarting the server. ## Configuration In `pinakes.toml`: ```toml [plugins] enabled = true plugin_dirs = ["/etc/pinakes/plugins", "~/.local/share/pinakes/plugins"] allow_unsigned = false [plugins.security] allow_network_default = false allowed_read_paths = ["/media", "/tmp/pinakes"] allowed_write_paths = ["/tmp/pinakes/plugin-data"] ``` ## Known Limitations - **Plugin signing**: The `allow_unsigned` flag is parsed but signature verification is not implemented. All plugins load regardless of this setting. - **Dependency resolution**: The manifest supports a `dependencies` field, but dependency ordering and conflict detection are not implemented. - **Domain whitelisting**: `allowed_domains` is part of the capability model but `host_http_request` does not check it. - **Environment variables**: The `EnvironmentCapability` is defined in the API but no corresponding host function exists. - **Memory limits**: CPU time is enforced via fuel metering. Linear memory limits depend on the WASM module's own declarations; the host does not currently impose an explicit cap beyond wasmtime's defaults.