# Findings on the relay spec, revision 1

Reviewed by **GPT-6 (Codex), 2026-09-07**, independently of the draft author, from the brief in `CODEX-REVIEW-BRIEF-RELAY.md`. Unedited apart from this note. Revision 2 of the spec records how each finding was taken.

Reviewed commit `3d453c6b1f50b9c1fe8dc0a835e688e913e8c548`. Spec line references below refer to the [relay spec at that revision](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md). I independently reproduced all three cryptographic values in the test vector; they match. Pion findings use v4.1.4’s published API and source. No files were changed; no live TURN test was run.

## R1-1 — P1 — The advertised JSON cannot pass through sealed config

**Section:** §2, lines 44–57; §5, lines 241–246.

`CARLOS_RELAY` contains JSON double quotes. The existing renderer rejects quotes in every value, including decrypted secrets. Writing it as `kind:"secret"` therefore does not solve delivery: the envelope can be stored, but rendering the app’s configuration fails. This blocks the contract Meet is supposed to implement.

**Evidence:** [`ValidVarValue`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/materialize.go:23) rejects quotes; [`RenderEnv`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/materialize.go:49) decrypts secrets and applies that same check.

**Correction:** Either specify a quote-free wire encoding and its app-side decoding, or explicitly extend config delivery to support JSON safely across both systemd and exec consumers. Test the complete seal → store → render → child-environment path before freezing §2.

## R1-2 — P1 — `AuthHandler` cannot try the previous master

**Section:** §4, verification step 4; §2’s rotation guarantee.

The handler receives no password or request integrity value and returns exactly one key. Computing the current credential and returning its key ends the callback. Pion does not call it again with the previous master after integrity verification fails. Consequently, putting the new master first immediately rejects credentials derived from the old one.

**Evidence:** Pion’s [`AuthHandler` signature](https://pkg.go.dev/github.com/pion/turn/v4#AuthHandler) returns one `[]byte`; [`authenticateRequest`](https://raw.githubusercontent.com/pion/turn/v4.1.4/internal/server/util.go) calls it once and performs one integrity check.

**Correction:** Specify an implementable key-selection mechanism before declaring the username format fixed. Options include an authenticated key identifier in the credential contract or an explicit request-verification adapter supporting multiple candidate keys. Add interoperability tests exercising old and new credentials concurrently.

## R1-3 — P1 — Authentication is not bound to the allocation’s owner

**Section:** §3 authentication and metering; §4 verification.

Pion v4.1.4 authenticates subsequent requests but locates their allocation by connection tuple without comparing the authenticated username to its original owner. A client can allocate using app A’s credential and then refresh or modify that allocation using app B’s valid credential on the same tuple. The proposed handler accepts B because B is enabled; nothing in the spec connects that decision to A’s allocation.

This breaks the cross-app credential boundary and can separate the identity authorizing traffic from the identity being metered.

**Evidence:** [`handleRefreshRequest`, `handleCreatePermissionRequest`, and `handleChannelBindRequest`](https://raw.githubusercontent.com/pion/turn/v4.1.4/internal/server/turn.go) use tuple-based lookup without an owner comparison. [RFC 8656 §21.3.3](https://www.rfc-editor.org/rfc/rfc8656.html#section-21.3.3) requires subsequent credentials to match the allocation’s original credentials.

**Correction:** Bind each allocation immutably to its authenticated identity and enforce that binding on subsequent requests. Name the patched library or adapter that supplies this check. Test credential substitution on an existing tuple, rather than only changing fields in a new Allocate request.

## R1-4 — P1 — Refusing refresh does not terminate expired traffic

**Section:** §2, lines 75–81; §3 lifetime limits; §4 expiry verification.

Rejecting an expired Refresh request does not delete the allocation. Existing ChannelData and Send indications bypass `AuthHandler`, so traffic can continue until existing allocation, permission, or channel state expires. The spec’s claim that the next refresh ends the relay is therefore false.

The ten-minute allocation lifetime is also a default, not the stated cap: Pion accepts requested lifetimes below one hour, including values well above ten minutes.

**Evidence:** Pion’s [request handlers](https://raw.githubusercontent.com/pion/turn/v4.1.4/internal/server/turn.go) return on authentication failure without deleting the allocation. Its [`allocationLifeTime`](https://raw.githubusercontent.com/pion/turn/v4.1.4/internal/server/util.go) accepts client-requested durations below one hour. The [roadmap’s stage 4a policy](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:650) requires established allocations to end at credential expiry.

**Correction:** Give allocations an absolute credential deadline and an active termination mechanism independent of client refreshes. Specify how the implementation closes individual allocations for expiry, disable, region removal, and lease expiry. Enforce the ten-minute maximum explicitly. Test continued traffic after the deadline with no further authenticated requests.

## R1-5 — P1 — The serving lease has no renewal mechanism

**Section:** §3 authority record; §5 records.

The console rewrites the authority record only on enable and disable. Relays fetch it only at startup or after a serial change. After fifteen quiet minutes, every relay must terminate its allocations and refuse new ones—even when the deployment is healthy.

Measuring age since receipt does not fix the missing refresh path. Nor may an unrelated serial bump renew authority merely by causing an old record to be reread.

**Evidence:** Spec lines 169–175 and 226–230 define those triggers. [`BumpSerial`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/release/serial.go:46) is explicitly a latency hint whose failure is tolerated, not a lease renewal.

**Correction:** Define periodic authority revalidation independently of serial changes, the exact freshness clock, and what successful observation renews the lease. Failed reads must not renew it. Test both a healthy deployment with no writes for more than fifteen minutes and an outage that continues past the lease deadline.

## R1-6 — P1 — Concurrent rebuilds can restore revoked authority

**Section:** §5 records, enable, disable, and deletion.

CAS protection on each app’s `relay.json` does not serialize replacement of the shared `control/relay.json`. An enable for B can read a snapshot containing A, pause, and overwrite the aggregate after A’s disable has removed A. The replacement can carry a fresh `written_at`, restoring A’s authority.

The multi-object operation also has no recovery protocol. Failure after changing the app record but before publishing the aggregate leaves contradictory state indefinitely.

**Evidence:** Spec lines 221–230 give CAS semantics only to the per-app record. Lines 240–259 describe sequential writes without aggregate fencing or a reconciliation loop. The existing [`appconfig.Apply`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/generation.go:113) guarantees one config-manifest write, not a transaction across these objects.

**Correction:** Specify a fenced aggregate publisher that rebuilds after conflicts, plus durable reconciliation of incomplete operations. Include deletion and rotation in the same concurrency model. Test overlapping enable/disable operations, stale publishers, and failure after every durable write.

## R1-7 — P1 — Console master custody contradicts the claimed exposure boundary

**Section:** §4 region master; §5 rotation.

The console holds every region’s masters as “sealed console secrets,” while the same paragraph says no master exists in the bucket, sealed or otherwise. If these use the existing sealed-config mechanism, both statements cannot be true: a compromised host can decrypt the console bundle and obtain every region’s master.

That exposure is stronger than the roadmap’s conceded access to enabled apps’ derived keys. Masters also derive keys for future incarnations and future enables.

**Evidence:** Spec lines 179–187. The [roadmap §1](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:52) explicitly describes deployment-wide decryption; [stage 4a’s custody statement](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:641) distinguishes regional masters from derived-key exposure.

**Correction:** Name the console’s actual secret store, encryption authority, and readers. To preserve the stated boundary, its master copies must not use the deployment-wide sealed-config key. Keep the already-accepted derived-key exposure explicit.

## R1-8 — P1 — The peer policy blocks two clients using the same relay

**Section:** §3 peer policy; §7 flagship gate.

Two clients forced to use TURN can receive relay candidates on the same region EIP at different allocation ports. Each then needs permission to reach the other’s candidate. The unconditional refusal of the relay’s own addresses rejects both.

This affects the promised fallback when both clients cannot send UDP directly and connect to the relay over TCP or TLS. Falling through among URLs on the same server does not resolve it.

**Evidence:** Spec lines 129–151 assign one public relay address and reject it as a peer. Pion’s [`PermissionHandler`](https://raw.githubusercontent.com/pion/turn/v4.1.3/server_config.go) receives a peer IP without its port, so that hook alone cannot distinguish a live relay allocation from another service on the box.

**Correction:** Specify a safe relay-to-relay mechanism that preserves the prohibition on reaching arbitrary host services. An allocation-aware internal forwarding path needs an explicit design; an IP-only exception is insufficient. Add a test with both browsers forced to relay through the same region.

## R1-9 — P2 — The operational limits do not bound bandwidth or accounts

**Section:** §3 limits; §9 decisions 2 and 3.

The 20 GB threshold refuses only new allocations. Existing allocations can keep transferring and refreshing after crossing it. One enabled app can therefore consume sustained bandwidth beyond the advertised budget.

There is also no account limit. One account controlling 32 apps can fill all 2,048 allocation slots while every app remains within its individual cap.

**Evidence:** Spec lines 153–160. The [roadmap’s policy](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:650) requires allocation, rate, bandwidth, and lifetime bounds per app, account, and box.

**Correction:** Define ongoing traffic enforcement and aggregate account/box limits. Specify atomic admission across all listeners, failed-allocation accounting, and restart behavior for counters. Test an established allocation crossing the byte limit and one account spreading allocations across multiple apps.

## R1-10 — P2 — Rotation drops calls and has no durable retirement deadline

**Section:** §2 rotation promises; §5 rotation procedure.

Restarting `carlos-relay.service` destroys its in-memory allocations and sockets. Even after fixing R1-2, step 1 drops calls rather than preserving them.

The master list also contains no activation or retirement timestamp. “Previous for twenty-four hours” cannot survive process restarts reliably without additional state. Finally, an old derived key can mint a fresh four-hour credential late in the grace period; removing its master at hour 24 does not imply every such credential has expired.

**Evidence:** Spec lines 87–91, 181, and 263–271. Pion’s [server lifecycle](https://raw.githubusercontent.com/pion/turn/v4.1.4/server.go) keeps allocation state inside the running server.

**Correction:** Specify master reload without process replacement, durable key epochs and retirement deadlines, and acknowledgements for relay installation and app key adoption. Define how minting near retirement is bounded. Test uninterrupted traffic during rotation, delayed app delivery, partial rollout, and relay restart during grace.

## R1-11 — P2 — The minting contract does not supply its identity inputs

**Section:** §2 environment and minting.

The app receives the regional keys and incarnation but must independently know the canonical account sqid and CARLOS app name. §2 does not specify where either comes from. An app’s own team identifier or application name is not necessarily its deployment identity.

The encoding instructions should also explicitly require decoding the supplied base64 key into 32 raw bytes before HMAC.

**Evidence:** Spec lines 44–73. The existing [agent-owned config variables](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/agentvars.go:19) do not supply these identities; the [exec backend’s injected variables](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/activator/backend_exec.go:660) do not supply them either.

**Correction:** Deliver the canonical account/app identity, or an exact username suffix, as part of the contract. State the base64 alphabet, padding, key decoding, and username byte encoding explicitly. Supply app-side pseudocode using only documented inputs.

## R1-12 — P2 — Region vocabulary and per-instance ordering are unresolved

**Section:** §2 regional array; §5 enable; §6 console configuration.

The example uses AWS region names, while CARLOS placement uses values such as `ie`. The spec requires the instance’s own region first but writes one ordered array into an app-wide config bundle. Two instances of one app in different regions cannot both receive their own region first through that operation alone.

The master-variable template is also incomplete: literal substitution of `eu-west-1` produces a name containing hyphens, which the config API rejects.

**Evidence:** [`edgesFile`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/edgesfile.go:72) explicitly separates AWS region from CARLOS placement. [`Apply`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/generation.go:113) writes the app’s default bundle. [`ValidVarName`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/appconfig.go:103) excludes hyphens.

**Correction:** Choose one relay-region vocabulary, define its placement mapping and environment-variable normalization, and specify where per-instance ordering occurs. Test one app placed in two regions and the case where its local region is outside the allowed relay list.

## R1-13 — P2 — Repeated enable contradicts the incarnation contract

**Section:** §2 incarnation; §5 enable.

§2 says incarnation changes only after disable followed by enable, and that region changes preserve it. Enable step 4 instead writes a fresh incarnation unconditionally. Retrying enable after a lost response can therefore invalidate live credentials. Using enable to change regions has the same problem.

**Evidence:** Spec lines 83–85 versus 221–224 and 238–240.

**Correction:** Define explicit state transitions: first enable, repeated enable, region update, repeated disable, and re-enable. Repeated enable must preserve the existing incarnation. Specify how removing a region terminates allocations there without rotating credentials in retained regions.

## R1-14 — P2 — Status cannot establish delivery or current service health

**Section:** §2 config restarts; §5 enable response and status.

A successful config write does not prove that an instance received or loaded it. The proposed response nevertheless reports delivery and a restart. Some supported TCP-address routes receive files without ever being restarted by CARLOS; others are not eligible for delivery.

The relay acknowledgement is also insufficient as specified. A dead relay’s last acknowledged `written_at` can equal the current record indefinitely. That establishes which record it once held, not whether it is alive, within lease, or listening on the advertised transports.

**Evidence:** [`SyncExec`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/syncexec.go:11) documents the route exceptions. [`store.go`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/store.go:24) explicitly distinguishes recorded writes from runtime delivery.

**Correction:** Report desired state, publication state, config generation, observed delivery, fresh relay heartbeat, lease state, and listener readiness separately. Define acknowledgement expiry and supported route shapes. The member command must expose incomplete enable and disable operations without requiring box access.

## R1-15 — P2 — Allowing the hostname does not cause certificate issuance

**Section:** §3 certificate for 5349.

Adding a relay hostname to the edge’s allowed set does not initiate an ACME order. TURN clients connect to 5349, where no listener starts until a certificate exists, so they cannot trigger the edge’s 443 certificate path.

The existing proactive certificate warmer does not automatically solve this: it enumerates routed/advertised hosts and excludes hosts under the apps domain. Issuance also belongs to the elected deployment issuer, which may be another box.

**Evidence:** [`warmSet`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/edge/certwarm.go:351) and [`issuerHostPolicy`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/edge/certsync.go:191).

**Correction:** Register relay names with the deployment-wide issuance and challenge machinery, explicitly trigger issuance and renewal, and define certificate propagation. Specify how 5349 starts when its first certificate arrives. Test from an empty cache with a different box holding the issuer role.

## R1-16 — P2 — Installing the unit does not activate or update its process

**Section:** §3 process.

Adding a service to `enabledUnits` enables it for boot but does not start it. Bootstrap deliberately starts only timers and paths. Platform self-update uses the same arming plan and explicitly restarts only the edge.

The relay can therefore remain inactive after installation or continue running the old executable after a platform update. Sharing the binary does not provide process adoption.

**Evidence:** [`systemctlPlan`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/bootstrap.go:116), [`systemApplier.arm`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/systemapply.go:661), and [`restartEdge`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/systemapply.go:568).

**Correction:** Specify initial activation, conditional enable/disable convergence, required directory creation, and update/rollback handling for the relay process. Include the staging/bootstrap-plan path, which cannot assume it contains the live box’s `host.env`. Test installation and update without rebooting.

## R1-17 — P2 — The metrics integration lacks a compatible schema and single writer

**Section:** §3 metering.

Existing metrics samples have no per-app relay fields. More seriously, the relay is a separate process, while the host metrics writer retains its own hourly sample buffer. Independently appending relay samples to the same objects is unsafe: subsequent host flushes can replace them using the host’s buffered view.

**Evidence:** [`metrics.Sample`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/metrics/sample.go:29) contains host and unit samples only. [`writeHourChunk`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/metrics_loop.go:338) merges existing data only on the writer’s first write for that hour.

**Correction:** Define an additive relay sample schema and one writer per object. Either let the host sampler collect relay counters over a local interface or give the relay separate chunk keys. Specify authenticated allocation attribution, counter reset semantics, and updates to raw readers and rollups.

## R1-18 — P2 — The inter-region meter and acceptance requirement were dropped

**Section:** §3 metering; §6 egress cost; §7 testing; §8 exclusions.

`relay_allocs` does not replace the roadmap’s third meter: inter-region bytes. The proposed samples cannot distinguish those bytes from other traffic. The flagship gate also reduces the roadmap’s account-usage requirement to unspecified meters moving, while deferring the usage view.

**Evidence:** The [roadmap’s metering contract](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:662) requires received, sent, and inter-region bytes. Its [stage 4a gate](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:931) requires those meters on the account’s usage line.

**Correction:** Restore the inter-region meter, define how traffic is classified and counted without double charging, and retain allocation count as an additional metric. Align the acceptance gate and stage boundary with the parent rather than silently weakening them.

## R1-19 — P2 — The unit does not establish that the relay loses first

**Section:** §3 hardening.

The edge already permits 80% of physical memory; the proposed relay permits another 25%, before console and other services. Those independent ceilings allow host-wide memory pressure before either service reaches its own ceiling. `Nice` and `CPUWeight` do not determine which process loses under memory pressure.

**Evidence:** The [edge unit](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/units/carlos-edge.service:57) explains its existing memory allocation. Systemd documents [memory ceilings and CPU weights](https://raw.githubusercontent.com/systemd/systemd/main/man/systemd.resource-control.xml) separately from [OOM victim adjustment](https://raw.githubusercontent.com/systemd/systemd/main/man/systemd.exec.xml).

**Correction:** Budget memory across the shared box and specify the intended memory-pressure priority explicitly. Validate that priority under combined edge, tenant, and relay load on a `t4g.small`; the listed settings alone do not establish it.

## R1-20 — P2 — Remaining factual claims and references need correction

**Sections:** §2, §3, §4, and §9.

The following defects are separate from the mechanism failures above:

| Claim | Evidence | Correction |
|---|---|---|
| The vector is already checked into `internal/relay/vector_test.go` (§2:103). | `internal/relay` does not exist at the reviewed commit. | State that the test will be added. Preserve the verified numerical vector. |
| `agent.go:1761` is the public-IP lookup (§3:134). | The lookup is [`readPublicIP`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/agent.go:1808); line 1761 is blank. | Cite the function and its IMDS leaf read. |
| `agent.go:1304` is the fast serial poll (§3:172). | That location is restart-failure bookkeeping. [`fastConvergeLoop`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/agent.go:1418) documents the actual loop. | Correct the citation and specify the relay’s separate polling or IPC integration. |
| RFC 8489 mandates MD5 with no alternative (§4:214–215). | [RFC 8489 §18.5.1](https://www.rfc-editor.org/rfc/rfc8489.html#section-18.5.1) defines both MD5 and SHA-256; MD5 is the legacy compatibility algorithm. | Attribute MD5 to the selected Pion compatibility mechanism, not an unconditional RFC mandate. |
| Pion sends refreshes, and longer calls must restart ICE “before the hour is up” (§2:77–80). | The app contract hands credentials to a browser; that client controls refreshes. The same section specifies four hours. | Describe client-driven refreshes and use the actual credential deadline. Remove the unsupported promise that four hours covers every meeting. |

**Verdict: request changes.** The app contract cannot be delivered through the current config path, and the proposed mechanisms do not yet establish allocation ownership, timely expiry, reliable revocation, or the claimed master-rotation behavior.