From 896cca2f2374fe4fa9fc0fe3eabb214086601383 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Sun, 1 Nov 2026 16:54:28 -0500 Subject: [PATCH] docs: plan, agent operating rules, and an honest security model PLAN.md locks the architectural decisions (WebRTC mesh with the sharer as sole offerer, JSON-only signaling, in-memory roster with no database, no auth, no CDN, Linux as the reference platform) and records the roster rules, the verification ladder, and the constraints that are accepted rather than hidden: the secure-context requirement, mesh upload scaling with watcher count, and the Wayland portal's per-share prompt. Phases are deliberately not written yet -- the prototype and its tests came first, so .agents/phases/todo/ holds only a README pointing at the candidate phase list in PLAN.md section 7. SECURITY.md answers the question a no-auth tool has to answer: not "who is allowed in" but "where is this allowed to run". It separates what an unauthenticated LAN visitor can do from the guarantees that are enforced and named to a test, and tells users the two things they will actually hit -- that http:// hides getDisplayMedia, and that on Wayland the compositor asks every time and mirror must not work around it. LLM-Generated: true --- .agents/PLAN.md | 234 +++++++++++++++++++++++++++++++ .agents/phases/complete/.gitkeep | 0 .agents/phases/todo/README.md | 14 ++ AGENTS.md | 143 +++++++++++++++++++ README.md | 103 ++++++++++++++ SECURITY.md | 102 ++++++++++++++ 6 files changed, 596 insertions(+) create mode 100644 .agents/PLAN.md create mode 100644 .agents/phases/complete/.gitkeep create mode 100644 .agents/phases/todo/README.md create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 SECURITY.md diff --git a/.agents/PLAN.md b/.agents/PLAN.md new file mode 100644 index 0000000..a62792c --- /dev/null +++ b/.agents/PLAN.md @@ -0,0 +1,234 @@ +# mirror — Master Plan + +> **This file is the source of truth.** Anything marked `LOCKED` may not be +> changed without an explicit decision recorded here first. Phases implement +> this plan; they do not revise it. + +## 1. Vision + +Mirror a screen from any device that has a browser, and watch every screen that +anyone is mirroring, from any other device — with nothing to install and nothing +to sign in to. + +Day one, one command: + +```bash +uv run mirror serve +``` + +* `/mirror` — pick a window, screen, or tab using the **operating system's own** + picker, and mirror it. +* `/watch` — see every live mirror as a wall of tiles; click one to take it over + the whole screen. +* N sharers and M watchers at the same time, in any combination. + +The single thing that must never regress: **a person on a Linux laptop, a person +on a phone, and a person on a work machine can all join by opening a URL.** + +### Design principles + +1. **The browser is the whole client.** No native helper, no extension, no + WebAssembly to fetch, no build step for the frontend. +2. **Media never touches the server.** The server is a roster and a signaling + relay. It handles JSON, always small, never frame data. +3. **Linux is the reference platform.** X11/Xorg is the guaranteed path; the + Wayland portal path is supported where the compositor provides it. Every + capture-related change is verified on Linux before anything else. +4. **Zero-configuration on a LAN.** STUN/TURN are opt-in, not defaults: peers on + one network reach each other with host candidates, and shipping STUN by + default would leak internal addresses to a third party. +5. **The prototype is the seed, not a spike.** Code in `src/mirror/` is + production-shaped: typed, linted, covered, and gated by the same test suite + future phases must keep green. + +## 2. Architectural anchors + +| # | Component | Decision | Rationale | Status | +|---|-----------|----------|-----------|--------| +| A1 | Language / runtime | Python **3.13+** (developed on 3.14), `uv` for everything | Matches the estate; `uv` is the only installer | **LOCKED** | +| A2 | HTTP + signaling | **FastAPI + uvicorn[standard]**, single process | Native WebSocket support, typed boundary, one dependency tree | **LOCKED** | +| A3 | Media transport | **WebRTC, mesh, one `RTCPeerConnection` per viewer** | Hardware codecs, sub-100ms LAN latency, no server media handling; server stays a roster | **LOCKED** | +| A4 | Who negotiates | The **sharer is always the offerer**; the viewer only answers | Removes renegotiation glare; the sharer owns the track set | **LOCKED** | +| A5 | Wire format | **JSON over one WebSocket** at `/ws`, camelCase keys, discriminated on `type` | One connection per peer, trivially debuggable with `websocat` | **LOCKED** | +| A6 | Validation | **Pydantic models with `alias_generator=to_camel`** and `extra="ignore"` | camelCase on the wire, snake_case in Python. Explicit aliases are mandatory: silent field-dropping is a bug we already shipped once | **LOCKED** | +| A7 | State | **In-memory roster only** (`mirror.registry.Registry`). No database, no ORM, no Alembic | A mirror that outlives the process is a lie: the peers are gone. Persistence would add a failure mode and remove none | **LOCKED** | +| A8 | Domain/transport split | `Registry` is pure and returns `list[Envelope]`; `Hub` owns sockets | The whole admission/teardown policy is unit-testable without a socket | **LOCKED** | +| A9 | Frontend | **Hand-written ES modules**, no framework, no bundler | Two pages and one WebSocket do not earn a build pipeline; `node --check` is the whole toolchain | **LOCKED** | +| A10 | Assets | **No external CDNs, fonts, or analytics.** Everything served from `/static` | Works offline on a LAN; nothing phones home; nothing to time out | **LOCKED** | +| A11 | Authentication | **None**, deliberately | Trusted-LAN tool, per the product brief. Threat model lives in `SECURITY.md` | **LOCKED** | +| A12 | Configuration | Environment variables (`MIRROR_*`) + optional `.env` | Twelve-factor, container-friendly, no config file format to invent | **LOCKED** | +| A13 | E2E testing | **Playwright + full Chromium** (`channel="chromium"`), headless, with a real frame-content assertion | See §5. The assertion is on decoded pixels, never on log lines | **LOCKED** | +| A14 | Linux-native capture test | Separate `realdisplay` marked suite: private **Xvfb** + headed Chromium pinned to `--ozone-platform=x11` | Proves the real capture backend without touching the developer's session | **LOCKED** | +| A15 | CLI | `argparse`, single `serve` subcommand, flags also accepted bare | One command; no dependency for a two-option CLI | **LOCKED** | +| A16 | Quality gates | `ruff check` + `ruff format --check` + `pyright` (0 errors) + `pytest` green, always | Non-negotiable before a phase is called complete | **LOCKED** | +| A17 | Scale-out beyond a mesh | *Proposed*: WebSocket/MSE relay or WebCodecs SFU when a sharer's uplink, not the CPU, is the limit | Revisit only with a measured problem. Status: **PROPOSED** | Proposed | +| A18 | TLS | *Proposed*: `mirror serve --https` with an auto-generated self-signed cert | Solves the secure-context problem (§6) for LAN IPs without a CA. Status: **PROPOSED** | Proposed | + +## 3. Architecture + +``` + sharer browser watcher browser(s) + ┌───────────────┐ ┌───────────────────────┐ + │ /mirror │ │ /watch │ + │ getDisplayMedia│ │ tile per mirror │ + │ ↓ track │ │ ↑ ontrack →