Plugin System
EnterpriseHow NetStacks plugins work: containerized services, manifests, capabilities, the install/enable lifecycle, the admin API, and the plugin SDK.
Overview
The NetStacks Plugin System lets you extend the platform with self-contained services that run as Docker containers alongside the controller. Each plugin ships a manifest.json that declares its container image, the host capabilities it needs, its database migrations, sidebar panels, and a settings schema. The controller manages the container lifecycle and reverse-proxies API traffic to it.
NetStacks ships three first-party plugins, all included with the Enterprise tier:
- Alert Ingestion & Pipeline (manifest name
alerts) -- multi-source alert ingestion (webhook, SNMP traps, optional Kafka) with deduplication and rule-based routing. - Incidents & ITSM (manifest name
incidents) -- incident lifecycle management with ServiceNow/Jira sync and auto-creation from alerts. - Profiling Agents (manifest name
profiling-agents) -- persistent AI agents that own devices over gNMI telemetry with behavioral profiling and an inter-agent mesh.
Plugins are installed in the disabled state and must be explicitly enabled. When enabled, the controller starts the plugin's container, applies its database migrations, and begins proxying requests to it.
How It Works
Plugins are containers
A plugin is a container image plus a manifest. When you enable a plugin, the controller launches a container named netstacks-plugin-{name} from the image in the manifest and applies CPU and memory limits from the resources block. The container exposes an HTTP service on the manifest's port (8080 for all first-party plugins).
Capability model
Instead of fine-grained UI/event permissions, plugins request a small set of coarse host capabilities via capabilities_required in the manifest. These grant the plugin access to controller-mediated resources:
read_devices-- read the device inventory.read_credentials-- read stored device credentials.invoke_llm-- call the platform LLM gateway (for AI triage, profiling, etc.).trigger_agent-- launch automation agents.execute_gnmi-- open gNMI sessions to devices.
For reference, the first-party plugins declare: alerts = read_devices, invoke_llm, trigger_agent; incidents = read_devices, invoke_llm; profiling-agents = read_devices, read_credentials, invoke_llm, execute_gnmi.
The reverse proxy
A plugin's own HTTP API is reached through the controller proxy at /api/plugins/{name}/*. The controller only proxies to plugins whose database state is enabled; requests to a disabled or missing plugin return a not-found error. This is separate from the management API under /api/admin/plugins (described below).
Terminal panels
A plugin can surface read-only data tables in the admin sidebar by declaring terminal_panels in its manifest. Each panel has an id, label, icon, a data_endpoint (served by the plugin and reached via the proxy), an optional refresh_interval_seconds, and a list of columns. The alerts plugin, for example, declares an "Alerts" panel and a "Pipeline Rules" panel.
Database migrations
Plugins own their own tables. The migrations_dir field (typically migrations) points at SQL migrations the controller applies when the plugin is enabled, keeping plugin schema isolated from the core database.
The Plugin Manifest
Every plugin includes a manifest.json at its root. The example below is the real first-party alerts manifest (abridged for the settings block). Note the actual key names: name, display_name, image, port, capabilities_required, resources, health_check, migrations_dir, terminal_panels, and settings_schema.
{
"name": "alerts",
"version": "1.0.0",
"display_name": "Alert Ingestion & Pipeline",
"description": "Multi-source alert ingestion with deduplication and configurable notification pipeline",
"image": "netstacks-plugin-alerts:1.0.0",
"port": 8080,
"capabilities_required": ["read_devices", "invoke_llm", "trigger_agent"],
"resources": {
"cpu_limit": 0.5,
"memory_limit": 268435456
},
"health_check": {
"command": "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/health')\"",
"interval_seconds": 30,
"timeout_seconds": 10,
"retries": 3,
"start_period_seconds": 15
},
"migrations_dir": "migrations",
"terminal_panels": [
{
"id": "alert-list",
"label": "Alerts",
"icon": "Bell",
"data_endpoint": "/admin/alerts",
"refresh_interval_seconds": 30,
"columns": [
{ "key": "severity", "label": "Severity" },
{ "key": "source", "label": "Source" },
{ "key": "summary", "label": "Summary" },
{ "key": "state", "label": "State" }
]
}
],
"settings_schema": {
"type": "object",
"properties": {
"default_severity": { "type": "string", "default": "warning", "enum": ["critical", "warning", "info"] },
"deduplication_window_seconds": { "type": "integer", "default": 300, "minimum": 60 }
}
}
}The health_check.command runs inside the container on the interval you specify. The controller uses container health to decide whether the plugin is healthy, and surfaces this through the health endpoint. Keep the command lightweight -- a single request to the plugin's /health route is typical.
Lifecycle & Admin API
Lifecycle states
The controller tracks each plugin's state in the plugins table. Installing a plugin (whether bundled or uploaded) inserts a record in the disabled state; enabling starts the container and moves it to enabled; disabling stops the container and returns it to disabled. If the container fails to start, the state becomes error with a stored message.
| State | Meaning | Action to leave |
|---|---|---|
disabled | Installed; container not running. Settings preserved. | Enable → enabled, or Uninstall to remove. |
enabled | Container running; the proxy routes traffic to it. | Disable → disabled. |
error | Container failed to start; an error message is stored. | Fix the cause, then Enable to retry. |
Management endpoints
Plugin management lives under /api/admin/plugins (admin authentication required). These manage the lifecycle and settings -- they are distinct from a plugin's own proxied API at /api/plugins/{name}/*.
# List installed plugins
GET /api/admin/plugins
# Inspect a single plugin
GET /api/admin/plugins/alerts
# Enable / disable (start / stop the container)
POST /api/admin/plugins/alerts/enable
POST /api/admin/plugins/alerts/disable
# Health
GET /api/admin/plugins/alerts/health
# Settings (read / write)
GET /api/admin/plugins/alerts/settings
PUT /api/admin/plugins/alerts/settings
# Uninstall (remove the plugin record)
DELETE /api/admin/plugins/alerts
# Install by uploading a package
POST /api/admin/plugins/upload # multipart/form-data, field "file" = a .nspkg archiveDisabling stops the container but keeps the plugin record and its settings, so you can re-enable later without reconfiguring. Uninstalling removes the plugin record. Data the plugin wrote to its own tables follows that plugin's migration/retention behavior -- review the plugin's own docs before uninstalling in production.
Settings
A plugin's configurable options come from settings_schema in its manifest -- a JSON Schema object whose property keys use a flat, dotted naming convention (for example kafka.enabled or snmp.trap_port). The admin UI renders a form from this schema, and values are read/written through the settings endpoint.
Example: alerts settings schema (excerpt)
{
"type": "object",
"properties": {
"default_severity": { "type": "string", "default": "warning", "enum": ["critical", "warning", "info"] },
"deduplication_window_seconds": { "type": "integer", "default": 300, "minimum": 60 },
"kafka.enabled": { "type": "boolean", "default": false, "description": "Enable Kafka alert consumer" },
"kafka.bootstrap_servers": { "type": "string", "default": "kafka:9092" },
"kafka.topics": { "type": "string", "default": "network-alerts", "description": "Comma-separated topics" },
"kafka.consumer_group": { "type": "string", "default": "netstacks-alerts" },
"kafka.sasl_password": { "type": "string", "default": "", "sensitive": true },
"snmp.trap_port": { "type": "integer", "default": 162, "description": "SNMP trap listener port" },
"snmp.webhook_url": { "type": "string", "default": "http://netstacks-plugin-alerts:8080/ingest/webhook" },
"snmp.community_filter": { "type": "string", "default": "", "description": "Allowed SNMP communities (JSON array)" }
}
}Fields marked "sensitive": true (such as kafka.sasl_password) are treated as secrets by the UI.
Updating settings via the API
curl -X PUT https://netstacks.example.com/api/admin/plugins/alerts/settings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"default_severity": "critical",
"deduplication_window_seconds": 600,
"kafka.enabled": true,
"kafka.bootstrap_servers": "kafka-1:9092,kafka-2:9092",
"kafka.topics": "network-alerts,fabric-events",
"snmp.trap_port": 162
}'The alerts plugin ingests events on its own service routes (for example POST /ingest/webhook) and can also consume from Kafka. Matching alerts are processed by routing rules whose action is one of route_to_agent, fast_path, or suppress. See the Alert Pipeline page for the full ingestion and routing model.
Developing a Plugin
NetStacks provides a plugin SDK (available for Rust, Python, and TypeScript) and a scaffolding CLI named netstacks-plugin. The CLI creates a project from a template, validates the structure and manifest, and packages the plugin into a .nspkg archive ready to upload.
Scaffold, validate, package
# Create a new plugin from a template (python is the default/supported template)
netstacks-plugin new my-plugin --template python --output-dir ./plugins
# Validate structure and manifest.json
netstacks-plugin validate ./plugins/my-plugin
# Package into a .nspkg file for upload
netstacks-plugin package ./plugins/my-plugin --output ./my-plugin.nspkgA .nspkg is a ZIP archive containing manifest.json and your plugin's files. Upload it via POST /api/admin/plugins/upload (multipart, field file) or through the admin UI. The controller validates the manifest, installs the plugin in the disabled state, then you enable it.
What a plugin must provide
- A
manifest.jsonwith a uniquename, animage, aport, and thecapabilities_requiredit needs. - An HTTP service that responds on
/healthso the container health check passes. - Optionally:
migrations_dirfor its tables,terminal_panelsfor sidebar views, and asettings_schemafor configuration.
Q&A
- Q: Which plugins ship with NetStacks?
- A: Three first-party plugins, all Enterprise-tier: Alert Ingestion & Pipeline (
alerts), Incidents & ITSM (incidents), and Profiling Agents (profiling-agents). They are installed in thedisabledstate and enabled from Administration → Plugins.
- Q: What is the difference between the admin API and the proxy?
- A:
/api/admin/plugins/{name}/...manages the plugin (enable, disable, health, settings, uninstall). A plugin's own API is reached through the reverse proxy at/api/plugins/{name}/*, which only routes to plugins whose state isenabled.
- Q: What capabilities can a plugin request?
- A: The coarse host grants
read_devices,read_credentials,invoke_llm,trigger_agent, andexecute_gnmi, declared incapabilities_required.
- Q: How are plugin settings configured?
- A: From the JSON Schema in
settings_schema(flat dotted keys likekafka.enabled). The admin UI renders a form, or you can write values withPUT /api/admin/plugins/{name}/settings.
- Q: Can I write my own plugin?
- A: Yes. Use the plugin SDK (Rust, Python, or TypeScript) and the
netstacks-pluginscaffold CLI to create, validate, and package a plugin into a.nspkg, then upload it viaPOST /api/admin/plugins/upload.
- Q: What happens to a plugin's data when I disable it?
- A: Disabling stops the container but keeps the plugin record and its settings. Data in the plugin's own tables remains in the database; re-enabling resumes with the previous configuration.
Troubleshooting
Plugin stuck in error state
- Call
GET /api/admin/plugins/{name}-- theerrorstate records a message explaining why the container failed to start. - Verify the manifest
imageexists and is pullable, and that the declaredportmatches what the container listens on.
Container starts but is unhealthy
- Confirm the plugin serves
/healthon its port; the manifesthealth_check.commandmust succeed inside the container. - Increase
start_period_secondsif the plugin needs more warm-up time before the first health probe. - Check resource limits (
cpu_limit,memory_limit) -- an OOM-killed container will keep restarting.
Proxy returns "not found or not enabled"
- The proxy only routes to plugins whose
stateisenabled. Enable the plugin first viaPOST /api/admin/plugins/{name}/enable.
Settings rejected on save
- Values are validated against
settings_schema. Use the exact flat dotted keys and types from the manifest (for examplesnmp.trap_portas an integer, not a string).
Related Features
- Alert Pipeline -- the
alertsplugin: ingestion sources and routing rules. - Incidents & ITSM -- the
incidentsplugin: ServiceNow/Jira sync and auto-creation. - Stacks & Deployments -- how NetStacks services and plugins are deployed.
- System Settings -- global settings that affect the controller and plugins.
- Roles & Permissions -- who can manage plugins in Administration.
The third first-party plugin, Profiling Agents (profiling-agents), provides persistent AI agents over gNMI telemetry and is managed through the same admin API and lifecycle described above.