NetStacksNetStacks

Plugin System

Enterprise

How 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.

Three first-party plugins

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.

manifest.jsonjson
{
  "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 }
    }
  }
}
Health checks

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.

StateMeaningAction to leave
disabledInstalled; container not running. Settings preserved.Enable → enabled, or Uninstall to remove.
enabledContainer running; the proxy routes traffic to it.Disable → disabled.
errorContainer 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}/*.

plugin-admin-api.httphttp
# 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 archive
Disable vs. uninstall

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

settings_schema.jsonjson
{
  "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

update-settings.shbash
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
      }'
Where ingestion and routing live

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

develop-plugin.shbash
# 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.nspkg
Installing a packaged plugin

A .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.json with a unique name, an image, a port, and the capabilities_required it needs.
  • An HTTP service that responds on /health so the container health check passes.
  • Optionally: migrations_dir for its tables, terminal_panels for sidebar views, and a settings_schema for 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 the disabled state 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 is enabled.
Q: What capabilities can a plugin request?
A: The coarse host grants read_devices, read_credentials, invoke_llm, trigger_agent, and execute_gnmi, declared in capabilities_required.
Q: How are plugin settings configured?
A: From the JSON Schema in settings_schema (flat dotted keys like kafka.enabled). The admin UI renders a form, or you can write values with PUT /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-plugin scaffold CLI to create, validate, and package a plugin into a .nspkg, then upload it via POST /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} -- the error state records a message explaining why the container failed to start.
  • Verify the manifest image exists and is pullable, and that the declared port matches what the container listens on.

Container starts but is unhealthy

  • Confirm the plugin serves /health on its port; the manifest health_check.command must succeed inside the container.
  • Increase start_period_seconds if 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 state is enabled. Enable the plugin first via POST /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 example snmp.trap_port as an integer, not a string).

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.