From d550fc81b55d7fc3e82704338df55d7a755125e8 Mon Sep 17 00:00:00 2001 From: Lance Tuller Date: Mon, 29 Jun 2026 10:46:04 -0400 Subject: [PATCH] docs: true up the doc set for local-writes; fix compat/roadmap/skill-lint drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADR-0002 local stdio write mode re-introduced drift across the doc set; this sweep makes the MCP write story consistent and corrects stale/broken docs. - MCP writes: every surface (README, MCP.md, SECURITY, KNOWN_ISSUES, AGENTS, skills, comparison) now distinguishes local stdio `--local-writes` (profile token) from shared HTTP/OIDC `--allow-writes` + `nbox:write` + `[serve.vault]`. - ROADMAP: drop stale read-only framing (read-first product/UX), fix the Writes section heading + anchor, note the live MCP E2E CI coverage. - COMPATIBILITY: split 4.5 vs 4.6 so ETag/If-Match is attributed only to 4.6, and expand the allocate-write note to all three reserve commands. - ARCHITECTURE: document mutation.rs, write_audit.rs, and the write flow. - BENCHMARKS: mark the numbers historical (0.13.0-era), not current 0.14. - Skill lint now covers the root SKILL.md (lint_skills.sh + skills-lint trigger). - release.yml: drop the stale keyring build comment (keyring removed in 0.8.0). Docs + workflows only — no Rust change. --- .github/workflows/release.yml | 3 +- .github/workflows/skills-lint.yml | 2 + AGENTS.md | 4 +- CHANGELOG.md | 18 +++++---- KNOWN_ISSUES.md | 7 ++-- README.md | 6 +-- ROADMAP.md | 62 +++++++++++++++++-------------- SECURITY.md | 15 ++++++-- SKILL.md | 8 ++-- docs/ARCHITECTURE.md | 35 ++++++++++++++--- docs/BENCHMARKS.md | 7 ++-- docs/COMPARISON.md | 13 ++++--- docs/COMPATIBILITY.md | 20 +++++----- docs/CONFIG.md | 11 +++++- docs/FEATURES.md | 9 +++-- docs/MCP.md | 23 +++++++----- docs/SCRIPTING.md | 4 +- scripts/lint_skills.sh | 15 +++++--- skills/serve/SKILL.md | 9 +++-- skills/writes/SKILL.md | 11 +++--- tests/integration/README.md | 7 +++- 21 files changed, 180 insertions(+), 109 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e7fb73..1ca657a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/.github/workflows/skills-lint.yml b/.github/workflows/skills-lint.yml index 90f33e7..9e1ad13 100644 --- a/.github/workflows/skills-lint.yml +++ b/.github/workflows/skills-lint.yml @@ -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' diff --git a/AGENTS.md b/AGENTS.md index 66dfca6..1ed87ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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, @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a406b8a..1c276ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: @@ -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.]` 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. diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index b41662c..3f68ebb 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -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. --- diff --git a/README.md b/README.md index 57ced17..5ffb09d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index e719a1a..2b5f283 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,23 +1,22 @@ # 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 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 set description` and `device set status ` (`PATCH`), `ip reserve ` / `prefix reserve ` / `ip-range reserve ` (three -`allocate` `POST`s to `available-ips` / `available-prefixes`), and `tag add` / -`tag remove ` (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 +` (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 @@ -25,7 +24,7 @@ Legend: ☐ planned · ◐ in progress · ☑ done --- -## Shipped — the read-only product +## Shipped — the read-first product The read surface is broad and stable today (full history in `CHANGELOG.md`): @@ -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), @@ -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. @@ -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 ` reverse-resolves a MAC to the interface(s)/device(s) that carry it — a top @@ -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. @@ -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 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 set description "…"` — the first write command (on the ADR-0001 foundation). - ☑ `nbox device set status ` — the second write command, diff --git a/SECURITY.md b/SECURITY.md index ba1fdcb..69de31c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 ` 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. diff --git a/SKILL.md b/SKILL.md index 912ebdc..2cdafcf 100644 --- a/SKILL.md +++ b/SKILL.md @@ -5,7 +5,7 @@ description: Query and modify NetBox (DCIM/IPAM) from the shell with the `nbox` # nbox — NetBox from the shell -`nbox` is a CLI / TUI / MCP client for [NetBox](https://github.com/netbox-community/netbox) +`nbox` is a CLI / TUI / MCP server for [NetBox](https://github.com/netbox-community/netbox) (DCIM + IPAM). Use it to answer questions about network inventory and addressing without clicking through the NetBox web UI. Reads are the default; seven safe-write commands (interface description, device status, IP/prefix/ip-range reserve, tag @@ -72,8 +72,10 @@ matches the question: `interface` (the cable-path A↔Z trace) / `mac` reverse-resolve, plus `rack` / `site` context - [MCP server](skills/serve/SKILL.md) — `nbox serve` as the MCP server (read-only - by default; optional `--allow-writes` write tools): stdio vs HTTP+OIDC, the read - tools and resources, the prompts catalog, and `--print-config` + by default; local stdio writes use `--local-writes`; shared HTTP/OIDC writes + use `--allow-writes` + caller `nbox:write` + `[serve.vault]`; HTTP + local/profile-token writes are rejected): stdio vs HTTP+OIDC, the read tools + and resources, the prompts catalog, and `--print-config` ## Safe writes diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c123c13..05a2112 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -10,13 +10,18 @@ behavior should get an ADR before it becomes a public contract. ## Layers - **`netbox/`** — NetBox clients and wire types. REST is canonical and powers - search, identity resolution, detail lookups, journals, `raw`, and the - available-IP/prefix math; GraphQL is an opt-in accelerator for the VRF and + search, identity resolution, detail lookups, journals, `raw`, available + previews, and safe writes; GraphQL is an opt-in accelerator for the VRF and route-target views. - `client.rs` — auth, paging, timeouts; retries HTTP 429 (`Retry-After` + backoff); maps statuses to typed errors (401→auth, 403→perms, 404→not-found). - The same authenticated client owns `/graphql/` POSTs. Holds the profile's - per-surface `ApiConfig` and exposes `api_preference`/`effective_backend`. + The same authenticated client owns `/graphql/` POSTs plus write `PATCH`/`POST` + calls. Holds the profile's per-surface `ApiConfig` and exposes + `api_preference`/`effective_backend`. + - `mutation.rs` — ADR-0001 `MutationPlan` / `MutationReceipt`, operation kind, + scoped before/after diffs, confirmation token, and write preconditions. + - `write_audit.rs` — one names-only tracing event per write outcome; never logs + raw patches, full objects, tokens, or message bodies. - `endpoints.rs` — endpoint paths. - `pagination.rs` — `Page`; `list` is one offset page, `list_all` follows the server's `next` link across pages. - `query.rs` — per-object resolvers (`*_by_ref`, candidates, scope labels). @@ -50,8 +55,9 @@ behavior should get an ADR before it becomes a public contract. 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, 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. + local stdio `--local-writes` or shared HTTP/OIDC `--allow-writes` plus caller + `nbox:write` and a per-user vault), `nbox://{kind}/{ref}` resources, and a + prompts catalog. ## Data flow @@ -67,6 +73,23 @@ CLI args ─► lib::run ─► query/search ─► netbox::client ─► NetBox The TUI replaces the last step with the ratatui render loop, reusing the same `domain` view models via `domain::detail`. +Safe writes use the same identity and view layer but split planning from apply: + +``` +write intent ─► domain planner ─► MutationPlan ─► gate / confirm + │ + ▼ + netbox::client PATCH/POST ─► NetBox REST + │ + ▼ + MutationReceipt + write_audit event +``` + +In-place updates (`PATCH`) carry an `ETag`/`If-Match` precondition on NetBox 4.6+ +or a conservative `last_updated` + before-hash re-read on 4.2–4.5. Allocation +writes (`available-ips` / `available-prefixes` POSTs) are server-authoritative +and carry no client precondition. + ## Output contracts Scriptable JSON is a compatibility surface. The broad `tests/output_flags_tests.rs` diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 315abc2..ff32915 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -38,10 +38,11 @@ What each row is: top — so comparing nbox's *full* MCP-ready time against this *imports-only* floor understates nbox's lead, not the reverse. -## Results +## Historical Results -Host: AMD Ryzen Threadripper 7970X, Linux 6.17 x86_64. nbox 0.13.0 (static musl), -Python 3.12.3, `mcp` + `pynetbox` from PyPI. +Host: AMD Ryzen Threadripper 7970X, Linux 6.17 x86_64. nbox release build +(static musl; measured from a 0.13.0-era build), Python 3.12.3, +`mcp` + `pynetbox` from PyPI. | Cold start (lower is better) | median | min | |-------------------------------------|--------:|--------:| diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index 4bcc694..1980d87 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -5,8 +5,8 @@ questions you ask at the terminal — *what is this IP, where is this device, wh owns this prefix?* — from the shell, a TUI, or an MCP server, against the same NetBox you already run. Reads are the default; nbox also performs a small set of safe writes — interface/device field edits, IP/prefix/IP-range reservation, -and tag add/remove — each gated behind `--allow-writes` + confirmation with a -default `--dry-run` preview. The tables below compare nbox against +and tag add/remove — with an explicit `--dry-run` preview and apply gated behind +`--allow-writes` + confirmation. The tables below compare nbox against the alternatives a NetBox user already has — the NetBox web UI, raw REST over `curl`, and `pynetbox` (the official Python client) — and say when to reach for which. @@ -24,14 +24,14 @@ which. | Works over SSH with no runtime | ✓ (single static binary) | ✗ (needs a browser) | ◐ (only if curl present) | ✗ (needs Python + the package) | | Machine output (JSON/CSV) + stable exit codes | ✓ (built in) | ✗ | ◐ (you build it) | ◐ (you build it) | | Built-in AI-agent access (MCP) | ✓ (`nbox serve`) | ✗ | ✗ | ✗ | -| Reserve / allocate / edit (writes) | ✓ (gated: `--allow-writes` + `--confirm`, default `--dry-run`) | ✓ | ✓ | ✓ | +| Reserve / allocate / edit (writes) | ✓ (explicit `--dry-run`; apply with `--allow-writes` + `--confirm`) | ✓ | ✓ | ✓ | | Cross-object navigation (device ↔ IP ↔ prefix ↔ VLAN) | ✓ (TUI `R`; resolved inline in CLI) | ✓ (links) | ✗ (you chase ids) | ✗ (you chase ids) | | Learning curve | low (subcommands + flags) | low (point and click) | high (endpoints, filters, joins) | medium (object model, Python) | Notes: - nbox is **read-only by default**. A small set of safe writes is available - behind `--allow-writes` + confirmation (with `--dry-run`); for bulk/admin + behind `--allow-writes` + confirmation (preview with explicit `--dry-run`); for bulk/admin mutation use the web UI or pynetbox. - `next-ip` / `next-prefix` query NetBox's available-IPs / available-prefixes endpoints (read-only; nothing is reserved in NetBox); `next-prefix --length L` @@ -61,8 +61,9 @@ 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`) - with local stdio `--local-writes` or shared HTTP/OIDC `[serve].allow_writes` - plus a per-user vault; unauthenticated HTTP stays read-only. + with local stdio `--local-writes` or shared HTTP/OIDC `[serve].allow_writes`, + caller `nbox:write`, and a per-user vault; unauthenticated HTTP stays + read-only. ### The NetBox web UI diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 508309c..b4fe495 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -16,22 +16,22 @@ flag, and the per-surface backend routing. ## Matrix -| Concern | 4.2 | 4.3 | 4.5 / 4.6 | -|---|---|---|---| -| Scope model | polymorphic `scope` (`scope_type` + `scope_id`); prefix `site` FK dropped ¹ | same | same | -| Search backend | REST `q=` fan-out | REST (GraphQL has no `q`) | REST (GraphQL has no `q`) | -| GraphQL filtering | per-field input objects | advanced per-field lookups (AND/OR, custom fields) ² | same; 4.6 adds a query-depth cap ⁴ | -| Prefix `utilization` | returned by the REST API | returned by the REST API | not returned → computed client-side ³ | -| `/api/status/` auth | open by default | open by default | requires auth ³ | -| Token scheme | v1 `Authorization: Token` | v1 `Token` | v1 `Token` **+ v2 `Authorization: Bearer nbt_…`** ¹ | -| Write concurrency | no `ETag`/`If-Match`; read-before-write fallback ⁵ | same | **`ETag` + `If-Match` (412)**; 4.2–4.5 keep the fallback ⁵ | +| Concern | 4.2 | 4.3 | 4.5 | 4.6 | +|---|---|---|---|---| +| Scope model | polymorphic `scope` (`scope_type` + `scope_id`); prefix `site` FK dropped ¹ | same | same | same | +| Search backend | REST `q=` fan-out | REST (GraphQL has no `q`) | REST (GraphQL has no `q`) | REST (GraphQL has no `q`) | +| GraphQL filtering | per-field input objects | advanced per-field lookups (AND/OR, custom fields) ² | same | same + query-depth cap ⁴ | +| Prefix `utilization` | returned by the REST API | returned by the REST API | not returned → computed client-side ³ | not returned → computed client-side ³ | +| `/api/status/` auth | open by default | open by default | requires auth ³ | requires auth ³ | +| Token scheme | v1 `Authorization: Token` | v1 `Token` | v1 `Token` **+ v2 `Authorization: Bearer nbt_…`** ¹ | v1 `Token` **+ v2 `Bearer nbt_…`** ¹ | +| Write concurrency | no `ETag`/`If-Match`; read-before-write fallback ⁵ | same | same | **`ETag` + `If-Match` (412)** ⁵ | ¹ In the official NetBox release notes — the `4.2.0` scope change (Jan 2025) and the `4.5` v2 tokens (HMAC, `nbt_` prefix, `Bearer`). v1 tokens are **deprecated but retained through the 4.x line; removal is planned for v5.0** (4.6 pushed this out from the originally-announced 4.7). nbox auto-detects the scheme, so the timeline doesn't affect it. ² NetBox [#7598](https://github.com/netbox-community/netbox/issues/7598), "adopt advanced query filtering in GraphQL." GraphQL never had a REST-style full-text `q`; this rework is why a per-kind GraphQL search can't stand in for REST search. ³ **Observed** against live instances (4.2 vs 4.5.10), **not called out in the release notes** — so treat as empirical, not a documented contract. `/api/status/` auth may reflect instance `LOGIN_REQUIRED`-style config rather than a strict version change; either way nbox authenticates **every** request (including the version probe), so it is unaffected. ⁴ NetBox 4.6 adds `GRAPHQL_MAX_QUERY_DEPTH`. nbox's GraphQL accelerators (the VRF and route-target bundles) issue nested queries; unsupported schemas resolve to REST in `nbox status`, and a runtime bundle failure (including a low depth cap) retries the same detail over REST with a warning. Search is REST regardless, so it's unaffected. -⁵ NetBox 4.6 returns an `ETag` on REST object-detail responses and honors `If-Match` on writes (a stale object yields `412 Precondition Failed`). The safe-write engine (ADR-0001 §3) records the `ETag` on the read-before-write and sends `If-Match` on apply when present; on 4.2–4.5 (no `ETag`) it falls back to a `last_updated` + before-hash read-before-write check. So 4.6+ gets race protection in one `PATCH`; older releases get a conservative re-read guard. This concurrency story is for in-place (`PATCH`) writes; the allocate write (`ip reserve` → `POST …/available-ips/`) is the exception — the endpoint is server-side race-safe on every supported release (NetBox never hands out the same address twice, returning `201` with the created object or `409` when exhausted), so it carries no client precondition on any version. Writes are off by default behind `--allow-writes` + confirmation. +⁵ NetBox 4.6 returns an `ETag` on REST object-detail responses and honors `If-Match` on writes (a stale object yields `412 Precondition Failed`). The safe-write engine (ADR-0001 §3) records the `ETag` on the read-before-write and sends `If-Match` on apply when present; on 4.2–4.5 (no `ETag`) it falls back to a `last_updated` + before-hash read-before-write check. So 4.6+ gets race protection in one `PATCH`; older releases get a conservative re-read guard. This concurrency story is for in-place (`PATCH`) writes; allocate writes (`ip reserve`, `prefix reserve`, `ip-range reserve`) are the exception — NetBox's `available-ips` / `available-prefixes` endpoints are server-side race-safe on every supported release (returning `201` with the created object or `409` when exhausted), so they carry no client precondition on any version. Writes are off by default behind `--allow-writes` + confirmation. ## How nbox adapts diff --git a/docs/CONFIG.md b/docs/CONFIG.md index fe7d5bc..43a7921 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -219,7 +219,9 @@ file with `--config `. ## MCP server (`[serve]`) The optional `[serve]` table holds defaults for `nbox serve` (the MCP server). -Absent ⇒ stdio (no HTTP). The matching CLI flags always override these keys. +Absent ⇒ stdio (no HTTP). For single-value settings, CLI flags override config. +`allowed_hosts` is additive, and boolean gates (`allow_writes`, +`local_writes`) are enabled if either the flag or config key is true. | Key | Effect | Flag | |-----|--------|------| @@ -248,8 +250,15 @@ http = "127.0.0.1:8080" # local_writes = true # shared HTTP/OIDC writes: # allow_writes = true + +# [serve.vault."alice@example.com"] +# token_env = "NETBOX_TOKEN_ALICE" ``` +Each `[serve.vault.""]` entry maps an OIDC `sub` to an environment variable +containing that user's NetBox token. nbox stores only the environment variable +name, never the token value. + See [docs/MCP.md](MCP.md) for the full server story. ## Logging diff --git a/docs/FEATURES.md b/docs/FEATURES.md index f1d35b8..0aa7c1e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -161,15 +161,16 @@ subprocess and speaks JSON-RPC over stdin/stdout; the tools reuse the CLI's quer stderr; URL/token from the active profile (same `-p`/`--config` flags). The read tools are annotated read-only; the write tools (`nbox_plan_write`/`nbox_apply_write`) are not and execute only with local stdio `--local-writes` or shared HTTP/OIDC -`--allow-writes` plus a vault. `nbox serve --print-config` prints the paste-ready +`--allow-writes` plus caller `nbox:write` and a vault. +`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 connection; see docs/MCP.md for the per-host -config-file path. +`--local-writes`, placeholder token) and exits — no server start, no connection; +see docs/MCP.md for the per-host config-file path. | Tool | What | | ---- | ---- | | `nbox_status` | Connection + active backend capabilities + NetBox/Django/Python versions + a token-validity preflight (`token`: `valid`/`invalid`/`unverified`; NetBox 4.5+). | -| `nbox_search` | Search devices/sites/racks/IPs/prefixes/VLANs/circuits/virtual-circuits/aggregates/ASNs/IP-ranges/tenants/contacts/providers/VMs/clusters/VRFs/route-targets; `query`, `limit`, `status`, `site`, `region`, `site_group`, `location`, `tenant`, `role`, `tag`, `owner`/`owner_group` (4.5+; user/group by name), `vrf` (id\|rd\|name; IP/prefix only). | +| `nbox_search` | Search devices/sites/racks/rack-groups/IPs/prefixes/VLANs/circuits/virtual-circuits/aggregates/ASNs/IP-ranges/tenants/contacts/providers/VMs/VM-types/clusters/VRFs/route-targets; `query`, `limit`, `status`, `site`, `region`, `site_group`, `location`, `tenant`, `role`, `tag`, `owner`/`owner_group` (4.5+; user/group by name), `vrf` (id\|rd\|name; IP/prefix only). | | `nbox_get` | One object by `kind` (device, ip, prefix, vlan, site, rack, rack_group, circuit, virtual_circuit, aggregate, asn, ip_range, tenant, contact, provider, vm, vm_type, cluster, vrf, route_target, mac, interface) + `ref`; `vrf`/`site`/`group` disambiguate. | | `nbox_get_interface` | One interface on a device, with its cable-path trace. | | `nbox_next_ip` | Next available address(es) in a prefix. | diff --git a/docs/MCP.md b/docs/MCP.md index 819d179..ce37712 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -95,11 +95,12 @@ $ nbox serve --print-config ``` The `command` is the absolute path to this `nbox` binary (so the host finds it -even if `nbox` isn't on its `PATH`); `args` echoes any `--profile`/`--config` -you passed so the snippet reproduces your invocation; and `NBOX_TOKEN` is a -placeholder — set it there, export it in your shell, or drop the `env` block -entirely if `nbox config init` already holds the token. (This prints the stdio -recipe; the HTTP/OIDC transport is configured separately — see below.) +even if `nbox` isn't on its `PATH`); `args` echoes any `--profile`/`--config`/ +`--local-writes` you passed so the snippet reproduces your invocation; and +`NBOX_TOKEN` is a placeholder — set it there, export it in your shell, or drop +the `env` block entirely if `nbox config init` already holds the token. (This +prints the stdio recipe; the HTTP/OIDC transport is configured separately — see +below.) ### Where each host reads it @@ -127,11 +128,13 @@ nbox serve --http 127.0.0.1:8080 (The transport lives behind the `http` cargo feature, which is on by default; `cargo install nbox --no-default-features` gives a lean stdio-only build.) -The same thirteen tools are mounted at `/mcp` (Streamable HTTP) — the 11 read -tools plus the two write tools, which reject unless writes are enabled. It binds **only** -loopback: a non-loopback address (e.g. `0.0.0.0:8080`) is a usage error unless -the OIDC resource-server auth mode is configured (see below) — there is no other -bypass flag. The trust boundary is the loopback interface; the same profile/token +The same thirteen tools are mounted at `/mcp` (Streamable HTTP). The two write +tools are discoverable there too, but on HTTP they execute only in shared +OIDC/vault mode (`--allow-writes`, caller `nbox:write`, and a vault entry); +loopback/static-bearer profile-token writes reject. It binds **only** loopback: +a non-loopback address (e.g. `0.0.0.0:8080`) is a usage error unless the OIDC +resource-server auth mode is configured (see below) — there is no other bypass +flag. The trust boundary is the loopback interface; the same profile/token resolution and `-p`/`--config` flags apply. Security on the HTTP path: diff --git a/docs/SCRIPTING.md b/docs/SCRIPTING.md index 0a496e6..93c8585 100644 --- a/docs/SCRIPTING.md +++ b/docs/SCRIPTING.md @@ -198,7 +198,7 @@ The eleven read tools, all annotated read-only: | Tool | What | |------|------| -| `nbox_status` | Connection + backend capabilities + NetBox/Django/Python versions (call first). | +| `nbox_status` | Connection + backend capabilities + NetBox/Django/Python versions + token preflight (`valid`/`invalid`/`unverified`; user on valid). Call first. | | `nbox_search` | Search across object types; `query` (required) plus scope/filter args. | | `nbox_get` | Fetch one object by `kind` + `ref` (`vrf`/`site`/`group` disambiguate). | | `nbox_get_interface` | One interface on a device, with its cable-path trace. | @@ -212,7 +212,7 @@ The eleven read tools, all annotated read-only: Two more tools — `nbox_plan_write` and `nbox_apply_write` (`read_only_hint = false`) — execute only with local stdio `--local-writes` or shared HTTP/OIDC -`--allow-writes`; see [MCP.md](MCP.md). +`--allow-writes` plus caller `nbox:write` and a vault entry; see [MCP.md](MCP.md). The same objects are also exposed as MCP resources via one template, `nbox://{kind}/{ref}` (e.g. `nbox://device/edge01`) — reading one returns the diff --git a/scripts/lint_skills.sh b/scripts/lint_skills.sh index 08f1c98..45028ba 100755 --- a/scripts/lint_skills.sh +++ b/scripts/lint_skills.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# Lint the skills/ catalog shape: each skills/*/SKILL.md must have YAML -# frontmatter with a non-empty `name` and `description`. Flag-free by design — -# the lint checks the file structure, not the flag content (flags live in -# `nbox --help` and can't drift here). +# Lint the agent skill catalog shape: root SKILL.md and each skills/*/SKILL.md +# must have YAML frontmatter with a non-empty `name` and `description`. +# Flag-free by design — the lint checks the file structure, not the flag content +# (flags live in `nbox --help` and can't drift here). # # Usage: scripts/lint_skills.sh # Exit 0 if all skill files pass, 1 otherwise. @@ -18,7 +18,7 @@ if [ ! -d "$skills_dir" ]; then exit 1 fi -# Find every SKILL.md under skills/. +# Check the root skill plus every SKILL.md under skills/. while IFS= read -r -d '' skill_file; do # Must start with `---` frontmatter. if ! head -1 "$skill_file" | grep -q '^---$'; then @@ -41,7 +41,10 @@ while IFS= read -r -d '' skill_file; do echo "error: $skill_file: frontmatter missing non-empty 'description:'" >&2 errors=$((errors + 1)) fi -done < <(find "$skills_dir" -name 'SKILL.md' -print0) +done < <( + printf '%s\0' "$root/SKILL.md" + find "$skills_dir" -name 'SKILL.md' -print0 +) if [ "$errors" -gt 0 ]; then echo "error: $errors skill file(s) failed lint" >&2 diff --git a/skills/serve/SKILL.md b/skills/serve/SKILL.md index ef046b5..33ab389 100644 --- a/skills/serve/SKILL.md +++ b/skills/serve/SKILL.md @@ -1,6 +1,6 @@ --- name: nbox-serve -description: Run nbox as an MCP server (read-only by default) so an agent host can query NetBox over JSON-RPC — stdio or HTTP+OIDC transport, the read tools (search/get/get_interface/history/…), the nbox://{kind}/{ref} resource template, the investigation prompts catalog, and --print-config for install recipes. Use when the user wants to wire NetBox into an MCP host or stand up the server. +description: Run nbox as an MCP server (read-only by default; optional local stdio writes with --local-writes, or shared HTTP/OIDC writes with --allow-writes + nbox:write + [serve.vault]) so an agent host can query NetBox over JSON-RPC — stdio or HTTP+OIDC transport, the read tools (search/get/get_interface/history/…), the nbox://{kind}/{ref} resource template, the investigation prompts catalog, and --print-config for install recipes. Use when the user wants to wire NetBox into an MCP host or stand up the server. --- # nbox serve (MCP server, read-only by default) @@ -16,7 +16,9 @@ For the flags, run `nbox serve --help` — this skill is flag-free by design. - **stdio (default).** An MCP host launches `nbox serve` as a subprocess and speaks JSON-RPC over stdin/stdout. JSON-RPC on stdout, logs on stderr; URL/token come from the active profile (same `--profile` / `--config` flags). - Always read-only. + Read-only by default; enable local single-user MCP writes with + `nbox serve --local-writes` or `[serve].local_writes = true`, using the active + profile token. - **HTTP** — `nbox serve --http 127.0.0.1:8080`, same tools mounted at `/mcp`, loopback only with `Origin`/`Host` validation and an optional static bearer. Add `--oidc-issuer ` + `--audience ` for OAuth 2.1 @@ -82,7 +84,8 @@ The MCP server is read-only by default. The write tools (`nbox_plan_write` / one explicit write mode: local stdio `nbox serve --local-writes`, which uses the active profile token, or shared HTTP/OIDC `nbox serve --http --allow-writes` plus the caller's `nbox:write` scope and a `[serve.vault]` entry mapping their OIDC -`sub` to a per-user NetBox token. HTTP local writes are deferred in this release. +`sub` to a per-user NetBox token. HTTP/static-bearer profile-token writes reject +in this release. `nbox_apply_write` applies the plan the server stored at plan time (looked up by the `confirm_token` from `nbox_plan_write`), not the plan you resubmit. For that lifecycle, see the [safe writes](../writes/SKILL.md) skill. diff --git a/skills/writes/SKILL.md b/skills/writes/SKILL.md index f94d1d5..f9c59d9 100644 --- a/skills/writes/SKILL.md +++ b/skills/writes/SKILL.md @@ -17,9 +17,10 @@ Every write follows the same path, so an agent can learn it once: diff, performs no mutation. Needs neither `--allow-writes` nor `--confirm`. With `--json`, returns the stable `MutationPlan` JSON (schema_version 1). -2. **Apply** (`--allow-writes --confirm`) — builds the same plan, checks the - precondition (ETag/If-Match on 4.6+, last_updated on pre-4.6), and applies - it. With `--json`, returns a `MutationReceipt` (schema_version 1). +2. **Apply** (`--allow-writes --confirm`) — builds the same plan and applies it. + PATCH writes check the precondition (ETag/If-Match on 4.6+, last_updated on + pre-4.6); allocate POSTs are server-authoritative and carry no client + precondition. With `--json`, returns a `MutationReceipt` (schema_version 1). 3. **Refusal** — `--confirm` without `--allow-writes` is a usage error (exit 2, empty stdout). Non-TTY / `--json` / `--no-tui` never prompts. @@ -74,8 +75,8 @@ explicit mode: local stdio `nbox serve --local-writes`, which uses the active profile token, or shared HTTP/OIDC `nbox serve --http --allow-writes` plus a `[serve.vault]` config mapping each caller's OIDC `sub` to a per-user NetBox token (each shared write runs as the calling user, and the caller's token must -carry the `nbox:write` scope). HTTP local writes are deferred. See the -[serve skill](../serve/SKILL.md), ADR-0001, and ADR-0002. +carry the `nbox:write` scope). HTTP/static-bearer profile-token writes reject. +See the [serve skill](../serve/SKILL.md), ADR-0001, and ADR-0002. ## Reference diff --git a/tests/integration/README.md b/tests/integration/README.md index f848bcb..0919dd2 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -3,7 +3,8 @@ End-to-end checks that run the compiled `nbox` binary against real, pinned NetBox fixtures. This catches what the offline wiremock suite can't: polymorphic scope filters, pagination offset windows, available-prefix/IP shapes, GraphQL schema -drift, and the serializer/detail-model shapes of the live API. +drift, MCP stdio read/local-write/refusal behavior, and the serializer/detail-model +shapes of the live API. These are heavy, so they live behind a separate workflow (`.github/workflows/netbox-integration.yml`) and the Rust tests are all @@ -63,6 +64,10 @@ export NETBOX_TOKEN="$(./tests/integration/resolve-token.sh)" - 25 child prefixes `10.10.1.0/24 .. 10.10.25.0/24` nested under the /16, to force multi-page pagination on the prefix detail's child-prefix list +The ignored live test suite also launches the compiled `nbox serve` over stdio +and verifies MCP read, local `--local-writes` reserve/apply/readback, and the +no-`--local-writes` refusal. That path mutates only the ephemeral fixture. + ## Run it locally ```sh