pinakes/docs/plugins.md
NotAShelf 5521488a93
docs: document plugin arcitechture
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0d19f04a6222c89b3de94b722171b3fc6a6a6964
2026-02-05 14:36:07 +03:00

9.4 KiB

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:

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:

[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

cargo new --lib my-plugin

In Cargo.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

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

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

[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:

# 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:

[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.