Remove console caps, share abuse limits
## Original requirement (verbatim)
> "Make the control-plane API highly concurrent and distributed: many processes, no per-task connection caps."
Epic 12693. Settled upstream: PRD (task 12694) and Architecture (task 12695). The operator overrode the PRD's multi-worker reading (D3): ONE Granian process per task on free-threaded CPython, async DB I/O on one shared pool. The full design (decisions D1–D9, risks, oracle tooling) is the `architecture` artifact of task 12695; read it (`artifact_read` / `task_read` on 12695) for the pinned seams S1–S5 (§4), the measured cause (§1), material risks (§6) and the burst-probe tooling and baseline (§8) before you start. This body carries this child's scope, acceptance and fences verbatim.
## Scope of this child: C6 — Console caps + shared abuse limits (R4, R5) (Architecture §5, verbatim)
### C6 — Console caps + shared abuse limits (R4, R5)
- `apps/api/app/routes/read_relay.py`: delete the per-session caps (`MAX_SOCKETS_PER_SESSION`,
`MAX_IN_FLIGHT_PER_SESSION`, session read/write token buckets, :80-88).
- Anonymous limits become shared: the handshake rate and `ANON_READS_PER_SECOND` move to `SharedRateLimiter`
buckets (`app/ratelimit.py:101`, DB, cross-task) with today's thresholds.
- `MAX_SOCKETS_PER_IP` (4) stays per process, **deliberately**, as an AS1 file-descriptor bound: a count of open
sockets that a sliding-window table cannot express without a presence row per socket.
- It multiplies by the task count, exactly as it does today (2 tasks on dev, up to 8 on main), and not by
workers (there is one process per task).
- Abuse throttling is carried by the two **shared** rate limits above: handshakes per IP and anonymous reads per
IP, both cross-task. So R5's "same threshold whatever serves it" holds for the rate limits.
- **Recorded scope note:** the socket count is not shared, by decision.
- `MAX_READS_PER_SOCKET` and `READ_TIMEOUT_SECONDS` stay (AS1).
- `idempotency.MAX_WAITERS` (`app/idempotency.py:101`): C6 **only removes the cap**. How waiters wait (an async
wait in place of the thread poll) is C2's, which owns the async idempotency layer. No dependency between them.
- `session_transcript_archive.MAX_CONCURRENT_ASSEMBLIES` (:35): remove the refusal. Assembly streams to the task's
`/tmp` spool through `run_cpu`, so memory no longer bounds concurrency.
- `support_agent/turn.py:137` `MAX_CONCURRENT_TURNS`: remove. The per-user turn rate limit stays (AS2).
- **Acceptance:** PRD AC8, AC9.
PRD R4, R5 (verbatim):
**R4 — No console-user caps (A2).** No per-console-session socket, in-flight, read-rate or write-rate cap remains in the read relay or elsewhere on signed-in console traffic. In-process concurrency gates on signed-in paths (`idempotency.MAX_WAITERS`, transcript assembly, support-agent concurrent turns) are removed or raised so that they never queue a legitimate user. The Architect may keep a gate only with a stated resource reason that does not reduce to a count cap.
**R5 — Abuse limits kept.** Anonymous/public limits (AS2) remain, and they are shared across workers and tasks (DB-backed, not per-process). Today's read-relay per-IP limits are per process, so with N workers they must not silently multiply.
## Approach (planner)
If C2's `run_cpu` is not on develop when this starts, offload with the existing thread executor and leave a one-line pointer; rebase onto `run_cpu` if it lands first.
## Applies to every child (verbatim)
Each child is one implementer's PR. **Dependencies are data/API only.** Children that edit the same file are not
serialized; they rebase at merge.
**Every child owns AC15:** its merge's Jaah Deploy must go green, and dev `/api/health` must answer 200. Every
child that changes request-path behaviour also runs the S4 regression probe.
## Constraints (PRD §7, verbatim)
## 7. Constraints
- Merges go through the merge lane on `./dc check full`. Each merge's Jaah Deploy must be watched to green (project instruction).
- Migrations, if any, must be additive while two releases overlap (`docs/standards/coding-standards.md`).
- The deploy overlap doubles tasks, so every connection-budget formula carries ×2.
- Live state is read through the console UI only (CLAUDE.md hard rule). `tools/cp_psql.sh` was refused by the credential broker for this BA run (`org/aws/cp-psql-reader`), so current dev latency was not measured here. The Architect must get it (AS6, AC12).
Credentials & reach:
| Oracle | Credential / identity / environment | Status |
|---|---|---|
| Burst probe, AC3, AC5, AC8, AC10 (console as operator) | `me/jaah/debug/main/operator-login` on `app.dev.jaah.ai` | verified: `credential_get` via broker OK. Login 200 and timed GETs from this node (ts node of task 12695). |
| AC9 (anonymous flood at the documented per-IP threshold) | No credential: anonymous requests to the read relay's anonymous handshake and a `SharedRateLimiter` public route (e.g. `/api/auth/login` with an unknown email, or trigger intake on `hooks.dev.jaah.ai`) | verified reach: anonymous HTTPS to `app.dev.jaah.ai` and `hooks.dev.jaah.ai` answers from this node (200/404, 0.11 s). The probe uses a throwaway email, never a real account, so no lockout. It runs in one short burst (seconds), because the per-IP bounds key on this node's egress IP, which other runners may share. |
Out of scope:
- Do NOT remove or loosen anonymous/public abuse controls (AS2): WAF, `SharedRateLimiter` public and trigger-intake limits, read-relay `MAX_SOCKETS_PER_IP` / `ANON_*`, site-chat per-IP limits, headscale nginx `limit_conn`/`limit_req`.
- Do NOT remove `MAX_READS_PER_SOCKET` or `READ_TIMEOUT_SECONDS` (AS1).
- Do NOT change how idempotency waiters wait (C2 owns that).
Epic-wide (PRD §6, verbatim):
- **Do not** promote to `master` or change prod's live config as part of done (Q8). Prod tfvars may be edited only so that the R7 budget test covers them. Do not apply them to prod.
- **Do not** remove or loosen anonymous/public abuse controls (AS2): the WAF, `SharedRateLimiter` public and trigger-intake limits, read-relay `MAX_SOCKETS_PER_IP` / `ANON_*`, site-chat per-IP limits, headscale edge nginx `limit_conn` / `limit_req` (`infra/jaah/headscale/nginx.conf`).
- **Do not** remove resource-safety bounds (AS1): body, frame and response size limits, read timeouts, `pool_timeout`.
- **Do not** remove per-ECS-task DB pool sizing as a concept, or exceed RDS `max_connections` (AS3).
- **Do not** add a numeric load-test gate or a benchmark harness. None was asked for (Q4, Q7).
- **Do not** change node-side caps that are not on the CP-call path: vnode conntrack limits (`apps/jaahd/vnodehelper/`), MicroVM egress `MAX_CONNECTIONS_PER_TAP`, reconcile probe parallelism, `pr_stats_capture` semaphore.
- **Do not** change unrelated product limits: plan and org limits (`orgs.limit_*`), `max_accounts_per_node`, node session caps, worktrees-per-node.
- **Do not** introduce a leader-elected singleton CP or pin `desired_count` to 1 (ADR-0142 anti-pattern still holds).
- **Do not** add a process-local registry consulted by a request path (ADR-0142 anti-pattern still holds).
- Do NOT change task CPU/memory, RDS class or autoscaling max (operator D2: sizing out of scope).
- Do NOT reintroduce multi-worker / multiple processes per container (operator D3), and do NOT keep a uvicorn fallback (D8).
Done means:
- AC8 — | AC8 | No console-session cap remains (R4). | `git grep -nE "MAX_SOCKETS_PER_SESSION\|MAX_IN_FLIGHT_PER_SESSION\|SESSION_READS_PER_SECOND\|SESSION_WRITES_PER_SECOND"` shows no enforcement. On dev, open the console in 10 tabs as one user and load a heavy page (Tasks board, Audit › Sessions) in all at once. No relay `rate_limited` error frames appear (DevTools → WS frames), and no request is refused for a per-session cap. |
- AC9 — | AC9 | Abuse limits for anonymous/public traffic remain and are shared across CP tasks (R5). | Gate tests for `SharedRateLimiter` and the read-relay anonymous path. On dev, send more than the documented per-IP anonymous limit to the public endpoint or trigger intake. A 429 or `rate_limited` comes back at the same threshold as before. The threshold does not multiply by the task count (D3: one process per task). | (The read relay handshake rate and `ANON_READS_PER_SECOND` are `SharedRateLimiter` buckets; `MAX_SOCKETS_PER_IP` stays a per-process AS1 fd bound by decision.)
- C6-a — `idempotency.MAX_WAITERS`, `session_transcript_archive.MAX_CONCURRENT_ASSEMBLIES` refusal and `support_agent` `MAX_CONCURRENT_TURNS` are removed; transcript assembly streams to the `/tmp` spool through `run_cpu` (or an equivalent thread offload if C2 has not merged yet); the per-user support turn rate limit stays — oracle: tests + `git grep` citations.
- S4 regression check — after this child deploys, the §8 burst probe on dev is no worse than the baseline at N=24 (`/api/tasks` N=24 max 4.6 s) — oracle: run the burst probe, record wall/p50/max in the PR body and report. Evidence, not a numeric gate; a child that makes it worse is not done.
- AC15 — Jaah Deploy to dev succeeds for this child's merge commit on `develop`, and dev `/api/health` answers 200 after it — oracle: GitHub Actions "Jaah Deploy" run for the merge commit is green; `curl https://dev.jaah.ai/api/health` → 200. (If the deploy is cancelled/superseded by another deploy, leave it and complete.)
Outcome
Removed relay per-session caps, MAX_WAITERS, transcript permits, signed-in turn cap; anon relay budget now a shared SharedRateLimiter (30/6s), per-IP socket+in-flight bounds kept. PR #7753 merged, Jaah Deploy green; dev AC8/AC9 + S4 probe (N=24 max 3.19s) pass.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)