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>
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-opsmust 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.
- 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
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) andhartman-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 asitetool 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.