Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y musl-tools

# The default feature set IS the canonical single binary (clipboard, http,
# keyring, updates) — every platform ships the same features, no variants.
# `keyring-secret-service` (D-Bus) stays off so the musl build links static.
# updates) — every platform ships the same features, no variants.
- name: Build (native)
if: ${{ !matrix.cross }}
run: cargo build --release --locked --target ${{ matrix.target }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/skills-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ on:
push:
branches: [master]
paths:
- 'SKILL.md'
- 'skills/**'
- 'scripts/lint_skills.sh'
- '.github/workflows/skills-lint.yml'
pull_request:
paths:
- 'SKILL.md'
- 'skills/**'
- 'scripts/lint_skills.sh'
- '.github/workflows/skills-lint.yml'
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ paste-ready `mcpServers` JSON (absolute binary path, echoed `--profile`/`--confi
| `nbox_list_tags` | List tags (name, slug, color, usage count) — valid `tag` values for `nbox_search`. |
| `nbox_tagged` | Objects carrying a tag, across kinds (NetBox 4.3+); `tag` (id\|name\|slug). Cross-kind reverse lookup — unlike `nbox_search --tag`, which narrows a free-text search per-endpoint. |
| `nbox_cache_clear` | Drop nbox's local read cache so the next lookups fetch fresh from NetBox (read-only w.r.t. NetBox; idempotent). |
| `nbox_plan_write` | Plan a write operation (interface description, device status, IP/prefix/IP-range reserve, tag add/remove). Builds a `MutationPlan` with a before/after diff and confirm token, without mutating. Review the plan, then call `nbox_apply_write`. Requires local stdio `--local-writes`, or shared HTTP/OIDC `--allow-writes` + `[serve.vault]` per-user credential mapping. |
| `nbox_plan_write` | Plan a write operation (interface description, device status, IP/prefix/IP-range reserve, tag add/remove). Builds a `MutationPlan` with a before/after diff and confirm token, without mutating. Review the plan, then call `nbox_apply_write`. Requires local stdio `--local-writes`, or shared HTTP/OIDC `--allow-writes` + caller `nbox:write` + `[serve.vault]` per-user credential mapping. |
| `nbox_apply_write` | Apply a previously planned write. Pass the full `MutationPlan` JSON from `nbox_plan_write`. The confirm token looks up the server-stored plan; forged, edited, or replayed plans are rejected. Returns a `MutationReceipt`. Same gating as `nbox_plan_write`. |

The same objects are also exposed as MCP **resources** via one template,
Expand Down Expand Up @@ -239,7 +239,7 @@ nbox device edge01 --json | jq '.primary_ip4'
`--count N` to reserve N IPs atomically in one call (a single list-body POST —
NetBox allocates all N or zero; `count` is bound into the confirmation token);
any failure leaves nothing created and exits 1, and the receipt's `object` is a
JSON array of the N created IPs. The MCP server is read-only by default; write tools (`nbox_plan_write`/`nbox_apply_write`) require either local stdio `--local-writes` or shared HTTP/OIDC `--allow-writes` + the per-user vault.
JSON array of the N created IPs. The MCP server is read-only by default; write tools (`nbox_plan_write`/`nbox_apply_write`) require either local stdio `--local-writes` or shared HTTP/OIDC `--allow-writes` + caller `nbox:write` + the per-user vault.
- Filters that an object type can't satisfy cause that type to be skipped in
`search` (nbox does not send NetBox unknown query params).
- `owner` (NetBox 4.5+): a native owner field (user or group) surfaced on most
Expand Down
18 changes: 10 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
gate. The server-issued plan store remains authoritative: forged, edited, or
replayed plans are rejected. Shared/network HTTP writes remain on the existing
`--allow-writes` + OIDC + `[serve.vault]` path, and HTTP profile-token writes
are intentionally deferred.
are intentionally deferred. Live NetBox CI now launches real stdio
`nbox serve` and verifies the read path, a `--local-writes` reserve/apply/readback,
and the no-`--local-writes` refusal.
- **Structured exports, two more on the same surface.** Alongside `nbox export
prometheus-sd`, the export surface now covers the `netbox-lists` niche as one
fast binary:
Expand Down Expand Up @@ -195,13 +197,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
own NetBox token via a configured credential vault (`[serve.vault.<sub>]` with
`token_env`) and bridged into the write at request time, so the same
`nbox:write` scope and ADR-0001 plan/confirm/audit lifecycle apply per
identity. It is fail-closed: a `sub` with no vault entry, or a token without
`nbox:write`, is refused, and every write is rejected over the stdio /
unauthenticated transports (no per-user identity to bridge) — for local,
single-user writes, use the equivalent CLI command (`nbox ip reserve …`,
`nbox … set …`) instead. Apply trusts only plans the server itself issued:
`nbox_apply_write` uses the submitted `confirm_token` to look up the plan the
server stored at plan time and applies that stored plan — never the
identity. It is fail-closed for the shared path: a `sub` with no vault entry,
or a token without `nbox:write`, is refused. Stdio writes are handled
separately by the ADR-0002 local mode above; HTTP/static-bearer
profile-token writes remain rejected in this release. Apply trusts only plans
the server itself issued: `nbox_apply_write` uses the submitted
`confirm_token` to look up the plan the server stored at plan time and applies
that stored plan — never the
caller-submitted contents — so a forged or edited plan (the `confirm_token` is
not a secret MAC) is rejected. An end-to-end harness exercises the path by
minting a JWT through the real OIDC → vault → write stack.
Expand Down
7 changes: 4 additions & 3 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ to preview). Reads remain the default everywhere.
**Remaining limitations:** Choosing a specific address/block, interface/VM
assignment, status/tags on reserve, and generic create/delete are still
deferred (ADR-0001 Decision 6). MCP writes are similarly opt-in: the server is
read-only by default and over stdio, exposing `nbox_plan_write` /
`nbox_apply_write` only with `[serve].allow_writes`, the `nbox:write` scope, and
a per-user `[serve.vault]` entry.
read-only by default. Local stdio writes require `--local-writes` /
`[serve].local_writes` and use the active profile token; shared HTTP/OIDC writes
require `[serve].allow_writes`, caller `nbox:write`, and a per-user
`[serve.vault]` entry. HTTP/static-bearer profile-token writes are rejected.

---

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ claude mcp add nbox -e NBOX_TOKEN=nbt_xxx.yyy -- nbox serve
```

`nbox serve` is a read-first MCP server (read-only by default; writes are opt-in
with local stdio `--local-writes` or shared HTTP/OIDC `--allow-writes` + a
per-user vault); an MCP host launches it as a subprocess and gets the same JSON
view models the CLI returns. See [docs/MCP.md](docs/MCP.md).
with local stdio `--local-writes` or shared HTTP/OIDC `--allow-writes` + caller
`nbox:write` + a per-user vault); an MCP host launches it as a subprocess and
gets the same JSON view models the CLI returns. See [docs/MCP.md](docs/MCP.md).

## Installation

Expand Down
62 changes: 35 additions & 27 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
# Roadmap

nbox is a **read-first** NetBox CLI, TUI, and MCP server. The near-term goal is the **best
possible read experience** — fast, correct, and pleasant both in the terminal and to agents.
write engine, with seven commands landed — `interface <device> <iface> set
nbox is a **read-first** NetBox CLI, TUI, and MCP server. The near-term goal is
the **best possible read experience** — fast, correct, and pleasant both in the
terminal and to agents — with a narrow, opt-in safe-write surface that widens
deliberately. Seven write commands have landed: `interface <device> <iface> set
description` and `device <name> set status <value>` (`PATCH`), `ip reserve
<prefix>` / `prefix reserve <cidr>` / `ip-range reserve <start|id>` (three
`allocate` `POST`s to `available-ips` / `available-prefixes`), and `tag add` /
`tag remove <type> <name> <tag>` (a `PATCH` to the `tags` array on any object
kind). Reads stay the default everywhere and the write surface widens only as
the read tool proves out in practice (see
[Writes](#writes--deferred-later-track)).
server-side allocation `POST`s), and `tag add` / `tag remove <type> <name>
<tag>` (tag-array `PATCH`). Reads stay the default everywhere (see
[Writes](#writes--safe-opt-in-track)).

Legend: ☐ planned · ◐ in progress · ☑ done

## Principles

- **Read-first; writes are narrow and opt-in.** Reads are the bulk of the product and get polished
first. The first writes have landed as a gated `PATCH`-based foundation (ADR-0001): minimal-diff,
before/after-previewed, confirmable, behind `--allow-writes` + confirmation — never the default.
first. The shipped writes reuse the ADR-0001 plan/confirm/audit foundation:
before/after-previewed, confirmable, behind explicit gates — never the default.
- **Agent-first.** CLI, TUI, and `nbox serve` (MCP) run off one command core; `--json`/`--envelope`/
`--fields`/`--raw` + `AGENTS.md` make every read scriptable, and the same views back the MCP tools.
- **Correctness over breadth.** Typed errors, real-NetBox integration CI, and ambiguity surfaced
(never silently guessed) before more surface area.

---

## Shipped — the read-only product
## Shipped — the read-first product

The read surface is broad and stable today (full history in `CHANGELOG.md`):

Expand All @@ -41,8 +40,10 @@ The read surface is broad and stable today (full history in `CHANGELOG.md`):
trace, VRF-scoped child prefixes + contained IPs.
- **Output:** `-o plain|json|csv`, `--raw`, `--envelope`, `--fields`, `--cols`; stable exit codes.
- **MCP server (`nbox serve`):** stdio **and** HTTP (Streamable HTTP), OAuth 2.1 OIDC resource-server
mode (RFC 9728 metadata, audit log, per-caller rate limit), 11 read tools + a `nbox://{kind}/{ref}`
resource template (DESIGN §24; read-only Pattern 3).
mode (RFC 9728 metadata, audit log, per-caller rate limit), 11 read tools,
two opt-in write tools, and a `nbox://{kind}/{ref}` resource template (DESIGN
§24). Local writes use stdio `--local-writes`; shared HTTP writes use
`--allow-writes` + OIDC `nbox:write` + `[serve.vault]`.
- **TUI:** list/preview split with focus, scrolling + position cues, 12 themes, command palette,
fuzzy filter, recents, auto-refresh, device tabs, open-in-browser/copy, profile switcher
(`P`/`Ctrl+P`), and an in-app **Config modal** (`S`) — profile editor (add/edit/select/delete),
Expand All @@ -56,9 +57,10 @@ The read surface is broad and stable today (full history in `CHANGELOG.md`):

---

## Now — best-in-class read-only UX (current focus)
## Now — best-in-class read-first UX (current focus)

Polish the read experience. No writes here.
Polish the read experience while keeping writes narrow, explicit, and covered by
the shared safe-write rails.

- ☑ **TUI search filters** — surface the CLI's `--status` / `--site` / scope / `--vrf` filters in the
TUI (filter chips + palette + `f` modal) so the TUI is as capable a search as the CLI.
Expand Down Expand Up @@ -254,8 +256,8 @@ Polish the read experience. No writes here.
## Keeping current with NetBox (4.6 → 4.7)

NetBox has moved to 4.6 (tick-tock cadence; 4.7 "tock" — may break — is the next watch).
A 2026-06 feature scan surfaced read-only, non-goal-respecting surface nbox doesn't yet
cover. All of these stay within the read-only product and the explicit non-goals.
A 2026-06 feature scan surfaced read-side, non-goal-respecting surface nbox doesn't yet
cover. All of these stay within the read-first product and the explicit non-goals.

- ☑ **MAC addresses as a first-class kind** _(NetBox 4.2)_. `nbox mac <addr>`
reverse-resolves a MAC to the interface(s)/device(s) that carry it — a top
Expand Down Expand Up @@ -400,6 +402,10 @@ reviewable PRs that lock contracts and reduce future change cost.
(`invalid_params` vs internal errors) are all pinned against direct server/tool calls in
`src/mcp/tests.rs::contracts` (not brittle protocol snapshots). Keep this number-free so new
kinds cannot stale the roadmap.
- ☑ **Live MCP E2E in the NetBox integration lane** — the ignored live suite
launches the compiled `nbox serve` over stdio and verifies read, local
`--local-writes` reserve/apply/readback, and the no-`--local-writes` refusal
against the ephemeral NetBox fixture.
- ◐ **Fixture migration pass** — move repeated inline NetBox JSON in `search_tests`, `query_tests`,
`scope_tests`, MCP tests, and custom-field tests onto `tests/support` builders as those files are
next touched.
Expand Down Expand Up @@ -429,18 +435,20 @@ reviewable PRs that lock contracts and reduce future change cost.

---

## Writes — deferred (later track)
## Writes — safe opt-in track

The safe-write **foundation** has landed ([ADR-0001](docs/adr/0001-safe-write-foundation.md)): the
shared `MutationPlan`/`MutationReceipt` engine plus the narrow `interface … set description` pilot,
gated behind `--allow-writes` + confirmation, `PATCH`-based with a before/after diff, read staying the
default everywhere. The **broader** write surface stays a deliberate later track — it widens only as
the read tool proves out in practice. Consolidated future scope:
The safe-write **foundation** has landed ([ADR-0001](docs/adr/0001-safe-write-foundation.md)):
shared `MutationPlan`/`MutationReceipt`, explicit dry-run/confirm flow,
`--allow-writes` gate for CLI apply, ETag/`If-Match` on NetBox 4.6+ with a
pre-4.6 read-before-write fallback, names-only audit events, and stdout/stderr
contract tests. The shipped write surface is deliberately narrow: two in-place
`PATCH` edits, three server-side allocation `POST`s, and tag add/remove. Reads
stay the default everywhere; future write work should continue widening by
specific safe operations rather than broad arbitrary editing.

- ☑ **Safe `PATCH` engine** — minimal diff, before/after preview, confirmation; agent-safe
read-only default. The ADR-0001 foundation landed: a shared `MutationPlan` /
`MutationReceipt` engine + the `nbox interface <device> <iface> set description
"…"` pilot (`--allow-writes` + `--confirm`/`--dry-run`, `ETag`/`If-Match` on
- ☑ **Safe write engine** — minimal diff, before/after preview, confirmation;
agent-safe read-first default. The ADR-0001 foundation covers both in-place
`PATCH` writes and server-authoritative allocation `POST`s.
- ☑ `nbox interface <device> <iface> set description "…"` — the first write
command (on the ADR-0001 foundation).
- ☑ `nbox device <name> set status <value>` — the second write command,
Expand Down
15 changes: 11 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,30 @@ Security issues of interest include:
- Leakage of the NetBox API token (in logs, error messages, or output)
- TLS verification bypass or weakening
- Memory-safety issues in API response parsing
- An unintended write, or a write that bypasses the `--allow-writes` + confirmation gate (ADR-0001)
- An unintended write, or a write that bypasses the configured write gate and
plan/confirmation lifecycle (ADR-0001/0002)

## Security Posture

- **Token storage.** nbox resolves the API token in order: the env var named by the profile's `token_env`, then `NBOX_TOKEN`, then the profile's `token` value in `config.toml`. There is no OS keyring. If you prefer to keep the token out of the config file, use `token_env`/`NBOX_TOKEN` and store only the env-var *name* in the file. When a token is saved to `config.toml`, the file is written owner-only (`0600` on Unix) and the value is redacted from `config show` / `--json` / `Debug`. The token is never logged — request logging shows only the auth-scheme marker. Inspect the active source with `nbox config token status` (never prints the value).
- **Read-first.** Reads are the default everywhere. Writes are a small set of safe, opt-in commands, each gated by `--allow-writes` AND confirmation (`--confirm`, or a TTY prompt), with a default-safe `--dry-run` preview (ADR-0001). The `raw` command stays `GET`-only. Use a read-scoped NetBox token for defense in depth, and grant write scope only where you mean to allow the gated writes.
- **Read-first.** Reads are the default everywhere. Writes are a small set of safe, opt-in commands, each gated by `--allow-writes` AND confirmation (`--confirm`, or a TTY prompt), with an explicit `--dry-run` preview (ADR-0001). The `raw` command stays `GET`-only. Use a read-scoped NetBox token for defense in depth, and grant write scope only where you mean to allow the gated writes.
- **TLS verified by default.** `verify_tls = false` is supported for labs with self-signed certs but must not be used against production.
- **Clean stdout.** Data goes to stdout; logs and errors go to stderr — safe for piping and for the `nbox serve` JSON-RPC stream.

### MCP server (`nbox serve`)

`nbox serve` exposes the read-only tools to an MCP client by default. The two write tools (`nbox_plan_write` / `nbox_apply_write`) are available only when `[serve].allow_writes` is set (or `--allow-writes`), the caller's token carries the `nbox:write` scope, and a `[serve.vault]` entry maps the caller's OIDC `sub` to a per-user NetBox token — and never over stdio or unauthenticated transports. Its network surface:
`nbox serve` exposes the read-only tools to an MCP client by default. The two write tools (`nbox_plan_write` / `nbox_apply_write`) stay opt-in:

- local stdio writes require `[serve].local_writes = true` or `--local-writes` and use the active profile token under the MCP host's local approval prompt;
- shared HTTP/OIDC writes require `[serve].allow_writes = true` or `--allow-writes`, caller `nbox:write`, and a `[serve.vault]` entry mapping the caller's OIDC `sub` to a per-user NetBox token;
- HTTP/static-bearer profile-token writes are rejected.

Its network surface:

- **stdio by default** — no network listener; the host launches nbox as a subprocess.
- **HTTP is loopback-only** unless OIDC is configured. `nbox serve --http <addr>` binds loopback and validates `Origin`/`Host` (a DNS-rebinding guard). An optional static bearer (`--http-token` / `NBOX_SERVE_TOKEN` — a secret; prefer the env var) gates `/mcp`.
- **A routable deployment is an OAuth 2.1 resource server.** A non-loopback bind requires `--oidc-issuer` + `--audience`: nbox validates inbound IdP JWTs on `/mcp` (signature via JWKS; `iss`/`aud`/`exp` checked; `alg: none` rejected) and advertises Protected Resource Metadata (RFC 9728). Terminate TLS in front (a reverse proxy).
- **Accountability, not per-user RBAC.** Reads use the single profile token, so scope that token read-only. Writes execute under a per-user vault identity keyed by the caller's OIDC `sub` (the service token is never used for writes — see [docs/MCP.md](docs/MCP.md)). An audit log (`nbox::audit`) records callers, and an optional per-caller rate limit (`--rate-limit`) bounds abuse.
- **Accountability, not per-user RBAC for reads.** Reads use the single profile token, so scope that token read-only for shared deployments. Shared HTTP writes execute under a per-user vault identity keyed by the caller's OIDC `sub`. Local stdio writes are a single-user exception: they use the active profile token only when `--local-writes` / `[serve].local_writes` is explicitly set. An audit log (`nbox::audit`) records callers, and an optional per-caller rate limit (`--rate-limit`) bounds abuse.

See [docs/MCP.md](docs/MCP.md) for the full security model.

Expand Down
Loading