Building Plugins
Plugins extend a Stratum node with extra capabilities — custom protocol handlers, integrations, observability exporters, or specialised data-plane rules — without touching the core agent. This page is the practical guide to building, signing, and shipping your own plugin. To install one you already have, see Installing Plugins.
Every plugin is a signed .cenvero-plugin package. A node will only load a plugin signed with a developer certificate that Cenvero issued to you — so you don't manage any signing infrastructure yourself; you just sign with your own key and the certificate we give you. You build and sign packages with the cnvstrpack tool.
How plugins run (sandboxed, out-of-process)
A plugin is a signed executable, not a shared library. The agent runs it as a supervised, unprivileged child process — never inside the agent and never as root:
- The agent verifies the package signature, your certificate, its scope, and that it has not been revoked _before_ it launches your executable. A bad package is rejected and the process is never started.
- Your plugin then runs as a separate process under a dedicated unprivileged user (
cenvero-str-plugin), in its own process group, with CPU / memory / open-file limits applied. It has no access to the agent's memory, private keys, configuration secrets, or root-owned files. - The only thing your plugin can do is talk to the agent over a small JSON protocol on its stdin/stdout, calling the capability-scoped host API the agent grants it (see Capabilities below). Everything is deny-by-default: a host call you weren't granted is rejected and your plugin is terminated.
The sandbox is designed so an over-scoped or misbehaving plugin is contained: it runs unprivileged, capability-gated, and is restarted or killed when it misbehaves.
What your plugin can and cannot reach
These are the constraints to design against. They are properties of the sandbox, not settings you can relax, and each one has caught someone out:
| Your plugin… | What that means for your code |
|---|---|
| has no network of its own | It cannot open sockets, call an external API, or reach the internet — it gets a private, empty network view. A plugin that phones home will fail. Anything network-facing must go through a granted host-API call. |
| cannot see other processes | It has a private process table: it cannot list, inspect, or signal the agent or anything else on the host. |
| cannot affect the host filesystem | It has a private filesystem view; changes it makes do not propagate back to the host. |
| may only make ordinary syscalls | The kernel refuses anything outside a fixed allowlist — mounting, loading modules, and similar privileged operations are denied even if something in the package tries. |
| is restarted if it dies | A crashed child is relaunched automatically on a backoff that widens with repeated failures, and resets once it stays up. Don't build your own restart loop. |
| must answer a hook promptly | Each hook invocation carries a deadline (5 seconds by default). A plugin that overruns it is killed, not merely timed out — do slow work in the background and answer quickly. |
The practical shape of a plugin follows from the first row: it is an event handler that reacts and reports, not a service that goes and fetches. If your design needs to reach something off-box, that belongs in your own service, with the plugin reporting to it via the host API.
A signed plugin is code you have chosen to trust. The signature proves who wrote the package and that it has not been altered — it is an authenticity guarantee. The sandbox above is what limits what that code can do. Treat installing a plugin as the decision it is, and install only plugins from a developer you trust.
1. Get the packer
Download cnvstrpack from the Releases page (it's listed alongside the agent). It's a single self-contained binary — no install needed.
cnvstrpack --help
2. Lay out your plugin
A plugin is a directory containing your entrypoint executable plus a manifest.json:
my-plugin/
├── manifest.json
└── entrypoint # your compiled binary (any language) — must be executable
{
"name": "my-plugin",
"version": "1.0.0",
"author": "Acme Corp",
"description": "What this plugin does",
"min_agent_version": "1.0.0",
"entrypoint": "entrypoint",
"requested_capabilities": ["log", "emit_event", "register_hook"],
"hooks": ["on:flow.new"],
"dependencies": []
}
entrypointis the executable the agent launches as your sandboxed child (defaults toentrypointif omitted). It can be written in any language; it just speaks the JSON protocol on stdin/stdout.requested_capabilitiesis the list of host-API functions your plugin needs. You only get the intersection of what you request and what your certificate's scope allows — request the minimum (see Capabilities below).hooksare the data-plane hook points your plugin registers for. You may only register hooks you declared here.
On startup your entrypoint must: (1) send a handshake advertising its name/version/API and declared capabilities, (2) handle hook_invoke messages within the per-hook deadline, and (3) make host-API calls only for capabilities it was granted.
The declarable hook points are:
| Hook | Fires on |
|---|---|
on:flow.new | A new connection is seen. |
on:dns.query | A DNS query is received. |
before:packet.forward | A packet is about to be forwarded. |
on:alert.trigger | An alert fires. |
Scope — hook delivery is not live yet. Packaging, signing, installing, launching, supervision, the host API and hook registration all work today, and a plugin that registers for a hook loads and runs normally. What is not wired yet is the last step: the agent's data-plane subsystems do not currently dispatch to registered hooks, so a handler for the points above will not be invoked on live traffic. Build and ship plugins against this interface by all means — it is stable and is what dispatch will use — but do not design a deployment that depends on a hook firing today. This page will say so plainly when that changes.
3. Become a developer + get your certificate
First, apply for developer access: in your account, choose Become a developer. Once an admin approves you, open your Developer portal at /account/developer.
Generate your signing keypair locally and keep the .key private:
cnvstrpack keygen -o mydev # writes mydev.key (secret) and mydev.pub
In the Developer portal, paste the contents of mydev.pub, choose a scope (see Scopes below), and request a certificate — it is issued instantly. From the same page, download the two certificate files you'll sign with: your developer.cert and the accompanying plugin-intermediate.cert bundle. Your private key never leaves your machine — you only ever handle your own .key; Cenvero manages all signing infrastructure on our side.
4. Pack & sign
cnvstrpack pack ./my-plugin \
--key mydev.key \
--devcert developer.cert \
--intcert plugin-intermediate.cert \
-o my-plugin-1.0.0.cenvero-plugin
cnvstrpack validates the manifest, archives the directory, signs it with your key, and embeds your developer certificate — producing a ready-to-ship my-plugin-1.0.0.cenvero-plugin.
Sanity-check it before sending:
cnvstrpack verify my-plugin-1.0.0.cenvero-plugin --pub mydev.pub
5. Ship it
Hand the .cenvero-plugin file to the node operator — they install it with one command (see Installing Plugins). The agent re-verifies the signature on load; a tampered or out-of-scope package is rejected with a clear error and never partially loaded.
Capabilities
Your plugin can only call host functions it was granted. You request capabilities in requested_capabilities; the agent grants the intersection of your request and what your certificate's scope permits — it can never exceed the certificate. Any call to an ungranted function is rejected and your plugin is terminated.
The fixed set of host-API capabilities:
| Capability | What it lets your plugin do |
|---|---|
log | Write structured log lines to the agent log. |
emit_event | Publish an event onto the agent's event bus (tagged with your plugin name). |
get_config_value | Read a small, read-only subset of non-secret config values (e.g. node id, agent version). Never exposes API tokens or keys. |
register_hook | Register for a hook point — but only one you also declared in hooks. |
kv_get / kv_put | A small key/value scratch store scoped to your plugin — you cannot read or write another plugin's keys. |
Capability availability by scope:
any(public distribution):log,emit_event,get_config_value,register_hook.license:<serial>/hardware:<id>(bound to a specific deployment): the above pluskv_get/kv_put.
Request the least you need: a smaller capability set is easier to get approved and reduces blast radius.
Scopes
When Cenvero issues your developer certificate, it carries a scope that decides which nodes will accept your plugins. Pick the one that matches how you distribute:
| Scope | Use it for |
|---|---|
any | A general-purpose plugin you distribute publicly — runs on any licensed node. |
license:<serial> | A customer- or enterprise-specific plugin — runs only on nodes activated with that license. |
hardware:<id> | A one-off plugin pinned to a single machine. Get the node's id with cenvero-str-ctl node info. |
Scope is fixed at issuance; to change it, request a new certificate.
Dependencies & licensing
- If your
manifest.jsondeclares dependencies, the agent installs in dependency order automatically and rejects circular or unsatisfiable version constraints at install time — you don't manage load order. - Plugin installs are blocked while a node's license is in the frozen state; already-running plugins keep going. See Licensing.
See also
- Installing Plugins — install, list, verify, and remove on a node.
- CLI Reference — the
plugincommand group.