Files
archy/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-15-PLAN.md
T

285 lines
22 KiB
Markdown

---
phase: 13-aiui-functional-conversational-node-control-and-content-surf
plan: 15
type: execute
wave: 8
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
autonomous: false
requirements: [AIUI-06]
must_haves:
truths:
- "The phase is verified on archi-dev-box in the real embedded iframe, desktop and mobile — not only in the local dev:mock loop (AIUI-06)"
- "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"
- path: ".planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md"
provides: "Per-Task Verification Map with Task ID / Plan / Wave / Threat Ref filled and every row statused"
contains: "nyquist_compliant"
key_links:
- from: ".planning/phases/13-.../13-VALIDATION.md"
to: ".planning/phases/13-.../13-UAT.md"
via: "each manual-only row cites the UAT section that discharged it"
pattern: "13-UAT"
---
<objective>
Close the phase against a real device.
AIUI-06 is a real acceptance gate, not a formality: verified on archi-dev-box **in the real
embedded iframe, mobile included** — not only in the local `dev:mock` loop. The `dev:mock` loop
does not reproduce the embed context, and several of this phase's properties only exist there:
the postMessage bridge's origin check, the CSP that makes the sandbox an enforced boundary, the
confirm modal rendering outside the iframe, and the deploy path that decides which bundle is
actually running.
Two verification traps this plan must not fall into. First, a green `cargo test` proves nothing
about the deployed surface — the model-proxy closure (S-15) is only real when `curl` says so
against a running node. Second, the node's `assets/` directory is a never-pruned graveyard: a
disk grep reports "deployed" before the deploy, because a dead chunk from an older build still
contains the string. Live chunks are resolved through the service worker manifest and fetched
over HTTP.
This plan also discharges `13-VALIDATION.md`, whose Per-Task Verification Map still carries TBD
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>
<flagged_assumptions>
**FLAGGED — unresolved edge probe, AIUI-06, category `unclassified`.** Not auto-resolved and not
auto-backstopped. Surfaced for a human read: AIUI-06 says "verified on device, in the real
embedded iframe on archi-dev-box, mobile included" but does not say whether "mobile" means a real
phone or a mobile viewport in desktop devtools. This plan requires **both** — a devtools mobile
viewport for layout, and at least one real handheld for touch, the on-screen keyboard and the
audio player — because the two catch different bugs and the phase's own history (the modal
Teleport rule, the `.local` https/mDNS problem on Android) shows the difference matters. If only
one was intended, say which; do not silently drop the other.
</flagged_assumptions>
<artifacts_this_phase_produces>
Symbols created by **this plan**: none in source. This plan produces two planning documents —
`13-UAT.md` (new) and the completed `13-VALIDATION.md` — and changes no code in either
repository.
</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-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-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>
<task type="auto">
<name>Task 1: Run everything, then fill the validation map from what actually ran</name>
<files>.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md</files>
<read_first>
- `.planning/phases/13-.../13-VALIDATION.md` in full — the Per-Task Verification Map's eleven rows with their TBD columns, the Wave 0 Requirements checklist, the Manual-Only table, the four Open Questions, and the Validation Sign-Off checklist.
- Every `13-NN-SUMMARY.md` produced so far — the authority for which task in which plan and wave discharged each row, and for the threat ids each one mitigated.
- `.planning/phases/13-.../13-AI-SPEC.md` §5's structural-invariant table S-01…S-15 — every one of these needs a named passing test or, for S-15, a recorded `curl` result.
- `CLAUDE.md`'s build gotcha: on `rust-lld: undefined hidden symbol`, rebuild with the incremental cache disabled. That is cache corruption, not a real failure — do not report it as a red test.
</read_first>
<action>
Run the complete automated surface across all three test frameworks and record the raw results before editing anything:
the Rust suite from `core/`, the neode-ui Vitest suite from `neode-ui/`, and AIUI's own Vitest suite from `/home/archipelago/Projects/AIUI/packages/app` (its command is `vitest run` — confirmed at plan time, which discharges `13-VALIDATION.md`'s "confirm AIUI's test command" Wave 0 item).
Then fill `13-VALIDATION.md`'s Per-Task Verification Map: for each of its eleven rows, set Task ID, Plan and Wave from the summaries, set Threat Ref to the `T-13-NN` id(s) from the owning plan's threat register, set File Exists to reflect reality, and set Status to green, red or flaky based on the run you just did — not on what the plan intended. A row nothing discharged is marked red and named in the summary; do not quietly mark it green.
Add rows for anything the phase produced that the seeded map did not anticipate — at minimum the S-01…S-15 structural invariants and the eighteen eval cases — so the map is a complete picture rather than the research-time subset.
Tick the Wave 0 Requirements checklist against reality: the assistant module and its tests, `toolConfirm.test.ts`, `archyContentAdapter.test.ts`, `scripts/build-aiui.sh`, AIUI's confirmed test command, and `contextBroker.test.ts` / `chatAiuiEmbed.test.ts` still green.
Update the four Open Questions with the answers the phase actually reached, each citing the plan that settled it: the port-3142 proxy (13-02, delete-and-replace with a session-gated Rust forwarder), the iframe sandbox mechanism (13-09, a `/aiui/`-scoped CSP plus G-B3, with `sandbox` rejected and the residual named), Routstr protocol accuracy (13-03's findings and 13-13's entry decision), and RBAC integration (13-01, a single `assistant.` prefix arm so the existing `role.can_access` gate applies unchanged before dispatch).
Set `nyquist_compliant` in the frontmatter to `true` only if every row has an automated verify or a discharged manual entry and no three consecutive tasks lack an automated verify. If that is not true, leave it `false` and name the gap — a validation document that claims compliance it does not have is worse than one that does not claim it.
</action>
<verify>
<automated>cd core &amp;&amp; CARGO_INCREMENTAL=0 cargo test --package archipelago 2>&amp;1 | tail -20</automated>
<automated>cd neode-ui &amp;&amp; npx vitest run 2>&amp;1 | tail -20</automated>
<automated>cd /home/archipelago/Projects/AIUI/packages/app &amp;&amp; npx vitest run 2>&amp;1 | tail -20</automated>
<automated>grep -c 'TBD' .planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-VALIDATION.md</automated>
</verify>
<acceptance_criteria>
- `cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago` exits 0
- `cd neode-ui && npx vitest run` exits 0
- `cd /home/archipelago/Projects/AIUI/packages/app && npx vitest run` exits 0
- `grep -c 'TBD' 13-VALIDATION.md` returns 0 — every Task ID, Plan, Wave and Threat Ref column is filled
- Every row in the Per-Task Verification Map has a Status that is not `pending`
- The map contains a row for each of S-01 through S-15, each citing a named passing test or, for S-15, the recorded `curl` status codes
- All six Wave 0 Requirements checkboxes are ticked, or an unticked one is named as an open gap in the summary
- Each of the four Open Questions has an answer citing the plan number that settled it
- `nyquist_compliant` is `true`, or it is `false` with the specific gap named
</acceptance_criteria>
<done>Every validation row is owned, statused from a real run, and traceable to the plan and threat that discharged it.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: The real embedded iframe, on real hardware, desktop and mobile</name>
<files>.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-UAT.md</files>
<read_first>
- `.planning/phases/13-.../13-VALIDATION.md` "Manual-Only Verifications" — the four entries this checkpoint discharges: the embedded iframe on device, the frontend bundle actually shipping, the confirm dialog being un-spoofable, and Routstr paying a live request.
- `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 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`
and **grep the built bundle** for a new string before shipping, then deploy to archi-dev-box per
`CLAUDE.md`. Confirm the deploy with `bash scripts/verify-aiui-deploy.sh <node> "<new marker>"`
and `bash tests/production-quality/aiui-proxy-closed.sh <node>`. Do not proceed on a deploy you
have not confirmed by fetching bytes.
**Desktop, in the real embedded iframe (not `dev:mock`):**
1. Open neode-ui's Chat view. AIUI renders — not a black page.
2. With all categories closed, ask "how much space is left". The assistant reports it cannot,
naming the category to open. Nothing is fabricated.
3. Open the `system` category. Ask again. A real free-space figure comes back matching
`system.disk-status`.
4. Ask a settings question, then make an allowlisted settings change conversationally. Confirm
the dialog appears and the change lands.
5. Ask to restart a specific app. Confirm the dialog renders **outside** the iframe with a
full-screen backdrop, names the app verbatim, and states the effect. Deny — nothing happens.
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 — 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
paid request succeeds and that exhausting the allowance stops with an explanation and no
overspend. If 13-13 deferred it, record that instead.
**Mobile — both a devtools mobile viewport and at least one real handheld** (reach the node by IP
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 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.
Record every step's result in `13-UAT.md`, including the hardware and browser used, the node
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 — 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 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>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| built bundle → deployed bundle | Where a silent no-op build or a stale chunk makes verification lie |
| source tests → deployed surface | A green `cargo test` says nothing about what nginx is serving |
| desktop verification → mobile reality | Different layout engine, different input, different network path to the node |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-13-101 | Repudiation | Verifying a bundle that was never deployed, because the asset graveyard still contains the string | high | mitigate | `verify-aiui-deploy.sh` resolves live chunks through the service worker manifest and greps fetched bytes; the deploy is not accepted until it exits 0 |
| T-13-102 | Elevation of Privilege | The model proxies reopening through a config drift or a partial deploy | **critical** | mitigate | `aiui-proxy-closed.sh` is re-run against the deployed node as part of sign-off, not only at 13-02 time. S-15 is a deployed-surface check by definition |
| T-13-103 | Elevation of Privilege | The CSP boundary present in the repo but absent on the node | high | mitigate | Step 9 exercises it per-frame in a real browser on the real node — the only place the boundary is actually observed |
| T-13-104 | Repudiation | Declaring the phase done from `dev:mock`, where the embed context does not exist | high | mitigate | AIUI-06 requires the real embedded iframe; the flagged assumption above requires both a devtools viewport and a real handheld, because they catch different bugs |
| 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>
<verification>
- 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 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 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>
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-15-SUMMARY.md` when done
</output>