diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/ASSESSMENT-FIX-PLAN-2026-08-07.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/ASSESSMENT-FIX-PLAN-2026-08-07.md new file mode 100644 index 00000000..37d075b9 --- /dev/null +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/ASSESSMENT-FIX-PLAN-2026-08-07.md @@ -0,0 +1,208 @@ +# AIUI Assessment & Fix Plan — 2026-08-07 + +Full-stack audit of the AIUI work (browser bundle, bridge, host broker, Rust assistant, +nginx boundary) against three questions the operator asked: + +1. **Security** — can it leak sensitive node info to the AI model or outside parties? +2. **Mission alignment** — nostr-first, Blossom, hashtree, FIPS, self-hosted AI? +3. **Seed-vs-reality** — does the `/seed` promise survive contact with a real node? + +Each finding below was verified against the working tree (many independently re-verified +during the audit write-up). File refs are `aiui/` = `aiui/packages/app/src`, +`core/` = `core/archipelago/src`, `ui/` = `neode-ui/src`. + +--- + +## A. Security verdict + +**The embedded chat path is genuinely well-engineered.** Grants are node-side, +default-closed, 0600 (`core/assistant/grants.rs`). The tool registry is a hand-written +allowlist with an excluded-authority test constant (`core/assistant/tools.rs:138-149`). +`execute_tool` re-checks grants server-side per call (`core/assistant/loop_.rs:213-225`). +The confirm gate is nonce-bound, node-authored, rendered in trusted chrome, and the iframe +provably cannot open or resolve it (`core/assistant/confirm.rs`, `ui/.../contextBroker.ts:219-225`). +Cloud egress is secret-screened on both cloud legs (BIP39 checksum-validated, secrets-dir +corpus, fail-closed, 64KB cap — `core/assistant/egress.rs`). The `/aiui/` CSP pins the +frame's network to its own prefix (verified live in a browser, 13-09-SUMMARY). + +**The real exposure lives at the edges — six fixable items:** + +| # | Severity | Finding | Evidence | +|---|----------|---------|----------| +| S1 | HIGH | `network_status` tool sends **WAN IP + Wi-Fi SSID** to cloud models while the operator toggle promises "no IP addresses" — consent against a false label | `core/assistant/tools.rs:357-369` → `network.diagnostics` → `core/api/rpc/router.rs:97,103`; label at `ui/stores/aiPermissions.ts:33-35` | +| S2 | HIGH | Claude API key **plaintext in localStorage** (`aiui-settings`), bypassing the encrypted key-vault that exists for exactly this | `aiui/stores/settings.ts:28,51,60,66-68`; `aiui/composables/useAI.ts:277-278`; violates aiui/CLAUDE.md | +| S3 | HIGH | **Standalone mode reachable on a node** (just drop `?embedded=true`) → full history + base64 images + anything pasted go to Anthropic through the forwarder **unscreened** (screen only runs in the Rust backends); forwarder also has **no rate limit** | `aiui/main.ts:34`, `aiui/composables/useAI.ts:264-327`; `core/.../model_proxy.rs` forwards verbatim | +| S4 | MED | `/aiui/api/web-search` proxy is **unauthenticated** (no session check, straight to SearXNG :8888) — anyone on the LAN runs searches attributed to the node's IP; sibling `/app/searxng/` and `/app/ollama/` are likewise direct | `image-recipe/configs/nginx-archipelago.conf:130-139,580,812` vs session-gated `:97,:116` | +| S5 | MED | Node-side `app_logs` enters model context **unredacted** (browser path redacts `password=`, long hex/base64; node tool only untrusted-wraps); a 40-char token or `rpcpassword=<32-hex>` passes the egress screen | `core/assistant/tools.rs:695-705` vs `ui/.../contextBroker.ts:1068-1078`; threshold at `core/assistant/egress.rs:174-187` | +| S6 | MED | G-B2 turn-minimality **silently strips prior user turns** on cloud legs since 13-10 history replay — model sees its own answers without the questions; module doc now false | `core/assistant/egress.rs:74-88,309-311,353-356` vs `core/assistant/mod.rs:859-877` | + +Lower: legacy broker actions beyond D-03's documented set (`read-file`/`tail-logs`/ +`search-web`/dead `install-app`); filebrowser JWT in JS-readable cookie (URL leak is fixed); +`webSearchEnabled` defaults true in browser localStorage (operator decision: node-side, +default-off — unimplemented); nginx drift can silently remove the CSP the boundary +depends on (live Wikipedia traffic observed from dev3's embedded frame). + +## B. Mission-alignment verdict + +**Aligned in direction and custody model; weakest exactly where the mission is most +distinctive.** + +- **Nostr-first: STRONG where shipped.** NIP-07-only identity (no nsec ever in-browser), + NIP-04 DMs, NIP-05, NIP-44 share encryption, NIP-50 search, NIP-65 relay lists, relay + manager, kind:30023 conversation sharing. Stubs honestly labelled: NWC (no NIP-47 + signing), LNURL-auth (no signature check), zap QR (placeholder drawing), naddr + (non-standard base64, self-consistent only). +- **Node-side AI chain embodies the mission**: Ollama-first ("node data stays on-node this + turn"), Claude gated, **Routstr = nostr-discovered, cashu-paid, Tor-preferred, hard + prepaid budget with typed `BudgetExhausted` stop** (`core/assistant/backends/routstr.rs`, + 1163 lines). OpenRouter deliberately 404'd on-node. +- **But**: Routstr is a *terminal fallback* with an unverified protocol (0/9 live claims, + 13-ROUTSTR-FINDINGS) and **no funding UX anywhere** (5fc82894 captured a todo, not code). + Standalone AIUI defaults to Claude and ships a **dead, unselectable Ollama adapter**. +- **Blossom: ABSENT** (design-doc intent for IndeeHub catalog only). + **Hashtree: ABSENT** (zero footprint repo-wide). + **FIPS: correctly inherited** as peer-content transport via the broker — AIUI should + never speak it directly. +- Centralized deps that should have node answers: mempool.space price polling from the + browser (node has Bitcoin Core), TMDB/iTunes/OpenLibrary/Wikipedia/OSM enrichment + (dev-middleware only, 404 on node), YouTube-embed "free films". + +## C. Seed-vs-reality verdict + +**The seeds are a standalone-mode demo. On a real node, almost everything that makes them +rich is absent.** Two surface mechanisms exist and they fight each other: + +- **Text extraction** (the seed path): tags like `[[film_ext:…]]` are taught only in + AIUI's *standalone* SYSTEM_PROMPT (`aiui/composables/useAI.ts:80-119`) — the node's + prompt contains zero tag vocabulary (verified by grep). `` and `` + are taught in *neither* prompt — seed-only by construction. Extraction statically imports + the mock libraries (`contentExtraction.ts:4-6`) and presents them to the model as + "the user's library" — mock songs carry `open.spotify.com/track/example` dead links. +- **Tool surfaces** (the real path): works for `content_list` only. `apps_list` returns a + bare array so the broker's `s.data?.items` drops it silently; IndeeHub `films` scope + items classify as `'excluded'` (no mime/filename) so that surface can never render; + once Archy content activates it *blocks* seed extraction and wipes grids to + "Nothing found" on every no-tool turn. +- **Playback is broken for real content**: `usePlayer.play()` never checks + `song.sources[]` — a real library track goes to (CSP-blocked) Wavlake and reports + "Not found on Wavlake"; `FilmDetail` only plays YouTube sources. `media-src 'self'` + would allow the real URLs — the UI never uses them. +- **The auto-seeded "Exploring My Node" demo runs on first load with no dev guard** and + persists fabricated node state — including `rpcpassword=archipelago123` in a fenced + bitcoin.conf — into every fresh install's chat history (`aiui/stores/chat.ts:476-508`, + `nodeDemoPrompts.ts:116`; chunk verified in prod bundle). +- Enrichment is dead on node (dev middleware + CSP), so every poster/cover resolves to an + SVG placeholder; degenerate metadata renders as "★ 0 · 0m"; recipe/app cards click to + nowhere (details never mounted); seed fixture's own gold tests are RED (expects 10 + songs, extractor yields 6). + +**Real on a node today**: own shared media (filename-derived cards, no art) and the +indexed music library (real tags, no art, unplayable in-app) — both measured +empty/unreachable on the demo node (INDEEHUB-CONTENT-INVENTORY: 0 items; 16 peers serving +nothing). + +--- + +## D. Fix plan — four waves + +Ordering logic: stop the leaks first (they're live), then make real content actually +render/play (the visible product), then mission alignment (the differentiator), then the +already-proposed follow-on phases. + +### Wave 0 — Security blockers (gate any wider demo/rollout) + +| Item | Fix | Files | +|---|---|---| +| S1 | Strip `wan_ip`/`wifi_ssid` from the `network_status` tool result (or split a new, honestly-labelled category); make the toggle label true | `core/assistant/tools.rs`, `core/api/rpc/router.rs` | +| S2 | Route `claudeApiKey` through the encrypted key-vault only; migrate-and-wipe any plaintext in `aiui-settings` | `aiui/stores/settings.ts`, `aiui/composables/useAI.ts`, `aiui/utils/key-vault.ts` | +| S3 | When served from a node, standalone mode is off (node banner or hard gate); run `screen_outbound` in the model forwarder as defense-in-depth; add forwarder rate limit | `aiui/main.ts`, `core/.../model_proxy.rs`, reuse `core/assistant/egress.rs` | +| S4 | Session-gate `/aiui/api/web-search` through the daemon (:5678) like the model proxies; audit `/app/searxng/` + `/app/ollama/` auth posture | `image-recipe/configs/nginx-archipelago.conf`, `core/.../model_proxy.rs` | +| S5 | Port the broker's log redaction into the node `app_logs` tool before untrusted-wrap | `core/assistant/tools.rs`, mirror `ui/.../contextBroker.ts:1068-1078` | +| S6 | Decide and implement: either screen full replayed history or document + fix the stale module doc; stop silently dropping user turns | `core/assistant/egress.rs`, `core/assistant/mod.rs` | +| S7 | Kill the auto-seeded "Exploring My Node" demo in prod (`isDev` guard at minimum, delete the fabricated `rpcpassword` content entirely) | `aiui/stores/chat.ts:476-508`, `aiui/data/nodeDemoPrompts.ts` | + +### Wave 1 — Content truth (make the real path rich; stop the fake one) + +1. **One surface mechanism**: tool surfaces are the only path on-node; either teach the + node prompt the tag vocabulary (cheap, brittle) or — preferred — extend `SURFACE_TOOLS` + coverage and fix the two shape bugs: `apps_list` returns `{items:[…]}`; IndeeHub + `films` scope carries a mime/filename hint so classification doesn't exclude it. + (`core/assistant/tools.rs:55`, `core/api/rpc/container.rs:247`, + `core/api/rpc/content.rs:1211`, `ui/.../contextBroker.ts:300-320`, + `ui/.../archyContentAdapter.ts:231-242`) +2. **Playback**: `usePlayer.play()` uses `song.sources[]` first (same-origin, CSP-legal), + Wavlake only as fallback; `FilmDetail` plays node files via Range requests (the rules + already exist — media streams via Range, never base64). (`aiui/composables/usePlayer.ts:235-277`, + `aiui/components/content/FilmDetail.vue:134-136`) +3. **Stop the mechanisms fighting**: `archyContentActive` must not latch on an *empty* + granted bucket; no-tool turns must not wipe populated grids. + (`aiui/composables/useContentPanel.ts:210-215,311-355`, `useArchy.ts:365-382`) +4. **Mock quarantine**: mocks/fixtures out of the prod bundle (dynamic import behind + `isDev`); SYSTEM_PROMPT never presents mocks as the user's library; `/seed` stays as a + dev/demo command but is labelled as such. (`aiui/composables/contentExtraction.ts:4-6`, + `useAI.ts:23-37`, bundle-verify by grep) +5. **Honest rendering**: hide zero-value metadata (no "★ 0 · 0m"), empty states name the + actual cause ("no media shared on this node yet" vs "No films match your search"), + mount or remove the dead detail views (apps/recipes), delete or wire the seed-only + surfaces (recipes/events) — no seed-only surface ships silently. +6. **Enrichment production answer** (per operator decision in FOLLOW-ON-SCOPE-PROPOSAL): + node-brokered enrichment/search endpoints, default-off, screened; fix the + `/api/web-search` vs `/aiui/api/web-search` path bug and give RSS a node route or + remove the feature flag. (`aiui/composables/useWebSearch.ts`, `useRssFetch.ts`) +7. **Green fixtures**: seed tests pass (fix the 10-vs-6 song extraction or correct the + fixture); add a regression test that a fresh prod bundle contains no mock hosts + (spotify.com/track/example, cloud.example.com, plex://). + +### Wave 2 — Mission alignment + +1. **Routstr funding UX** (already a captured todo — schedule it): provider dropdown, + in-chat funding, Settings modal, budget visible/raised only through trusted chrome. +2. **Standalone self-hosting**: wire the shipped Ollama adapter into the selectable + provider union; default standalone to local, Claude opt-in. + (`aiui/adapters/ollama-adapter.ts`, `aiui/composables/useAI.ts:16,121-123`) +3. **Price via the node**: replace browser mempool.space polling with a brokered value + from the node's own Bitcoin Core (or drop the widget). +4. **Honest stubs**: NWC/LNURL-auth/zap-QR either finished (NIP-47 signing is the blocker) + or visibly labelled "preview" in the UI; replace the fake QR with a real one or remove it. +5. **Blossom/hashtree**: do not fake. Document in the phase that both wait on the + IndeeHub catalog layer (`docs/dht-distribution-design.md`); when that lands, AIUI media + moves to blob-addressed storage. No AIUI-side work now. + +### Wave 3 — Already-proposed follow-on (unchanged, from FOLLOW-ON-SCOPE-PROPOSAL) + +Phase A (finish AIUI-02 settings + AIUI-05 delivery), Phase B (node-side relay bridge + +enrichment proxy, setting-derived CSP — relays first), Phase C (nostr polish + zaps +through the confirm gate). This plan's Wave 0/1 items slot *before* Phase B unblocks more +egress — widen the pipe only after the leaks at the current diameter are closed. + +--- + +## E. Coordination protocol (another agent is live in this tree) + +- All AIUI-tree work happens in `archy-phase13/aiui/` on + `gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf`; node-side + in `core/` and `image-recipe/` of the same worktree. +- Stage by explicit path; never `git add -A`. Commit + push per unit of work (repo rule). +- Wave 0 items S1/S5/S6 touch `core/assistant/` — check `git status`/`git log` for the + other agent's in-flight work there before editing; if conflicted, take S2/S3/S4/S7 + (aiui/nginx side) first. +- Do not touch `SongGrid.vue`, `ContentGridView.vue`, `packages/core/src/types/content.ts` + (D-12 holds: design stays, only data sources change). + +## F. Verification (per wave, on a real node) + +- **Wave 0**: grant Network → ask "what's my network status" on a cloud leg → assert no + WAN IP/SSID in the forwarded body (forwarder log + egress test); fresh profile → no + `claudeApiKey` key in localStorage; `/aiui/` without `?embedded=true` on a node refuses + or is screened; unauthenticated `curl /aiui/api/web-search` → 401; paste a + checksum-valid BIP39 into standalone chat → blocked notice; fresh install → no seeded + demo conversation. +- **Wave 1**: with a seeded test library on .228/archi-dev-box: a song answer plays + in-app from the real source URL; a film answer plays via Range; IndeeHub scope renders + cards when content exists; apps tool renders the apps grid; no-tool turn leaves grids + intact; prod bundle grep shows zero mock hosts; seed tests green. +- **Wave 2**: fund Routstr from the AIUI dropdown end-to-end on regtest-scale budget; + standalone default provider is Ollama; price widget works with browser devtools showing + no mempool.space call. +- Every wave: `tests/lifecycle/run-gate.sh` stays green on .228; AIUI verified in the real + embedded iframe, desktop + mobile (AIUI-06 holds).