refactoring plan
This commit is contained in:
862
docs/REFACTOR_PLAN.md
Normal file
862
docs/REFACTOR_PLAN.md
Normal file
@@ -0,0 +1,862 @@
|
|||||||
|
# Refactoring & Security Plan — adiuvAI Workspace
|
||||||
|
|
||||||
|
**Generated:** 2026-06-12 · **Tree state:** `main` @ `315c5d0` (clean). Line numbers reference this state — execute file-split refactors (QUAL-*) **last**, they invalidate line references.
|
||||||
|
**Scope:** `adiuvAI/` (Electron), `api/` (FastAPI), `waitlist/`, `website/`.
|
||||||
|
**Method:** 8 parallel read-only audit agents (security ×2, dead-code/deps ×2, perf/correctness ×2, quality/types, waitlist/website), tool-assisted (`knip`, `npm audit`, `ruff`, `tsc --noEmit`, import-graph). Every finding verified against actual code.
|
||||||
|
|
||||||
|
**Legend:** `⚠️ REVIEW` = needs human or strong-model validation before execution. Do NOT hand these to a low-capability executor unattended.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| ID | Category | Severity | File | Description |
|
||||||
|
|----|----------|----------|------|-------------|
|
||||||
|
| SEC-01 | Security | **Critical** | adiuvAI/src/main/api/drizzle-executor.ts | Backend can read ANY file on user's disk via generic FS tool handlers |
|
||||||
|
| SEC-02 | Security | High | api/app/api/middleware/rate_limit.py | Zero rate limiting on login/register/refresh (unauthenticated bypass) |
|
||||||
|
| SEC-03 | Security | High | api/app/models.py | Per-user Fernet key stored plaintext beside the ciphertext it protects |
|
||||||
|
| SEC-04 | Security | High | api/app/core/deep_agent.py +3 | Raw user content/PII sent to Langfuse (GDPR / L.132/2025) |
|
||||||
|
| SEC-05 | Security | High | adiuvAI/src/main/api/drizzle-executor.ts | Mass assignment: backend sets arbitrary columns on insert/update |
|
||||||
|
| SEC-06 | Security | High | adiuvAI/src/main/index.ts | No will-navigate / setWindowOpenHandler guards on main window |
|
||||||
|
| SEC-07 | Security | High | adiuvAI (app-wide) | No Content-Security-Policy |
|
||||||
|
| SEC-08 | Security | High | waitlist/app/rate_limit.py | Rate limit keyed on spoofable CF-Connecting-IP / X-Forwarded-For |
|
||||||
|
| SEC-09 | Security | High | waitlist/app/config.py | CONFIRM_SECRET defaults to per-process random → broken across workers |
|
||||||
|
| SEC-10 | Security | Medium | api/app/api/routes/auth.py | Login 500-crashes for social-only accounts (account-type oracle) |
|
||||||
|
| SEC-11 | Security | Medium | api/app/api/routes/auth.py | No password strength validation on register |
|
||||||
|
| SEC-12 | Security | Medium | api/app/api/routes/auth.py | No logout endpoint / refresh-token revocation |
|
||||||
|
| SEC-13 | Security | Medium | api + adiuvAI (WS handshake) | JWT in WebSocket URL query string (both sides) |
|
||||||
|
| SEC-14 | Security | Medium | api/app/config/settings.py | Insecure defaults (JWT_SECRET) with no prod startup guard |
|
||||||
|
| SEC-15 | Security | Medium | api (device_ws, scout_runner, deep_agent) | User message content in INFO logs (GDPR) |
|
||||||
|
| SEC-16 | Security | Medium | api/app/models.py | Relational-memory entity labels stored plaintext (third-party PII) |
|
||||||
|
| SEC-17 | Security | Medium | api/app/api/middleware/rate_limit.py | In-memory limiter state → ×4 bypass under gunicorn -w 4 |
|
||||||
|
| SEC-18 | Security | Medium | api (auth.py, scouts.py) | In-memory OAuth state stores break multi-worker; strand Gmail tokens |
|
||||||
|
| SEC-19 | Security | Medium | api/app/api/routes/scout_webhooks.py | Gmail Pub/Sub webhook fail-open when audience unset |
|
||||||
|
| SEC-20 | Security | Medium | adiuvAI/src/main/ipc.ts | IPC bridge doesn't validate sender; all procedures public |
|
||||||
|
| SEC-21 | Security | Medium | adiuvAI/src/main/ai/token.ts | Plaintext token + backup-key fallback in electron-store |
|
||||||
|
| SEC-22 | Security | Medium | adiuvAI/src/renderer/lib/httpLink.ts | Web SPA keeps JWT in localStorage |
|
||||||
|
| SEC-23 | Security | Medium | waitlist/app/security.py | Origin validation bypass via startswith |
|
||||||
|
| SEC-24 | Security | Medium | waitlist/app/routes.py | GDPR erasure + confirm triggered by bare GET (mail scanners) |
|
||||||
|
| SEC-25 | Security | Medium | waitlist/app/routes.py | Unauthenticated email re-send = mail-bombing vector |
|
||||||
|
| SEC-26 | Security | Medium | website/index.html | Third-party scripts without SRI; lucide@latest from unpkg |
|
||||||
|
| SEC-27 | Security | Medium | api/app/api/routes/device_ws.py | _index_sessions: no ownership check + never purged on disconnect |
|
||||||
|
| SEC-28 | Security | Medium | adiuvAI (db, attachments) | Local SQLite + attachments unencrypted at rest (product decision) |
|
||||||
|
| SEC-29 | Security | Low | api/app/api/routes/auth.py | Email enumeration via register 409 |
|
||||||
|
| SEC-30 | Security | Low | api/app/api/routes/device_ws.py | Internal exception strings returned to WS clients |
|
||||||
|
| SEC-31 | Security | Low | adiuvAI/src/main/router/index.ts | attachments.create accepts arbitrary sourcePath |
|
||||||
|
| SEC-32 | Security | Low | adiuvAI/src/main/index.ts | Scout Gmail deep link forwarded without local state check |
|
||||||
|
| SEC-33 | Security | Low | waitlist/app/routes.py | Partial email fragments in logs |
|
||||||
|
| SEC-34 | Security | Low | website (all pages) | No CSP / security headers on static site |
|
||||||
|
| SEC-35 | Security | Low | website/i18n.js | innerHTML i18n sink (latent XSS) |
|
||||||
|
| SEC-36 | Security | Low | adiuvAI/src/main/index.ts | sandbox not explicitly enabled in webPreferences |
|
||||||
|
| CORR-01 | Correctness | **Critical** | api/app/api/routes/device_ws.py | Chat tool calls: no timeout + send-before-register race → agent hangs forever |
|
||||||
|
| CORR-02 | Correctness | High | api/app/core/device_manager.py | WS unregister clobbers a freshly reconnected connection |
|
||||||
|
| CORR-03 | Correctness | High | api/app/core/memory_maintenance.py | Free-tier extraction drains with empty content — feature is a paid no-op |
|
||||||
|
| CORR-04 | Correctness | High | adiuvAI (renderer chat contexts) | Backend errors returned as success payloads ignored → chat wedged in "streaming" + listener leaks |
|
||||||
|
| CORR-05 | Correctness | High | adiuvAI/src/main/router/index.ts + drizzle-executor.ts | Cascade-delete gaps, zero transactions; AI-issued deletes orphan rows + disk files |
|
||||||
|
| CORR-06 | Correctness | High | adiuvAI (backend-client.ts + files/indexer.ts) | WS drop mid-index permanently wedges project in 'scanning' |
|
||||||
|
| CORR-07 | Correctness | Medium | adiuvAI/src/main/api/backend-client.ts | WS close handler clobbers newly opened connection (logout→login race) |
|
||||||
|
| CORR-08 | Correctness | Medium | api/app/core/memory_maintenance.py | Confidence decay applied per invocation, not per period |
|
||||||
|
| CORR-09 | Correctness | Medium | api (models + memory_middleware) | Check-then-insert races: memory_core / memory_relations lack unique constraints |
|
||||||
|
| CORR-10 | Correctness | Medium | api/app/api/routes/device_ws.py | Fire-and-forget create_task: no refs, no per-user concurrency cap |
|
||||||
|
| CORR-11 | Correctness | Medium | adiuvAI/src/main/router/index.ts | tasks.update unconditionally destroys briefing + chat history |
|
||||||
|
| CORR-12 | Correctness | Medium | adiuvAI/src/main/ai/orchestrator.ts | Stream errors emitted as plain stream_end → blank persisted messages |
|
||||||
|
| CORR-13 | Correctness | Medium | adiuvAI/src/main/api/backend-client.ts | withRetry retries non-idempotent POSTs (duplicate scout runs) |
|
||||||
|
| CORR-14 | Correctness | Medium | api/app/billing/stripe_service.py | Sync Stripe SDK calls block the event loop |
|
||||||
|
| CORR-15 | Correctness | Medium | api/app/api/routes/device_ws.py | run_home_stream swallows exceptions; client never gets error stream_end |
|
||||||
|
| CORR-16 | Correctness | Medium | api/app/api/routes/auth.py | delete_account: Stripe commit then DB deletes, broad except pass |
|
||||||
|
| CORR-17 | Correctness | Medium | adiuvAI/src/main/files + projectFolders.ts | Index-session TOCTOU + stale _active entry |
|
||||||
|
| CORR-18 | Correctness | Low | api/app/api/routes/scouts.py | Scout trigger TOCTOU → duplicate concurrent runs |
|
||||||
|
| CORR-19 | Correctness | Low | adiuvAI (router, drizzle-executor, auth-manager) | Swallowed-error triage: silent [] returns, bare catches, unguarded JSON.parse |
|
||||||
|
| CORR-20 | Correctness | Low | api/app/api/routes/auth.py | Naive/aware datetime kludge on refresh expiry |
|
||||||
|
| CORR-21 | Correctness | Low | adiuvAI (misc) | LanguageSync inside App body; fake AbortSignal; startsWith containment |
|
||||||
|
| PERF-01 | Performance | High | api/app/api/routes/auth.py | bcrypt runs on the event loop (rate-limit-exempt login path) |
|
||||||
|
| PERF-02 | Performance | High | api/app/core/deep_agent.py | Home channel buffers ALL tokens — streaming endpoint doesn't stream |
|
||||||
|
| PERF-03 | Performance | High | api/app/core/memory_maintenance.py | Hourly mining duplicates proactive rows; _load_proactive has no LIMIT |
|
||||||
|
| PERF-04 | Performance | Medium | adiuvAI/src/main/db/schema.ts | No SQLite indexes on any user-data table (sync full scans block IPC) |
|
||||||
|
| PERF-05 | Performance | Medium | api/app/models.py | Missing PG composite/pgvector indexes; refresh_tokens never purged |
|
||||||
|
| PERF-06 | Performance | Medium | adiuvAI (orchestrator + ChatSurface) | Per-token IPC message + full markdown re-parse per chunk |
|
||||||
|
| PERF-07 | Performance | Medium | api/app/core/deep_agent.py | Tool calls executed serially per LLM step (each a WS round-trip) |
|
||||||
|
| PERF-08 | Performance | Medium | api (deep_agent, scout_runner, langfuse_client) | Blocking lf.flush() per request; sync get_prompt on cache miss |
|
||||||
|
| PERF-09 | Performance | Medium | api/app/api/middleware/auth.py | get_current_user: 3 queries + full Fernet decrypt on EVERY request |
|
||||||
|
| PERF-10 | Performance | Medium | api/app/core/memory_middleware.py | Same User row re-fetched up to 4×/message; update_core commits per key |
|
||||||
|
| PERF-11 | Performance | Medium | adiuvAI/src/main/ai/orchestrator.ts | Health-check HTTP round-trip before every chat message |
|
||||||
|
| PERF-12 | Performance | Medium | adiuvAI/src/main/router/index.ts | Paid LLM brief regeneration fired by every week-relevant mutation |
|
||||||
|
| PERF-13 | Performance | Medium | adiuvAI (vite configs, ChatChartBlock, notes route) | No route code-splitting; recharts + Milkdown eager in initial bundle |
|
||||||
|
| PERF-14 | Performance | Medium | adiuvAI/src/renderer/index.tsx | Default QueryClient → refetch storms into sync SQLite on window focus |
|
||||||
|
| PERF-15 | Performance | Medium | adiuvAI (projectFolders.ts + indexer.ts) | Double full-folder walk; no WS backpressure (memory spike) |
|
||||||
|
| PERF-16 | Performance | Medium | api (scouts/engine.py, device_ws.py) | DB sessions held open across LLM calls (pool starvation) |
|
||||||
|
| PERF-17 | Performance | Medium | api/app/core/scout_runner.py | Per-file get_file_metadata WS round-trip during scans |
|
||||||
|
| PERF-18 | Performance | Low | api (embeddings.py, llm.py) | Fresh AsyncOpenAI client per call, never closed |
|
||||||
|
| PERF-19 | Performance | Low | adiuvAI/src/renderer/components/tasks/TaskListView.tsx | tasks.list returns all rows; pagination client-side |
|
||||||
|
| PERF-20 | Performance | Low | api/app/api/middleware/rate_limit.py | Limiter dict never sheds keys; tier read from JWT not DB |
|
||||||
|
| PERF-21 | Performance | Low | adiuvAI/src/main/api/drizzle-executor.ts | Unbounded page-details tables; uncapped pdf/docx base64 reads |
|
||||||
|
| PERF-22 | Performance | Low | adiuvAI/src/renderer/context | HeaderContext unmemoized value; ContextualChat re-renders route per token |
|
||||||
|
| DEAD-01 | Dead code | High | api/requirements.txt | 5 dead deps: pinecone, qdrant-client, boto3, moto[s3], google-auth-oauthlib |
|
||||||
|
| DEAD-02 | Dead code | High | api/app/core/scout_registry.py | Fully orphaned module (BaseAgent, zero importers) |
|
||||||
|
| DEAD-03 | Dead code | Medium | api/app/api/middleware/rate_limit.py | slowapi Limiter exported, zero decorated routes — dep removable |
|
||||||
|
| DEAD-04 | Dead code | Medium | api/app/api/routes/chat.py | HTTP /chat routes unused by Electron client (WS is the channel) |
|
||||||
|
| DEAD-05 | Dead code | Medium | adiuvAI/package.json | 4 unused deps: next-themes, mammoth, pdf-parse, @hello-pangea/dnd |
|
||||||
|
| DEAD-06 | Dead code | Medium | adiuvAI/src (5 files) | Dead modules: batch-types, useChatStream, useTaskBriefCache, ScoutRunLog, blocks barrel |
|
||||||
|
| DEAD-07 | Dead code | Medium | adiuvAI/src/main/auth/backup-key.ts | Documented architecture but zero importers — wire or remove |
|
||||||
|
| DEAD-08 | Dead code | Medium | adiuvAI/knip.json | Missing web-SPA entries → knip false-flags live files |
|
||||||
|
| DEAD-09 | Dead code | Medium | api/requirements.txt | langchain meta-package → langchain-core; redundant websockets pin |
|
||||||
|
| DEAD-10 | Dead code | Low | api (13 files) | 13 ruff F401 unused imports (auto-fixable) |
|
||||||
|
| DEAD-11 | Dead code | Low | adiuvAI/src (7 exports) | Dead utility exports (getRawSqlite, parseDateRange, formatTime, …) |
|
||||||
|
| DEAD-12 | Dead code | Low | api/requirements.txt + backend-client.ts | Dev deps in runtime requirements; stale endpoint doc comments |
|
||||||
|
| DEPS-01 | Dependencies | High | adiuvAI/package.json | ws 8.19.0 vulnerable (GHSA-58qx-3vcg-4xpx) — only runtime-reachable vuln |
|
||||||
|
| DEPS-02 | Dependencies | High | api/requirements.txt | python-jose floor 3.3.0 permits CVE-2024-33663/33664 versions |
|
||||||
|
| DEPS-03 | Dependencies | Medium | api/requirements.txt | cryptography floor 42.0.0 permits CVE-2024-26130 versions |
|
||||||
|
| DEPS-04 | Dependencies | Medium | api/ | No lock file — `>=` floors make installs unauditable |
|
||||||
|
| DEPS-05 | Dependencies | Medium | adiuvAI/package.json | eslint 8 + @typescript-eslint 5 both EOL |
|
||||||
|
| DEPS-06 | Dependencies | Medium | adiuvAI/package.json | Electron 40 → 42 (two majors of Chromium security patches behind) |
|
||||||
|
| DEPS-07 | Dependencies | Low | adiuvAI (transitive) | tmp/tar/esbuild advisories in forge/drizzle-kit toolchain — no fix, monitor |
|
||||||
|
| DEPS-08 | Dependencies | Low | adiuvAI/package.json | @types/ws in dependencies instead of devDependencies |
|
||||||
|
| QUAL-01 | Quality | High | adiuvAI/src/main/router/index.ts | God file: 1967 LOC, 15 sub-routers — split per domain |
|
||||||
|
| QUAL-02 | Quality | High | api/app/core/deep_agent.py | _run_single_agent vs _run_single_agent_stream ~90% duplicated |
|
||||||
|
| QUAL-03 | Quality | High | api (4 files) | Four hand-rolled LLM tool loops — extract one |
|
||||||
|
| QUAL-04 | Quality | High | adiuvAI/src/main/router/index.ts | CRUD repetition ×8 tables — extract factory |
|
||||||
|
| QUAL-05 | Quality | High | api/app/core/deep_agent.py | God file: 1329 LOC — split prompt-context / tools / runner |
|
||||||
|
| QUAL-06 | Quality | Medium | adiuvAI/src/main/api/backend-client.ts | God file 1191 LOC; 230-line openDeviceWebSocket; send* boilerplate ×6 |
|
||||||
|
| QUAL-07 | Quality | Medium | api/app/core/scout_runner.py | 1051 LOC; run_local_agent/run_cloud_agent 210-line twins |
|
||||||
|
| QUAL-08 | Quality | Medium | api/app/api/routes/device_ws.py | Protocol + business logic mixed — extract ws_handlers package |
|
||||||
|
| QUAL-09 | Quality | Medium | adiuvAI/src/renderer/components/projects/ProjectSidebar.tsx | 1292 LOC, 25+ useState — extract dialogs |
|
||||||
|
| QUAL-10 | Quality | Medium | api/app/api/routes/scouts.py | Ownership-check + 404 repeated 6× — FastAPI dependency |
|
||||||
|
| QUAL-11 | Quality | Medium | adiuvAI/src/main/router/index.ts | Three competing error-return styles, zero TRPCError |
|
||||||
|
| QUAL-12 | Quality | Medium | api/app/api/routes/device_ws.py | Raw-dict frames bypass pydantic schemas (also kills 7 type:ignores) |
|
||||||
|
| QUAL-13 | Quality | Medium | api + adiuvAI (WS boundary) | camelCase/snake_case contract untrusted (dual-read hedges) |
|
||||||
|
| QUAL-14 | Quality | Medium | api (auth.py, scouts.py) | OAuth route blocks → dedicated files; shared TTLStateStore |
|
||||||
|
| QUAL-15 | Quality | Low | api/app/agents | Copy-pasted _is_uuid + row formatting across agents |
|
||||||
|
| TYPE-01 | Type safety | High | adiuvAI/src/renderer/components/ai/blocks (3 files) | Broken relative imports — block types silently `any` |
|
||||||
|
| TYPE-02 | Type safety | High | api/ | No Python type checker configured at all |
|
||||||
|
| TYPE-03 | Type safety | Medium | adiuvAI/ | tsc --noEmit does not pass; no CI typecheck |
|
||||||
|
| TYPE-04 | Type safety | Medium | api/app/core/deep_agent.py | context: dict[str, Any] threaded everywhere — RequestContext model |
|
||||||
|
| TYPE-05 | Type safety | Medium | adiuvAI/src/main/api/drizzle-executor.ts | Cast cluster: table-as-Record ×5, bogus Boolean predicate casts |
|
||||||
|
| TYPE-06 | Type safety | Low | adiuvAI/src/renderer | (window as any).electronAI ×4 — global declaration file |
|
||||||
|
| TYPE-07 | Type safety | Low | api + adiuvAI | Misc: ToolResult TypedDict, 11 missing py annotations, vite/client types |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
Line numbers reference the current tree — **do mechanical fixes before file splits**.
|
||||||
|
|
||||||
|
**Phase 0 — Mechanical quick wins (safe for low-capability executor, no review needed):**
|
||||||
|
TYPE-01 → DEPS-01 → DEAD-10 → DEAD-01 → DEAD-02 → DEAD-05 → DEAD-08 → DEPS-08 → DEAD-11 → DEAD-12 → PERF-14 → PERF-18 → CORR-20
|
||||||
|
|
||||||
|
**Phase 1 — Critical & High security (ALL ⚠️ REVIEW, human validates each diff):**
|
||||||
|
SEC-01 → SEC-05 → SEC-06 → SEC-07 → SEC-20 (these four+one harden the Electron trust boundary together) → SEC-02 → SEC-14 → SEC-19 (share a startup-guard mechanism: do SEC-14 first) → SEC-09 → SEC-08 → SEC-04 → SEC-15 → SEC-03 (needs migration design)
|
||||||
|
|
||||||
|
**Phase 2 — Critical & High correctness:**
|
||||||
|
CORR-01 → CORR-02 (same files, same review session) → CORR-04 → CORR-12 (CORR-04 depends on CORR-12's error-frame contract — implement together) → CORR-06 → CORR-05 ⚠️ → CORR-03
|
||||||
|
|
||||||
|
**Phase 3 — Remaining Medium security + correctness:**
|
||||||
|
SEC-10 → SEC-11 → SEC-12 → SEC-13 (coordinated api+electron change) → SEC-17/SEC-18 (one Redis introduction covers both) → SEC-27 → SEC-21 → remaining SEC Mediums → CORR-07..CORR-17
|
||||||
|
|
||||||
|
**Phase 4 — Performance:**
|
||||||
|
PERF-01 → PERF-02 ⚠️ → PERF-03 → PERF-04 → PERF-05 (migrations) → PERF-09/PERF-10 (same subsystem) → PERF-06 → rest
|
||||||
|
|
||||||
|
**Phase 5 — Dependencies & tooling:**
|
||||||
|
DEPS-02 → DEPS-03 → DEPS-04 → TYPE-02 → TYPE-03 → DEPS-05 ⚠️ → DEPS-06 ⚠️
|
||||||
|
|
||||||
|
**Phase 6 — Structural refactors (after all above; invalidates line numbers):**
|
||||||
|
QUAL-02 ⚠️ → QUAL-03 ⚠️ → QUAL-01 → QUAL-04 ⚠️ → QUAL-05 → QUAL-06..QUAL-15 → TYPE-04..TYPE-07 → DEAD-04 ⚠️ / DEAD-07 ⚠️ (owner decisions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Security
|
||||||
|
|
||||||
|
> Per instruction: **every SEC item is ⚠️ REVIEW** unless explicitly noted "mechanical". A botched auth check or sanitizer is worse than the original bug.
|
||||||
|
|
||||||
|
### SEC-01 — Backend can read ANY file on the user's disk ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts` — `handleListDirectory` L375-398, `handleReadFileContent` L400-436, `handleGetFileMetadata` L438-461
|
||||||
|
- **Severity:** Critical · OWASP A01:2021 Broken Access Control
|
||||||
|
- **Problem:** These handlers take a backend-supplied `path`, resolve it (`fs.promises.realpath(path.resolve(dirPath))`) and read it with **no allowlist or root containment**. `read_file_content` returns up to 500 KB of any file (`~/.ssh/id_rsa`, browser cookie DBs). The backend is semi-trusted by design; if compromised, blast radius = the entire user filesystem. The backend adds no guard either (`api/app/agents/filesystem_agent.py:24-33` passes absolute paths straight through). Contrast: `handleReadProjectFolderFile` (L511-529) does containment correctly.
|
||||||
|
- **Fix:** After realpath, assert the resolved path is inside an allowed root: the set of configured `projects.folderPath` values (query the projects table) plus nothing else. Use `const rel = path.relative(root, resolved); if (rel.startsWith('..') || path.isAbsolute(rel)) throw new ExecutorError('Access denied')`. Apply identically in all three handlers. Also fix the existing `startsWith` containment in `handleReadProjectFolderFile` L527 to use the same `path.relative` check (defeats `C:\proj\foo-evil` vs `C:\proj\foo`).
|
||||||
|
- **Risk:** Local filesystem scouts or agent flows that legitimately read outside project folders will break — verify which directories local scout configs reference and add those roots to the allowlist before locking down. Test: agent file-read inside project folder still works; read of `C:\Windows\win.ini` rejected.
|
||||||
|
|
||||||
|
### SEC-02 — No rate limiting on login/register/refresh ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/middleware/rate_limit.py:41-48` (`_EXEMPT_PATHS`), `:88-91` (no-token pass-through)
|
||||||
|
- **Severity:** High · OWASP A07:2021 Identification & Authentication Failures
|
||||||
|
- **Problem:** Limiter only acts on requests with a valid Bearer JWT; `if not token: return await call_next(request)`. Login/register/refresh are unauthenticated → **never rate-limited**. Unlimited password guessing and refresh-token guessing.
|
||||||
|
- **Fix:** Remove `/api/v1/auth/login` and `/api/v1/auth/register` from `_EXEMPT_PATHS`. In `dispatch()`, when no token is present and `request.url.path.startswith("/api/v1/auth/")`, apply an IP-keyed sliding window (reuse the existing `_window` mechanism keyed on client IP, fixed limit e.g. 5/min for login/register/refresh, 429 on excess). Note this remains per-process until SEC-17 lands Redis.
|
||||||
|
- **Risk:** Shared-NAT users may hit limits; size window accordingly. Test: 6th login attempt in a minute → 429; authenticated traffic unaffected.
|
||||||
|
|
||||||
|
### SEC-03 — Fernet key stored in plaintext beside its ciphertext ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/models.py:83` (`User.encryption_key`), read at `api/app/core/memory_middleware.py:559-566`
|
||||||
|
- **Severity:** High · OWASP A02:2021 Cryptographic Failures
|
||||||
|
- **Problem:** The per-user key that encrypts all memory tiers sits as a plaintext column in the same PostgreSQL DB. Any DB-level compromise or backup leak decrypts everything — the "encrypted at rest" zero-trust claim collapses.
|
||||||
|
- **Fix:** Introduce a KEK from env/KMS (model: the existing `OAUTH_ENCRYPTION_KEY` pattern in `api/app/integrations/__init__.py:90-102`). Store `Fernet(KEK).encrypt(user_key)` in the column; unwrap in `_get_fernet`. One-time Alembic data migration re-wrapping existing keys; keep a dual-read window (try unwrap, fall back to raw) during rollout, then remove fallback.
|
||||||
|
- **Risk:** Migration must be reversible and tested against a DB snapshot; losing the KEK = losing all memory data. Requires key-rotation runbook. This is design work — human required.
|
||||||
|
|
||||||
|
### SEC-04 — Raw user content & PII sent to Langfuse ⚠️ REVIEW
|
||||||
|
- **Files:** `api/app/core/deep_agent.py:912,926,933,975` · `api/app/scouts/engine.py:260-268` (email subject/sender/2000-char body) · `api/app/core/scout_runner.py:266-278`
|
||||||
|
- **Severity:** High · OWASP A09:2021 / GDPR Art. 5, 28, 44 / Italian Law 132/2025
|
||||||
|
- **Problem:** Full conversation history, tool I/O, and triaged **email content** are transmitted to Langfuse when configured. `hash_user_id()` only pseudonymizes the identifier; bodies go in clear. Requires Langfuse as contracted sub-processor; possible international transfer depending on `LANGFUSE_BASE_URL`.
|
||||||
|
- **Fix:** Add `LANGFUSE_CAPTURE_CONTENT: bool = False` to settings. Wrap every `input=`/`output=` content assignment in the three files: when flag is false, pass `{"redacted": True, "chars": len(...)}` instead. Keep metadata/token counts. Document Langfuse as sub-processor in the privacy policy regardless.
|
||||||
|
- **Risk:** Reduced debugging richness. Verify Langfuse prompt management (`get_prompt`) is unaffected — it's a separate path.
|
||||||
|
|
||||||
|
### SEC-05 — Mass assignment in drizzle-executor insert/update ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts` — `handleInsert` L268-290, `handleUpdate` L292-327
|
||||||
|
- **Severity:** High · OWASP A08:2021
|
||||||
|
- **Problem:** Table name is allowlisted, columns are not: `{ id: crypto.randomUUID(), ...data, createdAt: now }` spreads backend keys verbatim — any real column writable. Also: `...data` is spread **after** `id`, so a backend-supplied `id` overrides the executor's generated one (contradicting the code's own comment).
|
||||||
|
- **Fix:** Define a per-table writable-column allowlist (object map table→string[]). Before building `values`/`withTimestamp`, pick only allowlisted keys; always strip `id` and `createdAt` from incoming `data`/`updates`. Reorder insert to `{ ...picked, id: crypto.randomUUID(), createdAt: now, updatedAt: now }`.
|
||||||
|
- **Risk:** Additive validation. Test every agent CRUD flow (create task/note/event, update, propose_note_edit) still works — the allowlist must cover all fields agents legitimately set.
|
||||||
|
|
||||||
|
### SEC-06 — No navigation / window-open guards ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/main/index.ts` — `createWindow` L85-117 (handlers absent repo-wide)
|
||||||
|
- **Severity:** High · OWASP A05:2021
|
||||||
|
- **Problem:** No `setWindowOpenHandler`, no `will-navigate` guard. Any navigation to remote content lands in the privileged renderer (which has `window.electronTRPC`/`electronAI`).
|
||||||
|
- **Fix:** In `createWindow` add:
|
||||||
|
```ts
|
||||||
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
if (url.startsWith('https://')) shell.openExternal(url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
mainWindow.webContents.on('will-navigate', (e, url) => {
|
||||||
|
const ok = (MAIN_WINDOW_VITE_DEV_SERVER_URL && url.startsWith(MAIN_WINDOW_VITE_DEV_SERVER_URL)) || url.startsWith('file://');
|
||||||
|
if (!ok) e.preventDefault();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
- **Risk:** Minimal — app uses internal TanStack routing. Test external links in chat still open in OS browser.
|
||||||
|
|
||||||
|
### SEC-07 — No Content-Security-Policy ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/index.html` (no meta CSP); `src/main/index.ts` (no onHeadersReceived)
|
||||||
|
- **Severity:** High · OWASP A05:2021
|
||||||
|
- **Problem:** Zero CSP → no defense-in-depth for any future XSS sink; `connect-src` unrestricted (compromised renderer exfiltrates anywhere).
|
||||||
|
- **Fix:** In production only (`!IS_DEV`), register `session.defaultSession.webRequest.onHeadersReceived` injecting: `default-src 'self'; script-src 'self'; connect-src 'self' https://<backend-host> wss://<backend-host>; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'none'`. Backend host from existing config.
|
||||||
|
- **Risk:** Tailwind/shadcn need `style-src 'unsafe-inline'`; Vite dev needs exemption (gate on IS_DEV). Test packaged build fully: chat, charts, Milkdown editor, images.
|
||||||
|
|
||||||
|
### SEC-08 — Waitlist rate limit keyed on spoofable headers ⚠️ REVIEW
|
||||||
|
- **File:** `waitlist/app/rate_limit.py:19-32`
|
||||||
|
- **Severity:** High · OWASP A04:2021 / CWE-348
|
||||||
|
- **Problem:** `_get_client_ip` trusts `cf-connecting-ip` then `x-forwarded-for` unconditionally. Random header per request = fresh 5/min bucket → unlimited signup spam, Brevo quota burn, poisoned stored `ip_address`.
|
||||||
|
- **Fix:** Add `TRUSTED_PROXY_IPS: str = ""` setting. Trust `cf-connecting-ip` only when `request.client.host` is in that list; otherwise use `request.client.host`. Also firewall origin to Cloudflare ranges at infra level.
|
||||||
|
- **Risk:** Misconfigured trusted-proxy list rate-limits Cloudflare itself (all users share CF egress IPs) — must be deployed together with correct env value.
|
||||||
|
|
||||||
|
### SEC-09 — Waitlist CONFIRM_SECRET random default ⚠️ REVIEW
|
||||||
|
- **File:** `waitlist/app/config.py:19`; `waitlist/Dockerfile:21` (`-w 2`)
|
||||||
|
- **Severity:** High · OWASP A05:2021
|
||||||
|
- **Problem:** `CONFIRM_SECRET: str = secrets.token_hex(32)` — with 2 gunicorn workers and no env var, each worker has a different HMAC secret: ~50% of confirm/unsubscribe links fail silently; all tokens die on restart. Breaks double opt-in AND the GDPR erasure path.
|
||||||
|
- **Fix:** Remove default: `CONFIRM_SECRET: str` (required field — pydantic fails startup if unset). Update `.env.example` comment.
|
||||||
|
- **Risk:** Deploys without the env var now fail loudly at boot — that's the point. Coordinate with deploy.
|
||||||
|
|
||||||
|
### SEC-10 — Login 500 for social-only accounts ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/routes/auth.py:169` (and `_verify_password` L73-74)
|
||||||
|
- **Severity:** Medium · OWASP A07:2021
|
||||||
|
- **Problem:** OAuth-only users have `password_hash=None`; `hashed.encode()` → uncaught `AttributeError` → 500. Differing responses (500 vs 401) = account-type oracle + crash.
|
||||||
|
- **Fix:** Replace the check with: `if user is None or user.password_hash is None or not _verify_password(body.password, user.password_hash): raise HTTPException(401, "Invalid credentials")`.
|
||||||
|
- **Risk:** None. Test: password login against a Google-only account → 401.
|
||||||
|
|
||||||
|
### SEC-11 — No password strength on register ⚠️ REVIEW (mechanical)
|
||||||
|
- **File:** `api/app/api/routes/auth.py:100-104`
|
||||||
|
- **Severity:** Medium · OWASP A07:2021
|
||||||
|
- **Problem:** `password: str` unconstrained (1-char/empty accepted); `_ChangePasswordRequest` already requires `min_length=8`. `email` is plain `str`, not `EmailStr`.
|
||||||
|
- **Fix:** Line 102: `password: str = Field(min_length=8)`; change `email: str` to `email: EmailStr` (import from pydantic). Mirror what change-password already does.
|
||||||
|
- **Risk:** None for new accounts. Existing weak-password accounts unaffected.
|
||||||
|
|
||||||
|
### SEC-12 — No logout / refresh-token revocation ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/routes/auth.py` (endpoint absent; rotation only at L209)
|
||||||
|
- **Severity:** Medium · OWASP A07:2021
|
||||||
|
- **Problem:** No server-side logout; stolen refresh token valid 30 days; no reuse detection on rotated tokens.
|
||||||
|
- **Fix:** Add `POST /api/v1/auth/logout` (authenticated): body `{refresh_token: str}`, delete the matching `RefreshToken` row (SHA-256 the input, filter by hash + `user_id`). Optional `all: bool` to delete all the user's tokens. Wire Electron `auth-manager.ts` logout to call it before clearing local tokens.
|
||||||
|
- **Risk:** Electron + backend change together. Test logout then refresh → 401.
|
||||||
|
|
||||||
|
### SEC-13 — JWT in WebSocket URL query string ⚠️ REVIEW
|
||||||
|
- **Files:** `api/app/api/routes/device_ws.py:78` · `adiuvAI/src/main/api/backend-client.ts:895`
|
||||||
|
- **Severity:** Medium · OWASP A09:2021
|
||||||
|
- **Problem:** `?token=<jwt>` lands in reverse-proxy access logs, APM traces. Token grants full account access for 30 min.
|
||||||
|
- **Fix:** The Electron client uses the `ws` package which supports handshake headers: pass `{ headers: { Authorization: \`Bearer ${token}\` } }` as the second arg to `new WebSocket(url, opts)`. Backend: read `websocket.headers.get("authorization")` first, fall back to `?token=` for one release, then remove fallback. Interim mitigation: strip query strings for `/api/v1/ws/device` in proxy access logs.
|
||||||
|
- **Risk:** Coordinated two-repo change; old clients break when fallback removed. Web SPA (browser WebSocket) cannot set headers — if web ever uses the WS, it needs the ticket pattern instead.
|
||||||
|
|
||||||
|
### SEC-14 — Insecure config defaults, no prod guard ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/config/settings.py:6-7`; `api/docker-compose.yml:22-23`
|
||||||
|
- **Severity:** Medium · OWASP A05:2021
|
||||||
|
- **Problem:** `JWT_SECRET = "change-me-in-production"` and `postgres:postgres` defaults; nothing refuses to start in prod with defaults → forgeable JWTs.
|
||||||
|
- **Fix:** Add a pydantic `model_validator` on `Settings`: if `ENV == "prod"` and (`JWT_SECRET == "change-me-in-production"` or `len(JWT_SECRET) < 32`), raise `ValueError` at import. Same guard hook used by SEC-19 (`GMAIL_PUBSUB_AUDIENCE` empty → fail in prod).
|
||||||
|
- **Risk:** Prod deploys with missing env now fail at boot (intended). Dev unaffected.
|
||||||
|
|
||||||
|
### SEC-15 — User message content in INFO logs ⚠️ REVIEW
|
||||||
|
- **Files:** `api/app/api/routes/device_ws.py:258-265,343` · `api/app/core/scout_runner.py:292-311` · `api/app/core/deep_agent.py:958-984` · `api/app/core/memory_middleware.py:528,554`
|
||||||
|
- **Severity:** Medium · OWASP A09:2021 / GDPR
|
||||||
|
- **Problem:** Chat messages (`message[:200]`), tool args/outputs (`[:800]`/`[:1200]`), memory queries logged at INFO — uncontrolled PII sink in centralized logs.
|
||||||
|
- **Fix:** Replace content interpolations with length+ID only (e.g. `logger.info("home_request user=%s chars=%d", user_id, len(message))`). Gate full content behind `logger.debug` + a `LOG_CONTENT=false` setting.
|
||||||
|
- **Risk:** Less debuggable ops. Each call site is a one-line change; review the full grep list before executing.
|
||||||
|
|
||||||
|
### SEC-16 — Relational-memory labels stored plaintext ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/models.py:432-435`; predicates at `api/app/api/routes/memory.py:34-45`
|
||||||
|
- **Severity:** Medium · OWASP A02:2021 / GDPR
|
||||||
|
- **Problem:** `subject_label`/`object_label` hold names of user's contacts/employers (`works_at`, `reports_to`) in plaintext — third-party personal data. Only `notes_encrypted` protected.
|
||||||
|
- **Fix (decision):** Either encrypt labels with the user Fernet key (breaks label-match queries at `memory_middleware.py:421-428` — would need deterministic encryption or hashing for lookups) or formally document in the processing record and rely on SEC-03's KEK to mitigate DB-leak. **Recommend the latter** + SEC-03. Human decision required.
|
||||||
|
- **Risk:** Encrypting breaks `upsert_relation` matching — substantial rework. Don't hand to low-capability model.
|
||||||
|
|
||||||
|
### SEC-17 — In-memory rate limiter ×4 bypass ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/middleware/rate_limit.py:67,81`
|
||||||
|
- **Severity:** Medium · OWASP A04:2021
|
||||||
|
- **Problem:** Per-process dicts under `gunicorn -w 4` → effective limit up to 4× tier limit, nondeterministic.
|
||||||
|
- **Fix:** Back the sliding window with Redis (`INCR` + `EXPIRE` or sorted-set window) keyed `rl:{user_id}:{minute}`. docker-compose already has a commented Redis service — enable it. Do together with SEC-18 (one Redis introduction).
|
||||||
|
- **Risk:** New infra dependency; need connection-failure fallback policy (fail-open vs fail-closed — recommend fail-open with warning log).
|
||||||
|
|
||||||
|
### SEC-18 — In-memory OAuth state stores ⚠️ REVIEW
|
||||||
|
- **Files:** `api/app/api/routes/auth.py:62` (`_pending_states`) · `api/app/api/routes/scouts.py:458,694-697` (`_pending_scout_oauth_states`, holds encrypted Gmail token between callback and finalize)
|
||||||
|
- **Severity:** Medium · OWASP A04:2021
|
||||||
|
- **Problem:** Multi-worker: authorize and callback can hit different workers → random 401s; scout flow strands the Gmail token; all state lost on redeploy.
|
||||||
|
- **Fix:** Move both to Redis with TTL (in-code comments already prescribe this). Implement one `TTLStateStore` (see QUAL-14) with `set(key, dict, ttl)` / `pop(key)`; Redis-backed in prod, dict-backed in tests.
|
||||||
|
- **Risk:** Prereq: Redis from SEC-17. Test full OAuth login + scout Gmail connect flows.
|
||||||
|
|
||||||
|
### SEC-19 — Gmail Pub/Sub webhook fail-open ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/routes/scout_webhooks.py:48-52`; default at `api/app/config/settings.py:69`
|
||||||
|
- **Severity:** Medium · OWASP A05:2021/A07:2021
|
||||||
|
- **Problem:** Empty `GMAIL_PUBSUB_AUDIENCE` → `_verify_pubsub_jwt` returns True for ANY unauthenticated POST; attacker-supplied `emailAddress` triggers scout runs (LLM cost) for arbitrary users. Also unauthenticated → not rate-limited (SEC-02).
|
||||||
|
- **Fix:** Fail closed: `if not settings.GMAIL_PUBSUB_AUDIENCE: if settings.ENV == "prod": return False; logger.warning(...); return True`. Tie into SEC-14's startup guard.
|
||||||
|
- **Risk:** Prod deploys without the env var stop receiving Gmail push (intended — they were unverified anyway).
|
||||||
|
|
||||||
|
### SEC-20 — IPC bridge doesn't validate sender ⚠️ REVIEW
|
||||||
|
- **Files:** `adiuvAI/src/main/ipc.ts:44-87` · `adiuvAI/src/main/index.ts:122-131` (dialog + scope handlers)
|
||||||
|
- **Severity:** Medium · OWASP A01:2021
|
||||||
|
- **Problem:** `ipcMain.on('trpc', …)` dispatches without checking `event.senderFrame`; all router procedures are `publicProcedure`. Any frame that runs in the renderer gets full CRUD/auth/scout access. The cheap control that contains SEC-06/SEC-07 failures.
|
||||||
|
- **Fix:** At the top of each `ipcMain.on/handle` callback: validate `event.senderFrame.url` starts with the dev-server URL (dev) or the app's `file://` index (prod); silently drop otherwise. Extract a `isTrustedSender(event)` helper used by all four registration points (`trpc`, `ai:contextual-scope-update`, `dialog:showOpenDialog`, any others found at execution time).
|
||||||
|
- **Risk:** None today (no sub-frames). Test dev + packaged.
|
||||||
|
|
||||||
|
### SEC-21 — Plaintext token/backup-key fallback ⚠️ REVIEW
|
||||||
|
- **Files:** `adiuvAI/src/main/ai/token.ts:37-47` · `adiuvAI/src/main/auth/backup-key.ts:24-35`
|
||||||
|
- **Severity:** Medium · OWASP A02:2021
|
||||||
|
- **Problem:** When `safeStorage.isEncryptionAvailable()` is false (keyring-less Linux/WSL), JWTs and the AES backup key are written plaintext to electron-store JSON. `readFromStore` also silently returns raw string on decrypt failure (downgrade masking).
|
||||||
|
- **Fix:** (1) Tag values with `enc:`/`plain:` prefix on write; on read, never attempt plaintext interpretation of an `enc:` value. (2) When safeStorage unavailable: keep tokens in-memory only (re-login per launch) and emit a renderer-visible warning. Never persist the backup key plaintext — fail backup-key creation instead.
|
||||||
|
- **Risk:** Keyring-less Linux users lose session persistence. Migration: existing stored values have no prefix — treat unprefixed as legacy-plaintext, re-encrypt on first read.
|
||||||
|
|
||||||
|
### SEC-22 — Web SPA token in localStorage ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/renderer/lib/httpLink.ts:19`
|
||||||
|
- **Severity:** Medium (web build only) · OWASP A07:2021
|
||||||
|
- **Problem:** `localStorage.getItem('adiuvai-token')` — readable by any XSS.
|
||||||
|
- **Fix (decision):** httpOnly secure cookie set by backend (needs CSRF protection + CORS review) or in-memory + silent refresh. If deferring, at minimum pair with strict CSP on the web deployment. Product/infra decision — don't execute blindly.
|
||||||
|
|
||||||
|
### SEC-23 — Waitlist Origin startswith bypass ⚠️ REVIEW (mechanical)
|
||||||
|
- **File:** `waitlist/app/security.py:50-51`
|
||||||
|
- **Severity:** Medium · OWASP A01:2021 / CWE-346
|
||||||
|
- **Problem:** `origin.startswith(o)` → `https://adiuvai.com.evil.example` passes.
|
||||||
|
- **Fix:** `origin in allowed` exact match; for Referer, compare `urllib.parse.urlsplit(referer)._replace(path="", query="", fragment="")` scheme+host against allowed origins.
|
||||||
|
- **Risk:** None; existing tests cover the endpoint.
|
||||||
|
|
||||||
|
### SEC-24 — GDPR erasure + confirm on bare GET ⚠️ REVIEW
|
||||||
|
- **File:** `waitlist/app/routes.py:87` (confirm), `:125-154` (unsubscribe → `_anonymize_entry`)
|
||||||
|
- **Severity:** Medium · CWE-650
|
||||||
|
- **Problem:** Mail scanners (SafeLinks, Mimecast) follow GETs → can anonymize a subscriber or auto-confirm a bot-planted email before any human clicks.
|
||||||
|
- **Fix:** Both GET handlers render a minimal HTML page with `<form method="POST">` + the token in a hidden field; move the state change to new POST handlers (`POST /waitlist/confirm`, `POST /waitlist/unsubscribe`). Keep token validation identical.
|
||||||
|
- **Risk:** Email templates unchanged (links still GET the page). Update the 19-test suite: existing GET tests now assert the form page; add POST tests.
|
||||||
|
|
||||||
|
### SEC-25 — Unauthenticated re-send mail bombing ⚠️ REVIEW
|
||||||
|
- **File:** `waitlist/app/routes.py:52-62`
|
||||||
|
- **Severity:** Medium · OWASP API4:2023
|
||||||
|
- **Problem:** Re-POSTing a known email re-triggers `send_confirmation_email`. With SEC-08's spoof bypass: mail-bomb any subscribed address via your Brevo account.
|
||||||
|
- **Fix:** Add `last_email_sent_at` column (Alembic migration). In the existing-entry branch, skip the send if `last_email_sent_at` < 15 min ago (still return the generic success response — preserves anti-enumeration).
|
||||||
|
- **Risk:** Migration + test. Depends on SEC-08 for full mitigation.
|
||||||
|
|
||||||
|
### SEC-26 — Website scripts without SRI; lucide@latest ⚠️ REVIEW (mechanical)
|
||||||
|
- **File:** `website/index.html:44-48`
|
||||||
|
- **Severity:** Medium · OWASP A08:2021 / CWE-353
|
||||||
|
- **Problem:** GSAP + ScrollTrigger (cdnjs) and `lucide@latest` (unpkg) loaded with no `integrity`/`crossorigin`; `@latest` ships whatever unpkg serves — supply-chain risk on the page hosting the PII signup form.
|
||||||
|
- **Fix:** Pin lucide to an exact version; add `integrity="sha384-…" crossorigin="anonymous"` to all three script tags (compute hashes from the pinned files), or self-host under `website/assets/`.
|
||||||
|
- **Risk:** Hash must match exact file; test animations + icons after.
|
||||||
|
|
||||||
|
### SEC-27 — _index_sessions: no ownership check, never purged ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/routes/device_ws.py:65` (registry), `:700` (cancel), `:732` (batch)
|
||||||
|
- **Severity:** Medium · OWASP A01:2021
|
||||||
|
- **Problem:** `_handle_index_file_batch` / `_handle_index_session_cancel` look up by `sessionId` without verifying `session["user_id"] == user_id` — a user who learns another's session UUID can cancel it or corrupt it. Sessions also leak forever on mid-index disconnect (no TTL, no cleanup in WS finally).
|
||||||
|
- **Fix:** (1) In both handlers, after lookup: `if session is None or session["user_id"] != user_id: send error frame, return`. (2) In the WS `finally` block, drop all `_index_sessions` entries whose `user_id == user_id` and `ws is websocket`.
|
||||||
|
- **Risk:** None — pure tightening. Test indexing E2E (test_ws_index_session suite exists).
|
||||||
|
|
||||||
|
### SEC-28 — Local data unencrypted at rest ⚠️ REVIEW (product decision)
|
||||||
|
- **Files:** `adiuvAI/src/main/db/index.ts:94` (plain SQLite), `adiuvAI/src/main/attachments/storage.ts:17-19`
|
||||||
|
- **Severity:** Medium (informational) · OWASP A02:2021
|
||||||
|
- **Problem:** All local user content (clients, projects, notes, attachments) plaintext on disk. For a data-sovereignty product, a deliberate gap.
|
||||||
|
- **Fix (decision):** Options: SQLCipher (better-sqlite3-multiple-ciphers fork), or document reliance on OS full-disk encryption, or encrypt attachments with the existing device backup key (DEAD-07). Roberto decides; do not execute without direction.
|
||||||
|
|
||||||
|
### SEC-29 — Register email enumeration · Low ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/routes/auth.py:127` — 409 "Email already registered". Acceptable UX trade-off for most SaaS; mitigate via SEC-02 rate limiting. **Fix:** no code change now; revisit if email verification flow lands.
|
||||||
|
|
||||||
|
### SEC-30 — Internal exception strings to WS clients · Low ⚠️ REVIEW (mechanical)
|
||||||
|
- **File:** `api/app/api/routes/device_ws.py:467-468, 503-505, 539-541, 578-580, 625-631, 653-659`
|
||||||
|
- **Problem:** `error=str(exc)` / f-string exception text sent to renderer — can leak DB errors, paths.
|
||||||
|
- **Fix:** Replace each with a generic message (`"Internal error"` + the request_id); keep the existing server-side `logger.exception`. 6 call sites, mechanical.
|
||||||
|
|
||||||
|
### SEC-31 — attachments.create arbitrary sourcePath · Low ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/main/router/index.ts:1893-1915` → `attachments/storage.ts:25-37`
|
||||||
|
- **Problem:** Renderer-supplied absolute path copied without restriction; defense-in-depth issue (matters only post-SEC-06/20 compromise).
|
||||||
|
- **Fix:** Track paths returned by the `dialog:showOpenDialog` handler in a per-session `Set`; `attachments.create` rejects `sourcePath` not in the set. Cap file size (e.g. 100 MB).
|
||||||
|
- **Risk:** Drag-and-drop flows (if any) bypass the dialog — verify before enforcing.
|
||||||
|
|
||||||
|
### SEC-32 — Scout Gmail deep link no state check · Low ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/main/index.ts:46-54`
|
||||||
|
- **Problem:** `adiuvai://scout/oauth/gmail/callback` code+state forwarded to renderer with no pending-flow check (login OAuth path does this correctly in `auth-manager.ts:321-359`). Backend validates state server-side, limiting impact.
|
||||||
|
- **Fix:** Mirror `_pendingOAuth`: main process records pending Gmail-OAuth state when the flow starts; `handleDeepLink` drops callbacks whose state doesn't match.
|
||||||
|
|
||||||
|
### SEC-33 — Waitlist partial email in logs · Low
|
||||||
|
- **File:** `waitlist/app/routes.py:75`. `email[:3]` can reveal short local-parts. **Fix:** log entry `id` only. Mechanical.
|
||||||
|
|
||||||
|
### SEC-34 — Website missing CSP/security headers · Low ⚠️ REVIEW
|
||||||
|
- **Fix:** At the reverse proxy serving `website/`: add `Content-Security-Policy` (allowlist cdnjs/unpkg or self-hosted after SEC-26), `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`. Infra change, not repo code.
|
||||||
|
|
||||||
|
### SEC-35 — Website innerHTML i18n sink · Low ⚠️ REVIEW (mechanical)
|
||||||
|
- **File:** `website/i18n.js:461-464`
|
||||||
|
- **Problem:** `el.innerHTML = t[key]` — not exploitable today (hardcoded dictionary) but latent; `?lang=` is user-controlled.
|
||||||
|
- **Fix:** Use `textContent` for all keys except an explicit whitelist of the 4 `data-i18n-html` keys that genuinely contain markup; never derive `t[key]` content from URL/user input.
|
||||||
|
|
||||||
|
### SEC-36 — sandbox not explicit · Low ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/main/index.ts:97-98` (webPreferences)
|
||||||
|
- **Problem:** `contextIsolation: true`, `nodeIntegration: false` are set (good), fuses are strong, but `sandbox` not enabled. Preload only uses `contextBridge`/`ipcRenderer`, so sandbox should be compatible.
|
||||||
|
- **Fix:** Add `sandbox: true` to webPreferences; full regression pass on packaged build (preload behavior changes under sandbox).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Correctness & Safety
|
||||||
|
|
||||||
|
### CORR-01 — Chat tool calls: no timeout + send-before-register race ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/routes/device_ws.py:238-245` (`_executor`); contrast correct pattern `api/app/core/scout_runner.py:190-198`
|
||||||
|
- **Severity:** Critical
|
||||||
|
- **Problem:** (1) `await future` with no `asyncio.wait_for` — if Electron never replies (renderer crash, swallowed executor exception) the agent run hangs forever. (2) Frame is sent **before** `create_pending_call` registers the future; a fast `tool_result` arriving first no-ops on unknown id → permanent hang.
|
||||||
|
- **Fix:** Reorder: `future = device_manager.create_pending_call(user_id, payload["id"])` THEN `await websocket.send_text(...)`. Wrap: `return await asyncio.wait_for(future, timeout=30)` with `except asyncio.TimeoutError:` → pop the entry from `device_manager` `pending_calls` and return `{"error": "tool call timed out"}` so the LLM loop can continue. Also pop the dict entry on timeout in the scout path (`device_manager.py:145` leaks it).
|
||||||
|
- **Risk:** 30 s must exceed slowest legitimate executor op (large folder file reads) — confirm against `read_project_folder_file` worst case. Test: kill renderer mid-tool-call → agent run ends with error within 30 s.
|
||||||
|
|
||||||
|
### CORR-02 — WS unregister clobbers reconnected connection ⚠️ REVIEW
|
||||||
|
- **Files:** `api/app/api/routes/device_ws.py:133-145` · `api/app/core/device_manager.py:73-81`
|
||||||
|
- **Severity:** High
|
||||||
|
- **Problem:** Message loop ends silently on disconnect; heartbeat keeps the handler alive up to 30 s. If the client reconnects in that window, `register()` stores the new connection, then the OLD handler's `finally` calls `unregister(user_id)` — **popping the new healthy connection** and cancelling its pending futures. Also user looks online during the zombie window (scout runs dispatched into void).
|
||||||
|
- **Fix:** (1) `device_manager.unregister(self, user_id, ws)`: only pop when `self._connections.get(user_id)` and `self._connections[user_id].ws is ws`. Update both call sites. (2) End the heartbeat promptly: replace `asyncio.gather(loop, heartbeat)` with `done, pending = await asyncio.wait({...}, return_when=FIRST_COMPLETED)` then cancel pending.
|
||||||
|
- **Risk:** Core connection lifecycle — test reconnect-within-30s, normal disconnect, heartbeat failure (test_device_ws suite exists).
|
||||||
|
|
||||||
|
### CORR-03 — Free-tier extraction drains with empty content
|
||||||
|
- **File:** `api/app/core/memory_maintenance.py:120-154`
|
||||||
|
- **Severity:** High
|
||||||
|
- **Problem:** `run_extraction(..., last_user_msg="", last_assistant_msg="", ...)` — `row.episode_id` never used. Every hourly tick: one wasted gpt-4o-mini call per queue row over `"User: \nAssistant: "`, then row deleted. Free-tier memory extraction has never worked, and it costs money.
|
||||||
|
- **Fix:** For each row, load `MemoryEpisodic` by `row.episode_id` (+ `user_id`), decrypt `summary` with the user's Fernet (reuse `memory_middleware._get_fernet` or pass middleware instance), pass decrypted content as the extraction input. Group rows per user into a single call. Skip+delete rows whose episode is missing.
|
||||||
|
- **Risk:** Test asserting extracted candidates reference episode content (extend test_memory_extraction). Watch cost: now real extractions run.
|
||||||
|
|
||||||
|
### CORR-04 — Backend errors as success payloads ignored → chat wedged
|
||||||
|
- **Files:** `adiuvAI/src/main/router/index.ts:957-960` (returns `{response:'', error}`) · `adiuvAI/src/renderer/context/ContextualChatContext.tsx:160,210-240` · `adiuvAI/src/renderer/components/brief/TaskBriefChat.tsx:125,144` · `adiuvAI/src/renderer/components/ai/AIChatPanel.tsx:313-319`. Correct template: `useAIChat.ts:226-256`.
|
||||||
|
- **Severity:** High
|
||||||
|
- **Problem:** When offline/unauthenticated, no stream frames ever arrive; the three listed call sites never check `data.error` on mutation success → `isStreaming` stays true forever, `send()` no-ops, stream listeners leak (each leaked listener receives every future `ai:stream` frame).
|
||||||
|
- **Fix:** Preferred (cleaner, pre-1.0): make `aiRouter.chat`/brief/research procedures **throw TRPCError** instead of returning `{error}` (ipcLink already propagates errors, `ipcLink.ts:76`); then `onError` is the single error path — update `useAIChat` accordingly. Each renderer call site: in the error path, call the active unsubscribe, push an error bubble, reset `isStreaming`/`isResearching`. Add `useEffect` unmount cleanup for `TaskBriefChat`'s send listener (L144). Delete dead `hooks/useChatStream.ts` (DEAD-06).
|
||||||
|
- **Risk:** Touches every chat surface. Test: stop backend → send in contextual sidebar, task brief, home chat → error bubble shown, re-send works. Implement together with CORR-12 (shared error-frame contract).
|
||||||
|
|
||||||
|
### CORR-05 — Cascade-delete gaps; zero transactions ⚠️ REVIEW
|
||||||
|
- **Files:** `adiuvAI/src/main/router/index.ts` — `projects.delete` L211-220, `clients.deleteWithCascade` L119-151, `timelineEvents.delete` L711-723, `notes.delete` L835-840, `tasks.delete` L511-534 · `adiuvAI/src/main/api/drizzle-executor.ts` `handleDelete` L329-340
|
||||||
|
- **Severity:** High (data integrity)
|
||||||
|
- **Problem:** No FK constraints, hand-rolled deletes, no `db.transaction()` anywhere in src/main: project delete orphans timelineEvents/notes/projectFolderFiles/noteEdits; event delete leaves dangling dependency edges that **poison the cycle-guard BFS** (L756-772, falsely rejects new deps); AI-issued deletes (`handleDelete`) cascade nothing — orphan rows + attachment files on disk; `tasks.delete` interleaves file unlinks between row deletes (partial state on crash).
|
||||||
|
- **Fix:** Pre-1.0 clean option (preferred per Roberto's preference): add `references(..., { onDelete: 'cascade' })` to schema + enable `PRAGMA foreign_keys = ON` in `db/index.ts`, generate migration. Alternative: shared `cascadeDelete{Task,Project,Client,Note,TimelineEvent}` helpers in a new `src/main/db/cascade.ts`, used by BOTH tRPC routers and `handleDelete`, wrapped in `db.transaction()` (better-sqlite3 sync transactions), file unlinks after commit.
|
||||||
|
- **Risk:** FK route requires verifying every insert ordering satisfies constraints, and migration against existing user DBs with already-orphaned rows (clean orphans first). Test matrix: delete project with events+deps+notes+pending edits; agent-delete task with attachments → no orphan rows, files removed. Human review required — this deletes data.
|
||||||
|
|
||||||
|
### CORR-06 — WS drop wedges folder indexing in 'scanning'
|
||||||
|
- **Files:** `adiuvAI/src/main/api/backend-client.ts:1099-1100` (listeners cleared, `onDone` never called) · `adiuvAI/src/main/files/indexer.ts:106-117` (only `finalize` resets status) · consumers refusing rescan: `drizzle-executor.ts:486`, `files/daily-rescan.ts:22`, `router/projectFolders.ts:61`
|
||||||
|
- **Severity:** High
|
||||||
|
- **Problem:** WS close clears `indexListeners` without invoking callbacks → `folderLastScanStatus` stays `'scanning'` forever → all future scans refuse to run. Recovery currently requires manual DB surgery.
|
||||||
|
- **Fix:** (1) In the WS close handler, before `indexListeners.clear()`: iterate and call each `onDone('error')`. (2) Startup recovery in `initDb()` or app-ready: `UPDATE projects SET folder_last_scan_status='error' WHERE folder_last_scan_status='scanning'`.
|
||||||
|
- **Risk:** None meaningful. Test: kill backend mid-index → status becomes 'error', rescan works.
|
||||||
|
|
||||||
|
### CORR-07 — backend-client close handler clobbers new socket
|
||||||
|
- **File:** `adiuvAI/src/main/api/backend-client.ts:1079-1104`
|
||||||
|
- **Severity:** Medium-High
|
||||||
|
- **Problem:** `ws.on('close')` unconditionally nulls `persistentWs`, stops heartbeat, rejects+clears ALL listener maps. Logout→login: the old socket's deferred close event destroys the new connection's state and schedules a duplicate reconnect.
|
||||||
|
- **Fix:** Capture `ws` in closure; first line of the close and error handlers: `if (this.persistentWs !== ws) return;`. Same guard inside the heartbeat interval callback.
|
||||||
|
- **Risk:** Low. Test logout+immediate login → exactly one socket, in-flight chat on new socket survives.
|
||||||
|
|
||||||
|
### CORR-08 — Decay applied per invocation, not per period
|
||||||
|
- **File:** `api/app/core/memory_maintenance.py:280-309` (proactive, runs hourly), `:61-104` (relations)
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** `periods = days_elapsed // PERIOD; new_confidence = confidence * FACTOR**periods` recomputed from immutable `created_at` and re-applied every tick — a row ≥8 days old loses 10% **per hour** (intended: per 7 days).
|
||||||
|
- **Fix:** Compute confidence as a pure function of age each read, OR add `last_decayed_at` column (migration) and decay only `(now - last_decayed_at) // PERIOD` periods, updating the column. Pure-function approach avoids the migration — preferred.
|
||||||
|
- **Risk:** Recompute approach changes stored semantics — confirm nothing else mutates `confidence` (confirmation boosts do; then `last_decayed_at` is the correct route). ⚠️ REVIEW the choice.
|
||||||
|
|
||||||
|
### CORR-09 — Check-then-insert races, missing unique constraints ⚠️ REVIEW
|
||||||
|
- **Files:** `api/app/core/memory_middleware.py:232-247` (`update_core`), `:421-450` (`upsert_relation`); `api/app/api/routes/auth.py:125-127` (register IntegrityError→500)
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** No `UNIQUE(user_id, key)` on memory_core, none on memory_relations `(user_id, subject_label, predicate, object_label)`. Concurrent writers (documented fire-and-forget language seeding + onboarding PUT) create duplicates; `scalar_one_or_none()` then raises `MultipleResultsFound`. Register duplicate-email race returns 500 instead of 409.
|
||||||
|
- **Fix:** Alembic migration adding both unique constraints (dedupe existing rows first: keep newest per key). Convert both writers to PostgreSQL `INSERT ... ON CONFLICT DO UPDATE` (copy the savepoint pattern from `scouts/engine.py:123-135`). Register: catch `IntegrityError` → 409.
|
||||||
|
- **Risk:** Migration fails if production data has duplicates — dedupe step mandatory. Test concurrent update_core calls.
|
||||||
|
|
||||||
|
### CORR-10 — Fire-and-forget create_task: no refs, no concurrency cap
|
||||||
|
- **Files:** `api/app/api/routes/device_ws.py:120,130,171-215`; also `scouts.py:247-249`, `memory_middleware.py:201`, `note_agent.py:84,112`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** Bare `asyncio.create_task` — loop holds weak refs (GC can collect mid-flight); exceptions surface only at GC; a frame-spamming client spawns unbounded concurrent agent runs (WS path has no rate limit).
|
||||||
|
- **Fix:** Per-connection `tasks: set[asyncio.Task]`; on spawn: `t = create_task(...); tasks.add(t); t.add_done_callback(tasks.discard)`. In WS `finally`: cancel all. Cap: if ≥3 in-flight stream-producing requests for the connection, reply immediately with `stream_end` error frame "busy".
|
||||||
|
- **Risk:** Cap value is judgment — confirm 3 is right for legitimate parallel use (home + contextual + brief). ⚠️ REVIEW the cap.
|
||||||
|
|
||||||
|
### CORR-11 — tasks.update wipes briefing + chat history on every update
|
||||||
|
- **File:** `adiuvAI/src/main/router/index.ts:504-506`; hash helper exists at `:55-64`; staleness check at `:976-989`
|
||||||
|
- **Severity:** Medium (silent user-data loss)
|
||||||
|
- **Problem:** Every update — including a kanban status drag — deletes `taskBriefings` + `taskBriefChats`.
|
||||||
|
- **Fix:** Load the task before update; compute `hashTaskForBriefing(prev)` vs hash of merged next state; run the two deletes only when hashes differ.
|
||||||
|
- **Risk:** Verify `hashTaskForBriefing` covers exactly the fields the briefing depends on. Test: status drag preserves brief chat; title edit invalidates it.
|
||||||
|
|
||||||
|
### CORR-12 — Stream errors emitted as plain stream_end
|
||||||
|
- **File:** `adiuvAI/src/main/ai/orchestrator.ts:105,111,141,147,192,198`; renderer persistence of empty message `ContextualChatContext.tsx:187-204`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** `onError` sends a normal `stream_end` (no error flag); catch blocks send `stream_end` with `requestId: ''` (matches nothing). Mid-stream failure looks like success → blank assistant bubble **persisted to SQLite**.
|
||||||
|
- **Fix:** Extend the stream-end frame with optional `error?: string` in `src/shared` types (mirror backend's existing error-capable `WsStreamEnd`). orchestrator: pass real requestId + error message; delete the `requestId: ''` sends. Renderers: on `error`, show error bubble, skip `appendMessage` persistence when content empty. Backend twin fix: CORR-15.
|
||||||
|
- **Risk:** Frame contract change across main↔renderer — do together with CORR-04.
|
||||||
|
|
||||||
|
### CORR-13 — withRetry retries non-idempotent POSTs ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/src/main/api/backend-client.ts:700-723`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** `proxyPost` retried up to 3× on timeout/5xx/429 — a timed-out-but-processed `/scouts/trigger` or scout create duplicates server work; immediate retry on 429 worsens it.
|
||||||
|
- **Fix:** Add `{ retry?: boolean }` option to `proxyPost` defaulting false; enable only for verified-idempotent endpoints (reads proxied via POST, if any). Exclude `RateLimitError` from retry entirely (or respect Retry-After). GET/DELETE-by-id keep retry.
|
||||||
|
- **Risk:** Requires classifying every proxyPost call site idempotent-or-not — review the list.
|
||||||
|
|
||||||
|
### CORR-14 — Sync Stripe calls block the event loop
|
||||||
|
- **File:** `api/app/billing/stripe_service.py:71-79, 197, 225-238` (auto_paging_iter paginates network synchronously)
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Fix:** Wrap each Stripe call in `await asyncio.to_thread(...)`; for `auto_paging_iter`, collect inside the thread: `await asyncio.to_thread(lambda: list(itertools.islice(it, 100)))`.
|
||||||
|
- **Risk:** None behavioral. Mechanical once pattern set.
|
||||||
|
|
||||||
|
### CORR-15 — run_home_stream swallows exceptions; no error stream_end
|
||||||
|
- **File:** `api/app/api/routes/device_ws.py:295-307`; correct pattern at `:498-505` (brief)
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** On exception: logged, partial chunks stored as an episode, client never receives `stream_end` with error → renderer hangs (pairs with CORR-04).
|
||||||
|
- **Fix:** In the except block: send `WsStreamEnd(request_id=..., error="Internal error")` (generic per SEC-30); skip `store_episode` when no response text was produced.
|
||||||
|
- **Risk:** None. Test: raise inside agent → client gets error frame.
|
||||||
|
|
||||||
|
### CORR-16 — delete_account: partial transaction + broad except ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/api/routes/auth.py:759-795`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** Stripe cancel commits its own transaction, then memory deletes, then user delete + final commit; failure mid-way = cancelled subscription with live account. `except Exception: pass` at L786-787 hides genuine failures. (DB-level `ondelete="CASCADE"` makes the manual memory deletes redundant.)
|
||||||
|
- **Fix:** Reorder: all DB deletes in one transaction first (drop the redundant manual memory-table deletes — FK cascade covers them; verify each memory table's FK has `ondelete="CASCADE"` in migrations before relying on it), commit, THEN Stripe cancel; on Stripe failure log + enqueue retry rather than failing the deletion. Narrow the except to expected types.
|
||||||
|
- **Risk:** Account deletion is irreversible — human review + test with seeded data.
|
||||||
|
|
||||||
|
### CORR-17 — Index-session TOCTOU + stale _active entry
|
||||||
|
- **Files:** `adiuvAI/src/main/files/indexer.ts:67` (`startIndexSession`) · `adiuvAI/src/main/router/projectFolders.ts:84-99` · trigger sites `drizzle-executor.ts:487-501`, `files/daily-rescan.ts:22`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** Three concurrent triggers can all pass the `!== 'scanning'` check before any writes 'scanning'. `_active.set` runs after `await startIndexSession` returns — a zero-delta session completes before the entry exists, leaving stale `{status:'starting'}`.
|
||||||
|
- **Fix:** Module-level `const _inFlight = new Set<string>()` in indexer.ts; first synchronous statement of `startIndexSession`: `if (_inFlight.has(projectId)) throw new Error('Scan already in progress'); _inFlight.add(projectId)`; remove in `finalize`'s finally. In projectFolders.ts, register the `_active` entry before awaiting.
|
||||||
|
- **Risk:** Low. Also add `.catch(console.error)` to the `void startIndexSession(...)` fire-and-forget sites (daily-rescan.ts:25, drizzle-executor.ts:497).
|
||||||
|
|
||||||
|
### CORR-18 — Scout trigger TOCTOU · Low
|
||||||
|
- **File:** `api/app/api/routes/scouts.py:225-249` (+ `scout_runner.py:577`)
|
||||||
|
- **Fix:** Add `stable_agent_id` to `_running_agents` synchronously in the route before `db.commit()`/`create_task`; discard on spawn failure. Keep the in-task add idempotent.
|
||||||
|
|
||||||
|
### CORR-19 — Swallowed-error triage · Low
|
||||||
|
- **Files/fixes (mechanical batch):**
|
||||||
|
- `adiuvAI/src/main/router/index.ts:1156-1164, 1276-1294, 1374-1382, 1429-1434` — scout list/labels/catalog/runs return `[]` on any error → return `{ data, error }` envelope (or throw, per QUAL-11's chosen convention).
|
||||||
|
- `adiuvAI/src/main/api/drizzle-executor.ts:566-568` — include `error: message` in the `kind:'error'` result.
|
||||||
|
- `adiuvAI/src/main/auth/auth-manager.ts:594` — wrap `JSON.parse(text)` in try/catch, return `{}` on failure.
|
||||||
|
- Documented-intentional `.catch(() => {})` seeding (router/index.ts:1577,1585) — leave as is.
|
||||||
|
|
||||||
|
### CORR-20 — Naive/aware datetime kludge · Low
|
||||||
|
- **File:** `api/app/api/routes/auth.py:205` — unconditional `.replace(tzinfo=timezone.utc)` would mislabel a non-UTC value. **Fix:** `if rt.expires_at.tzinfo is None: rt_exp = rt.expires_at.replace(tzinfo=timezone.utc) else: rt_exp = rt.expires_at` (pattern from `scout_runner.py:166-167`).
|
||||||
|
|
||||||
|
### CORR-21 — Renderer/main misc · Low
|
||||||
|
- `adiuvAI/src/renderer/index.tsx:22-30` — move `LanguageSync` component definition to module scope (currently declared inside `App`, remount trap).
|
||||||
|
- `adiuvAI/src/main/ipc.ts:68` — `undefined as unknown as AbortSignal`: make the field optional in the local type (see TYPE-05 family).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Performance
|
||||||
|
|
||||||
|
### PERF-01 — bcrypt on the event loop
|
||||||
|
- **File:** `api/app/api/routes/auth.py:69-74`, called at `:134, :169, :656-659`
|
||||||
|
- **Severity:** High
|
||||||
|
- **Problem:** 100-300 ms CPU per call freezes ALL WS streams/tool round-trips; login is currently rate-limit-exempt (SEC-02) → credential-stuffing stalls the whole server.
|
||||||
|
- **Fix:** `return await asyncio.to_thread(bcrypt.hashpw, password.encode(), bcrypt.gensalt())` etc. — make `_hash_password`/`_verify_password` async, update 3 call sites.
|
||||||
|
- **Risk:** None. Verify tests still pass (auth suite exists).
|
||||||
|
|
||||||
|
### PERF-02 — Home channel buffers all tokens (fake streaming) ⚠️ REVIEW
|
||||||
|
- **File:** `api/app/core/deep_agent.py:1219-1227` (token withholding), `:1075` (`ainvoke` not `astream`)
|
||||||
|
- **Severity:** High (UX latency)
|
||||||
|
- **Problem:** `run_home_stream` accumulates every token and yields ONE blob after the whole run (including tool loops). User sees nothing until the end. Root cause: `_normalize_tagged_list_lines` needs the full text.
|
||||||
|
- **Fix:** Switch the LLM call to `llm_with_tools.astream()` accumulating tool-call deltas; make `_normalize_tagged_list_lines` line-buffered (process and flush complete lines as they arrive, hold only the current partial line). Measure time-to-first-frame before/after.
|
||||||
|
- **Risk:** Tool-call delta accumulation across LangChain/LiteLLM is fiddly; the comment near L1080-1100 documents a past double-LLM-call bug that must not regress. Strong-model territory.
|
||||||
|
|
||||||
|
### PERF-03 — Hourly mining duplicates + unbounded prompt injection
|
||||||
|
- **File:** `api/app/core/memory_maintenance.py:175-253`; `api/app/core/memory_middleware.py:703-718` (`_load_proactive`, no LIMIT); cron at `api/app/main.py:46-77`
|
||||||
|
- **Severity:** High (cost + correctness)
|
||||||
|
- **Problem:** Hourly re-mining of the same 30-day window, 3-5 new rows per tick, no dedup → ~100 near-duplicate rows/user/day, all decrypted into every chat prompt.
|
||||||
|
- **Fix:** (1) `_load_proactive`: add `.order_by(confidence.desc()).limit(20)`. (2) Mine only when episodes newer than the user's latest `MemoryProactive.created_at` exist. (3) Dedup new patterns against existing (reuse `decide_action`). (4) Consider daily cadence for the mining portion of the cron.
|
||||||
|
- **Risk:** (1) is mechanical and urgent; (2)-(4) need the memory test suites re-run.
|
||||||
|
|
||||||
|
### PERF-04 — No SQLite indexes (Electron)
|
||||||
|
- **File:** `adiuvAI/src/main/db/schema.ts` (only ai_chat tables indexed, migration 0006)
|
||||||
|
- **Severity:** Medium (High at scale — scans are synchronous, blocking all IPC)
|
||||||
|
- **Fix:** Add Drizzle `index()` on: `tasks.projectId`, `tasks.dueDate`, `taskComments.taskId`, `taskAttachments.taskId`, `notes.projectId`, `noteEdits.noteId`, `timelineEvents.projectId`, `timelineEventDependencies.fromEventId`+`.toEventId`, `scoutRunActions.runId`; `uniqueIndex` on `projectFolderFiles(projectId, relativePath)` (indexer does a per-file SELECT by that pair — `files/indexer.ts:128-136`, O(n²)). `npx drizzle-kit generate`.
|
||||||
|
- **Risk:** Migration must apply cleanly on existing user DBs (bootstrapMigrationsLedger path) — test against a populated dev DB.
|
||||||
|
|
||||||
|
### PERF-05 — Missing PG indexes; refresh_tokens never purged
|
||||||
|
- **File:** `api/app/models.py` (+ alembic migration needed)
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Fix:** One Alembic migration adding: `memory_core(user_id, key)` (unique — shared with CORR-09), `memory_episodic(user_id, created_at DESC)`, `memory_relations(user_id, subject_label, predicate, object_label)` (unique — CORR-09), `scout_run_logs(user_id, started_at)`, HNSW index on `memory_associative.embedding` (`USING hnsw (embedding vector_cosine_ops)`), `refresh_tokens(expires_at)`. Add a daily cleanup to the existing cron: `DELETE FROM refresh_tokens WHERE expires_at < now()`.
|
||||||
|
- **Risk:** HNSW build time on large tables; coordinate with CORR-09 dedupe.
|
||||||
|
|
||||||
|
### PERF-06 — Per-token IPC + full markdown re-parse
|
||||||
|
- **Files:** `adiuvAI/src/main/ai/orchestrator.ts:103,139,180` · `adiuvAI/src/renderer/components/ai/ChatSurface.tsx:197-199,352-358` · `useAIChat.ts:185-188`, `ContextualChatContext.tsx:183-185`, `TaskBriefChat.tsx:98-101`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Problem:** token → IPC message → setState → full ReactMarkdown re-parse of the accumulated text + smooth scroll, per token. Quadratic on long answers.
|
||||||
|
- **Fix:** Coalesce in main: buffer chunks per requestId, flush on 40 ms timer (and on stream_end). Renderer: render the in-flight message as plain text (whitespace-pre-wrap), run ReactMarkdown only on `stream_end`; throttle autoscroll with requestAnimationFrame.
|
||||||
|
- **Risk:** Ensure stream_end flushes the tail buffer; visual check that markdown "pops in" acceptably at end.
|
||||||
|
|
||||||
|
### PERF-07 — Serial tool calls per LLM step
|
||||||
|
- **File:** `api/app/core/deep_agent.py:953-986, 1103-1137`; `api/app/core/scout_runner.py:289-312`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Fix:** `results = await asyncio.gather(*(t.ainvoke(args) for ...))`, then append `ToolMessage`s preserving the original tool_call id order. Apply in all three loops (or once, after QUAL-03's shared loop).
|
||||||
|
- **Risk:** Concurrent tool calls hit the Electron executor concurrently — drizzle-executor handlers are async over a sync driver, fine, but verify pending-call map handles parallel ids (it's keyed by id — fine). Order preservation matters for the LLM.
|
||||||
|
|
||||||
|
### PERF-08 — Blocking Langfuse flush/get_prompt
|
||||||
|
- **Files:** `api/app/core/deep_agent.py:1009-1010,1165-1166` · `scout_runner.py:323-324` · `memory_extraction.py:319-322` · `langfuse_client.py:99`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Fix:** Delete the per-request `lf.flush()` calls (SDK background-flushes; keep one flush in app shutdown lifespan). Wrap `lf.get_prompt(...)` in `asyncio.to_thread` at the call sites in langfuse_client, or pre-warm the prompt cache at startup.
|
||||||
|
- **Risk:** Traces may lag a few seconds in Langfuse UI — acceptable.
|
||||||
|
|
||||||
|
### PERF-09 — get_current_user does 3 queries + full memory decrypt per request
|
||||||
|
- **File:** `api/app/api/middleware/auth.py:62-91`
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Fix:** Split dependencies: `get_current_user` (user+tier in ONE query via outerjoin on subscriptions, no memory) and `get_current_user_with_memory` (adds core-block decrypt). Switch only the routes that actually read `.memory` (chat/profile) to the latter; scout CRUD, billing, etc. use the light one.
|
||||||
|
- **Risk:** Grep every `current_user.memory` access before switching routes. Test suite covers auth paths.
|
||||||
|
|
||||||
|
### PERF-10 — Redundant memory-middleware queries; per-key commits
|
||||||
|
- **Files:** `api/app/core/memory_middleware.py` (`_get_fernet`/`_get_user_debug` per method) · `api/app/api/routes/auth.py:560-575` (`update_memory` loop)
|
||||||
|
- **Severity:** Medium
|
||||||
|
- **Fix:** Cache `(user, fernet, tier)` on the middleware instance per request (it's constructed per call — add lazy `_ctx` loaded once). `update_memory`: add a `update_core_many(dict)` method doing one SELECT of existing keys, one bulk write, one commit.
|
||||||
|
- **Risk:** Instance lifetime check: confirm MemoryMiddleware isn't a long-lived singleton (it takes `db` per call — if constructed per request, caching is safe).
|
||||||
|
|
||||||
|
### PERF-11 — Health check before every chat
|
||||||
|
- **File:** `adiuvAI/src/main/ai/orchestrator.ts:76-87`
|
||||||
|
- **Fix:** In `checkConnectivity`, return early-success when `backendClient.persistentWs?.readyState === OPEN` (expose a `isWsConnected()` getter); keep the HTTP health check only as fallback when WS down.
|
||||||
|
- **Risk:** None — `sendHomeRequest` already rejects with OfflineError when WS is down.
|
||||||
|
|
||||||
|
### PERF-12 — Paid brief regen per mutation
|
||||||
|
- **File:** `adiuvAI/src/main/router/index.ts:447,502,532,668,706,721` → `orchestrator.ts:240-254` (1.5 s debounce)
|
||||||
|
- **Severity:** Medium (LLM cost)
|
||||||
|
- **Fix:** Replace eager regeneration with dirty-flag: `invalidateBriefCache()` only marks the cache stale; regeneration happens lazily on next `dailyBrief` read or the existing scheduler slot tick. Delete `scheduleBriefRegeneration`.
|
||||||
|
- **Risk:** First home-page visit after edits pays the generation latency — show the stale brief while regenerating (cache already stores it).
|
||||||
|
|
||||||
|
### PERF-13 — No code-splitting; heavy eager imports
|
||||||
|
- **Files:** `adiuvAI/vite.renderer.config.mts:13-16`, `vite.web.config.mts:22` · `ChatChartBlock.tsx:20` (recharts) · `routes/notes.$noteId.tsx:27` (Milkdown)
|
||||||
|
- **Fix:** Add `autoCodeSplitting: true` to TanStackRouterVite in both configs. `React.lazy` + Suspense for `ChatChartBlock` (renders only on `<chart>` tag) and `MilkdownEditor`.
|
||||||
|
- **Risk:** Test chart block + notes editor render after splitting (Suspense fallback flash).
|
||||||
|
|
||||||
|
### PERF-14 — Default QueryClient → focus refetch storms (mechanical)
|
||||||
|
- **File:** `adiuvAI/src/renderer/index.tsx:15` (contrast `web-main.tsx:24`)
|
||||||
|
- **Fix:** `new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, refetchOnWindowFocus: false } } })` — local writes already invalidate explicitly.
|
||||||
|
- **Risk:** Multi-window staleness (single-window app — fine).
|
||||||
|
|
||||||
|
### PERF-15 — Double folder walk; no WS backpressure
|
||||||
|
- **Files:** `adiuvAI/src/main/router/projectFolders.ts:65` + `files/indexer.ts:67,191-215`
|
||||||
|
- **Fix:** Pass the pre-flight `ScanDelta` into `startIndexSession` (skip re-walk). In the batch loop, check `ws.bufferedAmount` and await drain (or gate on `onProgress` acks) before sending the next batch.
|
||||||
|
- **Risk:** Backpressure logic needs a stall timeout; test with a large folder.
|
||||||
|
|
||||||
|
### PERF-16 — DB sessions across LLM calls
|
||||||
|
- **Files:** `api/app/scouts/engine.py:48-70` · `api/app/api/routes/device_ws.py:736-829`; pool config `api/app/db.py:24-28`
|
||||||
|
- **Fix:** Restructure to short sessions: load needed rows → close → LLM work → new session for writes. Interim: raise `pool_size`/`max_overflow` and set explicit `pool_timeout`.
|
||||||
|
- **Risk:** Detached-instance errors after session close — copy needed attrs to plain objects first.
|
||||||
|
|
||||||
|
### PERF-17 — Per-file metadata WS round-trips
|
||||||
|
- **File:** `api/app/core/scout_runner.py:386-405`
|
||||||
|
- **Fix:** Extend the Electron `list_directory` executor result to include `mtimeMs`/`size` per entry (same `fs.stat` it already does for dirents — `drizzle-executor.ts:375-398`); backend reads mtimes from the listing, drops per-file `get_file_metadata` calls.
|
||||||
|
- **Risk:** Two-repo protocol change; version-skew tolerance (backend falls back to old path if field absent).
|
||||||
|
|
||||||
|
### PERF-18 — Per-call AsyncOpenAI clients (mechanical)
|
||||||
|
- **Files:** `api/app/core/embeddings.py:23` · `api/app/core/llm.py:154`
|
||||||
|
- **Fix:** Module-level `_client: AsyncOpenAI | None` lazy singleton; reuse across calls.
|
||||||
|
|
||||||
|
### PERF-19 — tasks.list client-side pagination · Low
|
||||||
|
- **Files:** `adiuvAI/src/main/router/index.ts:314-317` · `TaskListView.tsx:80,101`
|
||||||
|
- **Fix:** Pass `limit/offset` from TaskPager state; add `statusIn` filter server-side for the `active` pseudo-filter (L94-95). Defer until data sizes warrant.
|
||||||
|
|
||||||
|
### PERF-20 — Limiter dict growth; tier from JWT · Low
|
||||||
|
- **File:** `api/app/api/middleware/rate_limit.py:81,97-98,107-128`
|
||||||
|
- **Fix:** Delete empty lists after trim; superseded entirely if SEC-17's Redis lands. Tier-from-JWT staleness: document or read from DB (cheap once PERF-09's joined query exists).
|
||||||
|
|
||||||
|
### PERF-21 — Unbounded reads in drizzle-executor · Low
|
||||||
|
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts:623-630` (page details), `:535-548` (pdf/docx/image base64 uncapped)
|
||||||
|
- **Fix:** Apply default limit 50 (as `handleSelect` does) to the `_all` page-detail queries; enforce the 500 KB cap on binary reads too (reject larger with explicit error).
|
||||||
|
|
||||||
|
### PERF-22 — Context re-render hotspots · Low
|
||||||
|
- **Files:** `adiuvAI/src/renderer/context/HeaderContext.tsx:30` (unmemoized value) · `ContextualChatContext.tsx:253-270` + `hooks/useContextualScope.ts:5`
|
||||||
|
- **Problem:** Every route re-renders per streamed sidebar token (scope consumers subscribe to the same context as `streamingContent`).
|
||||||
|
- **Fix:** `useMemo` the HeaderContext value. Split ContextualChatContext into stable-actions context (`setScope`, `open`, `toggle`) and volatile stream context (`messages`, `streamingContent`, `isStreaming`); `useContextualScope` consumes only the stable one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Dead Code
|
||||||
|
|
||||||
|
### DEAD-01 — Five dead Python deps · High (mechanical)
|
||||||
|
- **File:** `api/requirements.txt` lines 12, 25, 26, 27, 31
|
||||||
|
- **Problem:** `boto3`, `moto[s3]`, `pinecone`, `qdrant-client`, `google-auth-oauthlib` — zero imports/refs anywhere (grep-verified; pgvector is the live vector path; `oauth_providers.py:71` explicitly states it does NOT use google-auth-oauthlib).
|
||||||
|
- **Fix:** Delete those 5 lines. Run `pytest` after. Keep `python-dotenv` (pydantic-settings `env_file` needs it) and `google-auth-httplib2` (transitive need of googleapiclient).
|
||||||
|
|
||||||
|
### DEAD-02 — Orphaned scout_registry.py · High (mechanical)
|
||||||
|
- **File:** `api/app/core/scout_registry.py` — `BaseAgent`, zero importers, zero subclasses, references deleted vector-store era. **Fix:** delete file; run pytest.
|
||||||
|
|
||||||
|
### DEAD-03 — slowapi dead · Medium (mechanical)
|
||||||
|
- **Files:** `api/app/api/middleware/rate_limit.py:26-28,52,66-67` · `middleware/__init__.py:10,17` · `requirements.txt:13`
|
||||||
|
- **Problem:** `Limiter` exported "for optional route-level decoration" — zero `@limiter` decorated routes exist.
|
||||||
|
- **Fix:** Remove the Limiter creation/exports and the requirement. **Sequencing:** do AFTER SEC-02 (which may or may not reuse slowapi — recommended fix reuses the custom window, so slowapi still goes).
|
||||||
|
|
||||||
|
### DEAD-04 — chat.py HTTP routes production-unused ⚠️ REVIEW (owner decision)
|
||||||
|
- **Files:** `api/app/api/routes/chat.py:40,63,105`; stale comments `adiuvAI/src/main/api/backend-client.ts:9,14`
|
||||||
|
- **Problem:** Electron client hits none of `POST /chat`, `/chat/brief`, `/chat/embed` (all traffic on `WS /api/v1/ws/device`). Routes are test-covered but production-dead — unless an external consumer exists.
|
||||||
|
- **Fix:** Roberto confirms no external consumers → delete routes + their tests (`test_brief_agent.py`/`test_middleware.py` portions that exercise them — keep middleware tests by retargeting another POST route). Either way, fix the stale doc comments in backend-client.ts (mentions nonexistent `WS /api/v1/chat/stream`) and `adiuvAI/src/shared/api-types.ts:338,390,425` (references nonexistent `/agents/*` HTTP routes). Note CLAUDE.md path drift: actual WS route is `/api/v1/ws/device`, docs say `/api/v1/device`.
|
||||||
|
|
||||||
|
### DEAD-05 — Four unused npm deps · Medium (mechanical)
|
||||||
|
- **File:** `adiuvAI/package.json:56,76,77,78`
|
||||||
|
- **Fix:** `npm uninstall next-themes mammoth pdf-parse @hello-pangea/dnd` (knip + grep verified zero imports; mammoth/pdf-parse are LanceDB-era leftovers).
|
||||||
|
|
||||||
|
### DEAD-06 — Dead Electron modules · Medium
|
||||||
|
- **Files (knip-verified, delete):** `adiuvAI/src/shared/batch-types.ts` (~5 KB), `src/renderer/hooks/useChatStream.ts` (has the CORR-04 bug — delete, don't fix), `src/renderer/hooks/useTaskBriefCache.ts`, `src/renderer/components/agents/ScoutRunLog.tsx`, `src/renderer/components/ai/blocks/index.tsx` (dead barrel).
|
||||||
|
- **⚠️ REVIEW one:** `blocks/ChatTableBlock.tsx` is dead only because the barrel is — check `api/app/core/output_formatter.py` first: if the backend emits table segments, wire ChatTableBlock into ChatSurface's segment switch instead of deleting (missing feature, not dead code).
|
||||||
|
|
||||||
|
### DEAD-07 — backup-key.ts unwired ⚠️ REVIEW (owner decision)
|
||||||
|
- **File:** `adiuvAI/src/main/auth/backup-key.ts` — zero importers, but CLAUDE.md documents it as deliberate architecture. **Fix:** Roberto decides: wire into the backup feature roadmap or delete (recoverable from git). Interacts with SEC-21 (its storage fallback) and SEC-28 (could encrypt attachments).
|
||||||
|
|
||||||
|
### DEAD-08 — knip.json missing web entries · Medium (mechanical)
|
||||||
|
- **File:** `adiuvAI/knip.json`
|
||||||
|
- **Problem:** `web-main.tsx` + `lib/httpLink.ts` false-flagged (live via `web.html:11`).
|
||||||
|
- **Fix:** Add `"web.html"` and `"src/renderer/web-main.tsx"` to the `entry` array; optionally add ignore rule for TanStack `Route` exports. Do this FIRST so subsequent knip runs are trustworthy.
|
||||||
|
|
||||||
|
### DEAD-09 — langchain meta-package; websockets pin · Medium (mechanical)
|
||||||
|
- **File:** `api/requirements.txt:4,20`
|
||||||
|
- **Fix:** Replace `langchain>=0.3.0` with `langchain-core>=0.3.0` (only `langchain_core`/`langchain_openai`/`langchain_litellm` are imported). Drop explicit `websockets` pin (uvicorn[standard] bundles it). Run pytest.
|
||||||
|
|
||||||
|
### DEAD-10 — Ruff F401 batch · Low (mechanical)
|
||||||
|
- **Fix:** `cd api && ruff check . --select F401 --fix` (13 findings: quota.py:7, deep_agent.py:1247, 11 test files). Keep the ERA001-flagged comment blocks (scouts.py:438-457, email_html.py:22 — documentation, add `# noqa: ERA001` if noisy).
|
||||||
|
|
||||||
|
### DEAD-11 — Dead TS exports · Low (mechanical)
|
||||||
|
- **Fix:** Remove `export` (or delete bodies): `getDbPath`/`getRawSqlite`/`closeDb` (`src/main/db/index.ts:118-137`), `generateAndCacheBrief` (`orchestrator.ts:257` — superseded if PERF-12 lands; coordinate), `attachmentsRoot` (`attachments/storage.ts:17`), `parseMutationsToEntityTags` (`useAIChat.ts:76`), `formatTime` (`lib/date.ts:72`), `parseDateRange` (`lib/parseDate.ts:151`). Keep error classes exported (instanceof checks); keep `webPlatform` until DEAD-08 confirms.
|
||||||
|
|
||||||
|
### DEAD-12 — Dev deps in runtime requirements · Low (mechanical)
|
||||||
|
- **File:** `api/requirements.txt:22-25,41`
|
||||||
|
- **Fix:** Move `pytest`, `pytest-asyncio`, `aiosqlite`, `ruff` to a new `requirements-dev.txt`; update Dockerfile to install only runtime file; CI installs both.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Dependencies
|
||||||
|
|
||||||
|
### DEPS-01 — ws 8.19.0 vulnerable · High (mechanical)
|
||||||
|
- **File:** `adiuvAI/package.json` — GHSA-58qx-3vcg-4xpx (uninitialized memory disclosure, affects ≤8.20.0). Only runtime-reachable vuln of the 58 npm audit hits. **Fix:** `npm audit fix` (→ ^8.21.0, semver-compatible).
|
||||||
|
|
||||||
|
### DEPS-02 — python-jose floor permits CVE versions · High (mechanical)
|
||||||
|
- **File:** `api/requirements.txt:10` — floor 3.3.0 permits CVE-2024-33663 (algorithm confusion) / CVE-2024-33664 (JWE DoS), fixed in 3.4.0. App pins HS256 with explicit `algorithms=[...]` everywhere (mitigates the confusion path), but raise anyway. **Fix:** `python-jose[cryptography]>=3.4.0`. **⚠️ REVIEW (later):** consider migrating to PyJWT (python-jose barely maintained) — separate ticket.
|
||||||
|
|
||||||
|
### DEPS-03 — cryptography floor · Medium (mechanical)
|
||||||
|
- **File:** `api/requirements.txt:34` — floor 42.0.0 permits CVE-2024-26130 versions. **Fix:** `cryptography>=43.0.1`.
|
||||||
|
|
||||||
|
### DEPS-04 — No Python lock file · Medium
|
||||||
|
- **Problem:** All `>=` floors; installed versions unauditable. **Fix:** Adopt `uv lock` or `pip-compile`; commit the lock; run `pip-audit` against it in CI. ⚠️ REVIEW tool choice.
|
||||||
|
|
||||||
|
### DEPS-05 — eslint 8 + @typescript-eslint 5 EOL ⚠️ REVIEW
|
||||||
|
- **File:** `adiuvAI/package.json:38-39` — ts-eslint v5 predates TS 5.x support; lint coverage degraded. **Fix:** Dedicated PR: eslint 9/10 flat-config migration + typescript-eslint 8. Medium effort (config rewrite).
|
||||||
|
|
||||||
|
### DEPS-06 — Electron 40 → 42 ⚠️ REVIEW
|
||||||
|
- **Fix:** Dedicated PR bumping electron; rebuild/retest better-sqlite3 (forge cross-compilation hooks download platform binaries — verify versions), full smoke of packaged app.
|
||||||
|
|
||||||
|
### DEPS-07 — Transitive toolchain advisories · Low (no action)
|
||||||
|
- `tmp`/`tar` via @electron-forge 7.x, `esbuild` via drizzle-kit — build-machine-only, **no fix available**. Do NOT run `npm audit fix --force` (downgrades forge/drizzle-kit). Monitor forge releases.
|
||||||
|
|
||||||
|
### DEPS-08 — @types/ws placement · Low (mechanical)
|
||||||
|
- **File:** `adiuvAI/package.json:65` — move to devDependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Code Quality
|
||||||
|
|
||||||
|
> All QUAL items execute in **Phase 6**, after behavior fixes, because they move code and invalidate line references. Items marked ⚠️ involve behavior-preservation judgment.
|
||||||
|
|
||||||
|
### QUAL-01 — Split router/index.ts (1967 LOC) · High
|
||||||
|
- **Fix:** Split along existing boundaries into `src/main/router/{trpc.ts,clients.ts,projects.ts,tasks.ts,timeline.ts,notes.ts,ai.ts,scout.ts,auth.ts,memory.ts,settings.ts,attachments.ts}`; boundaries: clients 71-152, projects 154-228, tasks 230-560, timelineEvents 561-725, deps 726-787, notes 788-842, taskComments 843-877, settings 878-922, ai 923-1090, scout* 1091-1516, auth 1517-1729, memory 1730-1784, noteEdits 1785-1867, taskAttachments 1868-1946. `trpc.ts` holds the `t`/`router`/`publicProcedure` factory; `index.ts` becomes the `appRouter` merge + `AppRouter` export. Move brief helpers (L25-65) to `src/main/ai/brief-cache.ts` (with PERF-12).
|
||||||
|
- **Risk:** Low — routers are independent consts. Verify `AppRouter` type output unchanged (renderer compiles).
|
||||||
|
|
||||||
|
### QUAL-02 — Merge _run_single_agent twins ⚠️ REVIEW · High
|
||||||
|
- **File:** `api/app/core/deep_agent.py:873-1010` vs `:1013-1166` (~90% identical)
|
||||||
|
- **Fix:** Keep only the streaming generator; non-stream variant = join the tokens. **Preserve** the documented behavior at L1080-1100 (stream version intentionally avoids a second LLM call — past bug) as the single canonical path.
|
||||||
|
- **Risk:** Every chat path runs through this. Run full agent test suites; manual chat smoke.
|
||||||
|
|
||||||
|
### QUAL-03 — One LLM tool loop, four implementations ⚠️ REVIEW · High
|
||||||
|
- **Files:** `deep_agent.py:873,1013` · `scout_runner.py:222-324` · `scout_setup.py:237-349`
|
||||||
|
- **Fix:** New `api/app/core/tool_loop.py`: `async def run_tool_loop(llm, tools, messages, *, max_steps, lf_name, on_token=None)` yielding events. Four call sites become configuration. Use `contextlib.ExitStack` for Langfuse observations (kills the manual `__enter__`/`__exit__`). Parametrize observation names per site. Do AFTER QUAL-02 (reduces to three sites).
|
||||||
|
- **Risk:** Subtle per-site differences (Langfuse naming, error handling) — diff behavior carefully; PERF-07's gather lands here once.
|
||||||
|
|
||||||
|
### QUAL-04 — tRPC CRUD factory ⚠️ REVIEW · High
|
||||||
|
- **File:** `adiuvAI/src/main/router/index.ts` (8 tables repeat list/get/create/update/delete + `if (input.x !== undefined)` set-builders)
|
||||||
|
- **Fix:** `router/crud-factory.ts`: `makeCrudRouter(table, { createSchema, updateSchema, defaultOrder })`; plus `pickDefined(input, keys)` helper for update-set building. Prototype on `clients` first; domain procedures spread in.
|
||||||
|
- **Risk:** Generic factories can degrade tRPC type inference (`AppRouter` must stay exact for the renderer). If inference breaks, keep per-table routers and extract only `pickDefined` + create/update helpers.
|
||||||
|
|
||||||
|
### QUAL-05 — Split deep_agent.py (1329 LOC) · High
|
||||||
|
- **Fix:** (a) `app/core/prompt_context.py` ← `_*_injection` functions, `_request_context_block`, `format_folder_manifest`, `_prepare_context` (L43-519); (b) `app/core/agent_tools.py` ← `_memory_tools` (684-826), `_read_only_memory_tools`, `_brief_research_tools`, `_all_tools_for_user`, `_contextual_tools`, `get_page_details` (521-871); (c) `deep_agent.py` keeps runner + `run_*` entries. Pure moves; do after QUAL-02/03.
|
||||||
|
|
||||||
|
### QUAL-06 — backend-client.ts split + dispatch extract ⚠️ REVIEW · Medium
|
||||||
|
- **Fix:** (a) `backend-errors.ts` ← 5 error classes (L134-203); (b) extract `private dispatchFrame(frame)` + per-frame handlers from the 230-line `openDeviceWebSocket` (L876-1106); (c) `sendStreamFrame(frameType, payload, callbacks)` helper deduping the six `send*` methods (L276-660); (d) move `recordRunAction` (L55, SQLite write) to `src/main/scouts/scout-run-log.ts`. Full split into connection/requests modules optional second step.
|
||||||
|
- **Risk:** Shared mutable state (`streamListeners`, `persistentWs`) — extract as methods first, modules later. Do after CORR-07's guards land.
|
||||||
|
|
||||||
|
### QUAL-07 — scout_runner.py split + twin dedupe ⚠️ REVIEW · Medium
|
||||||
|
- **Fix:** `app/core/scouts/{local_runner.py,cloud_runner.py,runner_common.py}`; extract `_guard_device_online()` and `_process_items(items, process_one) -> RunStats` shared by the 210-line twins (`run_local_agent` 556-766 / `run_cloud_agent` 774-983).
|
||||||
|
- **Risk:** Per-item error semantics differ slightly between local/cloud — write characterization tests first (suites exist).
|
||||||
|
|
||||||
|
### QUAL-08 — device_ws.py handlers → package · Medium
|
||||||
|
- **Fix:** Keep `device_ws.py` as protocol layer (endpoint, `_message_loop`, `_heartbeat_loop`, `_mark_runs_disconnected`); move the 9 `_handle_*` to `app/api/ws_handlers/{chat,brief,journey,indexing}.py`. Handlers already take `(websocket, user_id, payload)`. Do with QUAL-12.
|
||||||
|
|
||||||
|
### QUAL-09 — ProjectSidebar.tsx (1292 LOC, 25+ useState) · Medium
|
||||||
|
- **Fix:** Extract `ProjectEditDialog.tsx`, `ClientManageDialogs.tsx`, `NewProjectDialog.tsx`, and a shared `ClientSelect` (the client+sub-client+create-inline combo duplicated between edit and new-project dialogs with parallel state pairs). Pure UI extraction.
|
||||||
|
|
||||||
|
### QUAL-10 — Ownership-check dependency · Medium (mechanical)
|
||||||
|
- **File:** `api/app/api/routes/scouts.py:344,372,391,407,702` (pattern ×6)
|
||||||
|
- **Fix:** `async def get_owned_cloud_scout(scout_id: UUID, current_user=Depends(get_current_user), db=Depends(get_session)) -> CloudScoutConfig` raising 404; use as route param. Same pattern available for other resource routes.
|
||||||
|
|
||||||
|
### QUAL-11 — Standardize tRPC error contract ⚠️ REVIEW · Medium
|
||||||
|
- **File:** `adiuvAI/src/main/router/index.ts` — three styles: `{error}` (L109,113), `{success:true}`, `{data,error}` envelopes (1157-1232), zero `TRPCError`.
|
||||||
|
- **Fix:** Standardize on throwing `TRPCError` (ipcLink propagates, `ipcLink.ts:76`); migrate procedure-by-procedure WITH their renderer consumers (each `useQuery/useMutation` error handling). Coordinates with CORR-04/CORR-19. Do during QUAL-01 split.
|
||||||
|
- **Risk:** Every consumer touched — easy to miss one. Grep all `\.error` accesses on tRPC results.
|
||||||
|
|
||||||
|
### QUAL-12 — Typed WS frames server-side · Medium
|
||||||
|
- **File:** `api/app/api/routes/device_ws.py:424,620,704,767-820` (raw `json.dumps` dicts) vs pydantic frames elsewhere; 7× `# type: ignore[union-attr]` (L293-294,382-383,491,567-568)
|
||||||
|
- **Fix:** Define pydantic models for index/journey reply frames in `app/schemas`; use a discriminated union (`Field(discriminator="type")`) for inbound frames, `model_validate` at dispatch, error frame on ValidationError. Kills the type:ignores and the hand-rolled snake/camel fallbacks (L673-676,746-750).
|
||||||
|
|
||||||
|
### QUAL-13 — Casing contract at WS boundary · Medium
|
||||||
|
- **Evidence:** `api/app/agents/note_agent.py:25` reads `row.get("aiSummary") or row.get("ai_summary")` — convention untrusted.
|
||||||
|
- **Fix:** Document the contract ("frames snake_case, tool-result rows camelCase") in `api/app/core/ws_context.py` docstring + `adiuvAI/src/shared/casing.ts`; delete the dual-read hedges; add a TypedDict for rows (TYPE-07).
|
||||||
|
|
||||||
|
### QUAL-14 — OAuth blocks → own files; shared TTLStateStore · Medium
|
||||||
|
- **Fix:** `app/api/routes/oauth.py` ← auth.py L45-67+299-505 (incl. extracting `link_or_create_oauth_user()` from the 124-line `oauth_callback`); `app/api/routes/scout_gmail_oauth.py` ← scouts.py L456-807. One `app/auth/state_store.py` `TTLStateStore` replacing both `_pending_states` dicts — the seam where SEC-18's Redis backend plugs in. Sequence: build TTLStateStore during SEC-18, move files in Phase 6.
|
||||||
|
|
||||||
|
### QUAL-15 — Agent helpers dedupe · Low (mechanical)
|
||||||
|
- **Fix:** `api/app/agents/_common.py` with `is_uuid()` (copies at task_agent.py:14-20, note_agent.py:15-22) and `format_rows(rows, line_fn, empty_msg)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Type Safety
|
||||||
|
|
||||||
|
### TYPE-01 — Broken relative imports, types silently `any` · High (mechanical)
|
||||||
|
- **Files:** `adiuvAI/src/renderer/components/ai/blocks/ChatEntityBlock.tsx:13`, `ChatChartBlock.tsx:27`, `ChatTimelineBlock.tsx:5`
|
||||||
|
- **Problem:** `'../../../../../shared/api-types'` (5 levels = repo root) — one `../` too many. `import type` so esbuild strips it without resolving; tsc confirms TS2307. Block data types are `any` throughout.
|
||||||
|
- **Fix:** Remove one `../` in each of the three imports (→ `'../../../../shared/api-types'`).
|
||||||
|
|
||||||
|
### TYPE-02 — No Python type checker · High ⚠️ REVIEW (config choices)
|
||||||
|
- **Fix:** Add `api/pyproject.toml` with `[tool.mypy]`: start `check_untyped_defs = true`, `warn_return_any = true`; per-module strict for `app/core`, `app/api`. Wire into CI beside `ruff check`. First run will surface real bugs — budget a cleanup pass.
|
||||||
|
|
||||||
|
### TYPE-03 — tsc doesn't pass; no CI typecheck · Medium
|
||||||
|
- **Evidence:** pre-existing errors at `backend-client.ts:963`, `drizzle-executor.ts:367`, `OnboardingFlow.tsx:157/165/291` (+ TYPE-01 cascade).
|
||||||
|
- **Fix:** After TYPE-01, fix the remaining tsc errors (inspect each — may be real bugs), then add `"typecheck": "tsc --noEmit"` script + CI step.
|
||||||
|
|
||||||
|
### TYPE-04 — RequestContext model for deep_agent · Medium
|
||||||
|
- **File:** `api/app/core/deep_agent.py` (19× `dict[str, Any]`; `context` threaded through L43-592+)
|
||||||
|
- **Fix:** `RequestContext` TypedDict/pydantic model in `app/schemas/` (trace_id, session_id, format_prefs, proactive hints, relational memory, …); `_trace_id_from_context`/`_session_id_from_context` (L574-590) become attribute access and get deleted. Do with QUAL-05.
|
||||||
|
|
||||||
|
### TYPE-05 — drizzle-executor cast cluster · Medium (mechanical)
|
||||||
|
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts:73,159,262,317,335` (table-as-Record ×5), `:230,249` (`Boolean as unknown as (x) => x is SQL` — dead weight, conditions already `SQL[]`)
|
||||||
|
- **Fix:** One helper using drizzle's `getTableColumns()`: `getColumn(table, name): SQLiteColumn | undefined`; replace the record casts. Delete the Boolean predicate casts (`and(...conditions)` suffices).
|
||||||
|
|
||||||
|
### TYPE-06 — window.electronAI escapes · Low (mechanical)
|
||||||
|
- **Files:** `ContextualChatContext.tsx:148-150`, `CloudScoutConfigPanel.tsx:39`, `CloudScoutCreationFlow.tsx:47`, `httpLink.ts:17`
|
||||||
|
- **Fix:** `src/renderer/types/electron-api.d.ts` with `declare global { interface Window { electronAI?: ElectronAI; electronTRPC?: …; electronDialog?: … } }`; add `"types": ["vite/client"]` to tsconfig (fixes `(import.meta as any).env`).
|
||||||
|
|
||||||
|
### TYPE-07 — Boundary typing misc · Low
|
||||||
|
- `ToolResult` TypedDict in `api/app/core/ws_context.py` (`rows`, `row`, `error`) — types the most-trafficked boundary; pairs with QUAL-13.
|
||||||
|
- 11 missing return annotations: `api/app/api/routes/scouts.py` cloud CRUD handlers (302, 313, 337, 366, 758), `device_ws.get_session_buffer:321` — add once TYPE-02's checker exists.
|
||||||
|
- `Provider` Protocol in `app/integrations/__init__.py` for `fetch_messages`/`fetch_emails` (kills 2 type:ignores in scout_runner.py:851+).
|
||||||
|
- `adiuvAI/src/main/ipc.ts:55` `(response: any)` → type the tRPC response envelope; `SerializedTRPCResponse` in `src/shared/` closes the ipcLink seam (`ipcLink.ts:66,76`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for the Executor
|
||||||
|
|
||||||
|
1. **Line numbers reference `main` @ `315c5d0`.** Re-verify each quoted snippet exists before editing; if it moved, find it by content, not line.
|
||||||
|
2. **Never execute ⚠️ REVIEW items unattended.** Security fixes especially: a sanitizer with a bypass or a broken auth check is worse than the original bug.
|
||||||
|
3. **After each Phase, run:** `cd api && ruff check . && pytest` · `cd adiuvAI && npm run lint && npx tsc --noEmit && npm run knip` (tsc becomes meaningful after TYPE-01/TYPE-03). There is no Electron test suite — manual smoke for renderer-affecting changes.
|
||||||
|
4. **Two-repo changes** (SEC-13, PERF-17, CORR-12, QUAL-13) must land backward-compatibly: backend accepts both old and new shapes for one release.
|
||||||
|
5. **Run `graphify update .` after each modifying session** (project rule).
|
||||||
|
6. Items needing **owner decisions before any execution:** SEC-16, SEC-22, SEC-28, DEAD-04, DEAD-07, CORR-08 (approach choice), DEPS-04 (tool choice).
|
||||||
203
docs/REFACTOR_PROGRESS.md
Normal file
203
docs/REFACTOR_PROGRESS.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Refactor Execution Progress
|
||||||
|
|
||||||
|
Companion to [REFACTOR_PLAN.md](REFACTOR_PLAN.md). The plan is **read-only**: never edit it. All progress, deviations, and lessons go HERE.
|
||||||
|
|
||||||
|
## Rules for the executing model
|
||||||
|
|
||||||
|
1. Before starting an item: set its Status to `in-progress`.
|
||||||
|
2. After finishing: set Status (`done` / `blocked` / `needs-review` / `skipped`), fill Commit with the short SHA, add Notes if anything was non-obvious.
|
||||||
|
3. **⚠️ items:** implement on the branch, set Status to `needs-review`, do NOT merge. A human or stronger model reviews the diff.
|
||||||
|
4. If the code does not match the plan's description (moved, already fixed, different shape): set `blocked`, write what you found in Notes, move to the next item. Do not improvise.
|
||||||
|
5. If you learn something that affects later items (e.g. "FK cascade route chosen in CORR-05, so DEAD-11's helper is gone"), add it to **Lessons Learned** at the bottom — next session reads it before starting.
|
||||||
|
6. One commit per item: `<ID>: <one-line description>`.
|
||||||
|
7. End of session: append a row to **Session Log**.
|
||||||
|
|
||||||
|
**Status values:** `pending` · `in-progress` · `done` · `needs-review` (⚠️ implemented, awaiting human) · `blocked` (mismatch/failed, see Notes) · `skipped` (owner decision) · `n/a`
|
||||||
|
|
||||||
|
## Owner decisions required (blocks the marked items)
|
||||||
|
|
||||||
|
| Decision | Blocks | Roberto's answer |
|
||||||
|
|---|---|---|
|
||||||
|
| Encrypt relational labels vs document + KEK only | SEC-16 | _pending_ |
|
||||||
|
| Web SPA token: httpOnly cookie vs in-memory vs defer | SEC-22 | _pending_ |
|
||||||
|
| Local at-rest encryption: SQLCipher / OS FDE / backup-key | SEC-28 | _pending_ |
|
||||||
|
| Delete chat.py HTTP routes or keep as public API | DEAD-04 | _pending_ |
|
||||||
|
| Wire backup-key.ts or delete | DEAD-07 | _pending_ |
|
||||||
|
| Decay fix: pure-function vs last_decayed_at column | CORR-08 | _pending_ |
|
||||||
|
| Python lock tool: uv vs pip-compile | DEPS-04 | _pending_ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Mechanical quick wins
|
||||||
|
Branch: `refactor/phase-0` · Verify after each: `cd api && ruff check . && pytest` / `cd adiuvAI && npm run lint && npx tsc --noEmit`
|
||||||
|
|
||||||
|
| ID | ⚠️ | Status | Commit | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| TYPE-01 | | pending | | |
|
||||||
|
| DEPS-01 | | pending | | |
|
||||||
|
| DEAD-10 | | pending | | |
|
||||||
|
| DEAD-01 | | pending | | |
|
||||||
|
| DEAD-02 | | pending | | |
|
||||||
|
| DEAD-05 | | pending | | |
|
||||||
|
| DEAD-08 | | pending | | |
|
||||||
|
| DEPS-08 | | pending | | |
|
||||||
|
| DEAD-11 | | pending | | |
|
||||||
|
| DEAD-12 | | pending | | |
|
||||||
|
| PERF-14 | | pending | | |
|
||||||
|
| PERF-18 | | pending | | |
|
||||||
|
| CORR-20 | | pending | | |
|
||||||
|
|
||||||
|
## Phase 1 — Critical & High security (ALL ⚠️ — implement, never merge unreviewed)
|
||||||
|
Branch: `refactor/phase-1-security`
|
||||||
|
|
||||||
|
| ID | ⚠️ | Status | Commit | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| SEC-01 | ⚠️ | pending | | |
|
||||||
|
| SEC-05 | ⚠️ | pending | | |
|
||||||
|
| SEC-06 | ⚠️ | pending | | |
|
||||||
|
| SEC-07 | ⚠️ | pending | | |
|
||||||
|
| SEC-20 | ⚠️ | pending | | |
|
||||||
|
| SEC-02 | ⚠️ | pending | | |
|
||||||
|
| SEC-14 | ⚠️ | pending | | do before SEC-19 (shared startup guard) |
|
||||||
|
| SEC-19 | ⚠️ | pending | | |
|
||||||
|
| SEC-09 | ⚠️ | pending | | |
|
||||||
|
| SEC-08 | ⚠️ | pending | | deploy together with env value |
|
||||||
|
| SEC-04 | ⚠️ | pending | | |
|
||||||
|
| SEC-15 | ⚠️ | pending | | |
|
||||||
|
| SEC-03 | ⚠️ | pending | | migration design — human-led |
|
||||||
|
|
||||||
|
## Phase 2 — Critical & High correctness
|
||||||
|
Branch: `refactor/phase-2-correctness`
|
||||||
|
|
||||||
|
| ID | ⚠️ | Status | Commit | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| CORR-01 | ⚠️ | pending | | with CORR-02, same files |
|
||||||
|
| CORR-02 | ⚠️ | pending | | |
|
||||||
|
| CORR-12 | | pending | | before CORR-04 (frame contract) |
|
||||||
|
| CORR-04 | | pending | | |
|
||||||
|
| CORR-06 | | pending | | |
|
||||||
|
| CORR-05 | ⚠️ | pending | | deletes data — human review |
|
||||||
|
| CORR-03 | | pending | | |
|
||||||
|
|
||||||
|
## Phase 3 — Medium security + remaining correctness
|
||||||
|
Branch: `refactor/phase-3`
|
||||||
|
|
||||||
|
| ID | ⚠️ | Status | Commit | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| SEC-10 | ⚠️ | pending | | |
|
||||||
|
| SEC-11 | ⚠️ | pending | | mechanical |
|
||||||
|
| SEC-12 | ⚠️ | pending | | two-repo |
|
||||||
|
| SEC-13 | ⚠️ | pending | | two-repo, keep fallback one release |
|
||||||
|
| SEC-17 | ⚠️ | pending | | Redis intro — with SEC-18 |
|
||||||
|
| SEC-18 | ⚠️ | pending | | |
|
||||||
|
| SEC-27 | ⚠️ | pending | | |
|
||||||
|
| SEC-21 | ⚠️ | pending | | |
|
||||||
|
| SEC-23 | ⚠️ | pending | | mechanical |
|
||||||
|
| SEC-24 | ⚠️ | pending | | |
|
||||||
|
| SEC-25 | ⚠️ | pending | | needs SEC-08 first |
|
||||||
|
| SEC-26 | ⚠️ | pending | | mechanical |
|
||||||
|
| SEC-16 | ⚠️ | skipped | | awaiting owner decision |
|
||||||
|
| SEC-22 | ⚠️ | skipped | | awaiting owner decision |
|
||||||
|
| SEC-28 | ⚠️ | skipped | | awaiting owner decision |
|
||||||
|
| SEC-29 | ⚠️ | pending | | no code change — verify + close |
|
||||||
|
| SEC-30 | ⚠️ | pending | | mechanical |
|
||||||
|
| SEC-31 | ⚠️ | pending | | |
|
||||||
|
| SEC-32 | ⚠️ | pending | | |
|
||||||
|
| SEC-33 | | pending | | |
|
||||||
|
| SEC-34 | ⚠️ | pending | | infra, not repo code |
|
||||||
|
| SEC-35 | ⚠️ | pending | | mechanical |
|
||||||
|
| SEC-36 | ⚠️ | pending | | full packaged-app regression |
|
||||||
|
| CORR-07 | | pending | | |
|
||||||
|
| CORR-08 | | skipped | | awaiting owner decision |
|
||||||
|
| CORR-09 | ⚠️ | pending | | migration + dedupe |
|
||||||
|
| CORR-10 | ⚠️ | pending | | cap value judgment |
|
||||||
|
| CORR-11 | | pending | | |
|
||||||
|
| CORR-13 | ⚠️ | pending | | classify call sites |
|
||||||
|
| CORR-14 | | pending | | |
|
||||||
|
| CORR-15 | | pending | | |
|
||||||
|
| CORR-16 | ⚠️ | pending | | irreversible path — review |
|
||||||
|
| CORR-17 | | pending | | |
|
||||||
|
| CORR-18 | | pending | | |
|
||||||
|
| CORR-19 | | pending | | |
|
||||||
|
| CORR-21 | | pending | | |
|
||||||
|
|
||||||
|
## Phase 4 — Performance
|
||||||
|
Branch: `refactor/phase-4-perf`
|
||||||
|
|
||||||
|
| ID | ⚠️ | Status | Commit | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| PERF-01 | | pending | | |
|
||||||
|
| PERF-02 | ⚠️ | pending | | strong-model territory |
|
||||||
|
| PERF-03 | | pending | | LIMIT part urgent + mechanical |
|
||||||
|
| PERF-04 | | pending | | test migration on populated dev DB |
|
||||||
|
| PERF-05 | | pending | | coordinate with CORR-09 |
|
||||||
|
| PERF-09 | | pending | | |
|
||||||
|
| PERF-10 | | pending | | |
|
||||||
|
| PERF-06 | | pending | | |
|
||||||
|
| PERF-07 | | pending | | or fold into QUAL-03 later |
|
||||||
|
| PERF-08 | | pending | | |
|
||||||
|
| PERF-11 | | pending | | |
|
||||||
|
| PERF-12 | | pending | | |
|
||||||
|
| PERF-13 | | pending | | |
|
||||||
|
| PERF-15 | | pending | | |
|
||||||
|
| PERF-16 | | pending | | |
|
||||||
|
| PERF-17 | | pending | | two-repo |
|
||||||
|
| PERF-19 | | pending | | defer OK |
|
||||||
|
| PERF-20 | | pending | | superseded if SEC-17 Redis lands |
|
||||||
|
| PERF-21 | | pending | | |
|
||||||
|
| PERF-22 | | pending | | |
|
||||||
|
|
||||||
|
## Phase 5 — Dependencies & tooling
|
||||||
|
Branch: `refactor/phase-5-deps`
|
||||||
|
|
||||||
|
| ID | ⚠️ | Status | Commit | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| DEPS-02 | | pending | | |
|
||||||
|
| DEPS-03 | | pending | | |
|
||||||
|
| DEPS-04 | | skipped | | awaiting owner decision (tool) |
|
||||||
|
| TYPE-02 | ⚠️ | pending | | config choices |
|
||||||
|
| TYPE-03 | | pending | | after TYPE-01 |
|
||||||
|
| DEPS-05 | ⚠️ | pending | | dedicated PR |
|
||||||
|
| DEPS-06 | ⚠️ | pending | | dedicated PR, native rebuild |
|
||||||
|
| DEPS-07 | | n/a | | monitor only |
|
||||||
|
|
||||||
|
## Phase 6 — Structural refactors (LAST — invalidates plan line numbers)
|
||||||
|
Branch: `refactor/phase-6-quality`
|
||||||
|
|
||||||
|
| ID | ⚠️ | Status | Commit | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| QUAL-02 | ⚠️ | pending | | before QUAL-03 |
|
||||||
|
| QUAL-03 | ⚠️ | pending | | |
|
||||||
|
| QUAL-01 | | pending | | with QUAL-11 |
|
||||||
|
| QUAL-04 | ⚠️ | pending | | prototype on clients first |
|
||||||
|
| QUAL-05 | | pending | | |
|
||||||
|
| QUAL-06 | ⚠️ | pending | | after CORR-07 |
|
||||||
|
| QUAL-07 | ⚠️ | pending | | characterization tests first |
|
||||||
|
| QUAL-08 | | pending | | with QUAL-12 |
|
||||||
|
| QUAL-09 | | pending | | |
|
||||||
|
| QUAL-10 | | pending | | |
|
||||||
|
| QUAL-11 | ⚠️ | pending | | |
|
||||||
|
| QUAL-12 | | pending | | |
|
||||||
|
| QUAL-13 | | pending | | |
|
||||||
|
| QUAL-14 | | pending | | store built in SEC-18; file moves here |
|
||||||
|
| QUAL-15 | | pending | | |
|
||||||
|
| TYPE-04 | | pending | | with QUAL-05 |
|
||||||
|
| TYPE-05 | | pending | | |
|
||||||
|
| TYPE-06 | | pending | | |
|
||||||
|
| TYPE-07 | | pending | | |
|
||||||
|
| DEAD-04 | ⚠️ | skipped | | awaiting owner decision |
|
||||||
|
| DEAD-07 | ⚠️ | skipped | | awaiting owner decision |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
_Append findings that affect later items. Format: `- [ID] lesson`_
|
||||||
|
|
||||||
|
(none yet)
|
||||||
|
|
||||||
|
## Session Log
|
||||||
|
|
||||||
|
| Date | Model | Phase | Items touched | Outcome |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| | | | | |
|
||||||
Reference in New Issue
Block a user