Compare commits

...
7 Commits
Author SHA1 Message Date
ducoterra 7cfe58fb21 phase: 87_big_read_progress
Build and Push Containers / build-and-push-app (push) Successful in 2m48s
Build and Push Containers / build-and-push-db (push) Successful in 19s
All criteria verified — no defects found, nothing to fix. Final report:

**Phase 87 — big read progress: final verification pass (all tasks already complete in `complete/`)**
- Verified implementation vs. overview: `TOOL_LINE_ELAPSED_AFTER_MS = 5_000`, single live `armToolLineClock` site, settle on thinking/retry/delta, `stopToolLineClock` in `setUiState`, visible `.typing-elapsed` (aria kept byte-identical), CSS AA pairing — all match design; `app/` byte-identical (0 changes)
- **Tests/lint (exact outcomes):**
  - `uv run pytest --cov=app --cov-report=term` → 1732 passed, coverage **99%** (>90% ✓)
  - `uv run pytest tests/e2e/test_big_read_progress.py -v --no-cov` → **4 passed** (ticking suffix, visible hint, settle, no-timer restore)
  - `test_thinking_display.py` → 5 passed · `test_agent_document_tools.py` → 4 passed · `test_smoke.py` → 3 passed (all isolated)
  - 3 pinned frontend suites + new unit pins → 62 passed · `uv run ruff check . && uv run pyright` → clean, 0 errors
- **Completion criteria:** E2E pins 1–4 ✓ · guard/state-machine byte-identical ✓ (diff is additive only) · diff scope limited to `app.js`, `styles.css`, 2 new test files, phase files; nothing in `app/` ✓
- **Notable:** no deviations; commit + `00_phase.md` move left to the harness per executor rules (task files already in `complete/`)
- **Next pending phase:** none — `todo/` contains only this phase (87 is the last)
2026-09-08 05:51:23 -04:00
ducoterra 0f6b9ff7e6 phase: 86_history_page_width
All criteria verified. Final report:

**Phase 86 final verification pass — all green, no defects found.**

- **Verified (previously implemented):** `position: relative` on shared `.table-wrap` (the one-rule A3 fix, +12 lines in `frontend/assets/styles.css`); unit pin `tests/unit/test_table_wrap_stacking.py` (3 tests); E2E story `tests/e2e/test_history_page_width.py` (5 tests: History 626→≤375, Tokens 618→≤375, SPA switch with window-sentinel, RAG regression, desktop 1280×800)
- **Tests/lint (this pass):** `uv run pytest` → 1720 passed · `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90% floor) · E2E in isolation: `test_history_page_width.py` 5 passed, `test_chat_history.py` 5 passed, `test_api_tokens.py` 9 passed, `test_smoke.py` 3 passed · `uv run ruff check . && uv run pyright` → clean, 0 errors
- **Completion criteria:** ① 375px direct+SPA `scrollWidth ≤ innerWidth` on History/Tokens — PASS (E2E pins) ② in-card scroll preserved + full-width + a11y spans in DOM — PASS ③ Sources + desktop regression — PASS ④ full suite/coverage/E2E/lint gates — PASS ⑤ `git diff --stat` limited to `styles.css` (+12) + new test files + phase files, no `app/`/markup/JS — PASS ⑥ commit/move — left to harness per executor rules (working tree intact, `todo/` dir removal already reflected)
- **Notable:** red→green (CSS reverted → 626px failure) and manual live check already recorded in `.agents/reports/86_history_page_width/`; pre-existing untracked `.agents/remediation_plan.md` (Sep 7 security audit) untouched
- **Next pending phase:** `87_big_read_progress`
2026-09-08 02:38:45 -04:00
ducoterra 412a560348 phase: 85_mobile_menu_gate_overlap
All verification complete. Final report:

**Phase 85 final verification pass — all green** (tasks 01–03 already complete; independently re-verified from scratch)
- Verified fix in tree: `styles.css` `.auth-gate` z-index 500→15 + rewritten stacking comment (no `z-index: 500` left); `index.html`/`document.html` comment-only; `token-gate.js` docstring-only (logic byte-identical); `test_mobile_hamburger_nav.py` real-click conversion + new `test_anonymous_toggle_tappable_with_gate_up`; new `tests/unit/test_gate_header_stacking.py` (3 pins); `test_api_tokens.py` untouched
- `uv run pytest` → 1717 passed, 1 warning (exit 0)
- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90%)
- E2E in isolation: `test_mobile_hamburger_nav.py` **8 passed**; `test_api_tokens.py` **9 passed** (unchanged); `test_smoke.py` **3 passed**; `test_document_viewer.py` **7 passed**
- `uv run ruff check . && uv run pyright` → clean / 0 errors
- Live probe (375×812, anonymous, fresh server): on `/` and `/history.html` `elementFromPoint` at toggle → SVG `path`, never `#auth-gate`; real click opens menu (`aria-expanded=true`); exactly one visible nav link (Chat) + sign-in copy; Chat link topmost over gate; `#main` inert — criteria 1, 2, 3 confirmed directly
- Criteria: (1) real-click menu on / + /history ✅ (2) dropdown above gate ✅ (3) anonymous contents + #main inert ✅ (4) admin byte-identical (phase-46 tests green) ✅ (5) doc-viewer gate under bar (CSS pin + doc-viewer suite) ✅ (6) full gate green ✅ (7) diff scoped to the 5 code files + new unit test + phase files, nothing in `app/` ✅ (8) commit + todo→complete move left to harness per executor rules (task files already in `complete/`)
- Note: `.agents/remediation_plan.md` (untracked) is a pre-existing artifact of the earlier security audit — left untouched; a missing "N passed" line seen mid-pass was my own `-q`+addopts `-q` = `-qq` quirk, not a defect
- Next pending phase: `86_history_page_width`
2026-09-08 01:56:37 -04:00
ducoterra f4150421bb phase: 84_docs_push_error_sanitization
**Phase 84 — final verification pass: all green, no defects found**

- Verified implementation: `app/core/errors.py` (verbatim lift of sync masker), `app/api/sync.py` alias import, docs-push 502 `detail=sanitize_error(str(exc))`, all five `llm.py` error sites sanitized; new/extended test pins in place
- Tests: `uv run pytest` → **1714 passed, 0 failed**; targeted pins (new unit ×2 + integration ×1, existing 502 pin) → 13 passed; sync/git-sources regression → 67 passed
- Coverage: `uv run pytest --cov=app --cov-report=term-missing` → **99%** (`app/core/errors.py` 100%, `app/rag/llm.py` 100%) — >90% met
- E2E isolation: `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → **3 passed**
- Lint/types: `uv run ruff check .` → clean; `uv run pyright` → **0 errors**
- Criteria: 502 masks `*****@`/never token + row untouched ✅; LLM base-URL masked, credential-free strings byte-identical ✅; `_CREDS_RE` only in `app/core/errors.py` (working-tree grep) ✅; full gate green ✅; `git diff --stat` limited to the 4 app files + 2 modified test files + 3 phase task files (untracked: new module, new unit test, complete/ dir, reports, audit plan) ✅
- Commit/phase move left to the harness per instructions (task files already in `complete/`)
- No deviations; nothing to fix
- Next pending phase: **85_mobile_menu_gate_overlap**
2026-09-08 01:11:45 -04:00
ducoterra fa189dede7 phase: 83_chat_save_payload_limits
All verification criteria are confirmed green. Final report:

**Phase 83 — final verification pass (all tasks already complete; verified, nothing to fix)**

- Verified `app/schemas.py` carries every A1 cap (text/thinking 32 000, sources 20, suggestions 50×200, tools 50, SourceRef 120/1000/500, ToolCall 100/2000, messages ≤200) — boundary-only, `app/api/chats.py` untouched
- Verified all test pins exist and pass: 32 unit boundary tests (both edges of every cap + round-trip), 4 oversized-422 integration pins (text/201-msgs/21-sources/PUT, each with nothing-stored/row-unchanged assertions), A3 SSE done-event pin at column maxima, dedicated E2E (anon 40 000-char POST → 422 + small save → 201)
- `uv run pytest tests/unit/test_schemas.py -v` — 32 passed
- `uv run pytest tests/integration/test_chats_api.py` — exit 0; A3 pin — passed
- `uv run pytest tests/e2e/test_chat_save_payload_limits.py -v --no-cov` — 2 passed (isolation); `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` — 5 passed
- `uv run pytest` — exit 0 (~1 704 tests, 0 fail/skip); `uv run pytest --cov=app` — TOTAL 99%, `app/schemas.py` 100% (>90% ✓)
- `uv run ruff check . && uv run pyright` — clean (0 errors)
- `git diff --stat` — only `app/schemas.py`, 3 test files (+`test_chat_api.py` A3 pin, sanctioned by task 02), phase files; no `app/api/chats.py`/`alembic`/`frontend`/`pyproject`/`uv.lock` diff ✓
- All completion criteria met; commit + phase move left to the harness per pipeline rules (changes stay in working tree)
- Deviation note: list caps use `max_length` instead of `max_items` — identical in pydantic 2.13 (`max_items` is a deprecated alias); both boundaries behaviorally pinned
- Next pending phase: `84_docs_push_error_sanitization`
2026-09-08 00:33:21 -04:00
ducoterra e29d68d9f0 phase: 82_security_headers
All completion criteria verified green — no defects found, nothing to fix. Final report:

**Phase 82 (security headers) — final verification pass: all green**

- Verified prior-run implementation: `app/core/security_headers.py` (pure-ASGI, header-only, exact A1 CSP), registration in `app/main.py` after `configure_caching` (outermost), unit/integration/E2E suites.
- Deviation confirmed sound: `data:`-URI favicon (blocked by locked CSP) → static `frontend/assets/favicon.svg` in 5 templates + Containerfile `cp`; SVG element byte-identical to the old data-URI (verified programmatically); serves 200 with all three headers.
- Curl check (server booted like e2e conftest, log: `/tmp/curl_security_headers_final.log`): `/`, `/api/health`, `/assets/styles.css`, `/nope` (404) → all three headers, CSP exactly `default-src 'self'; base-uri 'none'; frame-ancestors 'none'`.
- `uv run pytest tests/unit/test_security_headers.py tests/integration/test_security_headers.py -v --no-cov` → 13 passed (incl. SSE byte-identity pin).
- `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` (isolated) → 2 passed (headers + zero CSP violations + painted page).
- SSE tripwire `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` → 3 passed.
- `uv run pytest --cov=app --cov-report=term-missing` → 1665 passed, app/ 99% (>90%); `uv run ruff check . && uv run pyright` → clean (0 errors).
- `git diff --stat` limited to phase-82 files + the two documented deviations (favicon set, `tests/unit/__init__.py`); no `pyproject.toml`/`uv.lock`/JS diffs.
- Commit + phase-dir move left to the harness per pipeline rules (not executed by me).

Next pending phase: `83_chat_save_payload_limits`.
2026-09-07 23:54:41 -04:00
ducoterra 42a4222949 phase: 81_login_rate_limit
All gates verified. The phase is complete — everything was already implemented by prior task runs; this pass verified each completion criterion end-to-end and found no defects.

**Phase 81 final verification report** (tasks 01–03 all present in `complete/`; no code changes needed this pass)

- Verified: `app/core/rate_limit.py` (stdlib sliding window, fail-open) + both login routes' 429 pre-check/record/reset wiring + all unit/integration pins present (11th-429, blocked-success-still-429, clean-counter reset, shared counter, autouse `clean_rate_limit` fixture documented).
- Live check (task 03): dev server + 11 rapid wrong logins → `401 ×10, 429`; 429 body carries `retry-after: 900` + generic detail; server restarted (per-process counter cleared by design).
- `uv run pytest tests/unit/test_rate_limit.py -v --no-cov` → 10 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1652 passed, **TOTAL 99%** (>90%; rate_limit.py 100%, auth.py 100%)
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` (isolation) → 3 passed
- `uv run ruff check . && uv run pyright` → All checks passed / 0 errors, 0 warnings
- Completion criteria: all met, except commit + phase-dir move — per harness rules I left all changes uncommitted in the working tree (harness commits atomically and moves the phase).
- Diff scope: exactly `app/core/rate_limit.py`, `app/api/auth.py`, `tests/unit/test_rate_limit.py`, `tests/integration/test_auth_api.py` + phase files; `pyproject.toml` / `uv.lock` / `frontend/` untouched.
- Deviations: none in code; commit/move deferred to harness as instructed.
- Next pending phase: `82_security_headers`.
2026-09-07 23:08:46 -04:00
156 changed files with 7599 additions and 109 deletions
@@ -0,0 +1,69 @@
# Phase 81 — Rate-limit failed sign-ins: 429 + Retry-After after repeated failures
**Source:** `.agents/remediation_plan.md` SEC-03 (security audit 2026-09-07, severity Medium — "No rate limiting / lockout on login + token-auth")
**Story:** n/a (security hardening — audit-derived, no user story)
**Context:** `app/api/auth.py` (`POST /api/login` — `check_password` then `sign_in`; `POST /api/token-auth` — `find_active_by_token` then session keys — both return one generic 401 on every failure shape, no throttling of any kind), `app/core/auth.py` (the constant-time `check_password`, `sign_in`/`sign_out` — untouched by this phase), `app/main.py` (middleware registration order — this phase adds no middleware), `frontend/assets/login.js` (the login form's error path: non-204 → `Sign-in failed (HTTP ${r.status}) — try again.` via `showError` — a 429 already renders there as a generic message, so no frontend change is required), `tests/unit/test_auth.py` + `tests/integration/test_auth_api.py` (the existing auth contract pins — every 401 shape, no enumeration).
## Objective
A client IP that fails sign-in repeatedly (admin password or token) gets **429 + `Retry-After`** instead of unlimited line-rate guesses: a stdlib-only in-memory sliding-window limiter shared by both login routes, counting only failed attempts, reset on success, failing open (a limiter bug must never lock the owner out).
## Audit basis (read this, not the chat)
- Neither `POST /api/login` nor `POST /api/token-auth` throttles: the password is compared with `compare_digest` (fast), so an attacker who can reach `:8000` can try thousands of guesses per second (audit PoC: 10 000 sequential logins all answered at line rate).
- The fix is deliberately small and stateless-per-process (A10: no new service, no DB table, no Redis): a per-IP deque of failure timestamps in module memory, lost on restart (an acceptable reset — it is a friction bump, not the boundary; the homelab HTTP posture is the documented owner decision).
- Threshold (locked in this phase): **10 failed attempts per client IP per 15-minute sliding window** → subsequent attempts get **429** `{"detail": "too many failed sign-in attempts — try again later"}` + a `Retry-After: <seconds>` header until the window clears.
- The counter is SHARED by both routes (any auth failure from the IP counts — a token-spraying attack must not reset by alternating routes), and a SUCCESS on either route clears that IP's counter (a legitimate owner who fat-fingers twice is not poisoned).
## Owner decisions (chat, 2026-09-07 — recorded per AGENTS.md rule 3)
- **A1 — in-memory, stdlib-only:** no new dependency, no DB table, per-process state (A10/A12 spirit). A restart clears the counter — accepted.
- **A2 — 10 / 15 min, module constants:** `MAX_FAILURES = 10`, `WINDOW_SECONDS = 900` as documented module constants in the limiter module — NOT new `BOR_` settings (a security control with a fixed, tested default; no env surface to mistype).
- **A3 — 429 contract:** one generic detail (no enumeration between login/token failures), `Retry-After` header carrying the whole-window seconds; the login page's existing `Sign-in failed (HTTP 429) — try again.` line renders it — no frontend change in this phase.
- **A4 — client IP = `request.client.host`:** the direct peer address. ASSUMPTION (locked here): the app is served directly (homelab), no reverse proxy — if a proxy is ever put in front, the limiter needs `X-Forwarded-For` handling (noted in the module docstring, out of scope now).
## Design (shared by all tasks — the executor reads this, not the chat)
- **`app/core/rate_limit.py` (new)** — the limiter, pure stdlib (`collections.deque`, `time.monotonic`, `threading.Lock` for the dict — the ASGI event loop is single-threaded but the lock costs nothing and keeps the unit tests honest under pytest-xdist if ever added):
```python
MAX_FAILURES = 10
WINDOW_SECONDS = 900
_failures: dict[str, deque[float]] = {}
_lock = threading.Lock()
def record_failure(client_ip: str) -> None: ... # append now(); prune expired; never raises
def remaining_wait(client_ip: str) -> int: ... # 0 = allowed; else whole-window seconds (ceil) until the oldest counted failure expires
def reset(client_ip: str) -> None: ... # drop the IP's entry (on success)
```
Semantics (all unit-pinned): a failure is counted when it is recorded; the window slides — only failures within the last `WINDOW_SECONDS` count; the IP is blocked while the count of in-window failures `>= MAX_FAILURES` (the 10th failure already blocked? NO — the 10th failure is recorded and the **11th** attempt is the first 429: the check happens before the attempt); `remaining_wait` returns `int(ceil(WINDOW_SECONDS - (now - oldest)))` while blocked, `0` otherwise; **fail-open**: any internal error (corrupt deque, clock skew) → `remaining_wait` returns `0` and `record_failure` no-ops — a limiter bug never denies the owner.
- **`app/api/auth.py`** — the two route changes (identical shape):
- `login`: first line of the handler — `ip = request.client.host; if (wait := _wait(ip)) > 0: raise HTTPException(429, detail=TOO_MANY_DETAIL, headers={"Retry-After": str(wait)})`; on password failure → `record_failure(ip)` before the 401; on success → `reset(ip)` before the 204.
- `token_auth`: same three points (pre-check, record on the 401 path, reset on the 204 path).
- `TOO_MANY_DETAIL = "too many failed sign-in attempts — try again later"` (one string shared by both routes — no enumeration).
- Module docstrings updated: the 429 case joins the documented status set of each route.
- **Not touched:** `app/core/auth.py` (password check + session helpers), the 401 contract of both routes (byte-identical on the non-throttled path), everything else in the app, all frontend files.
## Dependencies
— (none; extends the completed phase-16/79 auth surface additively — the 401/204 contract is unchanged for non-throttled callers)
## Tasks
1. `01_rate_limiter_core.md` — `app/core/rate_limit.py` + the unit suite for the window semantics.
2. `02_auth_routes_wiring.md` — the 429 pre-check + failure/success bookkeeping on both login routes + integration tests.
3. `03_verify_and_commit.md` — full gate (suite + coverage + smoke E2E in isolation) + atomic commit.
## Testing & Quality
- Unit — `tests/unit/test_rate_limit.py` (new): 9 failures → allowed; the 10th failure recorded; the next attempt → `remaining_wait > 0`; two independent IPs independent; a failure older than the window expires (monkeypatch a fake clock or pre-seed timestamps) and unblocks; `reset` clears (a success unblocks immediately); `remaining_wait` is the whole-window seconds, not the per-failure age; fail-open: a pre-corrupted state (e.g. a non-deque entry injected) → `remaining_wait` returns 0, `record_failure` doesn't raise.
- Integration — `tests/integration/test_auth_api.py` (extend): 10 wrong-password logins from the TestClient → 11th → **429** with the generic detail + a `Retry-After` header (int > 0); a correct password after 9 failures → 204 AND the counter is reset (the next 10 failures needed to block again — assert by 9 more failures still 401); token-auth failures share the counter (5 login + 5 token failures → the 11th of either → 429); the non-throttled 401 contract (existing pins) byte-identical.
- E2E (isolation gate per AGENTS.md rule 9): `uv run pytest tests/e2e/test_smoke.py -v --no-cov` green — no UI change; the login form's existing 429 rendering is covered by the integration contract (the generic `HTTP ${status}` line).
- Coverage: **>90%** on `app/` — `app/core/rate_limit.py` is small and fully unit-exercised; the `app/api/auth.py` branches (pre-check hit/miss, record, reset) integration-covered.
## Completion Criteria
- [ ] 11 rapid failed logins from one client: the first 10 → 401 (unchanged detail), the 11th → 429 + `Retry-After`; the token-auth route participates in the same counter (integration pins).
- [ ] A successful login/token-auth resets the IP's counter (integration pin).
- [ ] `uv run pytest tests/unit/test_rate_limit.py -v` green; full `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%.
- [ ] `uv run pytest tests/e2e/test_smoke.py -v --no-cov` green in isolation.
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] No dependency change (`pyproject.toml` / `uv.lock` untouched); `git diff --stat` limited to `app/core/rate_limit.py`, `app/api/auth.py`, the two test files, phase files.
- [ ] One atomic `--no-gpg-sign` commit (e.g. `fix(auth): rate-limit failed logins and token-auth attempts with 429 + Retry-After`); phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **A10 intact** — no new service/table/middleware; one module + two route pre-checks.
- **401 contract unchanged** — the no-enumeration single-401 discipline of phase 16/79 is byte-identical for every non-throttled attempt; 429 is a new status, not a rewording.
- **No frontend change** — `login.js` already renders any non-204 as `Sign-in failed (HTTP ${status}) — try again.` (the 429 reads correctly there).
@@ -0,0 +1,33 @@
# Task 01 — The sliding-window limiter module + unit suite
**Phase:** `81_login_rate_limit` · **Story:** n/a (security hardening — audit SEC-03)
## Objective
`app/core/rate_limit.py` exists with the exact semantics from the phase design block, and `tests/unit/test_rate_limit.py` pins every one of them.
## Work
1. `app/core/rate_limit.py` (new) — implement per the phase overview's design block:
- constants `MAX_FAILURES = 10`, `WINDOW_SECONDS = 900`;
- module state: `_failures: dict[str, deque[float]]` + a `threading.Lock`;
- `record_failure(client_ip: str)` — under the lock: get/create the deque, append `time.monotonic()`, prune entries older than `WINDOW_SECONDS`; **never raises** (wrap the body, `except Exception: return` — the fail-open contract);
- `remaining_wait(client_ip: str) -> int` — under the lock: prune; count in-window entries; if `< MAX_FAILURES` return `0`; else `max(1, int(-oldest - now + WINDOW_SECONDS))` as a ceiling — i.e. `int(math.ceil(WINDOW_SECONDS - (now - oldest)))` clamped to >= 1; **never raises** (fail-open → 0);
- `reset(client_ip: str)` — under the lock: `_failures.pop(client_ip, None)`;
- module docstring: the audit basis (SEC-03), the 10/15-min threshold, the shared-counter intent (both login routes), fail-open rationale (a limiter bug must never lock the owner out), and the `request.client.host` assumption — direct peer, no reverse proxy in the current homelab deployment (proxy deployments would need `X-Forwarded-For` handling — out of scope).
2. `tests/unit/test_rate_limit.py` (new) — pin, in this order:
- 9 × `record_failure("ip")` → `remaining_wait("ip") == 0`;
- a 10th `record_failure` → `remaining_wait("ip") > 0` (and `<= WINDOW_SECONDS`);
- a second IP is unaffected (`remaining_wait("ip2") == 0`);
- expiry: pre-seed one IP's deque with a timestamp `WINDOW_SECONDS + 1` in the past (reach into the module state, or a small `for` loop calling `record_failure` after monkeypatching `time.monotonic` to walk the clock forward) → `remaining_wait` back to 0;
- `reset` after 10 failures → `remaining_wait == 0`;
- `remaining_wait` for an unknown IP → 0;
- fail-open: inject a corrupt entry (`_failures["bad"] = object()` — reach into module state) → `remaining_wait("bad") == 0` and `record_failure("bad")` returns without raising;
- `reset` on an unknown IP → no error.
## Testing & Quality
- `uv run pytest tests/unit/test_rate_limit.py -v` green.
- Coverage: **>90%** on `app/core/rate_limit.py` (the fail-open branches included — they are the point).
## Completion Criteria
- [ ] `uv run pytest tests/unit/test_rate_limit.py -v` — all tests green.
- [ ] `uv run ruff check app/core/rate_limit.py tests/unit/test_rate_limit.py && uv run pyright` clean.
- [ ] `git diff --stat` for this task: exactly the two new files.
@@ -0,0 +1,33 @@
# Task 02 — Wire the limiter into both login routes + integration tests
**Phase:** `81_login_rate_limit` · **Story:** n/a (security hardening — audit SEC-03)
## Objective
`POST /api/login` and `POST /api/token-auth` enforce the 429 pre-check and record/reset the counter, and the integration suite pins the HTTP contract (429 + `Retry-After`, shared counter, success reset, unchanged 401s).
## Work
1. `app/api/auth.py` — add the module constant `TOO_MANY_DETAIL = "too many failed sign-in attempts — try again later"` and import the limiter (`from app.core import rate_limit`).
2. `app/api/auth.py::login` — insert at the TOP of the handler (before `check_password`):
- `ip = request.client.host or "unknown"` (the `or` guards a scope with no client — TestClient edge; fail-open spirit);
- `if (wait := rate_limit.remaining_wait(ip)) > 0: raise HTTPException(status_code=429, detail=TOO_MANY_DETAIL, headers={"Retry-After": str(wait)})`;
- in the failure branch: `rate_limit.record_failure(ip)` immediately before the 401;
- in the success branch: `rate_limit.reset(ip)` immediately before the 204.
3. `app/api/auth.py::token_auth` — the identical three insertions (same `ip` derivation, same `TOO_MANY_DETAIL`, same pre-check/record/reset points — the counter is shared by design).
4. `app/api/auth.py` module docstring — extend the two route bullets with the 429 case (one line each: "while the per-IP failure window is exhausted → 429 + `Retry-After`, one generic detail — audit SEC-03").
5. `tests/integration/test_auth_api.py` — extend (keep every existing pin intact; the TestClient's `request.client.host` is a fixed "testclient" value, so these tests share one counter — **add a fixture or helper that calls `rate_limit.reset("testclient")` between tests** so existing tests can't trip the limiter, and document why in the test module docstring):
- 10 × wrong-password `POST /api/login` → 11th → **429**, `detail == TOO_MANY_DETAIL`, `Retry-After` header present and `int(...) > 0`;
- after the 429, a CORRECT password → still 429 (the window is not bypassed by success — success only RESETS a clean counter; while blocked, the pre-check fires first; pin this: it is the intended semantics — an exhausted window stays exhausted until it slides);
- reset: with a clean counter, 9 failures + 1 success (204) → the next 9 failures are all 401 (the counter is back to 0 after the success);
- shared counter: 5 login failures + 5 token failures → the next token-auth attempt → 429;
- the existing non-throttled 401 pins (generic `invalid password` / `invalid token`) still green with the clean-counter fixture.
## Testing & Quality
- `uv run pytest tests/integration/test_auth_api.py -v` green (new + existing pins).
- `uv run pytest tests/unit/test_rate_limit.py -v` still green (no regressions to task 01).
- Coverage: **>90%** on the modified `app/api/auth.py` branches (429 hit/miss, record, reset paths all hit by the integration tests).
## Completion Criteria
- [ ] The 11th rapid failed login from one client is a 429 with `Retry-After` (integration pin); the token route shares the counter (integration pin).
- [ ] A success on a clean counter resets it (integration pin); a success while blocked does not unblock (integration pin).
- [ ] All pre-existing `test_auth_api.py` pins green (the clean-counter fixture is documented).
- [ ] `uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,28 @@
# Task 03 — Full gate + atomic commit
**Phase:** `81_login_rate_limit` · **Story:** n/a (security hardening — audit SEC-03)
## Objective
Run the complete phase gate, land the phase as one atomic commit, and move the phase directory to `complete/`.
## Work
1. **Full regression gate** (AGENTS.md rule 9):
- `uv run pytest` — unit + integration green.
- `uv run pytest --cov=app --cov-report=term-missing` — `app/` coverage **>90%**; confirm `app/core/rate_limit.py` and `app/api/auth.py` show no meaningful uncovered lines in the new branches.
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` — green **in isolation** (this phase's E2E contract: no UI change — `login.js`'s existing non-204 rendering covers the 429).
- `uv run ruff check . && uv run pyright` — clean.
2. **Manual live check** (keep the output in the session log): start the dev server (`uv run uvicorn app.main:app`) and, from the shell, 11 rapid wrong-password logins: `for i in $(seq 11); do curl -s -o /dev/null -w "%{http_code} " -X POST localhost:8000/api/login -H 'Content-Type: application/json' -d '{"password":"wrong"}'; done` → expect `401 ×10, 429` (and the 429 body carries `Retry-After`). Restart the server to clear the counter (the per-process state is by design).
3. **Commit** (AGENTS.md rule 8 — one atomic, Conventional-Commits commit, always `--no-gpg-sign`), staging `app/core/rate_limit.py`, `app/api/auth.py`, `tests/unit/test_rate_limit.py`, `tests/integration/test_auth_api.py`, and the phase files:
`fix(auth): rate-limit failed logins and token-auth attempts with 429 + Retry-After`
— body: security audit SEC-03 (2026-09-07) — neither login route throttled, so a reachable deployment took unlimited line-rate guesses; a stdlib-only sliding-window limiter (10 failures / IP / 15 min, shared by both routes, success resets, fail-open) now answers the 11th attempt with 429 + `Retry-After` while every non-throttled 401/204 is byte-identical. No dependency or frontend change.
4. Move the phase directory: `mv .agents/phases/todo/81_login_rate_limit .agents/phases/complete/` and include the move in the same commit.
## Testing & Quality
- This task IS the phase-level gate — the commands above are the completion evidence.
- Coverage: >90% held.
## Completion Criteria
- [ ] The live check shows `401 ×10, 429` (output kept in the session log); the 429 response carries `Retry-After`.
- [ ] `uv run pytest` green; coverage >90%; `tests/e2e/test_smoke.py` green in isolation; ruff + pyright clean.
- [ ] Exactly one new commit; `git show --stat HEAD` lists the five files above + the phase files (todo → complete move) — nothing else (in particular `pyproject.toml` / `uv.lock` / `frontend/` untouched).
- [ ] `.agents/phases/complete/81_login_rate_limit/` exists; `todo/` no longer contains it.
@@ -0,0 +1,71 @@
# Phase 82 — Security headers on every response: strict CSP, no-framing, nosniff
**Source:** `.agents/remediation_plan.md` SEC-04 (security audit 2026-09-07, severity Medium — "No security headers (CSP, X-Frame-Options / frame-ancestors)")
**Story:** n/a (security hardening — audit-derived, no user story)
**Context:** no response in the app carries `Content-Security-Policy`, `X-Frame-Options`, or `X-Content-Type-Options` (checked `app/main.py`, `app/core/caching.py`, all templates — none do). The frontend is No-CDN and was verified for this phase: **no inline `<style>` blocks, no `style="…"` attributes, no `onclick`/`on*` handlers, no `javascript:` URLs, no external hosts** in any `frontend/*.html` or the JS-injected markup (inline `<svg>` elements are CSP-legal; the two `el.style.…` CSSOM writes in `app.js` are not CSP-blocked). `app/main.py` is the single app-assembly point (middleware added in order: `SessionMiddleware` → `configure_caching`'s `CachingMiddleware` → static mount last). `app/core/caching.py` is the existing transport-layer middleware precedent (but it BUFFERS page bodies — this phase's middleware must not).
## Objective
Every response — pages, static assets, API JSON, the SSE chat stream, even 404s — carries a strict same-origin CSP (`frame-ancestors 'none'` included → clickjacking closed), `X-Frame-Options: DENY`, and `X-Content-Type-Options: nosniff`, added by a header-only pure-ASGI middleware that never touches a body (the SSE stream passes through byte-identical).
## Audit basis (read this, not the chat)
- Every page (including the admin sign-in and the admin-only views) can be embedded in a third-party page's iframe: a malicious LAN page can overlay the admin UI and trick the signed-in owner into clicking Sync / Revoke / Delete-source actions (audit PoC: `<iframe src="http://<server>:8000/git-sources.html">` with a benign-looking overlay). `frame-ancestors 'none'` (in the CSP) + `X-Frame-Options: DENY` (legacy fallback) close it.
- The app serves no CSP today: the codebase is XSS-clean (escape-first renderer, `textContent` for data — audit-verified at every `innerHTML` site), but the CSP is the standard second line of defense against any future regression. It is cheap here precisely because of the No-CDN + no-inline-markup verification above: `default-src 'self'` is sufficient without any `'unsafe-inline'` — a genuinely strict policy.
- `nosniff` is the one-line belt-and-braces against MIME-confusion on the static mount.
## Owner decisions (chat, 2026-09-07 — recorded per AGENTS.md rule 3)
- **A1 — the exact policy:** `Content-Security-Policy: default-src 'self'; base-uri 'none'; frame-ancestors 'none'` — no `script-src`/`style-src`/`connect-src` entries (they inherit `default-src 'self'`); no `'unsafe-inline'` anywhere (verified unnecessary); no `report-uri`/`report-to` (no collector in the homelab — a report would just vanish).
- **A2 — header-only, pure ASGI:** a small pure-ASGI middleware (not `BaseHTTPMiddleware`) — it wraps `send` and writes headers on `http.response.start` only; no body drain, no buffering, so the SSE chat stream and every other response pass through byte-identical (the explicit contrast with `CachingMiddleware`, which does buffer page bodies).
- **A3 — outermost position:** registered in `app/main.py` AFTER `configure_caching(app)` (last `add_middleware` = outermost), so even responses built by the static catch-all / 404 handler carry the headers.
- **A4 — the three headers only:** CSP + `X-Frame-Options: DENY` + `X-Content-Type-Options: nosniff`. No `Referrer-Policy`, no `Permissions-Policy`, no HSTS (the app serves plain homelab HTTP — HSTS would be wrong here; TLS is the documented non-goal).
## Design (shared by all tasks — the executor reads this, not the chat)
- **`app/core/security_headers.py` (new)**:
```python
CSP = "default-src 'self'; base-uri 'none'; frame-ancestors 'none'"
class SecurityHeadersMiddleware:
"""Header-only, pure-ASGI — never reads or buffers a body (SSE-safe)."""
def __init__(self, app): self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def send_wrapper(message):
if message["type"] == "http.response.start":
headers = MutableHeaders(scope=message)
headers["Content-Security-Policy"] = CSP
headers["X-Frame-Options"] = "DENY"
headers["X-Content-Type-Options"] = "nosniff"
await send(message)
await self.app(scope, receive, send_wrapper)
```
Module docstring: the audit basis (SEC-04), the No-CDN verification that makes `'unsafe-inline'` unnecessary (re-verify if inline scripts/styles are ever introduced — the policy would break the page loudly, which is the feature), the pure-ASGI rationale (body untouched — SSE stream byte-identical, unlike `CachingMiddleware`'s page buffering).
- **`app/main.py`** — import + `app.add_middleware(SecurityHeadersMiddleware)` placed immediately AFTER the `configure_caching(app)` call (with a one-line comment: outermost on purpose — headers on every response incl. static 404s). Nothing else in `main.py` moves.
- **Not touched:** `app/core/caching.py`, any template, any JS, the API layer, the No-CDN surface.
## Dependencies
— (none; standalone transport hardening — wraps the whole app, no route or template change)
## Tasks
1. `01_headers_middleware.md` — `app/core/security_headers.py` + unit tests (incl. the SSE-stream passthrough pin).
2. `02_registration_and_integration.md` — `app/main.py` registration + integration tests on real app responses.
3. `03_e2e_and_commit.md` — the dedicated Playwright suite + full gate + atomic commit.
## Testing & Quality
- Unit — `tests/unit/test_security_headers.py` (new): a minimal ASGI test-app wrapped by the middleware → a plain response carries all three headers with the exact values; a 404-shaped response carries them too; a **streaming** response (a generator yielding two chunks, `media_type="text/event-stream"`) — the body is consumed byte-identical (both chunks, in order) AND the headers are present (the SSE passthrough pin); a non-http scope (e.g. `websocket`) passes through with the app called and no `send` wrapping error.
- Integration — `tests/integration/test_security_headers.py` (new): against the real app (the `conftest` client): `GET /` (a page) → all three headers, CSP exactly the A1 string; `GET /api/health` → all three; `GET /assets/styles.css` → all three; `GET /nope` (404) → all three. (If the existing `tests/integration/test_caching_revalidation.py` fixture pattern for the app client exists, reuse it — same setup, no new fixtures.)
- E2E (mandatory, dedicated suite — this phase changes what the browser receives): `tests/e2e/test_security_headers.py` — (1) navigate to the chat page: the navigation `response.headers` carry all three (CSP exact); (2) the page actually works under the CSP — no CSP violations: collect `console` messages during load + one chat turn is NOT required (mock LLM is not needed for a header test) — assert zero console messages matching `/Content Security Policy/` and that the stylesheet applied (computed `background-color` of `body` is not `rgba(0, 0, 0, 0)` — the page painted, not a CSP-broken white page); (3) navigate to `/sources.html` (a second page) → headers present, no CSP violations. Run in isolation per AGENTS.md rule 9.
- Coverage: **>90%** on `app/` — the new module is small and unit-exercised; `app/main.py`'s registration is covered by every integration/E2E boot.
## Completion Criteria
- [ ] `curl -sI localhost:8000/ | grep -iE "content-security-policy|x-frame-options|x-content-type"` shows all three with the exact A1 CSP value (manual check, output kept in the session log); same for `/api/health`, `/assets/styles.css`, and a 404 path.
- [ ] The SSE chat stream is byte-identical through the middleware: the unit streaming pin + the existing chat E2E suites (e.g. `tests/e2e/test_chat_rag.py`) green — a body-touching regression would break them.
- [ ] `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` green **in isolation** — headers on real page loads + zero CSP violations + the page painted.
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] No template/JS/`pyproject.toml`/`uv.lock` diff; `git diff --stat` limited to the two new files, `app/main.py`, the three test files, phase files.
- [ ] One atomic `--no-gpg-sign` commit (e.g. `fix(security): add CSP, X-Frame-Options and nosniff headers to every response`); phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **No-CDN contract intact** — the CSP is `default-src 'self'` because the audit verified zero external hosts and zero inline scripts/styles; introducing either later breaks pages loudly (the policy does the enforcing).
- **No HSTS / Referrer-Policy / Permissions-Policy** (owner A4) — plain homelab HTTP is the documented posture; the three headers above are the scope.
- **SSE byte-identity** — the middleware wraps `send` for headers only; any body change is a phase failure (the chat E2E suites are the tripwire).
@@ -0,0 +1,23 @@
# Task 01 — The pure-ASGI header middleware + unit suite
**Phase:** `82_security_headers` · **Story:** n/a (security hardening — audit SEC-04)
## Objective
`app/core/security_headers.py` exists exactly per the phase design block, and `tests/unit/test_security_headers.py` pins the header values, the 404 path, the SSE streaming passthrough (byte-identical body + headers present), and the non-http scope passthrough.
## Work
1. `app/core/security_headers.py` (new) — implement per the phase overview's design block: the `CSP` constant (the exact A1 string), `SecurityHeadersMiddleware` as a pure-ASGI class wrapping `send` (headers written on `http.response.start` via `starlette.datastructures.MutableHeaders(scope=message)`), the `scope["type"] != "http"` fast path, and the module docstring content listed in the design block (audit basis, the No-CDN verification, the pure-ASGI/SSE rationale).
2. `tests/unit/test_security_headers.py` (new):
- a tiny ASGI `test_app` (a plain 200 `Response`-like dict sequence, or starlette's `Response` wrapped in an ASGI callable) + the middleware: assert all three headers with exact values;
- a 404-shaped response (status 404) → same three headers;
- a streaming response (an async generator yielding `data: a\n\n` then `data: b\n\n`, `media_type="text/event-stream"`) → consume the full body via the ASGI `receive`/`send` protocol (drive it with a small async client loop, or `httpx.AsyncClient(transport=ASGITransport(...))` if already used in the unit suite) → body is exactly the two frames in order AND the three headers are present (the SSE pin);
- a `websocket` scope → the app is called, no crash, no header injection attempt.
## Testing & Quality
- `uv run pytest tests/unit/test_security_headers.py -v` green.
- Coverage: **>90%** on `app/core/security_headers.py` (every branch: http start, http other messages, non-http scope).
## Completion Criteria
- [ ] `uv run pytest tests/unit/test_security_headers.py -v` — all tests green, including the SSE byte-identity pin.
- [ ] `uv run ruff check app/core/security_headers.py tests/unit/test_security_headers.py && uv run pyright` clean.
- [ ] `git diff --stat` for this task: exactly the two new files.
@@ -0,0 +1,26 @@
# Task 02 — Register the middleware + integration tests on real app responses
**Phase:** `82_security_headers` · **Story:** n/a (security hardening — audit SEC-04)
## Objective
The middleware wraps the whole app (outermost), and the integration suite pins the three headers on a page, an API response, a static asset, and a 404 — against the real app.
## Work
1. `app/main.py` — import `SecurityHeadersMiddleware` and add `app.add_middleware(SecurityHeadersMiddleware)` immediately AFTER the `configure_caching(app)` call, with the one-line comment from the design block (outermost on purpose: headers on every response, including static catch-all 404s). Do not reorder or re-add any existing middleware.
2. `tests/integration/test_security_headers.py` (new) — use the same app/client fixture pattern as `tests/integration/test_caching_revalidation.py` (reuse its setup; do not duplicate the client factory if a shared helper exists):
- `GET /` → 200 + all three headers; `Content-Security-Policy` exactly `default-src 'self'; base-uri 'none'; frame-ancestors 'none'`;
- `GET /api/health` → 200 + all three;
- `GET /assets/styles.css` → 200 + all three;
- `GET /definitely-not-a-page` → 404 + all three (the static catch-all's 404 still carries them — the outermost-position proof);
- a regression pin: the page body is still the rewritten HTML with `?v=` asset refs (assert one `?v=` occurrence in the body — the two middlewares coexist and the caching rewrite still runs, i.e. header middleware did not swallow/alter the CachingMiddleware's work).
3. Run the existing caching integration suite (`tests/integration/test_caching_revalidation.py`) to prove no interaction regression (it must stay green untouched).
## Testing & Quality
- `uv run pytest tests/integration/test_security_headers.py tests/integration/test_caching_revalidation.py -v` green.
- Coverage: **>90%** on `app/` (the registration line is hit by every integration/E2E boot; no new uncovered branches).
## Completion Criteria
- [ ] All four real-response pins pass (page / API / asset / 404) with the exact CSP string.
- [ ] The `?v=` coexistence pin passes (caching rewrite intact under the new outermost middleware).
- [ ] `tests/integration/test_caching_revalidation.py` green untouched.
- [ ] `uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,37 @@
# Task 03 — Dedicated Playwright suite + full gate + atomic commit
**Phase:** `82_security_headers` · **Story:** n/a (security hardening — audit SEC-04)
## Objective
Prove the headers reach a real browser AND the strict CSP does not break the page (no violations, page painted), run the full gate, and land the phase as one atomic commit.
## Work
1. `tests/e2e/test_security_headers.py` (new) — follow the house E2E conventions (read `tests/e2e/conftest.py` + one existing suite, e.g. `test_header_consistency.py`, for the fixtures and the app-server pattern):
- **Headers on a real navigation:** `page.goto` the chat page (the shell at `/`) → the navigation response's headers carry all three, CSP exactly the A1 string; `page.goto` `/sources.html` → same (a second page).
- **No CSP violations:** attach a `page.on("console")` listener before navigation; after both loads assert zero console messages whose text matches `/Content Security Policy/i` (Chromium reports CSP denials to the console).
- **The page painted (CSP not breaking assets):** after the chat-page load, `document.querySelector('body')`'s computed `background-color` is not `rgba(0, 0, 0, 0)` (the dark-tech palette applied — styles loaded under the CSP) and the shell's root element is present (e.g. `#main` — the page booted, not a white-broken document).
- No LLM interaction needed (the page load is the contract) — the suite must not depend on the aipi endpoint.
2. **Manual live check** (keep the output in the session log): `curl -sI localhost:8000/ | grep -iE "content-security-policy|x-frame-options|x-content-type"` + the same for `/api/health`, `/assets/styles.css`, `/nope` (404).
3. **Full regression gate** (AGENTS.md rule 9):
- `uv run pytest` — unit + integration green.
- `uv run pytest --cov=app --cov-report=term-missing` — `app/` coverage **>90%**.
- `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` — green **in isolation** (this phase's mandatory E2E).
- ONE chat-stream suite to prove SSE byte-identity end-to-end: `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` green (the mock-LLM chat suite — a body-touching regression in the middleware would break the stream).
- `uv run ruff check . && uv run pyright` — clean.
4. **Commit** (AGENTS.md rule 8 — one atomic, Conventional-Commits commit, always `--no-gpg-sign`), staging `app/core/security_headers.py`, `app/main.py`, the three test files, and the phase files:
`fix(security): add CSP, X-Frame-Options and nosniff headers to every response`
— body: security audit SEC-04 (2026-09-07) — no response carried framing/XSS-mitigation headers: any page (incl. the admin UI) could be iframe-embedded for clickjacking. A header-only pure-ASGI middleware (outermost) now sets `default-src 'self'; base-uri 'none'; frame-ancestors 'none'` (no 'unsafe-inline' — the No-CDN + no-inline-markup audit made the strict policy possible), `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff` on every response including 404s; the SSE stream passes byte-identical (unit streaming pin + chat E2E regression). No template/JS/dependency change.
5. Move the phase directory: `mv .agents/phases/todo/82_security_headers .agents/phases/complete/` and include the move in the same commit.
## Testing & Quality
- E2E: the dedicated suite green in isolation (headers + zero CSP violations + painted page).
- Regression: `test_chat_rag.py` green (SSE byte-identity through the real middleware).
- Coverage: >90% held.
## Completion Criteria
- [ ] `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` green in isolation (both pages, exact CSP, no violations, page painted).
- [ ] `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` green (the stream is untouched).
- [ ] The `curl` header check output (4 URLs) kept in the session log.
- [ ] `uv run pytest` green; coverage >90%; ruff + pyright clean.
- [ ] Exactly one new commit; `git show --stat HEAD` lists the two app files, the three test files, and the phase files (todo → complete move) — nothing else.
- [ ] `.agents/phases/complete/82_security_headers/` exists; `todo/` no longer contains it.
@@ -0,0 +1,65 @@
# Phase 83 — Bound the anonymous saved-chat payload sizes at the schema boundary
**Source:** `.agents/remediation_plan.md` SEC-05 (security audit 2026-09-07, severity Medium — "Unbounded payload sizes on the anonymous public chat-save endpoints")
**Story:** n/a (security hardening — audit-derived, no user story)
**Context:** `app/schemas.py` — `ChatMessage` (`who`/`text: Field(min_length=1)` **no max**/`sources: list[SourceRef] | None`/`suggestions: list[str] | None`/`thinking: str | None` **no max**/`tools: list[ToolCall] | None`/`stopped`, `extra="forbid"`), `SourceRef` (`source`/`path`/`title` — all bare `str`, **no max** — shared with the server-built `ChatDoneEvent.sources` SSE shape), `ToolCall` (`name: str`/`argument: str | None` — bare, **no max**), `SavedChatCreate` (`title: max_length=500`/`messages: Field(min_length=1)` **no max_items**/`share`), `SavedChatUpdate` (same message list) — consumed by the **public** (no-session) routes `POST /api/chats` and `PUT /api/chats/{chat_id}` in `app/api/chats.py` (phase 55 A1: the write surface is public — the row id is the credential). `ChatMessage` is shared with `HistoryTurn` (`text: max_length=32_000`, `thinking: max_length=32_000` — the existing caps to mirror) and with the server-built `ChatDoneEvent`/SSE shapes. The DB side: `documents.source String(120)`, `documents.path String(1000)`, `documents.title String(500)` (the column lengths the caps mirror), `saved_chats.messages` is JSONB (unbounded at the DB level — the pydantic boundary is the only gate). `tests/unit/` has no existing `test_schemas.py`; `tests/integration/test_chats_api.py` exists (the phase-50/51/53/55 contract pins — the 201/404/409/share matrix).
## Objective
An anonymous `POST/PUT /api/chats` can no longer carry a single arbitrarily large string, an unbounded message list, or unbounded nested lists: every `ChatMessage`/`SourceRef`/`ToolCall`/messages-list field gets a pydantic cap (422 at the boundary, house style), sized to the realistic `bor.chat.v1` record the UI produces — while a normal save (the E2E suites' payloads) still lands 201 byte-for-byte.
## Audit basis (read this, not the chat)
- `ChatMessage.text` has `min_length=1` and **no `max_length`** (unlike its sibling `HistoryTurn.text` at 32 000), and NO list field anywhere in the saved-chat shape has `max_items` — an anonymous caller can POST one 200 MB JSON string (`{"messages":[{"who":"user","text":"aaa…"}]}`) and create unlimited such rows: an unauthenticated memory + storage DoS on the app's only anonymous write surface (audit PoC in the plan).
- The fix is boundary-only (the phase-56 house style: fail loud at the schema with a 422 — no route code changes at all; FastAPI's request validation rejects before the handler runs, nothing is stored).
- The caps mirror the EXISTING `HistoryTurn` caps for the text fields (32 000 — a single chat message longer than that is already rejected on the chat path, so a saved chat can never legitimately carry more) and the DB column lengths for the source-ref fields (a `SourceRef` is built from `documents` rows server-side — `source ≤120`, `path ≤1000`, `title ≤500` — so the SSE `done` event can never trip the new caps: the server's own values always fit).
## Owner decisions (chat, 2026-09-07 — recorded per AGENTS.md rule 3)
- **A1 — the exact caps** (pydantic boundary, 422 on overflow):
- `ChatMessage.text`: `max_length=32_000` (mirror `HistoryTurn.text`);
- `ChatMessage.thinking`: `max_length=32_000` (mirror `HistoryTurn.thinking`);
- `ChatMessage.sources`: `max_items=20` (top-N docs + agent reads — the UI shows a handful; 20 is 10× the realistic max);
- `ChatMessage.suggestions`: `max_items=50`, each item `max_length=200` (chips are short deterministic strings — `derive_suggestions` produces ≤ ~80 chars);
- `ChatMessage.tools`: `max_items=50` (one entry per tool call; the round cap is 10 and even generous multi-call turns stay far below 50);
- `SourceRef`: `source max_length=120`, `path max_length=1000`, `title max_length=500` (mirror the `documents` column lengths — server-built SSE values always fit);
- `ToolCall`: `name max_length=100`, `argument max_length=2000 | None` (the combined `source/path` identity is ≤ 120 + 1 + 1000; 2 000 is 2× headroom for a grep pattern);
- `SavedChatCreate.messages` / `SavedChatUpdate.messages`: `max_items=200` (well past any realistic conversation — the chat history budget itself is 40 turns — and far below a DoS-sized list).
- **A2 — boundary-only:** NO route/handler changes in `app/api/chats.py` (FastAPI validates the pydantic model before the handler — the 422 is the framework's standard validation response); the stored-row contract (JSONB round-trip, `extra="forbid"`, the null-safe restore path) is untouched.
- **A3 — `SourceRef` is shared:** the caps on `SourceRef` also constrain the client-side `sources` inside saved chats AND are satisfied by every server-built `ChatDoneEvent.sources` (column-length mirror) — the SSE path is provably unaffected (pinned by an integration test).
## Design (shared by all tasks — the executor reads this, not the chat)
- **`app/schemas.py`** — the ONLY file changed in `app/`:
- `SourceRef`: `source: str = Field(max_length=120)`, `path: str = Field(max_length=1000)`, `title: str = Field(max_length=500)` (docstring: caps mirror the `documents` column lengths — server-built SSE refs always fit; client-saved refs are bounded at the boundary).
- `ToolCall`: `name: str = Field(max_length=100)`, `argument: str | None = Field(default=None, max_length=2000)`.
- `ChatMessage`: `text: str = Field(min_length=1, max_length=32_000)`, `thinking: str | None = Field(default=None, max_length=32_000)`, `sources: list[SourceRef] | None = Field(default=None, max_items=20)`, `suggestions: list[str] | None = Field(default=None, max_items=50)` with item length enforced by `Field(max_length=200)` on the list item type (pydantic v2: annotate `list[Annotated[str, Field(max_length=200)]]` or a small `_Chip = Annotated[str, Field(max_length=200)]` alias — keep the JSON shape identical), `tools: list[ToolCall] | None = Field(default=None, max_items=50)`.
- `SavedChatCreate.messages` / `SavedChatUpdate.messages`: `Field(min_length=1, max_items=200)`.
- Update the affected docstrings: each cap's one-line rationale (the A1 mirror sources) — the file's dense-docstring house style.
- **Not touched:** `app/api/chats.py` (validation happens before the handlers — zero route diff), the frontend (the UI's real payloads are far inside every cap — the E2E save suites prove it), the DB (no migration — JSONB stays unbounded at rest; the boundary is the gate, A2).
- **422 shape:** FastAPI's standard validation error body (the house boundary response — same shape the existing `min_length` violations already produce; no custom error copy).
## Dependencies
— (none; boundary-only hardening on the completed phase-50/51/55 saved-chat surface — the 201/404/409/share contract is unchanged for in-cap payloads)
## Tasks
1. `01_schema_caps.md` — the `app/schemas.py` cap changes + the unit suite (one test per cap, boundary values included).
2. `02_integration_and_e2e.md` — the oversized-422 integration pins + the dedicated Playwright suite + the SSE `done`-event unaffected pin.
3. `03_verify_and_commit.md` — full gate (incl. the existing chat-save E2E suites) + atomic commit.
## Testing & Quality
- Unit — `tests/unit/test_schemas.py` (new; the first schema-boundary suite): for EACH cap — a value exactly at the cap validates; one past it raises a pydantic `ValidationError` naming the field (`text` 32_000/32_001, `thinking` same, `sources` 20/21 items, `suggestions` 50/51 items + a 200/201-char item, `tools` 50/51, `SourceRef` source/path/title 120/1000/500 boundaries, `ToolCall` name 100/101 + argument 2000/2001, `SavedChatCreate.messages` 200/201 items, `SavedChatUpdate.messages` 200/201) — plus a regression pin: a realistic `bor.chat.v1` payload (a few messages, sources, tools, thinking) validates cleanly and round-trips `model_dump()` (the stored-shape contract).
- Integration — `tests/integration/test_chats_api.py` (extend, existing pins intact): anonymous `POST /api/chats` with a 32_001-char text → **422** AND no row created (list stays the same length); `messages` with 201 items → 422; a 21-item `sources` list → 422; the existing 201 create / share / unshare / stale pins green; an SSE regression pin: a `ChatDoneEvent` built from a full-length `Document` row (source 120 / path 1000 / title 500 — construct the row values at the column maxima) still serializes (A3: the server-built refs fit the new caps — build the event and `model_dump()` it in an integration test next to the existing chat pins).
- E2E (dedicated suite — this phase's contract is reachable from a browser's network layer): `tests/e2e/test_chat_save_payload_limits.py` — using Playwright's API request context against the running app (the house pattern — `page.request` or the context's request API): anonymous `POST /api/chats` with an oversized text (e.g. 40_000 chars) → **422** (no session needed — the surface is public, exactly the audit vector); a normal small save → 201 + `id` in the body (the happy path still works end-to-end). Run in isolation per AGENTS.md rule 9.
- Regression E2E: the existing save flows stay green — `tests/e2e/test_chat_history.py` (save/restore through the real UI — the in-cap proof).
- Coverage: **>90%** on `app/` (the changed file is `app/schemas.py` — declarative, exercised by every unit/integration test).
## Completion Criteria
- [ ] Every A1 cap is pinned at both boundaries (at-cap passes, over-cap 422s) in `tests/unit/test_schemas.py`.
- [ ] Anonymous oversized `POST /api/chats` → 422 with **no row stored** (integration pin); the same vector from the Playwright suite → 422 (E2E pin).
- [ ] The SSE `done`-event pin passes at the column-maximum source-ref lengths (A3).
- [ ] `uv run pytest tests/e2e/test_chat_save_payload_limits.py -v --no-cov` green in isolation; `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` green (real UI saves unaffected).
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
- [ ] `git diff --stat` limited to `app/schemas.py`, the three test files, phase files (NO `app/api/chats.py` diff, no migration, no `pyproject.toml`/`uv.lock`/`frontend/` diff).
- [ ] One atomic `--no-gpg-sign` commit (e.g. `fix(api): bound anonymous saved-chat payload sizes at the schema boundary`); phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **A2 boundary-only** — no route/handler/migration change; the pydantic 422 IS the control (house style, phase 56 precedent).
- **Caps mirror existing invariants** (A1) — `HistoryTurn`'s 32 000 text cap and the `documents` column lengths are the sizing sources, so no legitimate payload (chat-path history OR server-built SSE refs) can ever trip a cap; only oversized anonymous input does.
- **JSONB stored shape untouched** — `extra="forbid"` and the null-safe round-trip contract (the phase-14/50 restore path) are byte-identical for in-cap payloads (the unit round-trip pin).
@@ -0,0 +1,32 @@
# Task 01 — The schema caps + unit boundary suite
**Phase:** `83_chat_save_payload_limits` · **Story:** n/a (security hardening — audit SEC-05)
## Objective
`app/schemas.py` carries every A1 cap from the phase overview, and `tests/unit/test_schemas.py` pins each cap at both boundaries plus the realistic-payload round-trip regression.
## Work
1. `app/schemas.py` — apply the caps exactly as the phase overview's design block lists them:
- `SourceRef` — `source`/`path`/`title` → `Field(max_length=120/1000/500)`;
- `ToolCall` — `name` → `Field(max_length=100)`, `argument` → `Field(default=None, max_length=2000)`;
- `ChatMessage` — `text` → `Field(min_length=1, max_length=32_000)`, `thinking` → `Field(default=None, max_length=32_000)`, `sources` → `Field(default=None, max_items=20)`, `suggestions` → a list of `_Chip` (`Annotated[str, Field(max_length=200)]`) with `Field(default=None, max_items=50)`, `tools` → `Field(default=None, max_items=50)`;
- `SavedChatCreate.messages` + `SavedChatUpdate.messages` → `Field(min_length=1, max_items=200)`;
- docstrings: one rationale line per cap group (the A1 mirror sources — `HistoryTurn` caps / `documents` column lengths), matching the file's dense style.
- Verify no JSON-shape change: the models still accept/reject exactly the same KEYS (`extra="forbid"` untouched) — only value bounds are added.
2. `tests/unit/test_schemas.py` (new) — one test per cap at BOTH boundaries (at-cap validates; one-over raises `ValidationError` — assert the failing field name via `e.errors()[0]["loc"]`):
- `ChatMessage.text` 32_000 / 32_001; `thinking` 32_000 / 32_001 (and `None` still valid);
- `sources` 20 / 21 items; `suggestions` 50 / 51 items + a single 200 / 201-char item; `tools` 50 / 51;
- `SourceRef.source` 120 / 121, `.path` 1000 / 1001, `.title` 500 / 501;
- `ToolCall.name` 100 / 101, `.argument` 2000 / 2001 (and `None` still valid);
- `SavedChatCreate.messages` 200 / 201 items, `SavedChatUpdate.messages` 200 / 201;
- the realistic-payload regression: a full `bor.chat.v1`-shaped `SavedChatCreate` (4–8 messages mixing user/brain, one brain message with `thinking` + `tools` + `sources`, one with `suggestions` + `stopped`) → validates, and `model_dump()` of the messages equals the input dict (the stored-shape round-trip contract, `None`-keys preserved).
## Testing & Quality
- `uv run pytest tests/unit/test_schemas.py -v` green.
- Coverage: **>90%** on `app/schemas.py` (declarative — exercised by every test in the new file).
## Completion Criteria
- [ ] `uv run pytest tests/unit/test_schemas.py -v` — every boundary test green (at-cap passes, over-cap 422-shaped `ValidationError` with the right `loc`).
- [ ] The round-trip regression test green (stored shape unchanged for in-cap payloads).
- [ ] `uv run pytest tests/unit/ -q` green (no unit regression — in particular any test that constructs `ChatMessage`/`SourceRef` values still passes).
- [ ] `uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,31 @@
# Task 02 — Integration pins + the dedicated Playwright suite
**Phase:** `83_chat_save_payload_limits` · **Story:** n/a (security hardening — audit SEC-05)
## Objective
The HTTP contract is pinned: anonymous oversized saves 422 and store nothing, the SSE `done`-event shape is provably unaffected at the column maxima, and the dedicated E2E suite drives the exact audit vector through a real browser's network layer.
## Work
1. `tests/integration/test_chats_api.py` — extend (every existing pin stays green; if the file's fixtures reset the DB between tests, the new tests follow the same pattern):
- anonymous `POST /api/chats` with one message whose `text` is 32_001 chars → **422** AND the subsequent admin `GET /api/chats` list length is unchanged (nothing stored);
- `POST /api/chats` with 201 messages (minimal valid each) → 422;
- `POST /api/chats` with one message carrying a 21-item `sources` list (valid `SourceRef` shapes) → 422;
- `PUT /api/chats/{id}` (an existing row) with an oversized message → 422 and the row content unchanged (a GET shows the original text);
- the happy path regression: a normal small save → 201 (the existing pins already cover this — confirm green).
2. **The A3 SSE pin** — in the same file (or the existing chat-API integration file where the `ChatDoneEvent` shape is already exercised — `tests/integration/test_chat_api.py`): build the event from maximum-length values — `SourceRef(source="s"*120, path="p"*1000, title="t"*500)` inside a `ChatDoneEvent(deflected=False, sources=[…], suggestions=[])` → `model_dump()` succeeds (the server-built refs fit the new caps; a failure here would mean the caps broke the SSE contract).
3. `tests/e2e/test_chat_save_payload_limits.py` (new) — house E2E conventions (read `tests/e2e/conftest.py` + `test_chat_history.py` for the server/client fixtures):
- the audit vector, end-to-end and anonymous (NO login): via the Playwright request API (`page.request.post("/api/chats", data={...})` on a fresh page) POST one message with a 40_000-char `text` → expect **422**;
- the happy path in the same suite: a small valid save → **201** with an `id` field (proves the boundary didn't break the real flow from the browser layer);
- no LLM dependency (the endpoints are DB-only).
## Testing & Quality
- `uv run pytest tests/integration/test_chats_api.py -v` green (new + existing).
- The A3 pin green wherever it lands.
- `uv run pytest tests/e2e/test_chat_save_payload_limits.py -v --no-cov` green **in isolation** (this phase's mandatory E2E).
- Coverage: **>90%** on `app/` (no new `app/` code — the gate is regression + boundary proof).
## Completion Criteria
- [ ] All four oversized-422 integration pins pass (text / message-count / sources / PUT), each with the "nothing stored / row unchanged" assertion.
- [ ] The A3 SSE pin passes at the column-maximum lengths.
- [ ] The dedicated E2E suite green in isolation (422 anonymous + 201 small save).
- [ ] `uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,28 @@
# Task 03 — Full gate + atomic commit
**Phase:** `83_chat_save_payload_limits` · **Story:** n/a (security hardening — audit SEC-05)
## Objective
Run the complete phase gate (including the real-UI save regression), land the phase as one atomic commit, and move the phase directory to `complete/`.
## Work
1. **Full regression gate** (AGENTS.md rule 9):
- `uv run pytest` — unit + integration green.
- `uv run pytest --cov=app --cov-report=term-missing` — `app/` coverage **>90%**.
- `uv run pytest tests/e2e/test_chat_save_payload_limits.py -v --no-cov` — green **in isolation** (this phase's E2E).
- `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` — green (the real UI save/restore flow with in-cap payloads — the no-regression proof for the surface the caps sit on).
- `uv run ruff check . && uv run pyright` — clean.
2. **Commit** (AGENTS.md rule 8 — one atomic, Conventional-Commits commit, always `--no-gpg-sign`), staging `app/schemas.py`, `tests/unit/test_schemas.py`, `tests/integration/test_chats_api.py` (+ the chat-API file if the A3 pin landed there), `tests/e2e/test_chat_save_payload_limits.py`, and the phase files:
`fix(api): bound anonymous saved-chat payload sizes at the schema boundary`
— body: security audit SEC-05 (2026-09-07) — `POST/PUT /api/chats` are public (phase 55) and every `ChatMessage`/list field was unbounded, so an anonymous caller could store arbitrarily large JSONB rows (memory + storage DoS). Pydantic caps at the boundary (text/thinking 32 000 mirroring `HistoryTurn`, `SourceRef` mirroring the `documents` column lengths, list `max_items`, `messages` ≤ 200) → 422 on overflow, nothing stored; the 201/404/share contract and the stored JSONB shape are unchanged for in-cap payloads (round-trip pin + real-UI E2E regression). Boundary-only: no route, migration, or frontend change.
3. Move the phase directory: `mv .agents/phases/todo/83_chat_save_payload_limits .agents/phases/complete/` and include the move in the same commit.
## Testing & Quality
- This task IS the phase-level gate — the commands above are the completion evidence.
- Coverage: >90% held.
## Completion Criteria
- [ ] `uv run pytest` green; coverage >90%; both E2E suites green (the dedicated one in isolation, `test_chat_history.py` as the regression).
- [ ] `uv run ruff check . && uv run pyright` clean.
- [ ] Exactly one new commit; `git show --stat HEAD` lists the files above + the phase files (todo → complete move) — in particular NO `app/api/chats.py`, NO `alembic/`, NO `frontend/`, NO `pyproject.toml`/`uv.lock`.
- [ ] `.agents/phases/complete/83_chat_save_payload_limits/` exists; `todo/` no longer contains it.
@@ -0,0 +1,63 @@
# Phase 84 — Mask credentials in docs-push and LLM error surfaces
**Source:** `.agents/remediation_plan.md` SEC-08 (security audit 2026-09-07, severity Medium — "Docs-push 502 surfaces git stderr verbatim (possible credential echo)"), with the SEC-13 fold-in (Low — "LLM error strings echo `llm_base_url`")
**Story:** n/a (security hardening — audit-derived, no user story)
**Context:** `app/api/sync.py` — the existing masker: `_CREDS_RE = re.compile(r"[A-Za-z0-9._~%*-]+:[A-Za-z0-9._~%*-]+@")` + `_sanitize_error(message)` (masks `user:pass@` in any error text; the sync failure detail runs through it — "no secrets in the UI"). `app/api/git_sources.py` — imports it (`from app.api.sync import _sanitize_error`) for upload-failure details. `app/api/doc_drafts.py::push_doc_draft` — the GAP: `except DocsPushError as exc: raise HTTPException(502, detail=str(exc))` — git's stderr verbatim (a `BOR_DOCS_REPO` URL with embedded credentials would reach the browser + server log on a failed push). `app/core/docs_push.py` — `push_document` raises `DocsPushError(str(err))` carrying `GitSyncError`'s "git … failed (exit N): <stderr>" text. `scripts/git_sync.py::run_git` — the single git invocation point (list args, no shell — the error text is git's own stderr). `app/rag/llm.py` — SEC-13: five error f-strings embed `self.settings.llm_base_url` raw (lines 288, 327, 331, 337, 486 — embed + chat + stream failure paths) — a base URL configured with embedded credentials would echo into SSE `error` frames and logs. `tests/integration/test_doc_drafts_api.py` (the existing 502-contract pins — git stderr in the detail) and `tests/unit/test_llm_client.py` (the error-string pins) are the regression anchors.
## Objective
No error surface ever ships a `user:pass@` credential: the sync masker moves to a shared core module, the docs-push 502 detail runs through it (SEC-08), and the LLM error construction sanitizes the base URL (SEC-13) — while every non-credential error string (the sync/git/LLM tests' pinned copies) stays byte-identical.
## Audit basis (read this, not the chat)
- SEC-08: `BOR_DOCS_REPO` is documented as "any remote (URL or local path)" — an `https://user:token@host/…` URL is a normal config shape. On a failed push (revoked token, network), git's stderr echoes the remote URL; `push_doc_draft` returns it verbatim as the 502 `detail` → the credential lands in the browser (admin screen) and in the server log (`logger` writes the 502? the detail is the response body — and `run_git`'s stderr also surfaces into any traceback logging). The sync path already solved this exact problem (`_sanitize_error`) — the docs-push path simply never got the treatment.
- SEC-13 (fold-in): the five `llm.py` error f-strings interpolate `llm_base_url` raw — same class of leak on the LLM side (the real deployment uses a bare URL + header key, so today it is latent).
- The masker is deliberately a NARROW regex (only the `user:pass@` userinfo shape — git/HTTP convention): it must not rewrite ordinary text (`a: b @ c` without the userinfo run, plain hosts, emails in prose stay untouched as long as they don't match the userinfo pattern — the existing sync tests pin the behavior; the move must be byte-identical).
## Owner decisions (chat, 2026-09-07 — recorded per AGENTS.md rule 3)
- **A1 — shared core module:** `app/core/errors.py` (new) owns `sanitize_error(message: str) -> str` — the regex + logic move VERBATIM from `app/api/sync.py` (same `_CREDS_RE` pattern, same `sub("*****@", …)` replacement — byte-identical behavior). `app/api/sync.py` keeps the private name as an alias (`from app.core.errors import sanitize_error as _sanitize_error`) so `app/api/git_sources.py`'s existing import and every sync test stay untouched (zero caller diff outside the two target surfaces).
- **A2 — SEC-08 application point:** `app/api/doc_drafts.py::push_doc_draft` — the `except DocsPushError` clause becomes `raise HTTPException(502, detail=sanitize_error(str(exc))) from None` (the ONLY line that changes there).
- **A3 — SEC-13 application point:** `app/rag/llm.py` — the five error f-strings sanitize the URL at construction: a module-level `self._base = sanitize_error(settings.llm_base_url)` is NOT introduced (the client is constructed with settings; simpler and more local: each f-string uses `sanitize_error(self.settings.llm_base_url)`). With a credential-free URL (every real deployment) `sanitize_error` is a no-op → the existing error-string pins in `tests/unit/test_llm_client.py` stay byte-identical green.
- **A4 — no other surfaces in scope:** the sync/git-sources upload details already sanitize; the chat SSE error frames carry fixed operator copy (not git/URL text) — untouched.
## Design (shared by all tasks — the executor reads this, not the chat)
- **`app/core/errors.py` (new):**
```python
_CREDS_RE = re.compile(r"[A-Za-z0-9._~%*-]+:[A-Za-z0-9._~%*-]+@")
def sanitize_error(message: str) -> str:
"""Mask user:pass@ userinfo in an error string (no secrets in the UI/logs)."""
return _CREDS_RE.sub("*****@", message)
```
Module docstring: the audit basis (SEC-08/SEC-13), the "narrow userinfo-regex only" contract (byte-identical for credential-free text — the sync tests are the proof), the replacement shape (`*****@` — the existing sync copy).
- **`app/api/sync.py`** — delete the local `_CREDS_RE` + `_sanitize_error` definitions; add `from app.core.errors import sanitize_error as _sanitize_error` (the `_run_sync` failure path and its tests see the identical function under the identical private name).
- **`app/api/doc_drafts.py`** — import `sanitize_error` from `app.core.errors`; the `push_doc_draft` except clause per A2; the route docstring's outcome-4 line updated (`502 with the SANITIZED git stderr in the detail — credential userinfo masked, the phase-59 `GitSyncError → detail` mapping kept`).
- **`app/rag/llm.py`** — import `sanitize_error`; the five f-strings (lines 288/327/331/337/486 today) wrap the URL: `f"embeddings request to {sanitize_error(self.settings.llm_base_url)} failed: {e}"` etc. (the message copy around it unchanged).
- **Not touched:** `scripts/git_sync.py` (the stderr source — unchanged), `app/core/docs_push.py` (raises the raw text — the SANITIZE point is the API boundary, where the response is built), the sync/upload failure paths' behavior (byte-identical via the alias).
## Dependencies
— (none; standalone error-surface hardening — builds on the completed phase-32 sync sanitizer and phase-59 docs-push; no behavior change for credential-free errors)
## Tasks
1. `01_shared_sanitizer.md` — `app/core/errors.py` + the sync alias refactor + unit suite.
2. `02_apply_docs_push_and_llm.md` — the docs-push 502 + the five LLM error sites + the integration/unit pins.
3. `03_verify_and_commit.md` — full gate + atomic commit.
## Testing & Quality
- Unit — `tests/unit/test_error_sanitization.py` (new): the exact existing sync behavior is pinned — `https://user:pass@host/x` → `https://*****@host/x`; multiple userinfo occurrences all masked; an ssh-style `git:token@host` userinfo masked; credential-free text (a git failure line with a bare `https://github.com/owner/repo.git` URL, a plain error sentence, an email-shaped `a@b.c` — no userinfo run) **byte-identical**; idempotent (sanitize(sanitize(x)) == sanitize(x)).
- Unit — `tests/unit/test_llm_client.py` (extend): with `llm_base_url="https://svc:topsecret@llm.local/v1"`, a forced transport failure on the embed path → the `EmbeddingError` message contains `https://*****@llm.local/v1` and NOT `topsecret` (one representative path — the embed failure; the other four sites share the same construction, pinned by the same test style if cheap); the credential-free base-URL error pins stay byte-identical (existing tests green).
- Integration — `tests/integration/test_doc_drafts_api.py` (extend): monkeypatch `app.core.docs_push.push_document` (or the doc_drafts module's reference) to raise `DocsPushError("git push failed (exit 128): fatal: Authentication failed for 'https://bot:ghp_LEAK@github.com/owner/docs.git/'")` → `POST /api/doc-drafts/{token}/push` → **502** whose `detail` contains `*****@github.com` and NOT `ghp_LEAK` (and the repo/exit/`fatal:` context still readable); the existing 502 pins (plain stderr detail) stay byte-identical green.
- Regression: `tests/integration/test_git_sources_upload.py` + the sync-status tests (the alias refactor's proof — the `_sanitize_error` import in `git_sources.py` still works; the sync `failed`-state error is masked exactly as before).
- E2E (isolation gate per AGENTS.md rule 9): `uv run pytest tests/e2e/test_smoke.py -v --no-cov` green — no UI change (the doc-edit screen renders whatever detail string the API returns; the shape is unchanged).
- Coverage: **>90%** on `app/` — the new module is two lines + fully unit-pinned; `app/rag/llm.py`'s changed lines hit by the new unit tests + the existing error-path tests.
## Completion Criteria
- [ ] A docs-push failure with userinfo in git's stderr → 502 detail shows `*****@` and never the token (integration pin); the row is untouched (status/branch/sha as found — the existing pin).
- [ ] A LLM transport failure with a userinfo-bearing `llm_base_url` → the error message is masked (unit pin); credential-free error strings byte-identical (existing pins green).
- [ ] `app/api/sync.py` no longer defines its own `_CREDS_RE`/`_sanitize_error` (the alias is the import); `git grep "_CREDS_RE" app/` shows only `app/core/errors.py`; the sync + git-sources upload suites green untouched.
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run pytest tests/e2e/test_smoke.py -v --no-cov` green in isolation; `uv run ruff check . && uv run pyright` clean.
- [ ] `git diff --stat` limited to `app/core/errors.py`, `app/api/sync.py`, `app/api/doc_drafts.py`, `app/rag/llm.py`, the three test files, phase files.
- [ ] One atomic `--no-gpg-sign` commit (e.g. `fix(security): mask credentials in docs-push and LLM error surfaces`); phase dir moved to `.agents/phases/complete/`.
## Locked decisions
- **Sanitize at the API boundary** (A2) — `DocsPushError` keeps carrying the full stderr (logs/inspection value); the RESPONSE is where the secret would leak, so that is where it is masked.
- **Byte-identical for credential-free text** (A1/A3) — the narrow-regex contract; the existing sync/LLM error pins are the tripwire, and they must pass unchanged.
- **No logging changes** — this phase changes what responses carry; the server-log side of SEC-08/SEC-13 is improved as a consequence (the 502 detail is the logged-adjacent surface) but no log-format change is in scope.
@@ -0,0 +1,29 @@
# Task 01 — The shared sanitizer module + sync alias refactor + unit suite
**Phase:** `84_docs_push_error_sanitization` · **Story:** n/a (security hardening — audit SEC-08)
## Objective
`app/core/errors.py` owns `sanitize_error` (verbatim behavior of the sync masker), `app/api/sync.py` becomes a thin alias caller (zero behavior change), and `tests/unit/test_error_sanitization.py` pins the masking + the byte-identical-for-plain-text contract.
## Work
1. `app/core/errors.py` (new) — per the phase overview's design block: `_CREDS_RE` (the exact pattern from `app/api/sync.py`) + `sanitize_error(message: str) -> str` (`_CREDS_RE.sub("*****@", message)`) + the module docstring content (audit basis SEC-08/SEC-13, the narrow-userinfo contract, the `*****@` replacement shape — the sync copy).
2. `app/api/sync.py` — remove the local `_CREDS_RE` definition and the `_sanitize_error` function body; add `from app.core.errors import sanitize_error as _sanitize_error`; the `_run_sync` failure path (`_status.error = _sanitize_error(str(e))`) and everything else in the file stays byte-identical (the private name keeps working — `app/api/git_sources.py`'s `from app.api.sync import _sanitize_error` continues to import the same function through the alias).
- Update the `app/api/sync.py` module docstring's mention of the masker (one line: it now lives in `app/core/errors.py`, imported under the private name).
3. `tests/unit/test_error_sanitization.py` (new):
- `https://user:pass@host/repo.git` → `https://*****@host/repo.git`;
- two userinfo occurrences in one string → both masked;
- `git push failed (exit 128): fatal: Authentication failed for 'https://bot:tok@github.com/o/r.git/'` → the token masked, `fatal:`/`exit 128`/host intact;
- byte-identical cases (the contract): a plain git error with a bare `https://github.com/owner/repo.git` (no userinfo), a sentence with a colon + space, an email `owner@example.com` (no userinfo run before the `@`… assert exactly what the regex does — if the pattern masks it, pin THAT and note it; the point is determinism, not guessing), the empty string;
- idempotence: `sanitize_error(sanitize_error(x)) == sanitize_error(x)` for the masked cases.
4. Run the sync-related suites to prove the alias refactor: `uv run pytest tests/integration/test_git_sources_upload.py -q` + any existing sync-status integration test file (`tests/integration/` — locate the sync suite, e.g. via `rg -l "sync" tests/integration/`) → all green untouched.
## Testing & Quality
- `uv run pytest tests/unit/test_error_sanitization.py -v` green.
- The sync/upload regression suites green (the alias is behavior-identical).
- Coverage: **>90%** on `app/core/errors.py` (trivial — both branches of the regex hit).
## Completion Criteria
- [ ] `git grep -n "_CREDS_RE" app/` shows exactly one definition — `app/core/errors.py` — and `app/api/sync.py` imports under the private alias.
- [ ] `uv run pytest tests/unit/test_error_sanitization.py -v` green.
- [ ] The sync/upload integration suites green with NO test edits.
- [ ] `uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,29 @@
# Task 02 — Apply the sanitizer to the docs-push 502 and the LLM error sites
**Phase:** `84_docs_push_error_sanitization` · **Story:** n/a (security hardening — audit SEC-08 + SEC-13)
## Objective
The two leaking surfaces are closed: `push_doc_draft`'s 502 detail is sanitized (SEC-08), and the five `llm.py` error f-strings sanitize the base URL (SEC-13) — with integration/unit pins for both and the credential-free error strings byte-identical.
## Work
1. `app/api/doc_drafts.py` — import `from app.core.errors import sanitize_error`; in `push_doc_draft`'s `except DocsPushError as exc:` clause change the raise to `raise HTTPException(status_code=502, detail=sanitize_error(str(exc))) from None`; update the route docstring's outcome-4 line (502 detail = the SANITIZED git stderr — userinfo masked, the `GitSyncError → detail` mapping otherwise kept) and the module docstring's one-line error-contract sentence if it names the raw stderr.
2. `tests/integration/test_doc_drafts_api.py` — extend (the existing 502 pins stay green):
- a draft with `status="draft"` + `docs_configured` (the file's existing configured-settings pattern — monkeypatch `get_settings` or use the fixture's env);
- monkeypatch the module-level `push_document` reference in `app.api.doc_drafts` to `raise DocsPushError("git push origin bor-docs failed (exit 128): fatal: Authentication failed for 'https://bot:ghp_LEAKTOKEN@github.com/owner/docs.git/'")`;
- `POST /api/doc-drafts/{token}/push` → **502**; the `detail` contains `*****@github.com` and `exit 128` and `fatal: Authentication failed` but NOT `ghp_LEAKTOKEN`;
- the row is untouched (status/branch/commit_sha as found — the existing pin style).
3. `app/rag/llm.py` — import `sanitize_error`; wrap the base URL in the five error f-strings (the lines building `EmbeddingError`/`LLMError` with `…to/from {self.settings.llm_base_url}…`): each becomes `{sanitize_error(self.settings.llm_base_url)}`; the surrounding copy is byte-identical.
4. `tests/unit/test_llm_client.py` — extend:
- a client built with `llm_base_url="https://svc:topsecret@llm.local/v1"` (the file's existing fake-settings pattern) + a forced transport failure on the EMBED path → the raised `EmbeddingError`'s message contains `https://*****@llm.local/v1` and NOT `topsecret`;
- if the file's structure makes it cheap, the same for one of the chat paths (the `chat_stream`/`chat` failure f-string) — otherwise the embed path alone plus a comment (the five sites share the construction; the byte-identical regression is the existing pins);
- the existing credential-free error-string pins (bare `https://aipi.example/v1`-style base URLs) stay byte-identical green.
## Testing & Quality
- `uv run pytest tests/integration/test_doc_drafts_api.py -v` green (new + existing).
- `uv run pytest tests/unit/test_llm_client.py -v` green (new + existing — the byte-identical proof).
- Coverage: **>90%** on `app/` — the changed lines in `doc_drafts.py`/`llm.py` are hit by the new pins; the other four LLM sites share the identical construction (the existing error-path tests still exercise them).
## Completion Criteria
- [ ] The 502 pin passes: `*****@github.com` present, `ghp_LEAKTOKEN` absent, context readable; the row untouched.
- [ ] The LLM unit pin passes: masked URL in the error message, `topsecret` absent; the pre-existing error-string tests green unchanged.
- [ ] `uv run ruff check . && uv run pyright` clean.
@@ -0,0 +1,26 @@
# Task 03 — Full gate + atomic commit
**Phase:** `84_docs_push_error_sanitization` · **Story:** n/a (security hardening — audit SEC-08 + SEC-13)
## Objective
Run the complete phase gate, land the phase as one atomic commit, and move the phase directory to `complete/`.
## Work
1. **Full regression gate** (AGENTS.md rule 9):
- `uv run pytest` — unit + integration green (the sync/upload + doc-drafts + LLM suites are the regression anchors for the alias refactor and the byte-identical contract).
- `uv run pytest --cov=app --cov-report=term-missing` — `app/` coverage **>90%**.
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` — green **in isolation** (this phase's E2E contract: no UI change — the doc-edit screen renders the detail string it always rendered; the shape is unchanged, only the userinfo run is masked).
- `uv run ruff check . && uv run pyright` — clean.
2. **Commit** (AGENTS.md rule 8 — one atomic, Conventional-Commits commit, always `--no-gpg-sign`), staging `app/core/errors.py`, `app/api/sync.py`, `app/api/doc_drafts.py`, `app/rag/llm.py`, `tests/unit/test_error_sanitization.py`, `tests/unit/test_llm_client.py`, `tests/integration/test_doc_drafts_api.py`, and the phase files:
`fix(security): mask credentials in docs-push and LLM error surfaces`
— body: security audit SEC-08 + SEC-13 (2026-09-07) — a docs-push failure returned git's stderr verbatim as the 502 detail (a `BOR_DOCS_REPO` URL with embedded credentials would reach the browser + logs), and the LLM error f-strings echoed `llm_base_url` raw; the sync-phase userinfo masker (`user:pass@` → `*****@`) now lives in `app/core/errors.py`, the docs-push 502 and the five LLM error sites run through it, and the sync import keeps working via a private alias. Credential-free error strings are byte-identical (existing pins green).
3. Move the phase directory: `mv .agents/phases/todo/84_docs_push_error_sanitization .agents/phases/complete/` and include the move in the same commit.
## Testing & Quality
- This task IS the phase-level gate — the commands above are the completion evidence.
- Coverage: >90% held.
## Completion Criteria
- [ ] `uv run pytest` green (all regression anchors included); coverage >90%; `tests/e2e/test_smoke.py` green in isolation; ruff + pyright clean.
- [ ] Exactly one new commit; `git show --stat HEAD` lists the files above + the phase files (todo → complete move) — in particular NO `scripts/`, NO `app/core/docs_push.py`, NO `frontend/`, NO `pyproject.toml`/`uv.lock`.
- [ ] `.agents/phases/complete/84_docs_push_error_sanitization/` exists; `todo/` no longer contains it.
@@ -0,0 +1,14 @@
All gates verified. The phase is complete — everything was already implemented by prior task runs; this pass verified each completion criterion end-to-end and found no defects.
**Phase 81 final verification report** (tasks 01–03 all present in `complete/`; no code changes needed this pass)
- Verified: `app/core/rate_limit.py` (stdlib sliding window, fail-open) + both login routes' 429 pre-check/record/reset wiring + all unit/integration pins present (11th-429, blocked-success-still-429, clean-counter reset, shared counter, autouse `clean_rate_limit` fixture documented).
- Live check (task 03): dev server + 11 rapid wrong logins → `401 ×10, 429`; 429 body carries `retry-after: 900` + generic detail; server restarted (per-process counter cleared by design).
- `uv run pytest tests/unit/test_rate_limit.py -v --no-cov` → 10 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1652 passed, **TOTAL 99%** (>90%; rate_limit.py 100%, auth.py 100%)
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` (isolation) → 3 passed
- `uv run ruff check . && uv run pyright` → All checks passed / 0 errors, 0 warnings
- Completion criteria: all met, except commit + phase-dir move — per harness rules I left all changes uncommitted in the working tree (harness commits atomically and moves the phase).
- Diff scope: exactly `app/core/rate_limit.py`, `app/api/auth.py`, `tests/unit/test_rate_limit.py`, `tests/integration/test_auth_api.py` + phase files; `pyproject.toml` / `uv.lock` / `frontend/` untouched.
- Deviations: none in code; commit/move deferred to harness as instructed.
- Next pending phase: `82_security_headers`.
@@ -0,0 +1,82 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 13%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 26%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 61%]
........................................................................ [ 65%]
........................................................................ [ 69%]
........................................................................ [ 74%]
........................................................................ [ 78%]
........................................................................ [ 82%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 95%]
.................................................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 62 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
-----------------------------------------------
TOTAL 3211 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,18 @@
Task 01 complete — all gates green.
**Implemented**
- `app/core/rate_limit.py` (new): stdlib-only sliding-window limiter — `MAX_FAILURES = 10` / `WINDOW_SECONDS = 900` module constants, per-IP `deque[float]` of `time.monotonic()` timestamps under a `threading.Lock`; `record_failure` (append + prune, never raises, drops the count silently on corrupt state), `remaining_wait` (0 when < 10 in-window; else `max(1, ceil(900 − (now − oldest)))`, never raises), `reset` (pop, no-op on unknown). Docstring carries the SEC-03 audit basis, shared-counter intent, fail-open rationale, and the `request.client.host` / no-proxy assumption.
- `tests/unit/test_rate_limit.py` (new): 10 tests pinning, in task order — 9 failures allowed → 10th blocks (`0 < wait ≤ 900`, int) → second IP independent → expiry via pre-seeded `WINDOW_SECONDS + 1` state AND a fake monotonic clock (whole-window wait from the oldest: 891 → 300 → clamped 1 → 0 the moment the oldest slides out) → `reset` unblocks → unknown IP 0 → fail-open on non-deque state and on a raising clock → `reset` no-op on unknown IP.
**Results**
- `uv run pytest tests/unit/test_rate_limit.py -v` → 10 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1648 passed; `app/core/rate_limit.py` **100%**, TOTAL **99%** (>90%)
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors
- `git status`: only the two new files are code changes (pre-existing untracked phase/report files untouched); no tracked file modified
**Notable decisions**
- Corrupt state entry → `record_failure` is a literal no-op (entry left in place, treated as not-blocked) per the phase's "record_failure no-ops" wording; over-denial is the only forbidden outcome.
- Removed one unreachable defensive branch after the first coverage run to reach 100% on the module.
- `.agents/PLAN.md` is absent from the repo (AGENTS.md references it); the phase overview's design block was followed as the binding spec — no deviation.
**Next pending task:** `02_auth_routes_wiring.md` (429 pre-check + bookkeeping on both login routes + integration tests).
@@ -0,0 +1,82 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 13%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 26%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 48%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 61%]
........................................................................ [ 65%]
........................................................................ [ 69%]
........................................................................ [ 74%]
........................................................................ [ 78%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 96%]
................................................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 40 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 62 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
-----------------------------------------------
TOTAL 3199 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,16 @@
**Task 02 complete — report**
**Implemented**
- `app/api/auth.py`: `TOO_MANY_DETAIL` constant + `rate_limit` import; on both `login` and `token_auth` — 429 pre-check (`Retry-After`, generic detail) at handler top, `record_failure` before the 401, `reset` before the 204 (shared per-IP counter); module + handler docstrings extended with the 429 case.
- `tests/integration/test_auth_api.py`: autouse `clean_rate_limit` fixture (`rate_limit.reset("testclient")` around every test — documented: TestClient's host is a fixed shared IP, counter is process state) + 4 new pins: 11th failed login → 429 + detail + `Retry-After` (≤ window); correct password while blocked → still 429; 9 failures + success → counter reset (next 9 all 401); 5 login + 5 token failures → 11th (valid token) → 429.
**Results**
- `uv run pytest tests/integration/test_auth_api.py tests/unit/test_rate_limit.py` → 27 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1652 passed; **99%** total (`app/api/auth.py` 100%, `app/core/rate_limit.py` 100%)
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed (isolation)
- `uv run ruff check . && uv run pyright` → clean (0 errors)
- `pyproject.toml`/`uv.lock` untouched; diff limited to `app/api/auth.py` + the two test files (plus task-01's two new files, untracked for the harness commit)
**Deviation**: used `request.client.host if request.client is not None else "unknown"` instead of the task's literal `or "unknown"` — starlette types `client` as optional; pyright (non-negotiable gate) requires the explicit check. Same fail-open semantics.
**Next pending task**: `.agents/phases/todo/81_login_rate_limit/03_verify_and_commit.md`
@@ -0,0 +1,82 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 13%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 26%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 61%]
........................................................................ [ 65%]
........................................................................ [ 69%]
........................................................................ [ 74%]
........................................................................ [ 78%]
........................................................................ [ 82%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 95%]
.................................................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 62 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
-----------------------------------------------
TOTAL 3211 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,15 @@
**Task 03 (phase-level gate) complete — report**
**Gate results (all run in this session):**
- `uv run pytest` → 1652 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 99% total; `app/api/auth.py` 100%, `app/core/rate_limit.py` 100%
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed (in isolation, server on :8123)
- `uv run ruff check . && uv run pyright` → all checks passed; 0 errors
- Live check (fresh `uv run uvicorn app.main:app` on :8000): 11 rapid wrong logins → `401 ×10, 429`; the 429 carries `retry-after: 900` + generic detail; correct password while blocked → 429; kill + restart → counter cleared (wrong login → 401). Server stopped, port free.
- `git diff` scope: only `app/api/auth.py`, `tests/integration/test_auth_api.py` (modified) + `app/core/rate_limit.py`, `tests/unit/test_rate_limit.py` (new); `pyproject.toml`/`uv.lock`/`frontend/` untouched (0 diff lines).
**Notable decisions:**
- Per harness rules (which override the task file), I did **not** run `git add`/`git commit` nor move the phase dir to `complete/` — the harness makes the atomic commit and the move. All changes left in the working tree.
- No defects found in tasks 01/02; one operational slip of my own (first restart check killed the wrong pid, so a stale blocked server answered) — redone cleanly with a full process kill; final results above are from a verified-fresh server.
**Next pending task:** `.agents/phases/todo/82_security_headers/01_headers_middleware.md` (phase 81 is fully gated — harness commit + move remains).
@@ -0,0 +1,82 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 13%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 26%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 61%]
........................................................................ [ 65%]
........................................................................ [ 69%]
........................................................................ [ 74%]
........................................................................ [ 78%]
........................................................................ [ 82%]
........................................................................ [ 87%]
........................................................................ [ 91%]
........................................................................ [ 95%]
.................................................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 62 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
-----------------------------------------------
TOTAL 3211 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,15 @@
All completion criteria verified green — no defects found, nothing to fix. Final report:
**Phase 82 (security headers) — final verification pass: all green**
- Verified prior-run implementation: `app/core/security_headers.py` (pure-ASGI, header-only, exact A1 CSP), registration in `app/main.py` after `configure_caching` (outermost), unit/integration/E2E suites.
- Deviation confirmed sound: `data:`-URI favicon (blocked by locked CSP) → static `frontend/assets/favicon.svg` in 5 templates + Containerfile `cp`; SVG element byte-identical to the old data-URI (verified programmatically); serves 200 with all three headers.
- Curl check (server booted like e2e conftest, log: `/tmp/curl_security_headers_final.log`): `/`, `/api/health`, `/assets/styles.css`, `/nope` (404) → all three headers, CSP exactly `default-src 'self'; base-uri 'none'; frame-ancestors 'none'`.
- `uv run pytest tests/unit/test_security_headers.py tests/integration/test_security_headers.py -v --no-cov` → 13 passed (incl. SSE byte-identity pin).
- `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` (isolated) → 2 passed (headers + zero CSP violations + painted page).
- SSE tripwire `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` → 3 passed.
- `uv run pytest --cov=app --cov-report=term-missing` → 1665 passed, app/ 99% (>90%); `uv run ruff check . && uv run pyright` → clean (0 errors).
- `git diff --stat` limited to phase-82 files + the two documented deviations (favicon set, `tests/unit/__init__.py`); no `pyproject.toml`/`uv.lock`/JS diffs.
- Commit + phase-dir move left to the harness per pipeline rules (not executed by me).
Next pending phase: `83_chat_save_payload_limits`.
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 38%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 51%]
........................................................................ [ 56%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 69%]
........................................................................ [ 73%]
........................................................................ [ 77%]
........................................................................ [ 82%]
........................................................................ [ 86%]
........................................................................ [ 90%]
........................................................................ [ 95%]
........................................................................ [ 99%]
......... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
--------------------------------------------------
TOTAL 3232 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,18 @@
Task 01 complete — all gates green.
**Implemented**
- `app/core/security_headers.py` (new): `CSP` constant (exact A1 string `default-src 'self'; base-uri 'none'; frame-ancestors 'none'`) + `SecurityHeadersMiddleware` — pure-ASGI, wraps `send`, writes CSP / `X-Frame-Options: DENY` / `X-Content-Type-Options: nosniff` on `http.response.start` only, non-http fast path, never touches a body (SSE-safe); module docstring carries the SEC-04 audit basis, No-CDN verification, and pure-ASGI/SSE rationale.
- `tests/unit/test_security_headers.py` (new, 8 tests): A1-string pin; plain 200 with exact header values; 404-shaped (empty header list); SSE `StreamingResponse` byte-identity pin (two frames in order, boundaries intact, headers present); message-level "only start is mutated" pin; http send-wrapped/receive-untouched identity; websocket + lifespan passthrough (original send/receive identity, zero injection).
**Test / lint / coverage**
- `uv run pytest tests/unit/test_security_headers.py -v --no-cov` → 8 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1660 passed, TOTAL 99% (>90%); `app/core/security_headers.py` 100%
- `uv run ruff check .` → All checks passed
- `uv run pyright` → 0 errors
- `git status` → only the two new files added (plus pre-existing harness artifacts under `.agents/`, untouched)
**Notable decisions**
- SSE pin accounts for starlette's own empty trailer frame + `; charset=utf-8` suffix (framework behavior, not the middleware).
- Tests drive raw ASGI via a small `asyncio.run` helper (no pytest-asyncio in the repo; matches `test_caching.py` style) and also use a real `StreamingResponse` for the SSE test.
**Next pending task:** `02_registration_and_integration.md` (`app/main.py` registration + integration tests on real app responses).
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 13%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 26%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 39%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 60%]
........................................................................ [ 65%]
........................................................................ [ 69%]
........................................................................ [ 73%]
........................................................................ [ 78%]
........................................................................ [ 82%]
........................................................................ [ 86%]
........................................................................ [ 91%]
........................................................................ [ 95%]
........................................................................ [ 99%]
.... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 62 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
--------------------------------------------------
TOTAL 3230 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,19 @@
All gates green. Final report:
**Phase 82 · Task 02 — Registration + integration tests (complete)**
Implemented:
- `app/main.py`: `SecurityHeadersMiddleware` registered immediately after `configure_caching(app)` — outermost on purpose (headers on every response incl. static-catch-all 404s); nothing else reordered.
- `tests/integration/test_security_headers.py` (new, 5 tests, shared `client` fixture — no new fixtures): `GET /`, `/api/health`, `/assets/styles.css`, `/definitely-not-a-page` (404) all carry the three headers with the exact A1 CSP; `?v=` rewrite coexistence pin (caching middleware intact under the header-only middleware).
Results:
- `uv run pytest tests/integration/test_security_headers.py tests/integration/test_caching_revalidation.py -v` → 10 passed (caching suite green untouched).
- `uv run pytest --cov=app --cov-report=term-missing` → 1665 passed, **99%** coverage (`security_headers.py` 100%).
- `uv run ruff check . && uv run pyright` → clean.
- SSE tripwire: `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` → 3 passed (stream byte-identical through the new middleware).
Notable decisions:
- **Defect fixed:** full-suite collection crashed with pytest "import file mismatch" — `test_security_headers.py` now exists in both `tests/unit/` and `tests/integration/` (both paths mandated by the phase). Fix: added `tests/unit/__init__.py` (unit modules import as `unit.*`), mirroring the existing `tests/e2e/__init__.py` precedent; `tests/integration/` deliberately stays a plain dir because 3 files there use sibling imports (`from test_chat_api import …`). One extra new file beyond the task's list — unavoidable given the phase's own "full `uv run pytest` green" gate.
- No `pyproject.toml`/template/JS diffs; no git add/commit (left in working tree for the harness).
Next pending task: `.agents/phases/todo/82_security_headers/03_e2e_and_commit.md` (dedicated Playwright suite + full gate + atomic commit).
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 38%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 51%]
........................................................................ [ 56%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 69%]
........................................................................ [ 73%]
........................................................................ [ 77%]
........................................................................ [ 82%]
........................................................................ [ 86%]
........................................................................ [ 90%]
........................................................................ [ 95%]
........................................................................ [ 99%]
......... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
--------------------------------------------------
TOTAL 3232 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,7 @@
**Phase 82 · Task 03 — E2E suite + full gate (complete; commit/move left to harness per pipeline rules)**
- New `tests/e2e/test_security_headers.py` (2 tests, house conftest, no LLM interaction): real `page.goto` of `/` and `/sources.html` → all three headers, CSP exactly the A1 string; console listener (attached pre-nav) asserts zero `/Content Security Policy/i` messages; chat page painted (canvas bg `rgb(15,10,10)` + `#main` booted).
- **Deviations (flagged):** (1) paint check pinned on the `<html>` canvas, not `body` — the phase-08 contract keeps `body` transparent by design, so the task's literal body check could never pass; (2) **audit miss fixed:** all 5 templates' `data:`-URI favicon is blocked by the locked `default-src 'self'` CSP (the mandated zero-violation gate was unmeetable). Fixed by serving `frontend/assets/favicon.svg` (byte-identical artwork) + `href="/assets/favicon.svg"` in the 5 templates + a Containerfile stage-1 `cp` (its asset-coverage pin required). The **locked A1 CSP string stays untouched** — no `img-src data:` weakening.
- Curl check (server on final tree; full output in `/tmp/curl_security_headers.log`): `/`, `/api/health`, `/assets/styles.css`, `/nope` all carry the exact A1 CSP + `DENY` + `nosniff` (incl. 404s); note `HEAD /api/health` → 404 is pre-existing (FastAPI 0.141 doesn't auto-register HEAD; the static catch-all answers it — middleware-proven status-neutral); `GET /api/health` → 200 with all three.
- Results: `uv run pytest tests/e2e/test_security_headers.py -v --no-cov` → 2 passed (isolated); `uv run pytest tests/e2e/test_chat_rag.py -v --no-cov` → 3 passed (SSE byte-identical); `uv run pytest --cov=app` → 1665 passed, **99%** (>90%); `uv run ruff check . && uv run pyright` → clean; template-touching E2E (`test_cache_busting`, `test_asset_cache_revalidation`, `test_dark_tech_theme`) → 15 passed.
- Next pending task: `.agents/phases/todo/83_chat_save_payload_limits` (after the harness commits + moves phase 82).
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 17%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 30%]
........................................................................ [ 34%]
........................................................................ [ 38%]
........................................................................ [ 43%]
........................................................................ [ 47%]
........................................................................ [ 51%]
........................................................................ [ 56%]
........................................................................ [ 60%]
........................................................................ [ 64%]
........................................................................ [ 69%]
........................................................................ [ 73%]
........................................................................ [ 77%]
........................................................................ [ 82%]
........................................................................ [ 86%]
........................................................................ [ 90%]
........................................................................ [ 95%]
........................................................................ [ 99%]
......... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 231 0 100%
--------------------------------------------------
TOTAL 3232 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,15 @@
All verification criteria are confirmed green. Final report:
**Phase 83 — final verification pass (all tasks already complete; verified, nothing to fix)**
- Verified `app/schemas.py` carries every A1 cap (text/thinking 32 000, sources 20, suggestions 50×200, tools 50, SourceRef 120/1000/500, ToolCall 100/2000, messages ≤200) — boundary-only, `app/api/chats.py` untouched
- Verified all test pins exist and pass: 32 unit boundary tests (both edges of every cap + round-trip), 4 oversized-422 integration pins (text/201-msgs/21-sources/PUT, each with nothing-stored/row-unchanged assertions), A3 SSE done-event pin at column maxima, dedicated E2E (anon 40 000-char POST → 422 + small save → 201)
- `uv run pytest tests/unit/test_schemas.py -v` — 32 passed
- `uv run pytest tests/integration/test_chats_api.py` — exit 0; A3 pin — passed
- `uv run pytest tests/e2e/test_chat_save_payload_limits.py -v --no-cov` — 2 passed (isolation); `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` — 5 passed
- `uv run pytest` — exit 0 (~1 704 tests, 0 fail/skip); `uv run pytest --cov=app` — TOTAL 99%, `app/schemas.py` 100% (>90% ✓)
- `uv run ruff check . && uv run pyright` — clean (0 errors)
- `git diff --stat` — only `app/schemas.py`, 3 test files (+`test_chat_api.py` A3 pin, sanctioned by task 02), phase files; no `app/api/chats.py`/`alembic`/`frontend`/`pyproject`/`uv.lock` diff ✓
- All completion criteria met; commit + phase move left to the harness per pipeline rules (changes stay in working tree)
- Deviation note: list caps use `max_length` instead of `max_items` — identical in pydantic 2.13 (`max_items` is a deprecated alias); both boundaries behaviorally pinned
- Next pending phase: `84_docs_push_error_sanitization`
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 76%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 93%]
........................................................................ [ 97%]
.............................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3233 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,16 @@
Task 01 complete. All gates green.
**Implemented**
- `app/schemas.py` (only file touched in `app/`): all A1 caps — `SourceRef` 120/1000/500 (documents column mirror), `ToolCall` name 100 / argument 2000, `ChatMessage` text/thinking 32 000 (HistoryTurn mirror), sources ≤20, suggestions ≤50 of `_Chip` (`Annotated[str, Field(max_length=200)]`), tools ≤50, `SavedChatCreate`/`SavedChatUpdate.messages` ≤200; dense rationale docstrings; `extra="forbid"`/keys untouched.
- `tests/unit/test_schemas.py` (new, 32 tests): every cap pinned at both boundaries with `ValidationError` `loc` assertions, `None`-validity pins, `extra="forbid"` + minimal-shape regressions, and the realistic `bor.chat.v1` round-trip (create + update, `None`-keys preserved).
**Results**
- `uv run pytest tests/unit/test_schemas.py -v` → 32 passed
- `uv run pytest tests/unit/ -q` → 1274 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1697 passed; `app/schemas.py` 100%, TOTAL 99% (>90% gate)
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors
- `git diff --stat` → `app/schemas.py` only (+ new untracked test file); no commit made
**Notable decision**: used `max_length` for the list caps instead of the design block's `max_items` — pydantic 2.13 deprecates `max_items` (import-time warnings), and the codebase's own precedent (`ChatRequest.history: list[HistoryTurn] = Field(..., max_length=100)`) uses `max_length`. Cap values and the 422 shape (`too_long`, same `loc`) are identical.
**Next pending task**: `.agents/phases/todo/83_chat_save_payload_limits/02_integration_and_e2e.md` (oversized-422 integration pins, dedicated Playwright suite, SSE done-event pin).
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 55%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 72%]
........................................................................ [ 76%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 89%]
........................................................................ [ 93%]
........................................................................ [ 97%]
......................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3233 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,15 @@
All gates green. Final verification of the complete picture:
- Integration: 91 passed (58 in `test_chats_api.py` incl. 4 new pins, 33 in `test_chat_api.py` incl. the A3 SSE pin)
- E2E dedicated suite: 2 passed in isolation; regression `test_chat_history.py`: 5 passed
- Full suite: 1702 passed, 99% coverage on `app/`
- `ruff check .` clean, `pyright` 0 errors
- Diff limited to `app/schemas.py`, `tests/unit/test_schemas.py`, the two integration files, the new E2E file, and phase files — no `app/api/chats.py`, no migration
**Task 02 report**
- Extended `tests/integration/test_chats_api.py`: 4 anonymous oversized-422 pins (32_001-char text, 201 messages, 21-item `sources`, PUT oversized) — each asserting nothing stored / row unchanged via the admin list/GET.
- Added the A3 SSE pin to `tests/integration/test_chat_api.py`: `ChatDoneEvent` built at the column maxima (120/1000/500) `model_dump()`s byte-identical; one-over caps raise (caps sit exactly on the `documents` column lengths).
- New `tests/e2e/test_chat_save_payload_limits.py`: the exact audit vector (40_000-char text, NO session) through `page.request.post` → 422 + title-scoped "nothing stored" proof; in-cap small save → 201 with valid `id` (row cleaned up via admin delete, house convention).
- Notable: Playwright's `APIRequestContext` rejects relative URLs — used `app_url + "/api/chats"` (same network layer, absolute form); ruff import order in `test_chat_api.py` corrected.
- Results: `uv run pytest tests/integration/test_chats_api.py -v` 58 passed · `tests/e2e/test_chat_save_payload_limits.py -v --no-cov` 2 passed (isolated) · `tests/e2e/test_chat_history.py -v --no-cov` 5 passed · `uv run pytest --cov=app` 1702 passed, 99% · `ruff check .` clean · `pyright` 0 errors.
- Next pending task: `03_verify_and_commit.md` (full gate + atomic commit).
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 76%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 93%]
........................................................................ [ 97%]
.............................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3233 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,13 @@
All gates green. Task 03 complete — report:
**Phase 83 gate (task 03) — all green**
- Verified the phase 01/02 implementation: all A1 caps in `app/schemas.py` (text/thinking 32 000, `SourceRef` 120/1000/500, `ToolCall` 100/2000, lists 20/50/50, `_Chip` ≤ 200, messages ≤ 200), boundary unit suite, 4 oversized-422 integration pins + "nothing stored", A3 SSE pin at column maxima, dedicated Playwright suite (anonymous 422 + 201 happy path).
- Investigated a suspected defect (`max_items` vs `max_length` on lists): tried the design's `max_items`, but pydantic 2.13 deprecates it for sequences (warnings in suite output) and the house style (existing `history` field) uses `max_length` — reverted; behavior identical, suite warning-free.
- `uv run pytest` → exit 0, all pass
- `uv run pytest --cov=app --cov-report=term-missing` → 1702 passed; `app/` **99%** (>90%), `app/schemas.py` 100%
- `uv run pytest tests/e2e/test_chat_save_payload_limits.py -v --no-cov` → 2 passed (isolation)
- `uv run pytest tests/e2e/test_chat_history.py -v --no-cov` → 5 passed (real-UI save regression)
- `uv run ruff check . && uv run pyright` → "All checks passed!" / "0 errors, 0 warnings, 0 informations"
- Diff scope verified: `app/schemas.py` + 3 test files (+ A3 pin in `test_chat_api.py`, explicitly allowed) + phase-file moves; no `app/api/chats.py`, no `alembic/`, no `frontend/`, no `pyproject.toml`/`uv.lock`.
- Per executor protocol: no commit / no phase-dir move made — all changes left in the working tree for the harness's atomic commit.
- Next pending: phase `84_docs_push_error_sanitization` (phase 83 is fully done).
@@ -0,0 +1,84 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 38%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 59%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 76%]
........................................................................ [ 80%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 93%]
........................................................................ [ 97%]
.............................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 101 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3233 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,11 @@
**Phase 84 — final verification pass: all green, no defects found**
- Verified implementation: `app/core/errors.py` (verbatim lift of sync masker), `app/api/sync.py` alias import, docs-push 502 `detail=sanitize_error(str(exc))`, all five `llm.py` error sites sanitized; new/extended test pins in place
- Tests: `uv run pytest` → **1714 passed, 0 failed**; targeted pins (new unit ×2 + integration ×1, existing 502 pin) → 13 passed; sync/git-sources regression → 67 passed
- Coverage: `uv run pytest --cov=app --cov-report=term-missing` → **99%** (`app/core/errors.py` 100%, `app/rag/llm.py` 100%) — >90% met
- E2E isolation: `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → **3 passed**
- Lint/types: `uv run ruff check .` → clean; `uv run pyright` → **0 errors**
- Criteria: 502 masks `*****@`/never token + row untouched ✅; LLM base-URL masked, credential-free strings byte-identical ✅; `_CREDS_RE` only in `app/core/errors.py` (working-tree grep) ✅; full gate green ✅; `git diff --stat` limited to the 4 app files + 2 modified test files + 3 phase task files (untracked: new module, new unit test, complete/ dir, reports, audit plan) ✅
- Commit/phase move left to the harness per instructions (task files already in `complete/`)
- No deviations; nothing to fix
- Next pending phase: **85_mobile_menu_gate_overlap**
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
.......................................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,18 @@
All criteria verified. Final report:
**Task 01 — shared sanitizer module + sync alias refactor: complete**
- `app/core/errors.py` (new): owns `_CREDS_RE` (exact phase-32 pattern) + `sanitize_error()` (`sub("*****@")`), with module docstring covering SEC-08/SEC-13 audit basis, narrow-userinfo byte-identity contract, and the `*****@` replacement shape.
- `app/api/sync.py`: local `_CREDS_RE`/`_sanitize_error` deleted; now `from app.core.errors import sanitize_error as _sanitize_error`; one-line module-docstring note added; now-unused `import re` removed. `_run_sync` failure path byte-identical; `git_sources.py`/`docs.py` alias imports untouched.
- `tests/unit/test_error_sanitization.py` (new, 9 tests): single/multiple userinfo masking, SEC-08 git push-failure line (token masked, `fatal:`/`exit 128`/host intact), byte-identity pins (bare https URL, colon+space prose, email `owner@example.com` — pattern doesn't match it, pinned as unchanged, empty string), idempotence on all masked forms, and `sync_api._sanitize_error is sanitize_error` alias identity.
Results:
- `uv run pytest tests/unit/test_error_sanitization.py -v` → 9 passed
- `uv run pytest tests/integration/test_git_sources_upload.py tests/integration/test_sync_api.py` → 44 passed (no test edits)
- `uv run pytest --cov=app --cov-report=term-missing` → 1711 passed; `app/core/errors.py` 100%, app/ total 99%
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors
- `grep -rn _CREDS_RE app/` → exactly one definition, in `app/core/errors.py`
Decisions: none — verbatim lift per design block. No pre-existing defects found. No git commits made (left for harness).
Next pending task: `.agents/phases/todo/84_docs_push_error_sanitization/02_apply_docs_push_and_llm.md`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
....................................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3235 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,18 @@
All green. Final report:
**Phase 84, Task 02 — docs-push 502 + LLM error sanitization**
Implemented:
- `app/api/doc_drafts.py`: `push_doc_draft` 502 detail now `sanitize_error(str(exc))` (SEC-08); route + module docstrings updated to name the SANITIZED stderr
- `app/rag/llm.py`: all five error f-strings (embed, chat×3, chat_stream) wrap `self.settings.llm_base_url` in `sanitize_error(...)` (SEC-13) — surrounding copy byte-identical
- `tests/integration/test_doc_drafts_api.py`: new pin — faked `DocsPushError` with `bot:ghp_LEAKTOKEN@github.com` → 502 detail has `*****@github.com`, `exit 128`, `fatal: Authentication failed`, never `ghp_LEAKTOKEN`; row untouched
- `tests/unit/test_llm_client.py`: new pins — embed + chat transport failures with `https://svc:topsecret@llm.local/v1` → masked `https://*****@llm.local/v1`, no `topsecret`; plus a byte-identical credential-free message assertion
Results:
- `uv run pytest tests/integration/test_doc_drafts_api.py -v` — 28 passed; `uv run pytest tests/unit/test_llm_client.py -v` — 57 passed (pre-existing error-string pins green unchanged)
- `uv run pytest --cov=app --cov-report=term-missing` — full suite green, TOTAL 99% (>90%); doc_drafts.py / llm.py / errors.py 100%
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` — 3 passed in isolation
- `uv run ruff check .` — clean; `uv run pyright` — 0 errors
- No deviations; diff limited to the four task files (task 01's files remain in the tree for the harness's atomic phase commit)
Next pending task: `.agents/phases/todo/84_docs_push_error_sanitization/03_verify_and_commit.md`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
.......................................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,21 @@
All gates verified. Every task-03 completion criterion is met in the working tree; no defects found in prior tasks' work.
**Phase 84 gate report — task 03 (verify) complete**
Implemented (by tasks 01–02, verified by me):
- `app/core/errors.py` (new): `sanitize_error` — verbatim lift of the sync `user:pass@` masker
- `app/api/sync.py`: local regex+function deleted, private alias import (callers `git_sources.py`/`docs.py` untouched)
- `app/api/doc_drafts.py`: docs-push 502 detail runs through `sanitize_error` (SEC-08)
- `app/rag/llm.py`: all five LLM error f-strings sanitize `llm_base_url` (SEC-13)
- New/extended pins: `tests/unit/test_error_sanitization.py`, `test_llm_client.py` (mask + byte-identical), `test_doc_drafts_api.py` (502 masks `ghp_LEAKTOKEN`, row untouched)
Gate results (all green):
- `uv run pytest` → 1714 passed, exit 0 (regression anchors incl. sync/upload, doc-drafts, LLM)
- `uv run pytest --cov=app --cov-report=term-missing` → `app/` TOTAL **99%** (>90% required)
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed in isolation
- `uv run ruff check . && uv run pyright` → clean (0 errors/warnings)
- `grep -rn _CREDS_RE app/ scripts/` → only `app/core/errors.py`; diff scope limited to the 7 code/test files + phase files; NO `scripts/`, `docs_push.py`, `frontend/`, `pyproject.toml`/`uv.lock`
Deviations (per harness executor rules, overriding task step 2–3): no `git add`/`commit` and no phase-dir move by me — all changes left in the working tree for the harness's one atomic `fix(security)` commit + todo→complete move.
Next pending task: none in phase 84 (03 was last); next phase: `85_mobile_menu_gate_overlap`.
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 21%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 42%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 63%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 84%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
.......................................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,12 @@
All verification complete. Final report:
**Phase 85 final verification pass — all green** (tasks 01–03 already complete; independently re-verified from scratch)
- Verified fix in tree: `styles.css` `.auth-gate` z-index 500→15 + rewritten stacking comment (no `z-index: 500` left); `index.html`/`document.html` comment-only; `token-gate.js` docstring-only (logic byte-identical); `test_mobile_hamburger_nav.py` real-click conversion + new `test_anonymous_toggle_tappable_with_gate_up`; new `tests/unit/test_gate_header_stacking.py` (3 pins); `test_api_tokens.py` untouched
- `uv run pytest` → 1717 passed, 1 warning (exit 0)
- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90%)
- E2E in isolation: `test_mobile_hamburger_nav.py` **8 passed**; `test_api_tokens.py` **9 passed** (unchanged); `test_smoke.py` **3 passed**; `test_document_viewer.py` **7 passed**
- `uv run ruff check . && uv run pyright` → clean / 0 errors
- Live probe (375×812, anonymous, fresh server): on `/` and `/history.html` `elementFromPoint` at toggle → SVG `path`, never `#auth-gate`; real click opens menu (`aria-expanded=true`); exactly one visible nav link (Chat) + sign-in copy; Chat link topmost over gate; `#main` inert — criteria 1, 2, 3 confirmed directly
- Criteria: (1) real-click menu on / + /history ✅ (2) dropdown above gate ✅ (3) anonymous contents + #main inert ✅ (4) admin byte-identical (phase-46 tests green) ✅ (5) doc-viewer gate under bar (CSS pin + doc-viewer suite) ✅ (6) full gate green ✅ (7) diff scoped to the 5 code files + new unit test + phase files, nothing in `app/` ✅ (8) commit + todo→complete move left to harness per executor rules (task files already in `complete/`)
- Note: `.agents/remediation_plan.md` (untracked) is a pre-existing artifact of the earlier security audit — left untouched; a missing "N passed" line seen mid-pass was my own `-q`+addopts `-q` = `-qq` quirk, not a defect
- Next pending phase: `86_history_page_width`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,17 @@
Task 01 complete — all green.
**Implemented**
- `frontend/assets/styles.css`: `.auth-gate` `z-index: 500 → 15` (the one-value fix, A1) + stacking comment rewritten to the phase-85 contract (15 gate < 20 header < 21 dropdown; skip-link 100 / 1000-family overlays unaffected; `#main` lock is JS, not z-order)
- `frontend/index.html` / `frontend/document.html`: gate-section comments reworded (header stays reachable; `#main` stays inert) — markup byte-identical, comment-only
- `frontend/assets/token-gate.js`: docstring sweep only — "only interactive surface" → "only CONTENT-level interactive surface" + phase-85 note; logic byte-identical
- New `tests/unit/test_gate_header_stacking.py`: 3 source-level z-order pins (gate=15, header=20 with gate<header, mobile `.app-nav` keeps 21>15) + the gate still covers content (fixed/inset:0, `[hidden]` display:none)
**Test / lint results**
- `uv run pytest tests/unit/test_gate_header_stacking.py -v` → 3 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1717 passed, `app/` TOTAL **99%** (>90% ✓)
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
- `git diff --stat` limited to the 4 frontend files (CSS value + comments only) + the new test; `grep "z-index: 500"` no longer matches anything
**Decisions**: the dropdown's `z-index: 21` lives on the base `.app-nav` rule (shared with `.is-open`), so the pin targets `.app-nav` in the ≤640px block — same as the existing phase-46 pin; value 15 kept as owner-decided.
**Next pending task**: `02_e2e_pin_updates.md` (same phase — real-click conversion + TODO-regression E2E pin).
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,19 @@
All green. Task 02 is complete.
**Implemented**
- `tests/e2e/test_mobile_hamburger_nav.py`: retired `_js_open_menu` (JS-dispatch drive) — anonymous tests now use the real-click helper `_open_menu` (`page.click("#nav-toggle")`), docstring rewritten to the phase-85 contract (gate z 15 below header z 20)
- New regression pin `test_anonymous_toggle_tappable_with_gate_up`: with the gate up (`#main` inert), `elementFromPoint` at the toggle center resolves to the toggle/SVG child, never `#auth-gate`; real click opens the menu above the gate (Chat link visible + topmost at its center, gate still up); Esc settles closed
- Module docstring: phase-85 stacking paragraph + numbered mapping entry; all phase-79 "intercepted by the gate" test docstrings/comments swept (assertions kept exactly: one visible "Chat" link, Esc refocus, outside-click at (10,780) doesn't close, 180ms pins)
- `test_api_tokens.py`: verified no hamburger pins (grep clean); run unchanged, green
**Results**
- `uv run pytest tests/e2e/test_mobile_hamburger_nav.py -v --no-cov` → 8 passed (isolation)
- `uv run pytest tests/e2e/test_api_tokens.py -v --no-cov` → 9 passed, file unchanged (isolation)
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1717 passed, coverage 99% (>90%)
- `uv run ruff check . && uv run pyright` → clean
- Criterion grep `querySelector('#nav-toggle').click()` → no matches; my diff touches only the one E2E file (frontend changes in tree are task-01's, committed atomically by the harness at phase end)
**Deviations** — none; the JS helper was folded into `_open_menu` rather than duplicated (identical real-click body + same 3 assertions).
**Next pending task:** `.agents/phases/todo/85_mobile_menu_gate_overlap/03_verify_and_commit.md`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,11 @@
Screenshot confirms the fix visually: the anonymous menu (Chat + Sign in) renders **above** the gate card. Phase 85 is fully gated.
**Report — Phase 85, task 03 (verify & commit)**
- Full gate green: `uv run pytest` → 1717 passed; `--cov=app` → TOTAL **99%** (>90% floor, no `app/` change)
- E2E in isolation: `test_mobile_hamburger_nav.py` → 8 passed (real-click contract + new `elementFromPoint` pin); `test_api_tokens.py` → 9 passed, **file unchanged** (phase-79 lock contract intact); `test_smoke.py` → 3 passed; `test_document_viewer.py` → 7 passed
- `uv run ruff check . && uv run pyright` → clean (0 errors/warnings)
- Live check (dev server :8000, 375×812, not signed in): 12/12 checks passed — real tap opens the menu above the gate (Chat + sign-in visible, token card behind/below), `elementFromPoint` at toggle resolves to the SVG (never `#auth-gate`), wrong-token error line + menu independence (Esc closes, gate stays), admin menu unchanged; log + 2 screenshots in `.agents/reports/85_mobile_menu_gate_overlap/`
- Working tree verified: only `styles.css` (z 500→15 + comment), `index.html`/`document.html` (comments only), `token-gate.js` (docstring only), `test_mobile_hamburger_nav.py`, new `tests/unit/test_gate_header_stacking.py` + phase files — nothing in `app/`, `pyproject.toml`, `uv.lock`
- Deviation (per executor rules, overrides task steps 3–4): **no `git commit` made and phase dir not moved** — all changes left in the working tree for the harness's single atomic phase commit and the `todo/ → complete/` move
- Next pending task: none in this phase (task 03 was the last); following phases: `86_history_page_width`, `87_big_read_progress`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 67%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 92%]
........................................................................ [ 96%]
............................................................. [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@@ -0,0 +1,9 @@
All criteria verified. Final report:
**Phase 86 final verification pass — all green, no defects found.**
- **Verified (previously implemented):** `position: relative` on shared `.table-wrap` (the one-rule A3 fix, +12 lines in `frontend/assets/styles.css`); unit pin `tests/unit/test_table_wrap_stacking.py` (3 tests); E2E story `tests/e2e/test_history_page_width.py` (5 tests: History 626→≤375, Tokens 618→≤375, SPA switch with window-sentinel, RAG regression, desktop 1280×800)
- **Tests/lint (this pass):** `uv run pytest` → 1720 passed · `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90% floor) · E2E in isolation: `test_history_page_width.py` 5 passed, `test_chat_history.py` 5 passed, `test_api_tokens.py` 9 passed, `test_smoke.py` 3 passed · `uv run ruff check . && uv run pyright` → clean, 0 errors
- **Completion criteria:** ① 375px direct+SPA `scrollWidth ≤ innerWidth` on History/Tokens — PASS (E2E pins) ② in-card scroll preserved + full-width + a11y spans in DOM — PASS ③ Sources + desktop regression — PASS ④ full suite/coverage/E2E/lint gates — PASS ⑤ `git diff --stat` limited to `styles.css` (+12) + new test files + phase files, no `app/`/markup/JS — PASS ⑥ commit/move — left to harness per executor rules (working tree intact, `todo/` dir removal already reflected)
- **Notable:** red→green (CSS reverted → 626px failure) and manual live check already recorded in `.agents/reports/86_history_page_width/`; pre-existing untracked `.agents/remediation_plan.md` (Sep 7 security audit) untouched
- **Next pending phase:** `87_big_read_progress`
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 66%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 92%]
........................................................................ [ 96%]
................................................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,13 @@
All gates green. Final state verified:
**Report — Phase 86, Task 01 (table-wrap containing-block fix)**
- Added `position: relative;` (first declaration) to the shared `.table-wrap` rule in `frontend/assets/styles.css`, with a root-cause comment (positioned `.visually-hidden` spans → initial containing block → document scroll leak; TODO.md L4) — the one-rule A3 fix
- New unit pin `tests/unit/test_table_wrap_stacking.py` (house CSS-source pattern): `position: relative` present + first, `overflow-x: auto` preserved, and the fix scoped to `.table-wrap` only (`.md-table-wrap` / `#git-sources-table-wrap` untouched); docstring carries the measured bug basis (626/618/375 at 375px, 2026-09-07)
- Live browser verification (375px, throwaway script, deleted after): History scrollWidth 626→375, Tokens 618→375, Sources 375 (unchanged); in-card scroll intact (wraps 688/640 > 344 client); a11y spans still in DOM
- `uv run pytest tests/unit/test_table_wrap_stacking.py -v` → 3 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1720 passed, TOTAL **99%** (>90%)
- `uv run ruff check . && uv run pyright` → All checks passed / 0 errors
- `uv run pytest tests/e2e/test_smoke.py -v --no-cov` → 3 passed (sanity, app functional)
- `git diff --stat -- frontend/` → only `frontend/assets/styles.css` (+12: comment + 1 property); no markup/JS changes; no commit made (harness commits)
- Next pending task: `02_e2e_story_suite.md` (the `tests/e2e/test_history_page_width.py` story suite)
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 66%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 92%]
........................................................................ [ 96%]
................................................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,17 @@
Task 02 complete. Report:
**Implemented**
- `tests/e2e/test_history_page_width.py` (new) — 5 isolated Playwright tests: History viewport-width at 375px (THE 626→≤375 pin + in-card scroll + a11y-span presence), Tokens (618 fold-in), SPA switch history→tokens (real `#nav-toggle` click + phase-76 window-sentinel same-document proof), RAG regression (viewport-width + full-width docs table), desktop 1280×800 no-overflow / container-width tables.
- RAG + desktop tests seed the KB house-way (truncate + 13 fixture docs) — the RAG view hides its table on an empty KB; History/Tokens pins hold empty, as the task assumed.
**Results**
- `uv run pytest tests/e2e/test_history_page_width.py -v --no-cov` → **5 passed** (in isolation, DB up)
- Red→green verified: with task-01 CSS reverted, `test_history_page_is_viewport_width` fails at the scrollWidth pin (**626px at a 375px viewport** — the exact reported defect); restored → green (recorded in the session above)
- `uv run pytest --cov=app --cov-report=term-missing` → **1720 passed, 99% coverage** (>90%)
- `uv run ruff check .` → clean; `uv run pyright` → **0 errors**
**Decisions**
- Reused the phase-76 window-sentinel pattern verbatim for the same-document proof (per the task's ASSUMPTION) instead of a navigation-timeline count.
- Card-width pin computes container content width minus the card's own 1px borders (clientWidth excludes borders) — a ±1px tolerance caught a first-draft miscalculation.
**Next pending task:** `03_verify_and_commit.md` (full gate + atomic commit).
@@ -0,0 +1,85 @@
........................................................................ [ 4%]
........................................................................ [ 8%]
........................................................................ [ 12%]
........................................................................ [ 16%]
........................................................................ [ 20%]
........................................................................ [ 25%]
........................................................................ [ 29%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 50%]
........................................................................ [ 54%]
........................................................................ [ 58%]
........................................................................ [ 62%]
........................................................................ [ 66%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 87%]
........................................................................ [ 92%]
........................................................................ [ 96%]
................................................................ [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
--------------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 52 0 100%
app/api/chat.py 178 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 94 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 29 0 100%
app/api/sync.py 98 0 100%
app/api/tokens.py 28 0 100%
app/config.py 141 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 45 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/errors.py 5 0 100%
app/core/logging.py 13 0 100%
app/core/rate_limit.py 44 0 100%
app/core/security_headers.py 19 0 100%
app/core/tokens.py 33 0 100%
app/db.py 21 0 100%
app/main.py 64 0 100%
app/models.py 94 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 217 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 232 0 100%
--------------------------------------------------
TOTAL 3237 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK

Some files were not shown because too many files have changed in this diff Show More