# mirror — Agent Operating Rules **Read `.agents/PLAN.md` before touching anything here.** It holds the locked architectural decisions; this file holds the rules for working inside them. `mirror` is browser-only screen mirroring for a local network: `/mirror` shares a window/screen/tab via the OS-native picker, `/watch` shows every live mirror and lets anyone take one fullscreen. Media flows peer-to-peer over WebRTC; the server is a roster and a signaling relay. Linux is the reference platform. --- ## 1. The order of reading 1. `.agents/PLAN.md` — architecture, locked anchors, verification ladder. 2. `README.md` — the user-facing contract. 3. `SECURITY.md` — why there is no authentication, and what that costs. 4. `src/mirror/registry.py` — the whole policy lives here. Read it before changing behavior; it is short and it is the product. ## 2. Phased execution ``` .agents/ ├── PLAN.md ← locked design (do not edit without an explicit decision) └── phases/ ├── todo/ ← pending phases: NN_name/00_phase.md + NN_task.md └── complete/ ← finished phases, read-only history ``` * New work enters through the **`phase-authoring` skill**, never as a direct implementation of a vague request. * Phases run through the **`phased-execution` skill**. * Never edit anything in `phases/complete/`. Ask before editing `phases/todo/`. * Never edit `PLAN.md` unless the user explicitly authorises a change to a locked decision, and record the reason in the same change. * A phase is not done until every gate in §4 passes. ## 3. Locked decisions that are easy to break by accident | Rule | Why it exists | |---|---| | **No external assets.** No CDN, remote font, remote icon, `@import`, or analytics | LAN/offline correctness. `tests/test_app.py::TestClientScripts::test_nothing_is_loaded_from_a_cdn` fails the build if you add one | | **No framework, no bundler** in `src/mirror/static/` | Two pages and one WebSocket. `node --check` is the entire frontend toolchain | | **Media never passes through the server** | The server relays JSON only. Anything that puts frames in the Python process violates A3 | | **`Registry` stays pure** — no sockets, no FastAPI, no `await` | It returns `list[Envelope]`. Transport lives in `Hub`. This is why the policy has 100%-ish unit coverage | | **camelCase on the wire, snake_case in Python**, via `alias_generator=to_camel` | A silently-dropped `displaySurface` was a real bug we shipped. Any new multi-word field needs an alias test | | **No authentication** (A11) — but the **address is the access control** (A19) | Product brief: no accounts. What decides who gets in is `MIRROR_ALLOW_CIDRS` (`src/mirror/access.py`), enforced in the HTTP middleware *and* before the socket accepts. If you think it needs auth, that is a `PLAN.md` discussion first — and see `SECURITY.md` | | **Trust `X-Forwarded-For` only from `MIRROR_TRUSTED_PROXIES`, left-most value only** | Otherwise a browser writes its own pass. The deployment's nginx must *overwrite* the header (`$remote_addr`, never `$proxy_add_x_forwarded_for`) — and the nginx `allow`/`deny` list and `MIRROR_ALLOW_CIDRS` change in the same commit | | **`/healthz` stays exempt from the address gate** | The podman `HealthCmd` probes it from loopback, which the production list otherwise excludes — an exempt probe is why the container is not permanently unhealthy | | **No STUN/TURN by default** | A LAN tool must not announce internal addresses to a third party | | **Teardown is broadcast to every viewer**, not just subscribers | Tiles are drawn on `stream_added`; a non-subscribed viewer still has a stale tile to clear | | **Keep both `