carlos / platform Public

spec: relay revision 6, codex round 5

One finding taken: enable refuses an app with a route lacking config
delivery, and a TCP exec route with file delivery establishes its
rotation cutover through carlos relay adopted, recorded against the
identities issued at that moment; status names routes without a cutover.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev fccd93f8d1bcedc4187ed6b91e35e1a5e8293b05 parent 600a8f1
2 files changed, +79 −28
  • docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md +48 −28
  • docs/superpowers/specs/CODEX-FINDINGS-RELAY-R5.md +31 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md b/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md
index 693305018..ddd575384 100644
--- a/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md
+++ b/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md
@@ -1,15 +1,16 @@
# Stage 4a: a platform STUN and TURN relay
-**Date:** 2026-09-07, revision 5
+**Date:** 2026-09-07, revision 6
**Status:** rulings taken. Paul ruled on the six decisions in §9 on
2026-09-07, each marked **(ruling)**; the roadmap's stage 4a text and
-gate are amended so the two documents agree. Reviewed four times by
+gate are amended so the two documents agree. Reviewed five times by
GPT-6 via Codex: revision 1's 20 findings are in
`CODEX-FINDINGS-RELAY.md`, revision 2's 12 in
`CODEX-FINDINGS-RELAY-R2.md`, revision 3's 8 in
`CODEX-FINDINGS-RELAY-R3.md`, revision 4's 3 in
-`CODEX-FINDINGS-RELAY-R4.md`, unedited; every one is taken, marked
-**(codex R1-N)** through **(codex R4-N)**. Stage 4a of the media roadmap
+`CODEX-FINDINGS-RELAY-R4.md`, revision 5's 1 in
+`CODEX-FINDINGS-RELAY-R5.md`, unedited; every one is taken, marked
+**(codex R1-N)** through **(codex R5-N)**. Stage 4a of the media roadmap
(`2026-09-07-media-platform-roadmap-design.md`), built under rulings 8,
10 and 11 there.
@@ -114,10 +115,13 @@ change does today, and what happens next depends on the route's shape
(`internal/appconfig/sync.go:30`, `syncexec.go:1`): a unit-backed route
and a socket-backed exec route are restarted onto the new file; a
TCP-address exec route whose bundle opts into file delivery receives
-the file and is **not** restarted, so the app must re-read it; a
-TCP-address route without file delivery receives nothing. `carlos relay
-status` names which of these each route is. **(codex R1-10, R1-14,
-R2-6, R3-4)**
+the file and is **not** restarted, because its process belongs to an
+external supervisor CARLOS cannot bounce, so the app must re-read it
+and then tell the platform it has (`carlos relay adopted`, §5); a
+TCP-address route without file delivery receives nothing, and **an app
+with such a route cannot enable the relay** until it opts that route
+into file delivery or moves it to a unit. `carlos relay status` names
+which of these each route is. **(codex R1-10, R1-14, R2-6, R3-4, R5-1)**
**A credential keeps working for its whole life** provided the master
that signed its key is not force-retired before its expiry. The
@@ -468,6 +472,10 @@ ruling 2):
1. `apiAccountApp` gate; unknown and foreign are one byte-identical 404.
2. Refuse if the deployment has no relay regions (`CARLOS_RELAY_REGIONS`
in console config is empty) with a message naming the operator.
+ Refuse if any of the app's routes has no config delivery (a
+ TCP-address route whose bundle does not opt into file delivery),
+ naming the route, because such a route could never adopt a rotated
+ key and would block retirement forever. **(codex R5-1)**
3. Regions are every region in `CARLOS_RELAY_REGIONS`, **in the
console's order**, or `--regions` narrows them keeping that order.
There is no placement-first promise: placement names (`ie`,
@@ -517,11 +525,13 @@ suffixes) the console last issued per region; delivery per route,
"adopted at <time>, restarted" when a config-ack names exactly the
issued identities and a successful restart,
"written, awaiting delivery" when no ack names them yet, "file
-delivered, not restarted by the platform" for a TCP exec route with
-file delivery (the app must re-read), "restart failed" when the ack
-says so, "hibernating, delivered on wake" when the route is asleep,
-and "not delivered: route has no config delivery" for a TCP-address
-route without it; disable progress as above; and per region, from
+delivered, awaiting `carlos relay adopted`" for a TCP exec route with
+file delivery until a member acknowledges it, "adopted by member at
+<time>" after, "restart failed" when the ack says so, and
+"hibernating, delivered on wake" when the route is asleep (a route
+added without delivery after enable shows "no config delivery: relay
+will not reach this route" and blocks retirement until fixed); disable
+progress as above; and per region, from
`control/relay-ack/<box>.json`, the heartbeat age, whether the record
it holds is current, whether its lease is live, which listeners are
up, and which `kid`s it has loaded. A heartbeat older than five minutes
@@ -555,20 +565,26 @@ R1-10, R2-4)**
the earliest permitted retirement time for the old `kid`: 28 hours
from now.
5. `carlos relay status --region r` shows, per route of every enabled
- app, which identities its config-ack names and when, and for the
- routes the sync cannot observe adopting (TCP-address exec routes
- with file delivery, and routes with no delivery) the time of their
- last platform-driven restart through `carlos restart`, which the
- activator records, since a process on such a route holds the old
- key until it is restarted. **Each route's cutover** is its ack time
- or, for an unobservable route, its restart time after the file was
- delivered. The old master's `retire_at` may be set no earlier than
- `max(rotation + 28h, latest cutover + 4h)` across every route of
- every enabled app in the region, and `status` prints that time once
- every route has one. Setting it earlier is refused by nothing but
- the docs, and ends any allocation still on the old `kid`, which is
- the force-retirement case §2 excludes from its guarantee. **(codex
- R3-4, R4-3)**
+ app, which identities its config-ack names and when. **Each route's
+ cutover** is established one of two ways, by route shape. A
+ unit-backed or socket-exec route: the config sync's ack naming the
+ issued identities, since the platform restarted it. A TCP-address
+ exec route with file delivery: its process belongs to an external
+ supervisor the platform cannot restart, so the cutover is a
+ **member acknowledgement**, `carlos relay adopted --host <route>`,
+ given after the member has restarted the process; the console
+ records it in `relay.json` against that route together with the
+ identities issued at that moment, so an acknowledgement given
+ before a later rotation does not count for it. A route in neither
+ category cannot exist under an enabled relay (enable refuses it),
+ and a route added later without delivery blocks retirement until
+ it is fixed. The old master's `retire_at` may be set no earlier
+ than `max(rotation + 28h, latest cutover + 4h)` across every route
+ of every enabled app in the region, and `status` prints that time
+ once every route has a cutover, or names the routes still without
+ one. Setting it earlier is refused by nothing but the docs, and ends
+ any allocation still on the old `kid`, which is the force-retirement
+ case §2 excludes from its guarantee. **(codex R3-4, R4-3, R5-1)**
6. After `retire_at` the operator removes the line from the boxes and
the console secret at leisure.
@@ -642,7 +658,11 @@ unfinished ones, publishes without the app within five minutes.
syncs not reported as adopted by the old ack, and `rotate` refusing
until the fixture heartbeats list the kid; the config sync writing a
per-route ack naming the identities it rendered; the retirement time
- computed with a manual cutover after hour 28.
+ computed with a manual cutover after hour 28; enable refused for an
+ app with a route lacking config delivery; a `carlos relay adopted`
+ given before a later rotation not counting as that rotation's
+ cutover; an externally supervised TCP route with file delivery
+ blocking retirement until acknowledged.
- `docs/site/cli.md` gains `### carlos relay` with a fenced example, or
the docsite gate fails.
- Flagship gate, as amended in the roadmap: a Meet call between two
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-RELAY-R5.md b/docs/superpowers/specs/CODEX-FINDINGS-RELAY-R5.md
new file mode 100644
index 000000000..f2f06a902
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-RELAY-R5.md
@@ -0,0 +1,31 @@
+# Findings on the relay spec, revision 5
+
+Reviewed by **GPT-6 (Codex), 2026-09-07**, fifth pass, against revision 5 at `600a8f1`, with Paul's rulings treated as fixed. Unedited apart from this note. Revision 6 records how the finding was taken.
+
+| Revision-4 finding | Disposition | Reason |
+|---|---|---|
+| R4-1 — Rotation publication fence | Closed | Lease ownership checked at each bucket write, combined with config CAS and per-app serialization, provides a buildable fence. |
+| R4-2 — Acknowledgement identity | Closed | Comparing the full `username_suffix` per region distinguishes credentials across disable/re-enable. |
+| R4-3 — Retirement after manual cutover | Partial | The formula includes late cutovers, but the prescribed restart cannot establish cutover for every unobservable route. |
+
+Reviewed commit `600a8f15912513eb4fb538a343a0877531b6fac3`, treating Paul’s six rulings as fixed. The cryptographic vector reproduces. [Pion v4.1.4’s API](https://pkg.go.dev/github.com/pion/turn/v4) and local authentication source were checked. No files were changed; no live TURN or EC2 tests were run.
+
+## R5-1 — P2 — Rotation has no cutover path for externally managed TCP routes
+
+**Sections:** §2, config delivery; §5, rotation step 5.
+
+Rotation step 5 uses an activator-recorded `carlos restart` time as the cutover for TCP-address routes whose adoption config sync cannot observe. CARLOS does not necessarily control those processes.
+
+Consider a TCP-address route with file delivery enabled, but no socket, unit backing or named unit. Its external supervisor started the process with the old relay key. Rotation delivers the new file, but `carlos restart` cannot restart that supervisor: the activator has no child to bounce, and the root fallback cannot reach the external process.
+
+The required cutover therefore never arrives. Recording the restart request or a no-op as cutover instead would permit retirement while that process still mints old-key credentials.
+
+Routes **without config delivery**, also included in step 5, have an additional contradiction: their cutover requires a restart “after the file was delivered,” but no such delivery occurs.
+
+**Evidence:** The [retirement procedure](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:557) requires these restart times. [TCP config delivery](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/syncexec.go:11) explicitly leaves process ownership with the external supervisor. [`Bounce`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/activator/activator.go:872) affects only a running activator entry; [restart dispatch](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/agent.go:2532) supplies no general external-supervisor mechanism.
+
+**Correction:** Define which route shapes can establish cutover. Either require managed delivery and a controllable restart path, or specify an external adoption acknowledgement bound to the newly issued identities. A route without delivery must remain a retirement blocker until that prerequisite is resolved. Preserve `max(rotation + 28h, latest cutover + 4h)`.
+
+Test an externally supervised TCP route with no named unit: a restart request must not establish cutover. Also test a route without config delivery.
+
+**Verdict: request changes.** No P1 remains. One P2 remains in the rotation cutover mechanism.
\ No newline at end of file