peat-mesh 0.8.1

Peat mesh networking library with CRDT sync, transport security, and topology management
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
# Deployment Guide

This guide covers building, containerizing, and deploying peat-mesh nodes to Kubernetes clusters using k3d for local development and testing. For code-level API details, see [kubernetes.md](kubernetes.md). For architectural context, see [ADR-0001](adr/0001-kubernetes-istio-deployment.md).

## Table of Contents

- [Binary target]#binary-target
- [Docker image]#docker-image
- [Helm chart]#helm-chart
- [Local cluster with k3d]#local-cluster-with-k3d
- [Verification]#verification
- [Cluster lifecycle]#cluster-lifecycle
- [Two-cluster federation]#two-cluster-federation
- [Functional testing]#functional-testing

## Binary target

The `peat-mesh-node` binary is a thin wrapper around the `PeatMesh` library that reads configuration from environment variables, sets up discovery, and serves the broker HTTP/WS API.

### Building

```bash
cargo build --release --bin peat-mesh-node --features node
```

The `node` meta-feature enables: `automerge-backend` + `broker` + `kubernetes` + `k8s-openapi/v1_32` + `tracing-subscriber`.

### Environment variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `PEAT_FORMATION_SECRET` | Yes || Base64-encoded formation secret (shared by all nodes in a formation) |
| `HOSTNAME` | No | `peat-mesh-0` | Pod name; used as deterministic keypair context (set automatically by K8s) |
| `PEAT_DISCOVERY` | No | `kubernetes` | Discovery mode: `kubernetes` or `mdns` |
| `PEAT_DATA_DIR` | No | `/data` | Persistent data directory (AutomergeStore uses `$PEAT_DATA_DIR/automerge/`) |
| `PEAT_BROKER_PORT` | No | `8081` | HTTP/WS broker listen port |
| `PEAT_IROH_BIND_PORT` | No | `11204` | Iroh QUIC (UDP) listen port |
| `PEAT_IROH_RELAY_URLS` | No || Comma-separated Iroh relay URLs for NAT traversal |
| `RUST_LOG` | No | `info,peat_mesh=debug` | Tracing filter |

### Running locally

```bash
export PEAT_FORMATION_SECRET=$(openssl rand -base64 32)
export PEAT_DISCOVERY=mdns
./target/release/peat-mesh-node
```

The binary logs structured output via `tracing-subscriber` and shuts down gracefully on SIGTERM or SIGINT.

### Deterministic identity

Each node derives a stable Ed25519 keypair from the formation secret and its hostname using HKDF-SHA256. This means:

- Same `PEAT_FORMATION_SECRET` + same `HOSTNAME` = same `device_id` (survives pod restarts)
- Same secret + different hostname = different `device_id` (each pod gets a unique identity)
- No persistent storage needed for identity

## Docker image

### Building

From the peat-mesh repository root:

```bash
docker build -t peat-mesh-node:latest -f deploy/Dockerfile .
```

The multi-stage build uses:
- **Builder stage**: `rust:1.93-bookworm` with `clang` and `mold` (matching `.cargo/config.toml`)
- **Runtime stage**: `debian:bookworm-slim` with `ca-certificates`, `tini` (PID 1 signal handler), and `curl` (for debugging)

### Ports

| Port | Protocol | Purpose |
|------|----------|---------|
| 8081 | TCP | Broker HTTP/WS API |
| 11204 | UDP | Iroh QUIC sync |

### Image size

The runtime image is ~90MB (slim Debian base + statically-linked binary).

## Helm chart

The Helm chart is in `deploy/helm/peat-mesh/`.

### Resources created

| Resource | Purpose |
|----------|---------|
| **StatefulSet** | Stable pod names (`peat-mesh-0`, `-1`, ...) for deterministic keypair derivation |
| **Headless Service** (`clusterIP: None`) | Generates EndpointSlice resources that `KubernetesDiscovery` watches |
| **ServiceAccount** | Pod identity for RBAC |
| **Role** | `list` + `watch` on `endpointslices` in `discovery.k8s.io` |
| **RoleBinding** | Binds Role to ServiceAccount |
| **Secret** | Stores formation secret as a K8s Secret |

### Key design decisions

- **StatefulSet over Deployment**: Stable pod names (`HOSTNAME`) drive deterministic keypair derivation. No PVCs needed since identity is derived from seed + hostname.
- **Headless Service**: Required so that Kubernetes creates `EndpointSlice` resources. `KubernetesDiscovery` watches these with label selector `app=peat-mesh`.
- **RBAC**: Minimal — only EndpointSlice read access for peer discovery.

### Configuration (values.yaml)

```yaml
replicaCount: 3                    # Number of mesh nodes
image:
  repository: peat-mesh-node      # Docker image
  tag: latest
  pullPolicy: IfNotPresent
formationSecret: ""                # Base64-encoded formation secret (required)
discoveryMode: kubernetes          # "kubernetes" or "mdns"
brokerPort: 8081                   # Broker HTTP/WS port
irohBindPort: 11204                # Iroh QUIC UDP port
irohRelayUrls: ""                  # Comma-separated relay URLs
rustLog: "info,peat_mesh=debug"    # Tracing filter
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi
```

### Installing

```bash
FORMATION_SECRET=$(openssl rand -base64 32)
helm install peat-mesh deploy/helm/peat-mesh \
  --set "formationSecret=$FORMATION_SECRET" \
  --set replicaCount=3
```

### Upgrading

```bash
helm upgrade peat-mesh deploy/helm/peat-mesh \
  --set "formationSecret=$FORMATION_SECRET" \
  --set replicaCount=5
```

### Uninstalling

```bash
helm uninstall peat-mesh
```

## Local cluster with k3d

[k3d](https://k3d.io/) runs lightweight k3s Kubernetes clusters inside Docker containers. It's ideal for local development and testing.

### Prerequisites

```bash
# kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && mv kubectl ~/.local/bin/

# k3d
curl -sL https://github.com/k3d-io/k3d/releases/latest/download/k3d-linux-amd64 -o ~/.local/bin/k3d
chmod +x ~/.local/bin/k3d

# helm
curl -sL https://get.helm.sh/helm-v3.17.3-linux-amd64.tar.gz | tar xz -C /tmp
mv /tmp/linux-amd64/helm ~/.local/bin/
```

### Creating a cluster

```bash
k3d cluster create peat-alpha
```

### Importing the Docker image

k3d clusters can't pull from the local Docker daemon directly. Import the image:

```bash
k3d image import peat-mesh-node:latest -c peat-alpha
```

### Full deploy workflow

```bash
# Build
docker build -t peat-mesh-node:latest -f deploy/Dockerfile .

# Create cluster and import image
k3d cluster create peat-alpha
k3d image import peat-mesh-node:latest -c peat-alpha

# Deploy
FORMATION_SECRET=$(openssl rand -base64 32)
helm install peat-mesh deploy/helm/peat-mesh \
  --set "formationSecret=$FORMATION_SECRET" \
  --set replicaCount=3
```

## Verification

### Pod status

```bash
kubectl get pods -l app=peat-mesh
# NAME          READY   STATUS    RESTARTS   AGE
# peat-mesh-0   1/1     Running   0          30s
# peat-mesh-1   1/1     Running   0          25s
# peat-mesh-2   1/1     Running   0          20s
```

### Health and readiness

```bash
# Health (liveness)
kubectl exec peat-mesh-0 -- curl -s localhost:8081/api/v1/health
# {"status":"healthy","node_id":"peat-mesh-0"}

# Readiness
kubectl exec peat-mesh-0 -- curl -s localhost:8081/api/v1/ready
# {"ready":true,"node_id":"peat-mesh-0","checks":[]}
```

### Node info

```bash
kubectl exec peat-mesh-0 -- curl -s localhost:8081/api/v1/node
# {"node_id":"peat-mesh-0","uptime_secs":120,"version":"0.1.0"}
```

### Peer discovery and Iroh connections

Kubernetes EndpointSlice discovery feeds the `PeerConnector`, which registers each peer with Iroh's `StaticProvider`. Verify the Iroh-level connections and sync stack in the logs:

```bash
kubectl logs peat-mesh-0 | grep "Peer connected to Iroh"
# Peer connected to Iroh peer=peat-mesh-1 endpoint_id=0c48030ef6 addresses=[10.42.0.10:11204]
# Peer connected to Iroh peer=peat-mesh-2 endpoint_id=e0ae6e4c6a addresses=[10.42.0.11:11204]
```

The broker's `/api/v1/peers` endpoint reports mesh-level peers (PeatMesh topology). Iroh-level peer connections (used by the blob store and Automerge sync) are visible in the logs.

### Automerge sync stack

The node starts the full Automerge sync pipeline:

1. **AutomergeStore** opens a redb database at `$PEAT_DATA_DIR/automerge/`
2. **MeshSyncTransport** shares the Iroh endpoint with the blob store
3. **AutomergeSyncCoordinator** + **SyncChannelManager** handle delta sync
4. **SyncProtocolHandler** accepts incoming sync connections on `cap/automerge/1` ALPN
5. A **polling task** syncs all documents with all connected peers every 5 seconds

Verify the sync stack initialized:

```bash
kubectl logs peat-mesh-0 | grep -E "automerge|blob store ready"
# Opening redb database with cache_size=16777216 bytes
# Iroh blob store ready (blobs + automerge sync) iroh_endpoint_id=c25a10ed6e
```

### EndpointSlice

```bash
kubectl get endpointslice -l app=peat-mesh
# NAME              ADDRESSTYPE   PORTS        ENDPOINTS                         AGE
# peat-mesh-xxxxx   IPv4          11204,8081   10.42.0.9,10.42.0.10,10.42.0.11  30s
```

### Deterministic keypair verification

Delete a pod and verify the device_id is identical after restart:

```bash
# Before
kubectl logs peat-mesh-0 | grep "Mesh started"
# Mesh started node_id=peat-mesh-0 device_id=df0036ca6cfb08b2cc03214a1bdb4f24

kubectl delete pod peat-mesh-0
kubectl wait --for=condition=ready pod/peat-mesh-0 --timeout=30s

# After — same device_id
kubectl logs peat-mesh-0 | grep "Mesh started"
# Mesh started node_id=peat-mesh-0 device_id=df0036ca6cfb08b2cc03214a1bdb4f24
```

### Logs

```bash
# Single pod
kubectl logs peat-mesh-0

# All pods
kubectl logs -l app=peat-mesh

# Follow
kubectl logs -f peat-mesh-0
```

## Cluster lifecycle

k3d clusters can be stopped and started without losing state. This is useful for preserving test environments across sessions.

### Stopping a cluster (preserves state)

```bash
k3d cluster stop peat-alpha
```

This stops the Docker containers but preserves all Kubernetes state (pods, services, secrets, etc.). The cluster uses no resources while stopped.

### Starting a stopped cluster

```bash
k3d cluster start peat-alpha
```

All pods, services, and Helm releases resume exactly where they left off. Pod identities (device_id) remain stable since they're derived from the same formation secret + hostname.

### Listing clusters

```bash
k3d cluster list
# NAME         SERVERS   AGENTS   LOADBALANCER
# peat-alpha   1/1       0/0      true
```

### Deleting a cluster (destroys state)

```bash
k3d cluster delete peat-alpha
```

### Rebuilding after code changes

After modifying peat-mesh source code:

```bash
# Rebuild image
docker build -t peat-mesh-node:latest -f deploy/Dockerfile .

# Re-import into running cluster
k3d image import peat-mesh-node:latest -c peat-alpha

# Rolling restart to pick up new image
kubectl rollout restart statefulset peat-mesh
kubectl rollout status statefulset peat-mesh
```

## Two-cluster federation

Two k3d clusters on a shared Docker network can test cross-cluster communication.

### Setup

```bash
# Create shared Docker network
docker network create peat-federation

# Create two clusters on the shared network
k3d cluster create peat-alpha --network peat-federation
k3d cluster create peat-bravo --network peat-federation

# Build and import image to both
docker build -t peat-mesh-node:latest -f deploy/Dockerfile .
k3d image import peat-mesh-node:latest -c peat-alpha
k3d image import peat-mesh-node:latest -c peat-bravo

# Deploy with the SAME formation secret to both clusters
FORMATION_SECRET=$(openssl rand -base64 32)

kubectl config use-context k3d-peat-alpha
helm install peat-mesh deploy/helm/peat-mesh \
  --set "formationSecret=$FORMATION_SECRET" \
  --set replicaCount=2

kubectl config use-context k3d-peat-bravo
helm install peat-mesh deploy/helm/peat-mesh \
  --set "formationSecret=$FORMATION_SECRET" \
  --set replicaCount=2
```

### Cross-cluster connectivity

Both k3d clusters share the `peat-federation` Docker network, meaning their k3d server node containers can reach each other. To enable cross-cluster mesh traffic:

1. **Expose Iroh QUIC via NodePort** in each cluster so pods in one cluster can reach pods in another via the k3d server container IP + NodePort.

2. **Configure gateway pods** with `PEAT_STATIC_PEERS` pointing to remote cluster gateway addresses. Gateway pods use `HybridDiscovery` combining `KubernetesDiscovery` (local) + `StaticDiscovery` (remote).

3. **Find k3d server IPs** on the shared network:
   ```bash
   docker inspect k3d-peat-alpha-server-0 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
   docker inspect k3d-peat-bravo-server-0 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
   ```

### Stopping and resuming federation clusters

```bash
# Stop both (preserves state, uses no resources)
k3d cluster stop peat-alpha
k3d cluster stop peat-bravo

# Resume later
k3d cluster start peat-alpha
k3d cluster start peat-bravo
```

The `peat-federation` Docker network persists independently of the clusters.

### Cleanup

```bash
k3d cluster delete peat-alpha
k3d cluster delete peat-bravo
docker network rm peat-federation
```

## Functional testing

k3d clusters are well-suited for automated functional tests. The clusters can be scripted end-to-end:

### Single-cluster test script

```bash
#!/bin/bash
set -euo pipefail

CLUSTER="peat-test-$$"
SECRET=$(openssl rand -base64 32)

cleanup() { k3d cluster delete "$CLUSTER" 2>/dev/null || true; }
trap cleanup EXIT

# Setup
k3d cluster create "$CLUSTER" --no-lb
docker build -t peat-mesh-node:latest -f deploy/Dockerfile .
k3d image import peat-mesh-node:latest -c "$CLUSTER"
helm install peat-mesh deploy/helm/peat-mesh \
  --set "formationSecret=$SECRET" --set replicaCount=3 \
  --set image.pullPolicy=Never \
  --kube-context "k3d-$CLUSTER"

# Wait for pods
kubectl --context "k3d-$CLUSTER" rollout status statefulset/peat-mesh --timeout=120s

# Verify health
for i in 0 1 2; do
  STATUS=$(kubectl --context "k3d-$CLUSTER" exec "peat-mesh-$i" -- \
    curl -sf localhost:8081/api/v1/health | jq -r .status)
  [ "$STATUS" = "healthy" ] || { echo "FAIL: peat-mesh-$i unhealthy"; exit 1; }
done
echo "OK: All pods healthy"

# Verify peer discovery + Iroh connections
sleep 35  # K8s discovery polls every 30s
for i in 0 1 2; do
  PEERS=$(kubectl --context "k3d-$CLUSTER" logs "peat-mesh-$i" | \
    grep -c "Peer connected to Iroh" || true)
  [ "$PEERS" -ge 2 ] || { echo "FAIL: peat-mesh-$i has $PEERS Iroh peers (expected 2)"; exit 1; }
done
echo "OK: All pods discovered peers via Iroh"

# Verify Automerge sync stack initialized
for i in 0 1 2; do
  kubectl --context "k3d-$CLUSTER" logs "peat-mesh-$i" | \
    grep -q "blobs + automerge sync" || { echo "FAIL: peat-mesh-$i sync stack not initialized"; exit 1; }
done
echo "OK: Automerge sync stack active on all pods"

# Verify deterministic identity survives restart
BEFORE=$(kubectl --context "k3d-$CLUSTER" logs peat-mesh-0 | \
  grep "Mesh started" | grep -oP 'device_id=\S+')
kubectl --context "k3d-$CLUSTER" delete pod peat-mesh-0
kubectl --context "k3d-$CLUSTER" wait --for=condition=ready \
  pod/peat-mesh-0 --timeout=30s
AFTER=$(kubectl --context "k3d-$CLUSTER" logs peat-mesh-0 | \
  grep "Mesh started" | grep -oP 'device_id=\S+')
[ "$BEFORE" = "$AFTER" ] || { echo "FAIL: device_id changed after restart"; exit 1; }
echo "OK: Deterministic identity stable across restart"

# Verify no errors in logs
for i in 0 1 2; do
  ERRORS=$(kubectl --context "k3d-$CLUSTER" logs "peat-mesh-$i" | \
    grep -ci "error" || true)
  [ "$ERRORS" -eq 0 ] || echo "WARN: peat-mesh-$i has $ERRORS error lines"
done

echo "PASS: All functional checks passed"
```

### Cross-cluster test approach

To test execution from one cluster to another:

1. **Expose the broker service** from each cluster via k3d port mapping or NodePort
2. **Hit the broker API** from outside the cluster (or from a pod in the other cluster via the shared Docker network)
3. **Write a document** via the broker API on cluster A, verify it syncs to cluster B

```bash
# Create clusters with port mapping for broker access
k3d cluster create peat-alpha --network peat-federation \
  -p "8081:8081@server:0"
k3d cluster create peat-bravo --network peat-federation \
  -p "8082:8081@server:0"

# After deploy, test from host:
curl http://localhost:8081/api/v1/health   # cluster alpha
curl http://localhost:8082/api/v1/health   # cluster bravo
```

Within a single cluster, the Automerge sync coordinator automatically syncs documents between all pods over QUIC (`cap/automerge/1` ALPN). For cross-cluster sync, federation gateway pods need to bridge the two clusters via the pattern described in [ADR-0001 Section 6](adr/0001-kubernetes-istio-deployment.md#6-cluster-to-cluster-federation).