fix(13): make D-13 track independence real in the wave graph
Plan-checker revision iteration 1 — 1 blocker + 2 warnings. BLOCKER (context_compliance, D-13): the music track blocked phase completion despite being locked as non-blocking. 13-15 depended on 13-11, which chains back through 13-07 to 13-04, so the phase could not close without the entire music chain. Took the checker's option (b): 13-15 depends_on is now ["13-06","13-09","13-14"] — 13-06 added so the content-grid check stays a real gate, 13-11 dropped so no path reaches 13-04/13-07/13-11. UAT step 7 is now content-only and blocking; new step 7b is the music view as record-and-defer, the same shape step 10 already used for Routstr. Verified: 13-15's transitive closure contains no music plan. WARNING (scope_reduction): T-13-32 claimed the filebrowser-client.ts JWT-in-query-string leak was "fixed" while only guaranteeing it was not propagated. Now actually fixed — streamUrl returns a query-free same-origin URL and relies on the path=/ cookie login() already sets; filebrowser-client.ts and a new regression test are in 13-06's files_modified. T-13-32 is scoped to new code; new T-13-39 owns the pre-existing leak and names the residual (the JWT is still 24h, now confined to the cookie jar). WARNING (verification_derivation): the edge-probe reconciliation did not match the files. Corrected in 13-VALIDATION.md — 10 probe findings vs 9 edge entries kept apart, 13-07's 3 truths retagged as authored rather than probe-surfaced. No truths deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
203966030b
commit
60625499ae
@@ -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.
|
||||
|
||||
+9
-2
@@ -112,8 +112,15 @@ Goes red if a future phase reintroduces the mesh-only assumption.
|
||||
</assumption_delta_decision>
|
||||
|
||||
<flagged_assumptions>
|
||||
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.
|
||||
</flagged_assumptions>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
|
||||
+56
-13
@@ -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`.
|
||||
</objective>
|
||||
|
||||
<flagged_assumptions>
|
||||
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.
|
||||
</flagged_assumptions>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
@@ -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<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`.
|
||||
@@ -111,7 +136,7 @@ Unchanged by design and therefore **not** new symbols: `FilmGrid.vue`, `SongGrid
|
||||
|
||||
<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</files>
|
||||
<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.
|
||||
@@ -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.
|
||||
</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` 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.
|
||||
</read_first>
|
||||
@@ -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/<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 an assertion in the test that no produced URL matches `[?&](auth|token)=`.
|
||||
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.
|
||||
|
||||
@@ -147,19 +182,24 @@ Write the tests FIRST in `archyContentAdapter.test.ts`, one per `<behavior>` bul
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd neode-ui && npx vitest run src/composables/__tests__/archyContentAdapter.test.ts</automated>
|
||||
<automated>cd neode-ui && npx vitest run src/api/__tests__/filebrowserStreamUrl.test.ts</automated>
|
||||
<automated>cd neode-ui && npx vitest run src/components/__tests__/MediaLightboxPip.test.ts</automated>
|
||||
<automated>cd neode-ui && 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 -cE '[?&](auth|token)=' neode-ui/src/composables/archyContentAdapter.ts` returns 0 — the `filebrowser-client.ts` leak is not propagated
|
||||
- The test file contains an assertion that no adapter-produced URL matches `[?&](auth|token)=`
|
||||
- `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.</done>
|
||||
<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">
|
||||
@@ -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
|
||||
</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; 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.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
+12
-3
@@ -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
|
||||
|
||||
<flagged_assumptions>
|
||||
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.
|
||||
</flagged_assumptions>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
|
||||
+9
-2
@@ -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
|
||||
|
||||
+53
-15
@@ -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.
|
||||
</objective>
|
||||
|
||||
@@ -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.
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
@@ -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.
|
||||
</read_first>
|
||||
<what-built>
|
||||
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.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
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.
|
||||
</how-to-verify>
|
||||
<acceptance_criteria>
|
||||
- `bash scripts/verify-aiui-deploy.sh <node> "<marker>"` exits 0 and `bash tests/production-quality/aiui-proxy-closed.sh <node>` 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`
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Type "approved" with the fifteen step results and the handheld model, or list which steps failed and what you saw.</resume-signal>
|
||||
<resume-signal>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.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -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 |
|
||||
</threat_model>
|
||||
|
||||
@@ -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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
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.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
+33
-1
@@ -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 `<flagged_assumptions>` 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 `<flagged_assumptions>` 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:
|
||||
|
||||
Reference in New Issue
Block a user