Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I0d19f04a6222c89b3de94b722171b3fc6a6a6964
296 lines
9.4 KiB
Markdown
296 lines
9.4 KiB
Markdown
# 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.
|