Author SHA1 Message Date
profit bd0fc50544 Merge branch 'main' into docs/reconcile-with-code 2026-06-21 04:00:07 +00:00
profit 86b30939fa Merge pull request 'security: SESSION_COOKIE_SECURE + nginx IP validation' (#3) from security/session-cookie-and-nginx-ip-guard into main 2026-06-21 03:57:23 +00:00
rootandClaude Opus 4.8 5ac2103a4b docs: reconcile PRD + fix-wave with current code (ea0ba33)
The audit anchors had drifted from the implementation. Corrected against
the live source as of commit ea0ba33:

- PRD invariant #1: mode registry is now 24 handlers in _VALID_MODES,
  not "only extract". Added invariant #2: unknown modes fail with HTTP
  400 + valid_modes (was 200-with-error-body). Added SECURE to the
  session-cookie invariant.
- PRD known-gaps + change-axis: marked the mode-registration and
  unknown-mode items RESOLVED; flagged the live codereview-vs-code_review
  naming mismatch that still 400s lakehouse observer escalations.
- SCRUM_FIX_WAVE items 1-2 struck through as DONE; recorded the cookie +
  nginx security fixes under "Recently landed".

Machine-generated .memory/ state left untouched (regenerated by the
scrum pipeline, not hand-edited).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:09:00 -05:00
rootandClaude Opus 4.8 ea0ba33212 security: set SESSION_COOKIE_SECURE + validate IP before nginx deny write
Two hardening fixes from a fresh security pass of the live code:

- SESSION_COOKIE_SECURE=True: the session cookie set only HTTPONLY +
  SAMESITE=Lax, so it could be sent over plaintext HTTP. App is served
  behind nginx TLS, so no functional downside.
- _nginx_ban: validate the IP with ipaddress.ip_address() before
  interpolating it into the nginx deny file. A malformed value could
  otherwise inject nginx directives at the last write before reload.
  Guard sits at the sink, covering all current and future callers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:00:50 -05:00
rootandClaude Opus 4.7 083f05c093 sentinel: log subprocess failures + AI-busy retry/callback + allowlist all ban-paths
Three issues from J 2026-04-30:

1. Silent fail2ban-client / nginx subprocess failures
   Pre-fix _execute_ban called fail2ban-client with capture_output=True
   but threw away the result. _nginx_ban had bare `except: pass`
   swallowing everything. So a non-zero fail2ban exit (jail not
   configured, IP already banned, IPv6 quirk) or PermissionError
   on /etc/nginx/banned_ips.conf logged "AI_BAN" while the attacker
   walked through unimpeded. The "errors in logs" J was seeing.

   Now every subprocess failure surfaces:
   - FAIL2BAN_FAILED rc=N stderr=... — non-zero exit
   - FAIL2BAN_TIMEOUT — client didn't return in 5s
   - FAIL2BAN_NOT_INSTALLED — binary missing
   - NGINX_BAN_WRITE_DENIED — permission error on conf file
   - NGINX_RELOAD_FAILED rc=N stderr=... — systemctl reload non-zero
   - NGINX_RELOAD_TIMEOUT / NGINX_RELOAD_NO_SYSTEMCTL — runtime gaps
   sec_log.error catches these so journalctl -u llm-team-ui shows
   the actual reason a ban didn't stick.

2. AI auto-scan failure callback when model is busy
   Pre-fix Ollama unreachable / busy / timeout silently preserved
   log position + skipped the scan. Operator only learned about
   the gap by manually checking sentinel-status. Now:
   - 1 retry inside same scan after SENTINEL_AI_RETRY_DELAY_SECS
     (30s) on connection error / timeout / 429 / 503
   - 4xx errors that won't recover (404 model missing, 400 bad
     prompt) fail fast without retrying
   - consecutive_ai_failures counter in _sentinel_stats
   - On 3+ consecutive failures, send_security_alert() fires —
     "Sentinel AI unreachable" email with last error + endpoint
     + model name. One alert per outage (ai_busy_alerted flag);
     clears on first successful scan so flapping doesn't spam.
   - AI_RECOVERED log line on first scan after a streak.

3. Sentinel ban path still substring-matched 192.168
   Same vulnerability class as admin_ban_ip had — only protected
   one /16. Replaced 4 sites with is_allowlisted(ip):
   - threat-list display filter (line 7638): now hides ALL
     allowlisted IPs from the panel
   - mass-ban API (line 8016): refuses ban for any allowlisted IP
   - sentinel analysis filter (line 12786): saves AI tokens by
     never sending allowlisted-IP traffic to the judge
   - sentinel ban verdict gate (line 12949): defense in depth —
     even if the AI says "ban" on an allowlisted IP, this catches it

   Combined with the layered defenses in b09b73c (track_violation,
   _auto_escalate, _nginx_ban, admin_ban_ip), there is now no
   code path that can ban an allowlisted IP. Operator self-ban
   is structurally impossible.

Privilege note: the systemd unit at /root/llm-team-ui/llm-team-ui.service
runs as User=root, so subprocess.run(["fail2ban-client", ...]) and
systemctl reload nginx have permission. The "errors in logs" J was
seeing weren't permission-denied; they were silent non-zero exits.
The new subprocess wrappers surface those.

If the operator later splits the app into a non-root tier
(Opus OB-3 architectural recommendation, deferred), this same
infrastructure still works — the wrappers will then surface
"PermissionError" with full path + uid context, telling the
operator exactly which command needs sudo NOPASSWD or
PolicyKit rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 03:33:20 -05:00
rootandClaude Opus 4.7 2575842f7b threat-intel: master 'select all' checkbox in toolbar
UX request 2026-04-30: when sorting by threat in the threat intel
panel, ban-selected required clicking each per-row checkbox
individually. Pages with 20-50 threats made bulk-ban tedious.

Adds a master `[ ] all` checkbox to the toolbar (right of the
Sort buttons, left of the existing 'N selected' counter) that
toggles every per-row .ip-check on the page in one click. Then
'Ban Selected' / 'Unban Selected' work over the whole set.

Three-state: unchecked (none selected) / checked (all) /
indeterminate (partial — browsers render this as a "half-tick"
so operators get visual feedback when they've toggled some rows
manually after using master). updateSelCount keeps the master
in sync as individual rows toggle so the visual is always
truthful.

No backend change — `/api/admin/security/mass-ban` already
accepts an arbitrary IP list. This is purely a frontend
ergonomics improvement on top of the existing mass-action
infrastructure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 03:27:20 -05:00
rootandClaude Opus 4.7 b09b73c409 llm_team_ui: ban-system defense in depth (extends OB-4)
The OB-4 fix at 939dfdd was incomplete. It bypassed the path-regex
EXPLOIT check for allowlisted-IP admins, but _track_violation is
called from THREE sites (exploit_scan, rate_limit, login_fail) and
only the exploit_scan path had the bypass. An admin who hit a rate
limit + had a session timeout + entered a wrong password within 60s
could still self-ban via the OTHER paths, ending up locked out
through the back door.

This commit adds 4 layers of defense in depth, each independently
sufficient to stop an allowlisted IP from being banned:

1. _track_violation: bail early if is_allowlisted(ip). Allowlisted
   IPs never accumulate violations from ANY path. Plus eviction
   sweep when _violation_tracker grows >10K (same shape as the
   _rate_limit eviction at 266de61).

2. _auto_escalate: re-check is_allowlisted before issuing any ban.
   Defense in depth — if a future call path bypasses #1, this
   catches it.

3. _nginx_ban: refuse to write the deny rule for allowlisted IPs,
   even if a buggy caller reached this far. Last write before nginx
   reload; last place to stop a bad ban.

4. admin_ban_ip: replace `ip.startswith("192.168.")` substring
   check with the canonical ALLOWLIST_IPS membership test. Pre-fix
   this only protected one LAN; 10.0.0.0/8, IPv6 loopback ::1, and
   custom allowlist entries (e.g. an external monitoring IP) were
   all banable by manual admin error. Now uses the same allowlist
   as the auto-ban paths.

Operationally the admin can no longer self-ban through any path.
The auto-escalate ban audit log entries get a corresponding
"AUTO_ESCALATE_BLOCKED ip=... — allowlisted" entry instead of the
ban firing silently. Same for nginx_ban: NGINX_BAN_BLOCKED entries
log the saved bullet for operator review.

Builds on 939dfdd (OB-4 path-regex bypass) + 266de61 (rate_limit
eviction + auth_login IP gate). Together these three commits close
the LLM Team UI's ban-system self-foot-shoot vulnerability surface.

Outstanding from the scrum (architectural, separate session):
- OB-3 root-running web app + privileged shell calls
- Sentinel prompt-injection WARN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 03:24:01 -05:00
rootandClaude Opus 4.7 266de613b2 llm_team_ui: 2 more scrum WARNs (rate_limit eviction + setup IP gate)
Closes the 2 remaining surgical-fix WARNs from the 2026-04-30
cross-lineage scrum on this codebase. OB-3 (root-running web app
with shell calls to fail2ban-client / systemctl / nginx config) and
the sentinel prompt-injection WARN both need bigger architectural
work and stay deferred.

OB-rate-limit (Opus WARN) — _rate_limit dict unbounded
  Pre-fix: per-worker dict with no eviction; an attacker slowly
  rotating IPs leaked memory forever. Fix: lazy eviction sweep
  triggered when dict grows beyond 10K entries (cheap because we
  only scan when growth is unusual). Real production wants a
  Redis-backed shared counter; this is the in-process band-aid
  that prevents runaway growth without changing the deploy shape.

OB-auth-setup (Opus WARN) — first-time setup grant from any IP
  Pre-fix: /api/auth/login with setup=true was gated only by
  COUNT(*) FROM users == 0. If the users table was ever truncated
  or restored empty, the next external visitor (ANY IP) claimed
  admin. Fix: also require the source IP to be in ALLOWLIST_IPS
  (typically loopback + LAN gateway). Local operator setup still
  works; remote attackers hitting the endpoint after an empty-
  users state get 403.

Both fixes are surgical — single function, no behavior change for
the happy path. The eviction sweep runs O(n) only when n>10K and
only drops entries already past their useful window, so it never
removes an active rate-limit count.

Outstanding from the scrum (deferred):
- OB-3 root-running web app: needs split into non-root Flask tier
  + privileged sudo wrapper service. 2-4 hr architectural work.
- Sentinel prompt-injection WARN: feeds attacker-controlled UA/
  path into LLM judge prompt. Needs prompt-template hardening or
  output validation gate before LLM verdicts can issue ban actions.
- CSP unsafe-inline WARN: defeats most XSS protection. Removing
  it requires moving inline scripts to external files (HTML
  refactor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 03:19:38 -05:00
rootandClaude Opus 4.7 939dfddb93 llm_team_ui: 4 fixes from 2026-04-30 cross-lineage scrum
Cross-lineage scrum (Opus 4.7 + Kimi K2.6 + Qwen3-coder via the
local-review-harness chatd) on this codebase surfaced 5 BLOCK-class
issues from Opus + a convergent finding from the harness. This
commit lands the 4 surgical fixes; OB-3 (web app runs as root with
fail2ban-client + systemctl reload nginx + writes to
/etc/nginx/banned_ips.conf) needs an architectural split into a
non-root web tier + a privileged sudo wrapper, deferred for its
own session.

OB-1 — log file open at import crashes app on perm error
  Pre-fix: `_sec_handler = logging.FileHandler("/var/log/llm-team-
  security.log")` raised PermissionError at import time on any
  non-root or fresh-install run, killing the app before Flask
  started — failure was silent (no Flask process to inspect logs
  on).
  Fix: try/except, fall back to StreamHandler(sys.stderr) when
  the path is unwritable. App starts; sec_log events still land
  in journald via stderr. LLM_TEAM_SECURITY_LOG env var lets
  operators override the path.

OB-2 — DB password hardcoded in source (CONVERGENT FINDING)
  The `kbuser` Postgres credential
  `IPbLBA0EQI8u4TeM2YZrbm1OAy5nSwqC` was leaked in source here
  AND in voice-ai/audiosocket_bridge.py + voice-ai/sales_assistant.py.
  Caught independently by harness LLM phase (qwen3.5 local) on
  voice-ai earlier today AND Opus on this file just now. Same
  password, same DB (`knowledge_base`) shared between services,
  three reviewers converged.
  Fix: source from LLM_TEAM_DB_DSN env var, fail loud on unset.
  Operator follow-ups:
    1. Rotate the password in Postgres (still in git history;
       redacting source doesn't un-leak it).
    2. Set LLM_TEAM_DB_DSN in /etc/llm-team-ui.env (mode 0600,
       loaded via systemd EnvironmentFile=).
    3. Same DSN env-var pattern needs applying to
       voice-ai/audiosocket_bridge.py:47 once that branch's
       workspace_context WIP lands.

OB-5 — demo_mode default=True ships public access on first boot
  Pre-fix: `_demo_mode = {"active": True, ...}` + the demo branch
  in login_required let users through without a session. Combined
  with /api/run + /api/imagegen proxies, fresh installs were open
  LLM/compute abuse surface from first boot.
  Fix: default to False; LLM_TEAM_DEMO_MODE=1 env override exists
  for the public devop.live deployment systemd unit so the demo
  doesn't need a manual flip on every restart, but everywhere else
  defaults closed.

OB-4 — EXPLOIT_PATTERNS LAN/admin lockout
  Pre-fix: regex matched on `request.path` + query string against
  patterns like UNION / SELECT / ;-- / <script /admin.php. Admin
  URLs containing those keywords in legitimate ways (e.g. a team
  name "select-rebrand" or a docs link /admin/select_a_mode) hit
  3 violations in 60s and auto-banned the admin's IP. No allowlist.
  Fix: bypass the path-based check for authenticated admins from
  an ALLOWLIST_IPS source. Body/UA checks still apply (the prompt-
  injection-as-DoS WARN in the scrum is separate). Combination
  prevents self-ban without weakening the broader scanner defense.

Plus a .gitignore: /.memory/ — the local-review-harness writes
JSONL findings under <repo>/.memory/ when scanning; harness's own
gitignore is at the harness repo root, not here, so without this
the .memory/ dir would show up as untracked on every harness run
against this tree.

Other Opus WARNs deferred:
- Sentinel feeds attacker-controlled UA into LLM prompt → can
  steer ban verdicts. Fix needs prompt-template hardening or
  output-validation gate.
- CSP `'unsafe-inline'` defeats most XSS protection (would break
  inline scripts; needs HTML refactor).
- _rate_limit unbounded dict + per-worker (needs eviction loop or
  Redis-backed counter).
- auth_login first-time setup gated only by COUNT(*)==0 (needs
  network-source restriction or a setup token).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 03:14:08 -05:00
rootandClaude Opus 4.6 205eff64b4 Deep Analysis mode + token tracking for all runs (including public)
New mode: Deep Analysis — 6-phase autonomous pipeline:
1. Research: all selected models answer in parallel
2. Debate: models challenge each other's findings
3. Consensus: merge research with critiques, identify strong/weak points
4. Self-Eval: structured scoring (accuracy, completeness, actionability, nuance)
5. Final Synthesis: strongest model produces definitive answer
6. Knowledge Base: result stored for future RAG retrieval

Designed for cloud models (Ollama Cloud, OpenRouter). Every successful
run trains the local knowledge base so future adaptive runs benefit.
Purple accent in mode selector to distinguish from standard modes.

Token tracking fix:
- Added est_tokens, input_chars, output_chars columns to team_runs
- save_run() now calculates and stores token estimates for ALL runs
- Both logged-in and public/demo/showcase runs track tokens
- Enables accurate usage analytics across all users

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:30:56 -05:00
rootandClaude Opus 4.6 34ee12e7ed Fix adaptive mode: model list + synthesizer dropdown were never populated
Two bugs preventing adaptive mode from working:
- ml-adaptive was missing from ML_IDS array (line 3160) — model checkboxes
  never rendered, so no models were selected
- adaptive-synthesizer was missing from populateAllSelects() ids array
  (line 3733) — synthesizer/judge dropdown was always empty

Both are single-line fixes. The backend pipeline (run_adaptive) was
complete and correct — self-eval, RAG retrieval, escalation, quality
scoring, KB storage all work. The UI just never wired the config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:24:47 -05:00
rootandClaude Opus 4.6 98bda6e337 OpenRouter: show all 343 models (free + paid) with pricing and filter
- Endpoint now returns all models, not just free ones
- Each model includes: name, context_length, free flag, prompt/completion cost
- UI shows pricing: "128K ctx · $2.50/M tok" for paid, "128K ctx · free" for free
- Filter dropdown: All Models / Free Only / Paid Only
- Search still works alongside the filter
- 29 free + 314 paid models available (GPT-5.4, Grok 4.20, Gemini 3.1, etc)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:22:51 -05:00
rootandClaude Opus 4.6 fa6ccff079 Ollama Cloud provider + model browser + OpenRouter key fix
New provider: Ollama Cloud (ollama.com)
- Native Ollama chat API with bearer token auth
- Provider card in Admin → Providers tab
- "Ollama Cloud" tab with Pull Models button (fetches 36 models)
- Search/filter models, one-click Add
- Models route as ollama_cloud::modelname through query_ollama_cloud()
- Test button verifies connection

OpenRouter fix:
- Cleared bad API key from config (was dd00bea4... not sk-or-)
- Real key from /home/profit/.env now used (sk-or-v1-579...)
- Fixed OpenAI provider that had wrong base_url (ollama.com→api.openai.com)
- Bumped OR timeout to 180s for free model rate limits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:20:44 -05:00
4 changed files with 926 additions and 66 deletions
+1
View File
@@ -2,3 +2,4 @@ __pycache__/
*.pyc *.pyc
.env .env
*.log *.log
.memory/
+99
View File
@@ -0,0 +1,99 @@
# LLM Team UI — Product Requirements (for scrum-master audits)
This doc is the anchor the lakehouse scrum-master uses when auditing
this codebase. It is the "ground truth" — every scrum finding is
evaluated against what this doc says the system should be. Written
2026-04-24 to give the scrum pipeline a target surface; keep it tight
and aligned with what the code actually tries to do. Last reconciled
2026-06-20 against commit `ea0ba33` — the mode registry and unknown-mode
handling described below were verified against the live source on that date.
## What LLM Team UI is
Flask-based web UI that lets a user configure and run multi-model
"teams" — pipelines of LLMs (OpenRouter + Ollama Cloud + local Ollama)
that collaborate on a task with roles like executor, reviewer, critic,
sentinel. Lives at devop.live:5000 behind nginx. Main file
`llm_team_ui.py` handles HTTP routes, session auth, model orchestration,
SSE streaming. Backed by PostgreSQL for users + runs + audit.
## Core invariants (scrum should flag if violated)
1. **Every mode accepted by `/api/run` must be in the `_VALID_MODES`
registry and have a matching `run_<mode>` handler.** As of 2026-06-20
there are 24 registered modes (brainstorm, pipeline, debate, validator,
roundrobin, redteam, consensus, codereview, ladder, tournament,
evolution, blindassembly, staircase, drift, mesh, hallucination,
timeloop, research, eval, extract, refine, adaptive, deep_analysis,
distill), each with a `run_<mode>` function. `run_team()` validates
`mode` against `_VALID_MODES` before opening the SSE stream and rejects
anything else with HTTP 400 (see invariant below). The registry is kept
in sync with the handlers manually — adding a `run_<mode>` without
listing it in `_VALID_MODES` (or vice-versa) is the hard fail to flag.
2. **Unknown modes must fail fast with HTTP 400 + the valid-mode list.**
`run_team()` returns `{"error": "Unknown mode: X", "valid_modes": [...]}`
with status 400 *before* streaming. Returning 200-with-error-body (the
old behavior) is a hard fail — it forces callers to parse SSE to learn
the request was bad.
3. **All authenticated routes must call `@login_required` decorator.**
The SESSION_COOKIE_HTTPONLY + SAMESITE=Lax + SECURE defaults must hold
(SECURE added 2026-06-20; the app sits behind nginx TLS). Any route
that mutates state without auth is a hard fail.
4. **Security logging goes to `/var/log/llm-team-security.log`** via
the `sec_log` logger. Fail2ban watches this file. Any new auth path
must emit there on failure.
5. **PostgreSQL connections come from `DB_URL` env.** Direct literal
connection strings are a hard fail.
6. **SMTP alerts via `send_security_alert(subject, body)`** — never
log secrets in the body.
7. **SSE streams must flush incrementally** — if an endpoint returns
all-at-once, it should not claim SSE in the Content-Type.
8. **Password storage: bcrypt only.** Any plain-text / SHA256 password
path is a hard fail.
9. **Rate limiting enforced** on /login, /register, and model-invocation
endpoints. No unbounded loops that invoke paid APIs.
## Known gaps (scrum can work on these)
- **~~Only `extract` mode is registered.~~ RESOLVED (2026-06-20).** The
mode set grew from one handler to 24 (`run_brainstorm` … `run_distill`),
all listed in `_VALID_MODES` and dispatched by `run_team()`. Note the
mode named in code is `codereview` (not `code_review`); callers using the
underscored name still get a 400. Historical context: observer.ts in
lakehouse was escalating to `/api/run?mode=code_review` and silently hit
"Unknown mode" for weeks (see lakehouse memory
`reference_llm_team_modes.md`) — the underscore-vs-no-underscore mismatch
is the remaining footgun, not the missing handler.
- **File is 13K lines.** Cohesive modules (auth, teams, runs, alerts,
SSE, templates) should be separated — but that's a refactor, out of
scope for individual scrum-master reviews.
- **Audit log schema is thin.** The PostgreSQL `runs` table stores the
fact of a run but not fine-grained per-step outcomes. Downstream
correlation is manual.
- **Templates inline in Python as `render_template_string(...)`.**
Makes HTML review impossible via the scrum pipeline — HTML is hidden
inside Python string literals.
## Change proposal axis (what the scrum should optimize for)
1. **~~Every Unknown-mode endpoint should either be registered or return
404/not-a-mode.~~ DONE (2026-06-20).** `run_team()` now returns HTTP 400
(not 404 — chosen so it reads as "bad request / wrong mode" rather than
"route missing") with a `valid_modes` list, before streaming. Callers
can now distinguish a wrong call from a missing mode. Remaining: see the
`codereview` vs `code_review` naming footgun in Known gaps.
2. **Auth decorators should be typed** — `@login_required` is a
runtime check; adding an `Annotated[User, Depends(auth)]`-style
type hint (possible via Flask's `g` object) would make review
mechanical.
3. **Security logging should be structured** — current `sec_log.warning(
f"...")` loses fields. Move to `logger.warning(msg, extra={...})`
so fail2ban and external tools can parse.
4. **Secret handling** — `FLASK_SECRET` in env is correct, but the
fallback to `~/.llm-team-secret` means dev mode silently generates
a persistent secret. Not bad, but should log once.
## How scrum-master audits this repo
The lakehouse scrum pipeline is repo-agnostic via env vars. See
`/root/llm-team-ui/CLAUDE.md` for the exact runbook.
+58
View File
@@ -0,0 +1,58 @@
# LLM Team scrum-master fix wave
Concrete changes the scrum-master should propose when auditing this
repo. Evaluated at ~confidence level per item. Keep focused — only
things that are safely landable per review, not the full refactor.
Last reconciled 2026-06-20 against commit `ea0ba33`. Items 1–2 below have
shipped since this list was written; they're kept (struck through) so the
numbering of 3–8 stays stable.
## Recently landed (verified 2026-06-20)
- **Session cookie hardening** — `SESSION_COOKIE_SECURE` added (was only
HTTPONLY + SAMESITE=Lax). Commit `ea0ba33`.
- **nginx ban IP validation** — `_nginx_ban` now validates the IP via
`ipaddress.ip_address()` before writing the `deny` rule, closing a
config-injection path. Commit `ea0ba33`.
## High priority (start here)
1. ~~**Register the missing modes** — `run_code_review`, `run_patch`,
`run_refactor`.~~ **DONE.** The mode set is now 24 handlers
(`run_brainstorm` … `run_distill`) in `_VALID_MODES`. NB: the code mode
is `codereview`, not `code_review` — lakehouse observer escalations
using the underscored name still 400. Closing that naming gap is the
only remaining work here.
2. ~~**Mode-dispatch should 404 when mode is unknown.**~~ **DONE (as 400).**
`run_team()` validates `mode` against `_VALID_MODES` and returns HTTP
400 + `valid_modes` before streaming. Implemented as 400 rather than the
proposed 404 (reads as "bad request", not "route missing").
3. **Structured security logging.** Replace `sec_log.warning(f"...")`
with `sec_log.warning("event", extra={"fields": ...})` so the log
format is JSON-parseable by fail2ban + external tools.
## Medium priority
4. **Rate-limit decorators on login/register.** Currently an attacker
can hammer /login at full request rate. Add per-IP + per-user rate
limits via `flask-limiter` or a simple in-memory sliding window.
5. **Audit log gains per-step fields.** Extend the `runs` table with
per-phase timestamps and outcomes so downstream analysis can
reconstruct what happened without replaying the full run.
6. **Templates → separate files.** Extract `render_template_string(...)`
HTML into `templates/*.html` so HTML/CSS review can happen
separately from Python logic.
## Low priority (nice to have, not urgent)
7. **Type hints on all public routes.** Adds IDE completion + makes
auditor pattern-match easier.
8. **Split llm_team_ui.py into modules.** 13K lines in one file makes
scrum-master reviews expensive (must tree-split). Modules: `auth.py`,
`teams.py`, `runs.py`, `alerts.py`, `sse.py`, `templates.py`.
+762 -60
View File
File diff suppressed because it is too large Load Diff