Files

32 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
13-aiui-functional-conversational-node-control-and-content-surf 06 execute 2
13-01
neode-ui/src/composables/archyContentAdapter.ts
neode-ui/src/composables/__tests__/archyContentAdapter.test.ts
neode-ui/src/api/filebrowser-client.ts
neode-ui/src/api/__tests__/filebrowserStreamUrl.test.ts
neode-ui/src/services/contextBroker.ts
neode-ui/src/types/aiui-protocol.ts
/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts
/home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts
true
AIUI-03
truths artifacts key_links
AIUI's content grids show the node's real peer files, movies and owned/paid content instead of records regex-scraped out of the model's own prose (D-12)
AIUI's FilmGrid/SongGrid/NewsGrid components take zero code changes — only the data source behind their existing props changes (D-12)
IndeeHub and peer video reach the grids through the content + paid-unlock subsystem that already exists — invoices, X-Payment-Token, Range streaming — with no new payment rail (D-14)
Two content items with identical filename and size from different peers render as two distinct cards keyed by id, never merged; an item present both in this node's own library and in a peer share appears once per source (edge: AIUI-03 adjacency)
An empty content list renders the grid's empty state, not a spinner and not an error; a single item renders a one-card grid; an item with a null or absent description maps to an empty string, never the literal 'null' or 'undefined' (edge: AIUI-03 empty)
Content ordering is added_at descending with id ascending as the deterministic tiebreak, so items with equal timestamps come back in the same order on every call (edge: AIUI-03 ordering)
A content refresh arriving while an earlier one is still in flight is discarded by a request-id guard, so the grids never flip back to older data (edge: AIUI-03 concurrency)
No new streaming URL in this phase carries a credential in its query string — the leak is not propagated into the adapter
The pre-existing leak is actually closed, not merely avoided: filebrowser-client.ts's streamUrl returns a bare same-origin raw-file URL with no query component, and playback still works because the same-origin filebrowser cookie already travels on media subresource requests
path provides contains
neode-ui/src/composables/archyContentAdapter.ts ContentItem -> Film/Song/Podcast mapping; there is no shape overlap, so this is hand-written mapping logic export function adaptContentItems
path provides min_lines
neode-ui/src/composables/__tests__/archyContentAdapter.test.ts Fixture-pinned mapping including the adjacency, empty, ordering and concurrency edges 80
path provides contains
neode-ui/src/api/__tests__/filebrowserStreamUrl.test.ts Regression pin that streamUrl emits no query component, so the JWT-in-URL leak cannot come back streamUrl
from to via pattern
neode-ui/src/services/contextBroker.ts neode-ui/src/composables/archyContentAdapter.ts content:push handler adapts content.* RPC records before they cross the iframe boundary adaptContentItems
from to via pattern
/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts /home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts setArchyContent() writes panelFilms/panelSongs/panelPodcasts directly, bypassing updatePanelFromText's regex path setArchyContent
Make AIUI's content surfaces real. Today they are fed by regex-parsing the model's own reply text (`updatePanelFromText` → `contentExtraction.ts`) against fixture catalogs that are themselves injected into the system prompt — the largest data bucket in AIUI is LLM-synthesized, not an API awaiting a base URL. D-12 keeps the design exactly and changes what fills it.

The hard part is named in RESEARCH Pitfall 4: content_server.rs::ContentItem (id, filename, mime_type, size_bytes, description, access, availability, added_at) has no shape overlap with AIUI's Film/Song/Podcast (posterUrl, coverUrl, sources[] with type: 'plex'|'nextcloud'|…, genres, runtime, director). This is a hand-written adapter with fixture-pinned tests, not a pass-through.

Two things this plan deliberately does not do. It does not revive ContentPanel.vue — that is verified dead code taking ArchyAppsGrid, FavoritesGrid, DiscoverPanel, RecipeDetail and AppDetail with it, and CONTEXT.md defers it explicitly. The live render tree is ChatPage.vueContentGridView.vue → the *Grid components, and that is what gets fed. And it does not attempt to fix AIUI's six dev-only Vite plugins: all of them are configureServer/configurePreviewServer only and are therefore absent from the static dist/ a node serves, so TMDB posters, web search and RSS stay 404 on a node. Only the slice D-12 replaces gets a production answer; the rest stays explicitly deferred, and the plan says so rather than implying otherwise.

This plan also closes the one credential-in-URL leak CONTEXT.md names by hand: filebrowser-client.ts's streamUrl puts the filebrowser JWT in the query string, where it reaches browser history, Referer headers and access logs. CONTEXT.md calls it "the known leak to fix rather than propagate", so not reproducing it in new code is only half the instruction. The fix is small because the credential there is redundant: login() already sets that JWT as a path=/ cookie on the page's own origin, and the browser attaches it to the same-origin media request without being asked.

Output: archyContentAdapter.ts, a content:push channel on the existing broker, setArchyContent in AIUI, and a query-free streamUrl.

<flagged_assumptions> None in this plan.

Edge-probe accounting for AIUI-03. The probe surfaced four edges — adjacency, empty, ordering, concurrency — and all four are discharged here as covered truths tagged (edge: AIUI-03 …). 13-07 carries three further truths with an AIUI-03 edge tag; those are planner-authored re-applications of the same edge kinds to the persisted music index, marked — authored, not probe-surfaced so the phase does not count one four-finding probe as seven. The reconciliation is in 13-VALIDATION.md § Edge-Probe Reconciliation. </flagged_assumptions>

<artifacts_this_phase_produces> Symbols created by this plan:

neode-ui

  • composables/archyContentAdapter.ts: export function adaptContentItems, adaptToFilm, adaptToSong, adaptToPodcast, classifyByMime, sortDeterministic, export type ArchyContentBundle, export interface ArchyContentItem
  • services/contextBroker.ts: handleContentRequest (private), pushContent (private), contentRequestSeq (private field — the concurrency guard)
  • types/aiui-protocol.ts: AIUIContentRequest, ArchyContentPush

AIUI (/home/archipelago/Projects/AIUI, branch development)

  • composables/useArchy.ts: requestArchyContent
  • composables/useContentPanel.ts: setArchyContent, archyContentActive (ref)

Changed, not created: neode-ui/src/api/filebrowser-client.tsstreamUrl's body only. No new export, no signature change; it still returns Promise<string>, so every existing call site is untouched.

Unchanged by design and therefore not new symbols: FilmGrid.vue, SongGrid.vue, NewsGrid.vue, ContentGridView.vue, and every Film/Song/Podcast type in packages/core/src/types/content.ts. </artifacts_this_phase_produces>

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/STATE.md @CLAUDE.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-CONTEXT.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-PATTERNS.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-01-SUMMARY.md Task 1: The adapter — hand-written mapping, fixture-pinned, edges decided neode-ui/src/composables/archyContentAdapter.ts, neode-ui/src/composables/__tests__/archyContentAdapter.test.ts, neode-ui/src/api/filebrowser-client.ts, neode-ui/src/api/__tests__/filebrowserStreamUrl.test.ts - A video-mime `ContentItem` becomes a `Film` with `id` carried through, `title` derived from `filename` minus its extension, and exactly one entry in `sources[]` describing where it came from. - An audio-mime `ContentItem` becomes a `Song`; an image or document mime becomes neither and is excluded from all three buckets rather than mis-typed. - `m4a`, `aac`, `opus` and `wma` classify as audio — the four extensions `ShareModal.vue`'s mime map omits today. - An `access: 'Paid'` item maps with its price and a locked flag so the grid can render the paid state; it does **not** get a playable source URL until unlocked. - Two items with identical `filename` and `size_bytes` but different `id` produce two cards. - An empty input array produces empty `films`/`songs`/`podcasts` arrays — not `undefined`, not a thrown error. - A `null`/absent `description` maps to `''`; a `null` `added_at` sorts last rather than crashing the comparator. - Sorting is `added_at` descending, `id` ascending on ties — calling the adapter twice on the same input in a different array order yields identical output order. - `fileBrowserClient.streamUrl('/Music/x.m4a')` resolves to a same-origin raw-file URL carrying no query component and no credential anywhere in the string — the returned value contains no `?`, and does not contain the cookie's value. - `streamUrl` still awaits authentication before returning, so the cookie the media request depends on is guaranteed to be set by the time the caller assigns the URL to a media element. - `sanitizePath` traversal handling is unchanged by the fix — a path containing `..` is still resolved and never escapes root. - `/home/archipelago/Projects/AIUI/packages/core/src/types/content.ts` lines 7-70 — the exact target shapes: `Film` (line 7), `FilmSource` (23), `SongSource` (37), `Song` (44), `Podcast` (63). **This file is read, never modified** — D-12 keeps AIUI's design exactly. - `core/archipelago/src/content_server.rs` — `ContentItem` and `AccessControl` (`Free | PeersOnly | Paid`), the source shape being mapped from. - `core/archipelago/src/api/rpc/content.rs` — `content.list-mine`, `content.browse-peer`, `content.owned-list`, `content.preview-peer`, and the MIME auto-filing logic around line 668 (the classification precedent to stay consistent with). - `neode-ui/src/api/filebrowser-client.ts` in full — CONTEXT.md names this "the known leak to fix rather than propagate", and **this task fixes it**, so read the whole client, not just the leaking function. The four facts that make the fix small and safe: `login()` (lines 55-83) sets the filebrowser JWT as a **cookie** with `path=/` and `SameSite=Lax` on the page's own origin; `baseUrl` (line 43) is `window.location.origin + '/app/filebrowser'`, so a media element's request for it is **same-origin**; a same-origin subresource request carries that cookie automatically and `SameSite=Lax` does not restrict same-site subresources; and filebrowser's own auth reads the `auth` cookie, which is why its own web UI works without a query parameter. The credential in the query string is therefore redundant, not load-bearing. - `neode-ui/src/stores/cloud.ts` lines 117-119 and `neode-ui/src/components/cloud/MediaLightbox.vue` lines 138 and 202-203 — the call sites. They consume a URL string and are unaffected by dropping its query component; confirm that before changing anything. - `neode-ui/src/composables/__tests__/useFileType.test.ts` — the in-repo convention for a fixture-driven pure-function Vitest suite. - `13-RESEARCH.md` Pitfall 4 and Pitfall 5. Create `neode-ui/src/composables/archyContentAdapter.ts` exporting `adaptContentItems(items: ArchyContentItem[], opts: { source: 'own' | 'peer' | 'indeehub'; peerOnion?: string }): ArchyContentBundle` where `ArchyContentBundle` is `{ films: Film[]; songs: Song[]; podcasts: Podcast[] }` structurally matching AIUI's exported types (declare the minimal local interfaces rather than importing across repos — neode-ui does not depend on `@aiui/core`).

classifyByMime decides the bucket from mime_type with an extension fallback for the cases the mime is wrong or generic. It must classify audio/mp4, audio/aac, audio/opus, audio/x-ms-wma and the .m4a/.aac/.opus/.wma extensions as audio — ShareModal.vue's mime map omits exactly these four today, which is why such files currently share as application/octet-stream, never route to the audio player, and get auto-filed to Documents instead of Music. 13-11 fixes the share side; the adapter must not inherit the same blind spot.

adaptToFilm/adaptToSong/adaptToPodcast carry id through unchanged as the card key (this is what makes the adjacency case correct: two peers sharing a byte-identical file are two rows, because they are two things the operator can act on separately). Derive title from filename with the extension stripped. Map description ?? ''. Build exactly one sources[] entry per item, with a type value that distinguishes this node's own file from a peer's file from IndeeHub — pin those three literal values in the test so a later refactor cannot quietly change what a grid badge means.

For playback URLs: do not build any URL containing a credential in its query string. Own-node media resolves through the existing content endpoints (/content/<id>), peer media through the existing Rust Range-streaming proxy (/api/peer-content/<onion>/<id>) — both of which already carry the page's session. Where a bare <audio>/<video src> is unavoidable and a token is genuinely required, the URL must be minted per-resource and single-use rather than reusing a general session token. Add a test assertion that no adapter-produced URL carries a credential as a query parameter.

Then close the pre-existing leak rather than merely routing around it. CONTEXT.md names filebrowser-client.ts's streamUrl as the known leak "to fix rather than propagate", and a phase that only avoids reproducing it has not fixed it. In filebrowser-client.ts, change streamUrl to keep its ensureAuth() await and its sanitizePath call, and return the raw-file URL with no query component appended at all — drop the getAuthCookie() read and the credential interpolation entirely. The cookie that request needs is already set on the page's origin at path=/ by login(), and the browser attaches it to the same-origin media subresource request by itself; that is the same mechanism filebrowser's own UI relies on. Leave headers(), authedFetch and fetchBlobUrl alone — they authenticate by X-Auth header and were never leaking.

Write neode-ui/src/api/__tests__/filebrowserStreamUrl.test.ts as the regression pin: stub document.cookie and the app.filebrowser-token RPC, call streamUrl, and assert the result has no query component, contains none of the token's characters, and still points at the same-origin raw-file path for the sanitized input path. Include a traversal case so the fix cannot quietly change path handling.

Two things to record honestly in the summary. This removes the credential from browser history, Referer headers and access logs — it does not make the cookie itself short-lived or single-use; the 24-hour filebrowser JWT remains a 24-hour JWT, now confined to the cookie jar. And if playback regresses on the node — the one way this fix can fail is a deployment where filebrowser does not honour the cookie on its raw endpoint — do not restore the query parameter; report it and stop, because restoring it reopens exactly the leak this task exists to close.

For access: 'Paid': map price_sats and set a locked flag; do not emit a playable source. D-14 routes unlock through the existing invoice / X-Payment-Token / Range-streaming path — no new payment rail, and none is introduced here.

sortDeterministic sorts added_at descending with id ascending as the tiebreak, treating a missing added_at as oldest. This is what makes repeated calls stable.

Write the tests FIRST in archyContentAdapter.test.ts, one per <behavior> bullet, with inline fixtures. Include an explicit "shape pinning" test that asserts every field AIUI's FilmGrid/SongGrid reads is present and correctly typed on the adapter's output — that is the regression pin RESEARCH Pitfall 4 asks for, and the thing that catches a silent AIUI type change. cd neode-ui && npx vitest run src/composables/tests/archyContentAdapter.test.ts cd neode-ui && npx vitest run src/api/tests/filebrowserStreamUrl.test.ts cd neode-ui && npx vitest run src/components/tests/MediaLightboxPip.test.ts cd neode-ui && npx vue-tsc --noEmit <acceptance_criteria>

  • cd neode-ui && npx vitest run src/composables/__tests__/archyContentAdapter.test.ts exits 0 with a test per <behavior> bullet
  • grep -q 'export function adaptContentItems' neode-ui/src/composables/archyContentAdapter.ts
  • grep -ciE 'm4a|aac|opus|wma' neode-ui/src/composables/archyContentAdapter.ts is ≥ 4
  • grep -vE '^\s*(//|\*|/\*)' neode-ui/src/composables/archyContentAdapter.ts | grep -cE '[?&](auth|token)=' returns 0 — no credential-bearing URL is produced by the adapter (comment lines stripped first, so prose in the file cannot self-invalidate the gate)
  • The test file contains an assertion that no adapter-produced URL carries a credential query parameter
  • cd neode-ui && npx vitest run src/api/__tests__/filebrowserStreamUrl.test.ts exits 0 — the pre-existing leak is closed and pinned
  • grep -vE '^\s*(//|\*|/\*)' neode-ui/src/api/filebrowser-client.ts | grep -cF 'raw${safePath}?' returns 0 — streamUrl appends no query component
  • cd neode-ui && npx vitest run src/components/__tests__/MediaLightboxPip.test.ts exits 0 — the lightbox's streamUrl consumer did not regress
  • git -C /home/archipelago/Projects/AIUI diff --exit-code -- packages/core/src/types/content.ts packages/app/src/components/content/FilmGrid.vue packages/app/src/components/content/SongGrid.vue exits 0 — D-12's "props unchanged" held
  • cd neode-ui && npx vue-tsc --noEmit exits 0 </acceptance_criteria> D-12's grid-source swap is rated costly in CONTEXT.md — the grids stay prop-driven and the source behind them is swappable, but every consumer is written against this mapping's field semantics. Flagged, not gated. Real ContentItem fixtures produce grid-ready Film/Song/Podcast records with stable ordering, correct empty/adjacency behaviour, no credential-bearing URLs, and no change to any AIUI grid component — and filebrowser-client.ts's streamUrl returns a query-free same-origin URL, so the leak CONTEXT.md named is closed rather than merely unrepeated.
Task 2: A content channel on the existing bridge, with a stale-response guard neode-ui/src/services/contextBroker.ts, neode-ui/src/types/aiui-protocol.ts - `neode-ui/src/services/contextBroker.ts` — the `handleMessage` switch (lines 71-84, now carrying 13-01's `chat:request` arm), `handleContextRequest` at 87, the ten `sanitize*` methods at 290-299, and `postToIframe` at 620. - `neode-ui/src/types/aiui-protocol.ts` — the unions 13-01 extended with `AIUIChatRequest`/`ArchyChatResponse`. - `neode-ui/src/services/__tests__/contextBroker.test.ts` — the suite that must stay green. - `core/archipelago/src/api/rpc/content.rs` — the exact `content.*` method names and their param shapes: `content.list-mine`, `content.browse-peer`, `content.owned-list`. - `neode-ui/src/stores/aiPermissions.ts` — the `media` and `files` categories; content push is gated on them. Add a content channel to the existing origin-checked bridge — a **single generic channel with a `kind` discriminator**, not one channel per content type. 13-11 adds music to it without touching this file again, which is what keeps the music track independent.

In aiui-protocol.ts add AIUIContentRequest { type: 'content:request'; id: string; kind: 'films' | 'songs' | 'podcasts' | 'all'; scope?: 'own' | 'peers' | 'owned' } and ArchyContentPush { type: 'content:push'; id: string; kind: string; films?: …; songs?: …; podcasts?: … }, adding each to the appropriate union.

In contextBroker.ts add a case 'content:request' arm and a private handleContentRequest(id, kind, scope) that: checks the media/files permission categories through the existing useAIPermissionsStore (this channel carries node data to the iframe, so it is a consent surface — unlike chat:request, whose authority is resolved node-side); calls the relevant content.* RPCs via rpcClient.call; runs the results through adaptContentItems; and posts a content:push back through the existing postToIframe.

Add the concurrency guard: a private monotonically-increasing contentRequestSeq. Each handleContentRequest captures its sequence number before awaiting and discards its own result if a newer request has started in the meantime. Without this, a slow content.browse-peer landing after a fast content.list-mine flips the grid back to older data — the failure the AIUI-03 concurrency edge names.

Do not add a second postMessage channel, do not relax this.allowedOrigin, and do not let AIUI supply the RPC method name or params — the iframe names a kind, the broker decides the call. Extend contextBroker.test.ts with a stale-response case asserting that an out-of-order resolution does not overwrite newer data. cd neode-ui && npx vitest run src/services/tests/contextBroker.test.ts cd neode-ui && npx vitest run src/views/tests/chatAiuiEmbed.test.ts cd neode-ui && npx vue-tsc --noEmit <acceptance_criteria>

  • grep -q "content:request" neode-ui/src/services/contextBroker.ts and grep -q "adaptContentItems" neode-ui/src/services/contextBroker.ts
  • grep -q "contentRequestSeq" neode-ui/src/services/contextBroker.ts — the stale-response guard exists
  • contextBroker.test.ts contains a test whose name mentions stale or out-of-order, and it passes
  • cd neode-ui && npx vitest run src/services/__tests__/contextBroker.test.ts && npx vitest run src/views/__tests__/chatAiuiEmbed.test.ts both exit 0
  • grep -c "method: msg\.\|method: request\." neode-ui/src/services/contextBroker.ts returns 0 — the iframe never names an RPC method
  • cd neode-ui && npx vue-tsc --noEmit exits 0 </acceptance_criteria> A content:request from the allowed origin, with the media/files categories granted, returns adapted grid records; an ungranted category returns a refusal; a stale in-flight response never overwrites newer data.
Task 3: AIUI renders Archy content in the grids it already has /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts, /home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts - `/home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts` lines 1-45 — the module-level `panelFilms`/`panelSongs`/`panelPodcasts` refs and the mock imports, and `updatePanelFromText` at line 80 with its export list at 495-520. - `/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts` — the `__AIUI_EMBEDDED__` detection at lines 80-81 and the existing `archyBridge.requestContext(cat).then(...)` shape at line 134. **Mirror this; do not invent a third convention.** - `/home/archipelago/Projects/AIUI/packages/app/src/pages/ChatPage.vue` — the live render tree (`ContentGridView`). **Note `ContentPanel.vue` is dead code and must not be built through** (CONTEXT.md Deferred). - `/home/archipelago/Projects/AIUI/packages/app/src/composables/__tests__/` — the existing suite, including `contentExtraction.test.ts`, which must stay green. Work in `/home/archipelago/Projects/AIUI` on branch `development`.

In useContentPanel.ts add setArchyContent(bundle: { films?; songs?; podcasts? }), which writes the module-level panelFilms/panelSongs/panelPodcasts refs directly, and an archyContentActive ref it sets true. Export both. Then guard updatePanelFromText so that when archyContentActive is true it does not overwrite the film/song/podcast buckets from regex-scraped model prose — the Archy-sourced grids are the source of truth for those three buckets when a node is supplying them. Leave the rest of updatePanelFromText (books, TV, images, places, magazine, code, recipes, news) untouched: those still have no Archy source and are outside D-12's slice.

Do not delete contentExtraction.ts or its regex path. 13-PATTERNS.md calls this a partial deprecation: the regex path stays for AIUI's non-Archy content and for standalone mode (D-17), and is bypassed only for the three Archy-sourced buckets.

In useArchy.ts add requestArchyContent(kind, scope) following the existing archyBridge.requestContext shape, and call setArchyContent from its content:push handler. Register the handler alongside the existing bridge listeners; do not add a second window.addEventListener('message').

Do not touch FilmGrid.vue, SongGrid.vue, NewsGrid.vue, ContentGridView.vue or packages/core/src/types/content.ts — D-12 is explicit that only the data source changes. Do not revive ContentPanel.vue, ArchyAppsGrid.vue, FavoritesGrid.vue, DiscoverPanel.vue, RecipeDetail.vue or AppDetail.vue.

Record honestly in the summary that TMDB posters, web search and RSS remain 404 on a node because their Vite plugins are dev-server-only — a Film adapted from a peer file has no posterUrl and the grid must render its existing no-artwork state rather than a broken image.

Commit and push on development, staging explicitly by path. cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run cd /home/archipelago/Projects/AIUI/packages/app && npx vue-tsc --noEmit cd /home/archipelago/Projects/AIUI && git diff --exit-code HEAD~1 -- packages/app/src/components/content/ packages/core/src/types/content.ts <acceptance_criteria>

  • grep -q 'setArchyContent' /home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts and it appears in the export list
  • grep -q 'archyContentActive' /home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts
  • grep -q 'requestArchyContent' /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts
  • grep -c 'ContentPanel' /home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts returns 0 — the dead path was not revived
  • git -C /home/archipelago/Projects/AIUI diff --exit-code HEAD~1 -- packages/app/src/components/content/ exits 0 — no grid component changed
  • cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run exits 0 (contentExtraction.test.ts still green — the regex path was guarded, not removed)
  • cd /home/archipelago/Projects/AIUI/packages/app && npx vue-tsc --noEmit exits 0
  • The commit is pushed to development </acceptance_criteria> With a node supplying content, FilmGrid and SongGrid render real peer/owned/paid records through their unchanged props; with no node, AIUI's own regex path still works exactly as before.

<threat_model>

Trust Boundaries

Boundary Description
peer-supplied filenames and descriptions → the browser DOM Peer-authored strings render as card titles and descriptions
peer-supplied filenames and descriptions → the model context Same strings will reach the assistant's context — D-10 territory, enforced in 13-12
broker → iframe Node content crosses into AIUI; gated on the media/files grants
media URL → <audio>/<video> Where credentials leak into history, access logs and Referer headers if built carelessly

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-13-32 Information Disclosure A credential in a media URL query string built by new code (the adapter) high mitigate The adapter builds no credential-bearing URL; own media goes through session-carrying content endpoints and peer media through the existing Rust Range proxy. Asserted by a comment-filtered grep gate and a test assertion. Scope of this row is the new code only — the pre-existing leak is T-13-39
T-13-39 Information Disclosure The pre-existing leak: filebrowser-client.ts's streamUrl puts the filebrowser JWT in the query string, so it reaches browser history, Referer headers and any access log on the path high mitigate streamUrl is changed in this plan's Task 1 to return a query-free same-origin URL and rely on the path=/ cookie login() already sets — filebrowser-client.ts is in files_modified and filebrowserStreamUrl.test.ts pins it. Residual, stated rather than implied: the JWT is still a 24-hour token, now confined to the cookie jar; making it short-lived or per-resource is a separate change this phase does not make
T-13-33 Information Disclosure Content pushed to the iframe without a grant high mitigate handleContentRequest checks media/files through the existing permissions store before any RPC call
T-13-34 Tampering Iframe names its own RPC method or params high mitigate The iframe supplies only a kind/scope enum; the broker chooses the method. Asserted by the "iframe never names an RPC method" grep
T-13-35 Elevation of Privilege Paid content playable without unlock high mitigate access: 'Paid' maps to a locked card with no playable source; unlock stays on the existing invoice / X-Payment-Token path (D-14). No new payment rail
T-13-36 Tampering Peer-authored filename rendered as HTML medium mitigate Vue's template interpolation escapes by default and no v-html is introduced; the adapter emits plain strings and never markup
T-13-37 Spoofing Two peers' byte-identical files merged into one card, hiding which peer served it medium mitigate Cards key on id, never on filename+size; asserted by the adjacency test
T-13-38 Denial of Service Stale slow response overwrites fresher grid data low mitigate contentRequestSeq guard; asserted by the out-of-order test
T-13-SC Tampering npm/pip/cargo installs high mitigate Zero packages added in either repo. No install task, so no legitimacy checkpoint required
</threat_model>
- `cd neode-ui && npx vitest run src/composables/__tests__/archyContentAdapter.test.ts && npx vitest run src/services/__tests__/contextBroker.test.ts && npx vitest run src/views/__tests__/chatAiuiEmbed.test.ts` all green - `cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run && npx vue-tsc --noEmit` green - `git -C /home/archipelago/Projects/AIUI diff --exit-code HEAD~1 -- packages/app/src/components/content/ packages/core/src/types/content.ts` exits 0 - No adapter-produced URL carries a credential query parameter, and `filebrowserStreamUrl.test.ts` is green

<success_criteria> AIUI's existing grids show the node's real content, with no grid component or content type changed; the mapping is pinned by fixtures at its adjacency, empty, ordering and concurrency edges; the phase gains no new credential-in-URL leak and no new payment rail; and the one credential-in-URL leak that already existed is closed at its source, with its remaining long-lived-token residual named rather than glossed. </success_criteria>

Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-06-SUMMARY.md` when done