# 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`](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 only `STORE_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: ```python from netbox.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: ```text 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}/`: ```json { "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: ```json { "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":{...}}`. ```json {"protocol_version":1,"method":"GET","path":"/v1/capabilities"} ``` ```json {"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"}} ``` ```json {"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: ```text 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 - `install` and `update` re-fetch plugin and release, exactly match the approval token, download and validate the wheel, then use `pip --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`/`setpriv` executables plus a dedicated build UID/GID. It builds without network access and with dropped privileges via `pip 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 (`netbox` and `netbox-rq` in the example). - `enable` revalidates its installed release, writes the include, migrates, collects static files, and restarts both services. `disable` writes the include and restarts both. `uninstall` is 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 `running` operations become `manual_recovery` at 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.