go/docs/cutover.md
Rama master · réplica de lectura del repositorio original.
# Nyro Go cutover runbook (P6)
The Go implementation (`go/`) is feature-complete and tested. This runbook covers the
dual-run shadow phase and the cutover from the Rust gateway. It is an
**operational** checklist — the code is done; this is deployment.
> **WebUI note:** the root `webui/` directory is Rust-only for the duration
> of the parallel period — it targets the Rust admin API and is not being
> ported. Go WebUI work lives entirely in `go/webui/`, targets the Go server's
> management API schema directly, and is documented in `go/README.md` (## Go WebUI) and
> `go/webui/README.md`.
## 0. Build the Go binary
```bash
cd go
go build -o /tmp/nyro .
# optional UI:
(cd webui && pnpm install && pnpm build)
```
## 1. Stand up both data planes side-by-side
- Rust gateway on the production port (e.g. `:19530`).
- Go proxy on a shadow port, initially using a standalone snapshot, and the Go
server on its loopback control-plane port with config-sync disabled:
```bash
# data plane (shadow):
/tmp/nyro proxy --listen 127.0.0.1:19529 \
--config ./config.yaml
# control plane (management API + WebUI):
/tmp/nyro serve --listen 127.0.0.1:19531 \
--sync-listen= --webui-dir ./webui/dist --auto-migrate
```
`--auto-migrate` above lets this first-boot server create its own (default
sqlite) schema; drop it once the db already exists. It's off by default
regardless of backend — see `go/docs/schema/database.md` for the
postgres migration workflow.
The standalone proxy reads `config.yaml` once at startup; edit the file and
restart to apply a change. The server manages its own database, but with
`--sync-listen=` its changes are not pushed to that proxy. This is the
simplest isolated shadow setup. The config schema is documented in
[Standalone `config.yaml`](schema/config.md).
To hot-reload the proxy's config from the server instead of a static
`--config` YAML file, enable the server's config-sync gRPC server and point
the proxy at it. This channel carries every upstream's `credentials_json`, so
no TLS paths select plaintext, while all three `--sync-tls-ca/-cert/-key` paths
select mTLS. A partial set or a certificate load failure stops startup; there
is no downgrade to plaintext. See
[config-sync transport and mTLS](security/config-sync-mtls.md) for the full
`nyro tool ca` workflow. For same-host shadow testing, plaintext + loopback is the
fastest path — and the only one that needs no token:
```bash
# control plane, with config-sync enabled (loopback plaintext, no token needed):
/tmp/nyro serve --listen 127.0.0.1:19531 \
--sync-listen 127.0.0.1:19532 --auto-migrate
# data plane, subscribing to config-sync instead of --config:
/tmp/nyro proxy --listen 127.0.0.1:19529 \
--server 127.0.0.1:19532
```
Off-host, plaintext config-sync requires a join token on both sides
(`--sync-token`): without one the processes refuse to start, because an
unauthenticated plaintext port hands the full configuration to anyone who can
reach it. A token still leaves credentials unencrypted in transit and is
replayable by an observer, so for normal cross-host deployments sign
certificates and configure a complete TLS path set on both processes:
```bash
/tmp/nyro tool ca init
/tmp/nyro tool ca sign-server
/tmp/nyro tool ca sign-proxy --node-id proxy-1
# distribute ca.pem + server.{pem,key.pem} / proxy.{pem,key.pem}, then:
/tmp/nyro serve --sync-listen 0.0.0.0:19532 --auto-migrate \
--sync-tls-ca ~/.nyro/pki/ca.pem --sync-tls-cert ~/.nyro/pki/server.pem --sync-tls-key ~/.nyro/pki/server-key.pem
/tmp/nyro proxy --server server.internal:19532 \
--sync-tls-ca ~/.nyro/pki/ca.pem --sync-tls-cert ~/.nyro/pki/proxy.pem --sync-tls-key ~/.nyro/pki/proxy-key.pem
```
`--config` and `--server` are mutually exclusive — exactly one must
be set. `--sync-listen=` disables the config-sync server; explicitly setting
any `--sync-tls-*` flag in that mode is an error.
Connected proxies are visible on the server at
`GET /api/v1/nodes` (and the WebUI's Nodes page) — a best-effort, in-memory
view that reflects only currently-open connections.
For multiple server replicas sharing one database (typically PostgreSQL), set a
positive polling interval on every replica so a write handled by one is noticed
and pushed by the others. Since this is exactly the shared postgres case
`go/docs/schema/database.md` covers, apply the schema with
DDL a DBA reviews (print it with `nyro tool migrate dump`/`diff`; see
`go/docs/schema/migrations.md`) before first boot instead of passing
`--auto-migrate` here:
```bash
# Run on each server host, with a distinct --listen/--sync-listen address.
/tmp/nyro serve --listen 10.0.0.11:19531 \
--sync-listen 10.0.0.11:19532 \
--dsn "$NYRO_SHARED_DSN"
```
The polling default is `0` (disabled); a single server still pushes its own
writes immediately. Add the complete mTLS path set above to every replica and
proxy unless the config-sync network is deliberately trusted for plaintext.
The server's REST/WebUI listener and the proxy's client listener are HTTP-only. The
config-sync TLS flags secure only the gRPC channel; terminate HTTPS for those
HTTP listeners at a reverse proxy, ingress, load balancer, or service mesh.
The management API has no built-in authentication and binds to loopback by
default. If `--listen` is exposed off-host, restrict it with a private network,
firewall, or an authenticated HTTPS reverse proxy. The browser same-origin and
loopback Host checks are CSRF/DNS-rebinding defenses, not operator authentication.
## 2. Shadow traffic + parity diff
Mirror a sample of real client traffic to the Go proxy and diff the responses
using the parity normalizer (`internal/parity`):
- For each mirrored request, run it through both data planes.
- Normalize both responses (`parity.NormalizeJSON` drops volatile fields:
generated ids, timestamps, fingerprints) and compare.
- Investigate any normalized mismatch — those are real parity gaps to fix
before cutover.
Streaming responses: diff per-frame after normalizing each SSE payload. Token
boundaries may differ; compare the concatenated content + final usage.
This is also where the **deferred OAuth acquisition flows** (Claude PKCE /
Codex / Vertex) get validated against the Rust gateway's wire behavior.
Request/token quota accounting and concurrency enforcement are already wired;
shadow traffic must validate their observed usage and denial behavior under
the intended memory or shared Redis State backend.
## 3. Gradual cutover
Once shadow parity is clean:
1. Move a small fraction of clients to the Go proxy (DNS / load balancer /
port flip).
2. Monitor error rate, latency, and the Go `/api/v1/stats/overview`.
3. Ramp to 100%.
## 4. Rollback
Keep the Rust gateway warm during ramp. On regression, flip traffic back — the
Rust gateway and its storage are untouched and still authoritative, so no
rollback data migration is needed.
## 5. Retire Rust
After the Go proxy serves 100% cleanly for the agreed bake period:
1. Final verification from `go/`: `go test ./...` + `go build ./...`.
2. Stop the Rust gateway.
3. Remove/archive the Rust crates (`crates/`, `src-server/`, `src-tauri/`).
4. Promote `go/` to the repo root (import-path rewrite) if desired.
## Known parity gaps to close before/during cutover
- **OAuth acquisition flows** (Claude/Codex/Vertex): framework is in
`internal/auth`; concrete driver flows + background refresh must be ported
and validated here.
- **Budget enforcement**: budget definitions persist, but enforcement still
requires pricing data and accounting semantics.
- **Conversation/API state**: protocol conversion for stateful APIs such as
OpenAI Responses and Gemini Interactions still needs an explicit State
contract and lifecycle; it must not reuse quota keys implicitly.
- **Codec edge cases** flagged `TODO(P5)` in each codec (multimodal parts,
Anthropic cache_control raw round-trip, Gemini schema uppercasing,
Responses reasoning-item passback, full event sequence).
- **Vendor metadata** (presets/channels → auth-mode resolution).