NetBox Store host agent (MVP)
This package is the deliberately small privileged boundary between the NetBox Store plugin and a NetBox 4.6.5–4.6.8 Linux host. It does not import the Store implementation or assume Django: every decision is revalidated against the configured JSON API immediately before a lifecycle action.
The daemon is dry-run by default. It serializes operations, persists idempotency and state in
SQLite, accepts one bounded JSON line per Unix-stream connection, verifies Linux peer credentials,
downloads only an approved immutable wheel or commit-bound source archive, and invokes subprocesses only as fixed argument arrays
with shell=False.
Install and one-time operator setup
Build/install into a dedicated administrative environment, copy
examples/agent.toml to /etc/netbox-store-agent/agent.toml, replace all
site-specific paths, hosts, UIDs/GIDs and versions, then make the file root-owned and mode 0600.
The optional bearer-token file must also be root-owned 0600. Store credentials are sent only to
the exact scheme/host/port origin of store.base_url, never to a separately allow-listed artifact
host.
allow_private_addresses = false is intentionally fail-closed. If the Store or an approved
artifact host consciously runs on RFC1918/ULA infrastructure, set it to true only after pinning
every expected hostname in allowed_hosts and deploying correctly verified TLS (including the
private CA via ca_file when needed). The host allow-list and exact-origin credential rule remain
active; this switch only permits private/reserved DNS results.
The agent owns only these two configured files:
paths.include_path, which contains onlySTORE_PLUGINS = [...];paths.requirements_path, which contains locked wheel references. Locally built wheels are cached below/opt/netbox/.netbox-store-wheels.
In the operator-owned NetBox configuration.py, add once, after the normal PLUGINS declaration:
from store_plugins import STORE_PLUGINS
PLUGINS += STORE_PLUGINS
Place store_plugins.py where that import resolves in your deployment. Optionally add a one-time
-r /opt/netbox/local_requirements_store.txt line to the operator-owned
/opt/netbox/local_requirements.txt for reproducible maintenance installs. The agent never edits
either operator-owned file.
Install the example systemd units, review their ReadWritePaths, SocketGroup, and ExecStart, then:
systemctl daemon-reload
systemctl enable --now netbox-store-agent.socket
netbox-store-agent capabilities
The canonical socket path used by the unit, example config, CLI default, and NetBox Store plugin is
/run/netbox-store-agent/agent.sock.
With peer checks enabled (the production default), Linux SO_PEERCRED must be available and either
the peer's effective UID must appear in allowed_peer_uids or its effective GID in
allowed_peer_gids; Unix-socket filesystem permissions still apply. Prefer allow-listing the exact
NetBox service UID, especially when socket access is granted through a supplementary group.
Keep dry_run = true through catalog and lifecycle acceptance tests. Enabling real mutation is an
explicit operator configuration change.
Store JSON contract
Both endpoint templates may be configured with or without a trailing slash. A 404 is retried once using the alternate form. Path parameters are URL-quoted. The agent accepts exactly these fields.
GET /api/v1/plugins/{slug}/:
{
"api_version": "v1",
"slug": "netbox-example",
"name": "Example",
"summary": "Example plugin",
"description": "Description",
"repository_url": "https://git.example/repo",
"latest_version": "1.2.3",
"package_name": "netbox-example",
"import_name": "netbox_example",
"min_netbox_version": "4.6.5",
"max_netbox_version": "4.6.8",
"approved": true,
"status": "approved",
"releases": []
}
GET /api/v1/plugins/{slug}/releases/{version}/ returns a release object directly:
{
"version": "1.2.3",
"download_url": "https://store.example/artifacts/netbox_example-1.2.3-py3-none-any.whl",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"artifact_size": 12345,
"commit_sha": "",
"min_netbox_version": "4.6.5",
"max_netbox_version": "4.6.8",
"published_at": "2026-08-24T12:00:00Z",
"approved": true,
"status": "approved",
"immutable": true,
"approved_payload_sha256": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"
}
If release detail returns 404, exactly one matching object in plugin releases[] is accepted. The
historic field approved_payload_sha256 is protocol-v1 naming: clients treat its lowercase 64-hex
value as an opaque Store marker, and the agent compares it exactly with the freshly fetched value.
Artifact sha256, in contrast, is always a lowercase 64-character SHA-256.
The plugin and release must be approved, the release immutable and compatible with the configured NetBox version. A wheel's filename distribution and version must match the catalog, and its byte count, digest, host, scheme and DNS addresses are checked while streaming. Redirects, source distributions, private/reserved DNS targets (unless explicitly enabled for a test environment), and catalog additions outside the v1 schema fail closed.
Client protocol
Transport is Unix SOCK_STREAM, UTF-8 JSON-lines, exactly one request and one response per
connection, maximum 64 KiB. Response shape is always
{"protocol_version":1,"status":...,"body":{...}}.
{"protocol_version":1,"method":"GET","path":"/v1/capabilities"}
{"protocol_version":1,"method":"POST","path":"/v1/operations","idempotency_key":"20f4274f-d4e5-42bf-9164-967b1a774481","body":{"request_id":"eea17d87-8944-4ee2-a076-363338ab746d","action":"install","plugin_slug":"netbox-example","version":"1.2.3","approved_payload_sha256":"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd","requested_by":"netbox:alice"}}
{"protocol_version":1,"method":"GET","path":"/v1/operations/20f4274f-d4e5-42bf-9164-967b1a774481"}
POST returns 202. Repeating the same idempotency UUID with the identical body returns the existing
operation (created:false); a different body returns 409. States are queued, running,
dry_run, succeeded, failed, or manual_recovery.
CLI examples:
netbox-store-agent submit install netbox-example --version 1.2.3 \
--approved-payload-sha256 abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd
netbox-store-agent status 20f4274f-d4e5-42bf-9164-967b1a774481
Lifecycle and fail-closed boundaries
installandupdatere-fetch plugin and release, exactly match the approval token, download and validate the wheel, then usepip --no-index --no-deps --only-binary=:all: --require-hashes.- Source archives must be tied to an exact commit. The agent rejects unsafe archive members and requires configured
unshare/setprivexecutables plus a dedicated build UID/GID. It builds without network access and with dropped privileges viapip wheel --no-index --no-deps --no-build-isolation, validates the result, and caches only that wheel for future NetBox upgrades. Reviewing source code remains a privileged trust decision because Python build backends execute code. - New installs remain disabled. Updating an enabled plugin runs NetBox
migrate,collectstatic, and restarts every configured service (netboxandnetbox-rqin the example). enablerevalidates its installed release, writes the include, migrates, collects static files, and restarts both services.disablewrites the include and restarts both.uninstallis allowed only after disable and revalidates current approved plugin identity first.- Self-management slugs are denied for every action. Package/import names and all executable paths, service units, managed paths, Store origins and NetBox compatibility are configuration/catalog policy—not caller-controlled command fragments.
- A root-owned global file lock prevents simultaneous host mutations. Interrupted
runningoperations becomemanual_recoveryat startup. Managed files are backed up per operation and atomically replaced; best-effort restoration does not claim package/service rollback.
This MVP intentionally has no TUF metadata, dependency-wheel set, transactional virtualenv switch,
or reliable rollback for package installation/database migrations/service restarts. --no-deps
means an approved plugin's dependencies must already be provisioned by the operator/base image.
Any failure after host mutation is marked manual_recovery; inspect the journal, backup directory,
installed distributions, migrations, both services and both managed files before retrying. If the
Store is unavailable or an entry is no longer approved, lifecycle calls fail closed; emergency
manual recovery remains an operator procedure outside this API.
The security boundary still depends on root ownership and permissions of the daemon executable, configuration, token, journal/state directories, socket and NetBox paths; TLS/CA integrity; Store approval operations; artifact build provenance; and a correctly restricted NetBox service account.