# 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

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);
- second control tab → `stream_already_connected`; with `takeover:true` the old
  ticket is durably revoked before the new claim commits;
- no raw ticket / cookie / TURN secret / worker address in any response or record.

The production claim path serializes on a Postgres transaction advisory lock keyed by
`run_id`; an in-process lock is forbidden because two ECS replicas would split-brain.
Takeover publishes `browser_stream_revoked` through Postgres `NOTIFY` in the same
transaction. Every live WebSocket holds an event-driven listener and closes immediately
when its run is revoked; the durable ticket row remains the reconnect authority.

## 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). |
| `proof_server.py` | Explicitly enabled private-host harness for the real-network ticket checks; never uses production users or profiles. |
| `selkies_worker.py` | `SelkiesWorkerConfig` — worker launch config; media policy OFF at the source; encoder asleep at boot. |
| `supervisor.py` | Starts Selkies only for live human control, listens on the private container network, and stops it after input is disabled. |
| `deploy/` | coTURN config, Selkies worker Dockerfile/entrypoint, compose, and MPL note. The image build fails loudly if Selkies is absent. |

## 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.

## Production wiring

The Browser Manager now mints from live run/handoff state, writes only ticket hashes
to `browser.stream_ticket`, exchanges the one-use ticket for a path-scoped HttpOnly
cookie on `stream.aimatrx.com`, re-runs canonical access checks, and proxies HTTP and
WebSocket traffic to Selkies on the private application network. The encoder starts
only after the worker accepts the human-control transition and stops before control
returns to the agent. The standalone plane remains the fast security test harness.

There is **no production low-latency observer ticket**. Observation is the redacted
screenshot channel. P5 tests only the one real control stream; a `view`-ticket test does
not count as product acceptance.

The production worker target is a private ECS/Fargate service in `us-east-1`. AI
Dream reaches its signed control port and Selkies port only through the worker's
Cloud Map name and security group. Active Chromium profiles mount encrypted EFS;
verified closed-profile checkpoints use KMS envelope encryption and a private S3
bucket. The resources and agent browser path are live. AI Dream consumes the opaque
public `cb-*` prefix as the authorization boundary, then strips it before forwarding
static files and `signalling/` to Selkies' private root.

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

- The ticket row currently records the canonical profile version as its audit
  revision while access itself is re-derived at mint, claim, renewal, and proxy
  admission. Replace that audit value if the platform publishes a dedicated share
  revision without adding a second authorization system.
- `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).
- Production acceptance still must prove the browser client receives media through
  the deployed relay and that return-control closes the live input path.

## Change log

- **2026-08-20 — readiness-gated takeover:** the Selkies supervisor waits for
  its private TCP listener before advertising human control. Startup timeout or
  early process exit fails closed and terminates the process group.

- **2026-08-20 — live prefix boundary:** the public opaque session slug remains the
  cookie and authorization scope but is no longer forwarded to Selkies, whose private
  static app and signalling socket are rooted at `/`. Replacement bootstrap restores
  human input state and restarts the encoder when the durable controller is human.

- **2026-08-20 — durable single controller:** production claims now serialize across
  replicas, refuse a second ordinary control stream, atomically revoke the old ticket on
  explicit takeover, and push an event-driven cross-replica socket kill through the
  canonical Postgres LISTEN/NOTIFY seam. Observer-ticket checks were removed from P5;
  screenshots are the only v1 observation path.

- **2026-08-20 — private AWS target:** documented the declared ECS worker, Cloud Map
  control path, encrypted EFS profiles, and KMS/S3 checkpoints. Deployment and live
  media acceptance remain open.

- **2026-08-19 — production relay:** mounted the live ticket ledger and
  cookie-authenticated HTTP/WebSocket proxy on `stream.aimatrx.com`; the private
  worker starts the encoder only for human control and the deployed relay provides
  short-lived connection credentials.

- **2026-08-19 — hard-kill recovery:** the worker entrypoint clears only the stale
  marker files for its fresh private X display before launch, so a forced container
  death cannot trap the replacement in a restart loop. Added the explicitly gated
  private-host gateway harness used by the live runbook.
- **2026-08-18 — operational worker image:** replaced the nonexistent package install
  with the checksum-pinned upstream portable release, added the callable worker
  process, and tied encoder start/stop to human control.
- **2026-08-18 — G1 live probe:** stopped the worker image from swallowing a missing
  Selkies install and recorded the verified runtime gap. The pre-fix image built with
  neither `pip` nor `selkies`; its entrypoint started Xvfb only. G1 remains closed.
- **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.
