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>
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-09-23 01:55:31 +00:00
1 parent b0d7adb70d
commit 31bef59151
1 file changed
+157
+157
View File
@@ -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.