carlos / platform Public

spec: media roadmap revision 5, codex round 4

Eight findings, all accepted. The proxy in front of LiveKit is the
admission gate, checked on every join and reconnect against platform
revocation state, so neither an unexpired token nor a LiveKit-refreshed
one restores a removed participant; removal is a platform door that
drops the signalling connection; webhooks observe, never enforce.
LiveKit is configured with the stage 4a relay as its only STUN and TURN
and the 4b gate blocks third-party network to prove it. A synchronous
transform's first requester waits for the publish door. Stale relay
authority ends carried allocations too. The media-host claim is
consulted by the instances door as well as app naming, and
trailing-hyphen app names are refused enable. Scanner enforcement is
described as optional per store; media is the fourth app-mintable kind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V676C9hPD1oP4Njdw3B26L
Paul Campbell pushed by paul@keymail.dev 1255d28529a1db3def775c46618df5e98e5eac21 parent 0ae7da6
2 files changed, +198 −60
  • docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md +102 −60
  • docs/superpowers/specs/CODEX-FINDINGS-MEDIA-R4.md +96 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md b/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md
index 1686339bc..db02fea45 100644
--- a/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md
+++ b/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md
@@ -1,18 +1,19 @@
# Media on CARLOS: a staged roadmap
-**Date:** 2026-09-07, revision 4
+**Date:** 2026-09-07, revision 5
**Status:** rulings taken. Paul ruled on all eleven decisions in §7 on
2026-09-07; each is marked **(ruling)**. Everything not so marked is a
**(decision)** that a stage spec may overturn cheaply.
Brainstormed with Claude from discussions #107, #112 and #113 and the
-requirements the Meet session sent on 2026-09-07. Reviewed three times
+requirements the Meet session sent on 2026-09-07. Reviewed four times
by GPT-6 via Codex, independently of the author: revision 1's 27
findings are in `CODEX-FINDINGS-MEDIA.md`, revision 2's 14 in
`CODEX-FINDINGS-MEDIA-R2.md`, revision 3's 9 in
-`CODEX-FINDINGS-MEDIA-R3.md`, all unedited. Each place a finding changed
-the text is marked **(codex N)**, **(codex R2-N)** or **(codex R3-N)**.
-§10 names the two parts declined.
+`CODEX-FINDINGS-MEDIA-R3.md`, revision 4's 8 in
+`CODEX-FINDINGS-MEDIA-R4.md`, all unedited. Each place a finding changed
+the text is marked **(codex N)**, **(codex R2-N)**, **(codex R3-N)** or
+**(codex R4-N)**. §10 names the two parts declined.
This is a roadmap, not an implementation spec. Each stage below gets its
own design and plan when it is picked up. The roadmap exists so the first
@@ -54,11 +55,13 @@ and corrected where the reviews found the citations wrong.
a per-region secret changes it. **(codex R2-2)**
- **The store already scans.** `internal/objstore` has a ClamAV scanner,
a resumable sweep that tags every object with a verdict, a scan queue
- of one object per item under the app's own prefix, and a `Deny` on
- `GetObject` for objects tagged infected, written into the store user's
- policy (`internal/objstore/aws.go:211`). There is no upload hook by
- design; the sweep counts what it has not seen. Any new reader of the
- bucket must carry the same deny, or it is a bypass. **(codex 4, 26)**
+ of one object per item under the app's own prefix, and, when a store's
+ scan block has enforcement on, a `Deny` on `GetObject` for objects
+ tagged infected, written into the store user's policy
+ (`internal/objstore/aws.go:211`, `objstore.go:82`). There is no upload
+ hook by design; the sweep counts what it has not seen. Any new reader
+ of the bucket must carry the same deny whenever the store user does,
+ or it is a bypass. **(codex 4, 26, R4-8)**
- **The edge cannot read a store bucket.** The region host role is scoped
to the deployment bucket and its own region's data bucket
(`infra/modules/region-host/main.tf:106`, residency argument in the
@@ -111,8 +114,13 @@ and corrected where the reviews found the citations wrong.
capabilities. **(codex R2-13)**
- **Wildcard certificates cover exactly two labels** under the apps
domain (`internal/edge/wildcard.go:186`), and app names are
- `^[a-z][a-z0-9-]{0,31}$` (`internal/release/release.go:239`), so any
- one-label media name is also a valid app name. **(codex 24, R2-10)**
+ `^[a-z][a-z0-9-]{0,31}$` (`internal/release/release.go:239`), so a
+ one-label media name for any app of up to 26 characters is also a
+ valid app name, and an app name may end in a hyphen, which a DNS
+ label may not (`internal/provision/provision.go:223`). Hostnames in
+ the account namespace are also claimed by the instances door
+ (`internal/console/apiinstances.go:571`), which lets an app take any
+ unclaimed label, not only its own name. **(codex 24, R2-10, R4-4, R4-5)**
- **The fleet is small ARM.** Four production regions on Graviton
`t4g.small`, one `t4g.medium`, burstable at a 20% per-vCPU baseline
with the same two vCPUs on both sizes; the medium adds memory, not
@@ -299,9 +307,11 @@ ruling 11)**
### Stage 1: `kind:media`, the serving class
-A fourth route kind that serves an app's store bucket through the edge,
-cached, with the three doors of §2. This is piece A of #107 with its
-open questions answered.
+A new serving class, the fourth app-mintable route kind beside
+`instance`, `service` and `static` (the edge also dispatches the
+platform's own `status`), that serves an app's store bucket through the
+edge, cached, with the three doors of §2. This is piece A of #107 with
+its open questions answered. **(codex R4-8)**
**What an app sees.** `carlos media enable` writes an additive `media`
block into `store.json` (prefix, allowed regions, key ids, hostname)
@@ -309,15 +319,20 @@ and mints `media-<app>.<sqid>.<apps domain>`, one label so the account
wildcard already covers it. **(codex 24)** Because `media-<app>` is
also a valid app name, the hostname is a **media-host claim** of its
own, owned by the source app and recorded in the account, not an app
-row: the claim accepts `media-` plus any valid app name (up to 38
-characters, well inside a DNS label), is taken atomically at enable,
-and is released at disable. New app names and renames check the claim
-table and are refused for a name a media host holds; enabling media is
-refused, with the error saying so, for an app whose `media-<x>` is an
-existing app's name until the operator renames one. The shared
-`release.ValidApp` grammar is not narrowed, because existing manifests
-and channel pointers parse through it. **(codex R2-10, R3-4)** The app
-writes objects under `<prefix>/public/` or
+row. It is one entry in the account's hostname namespace, and every
+door that acquires a hostname there consults it: new app names,
+renames, and the instances door that lets an app take an unclaimed
+label. Enable is refused, with the error saying so, when `media-<x>`
+is already an app's name or an existing route or instance host, and
+those doors refuse a label a media claim holds; the acquisitions are
+serialised on the claim so two racing requests cannot both win. An app
+whose name ends in a hyphen cannot form a DNS label with `media-`
+prefixed and is refused enable before any claim or grant is written,
+with the error saying to rename. The generated label can be up to 38
+characters, inside a DNS label; the shared `release.ValidApp` grammar
+is not narrowed, because existing manifests and channel pointers parse
+through it. **(codex R2-10, R3-4, R4-4, R4-5)** The app writes objects
+under `<prefix>/public/` or
`<prefix>/protected/` with the store credentials it already has, and
forms URLs:
@@ -419,13 +434,15 @@ That is one serialiser with the state to refuse, which a conditional
write against the manifest is not: an expired worker with no successor
would find the manifest's ETag unchanged and its `If-Match` would
succeed. The serving path reads derived output only through a committed
-manifest, with one exception: the edge that computed a synchronous
-transform may serve those bytes to the request that caused it while its
-publish is in flight, and no other edge sees them until the manifest
-does. Purge, an infected verdict, cancellation and app deletion each
-bump the generation in the registry, so a late commit is refused by the
-door. Attempt keys are swept. This lives in stage 1 because both later
-stages need it and neither may reinvent it. **(codex 17, R2-7, R3-2)**
+manifest, **with no exception**: a synchronous transform's first
+requester waits for the publish door's answer and receives the bytes
+only if it was 200, because an attempt that stalled past its lease or
+across an infected verdict must not reach even the request that caused
+it. One console round trip on a first miss is the cost. Purge, an
+infected verdict, cancellation and app deletion each bump the
+generation in the registry, so a late commit is refused by the door.
+Attempt keys are swept. This lives in stage 1 because both later stages
+need it and neither may reinvent it. **(codex 17, R2-7, R3-2, R4-3)**
**Residency, enforced.** The `media` block names the regions allowed to
serve and cache the app's media. The bucket policy grants only those
@@ -612,10 +629,12 @@ box, built on `pion/turn`, pure Go, in the same binary as the edge, a
bumps the incarnation in the record, which ends existing allocations
and makes every outstanding credential fail, and the incarnation is
never reused across deletion or name reuse. A relay whose authority
- record is older than fifteen minutes refuses new allocations, the
- same lease rule the edge applies to media. Rotation is a new region
- master and a re-render of every enabled app's config. **(codex 13,
- R2-2, R3-3; ruling 8)**
+ record is older than fifteen minutes refuses new allocations **and
+ terminates the ones it is carrying**, the same lease rule the edge
+ applies to media: a disable the relay never heard about must not
+ outlive the outage by the length of a credential. Rotation is a new
+ region master and a re-render of every enabled app's config.
+ **(codex 13, R2-2, R3-3, R4-6; ruling 8)**
- **What that does and does not protect.** Per-region masters mean a
relay box holds no other region's master. They do not mean a
compromised host learns nothing about other regions: sealed config is
@@ -670,22 +689,37 @@ about sixty room-hours a month, since both are dominated by bandwidth.
of its own, `sfu.<region>.<platform domain>`, with TLS terminated by
the edge on that box and the upgrade proxied to LiveKit. The browser
reaches it directly, never through the app's route, so an app on a
- tunnel-backed host still joins SFU rooms. TCP 7881 and UDP 50000 to
- 60000 are opened directly, the same shape as the relay. **(codex R3-5)**
-- **Tenant boundary.** The platform holds LiveKit's API key and secret;
- no app ever does. Rooms are named `<account>/<app>/<incarnation>/
- <room>` and only the platform mints tokens: the app calls `POST
+ tunnel-backed host still joins SFU rooms. Port 7880 is reachable only
+ from that proxy. TCP 7881 and UDP 50000 to 60000 are opened directly,
+ the same shape as the relay. **(codex R3-5)**
+- **Tenant boundary, enforced at admission.** The platform holds
+ LiveKit's API key and secret; no app ever does. Rooms are named
+ `<account>/<app>/<incarnation>/<room>` and only the platform mints
+ tokens: the app's backend calls `POST
/a/<sqid>/apps/<app>/media/sfu/token` with its route credential,
naming a room, a participant identity and grants, and the door
prefixes the room, caps the grants to that app's namespace, signs a
- token with a ten-minute life, and returns it. Participants refresh
- through the same door, so removal is the app declining to refresh
- plus the door calling LiveKit's remove-participant, and a removed
- identity is held on a per-room deny list the door checks on refresh
- and on LiveKit's participant-joined webhook, so an old token cannot
- restore access. Management operations are the door's, scoped to the
- app's rooms. The 4b gate includes a cross-app room name refused and a
- remove-then-rejoin refused. **(codex R3-6)**
+ ten-minute token, and returns it for the app to hand to its browser.
+ **The proxy in front of 7880 is the admission gate**: on every join
+ and reconnect it verifies the token, checks the room's revocation
+ list, and only then opens the upstream connection, serialised against
+ removals so a rejoin cannot race a removal. LiveKit's own token
+ refresh for connected clients does not matter, because admission is
+ keyed on identity and room against platform state, not on a token
+ being unexpired. Removal is an app-to-platform door that records the
+ revocation, calls LiveKit's remove-participant, and has the proxy drop
+ that participant's signalling connection, which ends the session
+ without waiting for anything the client does. Webhooks are for
+ observation and reconciliation, never enforcement. Management
+ operations are the door's, scoped to the app's rooms. **(codex R3-6,
+ R4-1, R4-2)**
+- **No third-party defaults.** Stock LiveKit hands participants Google's
+ and Twilio's STUN servers when none are configured and uses them for
+ its own external-IP discovery. The platform's LiveKit is configured
+ with the stage 4a relay as its only STUN and TURN, for the server's
+ discovery and the client ICE configuration alike, and the 4b gate
+ runs a call with all third-party network access blocked to prove no
+ fallback remains. This is why 4b follows 4a. **(codex R4-7)**
- **Recording and ingest are separate LiveKit services** and are
deferred until asked for, with their prerequisites named now so the
launch box is not mistaken for them: LiveKit Egress needs Redis to
@@ -870,9 +904,13 @@ failing, then playback working. **(codex 27, R3-9)**
and a public object moved to `protected/`, both stop being served by
a disconnected edge within fifteen minutes; an edge outside the
allowed regions refuses to fill; a late commit from a worker whose
- lease expired with no successor is refused by the publish door; an
- app named `media-<x>` created before enable keeps working and enable
- for `<x>` is refused with the right error. Then: a public hit served from
+ lease expired with no successor is refused by the publish door, and
+ the request that caused a stalled synchronous transform receives no
+ bytes; an app named `media-<x>` created before enable keeps working
+ and enable for `<x>` is refused with the right error; an instance
+ host `media-<x>` taken by another app before enable, and one
+ attempted after, are each refused in the right direction; an app
+ name ending in a hyphen is refused enable. Then: a public hit served from
a second region's cache, one object reached both by the app's check
and by a capability, an HLS playlist and its segments loading under
one capability, and a 2 GB MP4 seeking in a browser through a
@@ -891,14 +929,17 @@ failing, then playback working. **(codex 27, R3-9)**
a credential minted for one app refused for another; a credential
minted before a disable refused after re-enable, and the allocation
it held ended; a relay with an authority record older than fifteen
- minutes refusing a new allocation; a relay permission to a private
+ minutes refusing a new allocation and ending one already carrying
+ traffic; a relay permission to a private
address refused; the three meters moving on the account's usage line;
a 443-only network failing the way the docs say it will.
- **Stage 4b**: a token minted for one app refused for another app's
- room; a participant removed by the app unable to rejoin with its old
- token; a browser on an app served from a tunnel-backed host joining
- an SFU room; an eight-person room's egress appearing on the account's
- usage line.
+ room; a participant removed by the app dropped within a second and
+ unable to rejoin with its old token or a LiveKit-refreshed one, with
+ the webhook delayed and then lost; a browser on an app served from a
+ tunnel-backed host joining an SFU room; a call completing with all
+ third-party network access blocked from the SFU box; an eight-person
+ room's egress appearing on the account's usage line.
## 10. Findings not accepted
@@ -911,7 +952,7 @@ failing, then playback working. **(codex 27, R3-9)**
tenant reports a 443-only network, because it changes the fleet's
networking for every box to serve a case nobody has hit yet.
-Every revision-2 and revision-3 finding was accepted.
+Every revision-2, revision-3 and revision-4 finding was accepted.
## 11. Amendments to existing specs
@@ -926,6 +967,7 @@ Every revision-2 and revision-3 finding was accepted.
- `infra/modules/region-host/main.tf:6` header: residency by
construction now has a named exception, the per-region media bucket
policy, and the exception is itself region-scoped.
-- The console's name-claim gate (`internal/console/server.go:2904`):
- new app names and renames consult the media-host claim table. The
- `release.ValidApp` grammar is unchanged. **(codex R3-4)**
+- The console's name-claim gate (`internal/console/server.go:2904`) and
+ the instances door (`internal/console/apiinstances.go:571`): new app
+ names, renames and instance hosts consult the media-host claim table.
+ The `release.ValidApp` grammar is unchanged. **(codex R3-4, R4-4)**
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-MEDIA-R4.md b/docs/superpowers/specs/CODEX-FINDINGS-MEDIA-R4.md
new file mode 100644
index 000000000..d8dbcf2da
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-MEDIA-R4.md
@@ -0,0 +1,96 @@
+# Findings on the media roadmap, revision 4
+
+Reviewed by **GPT-6 (Codex), 2026-09-07**, fourth pass, against revision 4 at `0ae7da6`, with Paul's rulings treated as fixed. Unedited apart from this note. Revision 5 records which findings were accepted and how.
+
+| Revision-3 finding | Disposition | One-line reason |
+|---|---|---|
+| R3-1 — Public cache retirement | Closed | All doors now require fresh serving authority; retirement covers every relevant box through acknowledgement or lease expiry. |
+| R3-2 — Publication fence | Partial | The console can reject expired commits, but synchronous transforms may expose output before that check succeeds. |
+| R3-3 — Relay incarnation | Partial | Username, authority record and incarnation retention are defined; stale authority closes new allocations only. |
+| R3-4 — Hostname reservation | Partial | The separate claim fixes length and historical parsing, but misses instance-host acquisition and trailing-hyphen names. |
+| R3-5 — SFU signalling | Closed | A dedicated SFU hostname carries LiveKit WebSockets directly, including for tunnel-backed apps. |
+| R3-6 — SFU tenant authority and revocation | Partial | Namespacing and signing custody are specified; webhook-based rejection and the refresh mechanism do not enforce revocation. |
+| R3-7 — Positive authorization cache | Closed | The exception is removed for every tenant; changing it requires a new ruling. |
+| R3-8 — Recording prerequisites | Closed | Redis, separate recording workers and Chrome sandbox requirements are named and remain deferred. |
+| R3-9 — Sequential gate heading | Closed | Acceptance gates now follow the dependency graph. |
+
+Reviewed commit `0ae7da6fdebd198ecddfad419d2d05049dcf73f1`. Paul’s eleven rulings are treated as fixed. No files were changed. “Closed” means resolved at roadmap level, not implementation-proven.
+
+**Severity:** P1 breaks a trust guarantee or makes a stage unbuildable as written; P2 is a contract or claim defect to fix before the stage spec; P3 is advisory.
+
+## R4-1 — P1 — Stage 4b: the rejoin deny list is checked after access is restored
+
+**Problem and evidence:** The [tenant-boundary mechanism](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:675) checks removed identities on token refresh and on LiveKit’s participant-joined webhook, claiming an old token cannot restore access.
+
+LiveKit emits `participant_joined` after the participant becomes active and establishes its media connection. Webhook delivery is also not guaranteed: retries can end in abandonment. This is notification after admission, not an admission veto. [LiveKit webhook contract](https://docs.livekit.io/intro/basics/rooms-participants-tracks/webhooks-events/).
+
+A removed participant can therefore present an unexpired token, reconnect and receive room media before being removed again. If webhook delivery fails, the proposed check never runs. The stage-4b gate’s “unable to rejoin” guarantee does not follow.
+
+**Correction:** Enforce the deny list before admitting a join or reconnect, through an unavoidable SFU admission gate. The platform WebSocket endpoint is a possible enforcement point, provided direct signalling cannot bypass it and admission is serialized against removal. Keep webhooks for observation and reconciliation. Test repeated rejoin attempts with delayed and lost webhooks, asserting that no room media becomes accessible.
+
+## R4-2 — P2 — Stage 4b: platform token refresh does not govern established sessions
+
+**Problem and evidence:** [Lines 681–685](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:681) say participants refresh through the platform door and describe removal as declining refresh plus calling remove-participant.
+
+Stock LiveKit refreshes connected clients’ tokens itself. Its server starts a refresh loop and sends newly signed tokens without consulting the platform door. Token expiry does not terminate an established session. [LiveKit token lifecycle](https://docs.livekit.io/frontends/reference/tokens-grants/), [server refresh implementation](https://github.com/livekit/livekit/blob/master/pkg/service/roommanager.go#L702).
+
+A client need not request platform refresh. Refusing that request therefore cannot be the mechanism that discovers or enforces loss of authorization. This remains a defect even after fixing admission under R4-1.
+
+**Correction:** Define an explicit app-to-platform removal operation that records revocation and reliably terminates the active participant, independently of client refresh. If continued participation requires periodic app authorization, specify a server-enforced session lease and its failure behavior. Account for LiveKit-issued refreshed tokens in reconnect enforcement. Browser requests for platform tokens must go through the app’s authenticated backend; the route credential remains server-side.
+
+## R4-3 — P2 — Stages 1–2: synchronous transforms bypass the publication decision
+
+**Problem and evidence:** The [publication fence](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:410) gives the console exclusive publication authority, then expressly allows the computing edge to serve a synchronous transform while publication is still in flight.
+
+A transform can read its source, stall until its attempt lease expires or the source receives an infected verdict, and then finish. The console correctly refuses its commit, but the exception permits the initiating response to receive those same rejected bytes. Manifest-only serving protects subsequent readers; it does not protect this response.
+
+**Correction:** Require successful publication before releasing transformed bytes, or define an equally authoritative response-admission decision covering the attempt lease, source generation and incarnation. Merely retaining a fresh edge serving lease does not establish that this attempt remains valid. Add tests where the initiating request receives no transformed bytes after publication is refused, including expiry with no successor and an infected-source purge during computation.
+
+## R4-4 — P2 — Stage 1 and §11: app-name claims do not cover instance-host claims
+
+**Problem and evidence:** The [media-host claim](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:310) is checked by new app names and renames. Enable checks whether the generated label is an existing app name.
+
+There is another acquisition door. The [instances API](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/console/apiinstances.go:571) permits an app to create a hostname using another, unclaimed label in the account’s platform namespace. Its ownership check calls `HasAppIn`; an [existing test expressly permits unclaimed labels](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/console/apiinstancesenable_test.go:210).
+
+Thus app `widget` can create `media-docs.abc.oncarlos.com` without any app named `media-docs`. The roadmap’s enable-time check misses that existing host. Conversely, checking app creation alone does not prevent a later instance request from claiming a media-owned label.
+
+**Correction:** Make instance-host acquisition consult the media-host claim, and make media enable check existing instance and route ownership. Serialize these competing acquisitions against the same hostname claim. Test both acquisition orders and concurrent requests; preserve existing owners.
+
+## R4-5 — P2 — Stage 1: some valid app names still produce invalid media hostnames
+
+**Problem and evidence:** [Lines 312–318](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:312) promise that the claim accepts `media-` plus any valid app name.
+
+[`release.ValidApp`](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/release/release.go:239) permits trailing hyphens. App `docs-` therefore passes, but its generated label `media-docs-` fails the platform’s [hostname grammar](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/provision/provision.go:223).
+
+The length fix is sound: a 38-character media label can be a hostname without being an app name. However, the accompanying assertions in §1 and stage 1 that every generated label is also a valid app name remain false for source names longer than 26 characters.
+
+**Correction:** Define a collision-safe hostname mapping that covers trailing-hyphen names, or explicitly refuse media enable for them before writing any claim or grant. Preserve historical app parsing. Qualify the app-name collision explanation to the subset of generated labels that satisfy `ValidApp`.
+
+## R4-6 — P2 — Stage 4a: stale relay authority does not retire existing allocations
+
+**Problem and evidence:** The [relay authority rule](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:605) refuses **new allocations** after fifteen minutes, calling this the same lease rule as media serving. The [allocation policy](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:627) ends established allocations on credential expiry or observed disable.
+
+A relay can establish an allocation, lose authority updates, and miss a subsequent disable. At fifteen minutes it stops accepting new allocations but continues carrying the existing one, potentially until the original one-hour credential expires. That is a different stale-authority contract from closing every media door.
+
+**Correction:** Apply authority expiry to existing allocations and their refreshes, terminating them at the deadline. If the intended contract deliberately permits established traffic until credential expiry, state that longer disable bound explicitly and remove the “same lease rule” claim. Test disable during an authority outage with an allocation already carrying traffic.
+
+## R4-7 — P2 — Stage 4b: self-hosting LiveKit does not remove its external STUN defaults
+
+**Problem and evidence:** [Stage 4b](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:648) specifies the hosted server and network ports but does not configure its STUN dependencies.
+
+Current upstream LiveKit supplies default STUN servers to participants when no configured STUN source applies. Those defaults include Twilio and Google. External-IP discovery also uses those defaults when enabled without an explicit server list. [LiveKit ICE-server selection](https://github.com/livekit/livekit/blob/master/pkg/service/roommanager.go#L1061), [default server list](https://github.com/livekit/mediatransportutil/blob/master/pkg/rtcconfig/config.go#L36), [external-IP discovery](https://github.com/livekit/mediatransportutil/blob/master/pkg/rtcconfig/ip.go#L32).
+
+Consequently, hosting the SFU does not by itself satisfy the roadmap’s no-third-party path or the brief’s requirement that STUN be platform-operated. This concerns connection metadata, not a claim that STUN receives call media.
+
+**Correction:** Require explicit platform-owned STUN configuration for both server discovery and client ICE configuration, with no external fallback. Verify the chosen LiveKit version’s effective configuration and test a call with third-party network access blocked. This implements the existing doctrine without changing ruling 9.
+
+## R4-8 — P3 — §1 and stage 1: two descriptions of existing code remain inaccurate
+
+**Problem and evidence:**
+
+- [§1’s scanning description](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:55) presents the infected-object read deny as an unconditional consequence of scanning. [`ScanBlock.Enforce` is optional](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/objstore/objstore.go:82), and [policy construction adds the deny conditionally](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/objstore/aws.go:211).
+- [“A fourth route kind”](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:302) overlooks the existing `status` serving class in the [edge dispatch](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/edge/edge.go:533). It is fourth only in the app-mintable vocabulary.
+
+**Correction:** Qualify the existing scanner claim as enforcement mode, while preserving ruling 2’s requirement that the new media grant carry the infected deny. Call media a new serving class or the fourth app-mintable route kind.
+
+**Verdict: request changes.** One P1 and six P2 findings remain. The public serving lease and per-box retirement condition close the previous public-cache defect, but SFU revocation still permits access after removal. Publication, hostname ownership, relay staleness and LiveKit’s default external dependencies need the corrections above before their stage specs proceed. None requires reopening Paul’s eleven rulings.
\ No newline at end of file