diff --git a/CLAUDE.md b/CLAUDE.md index 43f04fd5..608c9826 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -187,11 +187,13 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L |-------|---------| | **Auth** | Optional HTTP Basic via `CODEMAN_USERNAME` (defaults to `admin`) / `CODEMAN_PASSWORD` env vars. Active only when `CODEMAN_PASSWORD` is set (`middleware/auth.ts`) | | **Network bind** | Defaults to `127.0.0.1` (loopback). A non-loopback bind (`--host`/`CODEMAN_HOST`) without `CODEMAN_PASSWORD` **starts but warns loudly** (0.9.0; was fail-closed in COD-29/#107). `--allow-unauthenticated-network` / `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1` acknowledges the warning. Classifier: `network-auth-policy.ts` | +| **Host guard** | Always-on Host-header allowlist blocks DNS rebinding (RCE on the default no-auth loopback install). Allows loopback, any IP literal, the bind host, `*.ts.net`/`*.trycloudflare.com`/`*.cfargotunnel.com`, the active managed tunnel, and `CODEMAN_ALLOWED_HOSTS`. ⚠️ **Custom reverse-proxy domains are rejected** unless added via `CODEMAN_ALLOWED_HOSTS=host,.suffix`. `registerHostGuard` in `server.ts`; policy in `network-auth-policy.ts` (`buildHostPolicy`/`isAllowedRequestHost`/`isAllowedRequestOrigin`) | +| **CSRF / Origin** | Always-on cross-site Origin guard rejects state-changing requests from foreign origins (covers self-update, session create/input, settings/tunnel toggles). **A missing Origin is allowed** so curl/CLI and Claude Code hooks keep working. The global body parser keeps `text/plain` RAW (no auto-JSON-parse, which had enabled simple-request CSRF); `/api/crash-diag` self-parses. WebSocket upgrade validates Origin+Host (anti-CSWSH) in `ws-routes.ts`. Added in `c669518` (closes 2026-06-09 review CRITICALs) | | **QR Auth** | Single-use 6-char tokens (60s TTL) for tunnel login. See `docs/qr-auth-plan.md` | | **Sessions** | 24h cookie (`codeman_session`), auto-extend, device context audit | | **Rate limit** | 10 failed auth/IP → 429 (15min decay). QR has separate limiter | | **Hook bypass** | `/api/hook-event` exempt from auth (localhost-only, schema-validated) | -| **Env vars** | `CODEMAN_MUX` (managed session), `CODEMAN_API_URL` (auto-set for hooks) | +| **Env vars** | `CODEMAN_MUX` (managed session), `CODEMAN_API_URL` (auto-set for hooks), `CODEMAN_ALLOWED_HOSTS` (extra Host/Origin allowlist entries for reverse proxies, comma-separated; bare `.suffix` matches subdomains) | | **Validation** | Zod schemas, path allowlist regex, `CLAUDE_CODE_*` env prefix allowlist | | **Headers** | CORS localhost-only, CSP, X-Frame-Options, HSTS if HTTPS | diff --git a/README.md b/README.md index 6e88561c..4f3f9bc7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

The missing control plane for AI coding agents

- Agent Visualization • Zero-Lag Input Overlay • Mobile-First UI • Respawn Controller • Multi-Session Dashboard + Agent Visualization • Zero-Lag Input • Autonomous Orchestrator • Respawn Controller • Mobile-First UI • Hardened Security

@@ -13,7 +13,7 @@ Node.js 18+ TypeScript 5.9 Fastify - Tests + Tests

@@ -34,7 +34,7 @@ You'll need at least one AI coding CLI installed — [Claude Code](https://docs. ```bash codeman web -# Open http://localhost:3000 — press Ctrl+Enter to start your first session +# Open http://localhost:3000 and start your first session ```

@@ -177,6 +177,8 @@ Watch background agents work in real-time. Codeman monitors agent activity and d - **Auto-behavior** — windows auto-open on spawn, auto-minimize on completion, tab badge shows "AGENT" or "AGENTS (n)" count - **Nested agents** — supports 3-level hierarchies (lead session -> teammate agents -> sub-subagents) +**Agent Teams** — first-class support for Claude Code's native multi-agent teams (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`). `TeamWatcher` polls `~/.claude/teams/`, matches teammates to their lead session, and surfaces them as live subagent windows with **team-aware idle detection** — so the Respawn Controller won't fire while teammates are still working. See [`docs/agent-teams/`](docs/agent-teams/). + --- ## Zero-Lag Input Overlay @@ -214,6 +216,20 @@ WATCHING → IDLE DETECTED → SEND UPDATE → /clear → /init → CONTINUE → --- +## Orchestrator Loop + +Beyond single-session respawn, the **Orchestrator** turns a high-level goal into a phased plan and drives it to completion across multiple agents — a state machine that runs `idle → planning → approval → executing → verifying → (replanning) → completed`. + +- **Plan, then execute** — generates a phased plan from your goal and pauses for approval before touching anything; reject with feedback to regenerate +- **Per-phase verification gates** — each phase is verified before the next begins; on failure the orchestrator replans instead of barreling ahead +- **Multi-agent execution** — fans phases out to team agents / a task queue, coordinating work too big for one session +- **Crash-safe** — full state persists under the `orchestrator` key in `state.json`, so it survives restarts +- **Driven from the UI or API** — the Orchestrator panel, or `POST /api/orchestrator/start` → `/approve` → `/status` (10 endpoints) + +> Distinct from Ralph (a single-session autonomous loop): the orchestrator coordinates multi-phase, multi-agent execution. Full design: [`docs/orchestrator-loop-architecture.md`](docs/orchestrator-loop-architecture.md). + +--- + ## Multi-Session Dashboard Run **20 parallel sessions** with full visibility — real-time xterm.js terminals at 60fps, per-session token and cost tracking, tab-based navigation, and one-click management. @@ -270,6 +286,20 @@ PTY Output → 16ms Server Batch → DEC 2026 Wrap → SSE → Client rAF → xt --- +## More Features + +- **Self-update** — git-clone installs under systemd/launchd update in place from **App Settings → Updates**: it detects the latest release, auto-stashes a dirty tree, and streams build progress across the service restart (npm installs report as non-updatable) +- **Dual-CLI** — run **Claude Code** or **OpenCode** per session; env-var prefixes auto-gate (`CLAUDE_CODE_*` vs `OPENCODE_*`). See [`docs/opencode-integration.md`](docs/opencode-integration.md) +- **Effort & Ultracode** — set a per-session default effort (`low`–`max`) or enable **ultracode** (dynamic multi-agent workflows). Soft defaults only — switchable anytime with `/effort` in-session. Extended-thinking budget is configurable too +- **Voice input** — dictate prompts with Deepgram Nova-3 (Web Speech API fallback): toggle recording, auto-silence stop, live level meter (`Ctrl+Shift+V`) +- **Image input** — paste or drag-and-drop images straight into a session +- **Gesture control** *(opt-in)* — a MediaPipe hand-tracking overlay to grab/drag session windows and pinch buttons, hands-free. Enable with `CODEMAN_GESTURE=1` + App Settings → Display +- **Multi-monitor span** *(macOS)* — one click opens a browser window maximized across all displays, so floating agent/gesture panels can cross the physical seam +- **CJK / IME input** — full composition support for Chinese / Japanese / Korean +- **OS notifications & hostname-aware titles** — desktop alerts and tab titles are prefixed `codeman:` so multi-host setups stay unambiguous + +--- + ## Remote Access — Cloudflare Tunnel Access Codeman from your phone or any device outside your local network using a free [Cloudflare quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) — no port forwarding, no DNS, no static IP required. @@ -391,6 +421,41 @@ When someone authenticates via QR, the desktop shows a notification toast with t --- +## Security + +Codeman launches sessions with `--dangerously-skip-permissions`, so the web UI is by design a remote-code-execution surface for whoever can reach it — the whole security model exists to control *who* that is. Recent hardening (v0.9.0 + v0.9.5) closes the browser-driven attack paths that bite self-hosted dev tools. Full model: [`docs/security-architecture.md`](docs/security-architecture.md). + +### Network & access + +- **Loopback by default** — binds `127.0.0.1`, reachable only from the same machine, so the no-password default is safe out of the box. Binding a non-loopback host without `CODEMAN_PASSWORD` *starts but prints a loud warning* with three concrete fixes (set a password, loopback + an authenticated tunnel, or explicitly acknowledge with `--allow-unauthenticated-network`) +- **Optional auth, real sessions** — HTTP Basic via `CODEMAN_USERNAME` (default `admin`) / `CODEMAN_PASSWORD`. Success issues an opaque 256-bit `codeman_session` cookie (`randomBytes(32)`) — validated server-side, not client-signed, so it can't be forged offline (24h TTL, auto-extend, device-context audit log) +- **Per-IP rate limiting** — 10 failed attempts → `429` with `Retry-After` (15-min decay). A valid cookie or correct password recovers *immediately* even while an attacker hammers the same IP — important because all tunnel traffic shares one loopback IP. QR auth has its own separate limiter + +### Always-on browser hardening (v0.9.5) + +These run for **every** request — before auth, even on the default no-password loopback install: + +- **Host-header allowlist → blocks DNS rebinding.** A custom domain rebound to `127.0.0.1` is rejected with `403 host not allowed` before any handler runs. Allowed: `localhost`, any IP literal, the bind host, `.ts.net` / `.trycloudflare.com` / `.cfargotunnel.com`, the active managed tunnel, and `CODEMAN_ALLOWED_HOSTS` (add custom reverse-proxy domains here — comma-separated; exact host or leading-dot `.suffix` for subdomains) +- **Cross-site Origin / CSRF guard.** On state-changing methods (`POST`/`PUT`/`PATCH`/`DELETE`) the `Origin` must pass the same allowlist, else `403 cross-site request blocked`. A *missing* Origin is allowed (so `curl`, the CLI, and Claude Code hooks keep working); only a present-but-foreign or opaque `null` origin is rejected +- **Raw `text/plain` bodies.** The global parser no longer JSON-parses `text/plain`, closing the CORS "simple request" CSRF vector where a cross-site `fetch` could smuggle JSON into a write route with no preflight +- **WebSocket origin validation.** The terminal WS upgrade runs the same Host + Origin check and closes with code `4003` on failure (anti-CSWSH) +- **XSS-escaped agent output.** AI-derived strings (tool names, command arguments, subagent descriptions) are HTML-escaped at every injection site before rendering in the subagent / activity panels + +### Input, files & headers + +- **Schema-validated inputs** — every API body is checked with Zod v4 schemas; a `CLAUDE_CODE_*` / `OPENCODE_*` env-prefix allowlist gates which settings each CLI can receive +- **Path containment** — file routes `realpath` before boundary checks (no TOCTOU); `..`, absolute paths, and symlinks resolving outside the working dir are rejected. Caps: 10 MB text preview / 50 MB raw & download; `/api/download` blocklists sensitive paths (`.env`, `*credentials*`, `~/.ssh/`, `.aws/credentials`). SVG/HTML is served `octet-stream` + `nosniff` + attachment so it downloads rather than executes +- **Security headers** — `Content-Security-Policy` (`default-src 'self'`, every exception enumerated), `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, HSTS over HTTPS, and CORS reflected **only** for `localhost` / `127.0.0.1` / `::1` + +### Supply chain & isolation + +- **Pinned & verified deps** — security-sensitive transitive deps are forced to patched versions via npm `overrides`; lockfile integrity is checked on every commit/PR (all entries resolve to `registry.npmjs.org` with `sha512` hashes). Public assets are NUL-byte-scanned and `node --check`-validated in CI +- **Multi-instance isolation** — `CODEMAN_INSTANCE` scopes both the tmux socket (`-L codeman-`) and data dir (`~/.codeman-`) so two instances never attach each other's live sessions + +> Mobile login uses single-use, 60-second QR tokens — see [QR Code Authentication](#qr-code-authentication) above for the full design (it addresses all 6 flaws from USENIX Security 2025's QR-login study). + +--- + ## SSH Alternative (`sc`) If you prefer SSH (Termius, Blink, etc.), the `sc` command is a thumb-friendly session chooser: @@ -407,24 +472,28 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De ## Keyboard Shortcuts +> Ctrl bindings also accept Cmd on macOS. + | Shortcut | Action | |----------|--------| -| `Ctrl+Enter` | Quick-start session | -| `Ctrl+W` | Close session | -| `Ctrl+Tab` | Next session | +| `Ctrl/Cmd+W` | Kill active session | +| `Ctrl/Cmd+Tab` | Next session | | `Alt+1`–`Alt+9` | Switch to tab N | | `Ctrl+Shift+{` / `Ctrl+Shift+}` | Move active tab left / right | -| `Ctrl+K` | Kill all sessions | -| `Ctrl+L` | Clear terminal | +| `Ctrl/Cmd+L` | Clear terminal | | `Ctrl+Shift+R` | Restore terminal size | | `Ctrl+Shift+V` | Toggle voice input | -| `Ctrl/Cmd +/-` | Font size | -| `Escape` | Close panels | +| `Ctrl/Cmd +` / `-` | Font size | +| `Ctrl/Cmd+?` | Keyboard help | +| `Shift+Enter` | Insert newline (sent to terminal) | +| `Escape` | Close panels & modals | --- ## API +REST over Fastify — **~140 handlers across 15 route modules**, plus an SSE stream and a WebSocket terminal channel. A representative subset: + ### Sessions | Method | Endpoint | Description | |--------|----------|-------------| @@ -446,6 +515,14 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De | `GET` | `/api/sessions/:id/ralph-state` | Get loop state + todos | | `POST` | `/api/sessions/:id/ralph-config` | Configure tracking | +### Orchestrator +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/api/orchestrator/start` | Start orchestration from a goal | +| `POST` | `/api/orchestrator/approve` | Approve the generated plan | +| `GET` | `/api/orchestrator/status` | Current phase + progress | +| `POST` | `/api/orchestrator/stop` | Stop and clean up | + ### Subagents | Method | Endpoint | Description | |--------|----------|-------------| @@ -460,6 +537,8 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De | `GET` | `/api/events` | SSE stream | | `GET` | `/api/status` | Full app state | | `POST` | `/api/hook-event` | Hook callbacks | +| `GET` | `/api/system/update/check` | Check for a new release | +| `POST` | `/api/system/update` | Self-update (git-clone installs) | | `POST` | `/api/clipboard` | Push text to all connected browsers (`{text}`) | | `GET` | `/api/sessions/:id/run-summary` | Timeline + stats | @@ -481,11 +560,13 @@ flowchart TB S1["Session (PTY)"] S2["Session (PTY)"] RC["Respawn Controller"] + ORC["Orchestrator Loop"] end subgraph Detection["Detection Layer"] RT["Ralph Tracker"] SW["Subagent Watcher
~/.claude/projects/*/subagents"] + TW["Team Watcher
~/.claude/teams/*"] end subgraph Persistence["Persistence Layer"] @@ -505,14 +586,17 @@ flowchart TB SM --> S1 SM --> S2 SM --> RC + SM --> ORC SM --> SS S1 --> RT S1 --> SCR S2 --> SCR RC --> SCR + ORC --> SCR SCR --> CLI SW --> BG SW --> SSE + TW --> SSE ``` --- @@ -537,13 +621,13 @@ The codebase went through a comprehensive 7-phase refactoring that eliminated go | Phase | What changed | Impact | |-------|-------------|--------| | **Performance** | Cached endpoints, SSE adaptive batching, buffer chunking | Sub-16ms terminal latency | -| **Route extraction** | `server.ts` split into 13 domain route modules + auth middleware + port interfaces | **−60%** server.ts LOC (6,736 → 2,697) | -| **Domain splitting** | `types.ts` → 14 domain files, `ralph-tracker` → 7 files, `respawn-controller` → 5 files, `session` → 6 files | No more god files | -| **Frontend modules** | `app.js` → 9 extracted modules (constants, mobile, voice, notifications, keyboard, CJK input, API, Ralph wizard, subagent windows) | **−24%** app.js LOC (15.2K → 11.5K) | -| **Config consolidation** | ~70 scattered magic numbers → 9 domain-focused config files | Zero cross-file duplicates | +| **Route extraction** | `server.ts` split into 15 domain route modules + auth middleware + port interfaces | **−67%** server.ts LOC (6,736 → 2,254) | +| **Domain splitting** | `types.ts` → 16 domain files, `ralph-tracker` → 7 files, `respawn-controller` → 5 files, `session` → 6 files | No more god files | +| **Frontend modules** | `app.js` → 18 extracted modules across infra, domain & feature layers | app.js core down to **~3.4K LOC** | +| **Config consolidation** | ~70 scattered magic numbers → 10 domain-focused config files | Zero cross-file duplicates | | **Test infrastructure** | Shared mock library, 12 route test files, consolidated MockSession | Testable route handlers via `app.inject()` | -Full details: [`docs/code-structure-findings.md`](docs/code-structure-findings.md) +Full details: [`docs/archive/code-structure-findings.md`](docs/archive/code-structure-findings.md) --- diff --git a/docs/reports/security-review-2026-06-09.md b/docs/reports/security-review-2026-06-09.md index 52e85f0f..191cea78 100644 --- a/docs/reports/security-review-2026-06-09.md +++ b/docs/reports/security-review-2026-06-09.md @@ -1,5 +1,16 @@ # Codeman Security Review — 2026-06-09 +> **⚠️ Remediation status (updated 2026‑06‑09):** the two CRITICALs and 5 of the 7 +> HIGHs below were **fixed the same day in commit `c669518` (shipped as 0.9.5)** — +> an always‑on `Host`‑header + cross‑site `Origin` allowlist (`registerHostGuard`), +> a raw `text/plain` body parser, a WebSocket `Origin`/`Host` check, and +> HTML‑escaped subagent‑panel sinks. **The present‑tense "is exploitable" wording +> below describes the pre‑fix v0.9.4 state.** Still open: **H2** (the self‑updater +> trusts an unsigned git tag — needs signing infra) and dropping CSP +> `'unsafe-inline'` (needs a nonce migration; H4's escaping already neutralises the +> known XSS). Per‑finding breakdown in the *Implementation status* section below; +> regression tests in `test/network-host-guard.test.ts`. + **Scope:** whole codebase (branch `master`, v0.9.4). Adversarial multi-agent review: 10 dimension specialists → diverse-lens skeptic verification of every finding (HIGH/CRITICAL got 3 independent refutation passes) → completeness-critic sweep. 47 raw findings → **25 survived verification** (+1 from the critic). 22 were refuted (mostly "already inside the OS trust boundary" same-uid claims and doc-accuracy nits). Several exploits were **confirmed live** with `curl` against throwaway test ports. ## TL;DR — the one thing that matters diff --git a/docs/security-architecture.md b/docs/security-architecture.md index 79637314..3b3b20bf 100644 --- a/docs/security-architecture.md +++ b/docs/security-architecture.md @@ -177,8 +177,52 @@ requests as local: up (it does not gate the hook‑event exemption, but it gates everything else and is the documented practice). Prefer `tailscale serve` (below), which authenticates at the tailnet layer so untrusted clients never reach the loopback port at all. -A future hardening could gate the hook‑event exemption on a shared secret while a -tunnel is active. + +### Host‑header & Origin allowlist (DNS‑rebinding & CSRF defense) + +Since **0.9.5** an **always‑on** `onRequest` hook (`registerHostGuard`, +`src/web/middleware/auth.ts`; policy in `src/web/network-auth-policy.ts`) runs +**before** the auth pipeline in §2 and guards **every** request — including the +localhost‑only exemptions above, SSE, the WebSocket upgrade, and static files. It +closes the browser‑driven RCE path (DNS rebinding plus a cross‑site `text/plain` +`POST`) that the loopback‑no‑password default otherwise exposed to any site the +operator merely visits. + +- **Host allowlist (anti‑DNS‑rebinding).** The `Host` header is validated on + **every** request, all methods. A custom domain rebound to `127.0.0.1` is + rejected with `403 Forbidden: host not allowed` before any handler runs. Allowed: + `localhost`; **any** IP literal (IPv4/IPv6 — a browser hitting a numeric address + can't be a rebinding victim); the bind host; the suffixes `.ts.net`, + `.trycloudflare.com`, `.cfargotunnel.com`; the hostname of the active + Codeman‑managed tunnel; and anything in `CODEMAN_ALLOWED_HOSTS`. A missing/empty + `Host` is rejected. +- **Origin / CSRF guard.** On **state‑changing** methods (everything except + `GET`/`HEAD`/`OPTIONS`) the `Origin` header must also pass the same allowlist, + else `403 Forbidden: cross‑site request blocked`. A **missing `Origin` is + allowed** (so `curl`, the CLI, and Claude Code hooks keep working); only a + present‑but‑foreign origin — or the opaque `null` origin (sandboxed iframe) — is + rejected. This blocks the cross‑site CSRF that could previously create sessions, + trigger self‑update, or flip `tunnelEnabled`. +- **Raw `text/plain` bodies.** The global `text/plain` content‑type parser no + longer JSON‑parses bodies — it hands handlers the raw string (`/api/crash-diag` + self‑parses its beacon payload). This removes the CORS "simple request" CSRF + vector, where a cross‑site `fetch` with `Content-Type: text/plain` smuggled a + JSON body into a write route with no preflight — defense‑in‑depth alongside the + Origin guard. +- **WebSocket upgrades.** The terminal WS upgrade (`src/web/routes/ws-routes.ts`) + runs the **same** Host + Origin check and closes with code `4003` on failure + (anti‑CSWSH). + +The policy is rebuilt per request from +`buildHostPolicy(bindHost, tunnelManager.getUrl())`, so starting or stopping a +tunnel at runtime updates the allowlist with no restart. + +> **Reverse‑proxy operators:** a custom proxy domain (e.g. `codeman.example.com`) +> is **not** in the default allowlist and gets `403 host not allowed`. Add it via +> `CODEMAN_ALLOWED_HOSTS` — comma‑separated, case‑insensitive; an exact hostname +> matches only itself, while a leading‑dot entry (`.corp.internal`) matches the +> bare domain **and** all subdomains. Behaviour is covered by +> `test/network-host-guard.test.ts`. --- @@ -342,6 +386,12 @@ production layout (`~/.codeman`, `-L codeman`, port 3000). (CDN fallback for a few libraries). `script-src` and `style-src` additionally allow `'unsafe-inline'` — relevant to the SVG/HTML handling in §5, where the `octet-stream` + `nosniff` download (not the CSP) is what blocks execution. + Because `'unsafe-inline'` is still present (removing it needs a nonce + migration), AI‑derived strings rendered into the subagent/activity panels are + HTML‑escaped at the injection sites (`escapeHtml` in + `src/web/public/constants.js`; sinks in `panels-ui.js` / `subagent-windows.js`) + so a hostile tool name or argument can't execute — defense‑in‑depth from the + 2026‑06‑09 review (H4). - `connect-src` allows `wss://api.deepgram.com` (streaming voice input). - `img-src` allows `data:` and `blob:` (inline / generated images, QR codes). - `frame-ancestors 'self'`. @@ -367,6 +417,7 @@ production layout (`~/.codeman`, `-L codeman`, port 3000). |------------|--------| | `CODEMAN_PASSWORD` (+ `CODEMAN_USERNAME`) | Enable HTTP Basic auth | | `--host` / `CODEMAN_HOST` | Bind host (default `127.0.0.1`) | +| `CODEMAN_ALLOWED_HOSTS` | Extra `Host`/`Origin` allowlist entries for reverse proxies (comma‑separated; exact host, or leading‑dot `.suffix` for subdomains) — see §3 | | `--allow-unauthenticated-network` / `CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK` | Acknowledge an unauthenticated non‑loopback bind (downgrades the warning) | | `--https` | Enable TLS (adds HSTS) | | `CODEMAN_INSTANCE` | Scope tmux socket + data dir for isolation | @@ -379,9 +430,9 @@ production layout (`~/.codeman`, `-L codeman`, port 3000). | Concern | File | |---------|------| -| Bind‑host classification, env‑flag parsing | `src/web/network-auth-policy.ts` | +| Bind‑host classification, env‑flag parsing, Host/Origin allowlist (`buildHostPolicy` / `isAllowedRequestHost` / `isAllowedRequestOrigin`) | `src/web/network-auth-policy.ts` | | Start‑and‑warn policy | `src/web/server.ts` (`WebServer.start()`) | -| Auth pipeline, rate limiting, security headers, CORS | `src/web/middleware/auth.ts` | +| Auth pipeline, rate limiting, security headers, CORS, Host/Origin guard (`registerHostGuard`) | `src/web/middleware/auth.ts` | | File‑path containment (realpath‑before‑check) | `src/web/route-helpers.ts` (`validateSessionFilePath`) | | File routes, caps, SVG handling, download blocklist | `src/web/routes/file-routes.ts` | | Instance/socket/data‑dir scoping | `src/config/instance.ts` |