Add edge routing design doc and deploy-agent script; ignore built binaries
Previously untracked working-tree files. mcp-cli and mcp-master are local build outputs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -17,3 +17,5 @@ srv/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.claude/
|
.claude/
|
||||||
|
/mcp-cli
|
||||||
|
/mcp-master
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
# Edge Routing Design
|
||||||
|
|
||||||
|
**Status: SUPERSEDED by ARCHITECTURE_V2.md (2026-04-01).**
|
||||||
|
|
||||||
|
This draft used agent-to-agent communication, a single shared cert with
|
||||||
|
both SANs, private key transmission over gRPC, and an `edge` field on
|
||||||
|
routes. All of these were replaced in the v2 architecture by master-
|
||||||
|
mediated coordination, two separate certs, edge-local cert provisioning,
|
||||||
|
and `public = true` on routes. Retained for historical context only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Draft design for publicly accessible services via edge nodes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Services run on internal nodes (rift, hyperborea) behind mc-proxy. To
|
||||||
|
make a service publicly accessible, an edge node (svc) needs an mc-proxy
|
||||||
|
route pointing back to the internal node. Today this is manual:
|
||||||
|
configure svc's mc-proxy, provision a cert, set up DNS. MCP should
|
||||||
|
automate everything except DNS.
|
||||||
|
|
||||||
|
## Model
|
||||||
|
|
||||||
|
A route in the service definition gains an `edge` field naming the
|
||||||
|
public-facing node:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[components.routes]]
|
||||||
|
port = 443
|
||||||
|
mode = "l7"
|
||||||
|
hostname = "kls.metacircular.net"
|
||||||
|
edge = "svc"
|
||||||
|
```
|
||||||
|
|
||||||
|
When `edge` is set, MCP registers the route on **two** mc-proxy
|
||||||
|
instances:
|
||||||
|
|
||||||
|
1. **Local** (rift mc-proxy): `kls.metacircular.net` →
|
||||||
|
`127.0.0.1:<assigned-port>`. Internal access.
|
||||||
|
2. **Edge** (svc mc-proxy): `kls.metacircular.net` →
|
||||||
|
`<rift-tailscale-ip>:<assigned-port>`. Public access.
|
||||||
|
|
||||||
|
## Agent-to-Agent Coordination
|
||||||
|
|
||||||
|
mc-proxy is a local concern — each node's MCP agent is the sole
|
||||||
|
authority for its mc-proxy. Cross-node route registration goes through
|
||||||
|
agent-to-agent gRPC, not direct mc-proxy access.
|
||||||
|
|
||||||
|
### Deploy Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp deploy kls
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
rift agent (node where container runs)
|
||||||
|
├── pull image, start container
|
||||||
|
├── allocate host port
|
||||||
|
├── provision TLS cert (both SANs, see below)
|
||||||
|
├── register route on rift mc-proxy (local)
|
||||||
|
│ hostname: kls.metacircular.net
|
||||||
|
│ backend: 127.0.0.1:<port>
|
||||||
|
│ cert: /srv/mc-proxy/certs/kls.pem (internal SAN)
|
||||||
|
│
|
||||||
|
└── call svc agent: RegisterEdgeRoute
|
||||||
|
├── hostname: kls.metacircular.net
|
||||||
|
├── backend: <rift-tailscale-ip>:<port>
|
||||||
|
├── mode: l7
|
||||||
|
└── cert: pushed with request (public SAN)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
svc agent
|
||||||
|
├── write cert to /srv/mc-proxy/certs/kls.pem
|
||||||
|
└── register route on svc mc-proxy (local)
|
||||||
|
hostname: kls.metacircular.net
|
||||||
|
backend: <rift-tailscale-ip>:<port>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Undeploy Flow
|
||||||
|
|
||||||
|
Reverse of deploy. Rift agent calls svc agent to remove the edge route,
|
||||||
|
then tears down locally.
|
||||||
|
|
||||||
|
## TLS Certificates
|
||||||
|
|
||||||
|
There are two hostnames involved:
|
||||||
|
|
||||||
|
| Hostname | Where used | Purpose |
|
||||||
|
|----------|-----------|---------|
|
||||||
|
| `kls.svc.mcp.metacircular.net` | rift mc-proxy | Internal platform access |
|
||||||
|
| `kls.metacircular.net` | svc mc-proxy | Public internet access |
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
**Option A: Two separate certs.** Rift's agent provisions one cert with
|
||||||
|
SAN `kls.svc.mcp.metacircular.net` for the internal route. For the edge
|
||||||
|
route, the rift agent (or svc agent) provisions a second cert with SAN
|
||||||
|
`kls.metacircular.net` and pushes it to svc.
|
||||||
|
|
||||||
|
**Option B: Single cert with both SANs.** The agent requests one cert
|
||||||
|
from Metacrypt with both `kls.svc.mcp.metacircular.net` and
|
||||||
|
`kls.metacircular.net` as SANs. This cert is used on both rift and svc
|
||||||
|
mc-proxy instances.
|
||||||
|
|
||||||
|
Option B is simpler (one cert to manage, one renewal) but means internal
|
||||||
|
certs carry public hostnames. Option A is cleaner separation but doubles
|
||||||
|
the cert management.
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
Option B (single cert, both SANs) for simplicity. The Metacrypt CA is
|
||||||
|
internal anyway — both names are under our control. The cert issuance
|
||||||
|
request already accepts a list of `dns_names`, so this is a natural fit:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns_names": [
|
||||||
|
"kls.svc.mcp.metacircular.net",
|
||||||
|
"kls.metacircular.net"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The agent pushes the same cert to both rift and svc mc-proxy cert dirs.
|
||||||
|
|
||||||
|
### Trust Consideration
|
||||||
|
|
||||||
|
The Metacrypt CA cert is not publicly trusted. External clients hitting
|
||||||
|
`kls.metacircular.net` will see an untrusted cert. This is acceptable
|
||||||
|
for now (platform services are operator-facing, `-k` or CA trust is
|
||||||
|
expected). Future option: Let's Encrypt / ACME on svc mc-proxy for
|
||||||
|
publicly trusted certs.
|
||||||
|
|
||||||
|
## New RPC: RegisterEdgeRoute
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc RegisterEdgeRoute(RegisterEdgeRouteRequest)
|
||||||
|
returns (RegisterEdgeRouteResponse);
|
||||||
|
|
||||||
|
message RegisterEdgeRouteRequest {
|
||||||
|
string hostname = 1;
|
||||||
|
string backend = 2; // e.g. "100.95.252.120:13881"
|
||||||
|
string mode = 3; // "l4" or "l7"
|
||||||
|
bytes cert_pem = 4; // TLS cert chain
|
||||||
|
bytes key_pem = 5; // TLS private key
|
||||||
|
string service = 6; // service name (for cert file naming)
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterEdgeRouteResponse {
|
||||||
|
bool success = 1;
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A corresponding `RemoveEdgeRoute` RPC for undeploy:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc RemoveEdgeRoute(RemoveEdgeRouteRequest)
|
||||||
|
returns (RemoveEdgeRouteResponse);
|
||||||
|
|
||||||
|
message RemoveEdgeRouteRequest {
|
||||||
|
string hostname = 1;
|
||||||
|
string service = 2;
|
||||||
|
int32 port = 3; // mc-proxy listener port
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveEdgeRouteResponse {
|
||||||
|
bool success = 1;
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Config Changes
|
||||||
|
|
||||||
|
The agent needs to know how to reach other agents for edge route
|
||||||
|
registration. This could be:
|
||||||
|
|
||||||
|
1. **Explicit config** — agent config lists peer agents:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[peers]]
|
||||||
|
name = "svc"
|
||||||
|
address = "100.x.x.x:9444"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Discovered from node registry** — the agent already receives node
|
||||||
|
info during sync. If the CLI pushes node addresses to agents, the
|
||||||
|
agent can dial peers without extra config.
|
||||||
|
|
||||||
|
Option 2 is preferable (less config duplication) but requires the agent
|
||||||
|
to have MCIAS credentials for authenticating to the peer agent.
|
||||||
|
|
||||||
|
## Service Definition Changes
|
||||||
|
|
||||||
|
Proto: add `edge` field to `RouteSpec`:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
message RouteSpec {
|
||||||
|
string name = 1;
|
||||||
|
int32 port = 2;
|
||||||
|
string mode = 3;
|
||||||
|
string hostname = 4;
|
||||||
|
string edge = 5; // edge node name (e.g. "svc")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Servicedef: add `Edge` field to `RouteDef`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type RouteDef struct {
|
||||||
|
Name string `toml:"name,omitempty"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
Mode string `toml:"mode,omitempty"`
|
||||||
|
Hostname string `toml:"hostname,omitempty"`
|
||||||
|
Edge string `toml:"edge,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. MCP agent provisioned on svc.
|
||||||
|
2. mc-proxy running on svc with gRPC admin socket (currently config-only
|
||||||
|
on svc, per svc-deployment-plan.md).
|
||||||
|
3. Agent-to-agent authentication (MCIAS service credentials).
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Should the internal route (`kls.svc.mcp.metacircular.net`) also be
|
||||||
|
registered on rift mc-proxy when an edge route exists? Or does the
|
||||||
|
edge route replace the internal one? Probably both — internal for
|
||||||
|
platform use, edge for public.
|
||||||
|
- Port for the public mc-proxy listener: should it always be 443 for
|
||||||
|
L7, or should the route's `port` field apply? For public services,
|
||||||
|
443 is the only sensible choice.
|
||||||
|
- Should the edge route backend use TLS to the internal node
|
||||||
|
(`backend_tls = true`)? Yes — traffic crosses Tailscale but
|
||||||
|
defense-in-depth is good practice.
|
||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# deploy-agent — build mcp-agent and deploy to a node via SSH.
|
||||||
|
#
|
||||||
|
# Usage: script/deploy-agent <node> [arch]
|
||||||
|
#
|
||||||
|
# Builds the mcp-agent binary for the target architecture, copies it
|
||||||
|
# to /srv/mcp/mcp-agent on the node, and restarts the systemd service.
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# node SSH hostname (e.g., rift, hyperborea)
|
||||||
|
# arch GOARCH value (default: amd64)
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# script/deploy-agent rift
|
||||||
|
# script/deploy-agent hyperborea arm64
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
NODE="${1:?usage: deploy-agent <node> [arch]}"
|
||||||
|
ARCH="${2:-amd64}"
|
||||||
|
REMOTE_PATH="/srv/mcp/mcp-agent"
|
||||||
|
SERVICE="mcp-agent"
|
||||||
|
|
||||||
|
VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo unknown)"
|
||||||
|
BINARY="mcp-agent.${NODE}"
|
||||||
|
|
||||||
|
printf "building mcp-agent %s for %s (%s)\n" "$VERSION" "$NODE" "$ARCH"
|
||||||
|
CGO_ENABLED=0 GOARCH="$ARCH" go build \
|
||||||
|
-trimpath \
|
||||||
|
-ldflags="-s -w -X main.version=${VERSION}" \
|
||||||
|
-o "$BINARY" \
|
||||||
|
./cmd/mcp-agent
|
||||||
|
|
||||||
|
printf "deploying to %s:%s\n" "$NODE" "$REMOTE_PATH"
|
||||||
|
scp "$BINARY" "${NODE}:/tmp/mcp-agent.new"
|
||||||
|
ssh "$NODE" "doas sh -c 'systemctl stop ${SERVICE} && cp /tmp/mcp-agent.new ${REMOTE_PATH} && systemctl start ${SERVICE}' && rm /tmp/mcp-agent.new"
|
||||||
|
|
||||||
|
printf "verifying %s on %s\n" "$SERVICE" "$NODE"
|
||||||
|
ssh "$NODE" "systemctl is-active ${SERVICE}"
|
||||||
|
|
||||||
|
rm -f "$BINARY"
|
||||||
|
printf "done: %s deployed to %s\n" "$VERSION" "$NODE"
|
||||||
Reference in New Issue
Block a user