Files
nisse/docs/Amendment-MCP-Deployment.md
T
dongarandClaude Opus 5.5 31bef59151 Add the MCP deployment amendment
Records a design session on MCP server deployment as a standalone
amendment, ahead of folding it into the spec:

- Every agent gets its own VM, justified by inter-agent isolation and
  per-agent credential attribution; Docker-in-Docker stays as an extra
  Dev Agent reason rather than the rule's foundation
- Egress is a declared, per-agent-instance default-deny allowlist,
  covering what MCP servers reach on the agent's behalf, with the
  tailnet's 100.64.0.0/10 called out as outside RFC1918
- MCP servers run per agent VM, one instance per (service, site,
  privilege), with site in the server identity rather than a tool
  argument
- Third-party MCP images are digest-pinned and excluded from Watchtower
- Routed meta-tools rejected; stateful-server relocation parked

Two consequences settled with the operator while drafting: each MCP
server instance gets its own Vaultwarden machine account (A7), and any
write, reversible or not, requires an approved plan (C2).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 01:55:31 +00:00

7.9 KiB

Amendment — Agent Hosts, Egress, and MCP Server Deployment

An amendment to Architecture-Build-Spec.md, recording decisions from a design session on MCP server deployment. It widens two decisions that were recorded as Dev Agent specific into rules for every agent, settles where MCP servers run and how they are split, and adjusts the secrets, permission, and image-delivery entries that those decisions touch.

Each numbered section below names the spec locations it changes. After review, this amendment is folded into the spec and removed, as the Tiered Inference amendment was.


1. Every agent gets its own VM

Changes: new Part F1; B3 first bullet; C5.

The spec records the dedicated VM as a Dev Agent decision, justified by Docker-in-Docker. That justification is specific to an agent that builds and runs containers. A future agent that does not build code would read it and correctly conclude it may run as a container. So the decision is promoted to a standing rule with its own rationale:

  • Every agent gets its own VM. No agent runs as a container on a shared host, and no two agents share a VM.
  • General rationale:
    • Isolation boundary between agents. The VM is the trust boundary (Part H, monorepo decision). A compromised or misbehaving agent must not reach another agent's state, credentials, or processes.
    • Per-agent credential attribution. Everything an agent holds lives on its own host and is issued to its own identity, so every upstream action traces back to exactly one agent.
  • Additional reason for the Dev Agent: it builds and runs Docker containers, so it needs its own real Docker daemon. Docker-in-a-container is either a socket mount, which is close to host root, or dind, which brings nested-storage problems. This reason is additive: it is not why the rule exists.

2. Egress policy is per agent instance

Changes: new Part F2; B3 "Sealed to start" bullet (becomes nisse-dev's instance of the rule); C5.

The Dev-Agent-scoped egress entry becomes a general rule:

  • Every agent instance has its own default-deny allowlist. Requirements differ by agent type. No agent inherits another agent's policy.
  • The allowlist is a declared artifact, versioned alongside the agent's definition. Each entry is justified by the component that needs it.
  • The declaration point is fixed. The enforcement point is not. Policy may be enforced at the VM, at the Docker network, at the UniFi firewall, or at several of these at once. Layering is deliberate: enforcement at the network layer survives compromise of the agent's own host.
  • The allowlist is the union of what the agent reaches directly and what its MCP servers (§3) reach on its behalf. nisse-ops must permit Proxmox and UniFi endpoints at both sites, even though the agent never opens those connections itself.
  • The tailnet is 100.64.0.0/10 (CGNAT shared address space), not RFC1918. A rule written around RFC1918 denial does not cover it. Deny it explicitly.

3. MCP servers: one instance per agent VM

Changes: new Part F3.

  • MCP servers run as containers on the owning agent's Docker host. Images are shared through the OCI registry. Processes are not shared between agents.
  • Rationale:
    • A shared process means a shared credential. The upstream API's own audit log then cannot attribute a call to a specific agent. Separate instances with separate tokens give attribution at the API layer, instead of depending on our own transcripts. This matters most for a read-only auditing agent, such as a future nisse-security.
    • An agent should not host the tooling used to inspect it.
    • Duplication is nearly free. These servers wrap remote HTTP APIs and hold no state.

4. Per-target, per-privilege server instances

Changes: Part F3; Part 0 (site identifiers); A7 (machine accounts); C2 (writes require an approved plan); C3.

There is one MCP server instance per (service, site, privilege level).

  • Site identifier is the domain stem: garrett-hall (Arlington) and hartman-hut (Cross Plains). The same string appears in the container name, the Vaultwarden machine account, and the MCP tool prefix. Site belongs in the server's identity, not in a site tool argument. A wrong-house action should require a tool that isn't mounted, not just a wrong parameter value.
  • Read-only and elevated access are separate instances with separate upstream credentials. The read-only instance is mounted by default. The elevated instance is mounted only after plan approval. This puts the capability gate at the credential layer, instead of a flag inside a third-party server.
  • Per-task tool profiles mount only the servers that a given task's scope requires.

Consequences for existing sections (settled with the operator while drafting this amendment):

  • A7: a machine account per MCP server instance. Each (service, site, privilege) instance gets its own dedicated Vaultwarden machine account and bootstrap token, alongside the agent's own. "One machine account per agent" becomes "one per agent, plus one per MCP server instance." Each bootstrap token is available only to its own container.
  • C2: writes require an approved plan. Every write credential sits behind the elevated instance, so every write, including a reversible service action such as a container restart, first needs an approved plan that mounts it. The C2 tiers then govern execution within that plan: reversible actions run without further confirmation, and high-consequence actions stay confirm-first. Read-only telemetry stays free, through the read-only instance.

5. Watchtower carve-out

Changes: Part F4 (the existing Part F image-delivery entries).

The existing pipeline (Gitea Actions → OCI registry → Watchtower, with restart gated on the idle flag) gets an exception. Third-party MCP server images hold root-equivalent credentials to Proxmox and UniFi. They are digest-pinned, excluded from Watchtower, and updated deliberately after review.

6. Reframe the network-gear browser item

Changes: Part H; C3.

The open item "Sysadmin browser access to network gear: standing vs. per-task hand-off" is narrowed. With UniFi reachable over MCP, the browser becomes a fallback for API gaps, not the primary path. The open question is now whether the fallback browser access, used only for operations the UniFi API does not expose, is standing or handed off per task. The lockout risk still argues for per-task.

7. Rejected alternative: routed meta-tools / lazy tool registration

Changes: Part F3.

Several community Proxmox and UniFi MCP servers use search-then-dispatch over a large generated tool catalog. This is rejected in favor of per-task tool profiles:

  • Two extra round-trips per call, which is worse with a local model driving the control loop.
  • The model can no longer discover capabilities it wasn't already looking for. That is backwards for exploratory diagnostic work.
  • Arguments arrive as an opaque blob, instead of being constrained by per-tool JSON Schema.
  • A dynamic tool list defeats prompt prefix caching, and caching is much of what makes a large static list affordable.

Acceptable only when consuming a third-party server whose surface we can't trim.

8. Parked: relocating stateful MCP servers

Changes: Part H.

Relocating stateful MCP servers off the owning agent's VM. The trigger is statefulness (a browser pool, a vector index, a warm cache), not sharing: duplicating a stateful server duplicates its state. Relocating has a cost. A shared server can't hold per-agent tokens, so per-agent identity has to move into the request, through caller auth plus per-caller upstream credentials. That loses the attribution property that makes instance-per-VM clean. Instance-per-agent-VM is the starting position. Migrate one server at a time, when a specific server justifies it.