np-agent
np-agent is the host agent for Linux, macOS and Windows. It collects basic system metrics
(load/CPU, memory, disks, processes, network), runs local Nagios plugins on a schedule and pushes
everything as passive results over HTTPS — by default no inbound port on the monitored host.
Optionally it pulls centrally defined checks from the server, and optionally it exposes an
NCPA-style HTTPS listener that the server polls with the builtin agent check.
| Mode | Default | Direction | Needs |
|---|---|---|---|
| Passive push | on | agent → server, POST /api/v1/results every interval |
token with objects:write |
| Central check pull | off (pull: true) |
agent ← server, GET /api/v1/agent/checks?host=… every pullInterval; results go back as passive pushes |
token with objects:write and objects:read, plus pullAllow |
| Active listener | off (listen: ":5693") |
server → agent over HTTPS (/v1/health, /v1/metrics, /v1/run/{service}) |
listenToken; a service with checkCommand: builtin:agent |
The agent is a single static binary with no dependencies (it does not read the server’s
config.yaml). Command-line flags and exit codes are listed in the
np-agent CLI reference.
Install
Section titled “Install”-
Get the binary. Linux/macOS: the release tarball
northplane_<tag>_<os>_<arch>.tar.gzcontainsnorthplaned,np,np-agentandLICENSE;install.shdownloads the latest release, verifies the checksum and installs into/usr/local/bin(or~/.local/binwhen it cannot write there). Windows:northplane_<tag>_windows_amd64.zipwithnp.exeandnp-agent.exe. Details: Installation.Terminal window curl -fsSL https://raw.githubusercontent.com/myfoxit/northplane/main/install.sh | shnp-agent -version -
Create the host and its services on the server — the agent does not create anything (see below).
-
Mint a token with scope
objects:write(plusobjects:readwhen you usepull): Admin → Agents (Agents) → Create token does exactly that, shows the token once and pastes it into the generatedagent.yaml; orPOST /api/v1/api-tokens {"name":"agent-web-01","scopes":["objects:write"]}(API tokens). -
Write
agent.yaml(reference below) to/etc/northplane/agent.yaml(Windows:C:\ProgramData\northplane\agent.yaml) and start the service (service units). The first push happens immediately, then everyinterval.
The Admin → Agents tab also offers a prefilled agent.yaml and the systemd/launchd/Windows
snippets used below; they are plain text — the same files you would write by hand.
Create the objects first
Section titled “Create the objects first”POST /api/v1/results accepts results only for objects that already exist; unknown
hosts/services are listed under rejected and silently dropped by the agent. So before the
first push create:
- a Host whose
nameequals the agent’shostname(the OS hostname unless you sethostname:inagent.yaml) — the agent’s host result (np-agent 1.2.0 alive on linux/amd64) keeps it UP; - one Service per collector you want, named exactly like the agent reports them:
load,memory,disk /(one per mount, with the space),cpu(Windows),processes,network, plus one service perchecks[].service.
Use checkCommand: passive and a stalenessAfter so a silent agent turns the services UNKNOWN:
kind: Templatemetadata: {name: agent-passive}spec: kind: service checkCommand: passive stalenessAfter: 5m stalenessText: "no result from np-agent for 5m"---kind: Hostmetadata: {name: web-01, folder: /prod, labels: {env: prod, agent: "true"}}spec: address: 10.0.0.5 checkCommand: passive stalenessAfter: 5m---kind: Servicemetadata: {name: load, host: web-01}spec: {templates: [agent-passive]}---kind: Servicemetadata: {name: memory, host: web-01}spec: {templates: [agent-passive]}---kind: Servicemetadata: {name: "disk /", host: web-01}spec: {templates: [agent-passive]}---kind: Servicemetadata: {name: processes, host: web-01}spec: {templates: [agent-passive]}---kind: Servicemetadata: {name: network, host: web-01}spec: {templates: [agent-passive]}np apply -f agent-host.yaml. You can also keep the host actively checked
(builtin:icmp) — passive results are accepted for active objects as well — but then the agent’s
own “alive” result competes with the ping; choose one.
agent.yaml reference
Section titled “agent.yaml reference”YAML, decoded strictly: durations must be strings with a unit (60s, 5m) — interval: 60
is a parse error. Default path: /etc/northplane/agent.yaml when running as root or when that
file exists, otherwise the per-user config directory (~/.config/northplane/agent.yaml on Linux,
~/Library/Application Support/northplane/agent.yaml on macOS, %AppData%\northplane\agent.yaml
on Windows); override with -config <path>.
| Key | Type | Default | Meaning |
|---|---|---|---|
server |
string | — (required) | base URL of the instance, e.g. https://northplane.example.net (trailing / trimmed) |
token |
string | — (required unless env NORTHPLANE_TOKEN) |
API token np_…; the environment variable overrides the file |
hostname |
string | OS hostname | host name in every result and in the pull query; must match the Host object |
insecure |
bool | false |
skip TLS verification for push and pull (self-signed server certificate) |
interval |
duration | 60s |
collection + push cadence; also the cadence for pulled checks without their own interval |
checks |
list | — | local plugin checks (below) |
checks[].service |
string | — | service name on the server |
checks[].command |
string | — | plugin path or bare name |
checks[].args |
list | — | argv (no shell) |
checks[].timeout |
duration | 30s |
per-check timeout |
pull |
bool | false |
fetch agent:exec: checks from the server |
pullInterval |
duration | 5m |
how often the check list is re-fetched |
pullAllow |
list | empty = deny all | local allowlist of bare plugin names that pulled checks may run |
disk |
list | ["/"] |
mount points reported as disk <mount>; an explicit empty list disables disk collection |
net |
list | empty = all non-loopback, max 8 | interface names reported in network |
listen |
string | empty = off | listener address, e.g. ":5693" |
listenToken |
string | — (required with listen) |
bearer token the server must present |
tlsCert, tlsKey |
string | empty = self-signed | PEM certificate/key for the listener |
Minimal file (what the Admin tab generates):
server: https://northplane.example.nettoken: np_…hostname: web-01interval: 60sdisk: ["/"]Everything in one file:
server: https://northplane.example.nettoken: np_0123456789abcdef0123456789abcdef0123456789abcdefhostname: db-01insecure: falseinterval: 60sdisk: ["/", "/var/lib/postgresql"]net: ["eth0"]checks: - service: postgres command: check_pgsql args: ["-H", "127.0.0.1", "-l", "postgres"] timeout: 20spull: truepullInterval: 5mpullAllow: ["check_disk", "check_http", "check_pgsql"]listen: ":5693"listenToken: 8f1c0a6d3f8e4f9d9a7b2c1d0e5f6a7btlsCert: /etc/northplane/agent.crttlsKey: /etc/northplane/agent.keyStartup fails (exit 1) when the file is unreadable or invalid, when server or token is empty,
when listen is set without listenToken, or when the listener cannot bind. There is no
connectivity check at start: an unreachable server or a bad token only logs
submit failed, buffering … err="HTTP 401" on every tick. The process handles SIGINT/SIGTERM
(clean stop); there is no reload signal — restart after editing the file.
Collectors: service names, output, perfdata
Section titled “Collectors: service names, output, perfdata”Every tick the agent submits one result per collector. Thresholds are fixed in the agent; “processes” and “network” are informational (grade them with alert rules if needed).
| Service name | Platforms | State rule | Output (example) | Perfdata |
|---|---|---|---|---|
| (host result, no service) | all | always UP | np-agent 1.2.0 alive on linux/amd64 | uptime=120s;;;; |
uptime (s) |
load |
Linux, macOS | WARNING > 2×CPUs, CRITICAL > 4×CPUs | load average 0.42 (8 cpus) | load1=0.42;16;32;0; |
load1 |
memory |
all | WARNING > 90 %, CRITICAL > 95 % used | RAM 61.3% used, 6.2 GB available of 16.0 GB | used=61.3%;90;95;0;100 available=6657199104B;;;0; |
used (%), available (B) |
disk <mount> |
all | WARNING > 85 %, CRITICAL > 95 % used | / 41.2% used, 118.4 GB free | used=41.2%;85;95;0;100 free=127129182208B;;;0; |
used (%), free (B) |
cpu |
Windows only, from the 2nd tick | WARNING > 85 %, CRITICAL > 95 % | CPU 12.5% busy (8 cpus) | cpu=12.5%;85;95;0;100 |
cpu (%) |
processes |
all (Windows: running = 0) | always OK | 312 processes (2 running) | total=312;;;0; running=2;;;0; |
total, running |
network |
Linux, macOS, from the 2nd tick | always OK | throughput eth0 rx 1523 B/s tx 884 B/s | rx_eth0=1523B/s;;;0; tx_eth0=884B/s;;;0; |
rx_<if>, tx_<if> (B/s) |
<checks[].service> |
all | plugin exit code | plugin stdout | whatever the plugin prints |
Data sources: Linux reads /proc/loadavg, /proc/meminfo (MemAvailable), statfs, /proc
and /proc/net/dev; macOS uses sysctl, vm_stat, ps and netstat -ibn; Windows uses
kernel32 (GlobalMemoryStatusEx, GetDiskFreeSpaceExW, Toolhelp, GetSystemTimes) — no load
average and no network counters there. Network rates are deltas between ticks; interfaces whose
counters reset are skipped for one round; without net: the list is capped at the first 8 names.
Because these are passive results, each collector is a hard state immediately — no
soft/retry cycle. Raise thresholds by post-processing in alert rules (e.g. match
event.object == "disk /" && event.state == "WARNING") or by switching a service to
listener mode, where -w/-c are yours.
Local plugin checks (checks:)
Section titled “Local plugin checks (checks:)”checks: - service: postgres command: check_pgsql args: ["-H", "127.0.0.1", "-l", "postgres"] timeout: 20s| Aspect | Behaviour |
|---|---|
| Command resolution | a bare name is searched in /usr/local/bin:/usr/bin:/bin:/usr/lib/nagios/plugins:/usr/lib64/nagios/plugins:/usr/local/libexec/nagios:/opt/homebrew/bin:/opt/homebrew/sbin, then in the process PATH; a path is used as is |
| Environment | only PATH=<that list> and LC_ALL=C — nothing inherited (no HOME, no proxy variables) |
| Execution | argv, no shell; timeout default 30 s → UNKNOWN - plugin timed out after 30s |
| State | exit codes 0–3; anything else or an exec failure → UNKNOWN |
| Output | stdout trimmed; if empty, stderr; if still empty UNKNOWN - no output. Multi-line output and perfdata pass through unchanged; the server splits the first line at | |
The same runner serves pulled checks and the listener’s /v1/run/{service}.
Passive push protocol
Section titled “Passive push protocol”| Item | Value |
|---|---|
| Request | POST {server}/api/v1/results with Authorization: Bearer <token>, Content-Type: application/json |
| Body | {"results":[{"host":"web-01","service":"load","state":0,"output":"…"}, …]} (service omitted for the host result) |
| Success | any status below 300 (the server answers 202 {"accepted":n,"rejected":[…]}); the agent does not inspect rejected |
| Failure | transport error or status ≥ 300 → submit failed, buffering; results stay in memory |
| Buffer | every tick appends new results; the whole buffer is re-sent each tick until one submit succeeds; capped at 10 000 results (oldest dropped); lost on restart |
| Backoff | none — the regular interval tick is the retry |
Central checks (pull) and agent:exec
Section titled “Central checks (pull) and agent:exec”Instead of editing checks: on every host you can define plugin checks on the server and let
the agent fetch them:
kind: Servicemetadata: {name: disk-data, host: db-01}spec: checkCommand: agent:exec:check_disk args: ["-w", "20%", "-c", "10%", "-p", "/data"] interval: 2m timeout: 15s stalenessAfter: 10m- The reference
agent:exec:<plugin>(or a named CheckCommand oftype: agent) marks the service as agent class: the server never executes it; it only schedules a freshness probe whenstalenessAfteris set. GET /api/v1/agent/checks?host=db-01(permissionobjects:read, reference) returns every agent-class service of that host:{"host":"db-01","checks":[{"service":"disk-data","command":"check_disk","args":["-w","20%","-c","10%","-p","/data"],"intervalSeconds":120,"timeoutSeconds":15}]}. Macros ($HOSTADDRESS$,$ARGn$, custom vars) are expanded server-side, except$SECRET:…$, which stays verbatim so secrets never leave the server.- The agent (with
pull: true) re-fetches everypullInterval, runs each check at its ownintervalSeconds(or every agent tick when 0) and pushes the results passively. On fetch errors it keeps the previous set. A pulled check with the sameservicename as a localchecks[]entry replaces the local one (“central wins”). - Allowlist (RCE guard): the command must be a bare name without
/,\or..and must be listed in the agent’spullAllow; otherwise the agent submitsUNKNOWN - command refused by agent allowlist: <reason>for that service and logs a warning. An emptypullAllowrefuses everything — set it deliberately.
Pulled checks are not available through the listener’s /v1/run (which only serves the local
checks: list). POST /api/v1/objects/{id}/check-now on an agent-class service only queues a
freshness probe — it does not contact the agent.
Active listener mode
Section titled “Active listener mode”With listen and listenToken set, the agent serves HTTPS (TLS 1.2+; a self-signed ECDSA
certificate valid for 10 years is generated in memory on every start unless tlsCert/tlsKey
are given; CN/SAN = hostname). Requests need Authorization: Bearer <listenToken>.
| Endpoint | Returns |
|---|---|
GET /v1/health |
{"agent":"np-agent","version":"…","hostname":"…","uptimeSeconds":n} |
GET /v1/metrics |
{"agent","version","hostname","uptimeSeconds","cpus","load1","cpuPct","memory":{usedPct,totalBytes,availableBytes},"disks":[{mount,usedPct,freeBytes}],"processes":{total,running},"network":[{name,rxBytesPerSec,txBytesPerSec}]} — fields absent when the platform/tick has no value |
GET /v1/run/{service} |
runs the local checks[] entry of that name → {"service","state","output"}; unknown → 404 unknown check (not in agent.yaml) |
The server side is the builtin agent check — summary mode with built-in thresholds, one metric
graded with your own -w/-c, or a remote plugin by name:
kind: Servicemetadata: {name: agent-summary, host: web-01}spec: checkCommand: builtin:agent args: ["--token", "$SECRET:agent-web01$", "--insecure"]---kind: Servicemetadata: {name: disk-root, host: web-01}spec: checkCommand: builtin:agent args: ["--token", "$SECRET:agent-web01$", "--insecure", "--metric", "disk:/", "-w", "80", "-c", "90"]---kind: Servicemetadata: {name: postgres, host: web-01}spec: checkCommand: builtin:agent args: ["--token", "$SECRET:agent-web01$", "--insecure", "--check", "postgres"]Store the listenToken as a secret (agent-web01 above) rather
than in the object spec. Flags, output formats and thresholds are in the
builtin checks reference. Listener mode and passive push
can run side by side; the listener has no rate limiting or IP allowlisting of its own — firewall
port 5693 to the server’s address. The agent still requires server and token even when you
only use the listener.
Staleness: detecting a silent agent
Section titled “Staleness: detecting a silent agent”Passive and agent-class objects are never checked actively, so without a freshness rule a dead
agent simply freezes the last state. Set stalenessAfter (and optionally stalenessText) on the
host and on each passive service — directly or via a template as in the bundle above. When no
result arrived within that window the object becomes UNKNOWN with the staleness text. The probe
runs at the stalenessAfter cadence, so detection takes between 1× and 2× that value, and a
stale object passes through the normal soft/hard attempts. Details:
Checks and states. In the UI the staleness fields are on the
Advanced tab for services; for hosts set them via API or bundle.
Run np-agent as a service
Section titled “Run np-agent as a service”[Unit]Description=Northplane host agentAfter=network-online.target
[Service]ExecStart=/usr/local/bin/np-agent -config /etc/northplane/agent.yamlRestart=alwaysRestartSec=5DynamicUser=yes
[Install]WantedBy=multi-user.targetsystemctl daemon-reloadsystemctl enable --now np-agentjournalctl -u np-agent -fDynamicUser=yes runs the agent as a transient unprivileged user. /etc/northplane/agent.yaml
must therefore be readable by that user (for example chmod 0644 — it contains the token, so
prefer a dedicated User= with chmod 0640 and a matching group), and plugins that need root
(raw sockets, privileged files) will not work under that unit without changes.
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>Label</key><string>com.northplane.agent</string> <key>ProgramArguments</key><array> <string>/usr/local/bin/np-agent</string> <string>-config</string><string>/etc/northplane/agent.yaml</string> </array> <key>RunAtLoad</key><true/> <key>KeepAlive</key><true/></dict></plist>sudo launchctl load -w /Library/LaunchDaemons/com.northplane.agent.plistPowerShell as administrator, after placing np-agent.exe from the release zip under
C:\Program Files\northplane\ and the config under C:\ProgramData\northplane\agent.yaml:
sc.exe create np-agent binPath= "C:\Program Files\northplane\np-agent.exe -config C:\ProgramData\northplane\agent.yaml" start= autosc.exe start np-agentThe agent has no Windows-service integration of its own; it runs as a plain console process
under sc.exe. Windows reports memory, disk, cpu, processes (running always 0) and local
checks — no load, no network.
No unit files ship in the tarball; copy them from here or from Admin → Agents.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause / fix |
|---|---|
| Host/services stay Pending in Objects | the names do not match: Host name ≠ agent hostname, or the service is not named disk / (with space), load, … — check POST /api/v1/results manually and read rejected |
Log submit failed, buffering … err="HTTP 401" / "HTTP 403" |
wrong token / missing objects:write scope; for pull also objects:read |
submit failed … x509 |
self-signed server certificate → insecure: true or a proper certificate (TLS and proxy) |
config parse err=… at start |
a duration without unit (interval: 60) or a YAML typo |
| Services flip to UNKNOWN “no result from np-agent” | staleness fired: the agent is down, buffering (server unreachable) or the interval is longer than stalenessAfter |
UNKNOWN - command refused by agent allowlist: … |
add the plugin’s bare name to pullAllow on that agent; pulled commands must not be paths |
UNKNOWN - no output for a local check |
the plugin wrote nothing — run it by hand with PATH restricted as the agent does (env -i PATH=/usr/lib/nagios/plugins:/usr/bin:/bin LC_ALL=C check_x …) |
builtin:agent → CRITICAL agent web-01: HTTP 401: unauthorized |
--token ≠ listenToken |
builtin:agent → CRITICAL … certificate signed by unknown authority |
add --insecure or configure tlsCert/tlsKey |
builtin:agent --metric load1 → UNKNOWN on Windows |
Windows agents expose cpu, not load1 |
network / cpu missing after start |
both need a previous sample; they appear from the second tick |
| Agent liveness / versions in the UI | there is no agent registry; watch the host’s “alive” result (uptime perfdata, version in the output) and the token’s last used time under Admin → API tokens |
Logs go to stderr as key=value text (journalctl -u np-agent); there is no log-level setting.
All messages are listed in the np-agent CLI reference.