Gate network bypasses in web

Task 12471 · Queued · Coder · created · updated

## Context (BA Task 12447 → Architecture Task 12448 → Plan Task 12449) Original request (verbatim): "Regarding task <https://app.jaah.ai/tasks/12393> This is a repeat situation with the same user apparently behind a broken corporate firewall. He says that previous fix helped and his task list is no longer causing trouble, but he was now struggling with Projects edit. Go over all UI surfaces and ensure that the same code is reused everywhere - one choke point - that works identically and uses the approach and protocols that are now proven working. If there will be other similar issues we would only have to fix it in one place" Root cause (Architecture §0, AC-0): the hang was a WRITE — `PUT /api/projects/54` sat on a dead direct HTTP/2 connection (prod `activity_events` 1661992/3; every page read was relay-rescued, `client_events` 1335-1345). Writes are bare `fetch` with no relay/timeout (`apps/web/src/api.ts:68`); the relay is GET-only (`apps/api/app/routes/read_relay.py:13`). Operator decisions (verbatim, PRD §3 + Architecture §1): D2 "**Relay once + idempotency key.** Every write carries a unique key. When the direct connection looks dead, the write also goes over the WebSocket relay, and the server applies a given key at most once." D3 "**All four:** all `api()` reads + writes; the raw-fetch stragglers (`useVersionCheck`, live `config.ts`, live rewriter, S3 upload PUT); streams + terminal (EventSource `/api/stream`, terminal WebSocket); browser-fetched URLs (`apiHref`: img, iframe, downloads)." D4 "**Yes, also unify edit pages:** load via `useEditRecord`, save via `useSubmit`". A1 one epic, parallel where no dependency. A2 **At most once** (claim before run). A3 **All console writes, capped** (body ≤ 1 MiB, per-session write budget, key required). A4 **Public relay, live paths only** (cookieless socket, `GET /api/public/live/*`, per-IP budget). Full documents (read your section in full before building): the PRD and Architecture are the settled `prd` / `architecture` artifacts of Tasks 12447 / 12448 (read with `task_read` / `artifact_read`). Your section below is copied from them; the pinned seams are binding contracts other children build against — do not change them without updating the sibling that consumes them. ## Your scope: C7 — Gate Add `tools/check_web_network.py` (with `--selftest`) to `./dc check quick` beside `check_web_deps.py` (`tools/check.sh`), allowlist as one dict with reasons, plus the AC-8 `.json()` grep. Put the inventory table (PRD §1 every former bypass → its new home) in the PR body. Runs last so the tree is already clean when the gate turns on. ### Architecture §3.6 (verbatim) ### 3.6 The gate [child C7] - `tools/check_web_network.py`, run in `./dc check quick` beside `check_web_deps.py` (`tools/check.sh:2234`), with `--selftest`. - It scans non-test `apps/web/src/**/*.{ts,tsx}` for: - `fetch(` (a call of the global, not a member or parameter), `new EventSource`, `new WebSocket`, `XMLHttpRequest`, `navigator.sendBeacon`; - `lazy` imported from `react` **and** the member call `React.lazy(`; - **any** `apiHref(` call outside `net/` and `api.ts`. This is call-site based, not JSX-attribute based, so assigning its result to a variable is caught too. Event-stream paths take `apiHref` through `net/streams.ts`. - `--selftest` has one positive and one negative fixture per pattern. Every hit outside `apps/web/src/net/` must be on the allowlist, else it fails, naming `file:line`. - **Allowlist = one dict in the checker** (single home), each entry with a one-line reason. Expected entries: `lib/clientTelemetry.ts` (the report must not race or recurse), `live/beacon.ts` (keepalive fire-and-forget), and `lib/lazyRetry.ts` (the one `lazy` wrapper). Everything else lives in `net/`. - The AC-8 grep is added to the same checker: `.json()` on a `Response` outside `net/` and `api.ts` fails. Known sites today: `components/ConsolePane.tsx:122`, `lib/supportChat.ts:113`, `components/FullDiskAccessStep.tsx:21`. **C3 moves each onto the `api()` helpers** (or `readJson`, exported from `net/`). They are not allowlisted, so no unhandled cut body remains. ### PRD assumptions that bind this child (verbatim) - AS4. **Intentional internals stay where they are, but are named.** The relay's own probe and socket, client telemetry and the live beacon (`keepalive` fire-and-forget) may keep their own `fetch`, as entries on the gate allowlist (AC-4), each with a one-line reason. Why: they are the choke point's own machinery, or must not be raced. - AS9. **`apps/site-widget`** (a separate deployable embedded on third-party sites) is not a console UI surface and is out of scope. ### Requirement constraints (PRD §7, verbatim) - Writes are never applied twice (D2, AC-2). Idempotency touches every mutating route, so it is a security-surface change (data integrity). Project instructions grant merge permission. The review lane still applies. - Act-as headers (`lib/actAs.ts`) and session headers (`lib/activeOrg.ts`) must ride relayed writes exactly as direct ones. A relayed write must be authorized as the same session. A relayed 401/403 never wins over the direct answer, the same rule as reads. - Single-home: the relay rules live in one module. `docs/standards/coding-standards.md` and the context policy apply. ### Acceptance criteria (PRD §5, verbatim) **AC-4 — One choke point, enforced by the gate.** All console network traffic from `apps/web/src` (non-test) goes through one module (or a small named set under one home). A gate check fails when a new bypass appears: `fetch(`, `new EventSource`, `new WebSocket`, `XMLHttpRequest`, `navigator.sendBeacon`, or `React.lazy`/`lazy` from `react`, outside the allowlist (AS4). Each allowlist entry carries a one-line reason. - verify: on a scratch branch, add `fetch('/api/x')` to any page → `./dc check quick` fails and names the file. Remove it → passes. The allowlist is in one file. - verify: an inventory table in the spec or PR lists every former bypass (section 1) and where it now goes. Credentials & reach: (Architecture §6, verbatim rows) | Oracle | Credential / identity / environment | Status | |---|---|---| | AC-2 API tests, AC-4/AC-8 gate | the worktree's `./dc` stack only | **verified**: needs nothing beyond the worktree | | AC-10 deploy run | `jaah-forge pr checks` / `ci log` (pre-authenticated forge) | **verified**: no credential to fetch | Out of scope: PRD §6 (verbatim) — - Do NOT add a hostname, port, certificate, or change cookies or auth to carry writes. The WebSocket relay carrier stays (decision of Task 11936). - Do NOT change the read relay's existing read semantics (race timing, probe, suspect and dropping memory) beyond what writes, streams and uploads need. - Do NOT change any edit page's visible UI: layout, fields, tabs, copy, URLs, routes (AC-9 quality). - Do NOT change React Query's global defaults (`apps/web/src/queryClient.ts`, `retry: false` for queries). - Do NOT change server route business logic. Server changes are limited to (a) idempotency for console writes and (b) the relay accepting writes under that key. - Do NOT touch `apps/site-widget`, `apps/docs`, `apps/jaahd`, `packages/` (AS9). - Do NOT refactor non-edit-page mutations (compliance, billing, playbook list, template cards) beyond what routing them through the choke point needs. - Do NOT add a feature flag (AS8). - Do NOT treat this as Nick-only: no per-user or per-org switch. - A sibling's leftover bypass is routed through `net/` in this child; NEVER add an allowlist entry to make a real bypass pass (the allowlist holds only the three §3.6 internals). Done means: - AC-4 — oracle verbatim above (PRD §5 AC-4 `verify:` lines) - AC-8 (contributor): the checker fails on `.json()` on a Response outside `net/` and `api.ts`. - Every gate failure message names `file:line` and the allowed home, so the next developer knows where the call belongs. - `./dc check quick` green; PR merged via the merge lane; Jaah Deploy to dev green. This child owns no dev-browser oracle. Watch that deploy; if it fails, fix and merge again.

Activity

  • Draft
  • Ready