Files
brain-of-reese/scripts/api_payload_audit.py
T
ducoterra 1200437bf8
Build and Push Containers / build-and-push-app (push) Successful in 4m4s
Build and Push Containers / build-and-push-db (push) Successful in 15s
phase: 168_admin_settings_panel
**Phase 168 — Admin Settings panel — final verification: PASS**

1. **Implemented/verified:** `/settings.html` (admin-only) with the 75-field catalogue (`app/core/settings_catalog.py`), DB-over-env overrides in Postgres (`app_settings`, migration 0028 at head, `settings_overlay.py` resolver, `app/api/admin_settings.py` — 4 admin-only routes), live-apply on save with no restart + restart-required badges, Reset/Clear all, write-only secrets, `.env`-never-written, `BOR_SETTINGS_DB=0` kill switch.
2. **Phase E2E: 12/12** — run with `E2E_APP_PORT=8200` (ports 8123/8124 are held by an unrelated "undercooked" project; no process killed, no code change — the documented env override).
3. **Regression set: all 7 suites green in isolation** (theme tab, API tokens, configurable brand, global tuning, admin auth, cache busting, wide desktop column — 38 tests).
4. **Full suite: unit+integration exit 0 (4066 passed), app/ coverage 99%** (gate >90%); **ruff clean, pyright 0 errors / 0 warnings.**
5. **Log contract (rule 10) verified live:** per-turn + per-response byte-budget lines present; settings write lines carry key + action, **never the value**.
6. **Byte-budget audit: all endpoints under the 262,144 B budget**; settings catalogue 23,513 B (gzip 3,326).
7. **Defect fixed (one):** criterion 5 had no integration pin — added `test_bad_row_inserted_directly_never_bricks_boot` to `tests/integration/test_admin_settings_api.py` (malformed row inserted directly → fresh `create_app()` boots, row skipped with logged reason, rest of table applies, `/api/health` 200) — 12/12 green.
8. **Docs pass:** `README.md` and `AGENTS.md` reviewed in full — both consistent with the as-built feature (Settings page table, Settings section, Configuration-reference note, `.env.example` block); **no changes required**.
9. **Cleanup:** audit app killed (by PID); `app_settings` left empty (0 rows); all changes left in the working tree (no commit, per protocol).
10. **Completion criteria:** all 7 checkboxes verified green (criterion 7's commit is the harness's).
11. **Next pending phase:** `170_edit_bubble_sizing`.
2026-10-10 01:05:14 -04:00

380 lines
15 KiB
Python

"""Audit the payload of every endpoint that could load a ton of data
(phase 167, task 05, D5, LOCKED A5).
TODO L5 is one sentence: "Fix that **any any other endpoints** that could
load a ton of data." ``GET /api/docs/tree`` was the reported offender; this
script is the measured answer for the rest of the API, against the app the
operator actually runs:
uv run python -m scripts.api_payload_audit --base-url http://localhost:8000
# (or against the deployed app: --base-url https://brain.reeseapps.com)
It signs in through the real ``POST /api/login`` (the session cookie is the
only credential — nothing is hardcoded: ``--password`` or the
``BOR_ADMIN_PASSWORD`` the app itself reads from ``.env``), walks the
candidate read routes, and prints one row per endpoint with the IDENTITY and
the GZIP transfer size, the ratio, the wall time and a verdict against the
budget (default 256 KB — the phase's "look at it" bar; LOCKED A5: this is a
report, never a gate, so the exit code is 0 whenever the measurement
succeeded).
**Credentials are password-only on purpose.** The single-admin session is
``POST /api/login {"password": …}`` (PLAN A10) — there is no username field
to send, so there is no ``--username``; ``BOR_ADMIN_PASSWORD`` (or
``--password``) is the whole credential.
Both variants are measured on every endpoint because that is the pair the
byte-budget log line reports (``bytes=`` / ``enc=``): ``Accept-Encoding:
identity`` gives the payload the route produced, ``Accept-Encoding: gzip``
gives what the client puts on the wire (``response.num_bytes_downloaded``,
the raw bytes — ``response.content`` is already decompressed by httpx). The
routes that publish a validator (the three catalogue reads, phase 167 task
03) get one extra conditional request each: the expected answer is
``304`` with an empty body, which is the row's verdict.
Run it against the 1,000-document live-replica snapshot (the
``restore-test-db`` skill) so the numbers are the production order of
magnitude and not the fixture KB's — the corpus the run measured is printed
with the table for exactly that reason.
Exit codes: **0** the table printed (an over-budget row is a FINDING, shown
as ``LOOK``, not a failure), **2** a precondition failed — the app
unreachable, the login rejected, or no credential available.
"""
from __future__ import annotations
import argparse
import os
import sys
import time
from dataclasses import dataclass
from typing import Any
import httpx
from dotenv import load_dotenv
load_dotenv()
#: The phase's "look at it" bar (256 KB uncompressed — D5/A5). Rows over it
#: are verdict ``LOOK`` and must be fixed in the phase or carry a named
#: follow-up in ``AUDIT.md``; nothing here rejects a request.
DEFAULT_BUDGET = 262_144
#: Per-request wall-clock ceiling. A large catalogue read on the restored
#: 1,000-doc replica is fast, but a cold first query can take seconds.
DEFAULT_TIMEOUT = 60.0
@dataclass
class Row:
"""One measured endpoint."""
label: str
path: str
status: int = 0
identity_bytes: int | None = None
identity_ms: int | None = None
gzip_bytes: int | None = None
gzip_ms: int | None = None
revalidated: bool | None = None
note: str = ""
@property
def ratio(self) -> str:
"""identity / gzip — how much the compression layer bought (< 1 is
a win; ``—`` when the route did not compress)."""
if not self.gzip_bytes or not self.identity_bytes:
return "—"
return f"{self.gzip_bytes / self.identity_bytes:.3f}"
def verdict(self, budget: int) -> str:
if self.status == 0:
# Nothing was measured — the corpus has no such row (an empty KB,
# no saved chats). Honest gap, not a pass and not a finding.
return "n/a"
if self.status == 304:
return "304"
if self.status != 200:
return f"HTTP {self.status}"
if self.revalidated:
return "304"
if (self.identity_bytes or 0) > budget:
return f"LOOK (> {budget})"
return "ok"
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="python -m scripts.api_payload_audit",
description=(
"Measure the response size (identity AND gzip) of every "
"candidate large-payload endpoint, against a byte budget."
),
)
p.add_argument(
"--base-url",
default="http://localhost:8000",
metavar="URL",
help="app to measure (default: http://localhost:8000)",
)
p.add_argument(
"--password",
default=None,
metavar="PASSWORD",
help=(
"admin password; falls back to $BOR_ADMIN_PASSWORD (the app's "
"own variable). The single-admin login has no username (A10)."
),
)
p.add_argument(
"--budget",
type=int,
default=DEFAULT_BUDGET,
metavar="BYTES",
help=f"the LOOK threshold in bytes (default: {DEFAULT_BUDGET})",
)
p.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
metavar="SECONDS",
help=f"per-request timeout (default: {DEFAULT_TIMEOUT:g})",
)
return p
def _fail(message: str) -> int:
print(f"api_payload_audit: {message}", file=sys.stderr)
return 2
def _get(client: httpx.Client, path: str, accept: str) -> tuple[int, int, int, int]:
"""GET ``path`` with an explicit ``Accept-Encoding`` →
``(status, body bytes, wire bytes, ms)``.
``body`` is the decoded payload (what the route produced); ``wire`` is
the raw transfer, which is what the browser pays. They differ exactly
when the response came back gzipped.
"""
started = time.perf_counter()
response = client.get(path, headers={"Accept-Encoding": accept})
ms = int(round((time.perf_counter() - started) * 1000))
return response.status_code, len(response.content), response.num_bytes_downloaded, ms
def measure(
client: httpx.Client, label: str, path: str, *, conditional: bool = False
) -> Row:
"""One endpoint, both encodings, plus the conditional re-request on the
routes that publish a validator (``conditional=True``)."""
row = Row(label=label, path=path)
row.status, row.identity_bytes, _, row.identity_ms = _get(client, path, "identity")
gzip_status, _, wire, ms = _get(client, path, "gzip")
row.gzip_bytes, row.gzip_ms = wire, ms
if gzip_status != row.status: # pragma: no cover - defensive
row.status = gzip_status
if conditional:
# The ETag from a fresh unconditional GET, replayed (fetch does the
# same thing on the second page view).
etag = client.get(path, headers={"Accept-Encoding": "identity"}).headers.get("etag")
if etag is None:
row.revalidated = False
else:
again = client.get(
path, headers={"Accept-Encoding": "identity", "If-None-Match": etag}
)
row.revalidated = again.status_code == 304 and len(again.content) == 0
return row
def _first_folder(tree: dict[str, Any]) -> tuple[str, str] | None:
"""``(source, folder path)`` of the first folder node the depth-1 tree
reports — a representative drill target for the node route."""
for source in tree.get("sources", []):
for child in source.get("children", []):
if child.get("kind") == "folder":
return str(source.get("name", "")), str(child.get("path", ""))
return None
def run(args: argparse.Namespace) -> tuple[int, list[Row], list[str]]:
"""Do the whole audit; returns ``(exit code, rows, corpus lines)``."""
password = args.password or os.environ.get("BOR_ADMIN_PASSWORD", "")
if not password:
return (
_fail(
"no admin password — pass --password or set BOR_ADMIN_PASSWORD "
"(the app's own variable; it is never hardcoded here)"
),
[],
[],
)
client = httpx.Client(
base_url=args.base_url, timeout=args.timeout, follow_redirects=False
)
try:
login = client.post("/api/login", json={"password": password})
except httpx.HTTPError as exc:
return (
_fail(
f"cannot reach the app at {args.base_url} ({exc.__class__.__name__}: {exc}) "
"— start it with `uv run uvicorn app.main:app`"
),
[],
[],
)
if login.status_code != 204:
detail = ""
try:
detail = str(login.json().get("detail", ""))
except Exception: # noqa: BLE001 - a non-JSON error body is still an error
detail = login.text[:120]
hint = {
401: "wrong password (BOR_ADMIN_PASSWORD of the app under test)",
429: "rate limited (10 failed attempts / 15 min) — wait for the window",
}.get(login.status_code, "unexpected answer from the real login route")
return (
_fail(
f"login to {args.base_url} failed: HTTP {login.status_code} — "
f"{hint} {detail}"
),
[],
[],
)
rows: list[Row] = []
corpus: list[str] = []
# --- the corpus the numbers describe (printed with the table) ---------
docs_response = client.get("/api/docs", headers={"Accept-Encoding": "identity"})
documents: list[dict[str, Any]] = list(docs_response.json().get("documents", []))
by_chunks = sorted(documents, key=lambda d: int(d.get("chunks", 0)), reverse=True)
biggest_doc = by_chunks[0] if by_chunks else None
chunks_total = sum(int(d.get("chunks", 0)) for d in documents)
corpus.append(f"documents={len(documents)} chunks_total={chunks_total}")
chats: list[dict[str, Any]] = list(
client.get("/api/chats", headers={"Accept-Encoding": "identity"}).json().get("chats", [])
)
biggest_chat = (
sorted(chats, key=lambda c: int(c.get("message_count", 0)), reverse=True)[0]
if chats
else None
)
# --- the candidates ---------------------------------------------------
rows.append(
measure(client, "tree depth=1 (default)", "/api/docs/tree?depth=1", conditional=True)
)
rows.append(
measure(client, "tree depth=8 (full)", "/api/docs/tree?depth=8", conditional=True)
)
depth1 = client.get(
"/api/docs/tree?depth=1", headers={"Accept-Encoding": "identity"}
).json()
folder = _first_folder(depth1)
if folder is not None and folder[0]:
node_path = f"/api/docs/tree/node?source={folder[0]}&path={folder[1]}&depth=1"
rows.append(
measure(client, f"tree node {folder[0]}/{folder[1]}", node_path, conditional=True)
)
else: # the honest gap: an empty KB has no drill to measure
rows.append(
Row(label="tree node (drill)", path="/api/docs/tree/node", note="no folder in corpus")
)
rows.append(measure(client, "docs catalogue", "/api/docs", conditional=True))
if biggest_doc is not None:
content_path = (
f"/api/documents/content?source={biggest_doc['source']}&path={biggest_doc['path']}"
)
row = measure(client, f"document content (biggest: {biggest_doc['path']})", content_path)
body = client.get(content_path, headers={"Accept-Encoding": "identity"}).json()
row.note = f"content field {len(str(body.get('content', '')))} chars"
rows.append(row)
else:
rows.append(Row(label="document content", path="/api/documents/content", note="empty KB"))
rows.append(measure(client, "saved chats list", "/api/chats"))
if biggest_chat is not None:
rows.append(
measure(
client,
f"saved chat (biggest: {int(biggest_chat.get('message_count', 0))} msgs)",
f"/api/chats/{biggest_chat['id']}",
)
)
else:
rows.append(Row(label="saved chat (one)", path="/api/chats/{id}", note="no saved chats"))
rows.append(measure(client, "git sources", "/api/git-sources"))
rows.append(measure(client, "suggestions", "/api/suggestions"))
rows.append(measure(client, "sync status (polled)", "/api/sync/status"))
rows.append(measure(client, "tokens", "/api/tokens"))
rows.append(measure(client, "config", "/api/config"))
rows.append(measure(client, "whoami", "/api/whoami"))
rows.append(measure(client, "admin settings (catalogue)", "/api/admin/settings"))
corpus.append(f"sources={len(depth1.get('sources', []))}")
totals = depth1.get("totals") or {}
if totals:
corpus.append(
f"tree totals documents={totals.get('documents')} chunks={totals.get('chunks')}"
)
corpus.append(f"saved_chats={len(chats)}")
corpus.append(f"biggest_doc={biggest_doc['path'] if biggest_doc else '—'}")
return 0, rows, corpus
def render(rows: list[Row], *, base_url: str, budget: int, corpus: list[str]) -> None:
"""The table: endpoint, identity bytes, gzip bytes, ratio, ms, verdict."""
header: tuple[str, ...] = ("endpoint", "identity", "gzip", "ratio", "ms", "verdict")
table: list[tuple[str, ...]] = [header]
for row in rows:
table.append(
(
row.label,
f"{row.identity_bytes:,}" if row.identity_bytes is not None else "—",
f"{row.gzip_bytes:,}" if row.gzip_bytes is not None else "—",
row.ratio,
f"{row.gzip_ms}" if row.gzip_ms is not None else "—",
row.verdict(budget),
)
)
widths = [max(len(str(r[i])) for r in table) for i in range(len(header))]
def _line(values: tuple[Any, ...]) -> str:
return (
f" {str(values[0]):<{widths[0]}} {str(values[1]):>10} "
f"{str(values[2]):>10} {str(values[3]):>{widths[3]}} "
f"{str(values[4]):>6} {str(values[5])}"
)
print(f"api_payload_audit: base={base_url} budget={budget:,} bytes (LOOK threshold)")
print(f"api_payload_audit: corpus {' '.join(corpus)}")
print(_line(header))
print(f" {'-' * (widths[0] + 44)}")
for row, source in zip(rows, table[1:], strict=True):
print(_line(source))
if row.note:
print(f" note: {row.note}")
print(
"api_payload_audit: ms is the gzip (real-client) variant; identity is the "
"route's own bytes — the pair the ' :: api ' log line reports."
)
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
code, rows, corpus = run(args)
if code != 0 or not rows:
return code
render(rows, base_url=args.base_url, budget=args.budget, corpus=corpus)
return 0
if __name__ == "__main__":
sys.exit(main())