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://<lan-ip> hides getDisplayMedia, and that on Wayland the compositor asks every time and mirror must not work around it. LLM-Generated: true
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# mirror
|
||||
|
||||
**Device-agnostic screen mirroring that lives entirely in the browser.**
|
||||
|
||||
`mirror` runs a small HTTP server on your machine. Open it from any device on
|
||||
your network:
|
||||
|
||||
| Page | URL | What it does |
|
||||
| ------------ | ------------------ | ----------------------------------------------------------------------- |
|
||||
| **Share** | `/mirror` | Picks a window, screen, or tab via the OS-native picker and mirrors it. |
|
||||
| **Watch** | `/watch` | Shows every screen currently being mirrored; click a tile to take the stage. |
|
||||
| **Index** | `/` | Signpost + live count of mirrors. |
|
||||
|
||||
Any number of people can mirror at the same time and any number of people can
|
||||
watch. There is **no authentication** — this is built for a trusted local
|
||||
network, and that assumption is deliberate and documented in
|
||||
[SECURITY.md](SECURITY.md).
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
uv sync --extra dev
|
||||
uv run mirror serve # http://0.0.0.0:8055
|
||||
```
|
||||
|
||||
Then, from another machine on the LAN:
|
||||
|
||||
```
|
||||
http://<this-machine>:8055/mirror # to share
|
||||
http://<this-machine>:8055/watch # to watch
|
||||
```
|
||||
|
||||
Linux is the primary target. On Linux the native picker is X11/Wayland's own
|
||||
(Chromium uses `xdg-desktop-portal` on Wayland/GNOME/KDE, and its built-in X11
|
||||
picker on Xorg), so nothing has to be installed on the client side.
|
||||
|
||||
## How it works
|
||||
|
||||
Media never touches the server. Each mirror is a WebRTC peer connection
|
||||
established directly between the sharer's browser and each watcher's browser
|
||||
(a "mesh" fan-out). The server is a **signaling relay and roster only**: it
|
||||
tracks who is publishing what, forwards SDP/ICE between peers, and pushes
|
||||
roster deltas to viewers.
|
||||
|
||||
```
|
||||
sharer browser ──ws──► ┌──────────────────────┐ ◄──ws── watcher browser
|
||||
│ │ FastAPI │ ▲
|
||||
│ offer/ICE ───►│ /ws (signalling) │◄─── offer/ICE │
|
||||
│ │ registry (in-mem) │ │
|
||||
└────── WebRTC (SRTP, LAN, p2p) ─────────┴─────────────────┘
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables (all optional; a `.env` file is loaded if present):
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
| ---------------------- | ------- | ---------------------------------------------------- |
|
||||
| `MIRROR_HOST` | `0.0.0.0` | Bind address for `mirror serve`. |
|
||||
| `MIRROR_PORT` | `8055` | Bind port. |
|
||||
| `MIRROR_ICE_SERVERS` | `[]` | JSON array of RTCIceServer objects, if peers cannot reach each other directly. |
|
||||
| `MIRROR_MAX_MIRRORS` | `0` | Cap on concurrent mirrors (`0` = unlimited). |
|
||||
| `DEBUGPY` | `0` | `1` opens a debugpy listener on `DEBUGPY_PORT`. |
|
||||
| `DEBUGPY_PORT` | `5678` | debugpy port. |
|
||||
|
||||
## Platform notes
|
||||
|
||||
**Screen capture needs a secure context.** `http://localhost` is one; a plain
|
||||
`http://192.168.x.x` opened from another device is not, and the browser hides
|
||||
`getDisplayMedia()` completely. The `/mirror` page detects this and prints the
|
||||
three ways forward (share from localhost, serve HTTPS, or mark that one origin as
|
||||
secure on the sharing device). Watching never needs permission.
|
||||
|
||||
**Linux.** On X11/Xorg Chromium uses its native capturer — the picker appears and
|
||||
that is the end of it. On Wayland (GNOME/KDE) Chromium goes through
|
||||
`xdg-desktop-portal`, so the compositor shows its **own permission dialog for
|
||||
each share**, which mirror neither controls nor tries to bypass. Chrome's picker
|
||||
offers whole screens and individual windows; on Wayland what the portal will
|
||||
agree to depends on the compositor.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
uv run pytest # unit + API + browser E2E (~20s)
|
||||
uv run pytest -m "not e2e" # everything except the browser (~0.5s)
|
||||
uv run ruff check . && uv run ruff format --check . && uv run pyright
|
||||
```
|
||||
|
||||
The browser suite drives **real capture**: Chromium's fake media device yields a
|
||||
moving 1280×720 monitor stream, so the assertions are on decoded, changing pixels
|
||||
at the watcher — not on log lines.
|
||||
|
||||
There is a second, opt-in rung that exercises Chromium's **real X11 capture
|
||||
backend** on a private Xvfb display, isolated from your own session so it can
|
||||
never prompt you or mirror your desktop:
|
||||
|
||||
```bash
|
||||
MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay
|
||||
```
|
||||
|
||||
See [AGENTS.md](AGENTS.md) for the operating rules,
|
||||
[.agents/PLAN.md](.agents/PLAN.md) for the locked architectural decisions, and
|
||||
[SECURITY.md](SECURITY.md) for why there is no login.
|
||||
Reference in New Issue
Block a user