7257fc3c0cd797b87e3f16f828f1e8e0d743ee61
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7257fc3c0c |
ci: run the suite where the runner can actually start it
Run #1 died in Checkout, before a single command of ours ran: actions/checkout is a JavaScript action, and the job's `container: python:3.13-slim` has no Node runtime for it. The same actions succeed on this instance with no `container:` key at all (Experimental/brain-of-reese run 264 is the proof), so the job stays on the runner and shells into the interpreter it needs instead. The suite now runs inside a pinned python:3.13-slim via whichever of podman or docker the runner has — the same runtime the publish job builds with, which is why "no runtime found" is a hard failure rather than a quiet fallback to whatever python the runner image happens to ship. Falling back there would make the gate depend on an interpreter nobody chose. 3.13 rather than the 3.14 of the dev venv: it is the floor declared in pyproject's requires-python, so CI checks the floor. The image-build smoke step lost its apt-get install podman for the same reason. LLM-Generated: true |
||
|
|
2e79430ac7 |
docs: what actually gates access, now that there is a deployment
The internal deployment exists (deployments repo, `mirror/`: quadlet, nginx edge, certbot, route53, VM SOP), so the docs that said "the real fix is HTTPS" were describing a future that has arrived from a different direction — TLS terminates in nginx on the service VM, not in this process, and there is no `--https` flag here. SECURITY.md's trust model said "trusted: anyone who can open a TCP connection". With an allow list in production that sentence was simply no longer true, so the table now names the address space, and a new section says what the list is and what it is not: one wall (network/nginx) and one belt (the app, for the path that bypasses nginx at 127.0.0.1:8080), plus the thing neither can do — once two browsers are in the room their media never asks the server again, and it can never tell you *who*, only *where from*. Saying "defence in depth" about that would have been the dishonest version. PLAN.md gains A19 (TLS + admission in production, LOCKED) and A18 becomes deprioritised rather than deleted — standalone `mirror serve --https` is still worth having for offline/ad-hoc use, it just stopped being the thing blocking cross-device sharing, and candidate phase 1 is now share-page resilience. AGENTS.md gains the two rules most likely to be broken by a well-meaning edit: trust XFF only from a trusted proxy, and change the nginx and app lists in the same commit. LLM-Generated: true |
||
|
|
7988da2b93 |
feat: decide who may connect at all, by source address
mirror has no accounts, so on a real network the address a connection arrives
from is not a nicety — it is the entire access control. Before this change the
answer was "everyone who can open a TCP connection", which is fine for a laptop
demo and alarming for a box someone leaves running.
`mirror serve --allow-cidr 10.2.0.0/22` (or MIRROR_ALLOW_CIDRS) now restricts
pages, the JSON API and the signaling socket to the networks named, and it is
strict where it needs to be: a malformed CIDR exits 2 with one line instead of
starting a server whose list silently failed to parse, because the two
outcomes of that typo are "nobody can connect" and "everybody can connect".
Empty remains the only permissive state, on purpose.
The rules that matter live in src/mirror/access.py and are the ones that get
either outcome:
* X-Forwarded-For is believed only from MIRROR_TRUSTED_PROXIES (loopback by
default), and only its left-most value. An untrusted peer that sends the
header gets judged on its own address — there is a test for exactly that,
because it is the shape of a bypass.
* The socket is closed before it accepts, so a refused address never creates
a roster entry to clean up behind it.
* An address that cannot be parsed fails closed, and a v6 address is never
"inside" a v4 network (comparing the two raises TypeError, which would be
a 500 on a dual-stack host).
* /healthz is exempt: the container health probe comes from loopback, which
a production list otherwise excludes.
The refused client is told why, and which address it was judged as — "403" in
front of a proxy is undiagnosable otherwise. Browsers get a page, fetches get
JSON, and the page is self-contained because a refused address cannot load
/static/app.css any more than it can load a page. net.js keys its own notice on
the /api/config 403 and stops: without that the pill reads "offline,
reconnecting in 32s" forever for a decision that will never change.
The static files drop their only inline script and style attribute (the landing
page's counter is /static/home.js, its fine print is .fineprint) so the
deployment can send `script-src 'self'; style-src 'self'` with no
'unsafe-inline'. That policy was verified through a real nginx edge carrying the
exact header set — capture through SRTP to decoded, painted frames at
1280x720, eight distinct, zero securitypolicyviolation events — which also
caught the edge sending `Cache-Control: no-store, no-store` and needed
proxy_hide_header, since nginx appends rather than replaces.
24 tests added; 176 pass, 2 skipped, pyright and ruff clean.
LLM-Generated: true
|
||
|
|
24cb10ee5c |
ci: publish the image, but only past the test suite
One image (gitea.reeseapps.com/experimental/mirror), tagged :main on main and a version tag on release, exactly like brain-of-reese — plus a `test` job the publish job `needs`. A red pipeline therefore leaves :main on the last good build, which is what podman auto-update keeps serving: red means nothing shipped, not that production broke. The test job runs unit + API only and --ignores tests/e2e rather than deselecting it: -m filtering happens after collection, and tests/e2e/conftest.py imports playwright, so a marker alone would not keep the browser suite out of a container that has no browser. The e2e and realdisplay rungs stay local gates (AGENTS.md), and pyright is not in CI either, since the python container has no node to run it with — that asymmetry is written down rather than pretended away. The image sheds curl and its HEALTHCHECK: health belongs to the deployment (HealthCmd in the quadlet, which probes with the python already in the image), so the runtime image no longer ships a package that exists only to answer /healthz — and no longer contains a way to make outbound requests, which is consistent with a server that dials nothing. 171 MB to 157 MB. The image source label points at the real repo now instead of example.invalid. Verified locally with podman 5.8: the image builds, `mirror --help` exits 0, and it serves on 8080 when MIRROR_PORT says 8080. LLM-Generated: true |
||
|
|
896cca2f23 |
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 |
||
|
|
7359ee4e07 |
test: four-rung suite that asserts on decoded pixels, not log lines
130 tests, ~20s, 94% coverage. Four rungs, each proving something the one below it cannot: registry admission, fan-out, signal authorization and teardown, no I/O wire camelCase round-trip, malformed frames, WebSocket lifecycle e2e real capture -> encode -> SRTP -> decode -> painted frames realdisplay the actual X11 capture backend on a private Xvfb display The e2e rung drives Chromium's fake media device, which produces a genuine moving 1280x720 monitor stream, so the assertion is that a watcher's <video> has a size and that its pixels change over time -- a frozen or never-started peer connection fails the suite, which a "signal arrived" assertion would not have caught. Findings encoded as flags and comments, because each cost a debugging cycle: the headless shell has no media stack at all (use channel="chromium"); mediaDevices is undefined off a secure context (serve over 127.0.0.1); and --use-fake-ui-for-media-stream breaks real X11 capture by approving the request without ever selecting a source, so the realdisplay suite selects the source explicitly instead. The realdisplay fixture strips WAYLAND_DISPLAY and DBUS_SESSION_BUS_ADDRESS from the browser environment. Without that, a headed Chromium reaches xdg-desktop-portal, mirrors the developer's actual desktop and raises a permission dialog on their real screen -- which is exactly what happened before this was isolated. Skipped unless MIRROR_REALDISPLAY=1. Also guards the frontend's structure: scripts must parse as ES modules, every id a script looks up must exist in its page, ids must not collide across pages that share one stylesheet, and nothing may load from a CDN. LLM-Generated: true |
||
|
|
555fc1c3b4 |
feat: browser-only screen mirroring prototype
A FastAPI server with two pages and one WebSocket. /mirror captures a window, screen or tab through the OS-native getDisplayMedia picker; /watch shows every live mirror as a wall and takes one fullscreen on click. Any number of sharers and watchers, at the same time. Media never touches the server: each watcher gets its own RTCPeerConnection to the sharer, and the Python process only relays JSON. The roster lives in registry.py, which is transport-free by design -- every operation returns the envelopes to deliver, so admission, fan-out and teardown policy are all unit-testable without a socket. Registry rules worth stating: a signal frame is relayed only along an existing subscription in either direction; viewer-to-peer traffic is refused; and teardown is broadcast to every viewer rather than only the subscribers, because a tile is drawn on stream_added and a viewer that has not yet subscribed still has a stale tile to clear. Frontend is hand-written ES modules with no build step and no external asset. Chromium scales a monitor capture to the capturing window and requires a secure context, so /mirror detects an insecure origin and prints the three ways out rather than failing on a button click. LLM-Generated: true |