Previously untracked working-tree files. mcp-cli and mcp-master are local build outputs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
240 lines
6.9 KiB
Markdown
240 lines
6.9 KiB
Markdown
# 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.
|