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:
@@ -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.
|
||||
Reference in New Issue
Block a user