diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 99edec19..69f6c0a7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -386,4 +386,8 @@ Plans: - [ ] 13-15-PLAN.md — On-device sign-off: archi-dev-box, embedded iframe, desktop + mobile (AIUI-06) **Track note (D-13):** the music-library track (13-04 → 13-07 → 13-11) is independent — no plan -on the control or content track depends on any music plan. +on the control or content track depends on any music plan, **and neither does the phase-closing +gate**. 13-15 depends on 13-06, 13-09 and 13-14 only, so there is no path from it to 13-04, +13-07 or 13-11: if the music track slips or is deferred, 13-15 records that at its step 7b and +the control and content work still closes and ships. 13-11 is therefore a terminal plan of the +phase rather than a gate on it. diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-01-PLAN.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-01-PLAN.md index 8ae61a40..e953b103 100644 --- a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-01-PLAN.md +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-01-PLAN.md @@ -112,8 +112,15 @@ Goes red if a future phase reintroduces the mesh-only assumption. -None in this plan. (The four `unclassified` edge-probe rows are surfaced in 13-05, 13-09 and -13-15, where their requirements live.) +None in this plan. + +**Edge-probe accounting for AIUI-01.** Its probe resolved `covered` and produced two findings, +both here: the pending-confirmation lifecycle truth tagged `(edge: AIUI-01 concurrency)` above, +and the two-tab nonce finding carried as a `verification: backstop` scalar rather than a plain +truth. The four probes that returned `unclassified` belong to other requirements and are +surfaced where those requirements live — AIUI-02 in 13-05, AIUI-04 and AIUI-05 in 13-09, AIUI-06 +in 13-15. Six requirements probed, two `covered`, four `unclassified`, nothing dropped; the full +reconciliation with its counts is in `13-VALIDATION.md` § Edge-Probe Reconciliation. diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-06-PLAN.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-06-PLAN.md index b22c9827..e665ccaa 100644 --- a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-06-PLAN.md +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-06-PLAN.md @@ -7,6 +7,8 @@ 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 @@ -23,7 +25,8 @@ must_haves: - "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 filebrowser-client.ts streamUrl JWT-in-URL leak is not propagated" + - "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" @@ -31,6 +34,9 @@ must_haves: - 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" @@ -65,12 +71,27 @@ a node serves, so TMDB posters, web search and RSS stay 404 on a node. Only the replaces gets a production answer; the rest stays explicitly deferred, and the plan says so rather than implying otherwise. -Output: `archyContentAdapter.ts`, a `content:push` channel on the existing broker, and -`setArchyContent` in AIUI. +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`. -None in this plan. (AIUI-03's four edge probes are all resolved as `covered` truths above.) +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. @@ -88,6 +109,10 @@ Symbols created by **this plan**: - `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`, 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`. @@ -111,7 +136,7 @@ Unchanged by design and therefore **not** new symbols: `FilmGrid.vue`, `SongGrid 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/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. @@ -121,12 +146,16 @@ Unchanged by design and therefore **not** new symbols: `FilmGrid.vue`, `SongGrid - 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` lines 172-176 — `streamUrl`'s `?auth=${token}` JWT-in-query-string. **This is the known leak to fix rather than propagate**; read it so you recognise the pattern and do not reproduce it. +- `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. @@ -137,7 +166,13 @@ Create `neode-ui/src/composables/archyContentAdapter.ts` exporting `adaptContent `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/`), peer media through the existing Rust Range-streaming proxy (`/api/peer-content//`) — both of which already carry the page's session. Where a bare ` @@ -257,7 +297,8 @@ Commit and push on `development`, staging explicitly by path. | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| -| T-13-32 | Information Disclosure | A credential in a media URL query string | high | mitigate | The adapter builds no `?auth=`/`?token=` URL; own media goes through session-carrying content endpoints and peer media through the existing Rust Range proxy. Asserted by a grep gate and a test assertion. This is the `filebrowser-client.ts` leak, fixed rather than propagated | +| 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 | @@ -271,13 +312,15 @@ Commit and push on `development`, staging explicitly by path. - `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 matches `[?&](auth|token)=` +- No adapter-produced URL carries a credential query parameter, and `filebrowserStreamUrl.test.ts` is green 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; and the phase gains no new credential-in-URL leak and no new payment rail. +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. diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-07-PLAN.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-07-PLAN.md index c1cd1b16..06f8a038 100644 --- a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-07-PLAN.md +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-07-PLAN.md @@ -17,9 +17,9 @@ must_haves: - "The node has a real music library — albums, artists and tracks derived from extracted tags, persisted under data_dir, not a MIME filter over a folder listing (D-13)" - "The index stays fresh: a file added, changed or removed since the last scan is reflected without a full rebuild, and a full reindex is available on demand" - "An index written by a newer MUSIC_SCHEMA_VERSION is refused and rebuilt rather than misread" - - "A concurrent read during a reindex returns a consistent snapshot, never a partially-written index (edge: AIUI-03 concurrency)" - - "Album and track ordering is deterministic and stable across repeated calls, with a defined tiebreak when sort keys are equal (edge: AIUI-03 ordering)" - - "An empty library returns empty arrays with a scanned-at timestamp, not an error and not a null (edge: AIUI-03 empty)" + - "A concurrent read during a reindex returns a consistent snapshot, never a partially-written index (edge: AIUI-03 concurrency — authored, not probe-surfaced)" + - "Album and track ordering is deterministic and stable across repeated calls, with a defined tiebreak when sort keys are equal (edge: AIUI-03 ordering — authored, not probe-surfaced)" + - "An empty library returns empty arrays with a scanned-at timestamp, not an error and not a null (edge: AIUI-03 empty — authored, not probe-surfaced)" - "The indexer never reads outside the configured media roots" artifacts: - path: "core/archipelago/src/music/index.rs" @@ -62,6 +62,15 @@ Output: `music/index.rs`, `music/mod.rs` completed, and the `music.*` RPC surfac None in this plan. + +**Edge-tag provenance.** The three truths above tagged `(edge: AIUI-03 … — authored, not +probe-surfaced)` are **not** deterministic-probe output. The AIUI-03 probe surfaced four edges +and all four are discharged in 13-06 as covered truths. These three re-apply the same edge kinds +— concurrency, ordering, empty — to a different subject (the on-disk music index rather than the +in-browser content adapter), because a persisted index has its own versions of them that +13-06's tests cannot reach. They are planner-authored coverage, and the tag says so, so the +phase's edge accounting is not double-counting one probe as seven findings. The full +reconciliation is in `13-VALIDATION.md` § Edge-Probe Reconciliation. diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-11-PLAN.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-11-PLAN.md index d86d2f98..131d7575 100644 --- a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-11-PLAN.md +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-11-PLAN.md @@ -17,7 +17,7 @@ must_haves: - "AIUI's SongGrid shows the node's real music library — albums, artists and tracks from the index, not a MIME-filtered folder listing (D-13, D-12)" - "An .m4a, .aac, .opus or .wma file shared from the cloud view gets a real audio MIME type, routes to the global bottom-bar player, and auto-files to Music instead of Documents" - "Audio opens in the global bottom-bar player and never in the lightbox — the rule enforced across five existing call sites is not broken by the new path" - - "The music track reached the UI without any control-track or content-track plan depending on a music plan (D-13)" + - "The music track reached the UI without any control-track or content-track plan depending on a music plan, and without the phase-closing gate 13-15 depending on one either (D-13)" artifacts: - path: "neode-ui/src/composables/archyContentAdapter.ts" provides: "music.* records mapped onto AIUI's Song/Album shape, alongside the existing ContentItem mapping" @@ -41,7 +41,14 @@ D-13 is explicit that the music library "lands as its own wave of plans inside P blocking the rest — peer files, movies and conversational control ship on their own track and the library lights up `SongGrid` when ready." This is that plan. Its `depends_on` points at the music indexer and the content adapter; **no plan on the control or content track depends on any -music plan**, so the track independence D-13 requires holds literally in the wave graph. +music plan, and neither does the phase-closing gate 13-15**, so the track independence D-13 +requires holds literally in the wave graph rather than only in prose. + +That makes this a **terminal** plan: nothing lists it in `depends_on`, by design. It is not +orphaned — it owns AIUI-03's `SongGrid` and share-MIME deliverables, it lands at wave 4 well +ahead of the wave-8 gate, and 13-15 step 7b reads its summary as a best-effort input and records +the result. But if it slips, is red, or is deferred, 13-15 records that and the phase closes on +the control and content tracks anyway. That is the whole point of D-13. The second half is a verified, directly-relevant landmine: `neode-ui/src/components/cloud/ShareModal.vue` line 358 maps `mp3`, `flac`, `ogg` and `wav` and omits `m4a`, `aac`, `opus` and `wma`. Those four diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-15-PLAN.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-15-PLAN.md index 00f840e6..095982bb 100644 --- a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-15-PLAN.md +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-15-PLAN.md @@ -3,7 +3,7 @@ phase: 13-aiui-functional-conversational-node-control-and-content-surf plan: 15 type: execute wave: 8 -depends_on: ["13-09", "13-11", "13-14"] +depends_on: ["13-06", "13-09", "13-14"] files_modified: - .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md - .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-UAT.md @@ -16,6 +16,7 @@ must_haves: - "Every row of 13-VALIDATION.md's Per-Task Verification Map is owned by a named task in a named plan and has a recorded status" - "The deployed surface is checked, not only the source: the model proxies are closed and the shipped bundle is the one that was built" - "Every manual-only verification listed in 13-VALIDATION.md has been performed and its result recorded" + - "This gate closes on the control and content tracks alone: its depends_on contains no music plan, and no music-track outcome can hold the phase open (D-13)" artifacts: - path: ".planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-UAT.md" provides: "The on-device acceptance record: what was exercised, on what hardware, at what viewport, with what result" @@ -50,6 +51,17 @@ This plan also discharges `13-VALIDATION.md`, whose Per-Task Verification Map st Task ID / Plan / Wave / Threat Ref columns by design — the planner left them fillable and execution fills them. +**What this gate does and does not gate on (D-13).** D-13 locks the music library as its own +track inside Phase 13 that does **not** block the rest: "peer files, movies and conversational +control ship on their own track and the library lights up `SongGrid` when ready." So this gate +depends on the control track (13-14, and through it 13-13 → 13-12 → 13-10 → 13-08 → 13-05 → +13-01), the delivery track (13-09 → 13-02) and the content track (13-06) — and on **no** music +plan. The music chain (13-04 → 13-07 → 13-11) lands at wave 4 and, if it is ready, its result is +recorded here as a bonus pass; if it slipped, is red, or was deferred, that is recorded and the +phase still closes. This is a real property of the wave graph, not a comment: there is no path +from this plan's `depends_on` to 13-04, 13-07 or 13-11. Step 7b below is the non-blocking music +step and takes exactly the record-and-defer shape step 10 already uses for Routstr. + Output: a completed `13-VALIDATION.md` and a `13-UAT.md` acceptance record. @@ -81,9 +93,13 @@ repository. @CLAUDE.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.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-06-SUMMARY.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-09-SUMMARY.md -@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-11-SUMMARY.md @.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-14-SUMMARY.md + +Deliberately **not** auto-included: `13-11-SUMMARY.md`. It may not exist when this plan runs, and +this gate must not fail to load because the music track has not landed. Task 2 reads it only if +it is present. @@ -139,12 +155,13 @@ Set `nyquist_compliant` in the frontmatter to `true` only if every row has an au - `CLAUDE.md` — deploy to the dev pair before any OTA; verify on the real node before any tag; the frontend-build verify rule; and the node-side verify rule that `assets/` is a graveyard so live chunks must be resolved via the service worker manifest and fetched over HTTP. - The project memory notes for archi-dev-box's address and credentials, and for the known mobile gotcha that `.local` https does not resolve on Android (no mDNS) — reach the node by IP or Tailscale name on the handheld, not by `.local`. - `.planning/phases/13-.../13-09-SUMMARY.md` — the deploy and verify scripts to use, and the CSP that landed. +- `.planning/phases/13-.../13-11-SUMMARY.md` — **only if the file exists.** This is the music track's landing summary and it is a best-effort input to step 7b, never a gate. If it is absent, the music track has not landed; go to step 7b's defer branch and do not wait for it. The whole phase, deployed to archi-dev-box: the node-side assistant with three backends, the curated tool registry, the confirm gate in trusted chrome, the untrusted-content boundary, the -egress guardrails, the content grids fed from real node data, the music library, the closed -model proxies and the verified delivery path. +egress guardrails, the content grids fed from real node data, the closed model proxies and the +verified delivery path — plus, if its independent track landed in time, the music library. Deploy first: `bash scripts/build-aiui.sh`, build the frontend with `cd neode-ui && npm run build` @@ -166,10 +183,21 @@ have not confirmed by fetching bytes. Ask again and approve — that container restarts. 6. Ask for something outside the ceiling: a wallet spend, the seed phrase, a factory reset. Each is refused plainly, redirected to the real UI path, and **not** described as done. -7. Open a content grid. Real peer/owned files render. Open the music view — real albums and - tracks. Play a track: it plays in the bottom bar and does **not** open the lightbox. -8. Share an `.m4a` from the cloud view. It gets an audio type, plays in the bottom bar, and - files under Music. +7. Open a content grid. Real peer/owned files render — not fixtures, not model-invented rows. + Play a media file from that grid: audio plays in the bottom bar and does **not** open the + lightbox. **Blocking** — this is 13-06's content track and it is a gate. +7b. **Non-blocking, music track (D-13).** If `13-11-SUMMARY.md` exists, open the music view: + real albums and tracks from the index, and a played track goes to the bottom bar, not the + lightbox. Then share an `.m4a` from the cloud view and confirm it gets an audio type, plays + in the bottom bar, and files under Music rather than Documents. If the music track has not + landed, is red, or was deferred, **record that in `13-UAT.md` and move on** — exactly as + step 10 does for Routstr. A missing or failing music view is recorded as a known gap and + does **not** block this phase; D-13 locked the library as an independent track precisely so + that the control and content work can ship without it. +8. Share a non-audio file (a video and a document) from the cloud view and confirm each still + gets its correct type and files where it always did. The share path's MIME map is edited by + the music track, so this is the regression check that the other types were not disturbed — + and it is meaningful whether or not that edit has landed yet. 9. In the AIUI frame's devtools console, POST to the RPC endpoint. It is CSP-blocked. From the top-level frame, the same call succeeds. 10. If Routstr shipped: set a small prepaid allowance, force the Routstr leg, and confirm a real @@ -181,7 +209,8 @@ or Tailscale name, not `.local`): 11. AIUI renders in the embedded iframe at phone width without horizontal scroll. 12. The confirm dialog covers the full viewport on the handheld and its buttons are tappable without zooming. -13. The content grid and the music grid are usable at phone width. +13. The content grid is usable at phone width. If the music track landed, the music grid is too — + if it did not, record that and carry on, per step 7b. 14. The bottom-bar audio player is reachable and does not collide with the mobile tab bar. 15. The on-screen keyboard does not push the chat input off-screen or under the tab bar. @@ -190,15 +219,17 @@ address, the build marker, and screenshots for steps 5, 9, 11 and 12. - `bash scripts/verify-aiui-deploy.sh ""` exits 0 and `bash tests/production-quality/aiui-proxy-closed.sh ` exits 0, both recorded with their output -- `13-UAT.md` exists with a row per numbered step above, each marked pass or fail with an observation — not a bare tick +- `13-UAT.md` exists with a row per numbered step above — the fifteen numbered steps plus 7b — each marked pass, fail or deferred with an observation, not a bare tick - Steps 2, 3, 5, 6, 7 and 9 all pass; any failure among them blocks the phase rather than being recorded as a known issue +- Steps 7b and 10 are the only two steps whose failure or absence does **not** block: each records either a real result or a named deferral, and neither may be left silent +- `13-UAT.md` states in one line that the phase closed on the control and content tracks, and gives 7b's music-track outcome as pass, gap or deferred — so a reader can tell which of the two tracks this sign-off covers - Screenshots for steps 5, 9, 11 and 12 are referenced in `13-UAT.md` - The hardware, browser and viewport used for the mobile pass are named, including the real handheld's model - Step 10 records either a successful paid request with a hard stop at the ceiling, or 13-13's recorded deferral — never silence - `13-UAT.md` cross-references the four Manual-Only rows in `13-VALIDATION.md` and each of those rows is marked discharged - Both planning documents are committed and pushed per `CLAUDE.md` - Type "approved" with the fifteen step results and the handheld model, or list which steps failed and what you saw. + Type "approved" with the fifteen numbered step results plus 7b's music-track outcome and the handheld model, or list which steps failed and what you saw. @@ -223,6 +254,7 @@ address, the build marker, and screenshots for steps 5, 9, 11 and 12. | T-13-105 | Spoofing | The confirm dialog clipped or trapped by an ancestor transform at phone width, so the backdrop is not full-screen | medium | mitigate | Step 12 checks it on real hardware. This is the project's repeatedly-reinforced Teleport-to-body rule and its failure mode is a partially-obscured signing screen | | T-13-106 | Denial of Service (financial) | Routstr's live behaviour untested, so the budget ceiling is only proven in unit tests | medium | mitigate | Step 10 either exercises a real paid request and a real exhaustion stop, or records 13-13's deferral. Silence is not an acceptable outcome | | T-13-107 | Repudiation | A validation map marked compliant while rows remain undischarged | medium | mitigate | `nyquist_compliant` is set true only when every row is discharged; otherwise it stays false with the gap named | +| T-13-108 | Denial of Service (delivery) | The independent music track holding the control/content sign-off hostage, so shippable work cannot be signed off | medium | mitigate | D-13 enforced structurally: this plan's `depends_on` has no path to 13-04/13-07/13-11, and step 7b is record-and-defer rather than a gate. The wave graph, not a comment, is what makes the two tracks separable | | T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | **Zero** packages added; this plan changes no source in either repository. No install task, so no legitimacy checkpoint required | @@ -230,15 +262,21 @@ address, the build marker, and screenshots for steps 5, 9, 11 and 12. - All three automated suites green: `cargo test --package archipelago` from `core/`, `vitest run` from `neode-ui/`, and `vitest run` from AIUI's `packages/app` - `verify-aiui-deploy.sh` and `aiui-proxy-closed.sh` both exit 0 against archi-dev-box - `grep -c TBD 13-VALIDATION.md` returns 0 and no row is `pending` -- `13-UAT.md` records all fifteen steps with observations and the four screenshots +- `13-UAT.md` records all fifteen numbered steps plus 7b with observations and the four screenshots +- This plan's `depends_on` names no music plan, and `13-UAT.md` states which track the sign-off covers The phase is done in the sense the phase itself demands: a typed request in the real embedded AIUI on real hardware reaches a real node action and returns a real result; writes stop at a -dialog the iframe cannot touch; the ceiling holds; the grids and the library show real data; -the unauthenticated doors are shut; and all of it is recorded against a node, on desktop and on -a phone, rather than asserted from a test run. +dialog the iframe cannot touch; the ceiling holds; the content grids show real data; the +unauthenticated doors are shut; and all of it is recorded against a node, on desktop and on a +phone, rather than asserted from a test run. + +The music library's state is recorded here, not required here. Per D-13 it is an independent +track: if it landed, 7b records it passing and the phase closes with the library lit; if it did +not, 7b records the gap and the phase closes anyway on the control and content tracks. Either +outcome is a valid close — an unrecorded one is not. diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md index ad054754..7492bb79 100644 --- a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md @@ -80,13 +80,45 @@ Requirement-level map seeded from research. **The planner fills Task ID / Plan / | Behavior | Requirement | Why Manual | Test Instructions | |----------|-------------|------------|-------------------| -| Embedded AIUI works in the real iframe | AIUI-06 | Real-device rendering in the actual embed context; `dev:mock` does not reproduce it | Load neode-ui Chat view on archi-dev-box, desktop **and** mobile viewport; exercise a read tool, a confirmed write, and a content grid | +| Embedded AIUI works in the real iframe | AIUI-06 | Real-device rendering in the actual embed context; `dev:mock` does not reproduce it | Load neode-ui Chat view on archi-dev-box, desktop **and** mobile viewport; exercise a read tool, a confirmed write, and a content grid. **Scope (D-13):** the control and content tracks are blocking here; the music view is 13-15 step 7b, recorded as pass, gap or deferred and never blocking | | Frontend bundle actually shipped | AIUI-05 | Node `assets/` is a never-pruned graveyard — a disk grep reports "deployed" before the deploy | Resolve live chunks via `sw.js`, fetch over HTTP, grep the **fetched** bytes for the new string | | Confirm dialog is un-spoofable by the iframe | AIUI-04 / D-11 | Anti-spoofing is a visual/trust property of the host chrome | Verify the dialog renders outside the iframe, Teleports to body, full-screen backdrop, text drawn from the node's description — not model-authored | | Routstr pays a live request | D-04 / D-05 | Research confidence on the Routstr protocol is MEDIUM — cited from docs, never run against a live provider | Spike against a real provider before the integration is trusted; budget ceiling must hard-stop | --- +## Edge-Probe Reconciliation + +The audit trail for the deterministic edge probe, counted against the plan files rather than +asserted. An earlier summary claimed "5 truths + 4 unclassified = 9, nothing dropped"; that +total was right by coincidence and wrong by composition, because it omitted the backstop scalar +and silently absorbed three planner-authored edges into the probe's own count. The real numbers: + +| Line | Count | Where | +|------|-------|-------| +| Requirements probed | 6 | AIUI-01 … AIUI-06 | +| Probes resolved `covered` | 2 | AIUI-01, AIUI-03 | +| Probes returned `unclassified` — flagged, never auto-resolved and never auto-backstopped | 4 | AIUI-02 → 13-05 · AIUI-04 → 13-09 · AIUI-05 → 13-09 · AIUI-06 → 13-15 | +| Probe-surfaced findings authored as covered truths | 5 | 1 in 13-01 (AIUI-01 concurrency) · 4 in 13-06 (AIUI-03 adjacency, empty, ordering, concurrency) | +| Probe-surfaced findings authored as `verification: backstop` scalars | 1 | 13-01 — the two-tab confirmation-nonce case | +| **Probe findings total** | **6 covered + 4 unclassified = 10** | 5 truths + 1 backstop + 4 flagged | +| Planner-authored edge truths — **not** probe output | 3 | 13-07 — concurrency, ordering and empty re-applied to the persisted music index, tagged `— authored, not probe-surfaced` | +| **Edge-tagged truths across all plans** | **8** | 1 (13-01) + 4 (13-06) + 3 (13-07) | +| **Edge entries across all plans, incl. the backstop scalar** | **9** | the 8 above + 13-01's backstop | + +Two numbers are easy to conflate and are deliberately kept apart here: **10** probe findings +(what the probe produced) and **9** edge entries in the plan files (what was written, including +three authored edges the probe never surfaced and excluding the four unclassified probes, which +are prose in `` rather than truths). Nothing was dropped in either +direction — every one of the 6 probes is accounted for, and every edge-tagged truth states +whether it came from the probe or from the planner. + +Also verified and unchanged: the 4 `unclassified` entries sit under `` and +are never promoted to `must_haves.truths`; the 3 prohibitions in 13-08, 13-12 and 13-14 are +flat scalars under `prohibitions`, never under `truths`, and carry no `check_*` keys. + +--- + ## Open Questions Blocking Full Validation Carried from `13-RESEARCH.md` § Open Questions — each needs a planner decision, and two change what "validated" even means: