carlos / platform Public

#162: design review brief for the replica-placements design round

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d6810d998ec9dadcf7ac13075bff782626859719 parent 91cf688
1 file changed, +199 −0
  • FABLE-REVIEW-BRIEF-162-design.md +199 −0

CI passed — run details

diff --git a/FABLE-REVIEW-BRIEF-162-design.md b/FABLE-REVIEW-BRIEF-162-design.md
new file mode 100644
index 000000000..46f1f7e66
--- /dev/null
+++ b/FABLE-REVIEW-BRIEF-162-design.md
@@ -0,0 +1,199 @@
+# Review brief: replica placements on a route (carlos/platform #162), design round
+
+You are the second reader on a design that has not been written up as a spec
+yet. Review the design sketch below against the code as it exists at the
+commit this file is committed in. Find what is wrong, what is missing, and
+what will not survive contact with the fleet — not what could be worded
+better.
+
+## What the ask is
+
+amadan discussion carlos/platform #162, "A route can carry replica
+placements, served locally instead of hairpinned (edge checkout)", filed by
+titogo. Its three asks, in titogo's words:
+
+1. **Replica placements on a route.** A route carries, alongside its `Home`,
+ a set of replica placements. A router whose own placement is in that set
+ serves the request from a LOCAL socket (the replica instance) instead of
+ hairpinning. Two constraints titogo names: the hairpin decision is made
+ only in root-domain path routing and the host-routed fork deliberately
+ never consults `remote()` (structural loop protection), so the replica
+ decision must cover host-routed requests too and a replica must never
+ hairpin; and for a replica to be reachable, the account host and custom
+ domains need latency routing to the replica placements as well.
+2. **Provisioning a replica instance.** The sandbox verb with a different
+ role and a chosen placement: same app, same account, `role=replica`,
+ placed on box X, with the instance API exposing role and placement so the
+ home instance can list its replicas and address them.
+3. **A signed instance-to-instance channel via the router.** Home POSTs to
+ its replicas (issue/renew/reclaim a lease); a replica POSTs to home
+ (drain a batch). titogo asks whether the relay's signed-delivery path
+ (`TITO_RELAY_SIGNING_KEY` -> `TITO_RELAY_PUBKEY`, forwarded through the
+ router's `/__internal/*` exception) may carry an app-defined internal
+ call, or what should.
+
+**Residence.** A replica in placement P holds order data for an account
+homed in H until the drain confirms. Replica placement must be per-account
+opt-in and constrained by the account's residence policy — never silently
+on.
+
+Not in scope: an AU box, and the lease/drain protocol itself (titogo's).
+
+Titogo's design record is `docs/superpowers/specs/2026-09-21-edge-checkout-design.md`
+in the titogo repo (`/home/paulca/github.com/tito/titogo`, commit c6ed556e0).
+
+## The correction this design makes to the ask
+
+#162 is written against **titogo's router** (`internal/carlos/router.go`
+in the titogo repo): `remote()`, the hairpin, `tables.slugs`. On CARLOS that
+is not the mechanism that carries account hosts:
+
+- `carlos-edge` owns host routing. A request for a host this box does not
+ serve is **backhauled** to the box that does — one HMAC-signed hop, mesh
+ key, `internal/edge/edge.go` `backhaul` (~:2507), home map from the fleet's
+ edges files (`internal/edge/homemap.go`).
+- The edge tries the local upstream first and only calls `backhaul` on a
+ miss (`ServeHTTP`, ~:1285).
+- titogo's `Route.Home` is never written at HEAD, so the hairpin is dormant
+ everywhere.
+
+So the platform's answer to Ask 1 is an EDGE change, not a router change,
+and the "never hairpins" property becomes structural rather than a rule:
+a box with a local row for a host never reaches the backhaul call at all.
+
+## The design under review
+
+### 1. Record shape (`internal/provision/provision.go`)
+
+Three additive fields on `provision.Record`, every one empty on records
+written before it, so an empty set is byte-identical to today at every
+layer:
+
+- `Role string` — `""` (the serving home) or `"replica"`. Closed
+ vocabulary validated by a `ValidRole` in `ValidSteering`'s shape.
+ `ValidateShape` refuses `role=replica` on anything but `kind=instance`.
+- `Replicas []string` — on the HOME record only: the placement codes the
+ account opts into. Validated against `ValidRegion`, deduplicated, sorted,
+ capped. Never defaulted and never inferred from `Region`. This field IS
+ the residence opt-in.
+- `Home string` — on the REPLICA record only: the public host it mirrors.
+ Written at create, never converged; repointing a replica is
+ delete-and-recreate.
+
+Provisioning a replica is the existing instance-create verb with a role and
+a placement: same app, same account, an unguessable host, `Region` = the
+replica's box placement. Because it carries the same app and channel, the
+ordinary deploy pass gives it the same version as home with no new code.
+
+### 2. Routing (Ask 1)
+
+On the replica's box, the reconcile pass mints, for a replica record:
+
+- the replica's own route row (host = the unguessable replica host ->
+ the replica instance's socket), exactly as any instance record does; and
+- a second row for the account's PUBLIC host (`Home`) pointing at the same
+ socket, plus rows for `Home`'s aliases whose `dns_status` is `pointing`.
+
+The edge then serves the account host locally because it has a local row,
+and `backhaul` — which only runs on an upstream miss — never fires there.
+Host-routed coverage is automatic: the edge has no other kind of routing.
+
+The replica box publishes those entries in its **edges file** under role
+`"replica"`, not `"home"`. `internal/edge/homemap.go` ignores roles it does
+not recognise, so every other box keeps backhauling the account host to the
+real home and the home map stays single-valued. The replica's own host is
+published as `"home"` (it genuinely is homed there), so home and siblings
+can reach the replica by its own name through ordinary backhaul.
+
+### 3. DNS / steering — no change
+
+`internal/steering/steering.go` `desiredState` already emits one latency A
+record per ARMED EDGE for every steered host, and custom domains CNAME to
+`edge.<apps-domain>`. Buyers therefore already land on the nearest edge
+today; the only question was what that edge does when the request arrives,
+which is §2. The design changes no steering code. titogo's second bullet
+under Ask 1 rests on the router model, where the set of answers is per-home;
+on CARLOS every edge already answers.
+
+### 4. Instance-to-instance calls (Ask 3)
+
+No new channel and no relay change. An instance dials a sibling at the
+sibling box's EIP with the real hostname as SNI — which is what `backhaul`
+itself does (`backhaulTransport`, dial the EIP, authority names the host) —
+and that bypasses the local edge, so a replica can reach home through the
+account host without landing on its own local row.
+
+The platform supplies only what an instance cannot know:
+
+- its own role and, for a replica, its home's host and dial address;
+- for a home, the list of its replicas with their hosts and dial addresses;
+- one per-account replica key shared by home and its replicas, derived from
+ the box mesh key (`internal/host/mesh.go`) as
+ `HMAC(meshKey, "replica/" + account)`, published with a `_NEXT` companion
+ so a mesh rotation does not break a call in flight.
+
+Delivered as unit environment through the named-unit config path (amadan
+discussion #114, `worktree-unit-route-config`). titogo signs and shapes the
+lease and drain calls itself; the answer to "may the relay carry an
+app-defined internal call" is "do not — use this instead".
+
+### 5. API and CLI
+
+`internal/console/apiinstances.go`: create accepts `role` and `home`; PATCH
+accepts `replicas` on a home record and refuses a placement the deployment
+does not have armed; list and get expose `role`, `home` and `replicas`
+beside the existing `served_by` (box + region). `instances_cli.go` grows a
+role column and a `carlos instances replicas` verb. A new `carlos` verb is
+not done until `docs/site/cli.md` names it (AGENTS.md).
+
+### 6. Testing
+
+Reconcile mints both rows on the replica's box and nothing on any other;
+the edge serves the account host from the local replica socket with no
+backhaul attempt; a third box's home map still resolves the account host to
+the real home; an empty `Replicas` set is byte-identical to today at every
+layer.
+
+## What to review for
+
+1. **Does §2 actually work in this repo's edge and reconcile code?** Read
+ `internal/provision/reconcile.go`, `internal/edge/edge.go`,
+ `internal/edge/homemap.go`, `internal/host/edgesfile.go`. Two route rows
+ pointing at one socket, a row for a host whose record lives on another
+ box, aliases, certificates (does the replica box obtain a certificate for
+ the account host and its custom domains? by what mechanism?), the
+ activator and hibernation, `Fingerprint`/`SnapshotFingerprint`, the
+ registry's uniqueness assumptions. Name the specific code that breaks.
+2. **Is publishing role `"replica"` in the edges file really ignored by
+ every reader**, including `internal/edgesfile` and anything in titogo
+ that parses the same file? What does a reader that treats an unknown role
+ as home do to the home map?
+3. **The certificate question is the one I am least sure of.** If the
+ replica box cannot obtain or mirror a certificate for the account host,
+ §2 does not serve a single request. Say what the real mechanism has to
+ be.
+4. **Is the mesh-derived per-account key sound**, and is the mesh key
+ something an instance process may hold a derivative of? Rotation
+ semantics, blast radius if one replica is compromised, whether the
+ platform should instead sign per call.
+5. **Residence**: is `Replicas` on the home record enough of a gate, given
+ the policy itself lives in titogo? What must the platform refuse on its
+ own?
+6. **What is missing entirely** — deploys to replicas, hibernation and
+ sleep/wake on a replica, the status/health surface, moves
+ (`docs/superpowers/specs/2026-08-28-instance-move-design.md`), deletion
+ ordering (a replica holding undrained orders), object storage and per
+ instance buckets, backups, the console's own views.
+7. **Scope**: anything here that should be cut to make the first landing
+ smaller, and anything cut that must not be.
+
+## How to answer
+
+A numbered list of findings, most severe first. For each: a severity
+(P1 blocks the design, P2 must be answered in the spec, P3 worth knowing),
+the file and line of the code it rests on, what breaks concretely, and what
+to do instead. Verify every code citation before you make it — cite what
+you read, not what the brief claims. Say plainly if a section is sound; do
+not invent findings to fill the list.
+
+Do not edit any file. Your output is the findings document.