Plugins

Loading and writing dylib collectors, adapters, and actions.

Plugins extend the static built-in set of collectors, adapters, and actions without rebuilding the agent. Each plugin is a Rust crate compiled to a cdylib and loaded at startup by nanook's plugin host.

Loading plugins

Tell the agent where to look:

[plugins]
dirs = ["/usr/lib/nanook", "./plugins"]
allowed = [
  "sha256:d3983dbf3dbc30aa4838bb16c38786d03af5faf2643db2a0cdab7444697ff28b",
]
verify = true
strict = true
FieldWhat
dirsDirectories searched for .so / .dylib / .dll files
allowedWhitelist of accepted digests, each prefixed with sha256: (64 hex chars) or sha512: (128 hex chars)
verifyIf true, refuse to load any file whose digest isn't in allowed. Default true.
strictIf true, refuse plugin dirs not owned by the running uid or that are world-writable

Disabling verify lets the host load any cdylib found in dirs. Only safe when the dirs are entirely under your control.

Signed plugins

The hash allowlist works fine for tiny fleets but doesn't scale: every rebuild changes the digest, and every operator has to copy a fresh sha256 into config. Signed manifests fix that. The publisher signs the cdylib's hash once with a release key, and operators trust the key, not the bytes.

The full operator and publisher story (trust list config, nanook keygen --kind signing, nanook plugins sign, nanook plugins verify, nanook plugins trust *, packaging modes, threat model, platform notes) lives on the Trust page. The same machinery covers the agent binary itself via [self.signature] and nanook self {sign,verify,trust}.

Quick summary: when [plugins.signature].require = true, only cdylibs carrying a manifest signed by one of [plugins.signature].signers will load. Operators add a publisher's key once with nanook plugins trust add <line-or-path> and forget it.

Allowlist + signatures

[plugins].allowed (hash whitelist) and [plugins.signature] (publisher trust) are orthogonal. They are not redundant by accident: each loaded cdylib must clear every gate that is configured.

  1. Hash in [plugins.signature].revoked rejects, unconditionally.
  2. If signature.require = true, the manifest must verify against one of signers.
  3. If [plugins].allowed is non-empty, the hash must appear in it.
  4. Otherwise the load proceeds.

A signed manifest already carries the artifact's sha256, so once you trust the publisher you usually leave allowed empty. Keep both only if you want a locally-curated second gate, so a compromise of the publisher's release key does not by itself admit a fresh build.

Discovering plugins

nanook plugins ls          # builtins + plugins
nanook plugins inspect cpu # full doc, options, metrics, file path, ABI status
nanook plugins where       # effective search paths
nanook plugins check       # validate digests and ABI without loading
nanook plugins doctor      # ls + check + diff against config (default op)

ABI versioning

Each plugin stamps the nanook-plugin ABI version and the plugin kind into its cdylib via declare_plugin!. The host reads those stamps statically out of the symbol table, with no dlopen involved: dlopen would run the cdylib's global constructors before the host could verify its hash, which means a stray plugin could execute code just by sitting in a search dir. Static reads avoid that.

ABI mismatches surface as a clear error rather than a runtime crash. When you bump the host, plugins built against an older nanook-plugin need a rebuild. nanook plugins doctor tells you which.

Authoring a plugin

Scaffold

nanook plugins new my_collector --kind collector

This creates a crate with the right Cargo.toml, the trait impl stubs, and a build profile suitable for cdylib.

Flags:

FlagWhat
-k, --kindcollector, adapter, or action
--pathParent directory to scaffold into
--nanook-pathUse a local checkout of nanook-plugin
--nanook-gitUse a git URL for nanook-plugin
--nanook-versionPin a crates.io version

Three kinds, three traits

KindTraitWhat it does
collectorCollectorPluginPolls and emits metrics
adapterAdapterPluginReceives metrics, ships them somewhere
actionActionPluginReceives alert payloads, takes action

Doc surface

Every plugin publishes a Doc struct: name, description, options, metrics, examples markdown. The host parses the examples markdown for nanook doc <name> and for this site's Reference section. Author docs once, get terminal output and HTML for free.

use nanook_doc::{Doc, Opt, MetricDoc};
use nanook_core::Unit;

pub static DOC: Doc = Doc::new("my_collector", "What it does in one line.")
    .with_opts(&[
        Opt::new("threshold", "trigger threshold").with_default("80"),
    ])
    .with_metrics(&[
        MetricDoc::new("my.value", "the thing").with_unit(Unit::Percent),
    ])
    .with_examples_md(include_str!("../docs/my_collector.md"));

docs/my_collector.md follows the ## Title + fenced code block convention. See any built-in collector's docs/ folder for a model.

Building

cargo build --release -p my_collector

The output .so / .dylib / .dll lives at target/release/. Drop it into one of the host's [plugins].dirs, add its digest to [plugins].allowed, and run nanook plugins check before going live.

Hot reload

nanook ctl reload picks up new plugin files and re-validates digests. Plugins that disappear get unloaded.

Reference

Built-in collectors, adapters, and actions ship with their own Doc, which feeds the reference pages. The same surface gets generated automatically for every plugin you write.