Skip to content
Merged
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
23 changes: 12 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,15 @@ the CLI's query + view layer, so they return the same JSON view models. URL/toke
come from the active profile (same `--profile` / `--config` flags). JSON-RPC is on
stdout; logs go to stderr. Read tools are annotated `read_only_hint = true`.

**Write tools (Pattern 2, opt-in):** `nbox_plan_write` and `nbox_apply_write` are
available when `nbox serve --http --allow-writes` is set and a `[serve.vault]`
config maps each caller's OIDC `sub` to a per-user NetBox token (env var). They
reuse the CLI's ADR-0001 safe-write engine: plan-first, confirm-token-gated,
per-user identity. Writes require the HTTP transport (OIDC identity); stdio
stays read-only. `nbox serve --print-config` prints the
paste-ready `mcpServers` JSON (absolute binary path, echoed `--profile`/`--config`,
placeholder token) and exits — no server start, no NetBox connection.
**Write tools (opt-in):** `nbox_plan_write` and `nbox_apply_write` reuse the
CLI's ADR-0001 safe-write engine: plan-first, confirm-token-gated, and backed by
the server-issued plan store. Local stdio writes require `nbox serve
--local-writes` / `[serve].local_writes` and use the active profile token. Shared
HTTP/OIDC writes require `nbox serve --http --allow-writes` plus a
`[serve.vault]` mapping each caller's OIDC `sub` to a per-user NetBox token (env
var) and the caller's `nbox:write` scope. `nbox serve --print-config` prints the
paste-ready `mcpServers` JSON (absolute binary path, echoed `--profile`/`--config`/
`--local-writes`, placeholder token) and exits — no server start, no NetBox connection.

| Tool | Purpose |
| ---- | ------- |
Expand All @@ -140,8 +141,8 @@ placeholder token) and exits — no server start, no NetBox connection.
| `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 `--allow-writes` + `[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 is verified, then the write executes under the caller's per-user NetBox identity. Returns a `MutationReceipt`. Requires `--allow-writes`. |
| `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_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,
`nbox://{kind}/{ref}` (e.g. `nbox://device/edge01`, `nbox://ip/10.0.0.1`), for
Expand Down Expand Up @@ -238,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 `--allow-writes` + the per-user vault (Pattern 2).
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.
- 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Local stdio MCP writes (ADR-0002).** `nbox serve --local-writes` and
`[serve].local_writes = true` enable the existing safe MCP write tools
(`nbox_plan_write` / `nbox_apply_write`) for the common local subprocess setup,
using the active profile token and the MCP host's approval prompt as the human
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.
- **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
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ See [Installation](#installation) below for setup instructions.

- **IPAM-aware** — IP → most-specific parent prefix → VLAN → scope resolution, prefix utilization and children, a navigable prefix tree, and `next-ip` / `next-prefix` to preview free addresses and blocks (read-only). To actually claim one, `nbox ip reserve <prefix>` allocates the next available IP from a prefix, `nbox prefix reserve <cidr>` allocates the next available child prefix, and `nbox ip-range reserve <start|id>` allocates the next available IP from an IP range (safe writes — ADR-0001).
- **A k9s-style TUI** — a three-pane home (navigation rail → results → live preview), an overview dashboard, a hierarchical prefix tree, cross-object navigation between related objects (every detail's related-object tabs are navigable — open an interface from a device and see its cable path drawn as an A↔Z diagram), fuzzy filters, server-side name filtering on the browse list, recents, and an in-app profile + settings editor. Twelve themes; `NO_COLOR` honored.
- **Agent-ready** — `nbox serve` is a read-first MCP server (read-only by default): the same lookups exposed as eleven read tools (plus every object as an `nbox://{kind}/{ref}` resource), returning the exact JSON view models the CLI does, so AI agents (Claude Code, Claude Desktop, …) query NetBox safely. Two opt-in write tools (`nbox_plan_write` / `nbox_apply_write`) are exposed only behind `--allow-writes` + a per-user OIDC vault. Stdio for a local subprocess, or a loopback HTTP transport with OIDC resource-server auth for a network-reachable deployment. Being one static binary with zero runtime, it cold-starts MCP-ready in ~9 ms — before a Python NetBox MCP server finishes importing its dependencies ([benchmarks](docs/BENCHMARKS.md)). See [docs/MCP.md](docs/MCP.md); [SKILL.md](SKILL.md) is an installable Agent Skill that drives the CLI on matching requests.
- **Agent-ready** — `nbox serve` is a read-first MCP server (read-only by default): the same lookups exposed as eleven read tools (plus every object as an `nbox://{kind}/{ref}` resource), returning the exact JSON view models the CLI does, so AI agents (Claude Code, Claude Desktop, …) query NetBox safely. Two opt-in write tools (`nbox_plan_write` / `nbox_apply_write`) support local stdio writes with `--local-writes` (profile token) or shared HTTP/OIDC writes with `--allow-writes` + a per-user vault. Being one static binary with zero runtime, it cold-starts MCP-ready in ~9 ms — before a Python NetBox MCP server finishes importing its dependencies ([benchmarks](docs/BENCHMARKS.md)). See [docs/MCP.md](docs/MCP.md); [SKILL.md](SKILL.md) is an installable Agent Skill that drives the CLI on matching requests.
- **Scriptable** — `-o plain|json|csv`, `--fields`, `--raw`, versioned `--envelope`, and stable exit codes; stdout stays clean for piping, logs go to stderr. Plus structured exports — `nbox export prometheus-sd | address-list | device-inventory` emit fixed shapes for Prometheus SD, firewall/blocklist address lists, and device inventories. See [docs/SCRIPTING.md](docs/SCRIPTING.md).
- **Fast on repeat** — a small in-memory read cache (per profile, ~30s) keeps TUI back-navigation and chatty agents from re-hitting NetBox; the detail footer shows "cached Ns ago" and `nbox_cache_clear` busts it.
- **Multi-instance** — profiles for several NetBox instances (switch live in the TUI), journals folded into detail lookups, open-in-browser and copy, and tag listing.
Expand Down Expand Up @@ -129,8 +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
behind `--allow-writes` + a per-user OIDC 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` + 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 Expand Up @@ -309,7 +310,8 @@ nbox export <prometheus-sd|address-list|device-inventory>
# structured read-only exports (Prometheus SD JSON, firewall/blocklist
# address list, device inventory) — fixed shapes for scripting
nbox serve [--http <addr>] # read-first MCP server for AI agents (read-only by default;
# --allow-writes enables write tools over OIDC HTTP); stdio, or loopback/OIDC HTTP
# --local-writes enables local stdio writes;
# --allow-writes enables shared HTTP/OIDC writes)
# --print-config prints the paste-ready mcpServers JSON and exits
nbox config <init|path|show|token> # token: status reports the resolved source (never echoed)
nbox profile <add|use|remove|list|show>
Expand Down Expand Up @@ -510,13 +512,13 @@ flags. JSON-RPC goes to stdout; all logging stays on stderr.

The eleven read tools below are annotated read-only (`read_only_hint = true`). Two
write tools — `nbox_plan_write` / `nbox_apply_write` — are **always registered** (so
they show up in `tools/list`), but execution is gated: a call is rejected unless
writes are enabled (`--allow-writes` / `[serve].allow_writes`) AND the caller is an
OIDC-authenticated identity carrying the `nbox:write` scope with a `[serve.vault]`
entry mapping its `sub` to a per-user NetBox token. The write then runs as that user
(never over stdio; without writes enabled the tools reject with "writes disabled").
`nbox_apply_write` applies the plan the server stored at plan time — looked up by the
`confirm_token` you pass back — not the plan JSON itself, so a forged plan is rejected.
they show up in `tools/list`), but execution is gated. Local stdio writes require
`--local-writes` / `[serve].local_writes` and run under the active profile token.
Shared HTTP/OIDC writes require `--allow-writes` / `[serve].allow_writes`, an
OIDC-authenticated caller carrying `nbox:write`, and a `[serve.vault]` entry mapping
the caller's `sub` to a per-user NetBox token. `nbox_apply_write` applies the plan
the server stored at plan time — looked up by the `confirm_token` you pass back —
not the plan JSON itself, so a forged plan is rejected.

| Tool | What |
|------|------|
Expand Down Expand Up @@ -557,7 +559,7 @@ claude mcp add nbox -- nbox serve

Or skip the hand-editing — `nbox serve --print-config` prints the ready-to-paste
`mcpServers` object (with the absolute binary path and any `--profile`/`--config`
you passed echoed into `args`); see [docs/MCP.md](docs/MCP.md) for the exact
or `--local-writes` you passed echoed into `args`); see [docs/MCP.md](docs/MCP.md) for the exact
config-file path per host (Claude Code, Claude Desktop, Cursor).

Both reuse the same `config.toml` / `NBOX_TOKEN` as the CLI. Prefer driving the CLI
Expand Down Expand Up @@ -592,11 +594,12 @@ single profile token, so scope that token read-only. An audit log
(`nbox::audit`) and an optional per-caller rate limit (`--rate-limit`) round it
out. Full setup, security model, and IdP notes: [docs/MCP.md](docs/MCP.md).

Per-user NetBox identity bridging (so NetBox sees the real caller) and the safe
MCP write tools have shipped: a `[serve.vault]` mapping each caller's OIDC `sub`
to a per-user NetBox token (via `token_env`), and `nbox_plan_write` /
`nbox_apply_write` behind `--allow-writes`. Only a raw escape-hatch MCP tool comes
later.
For local single-user MCP writes, use stdio plus `--local-writes`; the write tools
reuse the profile token and the MCP host's tool-approval prompt is the human gate.
For shared/network writes where NetBox must see the real caller, use
`--allow-writes` plus `[serve.vault]` to map each caller's OIDC `sub` to a per-user
NetBox token (via `token_env`). HTTP profile-token writes are intentionally
deferred; only a raw escape-hatch MCP tool comes later.

## NetBox Compatibility

Expand Down
7 changes: 6 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,12 @@ the read tool proves out in practice. Consolidated future scope:
- ☑ **Write-capable MCP tools + per-user credential vault (Pattern 2)** — `nbox_plan_write` and
`nbox_apply_write` tools reuse the CLI's ADR-0001 safe-write engine; the `[serve.vault]` config maps
OIDC `sub` → per-user NetBox token (env var), bridged at request time via `NetBoxClient::with_token`.
Writes require `--allow-writes` + HTTP transport (OIDC identity); stdio stays read-only.
Shared HTTP writes require `--allow-writes`, OIDC identity, `nbox:write`, and a vault entry.
- ☑ **Local stdio MCP writes (ADR-0002)** — `nbox serve --local-writes` /
`[serve].local_writes = true` enables the same `nbox_plan_write` /
`nbox_apply_write` tools for the local subprocess case, using the active
profile token and binding plans to a synthetic local actor. HTTP local writes
remain deferred until a separate guard decision.
- ☐ TUI edit mode (`e` / `d` / confirm).
- ☐ `nbox raw POST|PATCH|DELETE`; OPTIONS write-capability discovery (optional `schema` command; would
also enable value-level filter validation beyond today's typed allowlist, netbox#6489).
Expand Down
6 changes: 3 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ behavior should get an ADR before it becomes a public contract.
- **`mcp/`** — the MCP server (`nbox serve`, read-only by default): stdio plus a
loopback HTTP transport (OIDC resource-server auth, audit log, per-caller rate
limit), exposing the same query + view layer as thirteen tools (eleven read
tools plus the `nbox_plan_write`/`nbox_apply_write` write pair, the latter
enabled only with `--allow-writes`/`[serve].allow_writes` + a per-user vault
and never over stdio), `nbox://{kind}/{ref}` resources, and a prompts catalog.
tools plus the `nbox_plan_write`/`nbox_apply_write` write pair, enabled only by
local stdio `--local-writes` or shared HTTP/OIDC `--allow-writes` plus a
per-user vault), `nbox://{kind}/{ref}` resources, and a prompts catalog.

## Data flow

Expand Down
4 changes: 2 additions & 2 deletions docs/COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ Notes:
- Feeding an AI agent — `nbox serve` is read-only by default (11 read tools,
stdio or loopback HTTP with OIDC) returning the same JSON view models the CLI
does, and can expose 2 write tools (`nbox_plan_write` / `nbox_apply_write`)
when `[serve].allow_writes` is set over the authenticated HTTP/OIDC transport;
stdio and unauthenticated transports stay read-only.
with local stdio `--local-writes` or shared HTTP/OIDC `[serve].allow_writes`
plus a per-user vault; unauthenticated HTTP stays read-only.

### The NetBox web UI

Expand Down
6 changes: 6 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ Absent ⇒ stdio (no HTTP). The matching CLI flags always override these keys.
| `jwks_url` | JWKS URL override; absent ⇒ discover from the issuer. | `--oidc-jwks-url` |
| `allowed_hosts` | Extra hostnames for the DNS-rebinding allow-list (OIDC/routable mode only), on top of the audience host + loopback. | `--allowed-host` |
| `rate_limit` | Per-caller request cap on `/mcp`, in requests per minute. Absent / `0` ⇒ disabled. | `--rate-limit` |
| `local_writes` | Enable local single-user MCP writes over stdio using the active profile token. Separate from `allow_writes`; rejected with HTTP in this release. | `--local-writes` |
| `allow_writes` | Enable shared HTTP/OIDC MCP writes when paired with `[serve.vault]` and callers carrying `nbox:write`. Does not imply `local_writes`. | `--allow-writes` |

```toml
[serve]
Expand All @@ -242,6 +244,10 @@ http = "127.0.0.1:8080"
# jwks_url = "https://idp.example.com/keys"
# allowed_hosts = ["nbox.example.com"]
# rate_limit = 120
# local stdio writes:
# local_writes = true
# shared HTTP/OIDC writes:
# allow_writes = true
```

See [docs/MCP.md](MCP.md) for the full server story.
Expand Down
Loading