# FEATURE — Cloud Browser streaming & takeover plane (WS-4)

The media/input plane for **exact human takeover** of a Cloud Browser worker.

> **Narrowed by DECISIONS D-8.** The stream exists **only while a person is
> actively driving**. Nothing streams by default. There is exactly ONE
> controller, ONE stream, minted on takeover and revoked on return; the encoder
> is asleep otherwise (a requirement, not an optimization). Continuous streaming,
> read-only live viewer paths, and multi-viewer encoder work are **out of scope**.
> Periodic *screenshots on request* come from the worker capture path (WS-2/WS-8),
> not here.

**Authority:** [PLAN.md §WebRTC streaming](../../../../../../common-docs/projects/persistent-cloud-browser/PLAN.md)
· [contracts/S4-stream-tickets.md](../../../../../../common-docs/projects/persistent-cloud-browser/contracts/S4-stream-tickets.md)
· DECISIONS D-8 / D-10. Where this doc and S4 disagree, **S4 wins** — it is the
frozen seam three repos build against.

---

## What this plane is

Selkies (WebRTC) captures the worker's private Xvfb display and translates
WebRTC DataChannel input back through XTEST — **behind** an authenticated
gateway. Selkies is never the product auth boundary (PLAN.md §Authentication
boundary). The auth flow is:

```
web app ──mint──▶ Browser Manager (mint authority, writes browser.*)
   │  single-use 60s ES256 ticket (data: endpoint, media, ice, control)
   ▼
gateway ──claim──▶ 10 checks ▶ __Secure- HttpOnly path-scoped cookie
   │  outbound, PRIVATE network only
   ▼
worker (Selkies/Xvfb) ── ONE XTEST input path, revision-fenced, kill-switched
   ▲
coTURN ── short-lived REST creds (120s), NOT a revocation surface
```

## The security contract (proven standalone)

The Definition-of-Done is S4 §10's assertion table, run against the in-memory
plane with **no Browser Manager and no `browser.*` schema** (S4 §10 stub
contract). Every row is a test in
[`tests/cloud_browser_streaming/`](../../../../tests/cloud_browser_streaming/):

- replay a ticket → second claim rejected, exactly one session;
- wrong origin / wrong user / expired ticket → rejected;
- grant revision bumped after mint → `grant_revoked`, no session (**B7 re-derives
  access at claim — the check that makes revocation real**);
- stale `control_revision` → `control_lease_lost`;
- **revoke mid-session then send a keystroke → input dead in <1s, asserted AT THE
  WORKER**, not merely at the gateway;
- revoked-ticket reconnect → refused (ticket burnt, session gone);
- `view` session attempts input → refused **at the worker** (no `input:xtest`);
- second control tab → `stream_already_connected`; with `takeover:true` the old
  session is superseded and the revision bumps;
- no raw ticket / cookie / TURN secret / worker address in any response or record.

Two independent layers enforce read-only, as the platform requires: the gateway
declines to bind input, **and** the worker refuses input from any session whose
ticket lacked `input:xtest`.

## Files

| File | Role |
|---|---|
| `config.py` | CAPS constants (TTLs, cadence, viewport, media policy) + injected `StreamingConfig` VALUES. Nothing reads env. |
| `errors.py` | The typed error literals (S4 §2.7) + frozen HTTP mapping. |
| `models.py` | Pydantic wire models for mint/claim/renew/release/revoke. |
| `tickets.py` | `TicketSigner` — a thin wrapper over `matrx_connect.ScopedTokenIssuer` (reuse, not a second JWT signer). Mints/verifies the S4 §3.1 claim set. Lazy import keeps matrx-connect out of the core import graph. |
| `ticket_store.py` | Single-use ticket-**hash** store with the §3.3 CAS. Never stores the raw ticket. |
| `access.py` | The `iam.has_access_for` seam + `FixtureAccessResolver` with a bumpable grant revision. |
| `control_lease.py` | Run registry + control-lease CAS (§5.1), revision fencing. |
| `worker_input.py` | **The single writable input path + kill switch.** The executable spec of what the real Selkies/XTEST worker must enforce. |
| `turn_credentials.py` | coTURN short-lived REST creds (§8). |
| `plane.py` | `StreamPlane` — the injected component container. |
| `gateway.py` | `StreamGateway` (claim/renew), `SessionRegistry`, `RevocationCoordinator` (the one place the input-dies-first order lives). |
| `control_plane.py` | `MintService` — mint authority + control-lease ops (the Browser Manager side, stubbed for WS-4). |
| `app.py` | Standalone FastAPI harness (stub mint + real gateway). |
| `selkies_worker.py` | `SelkiesWorkerConfig` — worker launch config; media policy OFF at the source; encoder asleep at boot. |
| `deploy/` | coTURN config, Selkies worker Dockerfile/entrypoint, compose, MPL note. |

## Reuse — what this did NOT reinvent

- **Ticket signing** → `matrx_connect.ScopedTokenIssuer` (ES256), its OWN issuer
  name, audience, and key (`BROWSER_STREAM_TOKEN_SIGNING_KEY`). A hand-rolled
  ticket format or second JWT signer is a defect (S4 §0). The endpoint is NOT
  `POST /broker/tokens` — the primitive is reused, the route is not (S4 §0).
- **The one authorization answer** → the access resolver seam, re-derived at
  mint, claim, and every renewal — never trusted from the ticket.
- **coTURN REST** → the standard HMAC short-lived-credential mechanism.

## Wiring it into the real Browser Manager (WS-5)

1. Provide a real `AccessResolver` over `iam.has_access_for('browser_profile', …)`
   + the grant-revision digest (OPEN(grant-revision-source)).
2. Back `TicketStore` with `browser.stream_ticket` (OPEN(S1-stream-ticket-table))
   and `RunRegistry` with the `browser.run` row + the §5.1 UPDATE.
3. Replace `app._user_id` with `verify_supabase_token`. The gateway must never
   accept a stream ticket as a user login (`passthrough_issuers`).
4. Mint + control-lease ops mount in the Browser Manager routers on the app host;
   the gateway runs on its own `stream.aimatrx.com` subdomain.
5. Point `worker_input` at the real Selkies XTEST channel; `SelkiesWorkerConfig`
   generates the launch config. The encoder starts on a claimed takeover only.

## Known limits / open items (from S4 §11)

- `OPEN(S1-stream-ticket-table)` — the hash table must be added to the S1 schema.
- `OPEN(grant-revision-source)` — the digest needs the access owner's sign-off.
- `OPEN(gateway-host)` — recommended own subdomain.
- `OPEN(multi-view-floor)` — low-latency multi-view is refused (`multi_view_not_enabled`)
  until P5; the v1 view path is the redacted observation channel (not this encoder).
- `OPEN(turn-rest-hash)` — SHA1 vs stronger; pinned from the deployed coTURN build
  in the runbook.
- **Real Selkies/coTURN behaviour** (encoder wake/sleep latency, XTEST kill, SDP
  topology non-leak, TURN digest) is proven by the operator runbook, not here:
  [`RUNBOOK-real-server-tests.md`](./RUNBOOK-real-server-tests.md).

## Change log

- **2026-08-18 — created (WS-4):** streaming/takeover plane built against S4 v1 and
  PLAN.md §WebRTC streaming, narrowed by D-8 to single-controller-only. 25 standalone
  tests green (security assertion table + components + FastAPI wire flow). Deployment
  configs + Selkies MPL note + real-server runbook shipped.
