diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index af0d55b..aed8e20 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -286,6 +286,7 @@ writer + concurrent readers).
| `ledger.rs` | (external only: `rusqlite`, `serde_json`, `sha2`) — callers pass `db_path: &str` + `retain_days`, so ledger stays decoupled from `config` | `strategies`, `proxy::*` |
| `config.rs` | `error` | Everything else |
| `error.rs` | – | – |
+| `admin/*` *(POC: cockpit control API)* | `config`, `ledger`, `fsperm`, `proxy::*` read-only handles (external: `hyper`, `http-body-util`, `serde_json`, `getrandom`, `hex`) | `proxy::gateway` internals, `strategies::*` mutation — reads config/ledger only; **must not** share a surface with the OAuth-token-bearing gateway listener |
| `summarizer/mod.rs` | `config`, `proxy::upstream`, `strategies`, `reprune`, `ledger`, `summarizer::{api,harm_check,slice}` | `proxy::gateway` |
| `summarizer/api.rs` | `config`, `proxy::upstream`, `summarizer` (sibling constants) | `strategies`, `pairing`, `ledger` |
| `summarizer/harm_check.rs` | `summarizer` (sibling constants) | Everything else |
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000..476ed73
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,62 @@
+# trimwire Flightdeck — desktop app shell (Tauri 2, POC scaffold)
+
+A minimal **Tauri 2** shell that wraps the **same web cockpit** the trimwire binary
+serves on its loopback control API. This is the "multi-platform app" layer of the
+cockpit POC (see [`../docs/cockpit/05-multiplatform-app.md`](../docs/cockpit/05-multiplatform-app.md)).
+
+> **Scaffold only — not wired into the Rust workspace or CI.** The trimwire repo
+> root is a single Rust package (no `[workspace]`), so this nested `src-tauri/`
+> crate is ignored by `cargo build`/`fmt`/`clippy` at the root. Building the app
+> needs the Tauri toolchain (`npm`, `tauri-cli`) and is intentionally out of scope
+> for the daemon's CI. It exists to show the shape.
+
+## What it demonstrates
+
+- **Shape A** from the plan (`05-multiplatform-app.md` §1): the app is a *thin
+ webview shell* pointed at the already-running daemon's cockpit
+ (`http://127.0.0.1:8766`). No second UI — it loads the exact frontend the
+ browser does.
+- The Rust core is tiny (Tauri's value): `src-tauri/src/main.rs` is the standard
+ builder. The daemon is a *separate process* the app talks to over the loopback
+ control API — so there's no sidecar to notarize (sidestepping the worst signing
+ pitfalls, per `05-multiplatform-app.md` §3/§6).
+- One frontend, many shells: browser PWA + this desktop app + a future mobile/
+ remote client are the same bundle with different transports.
+
+## Run (requires the Tauri toolchain)
+
+```bash
+# 1. Start the daemon's cockpit (from the repo root):
+trimwire cockpit # serves the control API + web UI on 127.0.0.1:8766
+
+# 2. In another terminal, run the desktop shell:
+cd app
+npm install
+npm run tauri dev # opens a native window onto the cockpit
+```
+
+## Layout
+
+```
+app/
+ package.json # tauri-cli dev/build scripts
+ dist/index.html # offline fallback shown if the daemon isn't up yet
+ src-tauri/
+ Cargo.toml # independent crate (NOT a root workspace member)
+ build.rs # tauri-build
+ tauri.conf.json # window points at the loopback cockpit (shape A)
+ src/main.rs # standard Tauri 2 builder
+```
+
+## Production notes (from the plan, not done here)
+
+- Swap the hardcoded `127.0.0.1:8766` window URL for a discovered/configured base
+ URL once the control API's `[admin] listen` is read by the app.
+- Offer **shape B** (bundle + manage the daemon as a sidecar) as an opt-in mode.
+- Add desktop signing via `tauri-action`; defer mobile/iOS signing to the mobile
+ phase. Remote control (phone -> laptop) is the deferred, ToS-gated v3 phase
+ (`06-remote-control.md`) — the app just points the same window at a remote URL.
+- **Lock Tauri's CSP + capabilities.** `src-tauri/tauri.conf.json` now ships a **restrictive
+ CSP** (not `null`), but capabilities still need scoping to deny-by-default before this ships —
+ the Aug-2024 Tauri audit found any-origin IPC + an unauthenticated dev-server disk exposure.
+ See `../docs/cockpit/10-security-fresh-sources.md` G5.
diff --git a/app/dist/index.html b/app/dist/index.html
new file mode 100644
index 0000000..a9f6cdc
--- /dev/null
+++ b/app/dist/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+trimwire · Flightdeck
+
+
+
+
+
trimwire · Flightdeck
+
Connecting to the local trimwire cockpit…
+
+ If this persists, start the daemon's cockpit with
+ trimwire cockpit, then reopen this window.
+ The app loads the same control UI the binary serves on
+ 127.0.0.1:8766.
+
+
+
+
+
diff --git a/app/package.json b/app/package.json
new file mode 100644
index 0000000..046c4a2
--- /dev/null
+++ b/app/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "trimwire-flightdeck",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Tauri 2 desktop shell for the trimwire cockpit (POC scaffold)",
+ "scripts": {
+ "tauri": "tauri",
+ "dev": "tauri dev",
+ "build": "tauri build"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2"
+ }
+}
diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml
new file mode 100644
index 0000000..39d54e5
--- /dev/null
+++ b/app/src-tauri/Cargo.toml
@@ -0,0 +1,21 @@
+# Independent crate — NOT a member of the trimwire root package (the root has no
+# [workspace], so `cargo build` at the repo root ignores this). Build it with the
+# Tauri toolchain (`npm run tauri build`), never as part of the daemon's CI.
+[package]
+name = "trimwire-flightdeck"
+version = "0.1.0"
+edition = "2021"
+description = "Tauri 2 desktop shell for the trimwire cockpit (POC scaffold)"
+publish = false
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+tauri = { version = "2", features = [] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+[features]
+# Keep the default Tauri custom-protocol feature for production bundles.
+custom-protocol = ["tauri/custom-protocol"]
diff --git a/app/src-tauri/build.rs b/app/src-tauri/build.rs
new file mode 100644
index 0000000..261851f
--- /dev/null
+++ b/app/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build();
+}
diff --git a/app/src-tauri/src/main.rs b/app/src-tauri/src/main.rs
new file mode 100644
index 0000000..660862a
--- /dev/null
+++ b/app/src-tauri/src/main.rs
@@ -0,0 +1,18 @@
+// trimwire Flightdeck — desktop shell (POC scaffold).
+//
+// Deliberately tiny: the window (configured in tauri.conf.json) points at the
+// daemon's loopback cockpit (http://127.0.0.1:8766), so this shell reuses the
+// exact web frontend the trimwire binary serves. The daemon is a *separate*
+// process reached over the loopback control API — there is no sidecar here, so
+// nothing to notarize beyond the app itself. See ../README.md and
+// ../../docs/cockpit/05-multiplatform-app.md.
+#![cfg_attr(
+ all(not(debug_assertions), target_os = "windows"),
+ windows_subsystem = "windows"
+)]
+
+fn main() {
+ tauri::Builder::default()
+ .run(tauri::generate_context!())
+ .expect("error while running trimwire Flightdeck");
+}
diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json
new file mode 100644
index 0000000..68164a0
--- /dev/null
+++ b/app/src-tauri/tauri.conf.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "productName": "trimwire Flightdeck",
+ "version": "0.1.0",
+ "identifier": "dev.trimwire.flightdeck",
+ "build": {
+ "frontendDist": "../dist"
+ },
+ "app": {
+ "windows": [
+ {
+ "title": "trimwire · Flightdeck",
+ "width": 980,
+ "height": 740,
+ "minWidth": 720,
+ "minHeight": 520,
+ "url": "http://127.0.0.1:8766"
+ }
+ ],
+ "security": {
+ "comment": "Scaffold CSP — restrictive by default (the Aug-2024 Tauri audit found any-origin IPC + unauthenticated dev-server disk exposure). Tighten/scope capabilities before shipping; see ../docs/cockpit/10-security-fresh-sources.md G5.",
+ "csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:8766; img-src 'self'; style-src 'self'"
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all"
+ }
+}
diff --git a/docs/cockpit/01-research-grounding.md b/docs/cockpit/01-research-grounding.md
new file mode 100644
index 0000000..214dc6f
--- /dev/null
+++ b/docs/cockpit/01-research-grounding.md
@@ -0,0 +1,124 @@
+# 01 — Research & Grounding: what the cockpit builds on
+
+> What trimwire exposes **today**, and the gaps a cockpit must fill. All claims are
+> code-grounded (file:line) from a read-only exploration pass. This is the factual base
+> the rest of the plan stands on.
+
+## 1. Control surface (what you can drive today)
+
+trimwire runs as a daemon: `trimwire serve` (alias `daemon`), binding **`127.0.0.1:8765`** by
+default (`src/config.rs` `ServerConfig::default`), upstream `https://api.anthropic.com`.
+Service lifecycle auto-detects **systemd** (Linux), **launchd** (macOS), or a **supervisor**
+fallback (WSL2, pidfile `~/.trimwire/daemon.pid`) — `src/cli/service.rs`. systemd/launchd use
+**socket activation**.
+
+**The entire HTTP surface today is two endpoints** (`src/proxy/gateway.rs`):
+
+- `GET /healthz` → `{"ok":true,"version":"x.y.z"}`, answered locally, never forwarded.
+- `POST /v1/messages` → the Anthropic proxy passthrough (this connection carries the OAuth
+ `Authorization: Bearer` token).
+
+**There is no management/control API.** Everything else is CLI:
+
+| Group | Commands |
+|---|---|
+| Lifecycle | `install`, `uninstall`, `on`, `off`, `status`, `doctor`, `update`/`upgrade` |
+| Inspect | `stats` (`--json`), `recall` (`--json`), `preview` (`--json`), `dashboard` (HTML report) |
+| Summarizer | `summarizer setup/status/benchmark/probe` |
+| Share | `share enable/disable/stats/benchmark` |
+| Maintenance | `sweep list/all/file/undo`, `config show/edit` |
+| Shell | `statusline`, `completions`, `man` |
+| Hidden | `serve`/`daemon`, `run`, `hook` |
+
+**Config** is a figment merge of global `~/.config/trimwire.toml` + project `./.trimwire.toml`
++ `TRIMWIRE_*` env. Crucially, **`[server] upstream` is never read from a project file** —
+that key decides where the OAuth token is sent, so honoring it from a cloned repo would let a
+project redirect your token (`src/config.rs`, with a regression test). **The daemon reads
+config only at startup** — any change needs an `off → on` restart. There is no config *write*
+API; only `config edit` ($EDITOR).
+
+### Gaps the cockpit must fill
+
+1. **No local control API** — only `/healthz`. No config read/write, no service toggle over
+ HTTP, no strategy tuning.
+2. **No live event stream** — state can only be polled.
+3. **No hot-reload** — config changes require a restart.
+4. **No config-validation endpoint** (only `doctor`, CLI-only).
+5. **No structured live metrics export** beyond `stats --json` snapshots.
+
+## 2. Data surface (what a dashboard can show)
+
+The **ledger** is a local SQLite DB (`~/.trimwire/ledger.db`, `src/ledger.rs`) with three
+tables: `requests` (one row per `POST /v1/messages`), `summarizer_events`, `upstream_errors`.
+
+**Content-free guarantee (confirmed in code):** the ledger stores only byte counts, token
+counts (input / cache_read / cache_creation / output), **prefix hashes** (with `messages`
+removed), timestamps, `session_id`, model name, strategy names + per-strategy elided bytes,
+TTFT (µs), and status codes. **Never** message content, prompts, tool results, or file paths.
+*This is structural: the data to leak simply isn't there.*
+
+Already-available metrics (all via existing `--json`):
+
+- **`stats --json`**: totals, `bytes_saved`, `reduction_pct`, `est_tokens_removed`, `per_day`
+ timeseries, `per_strategy` (count + bytes), `cache_stability` ratio, `response_metrics`
+ (avg TTFT, token buckets, `cache_hit_pct`), summarizer outcomes, upstream errors.
+- **`stats --session --json`**: per-session, per-model breakdown.
+- **`recall --json`**: recent sessions (id, last_day, requests, bytes, tokens, model).
+- **`preview --json`**: deterministic what-if prune estimate per profile — powers a
+ live "what would be pruned" pane *without touching the daemon* (strategies are pure fns).
+- **`trimwire dashboard`**: already emits a **self-contained HTML report**
+ (`src/cli/dashboard.rs` + `src/cli/dashboard_template.html`).
+- **`statusline`**: a live per-session reduction signal Claude Code renders after each turn.
+
+**The 9 strategies** (`src/strategies/mod.rs`): `failed_input_purge`, `stale_input_cap`,
+`cross_turn_dedup`, `stale_reads`, `simhash_dedup` (opt-in), `bloat_cap`, `sliding_window`,
+`image_strip`, `thinking_strip`. **Two profiles**: `default` (aggressive) and `gentle`
+(conservative). The product guardrails forbid a third profile and any intensity dial.
+
+### Useful data that does NOT exist yet (dashboard opportunities, not v1 blockers)
+
+Per-strategy latency cost; strategy fire-rate trend; cache-busting root cause; compression by
+model; summarizer cost/latency; reprune checkpoint timeline; sub-agent (sidechain) metrics;
+ledger growth/retention pressure. All are additive ledger columns or queries — defer until a
+pane proves it needs them.
+
+## 3. Web & telemetry surface (what to align with / reuse)
+
+**Site** (`site/`): **Astro 6 + Starlight 0.40**, TypeScript, **vanilla DOM (no React/Vue)**,
+Vitest+jsdom, Playwright. Deployed static (trimwire.dev). It already has interactive,
+dependency-free components built exactly like the cockpit needs:
+
+- `CommunityDashboard.astro` + `dashboard.ts` — sortable sticky tables, in-cell bars, KPI
+ strip, expandable detail rows.
+- `BenchmarkTable.astro`, `Hero.astro` (flow diagram + before/after bars).
+
+**Design system** (`site/src/styles/custom.css`): accent teal **`#2aa39c`** (dark `#178a83` /
+light `#9fe7e2`); semantic good `#22c87a` / warn `#e0a000` / bad `#e85d3f`; radius `0.55rem`;
+3px top-accent cap; tight headings (`-0.02em`); `tabular-nums` data tables; dark/light parity
+via `:root[data-theme]`; `prefers-reduced-motion` respected.
+
+**Collector** (`collector/`): a Cloudflare Worker + D1 that ingests **k-anonymous, content-free
+community aggregates** (`POST /ingest`, `GET /aggregates.json`, benchmark equivalents). This is
+the *community* backend — **the local cockpit reads the local `ledger.db`, not the collector.**
+The collector is a model for privacy discipline, not a data source for the cockpit.
+
+**Brand voice:** restrained, technical, honest ("headroom, not dollars"), data-first, minimal
+ornament. The cockpit should honor the *cockpit metaphor* but pick an on-brand, non-hype
+visible name (see doc 04).
+
+## 4. Constraints inherited from AGENTS.md (carried into every later doc)
+
+- Single static binary, **no heavy runtime deps**; latency is acceptable, weight is not.
+- Transparent **ToS-compliant** proxy: never originate model calls on the subscription token;
+ **no detection-evasion** ("transparent" = faithful, not undetectable).
+- `main` is protected; every change is a PR through green CI: `fmt + clippy + test`,
+ MSRV 1.85, **Python parity oracle**, cargo-deny/audit, 3 cross-platform builds.
+- Layer rules: `gateway.rs` must not contain mutation logic; `strategies/*` are pure (no I/O);
+ `ledger.rs` is the only SQLite I/O; new modules require an `ARCHITECTURE.md` update.
+
+## Takeaway
+
+The cockpit is **~80% presentation over data and verbs that already exist**, plus **one new
+backend component** (the control API, doc 03). That framing — reuse the HTML-report data, the
+vanilla-DOM components, the design system, and the CLI verbs — is what keeps the build
+proportionate to trimwire's size and ethos.
diff --git a/docs/cockpit/02-framework-decision.md b/docs/cockpit/02-framework-decision.md
new file mode 100644
index 0000000..641def0
--- /dev/null
+++ b/docs/cockpit/02-framework-decision.md
@@ -0,0 +1,103 @@
+# 02 — Framework Decision: the app stack
+
+> The maintainer asked the agents to decide. We ran a **3-member council** (independent agents,
+> same question, web-research-grounded for the 2026 state of each framework) plus a
+> disagree-seeking pass. **The council was unanimous.**
+
+## Verdict: **Tauri 2** — build one web frontend, ship it as a browser PWA *and* a Tauri shell
+
+All three council members independently scored **Tauri 2** the winner (weighted ~4.5–4.6 / 5),
+named **PWA-only / shared web frontend** the runner-up, and **Flutter** the flip-case if mobile
+ever becomes a v1 priority. They independently derived the *same* architecture:
+
+> Build the cockpit UI **once** as a conservative web app. Serve it from the trimwire binary on
+> loopback for the **browser web UI**. Wrap the **same bundle** in a **Tauri 2** shell — whose
+> core is Rust and can link trimwire's own crates / run the daemon as a sidecar — for the
+> **multi-platform app**. The daemon exposes a plain HTTP control API, so the browser, the
+> desktop app, and a future phone client are *one frontend with three transports*.
+
+> **Refinement (maintainer steer): PWA is the *primary* multi-platform vehicle; Tauri is the
+> *desktop convenience wrapper*.** The runner-up and the winner are not in tension — the cockpit
+> is one installable web app (the PWA) that covers desktop browsers and **iOS + Android via
+> Add-to-Home-Screen, no store, no fee**, and Tauri simply gives that same app a nicer desktop
+> shell (tray, autostart, sidecar daemon, OS-keychain token). With Android tightening toward
+> iOS-style verification (Sept 2026, doc 05 §0), the PWA is also the *durable* store-free path on
+> both mobile OSes. So the "framework decision" is really **PWA + Tauri-for-desktop**, not "a
+> native app per platform." Native mobile builds are de-prioritized (doc 05 §0/§4).
+
+## Scored trade-off (representative; the three councils agreed within rounding)
+
+Weights reflect the brief's priorities. Scores 1–5 (5 = best fit).
+
+| Criterion | Weight | **Tauri 2** | Electron | Flutter | Wails v3 | PWA-only |
+|---|---:|:--:|:--:|:--:|:--:|:--:|
+| Lightweight / single-binary ethos | 0.20 | **5** | 1 | 3 | 5 | 5 |
+| Rust-native synergy (share crates / API client) | 0.20 | **5** | 1 | 3 | 1 | 1 |
+| Code-sharing with the vanilla-DOM web UI | 0.16 | **5** | 5 | 1 | 5 | 5 |
+| Desktop now (mac/win/linux) | 0.14 | 4 | 5 | 5 | 4 | 4 |
+| Mobile later (iOS/Android) | 0.12 | 3 | 1 | **5** | 1 | 3 |
+| Security / attack surface | 0.10 | **5** | 2 | 4 | 4 | 4 |
+| Build/CI + solo-maintainer burden | 0.08 | 3 | 4 | 3 | 3 | **5** |
+| **Weighted total** | 1.00 | **≈4.55** | ≈2.5 | ≈3.2 | ≈3.5 | ≈3.7 |
+
+## Why Tauri 2 wins (the decisive axes)
+
+- **It's the only Rust-native option.** trimwire is Rust; a Tauri core is Rust. The cockpit can
+ depend on trimwire's **own crates** — the ledger reader, the `preview` engine, the figment
+ config types, a typed control-API client — instead of reimplementing them, and can run
+ trimwire as a **sidecar**. No other framework shares code at the Rust level (Flutter bridges
+ via FFI, Electron/Wails/PWA share nothing in Rust).
+- **It honors the single-binary ethos.** Tauri uses the OS WebView: ~3–15 MB installers,
+ ~30–50 MB RAM, vs Electron's 80–150 MB / 200–300 MB. A context-*pruning* tool shipping a
+ 150 MB Chromium runtime would be self-parody — Electron is disqualified on principle.
+- **One frontend powers both web and app.** The web UI is already Astro + vanilla DOM; that
+ exact bundle drops into a Tauri WebView. Build once, ship twice. Flutter/KMP throw the web UI
+ away and rebuild it in Dart/Compose — two frontends for a solo maintainer.
+- **Best security posture.** Tauri's capability model is deny-by-default — the right default for
+ a tool controlling a privileged, token-bearing local daemon.
+- **The phasing maps cleanly.** v1: Tauri desktop hitting `127.0.0.1`. Remote: same frontend,
+ remote base URL + token. Mobile: add Tauri's iOS/Android targets to the same project.
+
+## Runner-up: PWA-only — and the recommended hybrid
+
+A PWA scores highest on *burden* (zero native build, the web UI *is* the product) and is the
+runner-up. **Pick PWA-only over Tauri if** mobile is soft/unlikely or solo bandwidth can't
+absorb a native release pipeline this quarter.
+
+But the councils' actual recommendation is **not either/or** — it's **both**: the same web
+frontend is *already* a browser PWA (the "web UI" deliverable) **and** the content of a Tauri
+shell (the "app" deliverable). PWA-now and Tauri-later is a coherent sequence, not a rewrite,
+because both start from one clean web frontend talking to the `/api/v1` control surface.
+
+**Flutter is ruled out** (maintainer preference: no Flutter/Dart) — and it lost on the merits
+anyway (it throws away the shared web UI for a separate Dart frontend). So the mobile story does
+**not** fall back to Flutter. The mobile fallback, if Tauri-mobile ever disappoints, is the
+**same web frontend as a PWA / Capacitor wrapper** — never a second UI in another language. See
+doc 05 for mobile, including distribution **without paid app-store accounts**.
+
+## Top risks of Tauri 2 + mitigations
+
+| # | Risk | Mitigation |
+|---|---|---|
+| 1 | **Linux WebKitGTK rendering drift / instability** (the strongest counter; a Tauri maintainer says they "cannot fully recommend Tauri for Linux"). **Fresh-sources update (doc 10): materially understated for a data-table-heavy cockpit** — 2025 reports include rendering glitches + DOM-heavy lag, not just cosmetic drift. | Keep the UI deliberately plain; run **Playwright against WebKit**. **The real mitigation is the HTTP-first / PWA hedge** (the same frontend survives as a PWA if the shell disappoints) — keep it load-bearing. Also lock Tauri capabilities deliberately (the Aug-2024 audit found any-origin IPC + an unauthenticated dev-server disk exposure). See doc 10 G5. |
+| 2 | **Tauri 2 mobile is "a foundation, not finished"** — plugin parity gaps | Mobile is *deferred*; the mobile client is a webview control panel (Tauri's strong case), not deep-native. Re-evaluate at mobile-phase kickoff; Capacitor-wrapping the same web UI is a cheap fallback. |
+| 3 | **iOS codesigning / multi-toolchain CI burden** for a solo maintainer | Don't pay it until the mobile phase. Desktop signing is incremental on the existing 3-platform CI; use Tauri's GitHub Action. |
+| 4 | **A JS/Node build chain enters a pure-Rust repo** | Keep the cockpit a separate workspace member; the frontend build is the *same* one the web UI already needs — not net-new tooling. The daemon's MSRV/cargo-deny gates stay isolated. |
+| 5 | **Bus factor on a younger framework** | The HTTP-first design means the web/PWA UI keeps working even if the native shell is abandoned — low regret. |
+
+## A note on the unanimity
+
+Three independent agents agreeing is a groupthink flag (per the repo's own working
+conventions). We treated it as such — but the convergence is *forced by a hard constraint*: the
+brief requires reusing the existing vanilla-DOM web UI **and** honoring the Rust single-binary
+ethos, and exactly one framework maxes both. The disagree-seeking review (doc 07) was pointed at
+the broader *concept* (should we build this at all, and how much), not at re-litigating the
+framework — which is where the real dissent lives.
+
+## Sources
+
+Tauri 2.0 stable & mobile/sidecar docs (v2.tauri.app); Tauri-vs-Electron 2026 comparisons
+(pkgpulse, tech-insider, buildmvpfast); WebKitGTK instability discussion (tauri-apps GitHub
+#8524); Tauri iOS feedback (#10197); Playwright/WebKit pitfall notes; `flutter_rust_bridge`
+(pub.dev/GitHub); Wails v3 alpha status; KMP-2026 readiness. Full URLs are in the session
+transcript.
diff --git a/docs/cockpit/03-control-api.md b/docs/cockpit/03-control-api.md
new file mode 100644
index 0000000..c1a30a0
--- /dev/null
+++ b/docs/cockpit/03-control-api.md
@@ -0,0 +1,245 @@
+# 03 — Local Control API
+
+> The central missing piece. Today the daemon serves only `GET /healthz` and the
+> `/v1/messages` passthrough. The cockpit (browser + app) needs a control plane. This is the
+> implementation-ready design, grounded against `src/proxy/{gateway,listener}.rs`,
+> `src/cli/{serve,service,stats,recall,preview,sweep}.rs`, and `src/config.rs`.
+
+## TL;DR
+
+1. **Transport:** a **separate admin listener on its own loopback port** (default
+ `127.0.0.1:8766`) — *not* extra routes on the gateway (8765), *not* a UDS in v1. New module
+ tree `src/admin/`; `gateway.rs` stays mutation-free.
+2. **Endpoints:** REST-ish under `/api/v1/...`. Every mutating endpoint is a thin HTTP wrapper
+ over the **same library function the CLI already calls** — no logic duplicated. Read
+ endpoints reuse the existing `stats`/`recall`/`preview` JSON verbatim.
+3. **Live events:** **SSE** at `/api/v1/events` — one content-free broadcast channel. Never
+ touches or buffers the proxy stream.
+4. **Auth:** loopback-only + a 256-bit **bearer token** at `~/.trimwire/control.token` (0600) +
+ an **Origin/Host allowlist** (DNS-rebind guard). Remote is a documented seam, not built.
+5. **Config writes:** validate (reuse every existing `config.rs` check) → atomic write (reuse
+ `sweep.rs`'s temp+fsync+rename) → apply. **Hot-reload** for strategy/profile/summarizer/share
+ knobs via an `ArcSwap`; **restart-required** for `[server] listen`/`upstream`.
+6. **Build:** 5 PRs, each green through the existing CI. The control plane never touches
+ `strategies/`/`pairing/`, so the **Python parity oracle is unaffected**.
+7. **Stability:** the cockpit speaks **only** this versioned `/api/v1` contract — it never
+ shells out to the CLI or parses CLI output. The CLI and the API are two consumers of one
+ library, and contract tests fail CI on any shape drift, so **CLI commands can change freely
+ without breaking the cockpit**. See [doc 11](11-api-stability.md).
+
+## 1. Why a separate loopback admin listener
+
+| Option | Verdict |
+|---|---|
+| (a) Extra routes on gateway `:8765` | **Rejected.** That connection carries the Anthropic OAuth `Bearer` token; co-mingling control verbs there means a routing bug or confused-deputy could expose control to the credential path, and any client pointed at 8765 (all of Claude Code) could hit control verbs. |
+| **(b) Separate admin listener `:8766`** | **Chosen.** Physically distinct socket; the proxy path can never route to it; loopback bind; its own token. The OAuth-bearing path and the control path share **zero** surface. |
+| (c) Unix domain socket | Best raw security, but **a browser cannot speak AF_UNIX** — the web-UI half is dead on arrival (plus Windows/WSL2 friction). Could be added later for the native app behind the same router. |
+
+Credential isolation is the hard constraint; browser reachability is a hard product
+requirement. (b) satisfies both. Loopback-only means no remote caller in v1 — defense *before*
+auth runs. Socket activation reuses `listener.rs::obtain()`, generalized to
+`obtain_indexed(addr, fd_index, name)` for a second inherited fd / named `Sockets` key.
+
+## 2. Module placement (layer rules respected)
+
+`gateway.rs` must not contain mutation logic, so the control plane gets its own tree:
+
+```
+src/admin/
+ mod.rs run(listener, AdminState) -> Result<()>
+ router.rs method+path dispatch (hyper service_fn, like gateway.rs)
+ auth.rs token load/verify + Origin/Host allowlist; Authenticator trait (remote seam)
+ state.rs AdminState { config: ArcSwap, ledger, events, reprune_cache, paths }
+ handlers/ service.rs, config.rs, ledger.rs, preview.rs, sweep.rs, summarizer.rs, share.rs
+ events.rs EventBus (tokio::sync::broadcast) + content-free Event enum
+ reload.rs validate -> atomic write -> ArcSwap publish (hot) / restart signal
+```
+
+The only gateway change: read config via `ArcSwap::load()` **per request** (a *read*
+change, within its existing allowance) so a hot-reload publishes new config without a restart.
+The admin listener is **opt-in-by-presence**: absent/disabled → the daemon runs exactly as
+today (zero new surface for users who never open the cockpit). `ARCHITECTURE.md` gains an
+`admin/*` layer-table row and a decision-log entry.
+
+## 3. Endpoint surface (`/api/v1`)
+
+All requests require `Authorization: Bearer ` (except `GET /health` and the SSE
+`GET /events`, which the browser `EventSource` cannot authenticate — it stays Host/Origin/
+Sec-Fetch-guarded and content-free). Errors use the
+Anthropic-shaped envelope already in `gateway.rs`. Every endpoint maps to existing code
+(Appendix A).
+
+```
+# Health / version
+GET /health {"ok":true,"version":"x.y.z"} (unauth ok)
+GET /version {version, profile, control_api, upstream}
+
+# Service lifecycle (wraps cli::service)
+GET /service ServiceStatus {manager, listening, serving, pid, uptime_secs, ...}
+POST /service/on | /off | /restart 202 {action, ok}
+
+# Config (wraps cli::config + config.rs)
+GET /config {toml, effective, source_map}
+GET /config/effective == config show --json
+PUT /config {toml} {applied:"hot"|"restart_required", restart_fields?, diff}
+POST /config/validate {toml} {valid:true} | 422 {valid:false, errors:[...]}
+
+# Profile + per-strategy (convenience over config write; hot-reloadable)
+GET/PUT /profile {active, available:["default","gentle"]}
+GET /strategies {strategies:{name:{enabled, ...knobs}}}
+PUT /strategies/{name} partial knobs -> {applied:"hot"}
+
+# Ledger (READ-ONLY, content-free; reuse existing --json shapes)
+GET /stats[?since=&until=] == stats --json
+GET /stats/session/{id|last} == stats --session --json
+GET /sessions[?query=&limit=] == recall --json
+
+# Preview / what-if (wraps cli::preview; pure, read-only)
+POST /preview {path, profile, with_summarizer} == preview --json
+GET /preview/last[?profile=]
+
+# Summarizer (wraps cli::summarizer)
+GET /summarizer status
+POST /summarizer/probe {runs, confirm} (paid API -> confirm:true required)
+
+# Sweep (wraps trimwire::sweep)
+GET /sweep list candidates
+POST /sweep/run {path, dry_run}
+POST /sweep/run-all {dry_run, confirm}
+POST /sweep/undo {path}
+
+# Share opt-in (wraps cli::share)
+GET/PUT /share {enabled}
+POST /share/stats | /share/benchmark {confirm}
+
+# Live events
+GET /events text/event-stream (SSE)
+```
+
+Paid actions map the CLI's `--yes` to a `"confirm":true` body flag. When the ledger is
+disabled, read endpoints return `200 {"available":false,...}` (a valid state, like the CLI).
+
+## 4. Live events — SSE
+
+v1 is one-directional (daemon→UI; control *actions* go through REST), so **SSE** beats
+WebSocket (no upgrade/framing/client channel) and poll. A single
+`tokio::sync::broadcast::Sender` lives in `AdminState`. The gateway already computes
+everything at ledger-write time; it calls `events.publish(Event::Request{..})` there with the
+**same content-free fields** it writes to the ledger — a non-blocking send that never
+backpressures and is fully decoupled from consumers. **The proxy SSE body is never tee'd** — we
+emit a *summary event after* the response is metered, not the response bytes.
+
+Event types (all content-free): `request` (per-request savings), `strategy_fire` (optional),
+`daemon` (reloaded / upstream_error), `summarizer` (outcome), `resync` (on `broadcast` lag → UI
+refetches `/stats`). A unit test mirroring `audit.rs`'s `capture_never_leaks_content` asserts
+the `Event` serializer can only emit allowlisted fields.
+
+## 5. Auth & security
+
+> **Fresh-sources update (doc 10):** Host-pin alone is necessary-but-not-sufficient.
+> Add two more independent gates, ordered **before** the token compare and any side
+> effect: (a) **`Sec-Fetch-Site`** enforcement (browser-set, unforgeable — reject
+> non-`same-origin`/`none`), and (b) a **custom non-simple header** on mutating
+> endpoints (e.g. `X-Trimwire-Control`) to force a CORS preflight that default-deny
+> CORS fails. Note Chrome 142 **Local Network Access does NOT cover localhost→localhost**,
+> so do not rely on browser prompts for sibling-localhost threats. See doc 10.
+
+- **Loopback-only bind** (`127.0.0.1`/`[::1]`); a non-loopback admin bind is **rejected** at
+ bind time (analogous to `config.rs`'s `is_unsafe_listen`).
+- **Bearer token:** 256-bit random, `~/.trimwire/control.token` mode `0600` (reuse
+ `fsperm.rs`). Required on every request incl. SSE. Constant-time compare. **Not a cookie** →
+ not auto-attached cross-site → CSRF is structurally prevented.
+- **DNS-rebinding guard:** reject any request whose `Host` isn't the literal loopback authority
+ (`127.0.0.1:8766` / `localhost:8766` / `[::1]:8766`) and whose `Origin` (when present) isn't
+ allowlisted.
+- **Same-origin UI:** the daemon serves the cockpit static bundle under `GET /` on the admin
+ port, so the UI never needs cross-origin; CORS stays default-deny. The browser UI is handed
+ the token via a same-origin bootstrap injected at page load (never in a URL).
+- **`X-Content-Type-Options: nosniff`, `Cache-Control: no-store`** on API responses.
+
+**Remote seam (designed, not built):** `auth.rs` exposes `trait Authenticator { fn
+authenticate(&Request) -> Result }` with one v1 impl `LoopbackToken`. Non-loopback
+bind is hard-rejected unless a strong Authenticator is active — so token-only auth can never be
+accidentally exposed to a network. See doc 06 for the full remote requirement list (R1–R10).
+
+## 6. Config write safety
+
+- **Validate before write:** run the *same* figment merge `Config::load` uses, with the
+ submitted TOML as the global layer, then every existing `config.rs` check (summarizer
+ provider/style/`accept_ratio`, `is_unsafe_listen`, profile/mode recognition, the upstream
+ credential-routing guard). Surface the existing user-facing messages verbatim in a `422`.
+ **Refactor:** extract `Config::validate()` / `load_from_str()` shared by `load()` and admin.
+- **Atomic write:** temp + fsync + rename + dir-fsync — **reuse `sweep.rs`'s** hardened
+ primitive. Keep one `.bak` so the UI can offer "revert". Whole-file replace (matches
+ `config edit`) avoids partial-merge/comment-loss footguns.
+- **Apply (hot-reload vs restart):** the gateway reads an `ArcSwap`.
+
+| Hot-reloadable (gateway reads per request) | Restart-required |
+|---|---|
+| `profile`, all `[strategies.*]`, `[reprune]`, `[summarizer]`, `[share]`, `[ledger] retain_days` | `[server] listen` (bind), `[server] upstream` (credential routing — deliberately restart-only), `[ledger] db_path` (open handle) |
+
+- **In-flight requests** snapshot `config.load()` at entry → a mid-flight swap never affects a
+ request already past that point; the proxy stream is never interrupted.
+- **Reprune cache:** clear (mark-dirty) the reprune `DashMap` when a *pruning-affecting* section
+ changes (profile/strategies/reprune/summarizer), so the next turn re-checkpoints under the new
+ config. Reprune is self-correcting, so a clear is safe. Non-pruning toggles (`[share]`,
+ `retain_days`) don't clear it.
+- `[server] upstream` is restart-only even though the gateway reads it per request — changing
+ where the OAuth token goes should be a conscious restart, never a config-editor keystroke.
+
+## 7. Phased build plan (mapped to PR/CI)
+
+Each PR is green through `fmt + clippy + test` (MSRV 1.85), the **Python parity oracle**
+(untouched — control plane doesn't touch the prune path), cargo-deny/audit, and 3
+cross-platform builds. Only new dep: `arc-swap` (tiny); `tokio` broadcast already in tree;
+`getrandom` for the token.
+
+1. **PR 1 — Plumbing + read-only API.** `ArcSwap` refactor (behavior-identical); admin
+ listener (loopback + socket-activation); `src/admin/` skeleton + auth (token + Host pin);
+ `GET /health`/`/version`/`/service`(status)/`/stats`/`/sessions`/`/config`. Extract the
+ `serde_json::Value` builders out of the `stats`/`recall` `println!` wrappers so CLI + HTTP
+ share them. Tests: 401/200 auth, Host-pin rejection, JSON shape == CLI `--json`.
+2. **PR 2 — Live events (SSE).** `EventBus`; publish content-free `Event::Request` at
+ ledger-write time; `GET /events` (keepalive, lagged→resync). Tests: `event_never_leaks_content`,
+ SSE framing, proxy stream path unchanged.
+3. **PR 3 — Config write + hot-reload.** Extract `Config::validate`; `reload.rs` (validate →
+ atomic write → diff → ArcSwap publish / restart-required; clear reprune cache); `PUT /config`,
+ `/config/validate`, `/profile`, `/strategies`, `/share`. Tests: bad TOML→422, hot-reload
+ visible to next request, restart classification, crash-safe atomic write.
+4. **PR 4 — Service + preview/sweep/summarizer/share actions.** `POST /service/{on,off,restart}`;
+ `/preview`; `/sweep/*`; `/summarizer/probe`; `/share/*`. Reuse `cli::*` bodies + `trimwire::sweep`.
+ Tests: self-`off` 202-then-teardown, `confirm`-gated paid calls, sweep run-all aborts on active file.
+5. **PR 5 — Static UI hosting + docs + remote seam.** Serve the cockpit bundle under `GET /`;
+ finalize the `Authenticator` seam; update `ARCHITECTURE.md`/`CONFIGURATION.md`/`SECURITY.md`/README;
+ add the global-only `[admin]` config section.
+
+```toml
+[admin]
+enabled = true # spawn the control listener (off -> daemon == today)
+listen = "127.0.0.1:8766" # loopback-only; non-loopback rejected in v1
+# token lives in ~/.trimwire/control.token (0600), not in config
+```
+`[admin]` is **global-only** (never read from a project `./.trimwire.toml`) — a cloned repo
+must not be able to open or relocate a control port.
+
+## Appendix A — endpoint → reuse map (no duplicated logic)
+
+| Endpoint | Backed by |
+|---|---|
+| `/health` `/version` | `gateway::health_response` + `Config` |
+| `/service*` | `cli::service::{detect,tcp_open,healthz_ok,on,off}` (+ new `restart`) |
+| `/config*` `/profile` `/strategies` | `config::{load_from_str,validate,PROFILES}`, `sweep.rs` atomic-write |
+| `/stats*` | `cli::stats` JSON builder (extracted) |
+| `/sessions` | `cli::recall` JSON builder (extracted) |
+| `/preview*` | `cli::preview` JSON builder (extracted) |
+| `/sweep*` | `trimwire::sweep` library module |
+| `/summarizer*` | `cli::summarizer` status/probe |
+| `/share*` | `cli::share` enable/stats/benchmark |
+| `/events` | `EventBus` fed by gateway/summarizer/ledger (content-free) |
+
+## Appendix B — deliberately NOT in v1
+
+No WebSocket (SSE suffices); no UDS (browser can't use it); no remote exposure (loopback hard-
+enforced; seam present); no interactive `summarizer setup` raw endpoint (UI drives config
+primitives); no partial-merge config PUT; no content in events/queries (structural — the ledger
+is content-free).
diff --git a/docs/cockpit/04-web-cockpit-ui.md b/docs/cockpit/04-web-cockpit-ui.md
new file mode 100644
index 0000000..a737145
--- /dev/null
+++ b/docs/cockpit/04-web-cockpit-ui.md
@@ -0,0 +1,160 @@
+# 04 — Web Cockpit UI ("Flightdeck")
+
+> The browser web UI that opens locally and gives **full control** of the trimwire daemon over
+> the control API (doc 03). Local-first for v1; remote-ready by construction.
+
+## 1. Delivery model — served by the binary
+
+A new command serves the cockpit from the same process that owns the control API and the
+ledger:
+
+```
+trimwire cockpit # start control API + static UI on loopback [IMPLEMENTED in the POC]
+# Proposed (not in the POC — doc 09 ships only `trimwire cockpit`):
+# trimwire cockpit --no-open # headless (for the app webview / remote phase)
+# trimwire dashboard --serve # alias upgrading the static HTML report into the live cockpit
+```
+
+- Static assets are **embedded into the Rust binary** at build time (`rust-embed` /
+ `include_dir!`) and served from the admin loopback handler alongside `/api/v1/*`. One origin →
+ no CORS, no second port, no separate install.
+- Bound to `127.0.0.1` only; same-origin → the control API's token/Host-pin auth covers the UI.
+- **Fully offline, zero CDN.** Vendor every asset; system font stacks; no analytics, no remote
+ calls — consistent with the content-free, transparent ethos.
+
+**Rejected:** a separate Node dev-server (adds a runtime dep to *run* the product — Node/Vite
+is a build-time tool only); folding into the public trimwire.dev site (that's a static,
+public, community-aggregate site reading the collector — wrong on security, offline, and
+lifecycle; the cockpit ships with the binary and must version-match it).
+
+**Migration to remote** is a transport swap only: bind beyond loopback (opt-in, TLS + token),
+and the UI's API base URL becomes configurable instead of same-origin. Because the UI talks to
+`/api/v1` (never to SQLite directly), nothing in the frontend changes except the fetch base +
+auth header.
+
+## 2. Frontend stack — vanilla DOM vs Svelte (OPEN DECISION)
+
+> **Maintainer input:** *"we'll install Svelte+Astro on another flow for page rebrand — I like
+> Svelte, but if vanilla is enough for subagents, ok."*
+
+This is a genuine fork, and the site rebrand changes the calculus. Both options are viable;
+here's the honest trade-off so the maintainer can decide:
+
+| | **Vanilla DOM + tiny store** | **Svelte** |
+|---|---|---|
+| Aligns with *current* site (`dashboard.ts`) | ✅ direct lift of existing components | ⚠️ would rebuild them |
+| Aligns with *rebranded* site (Svelte+Astro) | ⚠️ diverges from where the site is going | ✅ **shares idioms/components with the new site** |
+| Bundle size in the binary | Smallest (a few KB) | Small — Svelte compiles to vanilla JS, no VDOM runtime (~a few KB more) |
+| Live updates (counters tick, rows stream) | Manual DOM patches via a ~150-line store | First-class reactivity (`$state`/stores) — *better fit for the live monitor* |
+| Config forms / validation / dialogs | Hand-rolled | Componentized, less boilerplate |
+| Solo-maintainer ergonomics | Lean but more glue code | More structure; one more compile step (already present once Astro+Svelte lands) |
+| Tauri WebView compatibility | Perfect (plain DOM) | Perfect (compiles to plain DOM/JS) |
+
+**Recommendation:** **If the site is being rebranded to Svelte+Astro anyway, build the cockpit
+in Svelte too.** The original "reuse the vanilla-DOM components" argument was the main case for
+vanilla — and it weakens precisely because the site is moving off vanilla. Svelte keeps the
+cockpit and the rebranded site on **one idiom and one shared component library**, gives
+first-class reactivity for the live session monitor (the cockpit's most dynamic screen), and
+still compiles to a tiny, VDOM-free bundle that satisfies the lightweight ethos and embeds
+cleanly in both the browser and the Tauri shell. The **design tokens** (`custom.css`) port
+verbatim regardless of framework.
+
+Use **vanilla DOM only if** the cockpit ships *before* the Svelte rebrand and you want zero new
+build steps in the interim — in which case the components below are a direct lift from
+`dashboard.ts`, and a later port to Svelte is mechanical (the data contracts and tokens don't
+change). Either way, **screens consume the control API through a typed `api.*` client + a small
+reactive store**, so the rendering layer is swappable without touching data flow.
+
+*(The screen specs below are framework-agnostic — they describe data, actions, and states, not
+DOM construction. They hold whether rendered by Svelte components or vanilla render functions.)*
+
+## 3. Information architecture
+
+Single-page app, left-rail nav, persistent global status header. Ten destinations in four
+groups:
+
+```
+┌──────────────────────────────────────────────────────────────────────┐
+│ trimwire · Flightdeck ● running default 127.0.0.1:8765 ◑ ⏻ │ ← global header (always live)
+├────────────┬─────────────────────────────────────────────────────────┤
+│ MONITOR │ Live · Savings · Strategies · Sessions │
+│ TUNE │ Preview/What-if · Config │
+│ OPERATE │ Daemon · Sweep · Summarizer │
+│ SHARE │ Telemetry │
+└────────────┴─────────────────────────────────────────────────────────┘
+```
+
+The **global header** carries the four highest-frequency controls — power toggle (on/off),
+profile pill, listen address, theme toggle — always live via the control-API health poll / SSE.
+
+## 4. Reuse map (design system + components)
+
+Lift the token layer from `site/src/styles/custom.css` verbatim (teal `#2aa39c`, semantic
+good/warn/bad, `0.55rem` radius, 3px cap, dark/light parity, `tabular-nums`, reduced-motion) so
+the cockpit and site read as one brand. Component patterns to reuse (as components if Svelte, as
+render fns if vanilla): KPI strip, sortable sticky table (`twd` + `Col` model), in-cell mini-bar,
+labelled detail bar, expandable detail row, summary tiles, segmented token bar, per-day rows,
+honest empty-state, banner, semantic helpers (`fmtBytes` **must stay 1024-based** to match
+`stats::human_bytes`), `STRATEGY_LABELS`.
+
+New components: left-rail nav, global live status header, reactive store + SSE/poll-fallback
+client + platform adapter, live ticker, validated config form controls, confirm dialog, profile
+switcher, preview two-column diff, telemetry payload-preview.
+
+## 5. The ten screens (purpose · data · actions · states)
+
+1. **Live Session Monitor** — watch per-request savings as you work. Data: SSE `request` stream
+ + rolling KPIs. Actions: pause, clear, jump to session. Empty: "Idle — run a `claude` turn
+ and rows appear live." API: `GET /events` (live) + `GET /stats` (backfill).
+2. **Savings** — the `trimwire dashboard` report, live. Data: `stats --json` (totals, per_day,
+ cache_stability, response_metrics). Actions: window selector, export the self-contained HTML
+ snapshot. Keeps the **"headroom, not dollars"** framing verbatim.
+3. **Strategies** — per-strategy bytes/fire-rate for the active profile. Data: `stats`
+ per_strategy + `config` profile. Disabled strategies dimmed with "enabled in default only".
+ Deep-links each strategy to its Config knob.
+4. **Sessions** — `recall` list → per-session per-model drilldown (`stats --session`). Actions:
+ sort/filter by model, "Preview this session", copy id (truncated, full on hover).
+5. **Preview / What-if** — deterministic default-vs-gentle compare on a captured request.
+ **Content-free: shows byte deltas + per-strategy elided bytes + block *types*, never block
+ *content*.** Pure/read-only (`POST /preview`). The safe sandbox before any Config change.
+6. **Config editor** — validated per-strategy knobs + profile switch, with **per-field source
+ attribution** (global/project/env). Mandatory honesty banner: *"Saved. Restart to apply
+ (off → on)"* with a one-click restart, since the daemon reads config only at startup.
+ Project-scoped `upstream` shown **read-only/ignored** with the token-guard tooltip. Validate
+ live (`POST /config/validate`); write (`PUT /config`).
+7. **Daemon** — lifecycle + health + doctor. Status card (running/stopped, version, addr,
+ service manager), Doctor panel (pass/warn/fail), on/off/restart (confirm-dialog'd; restart
+ warns "in-flight requests complete"). Driven by `service` SSE events.
+8. **Sweep** — list → **dry-run by default** → review → Apply (confirm) → Undo always offered.
+ `trimwire::sweep` library; partial-apply state made explicit with Undo available.
+9. **Summarizer** — backend status, benchmark, probe. Copy states the boundary: **never
+ originates model calls on the user's Claude OAuth token**. Switch backend routes to Config.
+10. **Telemetry / Share** — opt-in with a **payload preview** (show the literal content-free
+ k-anon JSON before sending — making the privacy claim *inspectable*, not just asserted),
+ k-anon explainer (K=10 stats / K=5 bench), link to the public community dashboard.
+
+Every screen has loading (skeleton + "Connecting to daemon…"), empty (honest CTA, no fake
+data), and error (banner naming the failed endpoint + Retry; daemon-down → "Start daemon")
+states. If the SSE feed drops, fall back to a 2s `?since=` poll with exponential backoff.
+
+## 6. Brand / naming
+
+Honor the cockpit *metaphor*, avoid marketing-speak. Proposals:
+
+1. **trimwire Flightdeck** *(recommended)* — a flight deck = full instrumentation + control;
+ reads as a place, not a pitch; pairs with the "wire" word. Visible name "trimwire ·
+ Flightdeck"; command verb `trimwire cockpit` (metaphor in the verb, restrained noun in the
+ UI). Wordmark reuses the split-color treatment: **trim**wire (teal) · Flightdeck.
+2. **trimwire Console** — maximally plain fallback if any metaphor feels too cute.
+3. **trimwire Panel** — instrument-panel nod without the word.
+
+## 7. Shared frontend (build once, ship many)
+
+The cockpit `dist/` *is* the app's UI. The multi-platform app (doc 05) is a thin webview shell
+that either embeds the trimwire binary serving loopback and points the webview at it, or loads
+the embedded `dist/` and talks to the local control API. The only shell-specific layer is a
+tiny **platform adapter** behind the `api.*` client: same-origin fetch (browser/served),
+`tauri::invoke`/native bridge (app), or remote base-URL + token (deferred remote). Screens never
+know which transport they're on. Because all state flows through `/api/v1` + the event stream,
+**the remote-control phase is a transport swap, not a rewrite** — browser cockpit, desktop app,
+and future phone client are one frontend with three adapters.
diff --git a/docs/cockpit/05-multiplatform-app.md b/docs/cockpit/05-multiplatform-app.md
new file mode 100644
index 0000000..88a6ef4
--- /dev/null
+++ b/docs/cockpit/05-multiplatform-app.md
@@ -0,0 +1,185 @@
+# 05 — Multi-platform App: PWA-primary, Tauri-for-desktop
+
+> The "multi-platform app" that controls an installed trimwire. **Strategy (updated):
+> PWA-first.** The cockpit is one installable web app — that single artifact *is* the app on
+> every platform: desktop browsers, and **iOS + Android via Add-to-Home-Screen**, with **no app
+> store and no developer fee**. **Tauri 2** (doc 02) is the optional *desktop convenience wrapper*
+> around that same PWA (native window, tray, autostart, manage-the-daemon). Native mobile builds
+> are **deliberately de-prioritized** — see §4.
+
+## 0. Why PWA-first (and why native mobile is the wrong bet)
+
+- **Maximal code-sharing:** the PWA *is* the shared artifact — one build, every surface (§6).
+ A native mobile app would, at best, wrap the same web view; at worst, fork the UI. PWA gets the
+ reach with zero extra UI.
+- **No paid stores, on both OSes** (maintainer constraint): add-to-home-screen is free on iOS and
+ Android; the binary already serves the manifest + icon (the POC, doc 09).
+- **Android is converging toward iOS.** Google's **developer-verification** requirement begins
+ **Sept 2026** (Brazil/Indonesia/Singapore/Thailand first, then global): all apps — *including
+ sideloaded ones* — must be registered by a verified developer on certified devices
+ ([Android Developers Blog](https://android-developers.googleblog.com/2026/03/android-developer-verification-rolling-out-to-all-developers.html),
+ [Gadget Hacks](https://android.gadgethacks.com/news/googles-new-android-sideloading-rules-start-august-2026/)).
+ So a "free APK sideload" path is becoming as gated as iOS — **the durable store-free path on
+ both is the PWA.**
+- **The cockpit doesn't need native capabilities.** It's a foreground control panel over a live
+ local daemon. The known PWA gaps are all *background* features — no background sync / data-only
+ push, ~70–85% iOS push delivery
+ ([MagicBell](https://www.magicbell.com/blog/pwa-ios-limitations-safari-support-complete-guide)) —
+ none of which a foreground panel uses. (Caveat: EU DMA can downgrade standalone PWAs to a Safari
+ tab; acceptable for self-hosted dev tooling.)
+
+## 1. Process model — every shell is a thin client over the daemon
+
+The app does **not** reimplement trimwire. Two viable shapes (pick per-platform; they're not
+exclusive):
+
+- **A — Talk to the running daemon.** The app webview loads the shared `dist/` and calls the
+ local control API (`127.0.0.1:8766`) exactly as the browser cockpit does. The daemon is the
+ one the user already installed/`on`'d. Simplest; the app is "a nicer window onto the daemon."
+- **B — Sidecar.** The Tauri core bundles the trimwire binary as a **sidecar**, starting/managing
+ it. Good for users who want the app to *be* the install. Tauri's Rust core can also link
+ trimwire's crates directly (`config`, `ledger` read, `preview`) for actions that don't need
+ the daemon running.
+
+**Recommendation:** ship **A** first (lowest risk, mirrors the browser path), offer **B** as an
+opt-in "manage the daemon for me" mode once stable. Either way the **frontend bundle is
+identical** — no second UI.
+
+The only app-specific code is the **platform adapter** behind the `api.*` client (doc 04 §7):
+same-origin fetch in the browser; a Tauri command/native bridge in the app; remote base-URL +
+token in the deferred remote phase. Screens are transport-agnostic.
+
+## 2. Why Tauri here (recap of doc 02, app lens)
+
+- Rust core → can depend on trimwire's own crates and run it as a sidecar; no logic re-impl.
+- OS WebView → ~3–15 MB installer, ~30–50 MB RAM; honors the single-binary ethos.
+- Same web frontend in browser and app — build once.
+- Deny-by-default capability model — right for an app controlling a token-bearing daemon.
+- Phasing: desktop now → remote (point the same frontend at a remote URL) → mobile (add Tauri
+ iOS/Android targets to the same project).
+
+## 3. Build, signing & CI
+
+Keep the app as a **separate workspace member / sub-crate** so it doesn't entangle the daemon's
+MSRV-1.85 / cargo-deny / parity-oracle gates. The frontend build is the *same* one the web UI
+needs (doc 04), so it's not net-new tooling.
+
+| Platform | Packaging | Signing | CI note |
+|---|---|---|---|
+| Linux | AppImage / DEB / RPM | optional | incremental on the existing 3-platform matrix; WebKitGTK is the rendering risk (doc 02 R1) — test with Playwright/WebKit |
+| macOS | DMG / `.app` | notarization (Apple Developer acct) | add when shipping publicly; avoid `externalBin` notarization pitfalls by talking to a *separate* daemon process (shape A) rather than bundling, where possible |
+| Windows | NSIS / MSI | Authenticode | Tauri GitHub Action handles it |
+| iOS / Android | **see §4 — no paid store required** | self-sign / sideload / PWA | **deferred to the mobile phase**; the no-store distribution paths below avoid the App Store / Play fees entirely |
+
+Use Tauri's official `tauri-action` GitHub workflow. **Defer all mobile pipeline cost** to the
+mobile phase. Early internal builds can be unsigned/ad-hoc.
+
+## 4. Mobile — without paying Google Play or the App Store
+
+**Maintainer constraint:** *won't pay for Google Play / Apple Developer just for this app.* That
+is fine — none of trimwire's other distribution (the binary ships via GitHub releases / crates.io
+/ binstall) goes through an app store, and mobile here is a **remote-control panel** (phone →
+laptop's trimwire), not a consumer app. Distribution options, by platform, that need **no paid
+store account**:
+
+| Platform | No-store option(s) | Cost | Notes |
+|---|---|---|---|
+| **Android** | (a) **PWA / "Add to Home Screen"** of the cockpit web UI; (b) **direct APK** from GitHub Releases (sideload); (c) **F-Droid** (free, open-source store) | **$0** | Android allows sideloading by default. A Tauri-Android or Capacitor APK built in CI and attached to a release is installable with no Play account. F-Droid is the free "real store" path. |
+| **iOS** | (a) **PWA / "Add to Home Screen"** (Safari) — *recommended*; (b) free **sideload** (AltStore/SideStore, 7-day re-sign on a free Apple ID) | **$0** | iOS has no free public store, but an installable PWA covers the remote-control use case with zero fee and zero signing. Native sideload exists but is higher-friction (7-day expiry). |
+| **Desktop** | GitHub Releases (DMG/MSI/AppImage), Homebrew, etc. | $0–low | Optional code-signing only when shipping publicly. |
+
+**Recommendation: PWA-first for mobile.** Because the cockpit is one web frontend (doc 04), the
+phone "app" can simply be the **installable PWA** — add-to-home-screen on both iOS and Android,
+zero store, zero fee, zero second codebase. Layer a **Tauri-Android / Capacitor APK** (distributed
+via GitHub Releases + F-Droid) on top *only if* a native Android shell is wanted later. This keeps
+the maintainer's "no paid stores" constraint as a first-class design choice, not a compromise.
+
+> Note: a **remote** PWA controlling a laptop over the network interacts with the deferred remote
+> architecture (doc 06) and Chrome's Local Network Access (doc 10 G2/G4) — the phone reaches the
+> daemon over the user's **overlay/LAN** (a loopback-equivalent origin via the tunnel), not a
+> public page hitting `127.0.0.1`. Same-machine use stays trivial; cross-device is the v3 gate.
+
+Tauri 2 mobile itself is *stable-API but maturing* — fine for this webview-driven panel, weak for
+deep-native; re-evaluate at the mobile-phase kickoff. **Flutter is not the fallback** (maintainer
+preference, and it would mean a second UI); the fallback is always **the same web frontend** as a
+PWA or Capacitor wrapper.
+
+## 5. Code & component sharing (app ↔ web cockpit) — the core of the strategy
+
+The cockpit is **one codebase, one build artifact**. The browser PWA, the Tauri desktop shell,
+and any future remote/mobile client are the **same `dist/`** plus a thin per-platform adapter.
+That is the whole reason this is cheap for a solo maintainer.
+
+**Shared — ~100% of the frontend (single source of truth):**
+
+- **UI components** — KPI cards, sortable/sticky tables, in-cell bars, config forms, dialogs,
+ the live-monitor list (doc 04 inventory).
+- **Design tokens** — `tokens.css` (teal palette, `0.55rem` radius, dark/light, `tabular-nums`),
+ lifted verbatim from the site so cockpit + site read as one brand.
+- **The typed API client** for `/api/v1` — the *only* way any shell talks to the daemon (doc 11).
+- **The reactive store** + screen/route definitions + formatting/validation helpers
+ (`fmtBytes` stays 1024-based to match `stats::human_bytes`).
+
+**Not shared — a thin platform adapter (tens of lines), behind one interface:**
+
+| Concern | Browser **PWA** | **Tauri** desktop | Remote/mobile (deferred) |
+|---|---|---|---|
+| transport (`api.*`) | same-origin `fetch` / `EventSource` | same-origin to loopback/sidecar, or `invoke` | base URL + device token over TLS/overlay |
+| token / secure store | server-injected same-origin bootstrap | **OS keychain** | OS keychain + pairing (doc 06) |
+| shell chrome | browser tab / installed PWA window | native window, tray, autostart, manage-daemon | mobile home-screen PWA |
+
+**Adding a platform = writing one small adapter, never a new UI.** Screens are
+transport-agnostic; they call typed `api.*` methods and never know which shell they're in.
+
+**Build once, serve/embed everywhere:** a single Vite (or SvelteKit) build emits `dist/` + the
+PWA `manifest`/icon (and later a service worker). The trimwire binary `include_str!`/`rust-embed`s
+and serves it — **the POC already serves the HTML + `manifest.webmanifest` + `icon.svg` this way**
+(doc 09) — and the Tauri shell bundles the *same* `dist/`. One build feeds the browser, the PWA,
+and the desktop app.
+
+**An extra sharing layer on desktop only:** because Tauri's core is Rust, the desktop shell can
+*additionally* reuse trimwire's own crates (ledger read, config types, the typed control-API
+client) — a second layer of sharing no other framework offers (doc 02). The PWA can't link Rust
+and doesn't need to: it speaks the same `/api/v1` contract.
+
+**Net:** sharing isn't just "some components" — it's the **entire frontend artifact + the API
+contract**, with platform differences quarantined to a tiny adapter, plus a bonus Rust-crate layer
+on desktop. That is what lets "one app" mean *every* platform without a second team.
+
+### Offline / service-worker note (additional finding)
+
+A control panel needs the *live* daemon for data, so "offline" is limited by nature: a service
+worker can cache the **app shell** (HTML/CSS/JS/manifest/icon) so the window opens instantly and
+shows a clean "daemon unreachable" state, but the data panes still require the daemon. The POC
+ships the manifest + icon (installable today); the **service worker for app-shell caching is a
+tracked follow-up** — it must be served with a permissive-enough CSP (`worker-src 'self'`) and
+must never cache `/api/*` responses (always network for live, content-free data).
+
+### Secure-context / remote tie-in (additional finding)
+
+PWA install + service workers require a **secure context** (HTTPS *or* `localhost`). Same-machine
+use is `localhost` → secure → works with no TLS (the common case). A **remote** PWA (phone →
+laptop) therefore inherits the **v3 TLS/overlay requirement** (doc 06): the daemon must be reached
+over an HTTPS overlay/LAN origin, not a public page hitting `127.0.0.1` (which also trips Chrome
+Local Network Access, doc 10 G2/G4). So "PWA-first" and "remote is gated on TLS" are consistent,
+not in tension.
+
+## 6. Risks specific to the app
+
+- **WebKitGTK drift on Linux** — keep the UI plain; CI against WebKit. (doc 02 R1)
+- **Notarization/signing burden** — defer mobile; reuse `tauri-action`; prefer shape A to dodge
+ sidecar-notarization bugs.
+- **Daemon coupling** — the app is a *client* of the control API; if the daemon is old/missing,
+ the app shows a clear "daemon not found / version mismatch" state and offers to install/start
+ (shape B) rather than failing opaquely.
+- **Scope/regret** — the HTTP-first design means if Tauri ever disappoints, the same frontend
+ survives as a PWA. The app is the lowest-regret native option.
+
+## 7. Open questions for the maintainer
+
+- Shape **A vs B** as the default (talk-to-daemon vs sidecar)? (Recommended: A first.)
+- Does the desktop app ship in the **same repo** (workspace member) or a sibling repo? (Workspace
+ member keeps the shared frontend + Rust client in one place; sibling keeps the daemon repo
+ pure. Recommended: workspace member, gated so it doesn't touch the daemon's CI matrix.)
+- Is mobile genuinely wanted, or is "multi-platform" satisfied by cross-OS **desktop** + a
+ browser PWA on phones? (Decides whether the Apple/Play tax is ever paid — see doc 06.)
diff --git a/docs/cockpit/06-remote-control.md b/docs/cockpit/06-remote-control.md
new file mode 100644
index 0000000..5ff7aad
--- /dev/null
+++ b/docs/cockpit/06-remote-control.md
@@ -0,0 +1,122 @@
+# 06 — Remote Control (designed-for, deferred)
+
+> **v1 is local-loopback only.** Remote control (phone → laptop's trimwire) is a later phase.
+> This doc designs that phase **and** specifies the cheap seams v1 must include now so remote is
+> purely **additive**, never a rewrite.
+
+**Hard invariant (the whole ballgame, repeated everywhere):**
+> The Anthropic subscription **OAuth token never leaves the host** and is never exposed by the
+> control API. The control surface is **daemon control + content-free stats only**. `[server]
+> upstream` is **never remotely settable**. Enforced structurally (layer boundary) *and* by a CI
+> leak test.
+
+## 1. Transport options
+
+| Option | What it is | Lightweight | ToS-safe | Solo-maintainer | Secure-by-default | Reach | Verdict |
+|---|---|---|---|---|---|---|---|
+| **(a) Direct LAN** | daemon binds LAN iface, mDNS `_trimwire._tcp`, self-signed TLS | excellent | neutral | excellent | good *if* opt-in + TLS + pairing | same subnet only | **opt-in primary, same-WiFi** |
+| **(b) BYO overlay** (Tailscale/WireGuard) | **daemon stays loopback**; user's overlay forwards to `127.0.0.1` | excellent (ships nothing) | neutral | excellent (no infra) | **best — bind never widens** | full NAT traversal | **primary, cross-network** |
+| (c) trimwire-hosted relay | rendezvous/TURN brokers both ends | poor (24/7 infra, cost) | caution | poor | only if zero-knowledge | full | **v3 last resort** |
+| (d) Reverse tunnel | daemon dials out to a user-owned endpoint | good | neutral | medium | good | full (outbound) | v3 alternative |
+
+### Recommendation
+
+- **Cross-network primary: (b) bring-your-own overlay.** The daemon stays bound to loopback
+ exactly as in v1; the user runs Tailscale/WireGuard and the overlay forwards a private address
+ to `127.0.0.1:8766`. trimwire ships **nothing extra** and runs **no infrastructure**, and the
+ token-bearing process never widens its bind. Auth is doubled (overlay keys + trimwire pairing
+ token). **Do NOT embed `tsnet`/`tailscale-rs`** — too heavy for the single-binary ethos;
+ recommend BYO in docs.
+- **Same-LAN convenience: (a) Direct LAN, opt-in.** For "phone on the same WiFi," offer explicit
+ opt-in LAN exposure with mDNS + TLS + pairing. This is the only mode where trimwire changes its
+ bind, so it is **gated hard** (§4, §5).
+- **Relay/reverse-tunnel: v3 only**, off by default, **zero-knowledge** (relay brokers an
+ encrypted channel; never terminates TLS, never sees the pairing token or any stats). Build only
+ if BYO-overlay proves too high-friction.
+
+## 2. Pairing & auth — trust-on-first-use
+
+1. **Host generates a pairing offer** (`trimwire pair` or "Add device" in the local cockpit):
+ a short-lived (~120s), single-use, high-entropy pairing code + the daemon's TLS cert
+ fingerprint + reachable address, rendered as a **QR** (typeable fallback):
+ `trimwire-pair://?fp=&pc=&v=1`.
+2. **App scans / types**, connects, **pins the fingerprint** (TOFU — rejects on mismatch
+ thereafter), and presents the pairing code over the cert-pinned channel.
+3. **Token exchange:** daemon validates the code (single-use, unexpired), issues a **per-device
+ bearer token** (256-bit, stored hashed server-side), app stores it in the platform secure
+ store (Keychain/Keystore/DPAPI/libsecret). Pairing code is burned.
+4. **Subsequent calls** present `Authorization: Bearer ` over the pinned channel.
+
+This is the **OAuth 2.0 Device Authorization Grant shape** but fully self-contained — the local
+daemon *is* the authorization server; no external IdP, no trimwire-hosted auth. TOFU cert
+pinning + a pairing code is the documented best practice for local daemons that can't run PKI;
+the code defeats a same-LAN attacker who reaches the port but can't see the host's screen.
+
+**Per-device tokens** live in a `devices` table (separate from the content-free ledger):
+`device_id, label, token_hash, created_at, last_seen, expires_at, capabilities, revoked_at`.
+Revocation is immediate (`trimwire devices revoke `) — the server-side token store *is* the
+revocation list (no CRL). Tokens carry `expires_at` (30–90 days, configurable); optionally
+issue short-lived (5–60 min) access tokens derived from the device token to bound replay.
+
+## 3. Threat model
+
+| # | Threat | Mitigation |
+|---|---|---|
+| T1 | OAuth token theft | token never readable via control API (structural); `upstream` not remotely settable; control responses schema-tested to exclude it |
+| T2 | Daemon hijack to redirect upstream | `upstream` immutable from any remote/control path; `config:write` hard deny-lists `server.upstream`; re-pointing stays a local-file + restart op |
+| T3 | MITM on control channel | TLS required for any non-loopback bind; cert-fingerprint pinning (TOFU); mismatch = hard reject |
+| T4 | Replay | short-lived derived access tokens; TLS; optional per-request nonce/timestamp on writes |
+| T5 | Malicious LAN peer | bind opt-in (off by default); pairing code required to mint a token; rate-limit + lockout |
+| T6 | Exposed-port scanning | default loopback (unreachable); when exposed, TLS + valid token (scanner gets 401); `doctor` warns on non-loopback bind |
+| T7 | DNS rebinding / cross-origin (browser cockpit) | strict `Host`/`Origin` allowlist; same-origin token, no ambient auth |
+| T8 | Relay compromise (v3) | zero-knowledge relay — never sees plaintext/token/stats; can DoS but not read |
+| T9 | Pairing-code brute force | ≥40-bit code, ~120s TTL, single-use, attempt lockout, valid only on the pinned channel |
+| T10 | Stolen/lost device | per-device revocation; expiry; secure-store at rest; remote revoke from host |
+| T11 | Scope confusion (read token used for control) | capability scope checked per-endpoint, deny-by-default |
+| T12 | Detection-evasion creep | **forbidden** — remote control is faithful proxy control only; no obfuscation, no upstream spoofing |
+
+## 4. What v1 MUST NOT preclude — requirements for the v1 control API (doc 03)
+
+Each is cheap now, expensive to retrofit. The control-API design (doc 03) already commits to
+these:
+
+- **R1 — Auth-token abstraction.** Every request flows through an `Authenticator` trait;
+ handlers receive an authenticated `Principal`, never raw trust. v1 impl = `LoopbackToken`.
+- **R2 — Bind-address config + explicit opt-in.** Bind is a config field; v1 ships loopback-only
+ and **refuses** any non-loopback bind (error points at the future opt-in).
+- **R3 — TLS-readiness.** The listener is constructed through an abstraction that can wrap a
+ rustls acceptor (config-driven branch). Cert/key paths reserved in config (unused in v1).
+- **R4 — Per-request device identity.** Handlers operate on `Principal { device_id, scopes,
+ channel }`, even when v1 fills a synthetic "local" principal.
+- **R5 — Capability scoping.** Define caps now (`stats:read`, `ledger:read`, `service:toggle`,
+ `profile:switch`, `config:read`, `config:write` [upstream-excluded], `sweep:run`,
+ `summarizer:manage`); each endpoint declares its required cap; deny-by-default.
+- **R6 — Upstream-credential firewall.** Control handlers live in a module that *cannot* import
+ the proxy credential; `config:write` hard deny-lists `server.upstream`; **a CI test asserts no
+ control response field derives from the upstream credential.**
+- **R7 — Host/Origin validation hook** (defeats DNS-rebinding) — built now, even loopback.
+- **R8 — Versioned, content-free, token-free API contract** (`/api/v1`); pairing/`devices`
+ endpoints stubbed/absent in v1 but the version prefix + content-free discipline hold.
+- **R9 — Rate-limit/lockout seam** on the auth layer (no-op/generous in v1).
+- **R10 — Secure local-credential storage** (`0600` token in `~/.trimwire/`) — the convention the
+ `devices` table extends.
+
+## 5. Phasing & the gate at each step
+
+| Phase | Transport / bind | Auth | Gate to ship |
+|---|---|---|---|
+| **v1 — local loopback** | `127.0.0.1` only; refuses non-loopback | local principal (R1) + `0600` token (R10) | seams R1–R10 present & tested; **token-leak CI test is the merge gate** (R6) |
+| **v2a — Direct LAN (opt-in)** | LAN bind + mDNS + self-signed TLS | per-device tokens via QR/pairing TOFU; scopes (R5); revocation/expiry | off by default; refuses to expose without TLS+pairing; Host/Origin active; pairing rate-limit; `doctor` warns |
+| **v2b — BYO overlay (recommended)** | **daemon stays loopback**; user's overlay forwards | same per-device-token layer (defense in depth atop overlay keys) | bind unchanged from v1 (best posture); docs only; pairing still required |
+| **v3 — relay / reverse tunnel** | zero-knowledge rendezvous OR user-owned tunnel; off by default | E2E; per-device tokens unchanged; relay token/content-blind | build only if BYO friction demands it; provably content/token-blind; abuse/DoS plan |
+
+**Gate principle:** every phase is off by default, requires explicit opt-in, and cannot regress
+the token invariant. Each step up the ladder widens *reachability*, never *trust* — the
+auth/scope/credential model is identical from v2 onward; only the transport changes.
+
+## Sources
+
+Tailscale `tsnet` docs + `tailscale-rs` preview; mDNS/DNS-SD references; OAuth 2.0 Device
+Authorization Flow (Descope) + IETF cross-device security BCP; self-signed TLS client-auth /
+fingerprint-pinning patterns; bearer-token best-practice guides. Full URLs in the session
+transcript.
diff --git a/docs/cockpit/07-security-tos-redlines.md b/docs/cockpit/07-security-tos-redlines.md
new file mode 100644
index 0000000..6140964
--- /dev/null
+++ b/docs/cockpit/07-security-tos-redlines.md
@@ -0,0 +1,116 @@
+# 07 — Security, ToS & Red Lines (the disagree-seeking review)
+
+> This doc is deliberately adversarial. While the other docs design an exciting cockpit, this
+> one steelmans the case **against** building it — or against building parts of it — and where
+> it drifts from trimwire's identity. It is anchored in trimwire's *own* code, not speculation.
+> **The red lines here are non-negotiable constraints on everything else in this folder.**
+
+## The dissent in five sentences
+
+1. trimwire's whole value is "one small static binary, no heavy runtime deps, transparent and
+ ToS-compliant." A full-control local API + remote stack + native multi-platform app is the
+ single biggest scope expansion the project could make — driven by a *metaphor* ("cockpit"),
+ not measured user demand.
+2. The codebase already encodes, in `config.rs`, that **the most dangerous thing in the system is
+ "where does the Bearer token get sent"** — a mutating control API is a new write path into
+ exactly that surface, and a remote port is a network-reachable handle on a process holding a
+ Claude OAuth token.
+3. "Remote control of your Claude proxy" reads, to an enforcement team, like the redistributed-
+ token pattern that **already got OpenClaw/OpenCode/Cline killed in 2026** — even if
+ implemented innocently, it moves trimwire from *passive-gray* toward something that *looks*
+ active.
+4. The content-free guarantee holds today because the ledger *literally cannot* hold content;
+ several proposed panes ("preview what's pruned," "inspect sessions") create pressure to read
+ **raw transcripts** into a UI — the one move that breaks it.
+5. The existing `trimwire dashboard` HTML + statusline + CLI already deliver ~90% of the
+ cockpit's read value at ~0% of its risk.
+
+## Risk register
+
+| Risk | Likelihood | Severity | Mitigation |
+|---|---|---|---|
+| Control API exposes a config-write path that can redirect the OAuth token (`upstream`/summarizer URL) | Med | **Critical** | R1: forbid those keys from the API; reuse the existing global-only guard + its regression test |
+| Localhost control port hit by a malicious web page (CSRF → config write / info disclosure) | **High** if unguarded | Critical | R4: per-install bearer secret + Host/Origin checks; loopback bind |
+| "Remote control of your Claude proxy" pattern-matches enforcement → ToS/account action | Med | **Critical** | R6: defer + gate on ToS re-review; v1 loopback-only; keep the "compliant" brand |
+| Preview/inspect pane renders transcript content → breaks content-free guarantee | **High** (UX pull) | High | R7+R8: counts/structure only; ledger-and-`--json`-only data sources; never read live transcripts |
+| Native multi-platform + mobile build/signing burden sinks solo velocity | High | High | ship a PWA; no native app / mobile in the minimal v1 |
+| JS SPA + node_modules reverses the lightweight ethos / adds supply-chain surface | Med | Med | reuse the existing dashboard front-end machinery; no heavy SPA framework; JS is build-time only |
+| Remote relay creates recurring infra cost + uptime/abuse obligation | Med (if built) | High | don't build it in v1; BYO overlay (doc 06) needs no infra |
+| Mutation logic creeps into `gateway.rs`, eroding layer discipline | Med | Med | R9: separate `admin/` module/router; gateway stays non-mutating |
+| Scope absorbs bandwidth AGENTS.md reserves for CODE > DX | High | Med | decouple the cheap read view from the expensive control/remote/app; ship the cheap half first |
+
+## RED LINES (must not be crossed — bind every other doc)
+
+- **R1** — The control API can **never** write `server.upstream` or any summarizer
+ `base_url`/`full_url`. Route any config write through the same global-only guard, or forbid
+ those keys outright.
+- **R2** — The control API **stays bound to `127.0.0.1`** in v1; **no** `0.0.0.0`/LAN bind option
+ ships in v1.
+- **R3** — The API **never** returns the OAuth token, `Authorization` header, or any derived
+ value, and **never** exposes a "test/ping upstream" endpoint that originates a call on the
+ subscription token.
+- **R4** — The API **defends against drive-by browser requests**: a per-install bearer secret +
+ Host/Origin checks on every call. A localhost port is **not** a security boundary against local
+ web pages.
+- **R5** — **No detection-evasion, ever** — no client-fingerprint spoofing, header-order mimicry,
+ or proxy-hiding.
+- **R6** — **Remote control is deferred AND gated on an explicit, written ToS re-review** before
+ any relay/exposure code. "Do not design the relay until we've re-checked enforcement posture."
+ (Designing the *API shape* for remote-additivity, per doc 06 R1–R10, is fine; standing up
+ network infrastructure is not.)
+- **R7** — **No UI pane renders message content, prompts, tool-result bodies, or file paths.**
+ Preview shows counts, byte deltas, strategy names, block *types* — never the bytes.
+- **R8** — The API's data sources are **the ledger + content-free `--json` outputs only.** No
+ reading of `~/.claude/**` transcript bodies into any UI surface.
+- **R9** — The control API lives in its **own module/router**, not inside `gateway.rs` (which the
+ layer rules say must not contain mutation). Don't erode the layer discipline that keeps the
+ core testable.
+
+## Fresh-sources update (doc 10)
+
+A second adversarial pass against 2026 sources added these constraints (full detail +
+citations in [doc 10](10-security-fresh-sources.md)):
+
+- **R4 strengthened:** Host-pin is necessary-but-not-sufficient. Require a **`Sec-Fetch-Site`**
+ gate **and** a **custom preflight-forcing header** on writes, ordered before the token check.
+- **Token-in-HTML is a tradeoff, not solved:** any XSS exfiltrates the control token → require
+ a **strict CSP** (nonces, not `'unsafe-inline'`) or move to an **`HttpOnly` cookie handshake**;
+ native client reads the token from the **OS keychain**.
+- **New ToS note (R6 reinforced):** the 2026-02-20 clarification has **no carve-out for local
+ proxies/middleware**, and the **base passthrough proxy itself** (which modifies request bodies)
+ is grayer now — pre-write the affirmative compliance argument; keep remote hard-gated.
+- **Browser LNA (Chrome 142) does NOT cover localhost→localhost** — don't lean on it.
+
+## The safest minimal viable cockpit (the dissent's counter-proposal)
+
+If trimwire wanted the *smallest* responsible step:
+
+1. A **local-only, read-mostly page served by the binary**, reusing `dashboard.rs` +
+ `dashboard_template.html`. Data: ledger + content-free `--json`. ~90% of the value, ~0% new
+ risk.
+2. **Control limited to three verbs:** on/off; switch profile (`default`⇄`gentle`); a
+ **whitelisted** subset of strategy knobs applied via restart — **never** `upstream`/summarizer
+ endpoints. No general config-write; no sweep-from-UI in the minimal cut (sweep mutates on-disk
+ transcripts — keep it auditable in the CLI initially).
+3. **Ship as a PWA, not a native app.** Zero native build matrix, zero signing, zero app-store
+ tax. No mobile in the minimal v1.
+4. **No remote, at all, in the minimal v1.** Loopback only; design the data model for a future
+ remote phase but don't build (or design the relay for) it until the ToS re-review (R6).
+
+## How the rest of this folder reconciles with the dissent
+
+The plan does **not** ship the maximal version all at once. The reconciliation (see
+[README](README.md) and [roadmap](08-roadmap.md)):
+
+- The **roadmap's v0/v1 ≈ this minimal proposal** — local, content-free, binary-served, bounded
+ control. That's what gets built and proven first.
+- **Full config control, the native app, LAN/overlay remote, and mobile are later phases**, each
+ behind the red lines above and an explicit security/ToS gate.
+- The control-API design (doc 03) and the remote design (doc 06) already adopt R1–R9 as
+ load-bearing requirements — the separate loopback admin listener, the upstream-credential
+ firewall with a CI leak test, Host/Origin validation, content-free events, and the
+ "designed-for-but-deferred" remote seams are direct responses to this review.
+
+So this doc isn't an objection the plan ignores — it's the **constraint system the plan is built
+inside.** If a future change conflicts with a red line here, the red line wins, or the change
+needs explicit maintainer + ToS sign-off.
diff --git a/docs/cockpit/08-roadmap.md b/docs/cockpit/08-roadmap.md
new file mode 100644
index 0000000..d057882
--- /dev/null
+++ b/docs/cockpit/08-roadmap.md
@@ -0,0 +1,116 @@
+# 08 — Roadmap
+
+> The phased plan that ties docs 03–07 together. Each phase is **off by default until the prior
+> one is proven**, ships through the existing PR/CI flow, and is bounded by the red lines in
+> [doc 07](07-security-tos-redlines.md). The destination is the cockpit the maintainer asked for
+> (full control, multi-platform app, phased remote); the sequencing is what makes getting there
+> safe.
+
+## Phase map at a glance
+
+| Phase | Deliverable | Reach | Risk | Gate to start |
+|---|---|---|---|---|
+| **v0** | Read-only cockpit (live, served by the binary) | local loopback | very low | none — it's a reskin of `dashboard` |
+| **v1** | Full **local** control (the control API + Flightdeck UI), **installable as a PWA** | local loopback | low–med | v0 shipped; red lines R1–R9 enforced; token-leak CI test green |
+| **v2** | **Tauri desktop wrapper** of the same PWA (tray, autostart, sidecar) | local | med | v1 shipped; shell is a thin client of the v1 API |
+| **v3** | **Remote** control (BYO overlay + opt-in LAN) — the **PWA over TLS/overlay** | cross-network, opt-in | high | **written ToS re-review (R6)**; remote seams from v1 in place; pairing/devices built |
+| **v4** | **Mobile = the PWA** (Add-to-Home-Screen, no store/fee); native APK only if ever needed | remote | low–med | v3 shipped (for cross-device); **no Apple/Play pipeline required** |
+
+## v0 — Read-only cockpit (the cheap, near-free win)
+
+**What:** upgrade `trimwire dashboard` into a live, locally-served page. Reuse `dashboard.rs` +
+`dashboard_template.html` + the site's dashboard components + design tokens. Data: ledger +
+`stats`/`recall`/`preview --json`. **No control actions**, **no remote**, content-free.
+
+**Why first:** it's ~90% of the perceived value at ~0% new risk, validates the UI/design, and
+ships before any of the contentious surface exists. Maps to the disagree-seeking review's
+"safest minimal" read view (doc 07).
+
+**Backend need:** minimal — could even be the static HTML report plus a tiny read-only loopback
+serve. If building toward v1, start the `admin/` read endpoints here (control-API PR 1, read-only
+subset).
+
+## v1 — Full local control
+
+**What:** the control API (doc 03, PRs 1–5) + the Flightdeck UI (doc 04) with full control:
+on/off, profile switch, validated config + per-strategy knobs (restart-or-hot-reload), preview,
+sweep (dry-run-first), summarizer, share opt-in, live SSE monitor. **Loopback only.**
+
+**Non-negotiables (doc 07):** separate `127.0.0.1:8766` admin listener (R2, R9); bearer token +
+Host/Origin guard (R4); `config:write` deny-lists `server.upstream`/summarizer URLs (R1, R3);
+content-free panes & data sources only (R7, R8); no detection-evasion (R5). **The token-leak CI
+test (doc 06 R6) is the merge gate.**
+
+**Frontend stack decision:** vanilla DOM vs **Svelte** (doc 04 §2). Given the site is being
+rebranded to Svelte+Astro, the recommendation is to build the cockpit in **Svelte** so it shares
+one idiom + component library with the new site — *unless* the cockpit ships before the rebrand,
+in which case start vanilla (a direct lift) and port later (mechanical; data contracts + tokens
+don't change). **Open for maintainer confirmation.**
+
+**PWA in v1:** the binary serves a Web App Manifest + icon (already in the POC), so v1 is
+**installable** — desktop browsers get "Install app", and phones on the same machine/overlay get
+Add-to-Home-Screen. This is the *multi-platform app* for most users; v2 is just a nicer desktop
+shell on top.
+
+**Exit:** people actually use it locally. That usage is the evidence (AGENTS.md: "measure, don't
+guess") that justifies climbing to v2+.
+
+## v2 — Tauri desktop wrapper (optional convenience)
+
+**What:** a Tauri 2 desktop shell wrapping the **same** PWA bundle (doc 05). Shape **A**
+(talk-to-running-daemon) first; optional sidecar mode (B) later. Adds desktop packaging to the
+existing 3-platform CI via `tauri-action`. **Still local** — the shell talks to `127.0.0.1:8766`.
+It adds tray/autostart/sidecar + OS-keychain token storage; it does **not** add a new UI.
+
+**Why after v1, and why optional:** the shell is a thin client of the v1 API, and the PWA already
+covers "an installable app on every platform." Tauri is a desktop *enhancement*, not the thing
+that makes the cockpit multi-platform. The HTTP-first design means v2 is low-regret (if Tauri
+disappoints, the PWA still works).
+
+## v3 — Remote control (the gated leap)
+
+**What:** reach a daemon on another machine. **Primary: BYO overlay (Tailscale/WireGuard)** — the
+daemon stays loopback, the user's overlay forwards (trimwire ships no infra). **Plus opt-in
+Direct LAN** (mDNS + self-signed TLS + QR/pairing-code TOFU). Per-device tokens, capability
+scopes, revocation/expiry (doc 06).
+
+**Hard gate (doc 07 R6):** a **written ToS re-review before any exposure/relay code.** "Remote
+control of the process holding your Claude OAuth token" is the feature most likely to spend
+trimwire's compliance moat — it must be deliberate, off by default, and never expose the token or
+make `upstream` remotely settable. The v1 control API already ships the additive seams (doc 06
+R1–R10) so this is new endpoints (`/pair`, `/devices`) + a transport, not a v2 API.
+
+**Relay (c)/reverse-tunnel (d) are v3.x last resorts** — zero-knowledge, off by default, built
+only if BYO-overlay friction demands it.
+
+## v4 — Mobile (no paid app store)
+
+**What:** a phone client (remote controller) = the **same web frontend as an installable PWA**
+(add-to-home-screen on iOS + Android, $0, no store) — the recommended default. Optionally a
+**Tauri-Android / Capacitor APK** distributed via **GitHub Releases + F-Droid** (also $0, no Play
+account) if a native Android shell is wanted. **No Apple Developer / Google Play fee is required**
+(maintainer constraint), and **Flutter is not the fallback** — it's always the same web frontend
+(doc 05 §4). Depends on v3 (remote) being in place for cross-device control.
+
+## Cross-cutting workstreams
+
+- **Docs:** update `ARCHITECTURE.md` (new `admin/` module + layer rows + decision log),
+ `CONFIGURATION.md` (`[admin]`), `SECURITY.md` (loopback+token+Host-pin model, content-free
+ events) as the relevant phase lands.
+- **Tests:** content-free event test (mirrors `audit.rs`); token-leak CI test; **API contract
+ tests** so the cockpit can't break when CLI commands change (`/api/v1` shape pinned; byte-equal
+ to `--json` from the shared builder — doc 11); atomic-write crash-safety; hot-reload visibility.
+- **Parity oracle:** untouched throughout — the control plane never touches `strategies/` or
+ `pairing/`.
+
+## Decision log (open questions for the maintainer)
+
+1. **Frontend stack:** Svelte (recommended given the site rebrand) vs vanilla-now-port-later?
+2. **How far is v1's config control?** Full validated editor (doc 03) vs the disagree-seeking
+ review's bounded "on/off + profile + whitelisted knobs" (doc 07)? Recommendation: ship the
+ bounded version in v1, expand to the full editor once the write path + restart UX are proven.
+3. **App home:** workspace member vs sibling repo (doc 05 §7)?
+4. **Is mobile (v4) actually wanted,** or is multi-platform satisfied by desktop + PWA?
+5. **Visible name:** Flightdeck / Console / Panel (doc 04 §6)?
+
+These don't block v0/v1 — they shape v2+. None requires answering tonight.
diff --git a/docs/cockpit/09-poc.md b/docs/cockpit/09-poc.md
new file mode 100644
index 0000000..fef1b99
--- /dev/null
+++ b/docs/cockpit/09-poc.md
@@ -0,0 +1,91 @@
+# 09 — Proof of Concept (small, but touches every layer)
+
+> A deliberately small but **end-to-end vertical slice** of the cockpit, built on
+> this branch. It compiles clean (`fmt` / `clippy -D warnings` / full test suite)
+> and runs. It is **off by default** — the daemon is byte-for-byte unchanged unless
+> you opt in via `[admin] enabled = true` or run `trimwire cockpit`.
+
+
+
+*The running cockpit: teal Flightdeck brand, a live "gateway serving" indicator,
+content-free KPI cards from the local ledger, and a working SSE feed — served by
+the trimwire binary on a loopback-only control API.*
+
+## What the POC includes (every layer, minimally)
+
+| Layer (plan doc) | POC artifact | State |
+|---|---|---|
+| **Control API** (doc 03) | `src/admin/mod.rs` — separate **loopback admin listener** (`127.0.0.1:8766`), kept off the token-bearing gateway port | real |
+| ↳ Auth seam (doc 06 R1) | `Authenticator` trait + `LoopbackToken` impl; 256-bit bearer token at `~/.trimwire/control.token` (`0600`), constant-time compare | real |
+| ↳ DNS-rebind / CSRF guard (doc 03 §5, doc 06 R7) | authority (`Host`/h2 `:authority`) allowlist + same-origin `Origin` + `Sec-Fetch-Site` — three gates, ordered before auth | real |
+| ↳ Token-page XSS hardening (doc 10 G3) | per-render **nonce CSP** (no `'unsafe-inline'`, `worker-src 'none'`) + `X-Frame-Options: DENY` on the token-bearing HTML | real |
+| ↳ Read endpoints | `GET /api/v1/{health,version,service,stats}` — `stats` reuses the content-free ledger `Report`; `version` omits `upstream` | real |
+| ↳ Live events (doc 03 §4) | `GET /api/v1/events` — SSE, content-free aggregate snapshot | real (one-shot; prod uses a broadcast channel) |
+| **Web cockpit** (doc 04) | `src/admin/cockpit.html` — embedded single-file UI, teal design tokens, same-origin token bootstrap, KPIs + SSE log | real (vanilla; prod may use Svelte) |
+| ↳ **PWA install** (doc 05 — PWA-first) | served `GET /manifest.webmanifest` + `GET /icon.svg`, linked from the HTML → **installable** ("Install app" / Add-to-Home-Screen, no store) | real (manifest+icon; service worker is a follow-up) |
+| ↳ CLI surface | `trimwire cockpit` subcommand + `[admin]` config section | real |
+| **Multi-platform app** (doc 05) | **PWA-primary** (the page above is the app on every platform) + `app/` Tauri 2 desktop-shell scaffold | PWA real; Tauri scaffold (not in CI) |
+| **Remote** (doc 06) | non-loopback bind **refused** at startup; auth/identity seam present | seam only (deferred, by design) |
+| **Security red lines** (doc 07) | loopback-only, token never exposed, `upstream` never written **nor returned**, content-free responses, separate module (not in `gateway.rs`) | enforced |
+
+## Run it
+
+```bash
+# Easiest: one command starts the gateway + the control API + the web UI.
+trimwire cockpit
+# [cockpit] open http://127.0.0.1:8766 in your browser
+
+# Or enable it permanently for the always-on daemon:
+# ~/.config/trimwire.toml
+# [admin]
+# enabled = true
+# listen = "127.0.0.1:8766"
+```
+
+The control token is printed/located at `~/.trimwire/control.token`. The browser UI
+is handed it same-origin at page load; CLI/curl callers pass
+`Authorization: Bearer `.
+
+## Verified behaviour (smoke test)
+
+```
+GET /api/v1/health → 200 {"ok":true,"version":"0.3.16"} (unauth)
+GET /api/v1/stats (no token) → 401 unauthorized
+GET /api/v1/stats (Bearer token) → 200 full content-free ledger Report
+GET /api/v1/version (Bearer token) → 200 {version, control_api, profile, listen addrs}
+ (NO `upstream` — credential routing never crosses the wire)
+GET /api/v1/service (Bearer token) → 200 {serving:true, ...} (live gateway probe)
+GET /api/v1/health (Host: evil.com) → 403 forbidden: bad Host (DNS-rebind guard)
+GET /api/v1/health (Origin: evil) → 403 forbidden: bad Origin (cross-origin guard)
+GET / → HTML, token injected, nonce-CSP (no `'unsafe-inline'`)
+```
+
+Plus 13 unit tests in `src/admin/mod.rs` (constant-time compare, authority/Origin/
+`Sec-Fetch-Site` guards, bearer extraction, token generation = 256-bit hex + stable +
+concurrency-safe, authenticator accept/reject, `/version` contract incl. **`upstream`
+absent**, `/stats` content-free, PWA manifest, nonce-CSP, HTML-placeholder guard) and a
+`[admin]`-global-only regression test in `src/config.rs`. Full suite stays green:
+`cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test --all-features`.
+
+## What is deliberately NOT in the POC
+
+- No config **write** / hot-reload (read + control-shape only); no `ArcSwap` refactor.
+- No socket-activation for the admin port (plain loopback bind).
+- No real streaming SSE (one-shot snapshot; `EventSource` reconnects make it live-ish — and
+ each reconnect re-runs a ledger `report()` query, so an open tab polls ~every 2s. Production
+ replaces this with a broadcast channel fed at ledger-write time, doc 03 §4).
+- No custom **`X-Trimwire-Control`** preflight-forcing header — not needed yet (the POC is
+ read-only + token-gated), but **required before any mutating endpoint** (`POST /service/*`,
+ `PUT /config`) lands, per doc 10 G1.
+- No remote transport, pairing, or `devices` table (deferred phase — only the seams).
+- The Tauri `app/` is a scaffold: it shows the shape but is not built by CI and needs
+ the Tauri toolchain to run.
+
+These are the exact follow-ups itemized in docs 03/06/08 — the POC is the first
+slice of that plan, not a shortcut around it.
+
+## New dependencies
+
+**None.** The control API reuses `hyper` / `http-body-util` / `tokio` / `serde_json`
+/ `getrandom` / `hex` already in the dependency tree — honoring the single-static-
+binary, no-heavy-runtime-deps ethos.
diff --git a/docs/cockpit/10-security-fresh-sources.md b/docs/cockpit/10-security-fresh-sources.md
new file mode 100644
index 0000000..7fadf10
--- /dev/null
+++ b/docs/cockpit/10-security-fresh-sources.md
@@ -0,0 +1,133 @@
+# 10 — Security: Fresh-Sources Addendum (2026 review)
+
+> A second, adversarial research pass against the plan using sources the other docs
+> do **not** cite (independent / 2025–2026-dated). It found four things the plan got
+> right, and several real gaps. The corrections below are folded back into docs 03,
+> 04, 07, 02 by reference, and the cheap ones are already implemented in the POC
+> (doc 09). Citations are inline.
+
+## What the plan got right (confirmed)
+
+1. **Treating localhost as hostile is correct.** The Ollama DNS-rebinding CVE
+ (CVE-2024-28224) is a localhost daemon with *no* Host pinning getting full remote
+ API access from a malicious web page — exactly the attack the Host-pin guards.
+ ([NCC Group](https://www.nccgroup.com/research/technical-advisory-ollama-dns-rebinding-attack-cve-2024-28224/))
+2. **"Bearer token, not a cookie → CSRF structurally prevented" is the strongest part
+ of the design** — a token in an `Authorization` header is not ambient/auto-attached.
+ ([OWASP CSRF](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html))
+3. **Serving the UI same-origin on the admin port** (CORS stays default-deny) is right,
+ and `http://localhost` / `127.0.0.0/8` are "potentially trustworthy" secure contexts,
+ so the page can use SubtleCrypto and isn't mixed-content-blocked.
+ ([W3C Secure Contexts](https://www.w3.org/TR/secure-contexts/))
+4. **"Defer remote, stay loopback, no detection-evasion" (R5/R6) is vindicated by 2026
+ events** — Anthropic's Jan–Apr 2026 crackdown killed exactly the named tools.
+ ([The Register, 2026-02-20](https://www.theregister.com/2026/02/20/anthropic_clarifies_ban_third_party_claude_access/))
+
+## Gaps and corrections
+
+### G1 — Host-pin alone is too thin; add two more independent gates *(highest priority)*
+
+DNS rebinding **defeats Origin checks** (the attacker's page origin is unchanged; only
+the resolved IP flips), so Host-pin is the *only* barrier in the original design — and a
+single allowlist bug collapses it. A cross-site **simple request** (`GET`/form `POST`)
+reaches localhost with **no preflight**, so a *blind write* needs no response-read.
+
+**Mitigations (defense-in-depth, independent of Host-pin correctness):**
+- **`Sec-Fetch-Site` enforcement** — browser-set, page-unforgeable; reject anything not
+ `same-origin`/`none`. **Implemented in the POC** (`src/admin/mod.rs` `sec_fetch_site_ok`).
+- **A custom non-simple header on mutating endpoints** (e.g. `X-Trimwire-Control: 1`) to
+ force a CORS preflight that default-deny CORS fails. The POC is read-only + token-gated,
+ so this is a **production requirement** for when `POST /service/*` / `PUT /config` land.
+- **Order all gates before the token compare and before any side effect** — a rebinding
+ caller never reaches auth even if the token check had a bug. **Done in the POC.**
+
+([OWASP CSRF](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html),
+[Mixmax: CORS-as-CSRF](https://www.mixmax.com/engineering/modern-csrf))
+
+### G2 — Browser Local Network Access does NOT cover localhost→localhost
+
+Chrome shipped **Local Network Access** (LNA, ex-PNA) in **Chrome 142 (2025-10-28)**,
+gating *public-site → loopback* behind a permission prompt — good for the public drive-by
+case. **But the same announcement says localhost→localhost is NOT yet gated**, and the old
+PNA CORS preflight was **withdrawn**. So: a malicious *other localhost app* is unchanged by
+LNA, and the plan must **not** lean on browser network-access prompts — the G1 gates carry
+the sibling-localhost threat. ([Chrome for Developers](https://developer.chrome.com/blog/local-network-access),
+[chromestatus](https://chromestatus.com/feature/5152728072060928))
+
+### G3 — "Token injected into served HTML" is a tradeoff, not "solved"
+
+Any **XSS in the cockpit UI exfiltrates the control token** (then full control). Precedent
+is mixed (VS Code's connection-token is widely seen as the weak link). Corrections:
+- **Mandatory strict CSP** (nonces, not `'unsafe-inline'`) on the token-bearing page.
+ **Done in the POC:** the cockpit HTML is served with a **per-render nonce CSP**
+ (`script-src 'nonce-…'; style-src 'nonce-…'; worker-src 'none'`, no `'unsafe-inline'`) via
+ `html_response` in `src/admin/mod.rs`; an injected `
+