NetStacksNetStacks

Building Plugins

Enterprise

Build your own NetStacks Controller plugin: the .nspkg package layout, the manifest, the netstacks-plugin scaffold CLI, database migrations, Terminal panels, and the Controller's internal API for plugins.

Overview

A NetStacks plugin is an HTTP service that runs in its own Docker container next to the Controller. You ship it as a .nspkg file; the Controller builds the image, runs the database migrations, starts the container when you enable it, and forwards requests from /api/plugins/<name>/* to the container. The plugin can call back into the Controller through a versioned internal API using a service token the Controller gives it.

The NetStacks Alerts and Incidents plugins are built this way, with Python 3.12 and FastAPI. Any language works as long as the service runs in a container and listens on the port in its manifest.

To install and manage a finished package, see Plugin System.

Package Layout

A .nspkg is a ZIP archive with the .nspkg extension. Only manifest.json at the root is required; the conventional layout is:

my-plugin/
├── manifest.json        # required: plugin metadata (see Manifest Reference)
├── migrations/          # optional: *.sql files, applied in version order
│   └── 001_initial_schema.sql
└── service/
    ├── Dockerfile       # the Controller builds the image from this on upload
    └── ...              # your service source code
  • If service/Dockerfile exists, the Controller builds the image on upload and tags it with the manifest's image. If the Dockerfile uses COPY service/... paths, the build context is the package root; otherwise it is the service/ folder.
  • The package can be up to 100 MB.
  • Paths must be relative — entries containing .. or starting with / are rejected.

The netstacks-plugin CLI

The netstacks-plugin command-line tool creates a new plugin project, checks it and packages it. It has three commands:

CommandWhat it does
netstacks-plugin new <name>Creates <name>/ from the Python FastAPI template. Asks for a display name, description, author, the capabilities you need, whether the plugin needs a database, and the port (default 8080). Options: -o, --output-dir <DIR> (default .), -t, --template python (Python is the only template).
netstacks-plugin validate [path]Checks the project in path (default .): manifest.json exists and has the required fields, service/Dockerfile and a service entry point exist, the name, version and port are valid, resources sets both limits, and the migrations_dir contains at least one .sql file.
netstacks-plugin package [path]Runs validate, then writes <name>-<version>.nspkg (or the file given with -o, --output <FILE>). It leaves out caches, virtual environments, .git, node_modules, target, .env and editor folders.

A typical session:

netstacks-plugin new config-auditor
cd config-auditor
# ...write your service code and migrations...
netstacks-plugin validate
netstacks-plugin package

Plugin names for the CLI use lowercase letters and hyphens, are 3 to 50 characters long, start with a letter, and can't be core, internal, admin, api, system or plugin.

The template generates a manifest.json, a multi-stage service/Dockerfile based on python:3.12-slim that runs as a non-root user, a FastAPI app with a GET /health endpoint and an example /admin/items CRUD router, an asyncpg database module, and migrations/001_initial_schema.sql if you chose a database.

Health check command
The python:3.12-slim base image doesn't include curl. Make sure the manifest's health_check.command uses a tool that exists in your image, for example: python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')". Also keep the manifest port equal to the port your service actually listens on (the template's Dockerfile starts uvicorn on 8080).

Manifest Reference

The Controller validates manifest.json on upload. A minimal manifest:

manifest.jsonjson
{
  "name": "config-auditor",
  "version": "1.0.0",
  "display_name": "Config Auditor",
  "description": "Audits device configurations against policy",
  "image": "netstacks-plugin-config-auditor:1.0.0",
  "port": 8080,
  "capabilities_required": ["read_devices"],
  "resources": { "cpu_limit": 0.5, "memory_limit": 268435456 },
  "health_check": {
    "command": "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/health')\""
  },
  "migrations_dir": "migrations"
}
FieldRequiredRules / meaning
nameyesLowercase, starts with a letter, letters/digits/hyphens, 3–50 characters. Becomes the container name netstacks-plugin-<name>, the API prefix /api/plugins/<name> and the database schema plugin_<name> (hyphens become underscores).
versionyesMAJOR.MINOR.PATCH, for example 1.0.0.
display_nameyes1–100 characters. Shown in the admin console.
descriptionyesUp to 500 characters.
imageyesDocker image tag used to build and run the container.
portyes1024–65535. The port your service listens on inside the container.
capabilities_requiredyesList of capabilities granted to the plugin's service token. See Internal API for the capabilities the Controller checks. "*" is not allowed.
resources.cpu_limitnoCPU cores, 0.1–4.0.
resources.memory_limitnoBytes, 67108864 (64 MB) to 4294967296 (4 GB).
health_check.commandif health_check is setShell command used as the container's Docker health check. Optional tuning: interval_seconds (5–300, default 30), timeout_seconds (1–60, default 10), retries (0–10, default 3), start_period_seconds (0–300, default 15).
migrations_dirnoFolder in the package with .sql migrations. If you leave it out, no migrations run.
terminal_panelsnoPanels shown in the Terminal activity bar and the admin console sidebar. See Terminal and Admin Panels.
settings_schemanoA JSON Schema object. The plugin detail page renders a Settings form from it (enum → dropdown, boolean → checkbox, number/integer → number field, anything else → text field; description becomes help text) and validates values on save.
openapi_pathnoPath of your service's OpenAPI document (default /openapi.json). The Controller merges it into its own API documentation under /api/plugins/<name>/.

The manifest schema also accepts ui_bundle, navigation, routes and dashboard_widgets for loading a JavaScript UI bundle from the package's ui/ folder into the admin console. ui_bundle is required when you use routes or dashboard_widgets.

Terminal and Admin Panels

Each entry in terminal_panels becomes an icon in the Terminal activity bar and an entry in the admin console's Plugins sidebar section. The panel shows a table filled from a GET on your plugin's data_endpoint.

"terminal_panels": [
  {
    "id": "findings",
    "label": "Audit Findings",
    "icon": "FileCode",
    "data_endpoint": "/admin/findings",
    "refresh_interval_seconds": 30,
    "columns": [
      { "key": "device", "label": "Device" },
      { "key": "rule", "label": "Rule" },
      { "key": "created_at", "label": "Found" }
    ],
    "actions": [
      { "id": "rescan", "label": "Re-scan", "endpoint": "/admin/rescan", "method": "POST" },
      { "id": "dismiss", "label": "Dismiss", "endpoint": "/admin/findings/{id}/dismiss",
        "confirm": "Dismiss this finding?", "style": "danger" }
    ]
  }
]
FieldMeaning
id, label, data_endpointRequired. The admin console opens the panel at /plugin/<name>/<id>. label is the panel title and Terminal tooltip.
iconBell, Filter, FileCode and Rocket are recognized in both places (the Terminal also knows AlertTriangle and Activity). Other values get a generic plugin icon.
columns{key, label} objects. Keys ending in _at are shown as date-times in the admin console.
actionsTerminal only. Buttons with id, label, endpoint, optional method (default POST), confirm text and style (primary, danger, default). An endpoint containing {id} becomes a per-row action.
refresh_interval_secondsHow often the panel reloads its data.

Your data_endpoint receives an org_id query parameter set by the Controller — filter your data by it.

Database Migrations

  • Each plugin gets its own PostgreSQL schema, plugin_<name> (hyphens become underscores).
  • Migration files are *.sql files in migrations_dir. The version is the number before the first underscore (001_initial_schema.sql is version 1). Files apply in version order, each in its own transaction.
  • Migrations run on upload and again on every enable; the Controller records applied versions in plugin_<name>._sqlx_migrations, so each file runs once. When you ship a new version, add new files rather than editing old ones.
  • Prefix table names with your schema (CREATE TABLE plugin_config_auditor.findings ...). Don't add foreign keys to core Controller tables.

Runtime Environment

The Controller sets these environment variables in the container:

VariableValue
CONTROLLER_API_URLBase URL of the Controller API, for internal API calls.
NETSTACKS_SERVICE_TOKENThe plugin's service token, carrying the capabilities from capabilities_required.
DATABASE_URLPostgreSQL connection string. Use your plugin schema.
PLUGIN_NAME, PLUGIN_PORTThe manifest name and port.

Requests forwarded from /api/plugins/<name>/* carry the caller's Authorization header, an X-Plugin-Name header, and — for users — the caller's organization as the org_id query parameter and X-Org-Id header. The Controller replaces any client-supplied values for these two. Forwarded requests time out after 30 seconds.

Internal API for Plugins

Plugins call the Controller at $CONTROLLER_API_URL/internal/v1/... with the header Authorization: Bearer $NETSTACKS_SERVICE_TOKEN. Each endpoint checks for a capability in the token, so list every capability you need in capabilities_required. These are all the internal endpoints:

EndpointCapabilityPurpose
GET /devicesread_devicesDevice list: {"devices": [{id, name, hostname, device_type, status}]}
GET /profile-secrets/:idread_profile_secretsDecrypted credentials of a credential profile. Every call is audit-logged.
GET /knowledge/search?q=search_knowledgeText search over knowledge base document titles and content. Returns up to 20 {id, title, snippet}.
POST /knowledge/documentscreate_knowledge_documentAdd a document: {org_id, title, raw_content, source_type?, category?, tags?}
POST /llm/chatinvoke_llmChat completion through the Controller's configured AI provider: {messages: [{role, content}]} → {content, model}.
POST /netconf/executeexecute_netconf{device_id, credential_id, operation: get_config | load_merge, commands?, format?}
POST /gnmi/executeexecute_gnmi{device_id, credential_id, operation: get | set, path?, commands?, encoding?}
GET /api-resources, GET /api-resources/:idread_api_resourcesRead the Controller's configured API resources.
POST /agents/triggertrigger_agentStart AI agent runs for an alert (used by the Alerts plugin).
POST /plugin-tools/registerany valid tokenRegister a tool AI agents can call (below).
POST /execution-webhooks/registerany valid tokenGet a callback when agent runs finish (below).

Example from Python with httpx:

import os, httpx

BASE = os.environ["CONTROLLER_API_URL"] + "/internal/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['NETSTACKS_SERVICE_TOKEN']}"}

async def list_devices():
    async with httpx.AsyncClient(verify=False, timeout=30) as client:
        r = await client.get(f"{BASE}/devices", headers=HEADERS)
        r.raise_for_status()
        return r.json()["devices"]
Use the /internal/v1 paths
Call the endpoints in the table above directly. Only these paths exist; for example there is no single-device or /credentials endpoint — list devices with GET /devices and fetch credentials with GET /profile-secrets/:id. Set verify to match how your Controller's certificate is issued.

Giving AI Agents New Tools

A plugin can add tools that Controller AI agents can call. Register each tool at startup:

POST /internal/v1/plugin-tools/register
{
  "plugin_name": "config-auditor",
  "tool_name": "audit_device",
  "description": "Run the policy audit on one device and return findings",
  "parameters_schema": {
    "type": "object",
    "properties": { "device_id": { "type": "string" } },
    "required": ["device_id"]
  },
  "execute_url": "/tools/audit-device",
  "is_destructive": false
}

When an agent uses the tool, the Controller sends a POST to http://netstacks-plugin-<name>:<port><execute_url> with the agent's arguments plus org_id (and trigger_source_id when the run was triggered by something, such as an alert). Your response body is returned to the agent. Optional fields: category (default plugin), required_permission (default plugins.use) and trigger_type — set it (for example alert) to offer the tool only in runs with that trigger; leave it out to offer it in every run.

Agent Execution Callbacks

Register a callback to hear when agent runs of a given trigger type finish:

POST /internal/v1/execution-webhooks/register
{ "plugin_name": "config-auditor", "callback_url": "http://netstacks-plugin-config-auditor:8080/internal/execution-event", "trigger_type": "alert" }

When a matching run ends, the Controller POSTs {execution_id, agent_id, agent_name, trigger_source_id, event_type: completed | failed, summary, detail: {state, final_answer, iterations_used, tool_calls_made}} to your callback URL.

Calling Your Plugin's API

Users and scripts reach your plugin at https://<controller>/api/plugins/<name>/<path> with a NetStacks access token (see API Authentication). The plugin must be enabled in the caller's organization. The Controller authenticates the caller; checking what each caller may do inside your plugin is up to your service. The Terminal also sends an X-User-Id header on plugin requests.

Static files in the package's ui/ folder are served at /api/plugins/<name>/ui/*.

Build and Test Checklist

  1. Run netstacks-plugin validate and fix every error.
  2. Build the image locally (docker build) and run it to confirm the health check command succeeds inside the container.
  3. Run netstacks-plugin package.
  4. Upload the .nspkg on a test Controller (Administration → Plugins → Upload Plugin), then click Enable.
  5. Open the plugin detail page and confirm Health Status shows running and healthy.
  6. Check your panel in the admin console sidebar and in the Terminal activity bar. If something fails, read docker logs netstacks-plugin-<name> on the Controller host.