Plugins & Scripting
Octopus ships a plugin SDK and a plugin runtime for extending the gateway with custom request/response logic, authentication schemes, transformations, and protocol handlers. It also embeds a Rhai scripting engine for lightweight, config-driven logic.
Implementation status. The plugin SDK, lifecycle runtime, and dynamic loader are all
implemented and tested. However, the gateway server does not yet load plugins from your
configuration — the plugins: array is parsed and validated but never wired into the request
path. Treat the plugin system as a stable foundation for authoring plugins, not as a feature you
can enable end-to-end in production today. The one scripting integration that is wired live is
convention host-resolution (see Scripting). Each page below is explicit
about what is real versus planned.
Two plugin layers01
Octopus has two distinct plugin abstractions in the codebase. Knowing which is which avoids confusion.
| Crate | Trait | Purpose |
octopus-plugin-api | Plugin (+ RequestInterceptor, ResponseInterceptor, AuthProvider, TransformPlugin, ProtocolHandler, ScriptInterceptorPlugin) | The rich SDK plugin authors target. Lifecycle hooks plus capability-specific traits. |
octopus-plugin-runtime | — | Loads, registers, and manages the lifecycle of octopus-plugin-api plugins (PluginRegistry, PluginManager, hot reload). |
octopus-plugins | Plugin (a different, simpler trait) | A separate, lighter abstraction tied to octopus_core::Middleware, plus the dynamic shared-library loader and the octopus_declare_plugin! ABI macro. |
The Writing a Plugin page targets the octopus-plugin-api SDK,
which is the richer and primary surface. The dynamic-loading ABI lives in octopus-plugins and is
covered there as well.
The core Plugin trait02
Every plugin in the SDK implements octopus_plugin_api::Plugin. The trait combines identity,
metadata, and lifecycle hooks:
#[async_trait]
pub trait Plugin: Send + Sync + fmt::Debug {
fn name(&self) -> &str; // unique
fn version(&self) -> &str; // semver
fn description(&self) -> &str { "" }
fn author(&self) -> &str { "Unknown" }
fn homepage(&self) -> Option<&str> { None }
fn dependencies(&self) -> Vec<PluginDependency> { vec![] }
async fn init(&mut self, config: serde_json::Value) -> Result<()>;
async fn start(&mut self) -> Result<()>;
async fn stop(&mut self) -> Result<()>;
async fn health_check(&self) -> Result<HealthStatus> { Ok(HealthStatus::Healthy) }
async fn reload(&mut self, config: serde_json::Value) -> Result<()> { /* stop; init; start */ }
fn metadata(&self) -> PluginMetadata { /* derived from the accessors above */ }
}A plugin gains capabilities by also implementing one or more capability traits — for example
RequestInterceptor to inspect or rewrite requests. Those are covered in
Writing a Plugin.
Lifecycle03
The runtime moves each plugin through a small state machine, enforced by PluginRegistry:
register → initialize(config) → start → (running) → stop
↑ ↓
└── reload ─┘register— adds the plugin to the registry, validates required dependencies exist, and checks for dependency cycles. State becomesRegistered.initialize(config)— callsPlugin::initwith the plugin's JSON config. State becomesInitialized. Only valid fromRegistered.start— starts dependencies first (recursively), then callsPlugin::start. State becomesStarted. Valid fromInitializedorStopped.stop— callsPlugin::stop. State becomesStopped. Valid only fromStarted.reload— callsPlugin::reload(default: stop → init → start) with new config.
Any hook returning an error transitions the plugin to Failed(reason). start_all / stop_all
iterate every registered plugin and log (rather than abort) on individual failures.
Health and hot reload
Each plugin exposes health_check() -> HealthStatus (Healthy / Degraded(msg) /
Unhealthy(msg)). The runtime also provides a HotReloadWatcher that watches a configuration
directory and calls reload when a matching *.json / *.yaml / *.toml file changes (debounced,
plugin name derived from the file stem).
Static vs dynamic plugins04
Octopus distinguishes two delivery models:
Static — the plugin is compiled into the gateway binary and registered programmatically at startup. This is the
default(plugin_type: "static") and the model the SDK examples use.Dynamic — the plugin is built as a shared library (
.so/.dylib/.dll) and loaded at runtime byoctopus-plugins'PluginLoadervialibloading. Dynamic plugins must export the C-ABI symbols emitted by theoctopus_declare_plugin!macro, and the loader rejects any library whosePLUGIN_ABI_VERSIONdoes not match (current ABI version:1).
Dynamic loading is implemented in the octopus-plugins crate, but it targets that crate's simpler
Plugin trait — not the full octopus-plugin-api SDK trait set. The two systems are not yet
unified.
The plugins configuration array05
The gateway configuration accepts a top-level plugins: array. Each entry deserializes into
PluginConfig (octopus-config):
| Field | Type | Default | Description |
name | string | — (required) | Plugin name. |
plugin_type | string | "static" | Delivery model: static or dynamic. |
enabled | bool | true | Whether the plugin is active. |
priority | i32 | 0 | Ordering hint; higher runs first. |
config | map<string, any> | {} | Arbitrary JSON passed to the plugin's init. |
plugins:
- name: my-plugin
plugin_type: static
enabled: true
priority: 100
config:
some_key: some_valueScript plugins are loaded (plugin_type: script): each enabled entry's config is deserialized
into a ScriptMiddlewareConfig and added to the request chain.
static and dynamic plugins are not yet loaded — those entries are skipped with a warning
(there are no built-in plugins, and dynamic library loading from config is not wired). The schema is
stable.
Priority ordering
priority orders the loaded plugin middleware (higher runs first). It is observable for script
plugins today; for static/dynamic types it documents intended ordering for when their loading
lands.
How plugins integrate with request handling (the honest version)06
The capability traits (
RequestInterceptor, etc.) defineintercept_request/intercept_response, which return anInterceptorAction:Continue,Return(response)(short-circuit), orAbort(error). This is the intended execution model.PluginManagerexposes accessors likeget_request_interceptors()— but these currently return empty vectors. There is no downcast/type-registry wiring that surfaces a registered plugin as a typed interceptor.The runtime request handler does not call into the plugin manager for interception.
In other words: the SDK, lifecycle management, and dynamic loader are real and tested, but the "plugin runs on every request" path is not connected. If you need request/response logic today, use middleware (many capabilities exist as first-class middleware) or scripting for the one wired use case.
Related07
Configuration — where the
plugins:array andPluginConfiglive.Concepts: Plugins — conceptual overview of the plugin model.
Middleware — the wired-up extension point for cross-cutting request logic.