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:
1 parent
b0d7adb70d
commit
31bef59151
1 file changed
+157
@@ -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.
|
||||
Reference in new issue
Block a user