docs: document plugin arcitechture
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I0d19f04a6222c89b3de94b722171b3fc6a6a6964
This commit is contained in:
parent
cc88ecc5b7
commit
5521488a93
1 changed files with 296 additions and 0 deletions
296
docs/plugins.md
Normal file
296
docs/plugins.md
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue