From 31bef59151c6fc81896268c5c985481132b2b99a Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Wed, 23 Sep 2026 01:55:31 +0000 Subject: [PATCH] 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) --- docs/Amendment-MCP-Deployment.md | 157 +++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 docs/Amendment-MCP-Deployment.md diff --git a/docs/Amendment-MCP-Deployment.md b/docs/Amendment-MCP-Deployment.md new file mode 100644 index 0000000..737ce31 --- /dev/null +++ b/docs/Amendment-MCP-Deployment.md @@ -0,0 +1,157 @@ +# 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.