chore(app): add hardening plan for overnight TUI loop execution

12-phase plan covering security fixes, proxy hardening, state bugs,
content extraction, cache bounds, accessibility, and test coverage.
42 atomic tasks structured for sequential agent execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 00:07:43 +00:00
co-authored by Claude Opus 4.6
parent cb22131909
commit 91763246c3
+286
View File
@@ -0,0 +1,286 @@
# AIUI Debug, Fix & Hardening Plan
## Execution Rules
- Run on `development` branch
- Each `- [ ]` task is one agent iteration — complete fully before moving on
- After each phase: `pnpm typecheck && pnpm lint && pnpm test -- --run`
- If checks fail, fix before proceeding
- Commit at end of each phase with `type(scope): description` format
- Do NOT push — human will review and push
---
## PHASE 0: Baseline Verification
- [ ] **P0.1 — Baseline check**
- Run: `pnpm install && pnpm typecheck && pnpm lint && pnpm test -- --run`
- Record output. Note existing failures. Fix any blockers before proceeding.
- Commit: `chore(app): verify baseline before hardening`
---
## PHASE 1: Critical Security Fixes
- [ ] **P1.1 — Fix CORS wildcard in claude-proxy.ts**
- File: `packages/app/server/claude-proxy.ts`
- Replace all `'Access-Control-Allow-Origin': '*'` (lines ~415, 473, 531) with the correct localhost origin or import `ALLOWED_ORIGIN` from `dev-auth.ts`. Match the pattern already used on lines 294/303.
- Verify: `grep -n "Allow-Origin.*\*" packages/app/server/claude-proxy.ts` returns zero matches.
- [ ] **P1.2 — Fix dev auth token bypass**
- File: `packages/app/server/dev-auth.ts`
- Line 11: `if (!token) return true` skips auth entirely when token empty.
- Fix: Only skip in non-production. `if (!token) { if (process.env.NODE_ENV === 'production') { res.writeHead(401); res.end('Unauthorized'); return false; } return true; }`
- Add console.warn when auth disabled.
- [ ] **P1.3 — Symlink traversal protection in vite-fs.ts**
- File: `packages/app/vite-fs.ts`
- In `walk` function: after constructing `fullPath`, add `if (lstatSync(fullPath).isSymbolicLink()) continue;`
- In `handleRead`: before `statSync`, check `lstatSync(filePath).isSymbolicLink()` → return 403.
- Import `lstatSync` from `fs`.
- [ ] **P1.4 — Body size limit on vite-fs.ts mkdir**
- File: `packages/app/vite-fs.ts`
- `handleMkdir` reads body with no size cap (lines 185-209).
- Add `const MAX_BODY_SIZE = 1024`. Track `let size = 0` on `data` events. Return 413 if exceeded.
- [ ] **P1.5 — JSON schema validation in vite-dev-chats.ts**
- File: `packages/app/vite-dev-chats.ts`
- After `JSON.parse(body)` on line 66, validate shape: must be object with optional `conversations` (object) and `activeConversationId` (string|null). Reject with 400 if invalid.
- [ ] **P1.6 — CSP headers in nginx-archy.conf**
- File: `packages/app/server/nginx-archy.conf`
- Add inside `/aiui/` location block:
```
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self' https://api.anthropic.com https://openrouter.ai https://wavlake.com https://itunes.apple.com https://openlibrary.org https://covers.openlibrary.org https://en.wikipedia.org https://www.googleapis.com https://image.tmdb.org; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
```
- [ ] **P1.7 — OpenRouter validation + streaming timeout**
- File: `packages/app/server/claude-proxy.ts`
- OpenRouter handler: parse `reqBody`, validate has `model` (string), `messages` (array), `stream` (boolean). Return 400 if invalid.
- Both streaming reader loops: add idle timeout (120s). `let idleTimer = setTimeout(() => reader.cancel(), 120000)`. Reset on each chunk. Clear on completion.
- Verify: `pnpm typecheck`
- **Commit**: `fix(app): critical security — CORS, auth, symlink, CSP, body validation`
---
## PHASE 2: Proxy & Server Hardening
- [ ] **P2.1 — Fix tool use loop in claude-proxy.ts**
- File: `packages/app/server/claude-proxy.ts` (lines 164-222)
- Unknown tool names: push error tool_result `{ type: 'tool_result', tool_use_id: tu.id, content: 'Error: unknown tool', is_error: true }`.
- Add turnMessages size guard: `if (JSON.stringify(turnMessages).length > 500_000)` break loop.
- Add `AbortSignal.timeout(30000)` on each API call within loop.
- [ ] **P2.2 — SSRF mitigation in vite-rss.ts**
- File: `packages/app/vite-rss.ts`
- After `tryParseFeed` returns, validate returned article URLs with `isPrivateUrl`. Filter out any private URLs.
- Add comment documenting residual TOCTOU risk (dev-only middleware).
- [ ] **P2.3 — Sanitize web search context in useAI.ts**
- File: `packages/app/src/composables/useAI.ts` (lines 399-408)
- Create helper: `function sanitizeSearchText(s: string): string { return s.replace(/[[\]()#*_~` >]/g, '\\$&').replace(/\n/g, ' ').slice(0, 500) }`
- Apply to `r.title` and `r.content` in `formatWebSearchContext`.
- **Commit**: `fix(app): proxy hardening — tool loop, SSRF, prompt injection`
---
## PHASE 3: State & Logic Bugs
- [ ] **P3.1 — Fix contentType assignment in useContentPanel.ts**
- File: `packages/app/src/composables/useContentPanel.ts` (lines 219-225)
- The `contentType` ref type is `'film' | 'song' | 'podcast'`. Expand to include all content types or use `ContentTab`. Fix assignments: books→'film' is acceptable if type can't expand, but news/TV should map correctly. If the type is only used for player logic, keep narrow type but fix assignments to be semantically correct.
- [ ] **P3.2 — Fix apiFetching race in useBannerFallback.ts**
- File: `packages/app/src/composables/useBannerFallback.ts`
- Change `let apiFetching = false` to `const apiFetching = ref(false)`. Update all references to `.value`.
- [ ] **P3.3 — Fix event listener cleanup in chat.ts**
- File: `packages/app/src/stores/chat.ts` (lines 111-118)
- Extract handlers to named functions. Add `import.meta.hot?.dispose()` cleanup for HMR.
- [ ] **P3.4 — Fix news RSS race condition in useContentPanel.ts**
- File: `packages/app/src/composables/useContentPanel.ts` (lines 134-154)
- Capture `const tabAtStart = activeTab.value` before RSS fetch. In `.then()`, only set `activeTab.value = 'news'` if `activeTab.value === tabAtStart` (user hasn't manually switched).
- [ ] **P3.5 — Add error logging to silent catch blocks**
- Files: `useImageFallback.ts`, `useAI.ts`, `stores/chat.ts`
- Replace all `catch { }` and `catch { /* ignore */ }` with `catch(e) { console.debug('[module] op failed:', e) }`.
- Keep `console.debug` (not `error`) for expected failures.
- **Commit**: `fix(app): state bugs — contentType, race conditions, error logging`
---
## PHASE 4: Content Extraction & Filtering
- [ ] **P4.1 — Add recipe instructions to system prompt**
- File: `packages/app/src/composables/useAI.ts` (SYSTEM_PROMPT)
- Add after Apps section:
```
**Recipes:** When sharing recipes, use the <recipe_ext> XML tag:
<recipe_ext title="Name" servings="4" time="30 min" calories="450">
- ingredient 1
1. Step one
</recipe_ext>
```
- [ ] **P4.2 — Fix app category validation**
- File: `packages/app/src/composables/contentExtraction.ts` (line ~1330)
- Add `const VALID_CATEGORIES = new Set(['nostr-client','lightning-wallet','bitcoin-wallet','privacy','node','dev-tool','relay'])`. Validate before cast, fallback to `'dev-tool'`.
- [ ] **P4.3 — Fix song deduplication**
- File: `packages/app/src/composables/contentExtraction.ts` (lines 606-638)
- After combining library + external songs, deduplicate by normalized `title|artist` key. Library songs (with real IDs) take priority.
- [ ] **P4.4 — Fix film/TV tag ambiguity**
- File: `packages/app/src/composables/contentExtraction.ts` (lines 914-916)
- Only convert `film_ext` to TV when: query is TV-like AND no explicit `tv_ext` tags present AND response reads TV-like. If AI used both `film_ext` and `tv_ext`, keep both as-is.
- [ ] **P4.5 — Fix isBookLikeResponse false positives**
- File: `packages/app/src/composables/contentFiltering.ts` (lines 60-62)
- Increase `by [Author]` threshold from `>= 1` to `>= 2`.
- [ ] **P4.6 — Fix isRecipeLikeResponse for prose recipes**
- File: `packages/app/src/composables/contentFiltering.ts` (lines 92-94)
- Add prose detection: recipe keywords + list structure pattern.
- [ ] **P4.7 — Fix looksLikeSong false positives**
- File: `packages/app/src/composables/contentExtraction.ts` (lines 488-506)
- Add financial terms to blocklist: `'market cap','etf','price','trading','volume','earnings','valuation','stock','portfolio','investment','yield','inflation','interest rate'`.
- **Commit**: `fix(app): extraction — dedup, tag ambiguity, false positives, recipes`
---
## PHASE 5: Memory & Cache Hardening
- [ ] **P5.1 — Bound caches in useImageFallback.ts**
- File: `packages/app/src/composables/useImageFallback.ts`
- Create helper: `function boundedSet<K,V>(map: Map<K,V>, key: K, val: V, max=500) { if (map.size >= max) { const first = map.keys().next().value; if (first !== undefined) map.delete(first); } map.set(key, val); }`
- Replace all `.set()` calls on memory caches with `boundedSet()`.
- Also bound `failedUrls` Set to 1000 entries.
- [ ] **P5.2 — Bound caches in usePlayer.ts + cleanup**
- File: `packages/app/src/composables/usePlayer.ts`
- Bound `resultCache` and `nullCacheTimestamps` to 200 entries.
- Add `destroy()` method that calls `destroyPlayer()`, resets DOM refs, cancels active search controller.
- **Commit**: `fix(app): bound caches, player cleanup`
---
## PHASE 6: Accessibility
- [ ] **P6.1 — Fix touch targets and aria attributes**
- Audit all `*Grid.vue` and `*Detail.vue` for: interactive elements < 44px, images without alt, SVG fallbacks without aria-label.
- ImageGrid.vue: add `role="img"` `aria-label="Image unavailable"` to fallback SVG wrapper.
- Ensure all interactive genre/tag pills in grids have adequate touch targets (min 44x44 including padding).
- Non-interactive info pills are exempt.
- **Commit**: `fix(app): accessibility — touch targets, alt text, aria`
---
## PHASE 7: Security Tests
- [ ] **P7.1 — Write dev-auth tests**
- New file: `packages/app/src/__tests__/dev-auth.test.ts`
- Tests: auth bypass when no token (dev), auth required when token set, rate limiting 429, CORS headers correct.
- [ ] **P7.2 — Extend proxy tests**
- File: `packages/app/src/__tests__/proxy.test.ts`
- Tests: unknown tool error result, max rounds termination, turnMessages size guard, streaming timeout.
- [ ] **P7.3 — Write vite-fs security tests**
- New file: `packages/app/src/__tests__/vite-fs.test.ts`
- Tests: path traversal rejected, symlink blocked, sensitive files blocked, body size limit enforced.
- [ ] **P7.4 — Write SSRF tests for vite-rss**
- New file: `packages/app/src/__tests__/vite-rss.test.ts`
- Tests: private IP detection, localhost blocked, non-http blocked, private URLs filtered from results.
- **Commit**: `test(app): security tests — auth, proxy, fs, SSRF`
---
## PHASE 8: Content Extraction Tests
- [ ] **P8.1 — Write content filtering tests**
- New file: `packages/app/src/composables/__tests__/contentFiltering.test.ts`
- Tests: isBookLikeResponse false positive fix, isRecipeLikeResponse prose detection, isMusicQuery rejects financial terms, filterTabsByContext ordering.
- [ ] **P8.2 — Extend content extraction tests**
- File: `packages/app/src/__tests__/contentExtraction.test.ts`
- Tests: song dedup, looksLikeSong rejects financial terms, app category fallback, recipe parsing.
- [ ] **P8.3 — Write TV/film resolution tests**
- Same file as P8.2.
- Tests: film_ext→TV conversion rules, book_ext in news context, explicit tags respected.
- **Commit**: `test(app): content extraction — filtering, dedup, tag resolution`
---
## PHASE 9: State & Composable Tests
- [ ] **P9.1 — Write useContentPanel tests**
- File: `packages/app/src/composables/__tests__/useContentPanel.test.ts`
- Tests: contentType assignment per content type, RSS tab override prevention, closePanel resets, empty text → no tabs.
- [ ] **P9.2 — Write useBannerFallback tests**
- New file: `packages/app/src/composables/__tests__/useBannerFallback.test.ts`
- Tests: primary URL fallthrough, API fetch on exhaustion, apiFetching guard, gradient fallback.
- [ ] **P9.3 — Write chat store tests**
- New file: `packages/app/src/__tests__/chat-store.test.ts`
- Tests: createConversation, addMessage, deleteConversation, branchFromMessage, debouncedIDBSave.
- **Commit**: `test(app): state tests — contentPanel, bannerFallback, chat store`
---
## PHASE 10: AI Integration Tests
- [ ] **P10.1 — Extend useAI tests**
- File: `packages/app/src/__tests__/useAI.test.ts`
- Tests: system prompt includes recipe instructions, web search results sanitized, editAndResend truncates, sanitizeHistory merges consecutive roles.
- [ ] **P10.2 — Write useImageFallback cache tests**
- New file: `packages/app/src/composables/__tests__/useImageFallback.test.ts`
- Tests: boundedSet evicts at max, generatePosterFallback valid SVG, escapeXml handles specials.
- **Commit**: `test(app): AI integration, image fallback cache tests`
---
## PHASE 11: Final Verification
- [ ] **P11.1 — Full integration check**
- Run: `pnpm typecheck && pnpm lint && pnpm test -- --run`
- Run: `pnpm build` — verify production build succeeds
- Fix any regressions.
- [ ] **P11.2 — Final commit**
- Run coverage report if configured: `pnpm test -- --run --coverage`
- Ensure all changes committed on `development`.
- **Commit**: `chore(app): hardening complete — all checks pass`
---
## Phase Dependencies
```
P0 → P1 → P2 → P3 → P4 → P5 ─┐
├→ P7 (tests P1,P2)
P6 ──┤
├→ P8 (tests P4)
├→ P9 (tests P3,P5)
├→ P10 (tests P2.3,P4.1)
└→ P11 (final)
```
P5 and P6 can run in parallel. P7-P10 can run in any order after their fix phases.
## Summary
- **12 phases**, **42 tasks**
- Phases 0-6: fixes (security → proxy → state → extraction → caches → a11y)
- Phases 7-10: tests (security → extraction → state → AI)
- Phase 11: final verification
- Target: all checks pass, 40%+ test coverage on critical paths, secure for Archy deployment