Fixes BigscreenVR/spt-bs-releases#249

## What

A node-prefixed URL for a **known subnet peer** is now answered by the owning node through the local daemon: `http://localhost:5474/<peer>/f/report.md` on any subnet machine fetches the file from `<peer>` and streams it back. Nothing is cached on the requesting node.

- **Wire**: `spt-net` `webmsg.rs` — a new tagged stream family (`kind: "web"`): `Web` request, `WebReply` head, `WebChunk` body (64 KiB raw per record), `WebDone`, `WebErr { message, refused }`. Only `refused: true` (the owner's access gate) becomes a 403 at the requester; any other owner-side failure becomes the 502 naming the node with the owner's reason (403 ⇔ names `WEB`). An N-1 daemon classifies it `Unknown` and drops it; the requester then answers 502 naming the node within the pump deadlines.
- **Dispatcher**: `StreamFamily::Web` arm (census 19) → `webproxy::serve_web`.
- **Owner side** (`webproxy::serve_web`): `access_check(subject = the served entry's registering endpoint, origin = the handshake-proven stream identity, surface WEB)`; a refusal is one `WebErr` whose body names the surface (no sender — WEB is non-attributable in W1). The owner resolves the path through the same `webserve::resolve_path` its own listener uses, applies `Range` itself (one grammar), and streams `File` plans from disk in bounded chunks.
- **Requester side** (`webproxy::fetch_via_peer`, on a plain thread from the listener): pump-mode brain (`cold_start_pump`, 20 s carrier, 10 s per-frame silence budget); dial via peer-addr cache → roster address → bare id; 502 body `NODE_UNAVAILABLE: <label>: <why>` for dial failure / stream drop / silence; 403 for the owner's `WebErr`; head over a oneshot, body over a bounded channel into a streamed `BoxBody` (`ChannelBody`).
- **Router** (`webserve.rs`): resolve-then-materialize split (`Resolved::{Ready, File, Proxy}`), `known_subnet_node` → node hex, `apply_range` / `plan_file`, `served_subject`, and the peer arm's proxy-or-local decision: `/<peer>/`, `docs/`, named `f/` and `a/`, short aliases → proxy; `m/`, `bin/`, `install` → local `FACET_UNAVAILABLE`; bare `f`/`a` → local `FACET_NOT_FOUND`; bare `/<peer>` still 302s. An **unknown** label stays the docs compatibility 404 (ADR-0056 Am.1 order, ruled 2026-09-07).
- **Listener** (`docshost.rs`): `locate_path` split out of `serve_path`, `start_serving_with_broker`; `daemon.rs` passes its broker socket name.

### Design note to read

The owner serves a proxied request **under its own prefix** whatever label the requester's roster carried (`webproxy::under_own_prefix`). The requester already resolved the label to the owner's node id before dialing, so the label is the requester's name for the owner, not a claim to re-check. This is what lets the one-box mechanized rig (both daemons on one hostname) exercise the real path; in production the label and the hostname coincide.

`known_subnet_node` resolves a label to the first match in subnet-then-roster order; two members advertising one label pick the first.

## Evidence (exit files, not harness notifications)

- Unit leg (`webmsg`, `dispatch`, `webserve`, `webproxy`, `docshost`, W0 `webserve_e2e`/`docs_server_e2e`): 55 run / 55 pass, one Summary.
- Mechanized two-daemon rig `crates/spt/tests/webserve_cross_node_e2e.rs` (one `#[test]`, eight arms): 1/1 green — byte-equal fetch (391 ms), edit visible (no cache; no file named after the resource under the requester's home), HEAD, `Range: bytes=0-3` → 206 `bytes 0-3/20`, `bytes=99-` → 416, owner's 404 carried as 404, peer index `?json` through the proxy, WEB deny on the owner → 403 `ACCESS_DENIED: WEB:` then 200 after the rule is cleared, unknown label → the docs 404, `/<peer>/f/` and `/<peer>/m/x` answered locally, owner stopped → 502 `NODE_UNAVAILABLE: rig-b:` in 10.0 s.
- `traceable-reqs check` exit 0; `mdbook build` exit 0.
- Full driver batteries (`.github/ci/ws272-w1.py`) on hfenduleam and kitsubito: see the gate thread.

## Two-host cells

`crates/spt-daemon/tests/twohost_web.rs` — env-gated like `twohost.rs`, ports +20. Role B is one fn serving `open.md` (endpoint `w1-open`) and `locked.md` (endpoint `w1-locked`, with an endpoint-scoped WEB deny for A's node), exiting once it has seen three peer streams; role A is three fns (`fetch`, `range`, `deny`), each its own broker/home/listener. Wired into golden's HEAVY set and both twohost jobs as their own steps; strip the steps if the gate wants the field leg only.

## Traceability

`REQ-WEB-CROSS-NODE-PROXY` minted (doc / impl / unit / int); `REQ-WEB-ACCESS-SURFACE` int activated (the deny cell is the first evidence the row is dialled).

## Docs

`serving/cross-node.md` (grammar, who decides, the status table, the deadline, no-cache), `access-viewing.md` WEB row, `serving/overview.md` rewrites, `SUMMARY.md`, `llms.txt`, CHANGELOG `[Unreleased]`. No CLI change (`reference.md` untouched).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_015SQWGdNjq4sUbTzPQMLEQA
