Files

329 lines
32 KiB
Markdown
Raw Permalink Normal View History

2026-08-12 10:55:49 +00:00
---
phase: 13-aiui-functional-conversational-node-control-and-content-surf
plan: 06
type: execute
wave: 2
depends_on: ["13-01"]
files_modified:
- 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
autonomous: true
requirements: [AIUI-03]
must_haves:
truths:
- "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"
artifacts:
- path: "neode-ui/src/composables/archyContentAdapter.ts"
provides: "ContentItem -> Film/Song/Podcast mapping; there is no shape overlap, so this is hand-written mapping logic"
contains: "export function adaptContentItems"
- path: "neode-ui/src/composables/__tests__/archyContentAdapter.test.ts"
provides: "Fixture-pinned mapping including the adjacency, empty, ordering and concurrency edges"
min_lines: 80
- path: "neode-ui/src/api/__tests__/filebrowserStreamUrl.test.ts"
provides: "Regression pin that streamUrl emits no query component, so the JWT-in-URL leak cannot come back"
contains: "streamUrl"
key_links:
- from: "neode-ui/src/services/contextBroker.ts"
to: "neode-ui/src/composables/archyContentAdapter.ts"
via: "content:push handler adapts content.* RPC records before they cross the iframe boundary"
pattern: "adaptContentItems"
- from: "/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts"
to: "/home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts"
via: "setArchyContent() writes panelFilms/panelSongs/panelPodcasts directly, bypassing updatePanelFromText's regex path"
pattern: "setArchyContent"
---
<objective>
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.vue``ContentGridView.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`.
</objective>
<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.ts``streamUrl`'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>
<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
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: The adapter — hand-written mapping, fixture-pinned, edges decided</name>
<files>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</files>
<behavior>
- 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.
</behavior>
<read_first>
- `/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.
</read_first>
<action>
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.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; npx vitest run src/composables/__tests__/archyContentAdapter.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vitest run src/api/__tests__/filebrowserStreamUrl.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vitest run src/components/__tests__/MediaLightboxPip.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vue-tsc --noEmit</automated>
</verify>
<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>
<reversibility rating="costly">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.</reversibility>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: A content channel on the existing bridge, with a stale-response guard</name>
<files>neode-ui/src/services/contextBroker.ts, neode-ui/src/types/aiui-protocol.ts</files>
<read_first>
- `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.
</read_first>
<action>
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.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; npx vitest run src/services/__tests__/contextBroker.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vitest run src/views/__tests__/chatAiuiEmbed.test.ts</automated>
<automated>cd neode-ui &amp;&amp; npx vue-tsc --noEmit</automated>
</verify>
<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>
<done>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.</done>
</task>
<task type="auto">
<name>Task 3: AIUI renders Archy content in the grids it already has</name>
<files>/home/archipelago/Projects/AIUI/packages/app/src/composables/useArchy.ts, /home/archipelago/Projects/AIUI/packages/app/src/composables/useContentPanel.ts</files>
<read_first>
- `/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.
</read_first>
<action>
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.
</action>
<verify>
<automated>cd /home/archipelago/Projects/AIUI/packages/app &amp;&amp; npx vitest run</automated>
<automated>cd /home/archipelago/Projects/AIUI/packages/app &amp;&amp; npx vue-tsc --noEmit</automated>
<automated>cd /home/archipelago/Projects/AIUI &amp;&amp; git diff --exit-code HEAD~1 -- packages/app/src/components/content/ packages/core/src/types/content.ts</automated>
</verify>
<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>
<done>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.</done>
</task>
</tasks>
<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>
<verification>
- `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
</verification>
<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>
<output>
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-06-SUMMARY.md` when done
</output>