Plugin SDK & Scaffold
EnterpriseBuild NetStacks plugins: the netstacks-plugin scaffold CLI, the Rust/Python/TypeScript SDKs, capabilities, .nspkg packaging, and the internal /internal/v1 API.
Overview
NetStacks plugins are self-contained Docker services that extend the Controller with new capabilities — alert ingestion, incident management, autonomous agents, custom integrations. A plugin runs as its own container on the Controller's Docker network, exposes an HTTP API, and talks back to the platform through a stable, versioned /internal/v1 API guarded by capability-scoped service tokens.
This page covers the full developer toolchain:
- netstacks-plugin — the scaffold CLI that generates a project (
new), checks it (validate), and builds a distributable archive (package). - The SDKs — official
netstacks-sdkpackages for Rust, Python, and TypeScript that wrap the internal API with typed clients. - The manifest —
manifest.json, which declares the plugin's identity, image, port, required capabilities, resource limits, health check, migrations, and any UI contributions. - .nspkg packaging — the ZIP-based bundle format you upload to the Controller.
The plugin system is part of the NetStacks Controller and runs in enterprise mode. Plugins are installed by an admin through the Controller Admin UI or the /api/admin/plugins endpoints. The free, standalone Terminal does not run plugins.
Anatomy of a Plugin
Every plugin is a directory containing a manifest, a Dockerized service, and (optionally) SQL migrations. The scaffold's Python/FastAPI template generates this structure:
my-plugin/
├── manifest.json # Plugin metadata, capabilities, resources, health check
├── service/
│ ├── Dockerfile # Multi-stage Python build (non-root, EXPOSE 8080)
│ ├── requirements.txt # Python dependencies (fastapi, uvicorn, httpx, asyncpg...)
│ ├── main.py # FastAPI app: /health + included routers
│ ├── sdk.py # Bundled NetStacks SDK client (reference)
│ ├── database.py # asyncpg pool with per-plugin schema isolation
│ ├── models.py # Pydantic models
│ └── routes/
│ ├── __init__.py
│ └── admin.py # Example CRUD endpoints (/admin/items)
└── migrations/
└── 001_initial_schema.sql # Creates the plugin_<name> schema + base tablesAt runtime, the Controller builds/loads the plugin image, starts a container named netstacks-plugin-<name> on the netstacks_net Docker network, injects environment variables (including a service token), and proxies admin and Terminal requests to it. The plugin only ever needs to:
- Serve a
/healthendpoint for the Docker health check. - Serve whatever HTTP routes its UI panels and admin pages call.
- Use the SDK to reach back into the platform over
/internal/v1.
The scaffold currently generates a Python/FastAPI project, but a plugin is just a container that speaks HTTP — you can write the service in any language. The first-party SDKs ship for Rust, Python, and TypeScript, so those three have the smoothest path.
The netstacks-plugin Scaffold CLI
netstacks-plugin is a Rust CLI (crate netstacks-plugin-scaffold) with three subcommands: new, validate, and package. Build it from source:
cd plugin-scaffold
cargo build --release
# The binary is named "netstacks-plugin"
./target/release/netstacks-plugin --help
# Put it on your PATH (example)
ln -s "$PWD/target/release/netstacks-plugin" /usr/local/bin/netstacks-pluginThe top-level help lists the commands:
$ netstacks-plugin --help
NetStacks plugin development toolkit
Usage: netstacks-plugin <COMMAND>
Commands:
new Create a new plugin project from a template
validate Validate plugin structure and manifest
package Package plugin into .nspkg filenew — Scaffold a Project
new generates a complete plugin project. It takes a plugin name and an optional --template (default python) and --output-dir (default .), then prompts interactively for the rest.
netstacks-plugin new my-awesome-plugin
# Pin the template and output directory explicitly
netstacks-plugin new my-plugin --template python --output-dir ./pluginsThe interactive prompts collect:
- Display name — defaults to a title-cased version of the plugin name (e.g.
my-awesome-plugin→ "My Awesome Plugin"). - Description and Author — both required.
- Capabilities — a multi-select over
read_devices,read_credentials,invoke_llm,search_knowledge, andindex_knowledge. - Database — whether the plugin needs a database (default yes; adds a
migrations/directory andmigrations_dirto the manifest). - Port — the in-container listen port (default
8080, must be at least 1024).
Plugin name rules
The CLI validates the name before scaffolding. It must be lowercase letters and hyphens only, 3–50 characters, start with a lowercase letter, and must not be one of the reserved names core, internal, admin, api, system, or plugin.
Only the python template (alias python-fastapi) is currently accepted; any other value fails with "Only 'python' template is currently supported". The generated version is always 1.0.0 — bump it in manifest.json for later releases.
validate — Check Structure & Manifest
validate runs a series of structural and manifest checks against a plugin directory (default .). It is also run automatically as the first step of package.
# From outside the plugin directory
netstacks-plugin validate my-plugin
# Or from within it
cd my-plugin && netstacks-plugin validateThe eight scored checks are:
manifest.jsonexists.service/Dockerfileexists.- A service entry point exists — either
service/main.pyorservice/src/main.rs. - All required manifest fields are present:
name,version,display_name,description,image,port,capabilities_required,resources, andhealth_check. namematches the pattern (lowercase + hyphens, 3–50 chars, starts with a letter).versionis semver (X.Y.Zwith numeric parts).portis within 1024–65535.resourcescontains bothcpu_limitandmemory_limit.
If migrations_dir is set, the CLI additionally confirms the directory exists and contains at least one .sql file. A passing run looks like:
$ netstacks-plugin validate my-plugin
Validating plugin at: my-plugin
✓ manifest.json exists
✓ service/Dockerfile exists
✓ service entry point exists
✓ All required fields present
✓ Name matches pattern
✓ Version matches semver pattern
✓ Port in valid range
✓ Resources has cpu_limit and memory_limit
✓ Migrations directory has 1 SQL file(s)
Validation results: 8/8 checks passed
✓ Plugin validation passedThe CLI's field-presence checks are a fast local pre-flight. When you upload, the Controller re-validates the manifest against a stricter JSON Schema (Draft-07) — for example cpu_limit must be 0.1–4.0, memory_limit must be 64 MB–4 GB (67108864–4294967296 bytes), and health_check.interval_seconds must be 5–300. See The manifest.json.
package — Build a .nspkg
package produces a .nspkg file — a Deflate-compressed ZIP archive of the plugin directory — ready to upload to the Controller. It always runs validate first and aborts if validation fails.
# Produces <name>-<version>.nspkg in the current directory
netstacks-plugin package my-plugin
# Choose the output path
netstacks-plugin package my-plugin --output dist/my-plugin.nspkgThe archive name is taken from the manifest: {name}-{version}.nspkg (for example my-plugin-1.0.0.nspkg). Development cruft is excluded automatically: __pycache__, .pyc, .git, .DS_Store, .pytest_cache, venv/.venv, node_modules, target, .env, .idea, and .vscode.
$ netstacks-plugin package my-plugin
Running validation before packaging...
... 8/8 checks passed ...
Creating package: my-plugin-1.0.0.nspkg
Package summary:
Files: 12
Total size: 18934 bytes (18.49 KB)
Package size: 7421 bytes (7.25 KB)
✓ Package created successfully: my-plugin-1.0.0.nspkg
To upload this plugin:
1. Open Controller Admin UI: https://localhost:3000
2. Navigate to Plugins > Upload Plugin
3. Select file: my-plugin-1.0.0.nspkgThe first-party plugins repo also ships a build-nspkg.sh helper that does the same thing with zip and writes to a plugins/dist/ folder — handy in CI where you do not want to build the Rust CLI. Usage: ./plugins/build-nspkg.sh plugins/alerts.
The manifest.json
manifest.json is the contract between your plugin and the Controller. Here is a complete, valid manifest modeled on the first-party alerts plugin:
{
"name": "alerts",
"version": "1.0.0",
"display_name": "Alert Ingestion & Pipeline",
"description": "Multi-source alert ingestion with deduplication and a 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": "curl -sf http://localhost:8080/health || exit 1",
"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": "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 }
}
}
}Required fields
name- Identifier matching
^[a-z][a-z0-9-]*$, 3–50 chars. Also the schema namespace prefix and container name suffix. version- Semantic version
X.Y.Z(numeric parts only). display_name/description- Human-readable name (1–100 chars) and short description (≤500 chars) shown in the Admin UI.
image- The Docker image reference the Controller runs, e.g.
netstacks-plugin-alerts:1.0.0. port- The port the service listens on inside its container (1024–65535). Requests are proxied here; the port is not published to the host.
capabilities_required- The list of internal-API permissions baked into this plugin's service token. See Capability Declarations.
Optional fields
resourcescpu_limit(cores, 0.1–4.0) andmemory_limit(bytes, 64 MB–4 GB). Applied as DockerNanoCPUsandMemorylimits.health_check- A
commandplusinterval_seconds(5–300),timeout_seconds(1–60),retries(0–10), andstart_period_seconds(0–300). Wired into the container's DockerHEALTHCHECK. migrations_dir- Path inside the
.nspkgto SQL migrations. When set, the Controller runs them in the plugin's isolated schema on install. terminal_panels/settings_schema- Terminal UI panels and a JSON Schema describing admin-editable settings. See Terminal Panels & Admin UI.
ui_bundle/navigation/routes/dashboard_widgets- Federated Admin-UI contributions.
routesanddashboard_widgetsreference component exports fromui_bundle, so the schema requiresui_bundleto be present whenever either is declared. openapi_path- Path the plugin serves its OpenAPI spec from (default
/openapi.json); merged into the Controller's Swagger UI. Set tonullto opt out.
Capability Declarations
Capabilities are the permission model for the internal API. Each capability you list in capabilities_required is baked into the JWT service token the Controller mints for your plugin at startup. When the plugin calls /internal/v1/*, the endpoint checks the token for the matching capability and returns 403 Forbidden with {"error":"Insufficient capabilities","required":"..."} if it is missing.
Declare only what you use — the token carries exactly your declared set, and the wildcard * capability is rejected at token-generation time. The capabilities enforced by the internal API today:
| Capability | Unlocks | Internal endpoint |
|---|---|---|
read_devices | List/read device inventory | GET /internal/v1/devices |
read_credentials | Fetch a decrypted credential by ID (audit logged) | GET /internal/v1/credentials/{id} |
invoke_llm | Chat completions via the LLM orchestrator | POST /internal/v1/llm/chat |
search_knowledge | Vector search of the knowledge base | GET /internal/v1/knowledge/search |
create_knowledge_document | Index a document into the knowledge base | POST /internal/v1/knowledge/documents |
execute_gnmi | Run gNMI operations against a device | POST /internal/v1/gnmi/execute |
trigger_agent | Trigger an autonomous agent run | POST /internal/v1/agents/trigger |
manage_containers | Mounts the Docker socket into the plugin container (for plugins that orchestrate containers) | — |
Declaring manage_containers causes the Controller to bind-mount /var/run/docker.sock into your container. That grants effective host-level control — only request it for plugins that genuinely orchestrate Docker, and treat it as a high-trust capability.
The scaffold's interactive prompt exposes the five most common capabilities (read_devices, read_credentials, invoke_llm, search_knowledge, index_knowledge). To use any other capability, add it to capabilities_required in manifest.json by hand — the first-party profiling-agents plugin, for instance, declares execute_gnmi.
The /internal/v1 API
The internal API is the stable, versioned surface plugins use to reach core platform services. It is mounted at /internal/v1 on the Controller and is not a public, end-user API — every route requires a valid plugin service token (Bearer auth) and the appropriate capability.
Authentication & environment
When the Controller starts your plugin container, it injects these environment variables (alongside PLUGIN_NAME and PLUGIN_PORT):
# Base URL of the Controller API, reachable on the plugin Docker network
CONTROLLER_API_URL=https://netstacks-api:3000
# A JWT service token carrying this plugin's declared capabilities
NETSTACKS_SERVICE_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Connection string for the plugin's own database (schema-isolated)
DATABASE_URL=postgres://netstacks:...@db:5432/netstacksEvery request authenticates with Authorization: Bearer $NETSTACKS_SERVICE_TOKEN. The endpoints:
| Method & path | Purpose | Capability |
|---|---|---|
GET /internal/v1/devices | List devices (filters: org_id, device_type) | read_devices |
GET /internal/v1/credentials/{id} | Get a decrypted credential | read_credentials |
POST /internal/v1/llm/chat | Chat completion | invoke_llm |
GET /internal/v1/knowledge/search | Vector search (org_id, q, limit) | search_knowledge |
POST /internal/v1/knowledge/documents | Index a document | create_knowledge_document |
POST /internal/v1/gnmi/execute | Execute a gNMI operation | execute_gnmi |
POST /internal/v1/netconf/execute | Execute a NETCONF operation | execute_netconf |
POST /internal/v1/agents/trigger | Trigger an agent run | trigger_agent |
GET /internal/v1/api-resources | List API resources | read_api_resources |
You can call these directly with any HTTP client, but the SDKs handle the base URL, auth header, query/JSON serialization, and 404-to-None mapping for you. A raw example:
# List routers for an org, straight against the internal API
curl -sk "$CONTROLLER_API_URL/internal/v1/devices?org_id=$ORG_ID&device_type=router" \
-H "Authorization: Bearer $NETSTACKS_SERVICE_TOKEN"/internal/v1 is the path plugins call into the Controller. Separately, the Controller proxies admin/Terminal traffic into your plugin at /api/plugins/<name>/* (and federated UI at /api/plugins/<name>/ui/*). The org id from the caller's JWT is injected into proxied requests as X-Org-Id and an org_id query param, so your routes can trust it.
Per-Language SDKs
The official netstacks-sdk packages (version 2.x) wrap /internal/v1 with idiomatic, typed clients for Python, TypeScript, and Rust. All three share the same conventions:
- Read
CONTROLLER_API_URLandNETSTACKS_SERVICE_TOKENfrom the environment (the Controller sets both), or accept them explicitly. - Append
/internal/v1to the base URL and attach the Bearer token to every request. - Expose the same operations: list/get devices, get a credential, chat with the LLM, and search the knowledge base.
- Return
None/null/Option::Nonefor404on single-resource lookups, and surface other failures as a typed API error.
The Python/FastAPI scaffold bundles a self-contained service/sdk.py (NetStacksSDK) so a fresh plugin works with zero extra dependencies. For production, prefer the installable netstacks-sdk package for your language — it is versioned and typed.
Python SDK
Install netstacks-sdk (requires Python 3.9+, depends on httpx and pydantic). The client is async and works as a context manager:
pip install netstacks-sdkimport asyncio
from netstacks_sdk import NetStacksClient, ChatMessage
async def main():
# Reads CONTROLLER_API_URL and NETSTACKS_SERVICE_TOKEN from the environment
async with NetStacksClient() as client:
# Devices
devices = await client.devices.list(device_type="switch")
for d in devices:
print(f"{d.name}: {d.hostname} ({d.device_type})")
device = await client.devices.get("550e8400-e29b-41d4-a716-446655440000")
# Credentials (decrypted, audit logged)
cred = await client.credentials.get("660e8400-e29b-41d4-a716-446655440001")
if cred:
print(cred.username, cred.secret)
# LLM
resp = await client.llm.chat(
[ChatMessage(role="user", content="Summarize this BGP config.")],
model="claude-3-5-sonnet-20241022",
)
print(resp.content)
# Knowledge base
for r in await client.knowledge.search("firewall rules"):
print(r.title, r.snippet)
asyncio.run(main())You can also configure it explicitly instead of via the environment:
client = NetStacksClient(
base_url="http://custom-controller:3000",
token="your-service-token",
timeout=60.0, # seconds
)TypeScript SDK
@netstacks/sdk has zero runtime dependencies and uses native fetch, so it needs Node.js 18+. The client is promise-based:
npm install @netstacks/sdkimport { NetStacksClient, NetStacksAPIError } from "@netstacks/sdk";
import type { ChatMessage } from "@netstacks/sdk";
// Reads CONTROLLER_API_URL and NETSTACKS_SERVICE_TOKEN from the environment
const client = new NetStacksClient();
try {
// Devices
const switches = await client.devices.list("switch");
const device = await client.devices.get("550e8400-e29b-41d4-a716-446655440000");
// Credentials
const cred = await client.credentials.get("660e8400-e29b-41d4-a716-446655440001");
if (cred) console.log(cred.username, cred.secret);
// LLM
const messages: ChatMessage[] = [{ role: "user", content: "Explain VLANs." }];
const resp = await client.llm.chat(messages, "claude-3-5-sonnet-20241022");
console.log(resp.content);
// Knowledge base
for (const r of await client.knowledge.search("firewall rules")) {
console.log(r.title, r.snippet);
}
} catch (err) {
if (err instanceof NetStacksAPIError) {
console.error(`API error ${err.status}: ${err.message}`, err.body);
}
}Explicit configuration uses an options object (timeout is in milliseconds):
const client = new NetStacksClient({
baseUrl: "http://custom-controller:3000",
token: "your-service-token",
timeout: 60000, // ms
});Rust SDK
The Rust netstacks-sdk crate (built on reqwest) needs an async runtime such as tokio:
[dependencies]
netstacks-sdk = "2.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }use netstacks_sdk::{NetStacksClient, ChatMessage, LLMRequest, NetStacksError};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Reads CONTROLLER_API_URL and NETSTACKS_SERVICE_TOKEN from the environment
let client = NetStacksClient::from_env()?;
// Devices
let switches = client.list_devices(Some("switch")).await?;
if let Some(device) = client.get_device("550e8400-...").await? {
println!("Found: {}", device.name);
}
// Credentials
if let Some(cred) = client.get_credential("660e8400-...").await? {
println!("{}: {}", cred.username, cred.secret);
}
// LLM
let req = LLMRequest::new(vec![ChatMessage {
role: "user".to_string(),
content: "Explain VLANs.".to_string(),
}])
.with_model("claude-3-5-sonnet-20241022");
let resp = client.chat(&req).await?;
println!("{}", resp.content);
// Knowledge base
for r in client.search_knowledge("firewall rules").await? {
println!("{}: {}", r.title, r.snippet);
}
Ok(())
}Errors come back as the NetStacksError enum — match on HttpError, ApiError { status, message }, or MissingToken. Explicit construction: NetStacksClient::new(base_url, token).
Database Schema Isolation
Plugins that declare migrations_dir get their own PostgreSQL schema. On install, the Controller creates a schema named plugin_<name> (hyphens in the plugin name become underscores) and runs the .sql files from the migrations directory inside it. Your plugin shares the cluster but never touches another plugin's tables.
The generated migration creates the schema and a starter table:
-- 001_initial_schema.sql (for a plugin named "my-plugin")
CREATE SCHEMA IF NOT EXISTS plugin_my_plugin;
CREATE TABLE plugin_my_plugin.my_plugin_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_my_plugin_items_org_id ON plugin_my_plugin.my_plugin_items(org_id);The generated database.py connects with DATABASE_URL and pins search_path to plugin_<name>,public, so your queries default to your own schema while still seeing shared extensions and types:
pool = await asyncpg.create_pool(
os.getenv("DATABASE_URL"),
min_size=2,
max_size=10,
server_settings={
"search_path": "plugin_my_plugin,public",
},
)Core entities (orgs, devices, credentials) live outside your schema, so plugin tables store org_id as a plain UUID with no foreign key. Resolve the actual org/device/credential through the SDK (the proxy injects the authoritative org_id for incoming requests), and always scope your queries by org_id.
Terminal Panels & Admin UI
Plugins surface in the product through two manifest sections. Both are optional — a headless integration plugin can omit them entirely.
Terminal panels
terminal_panels adds read-only data panels to the Terminal. Each panel has an id, label, optional icon, a data_endpoint (relative to your plugin's base path, served via the proxy), optional columns for table rendering, and an optional refresh_interval_seconds. The Controller attaches enabled plugins' panels to its capabilities response, and the Terminal renders them automatically.
"terminal_panels": [
{
"id": "alert-list",
"label": "Alerts",
"icon": "Bell",
"data_endpoint": "/admin/alerts",
"refresh_interval_seconds": 30,
"columns": [
{ "key": "severity", "label": "Severity" },
{ "key": "summary", "label": "Summary" },
{ "key": "state", "label": "State" }
]
}
]Plugin settings
settings_schema is a JSON Schema describing admin-editable settings. The Admin UI renders a form from it, and saved values are validated against the schema and stored with the plugin record — your service reads them at runtime. Mark secrets with "sensitive": true.
"settings_schema": {
"type": "object",
"properties": {
"default_severity": { "type": "string", "default": "warning",
"enum": ["critical", "warning", "info"] },
"deduplication_window_seconds": { "type": "integer", "default": 300, "minimum": 60 },
"kafka.sasl_password": { "type": "string", "default": "", "sensitive": true }
}
}Federated Admin-UI contributions
For richer admin experiences, ship a federated UI bundle: set ui_bundle (entry relative to the plugin's ui/ directory, served at /api/plugins/<name>/ui/...) and declare navigation entries, routes, and dashboard_widgets that mount components from it. The manifest schema requires ui_bundle when routes or dashboard_widgets are present.
End-to-End Workflow
The full loop from empty directory to a running plugin:
# 1. Scaffold (answer the interactive prompts)
netstacks-plugin new monitoring-integration
# 2. Customize: edit service/routes/admin.py, models.py, and
# migrations/001_initial_schema.sql for your schema
cd monitoring-integration
# 3. Build the Docker image (tag must match manifest.image)
docker build -t netstacks-plugin-monitoring-integration:1.0.0 service/
# 4. Validate
netstacks-plugin validate
# 5. Package into a .nspkg (validation runs again first)
netstacks-plugin package
# -> monitoring-integration-1.0.0.nspkgThen install it on the Controller, either through the Admin UI or the API:
# Upload via the admin API (multipart "file" field)
curl -sk -X POST https://localhost:3000/api/admin/plugins/upload \
-H "Authorization: Bearer $ADMIN_JWT" \
-F "file=@monitoring-integration-1.0.0.nspkg"
# Enable it (starts the container on the netstacks_net network)
curl -sk -X POST https://localhost:3000/api/admin/plugins/monitoring-integration/enable \
-H "Authorization: Bearer $ADMIN_JWT"
# Check health
curl -sk https://localhost:3000/api/admin/plugins/monitoring-integration/health \
-H "Authorization: Bearer $ADMIN_JWT"To test the service in isolation before uploading, run the image standalone with the env vars the Controller would normally inject:
docker run -p 8080:8080 \
-e DATABASE_URL=postgresql://user:pass@host/db \
-e NETSTACKS_SERVICE_TOKEN=your-test-token \
-e CONTROLLER_API_URL=https://host.docker.internal:3000 \
netstacks-plugin-monitoring-integration:1.0.0
curl http://localhost:8080/health
# {"status":"healthy"}The Controller extracts each .nspkg under PLUGIN_STORAGE_DIR (default /data/plugins) and stores the parsed manifest as JSONB in the plugins table. Plugin containers join the netstacks_net network (override with PLUGIN_NETWORK), and the whole subsystem can be turned off with PLUGINS_ENABLED=false.
Q&A
- How do I create a new plugin?
- Build the scaffold CLI (
cargo build --releaseinplugin-scaffold), then runnetstacks-plugin new my-pluginand answer the prompts. See new. - What is a .nspkg file?
- A Deflate-compressed ZIP of your plugin directory, named
{name}-{version}.nspkg, produced bynetstacks-plugin package. It contains the manifest, the Dockerized service, and migrations — development cruft (venv,node_modules,target,.git, etc.) is excluded automatically. - What languages can I write a plugin in?
- Any — a plugin is just a container that serves HTTP. The scaffold generates a Python/FastAPI project, and official
netstacks-sdkpackages ship for Python, TypeScript, and Rust. See Per-Language SDKs. - How does a plugin authenticate to the platform?
- The Controller injects
NETSTACKS_SERVICE_TOKEN(a capability-scoped JWT) andCONTROLLER_API_URLinto the container. The SDKs read both from the environment and attach the token as a Bearer header on every/internal/v1call. See The /internal/v1 API. - What happens if I call an endpoint without the right capability?
- The internal API returns
403 Forbiddenwith{"error":"Insufficient capabilities","required":"<cap>"}. Add the capability tocapabilities_requiredin your manifest and reinstall. See Capability Declarations. - Why does scaffold only offer five capabilities?
- The interactive prompt covers the five most common ones. Others —
execute_gnmi,trigger_agent,create_knowledge_document,manage_containers— are added by editingcapabilities_requiredinmanifest.jsondirectly. - Where do my plugin's database tables go?
- In a dedicated
plugin_<name>PostgreSQL schema the Controller creates on install, populated from yourmigrations_dirSQL files. See Database Schema Isolation. - Why did the Controller reject my manifest even though the CLI passed?
- The CLI checks field presence; the Controller enforces a stricter Draft-07 JSON Schema (range limits on
cpu_limit,memory_limit, health-check timings, and the rule thatroutes/dashboard_widgetsrequire aui_bundle). See The manifest.json. - How do I show my plugin's data in the Terminal?
- Declare
terminal_panelsin the manifest pointing at adata_endpointyour service serves; the Controller advertises enabled plugins' panels and the Terminal renders them. See Terminal Panels & Admin UI.
Related
- Plugin System — how plugins install, run, and are managed by the Controller.
- Profiling Agents — a first-party plugin using
execute_gnmiand autonomous agents. - Alert Pipeline — reference plugin for multi-source ingestion with terminal panels and settings.
- Incidents & ITSM — reference plugin with ServiceNow/Jira sync.
- Installation — deploy the Controller that hosts your plugins.
- API Authentication — authenticating against the public Controller API.