carlos / platform Public

docs: P7 plan rev 6 (a door request is a request from the box); round-5 findings unedited; round-6 brief

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 256645fceefbd29e372928934fa2f7c21057fcf9 parent 083b5c1
3 files changed, +103 −40
  • docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md +41 −40
  • docs/superpowers/plans/CODEX-FINDINGS-P7-EDGE-DOOR-R5.md +45 −0
  • docs/superpowers/plans/CODEX-REVIEW-BRIEF-P7-EDGE-DOOR-R6.md +17 −0

CI passed — run details

diff --git a/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md b/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md
index ef3ec7294..7598aba2b 100644
--- a/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md
+++ b/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md
@@ -1,4 +1,4 @@
-# Instance moves P7: the edge-local door — Implementation Plan (rev 5)
+# Instance moves P7: the edge-local door — Implementation Plan (rev 6)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
@@ -6,24 +6,18 @@
**Architecture:** A signed-in person mints a door key at the console. The console's own database is the authority for keys. The bucket holds a signed projection of them, under `control/door-keys/<account>/<app>.json`, signed with the deployment's root release key, carrying a revision and a 72-hour expiry, re-signed every 12 hours, and written by one serialized, compare-and-swap publisher that always rebuilds from the database. Every edge mirrors the projections on their own 30-second loop and verifies them against `CARLOS_RELEASE_PUBKEY`. A second listener on `/run/carlos/edge-local.sock` (0660, group `carlos-sockets` through the directory's setgid) authenticates `X-Carlos-Door-Key`, strips it, removes every forwarded-address header, marks the request as a door caller and hands it to `Edge.ServeHTTP`. For a host this box serves, `ServeHTTP` refuses reserved handlers to door callers and checks ownership against the final upstream it is about to serve. For a host served elsewhere, the door checks the owner the home box published, which a box publishes only for a host whose every route has one owner, and then backhauls it as an ordinary hop.
-## Security model (rev 5; Paul's ruling, 2026-09-28)
+## Security model (rev 6; Paul's ruling, 2026-09-28)
-**The door grants no privilege over the public door.** Paul ruled (2026-09-28, after Astra's round 3 showed that a believed address changes the password gate's guess limits and any app that authorizes by IP) that the door believes no caller-supplied visitor address. So:
+**A door request is a request from this box.** Paul ruled (2026-09-28) that the door believes no caller-supplied visitor address. Rounds 4 and 5 showed that any *invented* representation (`unknown`, then RFC 7600's `192.0.0.8`) has some framework that reads it as local, private or trusted (Rails' `local?` fallback; python-ipware, where Python classes `192.0.0.8` as private). So the door invents nothing. It presents the box's own public address, the one its edges file publishes (`readPublicIP`, `internal/host/edgesfile.go:534`). That is exactly what the app would see if the calling process, on this box, requested the instance's public URL, since the request would leave the box and come back through an edge from that address. So the door grants nothing a public request from the same box lacks, whatever framework the app runs.
-- The door deletes every header that claims a client address: `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host` and `X-Forwarded-Proto` (`stripForwardedHeaders`, `homemap.go:806`), plus `doorAddressHeaders`: `Client-IP`, `X-Real-IP`, `True-Client-IP`, `CF-Connecting-IP`, `Fastly-Client-IP`, `X-Client-IP`, `X-Cluster-Client-IP` and `X-Originating-IP`. Frameworks consult these when the forwarded chain is unusable (Astra round 4: Rails 8's `RemoteIp`).
-- It sets `RemoteAddr` to `door/<account>/<app>`. That value has no port, so `net.SplitHostPort` fails on it, and the gate's key (`gate/gate.go:230-235`) and the analytics address (`analytics.go:86-90`) fall back to the raw string. The partitions are what the code does, not a new promise:
- - **Served on this box:** the gate's limiter is keyed by (route host, `door/<account>/<app>`) (`gate.go:151`). Analytics hashes (host, User-Agent, `door/<account>/<app>`) (`internal/analytics/meter.go:178`).
- - **Backhauled:** the far gate keys on the sending edge's address, as every backhauled request does today. Far analytics hashes (host, User-Agent, `192.0.0.8`).
-
- No gate or analytics code changes.
-- `setForwarded` (`homemap.go:912`) gives a door request `X-Forwarded-For: 192.0.0.8` and `X-Forwarded-Proto: https`, before its hop check. **192.0.0.8 is the IPv4 dummy address (RFC 7600)**, reserved for exactly this: a source that has no real address to give.
- - It is a valid IP, so no framework discards the chain and falls back to an alternate header or to the TCP peer. For apps behind the edge's loopback TCP proxy (`edge.go:2182`), that peer would be loopback, which Rails treats as `local?` (Astra round 4, finding 1).
- - It is not loopback, not private (`net.IP.IsPrivate`), and not in Rails' default trusted-proxy set. A framework takes it as the client and runs its usual spoofing checks against it.
- - A backhauled door request reaches the far edge carrying `192.0.0.8`. The far edge believes that chain as it believes any verified hop's, so the app sees `192.0.0.8` wherever the instance lives. The far edge's TLS makes the scheme `https` there too (round 3, finding 3).
-- **Degraded mode, stated:** if the far edge cannot verify the sender's mesh key (the rotation window), it strips the forwarded headers and serves the request as a client at the sending edge's address (`homemap.go:793`), exactly as it does for a public backhaul in the same failure. That address is a platform box's public EIP, never loopback or private, so it confers no locality. The app sees an infrastructure address instead of `192.0.0.8` until the key mirror catches up (Astra round 4, finding 3).
-- **Door requests bypass the response cache:** no lookup, no store. The cache keys `Vary` on the inbound request (`edge.go:1583`, `:1702`), which the door has rewritten, so a door response could otherwise be served to a public request or the reverse (Astra round 4, finding 2). The same inbound-versus-outbound mismatch exists for public traffic on `Vary: X-Forwarded-For`, and is filed separately rather than fixed here.
-- No loopback or private address is ever used. Some apps treat loopback as a local administrator, and the door must not be the way to become one.
-- An app that needs the buyer's address has its own router send it in the app's own signed header, as Tito already does for identity (`X-Tito-Auth-*`). Headers the app sets pass through untouched. The platform reads none of them.
+- `serveDoor` deletes every header that claims a client address, then sets `RemoteAddr` to `<box public address>:0`. The headers deleted are:
+ - `stripForwardedHeaders` (`homemap.go:806`): `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto`;
+ - `doorAddressHeaders`: `X-Forwarded`, `Forwarded-For`, `Client-IP`, `X-Real-IP`, `True-Client-IP`, `CF-Connecting-IP`, `Fastly-Client-IP`, `X-Client-IP`, `X-Cluster-Client-IP`, `X-Originating-IP`.
+- The ordinary `SetXForwarded` in `setForwarded` then writes that address into `X-Forwarded-For`, as it does for any client. The deletion is belt and braces. With a global address in `X-Forwarded-For`, the resolvers Astra traced (Rails `RemoteIp`, Express `proxy-addr`, Laravel/Symfony, Go `realip`, chi, python-ipware) pick it, exactly as they do for a public request.
+- `setForwarded`'s only door branch sets `X-Forwarded-Proto: https`, the scheme the same request would have had through the public URL. The far edge of a backhaul gives `https` by its own TLS.
+- Everything downstream then treats the request as a request from the box: the gate's limiter, analytics' visitor hash, the response cache, and a backhaul. Backhauled, the far edge believes the near edge's chain (the box address). If it cannot verify the hop, it uses the near edge's peer address, which is the same box. There is no separate degraded mode. **Door requests are not excluded from the cache.** Rev 5's bypass is dropped, because a request from the box is cached like any public request. The cache keys `Vary` on inbound headers (`edge.go:1583`, `:1702`) while forwarding rewrites them, and that mismatch is pre-existing for all traffic. It is filed as its own discussion.
+- **No public address, no door.** If the agent cannot read the box's public address, the door stays closed and says so in the journal. It never falls back to loopback, a private address or a placeholder.
+- An app that needs the buyer's address has its own router carry it in the app's own signed header, as Tito already does for identity (`X-Tito-Auth-*`). The platform reads no such header.
**What the door key is for, then.** It says which app's processes are calling, and confines them to that app's hosts (ruling Q3). With no privilege behind the door, a gap in that confinement lets a caller reach a host through the door that it could already reach from the internet, with less than an internet request carries. That is why the residuals below are accepted. They are hygiene, not security boundaries:
@@ -55,6 +49,13 @@ The credential machinery (signed, revisioned, expiring records; one serialized p
| 9 | wrong file and fixture references | **Accepted:** `runInstances` is in `cli.go:284`; fixture is `fx.ts`/`fx.pub`, helpers in `apiinstances_test.go`; catchall publisher `edgesfile.go:363` and the alias-versus-route fold covered; docs go to `docs/site/cli.md` (the only reference page). |
| 10 | root signing acceptable; add a domain prefix | **Accepted:** signatures cover `"carlos door-keys v1\n" + payload`; cross-kind rejection tests (Task 1). |
+## Revision 6: Astra round 5 dispositions
+
+| # | finding | disposition |
+|---|---|---|
+| R5-1 | python-ipware: `X-Forwarded`/`Forwarded-For` survive, and Python classes `192.0.0.8` as private | **Accepted, fixed by removing the invented address:** the door presents the box's own public address (Security model), which is global in every classifier and is what a public request from the box shows. Both headers join the strip list anyway. |
+| R5-2 | the cache bypass does not cross a backhaul | **Superseded:** a door request is a request from the box and is cached like one; no bypass. The pre-existing inbound-`Vary` mismatch is filed separately. |
+
## Revision 5: Astra round 4 dispositions
| # | finding | disposition |
@@ -115,7 +116,7 @@ The credential machinery (signed, revisioned, expiring records; one serialized p
2. **A path-prefix hostname whose prefixes belong to different apps**: locally, app A's key never reaches app B's prefix; remotely, the host publishes no owner and the door refuses it (Tasks 4 and 5 tests).
3. **A gate login POST whose `next` names another app's prefix** is refused before the gate judges the password (Task 5 test).
4. **A revoke racing a renewal** never leaves the older record in the bucket (Task 2 test).
-5. **Any `Forwarded` or `X-Forwarded-*` header a caller sends** never reaches the app, the gate or analytics. The app sees `X-Forwarded-For: 192.0.0.8` and `X-Forwarded-Proto: https`, served on this box or across a hop (Tasks 5 and 6 tests).
+5. **Any header claiming a client address** (`Forwarded`, `X-Forwarded-*`, and every name in `doorAddressHeaders`) never reaches the app, the gate or analytics. The app sees the box's public address and `https`, served on this box or across a hop (Tasks 5 and 6 tests).
---
@@ -658,19 +659,17 @@ Mirror rules, each with its own test:
- `func (e *Edge) backhaulTo(w http.ResponseWriter, r *http.Request, host, eip, box string) bool`: the dial half of `backhaul`, split out so the door dials exactly the target `Owner` returned. `backhaul` becomes `Home` plus `backhaulTo`, and its behaviour is unchanged.
- `func doorOwnsRoute(c doorCaller, rt registry.Route) bool` → `release.RouteAccount(rt) == c.Account && rt.App == c.App`
- `func (e *Edge) doorReservedHost(host string) bool` — true for the tunnel hostname and the steering probe hostname, compared after lowercasing and stripping one trailing dot (the normalization `steeringProbe` and `canonicalizeHopHost` use)
-- `const doorVisitorAddr = "192.0.0.8"` (RFC 7600, the IPv4 dummy address)
-- `var doorAddressHeaders = []string{"Client-IP", "X-Real-IP", "True-Client-IP", "CF-Connecting-IP", "Fastly-Client-IP", "X-Client-IP", "X-Cluster-Client-IP", "X-Originating-IP"}`
+- `var doorAddressHeaders = []string{"X-Forwarded", "Forwarded-For", "Client-IP", "X-Real-IP", "True-Client-IP", "CF-Connecting-IP", "Fastly-Client-IP", "X-Client-IP", "X-Cluster-Client-IP", "X-Originating-IP"}`
- `setForwarded` gains a first branch:
```go
func setForwarded(pr *httputil.ProxyRequest) {
pr.SetXForwarded()
- // A door request discloses no visitor (Security model): RFC 7600's
- // dummy address, a valid IP no framework treats as local, and the
- // scheme the far side of a hop would give it, so the app sees the same
- // thing whichever box serves it.
+ // A door request is a request from this box (Security model): its
+ // RemoteAddr is already the box's public address, so SetXForwarded wrote
+ // the right chain. Only the scheme differs from the public route to the
+ // same URL, which would have arrived over TLS.
if _, ok := doorCallerFrom(pr.In.Context()); ok {
- pr.Out.Header.Set("X-Forwarded-For", doorVisitorAddr)
pr.Out.Header.Set("X-Forwarded-Proto", "https")
return
}
@@ -685,9 +684,8 @@ func setForwarded(pr *httputil.ProxyRequest) {
Enforcement, in `ServeHTTP` order:
1. **First statement after the port strip:** `door, isDoor := doorCallerFrom(r.Context())`. If `isDoor && e.doorReservedHost(host)`: `e.unknownHost(w, r); return`. If `isDoor`, skip the steering probe and tunnel branches entirely (wrap them in `if !isDoor { ... }`). The mark is only ever set by `serveDoor` on this box. Nothing carries it across a hop, and `acceptHop` is unchanged.
-2. **Miss branch:** if `isDoor`, call `acct, app, eip, box, ok := e.homes.Load().Owner(host)`. Unless `ok && acct == door.Account && app == door.App`, answer `unknownHost`. Otherwise `backhaulTo(w, r, host, eip, box)`. The receiving edge sees an ordinary verified hop, as it does for public backhaul today. It believes the forwarded chain, which is `192.0.0.8`, so the app there sees exactly what a local serve shows it (Security model).
-3. **Cache:** wherever `ServeHTTP` consults or fills the response cache (`edge.go:1583` lookup, `:1702` store), add `!isDoor` to the condition, so a door request neither reads nor writes it.
-4. **After the final `up` is chosen**, that is, after the login POST re-resolution block (~`edge.go:1416-1446`) and before the gate (`:1516`), the cache and dispatch: `if isDoor && !doorOwnsRoute(door, up.route) { e.unknownHost(w, r); return }`. The same `up` value is then used for everything after it, so authorization and dispatch cannot diverge.
+2. **Miss branch:** if `isDoor`, call `acct, app, eip, box, ok := e.homes.Load().Owner(host)`. Unless `ok && acct == door.Account && app == door.App`, answer `unknownHost`. Otherwise `backhaulTo(w, r, host, eip, box)`. The receiving edge sees an ordinary verified hop, as it does for public backhaul today. It believes the forwarded chain, which is the box's public address, so the app there sees exactly what a local serve shows it (Security model).
+3. **After the final `up` is chosen**, that is, after the login POST re-resolution block (~`edge.go:1416-1446`) and before the gate (`:1516`), the cache and dispatch: `if isDoor && !doorOwnsRoute(door, up.route) { e.unknownHost(w, r); return }`. The same `up` value is then used for everything after it, so authorization and dispatch cannot diverge.
- [ ] **Step 1: Failing tests** (an in-process `Edge` built the way existing `edge_test.go` tests build one with local routes and a stubbed home map; `grep -n "func newTestEdge\|func testEdge" internal/edge/*_test.go`):
1. door caller, local route of its app → served;
@@ -727,14 +725,14 @@ func (e *Edge) serveDoor(w http.ResponseWriter, r *http.Request) {
doorRefused(w)
return
}
- // No visitor address is believed (Security model): every forwarded
- // header goes, and RemoteAddr names the calling app without a port, so
- // the gate's limiter and the visitor hash key on it as one client.
+ // No visitor address is believed (Security model): every header that
+ // claims one goes, and the request comes from this box's own public
+ // address, exactly as it would through the public URL.
stripForwardedHeaders(r)
for _, h := range doorAddressHeaders {
r.Header.Del(h)
}
- r.RemoteAddr = "door/" + tok.Account + "/" + tok.App
+ r.RemoteAddr = net.JoinHostPort(e.doorAddr, "0")
e.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), doorCallerKey{}, doorCaller{Account: tok.Account, App: tok.App})))
}
@@ -744,18 +742,21 @@ func doorRefused(w http.ResponseWriter) {
}
```
-`openDoor(path)`: refuse if `e.doorKeys == nil`; `os.Remove(path)` a stale socket; `net.Listen("unix", path)`; `os.Chmod(path, 0o660)`; serve `&http.Server{Handler: http.HandlerFunc(e.serveDoor), ReadHeaderTimeout: 10 * time.Second}` in a goroutine; `closeDoor` shuts the server down with a 5 s context and removes the socket. Group comes from `/run/carlos`'s setgid `carlos-sockets` (bootstrap.go:61, 86-87); do not chgrp.
+`openDoor(path)`: refuse if `e.doorKeys == nil` or `e.doorAddr` does not parse as a global unicast IP (`net.IP.IsGlobalUnicast` and not `IsPrivate`); `os.Remove(path)` a stale socket; `net.Listen("unix", path)`; `os.Chmod(path, 0o660)`; serve `&http.Server{Handler: http.HandlerFunc(e.serveDoor), ReadHeaderTimeout: 10 * time.Second}` in a goroutine; `closeDoor` shuts the server down with a 5 s context and removes the socket. Group comes from `/run/carlos`'s setgid `carlos-sockets` (bootstrap.go:61, 86-87); do not chgrp.
- [ ] **Step 1: Failing tests:**
1. no key, malformed key, revoked key → 401 with `Cache-Control: no-store`, upstream never called;
2. valid key → served; the upstream sees no `X-Carlos-Door-Key` and sees `X-Tito-Auth-User` untouched;
3. inbound `X-Carlos-Hop`, `X-Carlos-Hop-Auth` → neither reaches the upstream, and a miss goes to the door's owner check, never `staleHopRefusal`;
- 4. forwarded headers: a request with `X-Forwarded-For: 203.0.113.9`, `Forwarded: for=198.51.100.7` and `X-Forwarded-Proto: http` reaches the upstream with exactly `X-Forwarded-For: 192.0.0.8` and `X-Forwarded-Proto: https` and no `Forwarded`. The same holds with none of them sent. `clientIP` returns `door/bab/a`. The gate stub's `clientKey` sees `door/bab/a` for two requests that claimed different addresses;
+ 4. forwarded headers: with the door's address set to `203.0.113.10`, a request carrying `X-Forwarded-For: 198.51.100.7`, `Forwarded: for=198.51.100.9` and `X-Forwarded-Proto: http` reaches the upstream with exactly `X-Forwarded-For: 203.0.113.10` and `X-Forwarded-Proto: https`, and no `Forwarded`. The same holds when none of them is sent. `clientIP` returns `203.0.113.10`, and so does the gate stub's `clientKey`;
4b. alternate address headers: a request carrying every name in `doorAddressHeaders` reaches the upstream with none of them;
- 4c. **a Rails-rule resolver in the upstream**, written in the test to Rails 8.0.2's `RemoteIp#calculate_ip` rules (drop non-IP XFF entries; strip trusted proxies: loopback, `fc00::/7`, 10/8, 172.16/12, 192.168/16; if nothing is left, prefer `Client-IP`, else the peer), fed the upstream's `X-Forwarded-For`, `Client-IP` and a loopback peer, resolves `192.0.0.8` for a door request, with and without an inbound `Client-IP`, served locally and backhauled. It never resolves loopback or a caller-chosen address;
+ 4c. **resolver-rule tests in the upstream**, written in the test to two published rule sets, fed the upstream's headers and a loopback peer:
+ - Rails 8.0.2 `RemoteIp#calculate_ip`: drop non-IP XFF entries; strip trusted proxies (loopback, `fc00::/7`, 10/8, 172.16/12, 192.168/16); if nothing is left, prefer `Client-IP`, else the peer.
+ - python-ipware 3.0: consult its header list; prefer a global address over a non-global one; Python's `is_private` ranges.
+
+ A door request carrying every name in `doorAddressHeaders` set to `8.8.8.8` resolves to `203.0.113.10` under both, served locally and backhauled. It never resolves loopback, a private address or `8.8.8.8`;
5. a gated route still answers with the gate's page;
- 5a. cache: with a cacheable upstream response, a door request neither populates the cache (a following public request goes to the upstream) nor is served from it (a public request's cached response is not returned to a following door request);
- 5b. a backhauled door request: using two in-process edges joined as the existing backhaul tests join them (`grep -n "func.*TwoEdges\|backhaul.*httptest" internal/edge/*_test.go`), the far upstream sees `X-Forwarded-For: 192.0.0.8` and `X-Forwarded-Proto: https`, the same as case 4;
+ 5b. a backhauled door request: using two in-process edges joined as the existing backhaul tests join them (`grep -n "func.*TwoEdges\|backhaul.*httptest" internal/edge/*_test.go`), the far upstream sees `X-Forwarded-For: 203.0.113.10` and `X-Forwarded-Proto: https`, the same as case 4, and the same again when the far edge cannot verify the hop;
6. `openDoor` on a temp path makes a 0660 socket; a real unix-socket client round-trips case 2; `closeDoor` removes the socket; `openDoor` with a nil mirror returns an error and creates nothing.
- [ ] **Step 2–4:** run, implement, run.
- [ ] **Step 5: Commit** `edge: the edge-local door (#175 P7)`.
@@ -767,10 +768,10 @@ func doorRefused(w http.ResponseWriter) {
**Files:** Modify `internal/edge/edge.go` (a new `RunOption`; the home-map arming block ~line 2985; the shutdown block ~line 3110; the dev-mode branch that returns early), `internal/host/agent.go` (the options passed to `edge.RunWithActivatorReady`, ~line 510). Tests in `internal/edge/edge_test.go` (or `door_test.go`) and `internal/host/agent_test.go`.
**Interfaces:**
-- `func WithEdgeDoor(path string) RunOption` — records the path on the `Edge`.
-- In the run path, immediately after `e.homes.Store(hm)` and its `go hm.run(...)`: when a door path is set and `e.staticStore` and `e.staticPub` are non-nil, build `e.doorKeys = newDoorKeyMirror(e.staticStore, e.staticPub)`, run one synchronous `Refresh` with a 10 s budget (log a failure, carry on: every key is refused until a refresh lands), start `go e.doorKeys.run(...)`, then `openDoor(path)` and keep `closeDoor` on the `Edge`. With no store or no public key: log that the door is off and why. In dev mode: log that the door is off in dev mode.
+- `func WithEdgeDoor(path, publicAddr string) RunOption` — records the path, and the box's public address as `e.doorAddr`.
+- In the run path, immediately after `e.homes.Store(hm)` and its `go hm.run(...)`: when a door path is set and `e.staticStore` and `e.staticPub` are non-nil, build `e.doorKeys = newDoorKeyMirror(e.staticStore, e.staticPub)`, run one synchronous `Refresh` with a 10 s budget (log a failure, carry on: every key is refused until a refresh lands), start `go e.doorKeys.run(...)`, then `openDoor(path)` and keep `closeDoor` on the `Edge`. With no store, no public key, or no usable public address: log that the door is off and why. In dev mode: log that the door is off in dev mode.
- Shutdown calls `closeDoor` before the HTTP servers' `Shutdown`.
-- agent: `if p := strings.TrimSpace(os.Getenv("CARLOS_EDGE_DOOR")); p != "" { opts = append(opts, edge.WithEdgeDoor(p)) }`.
+- agent: `if p := strings.TrimSpace(os.Getenv("CARLOS_EDGE_DOOR")); p != "" { opts = append(opts, edge.WithEdgeDoor(p, readPublicIP())) }`.
- [ ] **Step 1: Failing tests:** extract the arming block into `func (e *Edge) armDoor(ctx context.Context) error` so it is testable: with a `DirStore` holding one signed record and a temp socket path, `armDoor` then a real unix-socket request with the key → served; before any record exists → 401 (not a panic); after `closeDoor` the socket is gone. Agent: with `CARLOS_EDGE_DOOR` unset no `WithEdgeDoor` option is passed (extract `edgeDoorOptions(getenv func(string) string, dataDir string) []edge.RunOption`).
- [ ] **Step 2–4:** run, implement, run; then `GOOS=darwin go build ./... && GOOS=windows go build ./...`.
diff --git a/docs/superpowers/plans/CODEX-FINDINGS-P7-EDGE-DOOR-R5.md b/docs/superpowers/plans/CODEX-FINDINGS-P7-EDGE-DOOR-R5.md
new file mode 100644
index 000000000..f3eabd314
--- /dev/null
+++ b/docs/superpowers/plans/CODEX-FINDINGS-P7-EDGE-DOOR-R5.md
@@ -0,0 +1,45 @@
+Reviewed `bb1be838`. Two blockers remain in the revised address representation and cache bypass.
+
+| Round-4 finding | Disposition |
+|---|---|
+| R4-1: framework fallback to caller headers or loopback | **Partly.** The Rails case is fixed, but Django’s `ipware` can still select caller-controlled headers; see finding 1. |
+| R4-2: cache variation uses rewritten inbound headers | **Partly.** Local requests bypass caching; backhauled requests do not. See finding 2. |
+| R4-3: failed hop verification substitutes an infrastructure address | **Closed as a stated degraded mode.** [Plan:23](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md:23) matches the forwarding fallback. The public-EIP property remains a deployment assumption. |
+| R4-4: gate and visitor partitions misstated | **Closed.** [Plan:14](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md:14) now distinguishes host/app-local keys, backhaul peer addresses, and analytics’ User-Agent contribution. |
+
+With the edge trusted for XFF, source tracing indicates that Rails `RemoteIp`, Express `trust proxy`, and Laravel TrustProxies resolve the supplied single address to `192.0.0.8`. The reviewed Go resolvers also select it after their alternate headers are stripped. I found no additional client-address header omission in those implementations. Sources: [Rails](https://raw.githubusercontent.com/rails/rails/v8.0.2/actionpack/lib/action_dispatch/middleware/remote_ip.rb), [Express’s proxy resolver](https://raw.githubusercontent.com/jshttp/proxy-addr/master/index.js), [Laravel](https://raw.githubusercontent.com/laravel/framework/12.x/src/Illuminate/Http/Middleware/TrustProxies.php), [Symfony’s underlying resolver](https://raw.githubusercontent.com/symfony/http-foundation/7.3/Request.php), [Go realip](https://raw.githubusercontent.com/tomasen/realip/master/realip.go), [Chi](https://raw.githubusercontent.com/go-chi/chi/master/middleware/realip.go).
+
+Frameworks configured to ignore forwarding can still report the backend peer, including loopback; that also happens for public requests and is not a new door-specific defect.
+
+1. **P1 — Django-ipware can replace the dummy address with a caller-chosen address.**
+
+ The strip list at [plan:662](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md:662) omits **`X-Forwarded` and `Forwarded-For`**. Both survive `stripForwardedHeaders` at [homemap.go:806](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/internal/edge/homemap.go:806).
+
+ Django-ipware delegates resolution to python-ipware. Its resolver checks both omitted headers and prefers a globally routable address over an earlier non-global candidate. [Django wrapper](https://raw.githubusercontent.com/un33k/django-ipware/master/ipware/ip.py), [python-ipware implementation](https://raw.githubusercontent.com/un33k/python-ipware/v3.0.0/python_ipware/python_ipware.py).
+
+ Crucially, Python 3.13 classifies `192.0.0.8` as **private**, despite Go’s narrower `IsPrivate` result. [Python documentation](https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv4Address.is_private).
+
+ I executed the retrieved resolver source on Python 3.13.5 with a loopback backend peer:
+
+ | Upstream headers | Resolved address |
+ |---|---|
+ | XFF `192.0.0.8` only | `192.0.0.8`, classified private |
+ | Same XFF plus `X-Forwarded: 8.8.8.8` | `8.8.8.8` |
+ | Same XFF plus `Forwarded-For: 8.8.8.8` | `8.8.8.8` |
+ | Public comparison: XFF `1.1.1.1` plus either alternate | `1.1.1.1` |
+
+ Thus the new representation enables address spoofing that the public comparison does not. Applications using the resolved address for authorization or rate limits can grant additional authority. Even without alternate headers, a blanket private-address authorization check can classify the dummy as trusted.
+
+ **Required:** strip these additional headers and resolve the Python private-address classification before relying on the no-privilege premise. Add actual Django-ipware tests alongside the Rails test, locally and across backhaul.
+
+2. **P2 — The cache bypass disappears across backhaul.**
+
+ [Plan:687](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md:687) explicitly keeps the door marker local, while [plan:689](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/docs/superpowers/plans/2026-09-28-instance-moves-p7-edge-door.md:689) predicates cache exclusion solely on that marker. Consequently, the receiving edge still performs lookup and storage.
+
+ For example, a verified door response with `Vary: X-Forwarded-For` is stored under inbound `192.0.0.8`. A direct public request supplying that XFF can retrieve it, although forwarding to the application would replace that header with the public caller’s address. Lookup uses inbound headers at [edge.go:1583](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/internal/edge/edge.go:1583); storage does likewise at [edge.go:1702](/home/paulca/amadan.net/carlos/platform-instance-moves-p7/internal/edge/edge.go:1702).
+
+ Failed hop verification also loses the bypass and strips XFF, restoring the empty-header collision from round 4.
+
+ **Required:** make cache exclusion effective at the receiving edge, including the defined verification-failure path. Extend the cache tests to two edges, both request orderings, and mismatched mesh keys.
+
+**The plan is not ready to build.**
\ No newline at end of file
diff --git a/docs/superpowers/plans/CODEX-REVIEW-BRIEF-P7-EDGE-DOOR-R6.md b/docs/superpowers/plans/CODEX-REVIEW-BRIEF-P7-EDGE-DOOR-R6.md
new file mode 100644
index 000000000..adc073b69
--- /dev/null
+++ b/docs/superpowers/plans/CODEX-REVIEW-BRIEF-P7-EDGE-DOOR-R6.md
@@ -0,0 +1,17 @@
+# Brief: P7 edge-door plan, review round 6 (confirming)
+
+Rounds 1–5 are the `CODEX-FINDINGS-P7-EDGE-DOOR*.md` files beside the plan.
+The plan is now revision 6.
+
+Rounds 4 and 5 showed that every invented visitor address (`unknown`, then
+`192.0.0.8`) meets some framework that reads it as local, private or trusted.
+So revision 6 invents nothing. A door request carries the box's own public
+address, the one its edges file publishes, and is claimed to be exactly
+equivalent to a request from a process on the box to the instance's public
+URL.
+
+1. **Disposition table** for round 5's two findings: closed / partly / not closed.
+2. **Test the equivalence claim.** Find any observable difference, to the app, the gate, analytics, the cache, or the far edge of a backhaul, between a door request and a public HTTPS request from the same box to the same URL. Include headers the edge adds or strips on each path. Say which differences confer anything.
+3. **New findings only where they are P1 or P2.**
+
+Only the code in this checkout exists; Tasks 1 and 3 are built and Task 2 is being built, so read the plan for the rest. Check every `path:line` you cite. End with one line saying whether the plan is ready to build. Keep it under about 800 words.