Octopus
1.x
Docs/Octopus/Plugins & Scripting
Open

Reading6 min
Updated31 Jul 2026
Sourcev1/plugins/index.mdx

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.

Warning

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.

CrateTraitPurpose
octopus-plugin-apiPlugin (+ RequestInterceptor, ResponseInterceptor, AuthProvider, TransformPlugin, ProtocolHandler, ScriptInterceptorPlugin)The rich SDK plugin authors target. Lifecycle hooks plus capability-specific traits.
octopus-plugin-runtimeLoads, registers, and manages the lifecycle of octopus-plugin-api plugins (PluginRegistry, PluginManager, hot reload).
octopus-pluginsPlugin (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 becomes Registered.

  • initialize(config) — calls Plugin::init with the plugin's JSON config. State becomes Initialized. Only valid from Registered.

  • start — starts dependencies first (recursively), then calls Plugin::start. State becomes Started. Valid from Initialized or Stopped.

  • stop — calls Plugin::stop. State becomes Stopped. Valid only from Started.

  • reload — calls Plugin::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 by octopus-plugins' PluginLoader via libloading. Dynamic plugins must export the C-ABI symbols emitted by the octopus_declare_plugin! macro, and the loader rejects any library whose PLUGIN_ABI_VERSION does not match (current ABI version: 1).

Note

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

FieldTypeDefaultDescription
namestring— (required)Plugin name.
plugin_typestring"static"Delivery model: static or dynamic.
enabledbooltrueWhether the plugin is active.
priorityi320Ordering hint; higher runs first.
configmap<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_value
Note

Script 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.) define intercept_request / intercept_response, which return an InterceptorAction: Continue, Return(response) (short-circuit), or Abort(error). This is the intended execution model.

  • PluginManager exposes accessors like get_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.

  • Configuration — where the plugins: array and PluginConfig live.

  • Concepts: Plugins — conceptual overview of the plugin model.

  • Middleware — the wired-up extension point for cross-cutting request logic.