--- phase: 01-federation-mesh-hardening plan: 02 type: execute wave: 1 depends_on: [] files_modified: - neode-ui/mock-backend.js - neode-ui/scripts/mock-rpc-parity.mjs - neode-ui/package.json autonomous: true requirements: [FED-04] must_haves: truths: - "Every mesh.* and federation.* RPC method the neode-ui frontend calls has a matching handler in mock-backend.js — the demo never answers a UI call with 'Method not found'" - "Renaming a mesh peer on the demo persists: mesh.contacts-save then mesh.contacts-list returns the saved alias, mirroring the daemon's handle_mesh_contacts_save/list behavior" - "A reaction, reply, edit, delete, or forward performed on the demo mutates the demo message store and is visible on the next mesh.messages read — it is not a bare ok acknowledgement" - "The demo's transport decision for an attachment matches the daemon's size tiers (auto under 1024 bytes, chooser in the 1024..2300 band, tor-only above 2300) — no demo-only chooser modal" - "An automated parity check fails when a UI-called mesh.*/federation.* method has no mock-backend handler, so the gap class is caught before manual demo testing" prohibitions: - statement: "The demo/mock backend MUST NOT gain behavior that diverges from the real daemon — it must never invent a demo-only modal, a demo-only response shape, or a success path a real node does not produce; every mirrored handler cites the daemon source file and line range it mirrors" category: transparency artifacts: - path: neode-ui/scripts/mock-rpc-parity.mjs provides: "Static UI-call vs mock-handler cross-reference plus a live RPC smoke sequence" min_lines: 60 - path: neode-ui/mock-backend.js provides: "mesh.contacts-list/save, stateful message-mutation handlers, and the 10 previously-missing UI-called methods" contains: "mesh.contacts-list" key_links: - from: neode-ui/scripts/mock-rpc-parity.mjs to: neode-ui/mock-backend.js via: "spawns mock-backend.js on MOCK_BACKEND_PORT and posts a scripted JSON-RPC sequence" pattern: "MOCK_BACKEND_PORT" --- Finish demo/real mesh parity: the demo backend answers every mesh and federation RPC the UI calls, and the message-mutation calls actually mutate demo state instead of returning a bare acknowledgement. Purpose: FED-04. Attachment-send parity already landed on main (`c2ce71c6`) — `mesh.send-content-inline` / `mesh.send-content` / `mesh.fetch-content` / `mesh.transport-advice` now mirror the daemon's tier logic. RESEARCH.md and a fresh cross-reference of `neode-ui/src/**` against `mock-backend.js` show what remains: **12 methods the UI calls that have no case at all** (they fall through to a `Method not found` error the frontend swallows in `try/catch`), and **six ack-only stubs** that never touch the demo message store, so reactions/edits/deletes silently do not render on the demo. Output: those gaps closed, plus a repeatable parity harness so this class of drift is caught by a command instead of by squinting at the browser console. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-federation-mesh-hardening/01-RESEARCH.md @.planning/phases/01-federation-mesh-hardening/01-PATTERNS.md @neode-ui/mock-backend.js @core/archipelago/src/api/rpc/mesh/typed_messages.rs ## Artifacts this phase produces Created or changed by **this plan**: | Symbol | Kind | File | |---|---|---| | `neode-ui/scripts/mock-rpc-parity.mjs` | new node script (static cross-reference + live smoke) | new file | | `test:mock-parity` | npm script | `neode-ui/package.json` | | `MOCK_BACKEND_PORT` | env var override for the mock's listen port | `neode-ui/mock-backend.js` | | `mesh.contacts-list`, `mesh.contacts-save` | new mock RPC cases | `neode-ui/mock-backend.js` | | `mesh.clear-all`, `mesh.schedule-message`, `mesh.list-scheduled`, `mesh.cancel-scheduled`, `mesh.assistant-status`, `mesh.assistant-configure` | new mock RPC cases | same | | `federation.nodes`, `federation.dwn-status`, `federation.notify-did-change`, `federation.cancel-request` | new mock RPC cases | same | | `store.mesh.contacts`, `store.mesh.scheduled` | new per-session mock store buckets | same | Task 1: End-to-end — alias a mesh peer on the demo and it sticks, proven by a parity harness neode-ui/mock-backend.js, neode-ui/scripts/mock-rpc-parity.mjs, neode-ui/package.json - `neode-ui/mock-backend.js` lines 4300-4500 — the `mesh.transport-advice` case and the comment block above it (the house convention: mirror the daemon and cite the source file), the `mesh.send-content-inline` case for how a handler mutates `currentStore().mesh.dynamic`, and the ack-only stub block at the end of the mesh cases. - `neode-ui/mock-backend.js` lines 5495-5530 — the per-session store shape (`mesh: { dynamic: [], blobs: {} }`) and `currentStore()`. - `neode-ui/mock-backend.js` lines 80-90 and 5710-5730 — the `PORT` constant and the `server.listen` call. - `core/archipelago/src/api/rpc/mesh/typed_messages.rs` — `handle_mesh_contacts_list` (from L1218) and `handle_mesh_contacts_save` (from L1253): the real merge of `state.contacts` (a `ContactEntry` map with `alias`, `notes`, `pinned`, `blocked`) over `state.peers`, and the exact response shape the UI consumes. - `neode-ui/src/api/rpc-client.ts` lines 795-820 — the `mesh.contacts-list` / `mesh.contacts-save` wrappers and their params shape. - `neode-ui/src/views/Mesh.vue` — the two call sites (on mount, and on peer rename) to confirm which response fields are read. Add a `contacts` bucket (a plain object keyed by peer contact id) to the per-session mock store alongside the existing `dynamic` and `blobs` keys. Implement `mesh.contacts-save`: accept the same params the daemon's handler takes, upsert `{ alias, notes, pinned, blocked }` for the given peer key into the session `contacts` bucket, and return the same result shape the real handler returns. Implement `mesh.contacts-list`: merge the session `contacts` bucket over the demo's `mesh.peers` list exactly as the daemon merges `state.contacts` over `state.peers`, and return the same field names. Follow the house convention already used above `mesh.transport-advice`: a comment naming `typed_messages.rs handle_mesh_contacts_list` / `handle_mesh_contacts_save` as the source of truth, so a future reader knows where to re-check parity. Change the hardcoded listen port to read an env override first, defaulting to the existing value, so a harness can bind an ephemeral port without colliding with a running dev preview. Use the env var name `MOCK_BACKEND_PORT`. Create `neode-ui/scripts/mock-rpc-parity.mjs` with two stages and a non-zero exit on any failure: (1) STATIC — scan `neode-ui/src/**` for every `'mesh.'` / `'federation.'` string literal, scan `mock-backend.js` for every `case '':`, and report methods called by the UI with no mock case. Print the offending method names. (2) LIVE — spawn `node mock-backend.js` with `MOCK_BACKEND_PORT` set to a free port, poll `/rpc/v1` until ready (bounded ~10s), then POST a scripted JSON-RPC sequence and assert on the responses: `mesh.contacts-save` with an alias, then `mesh.contacts-list` returns that alias for that peer. Kill the child in a `finally` block. Do not use `|| echo`-style fallbacks anywhere in the script or its npm wiring — a failed spawn, a failed fetch, or a missing field must propagate as a non-zero exit, never a passing run that measured nothing. Register it as the `test:mock-parity` npm script in `neode-ui/package.json`. In this task the STATIC stage is expected to still report the other missing methods; make it print them and exit non-zero only when the LIVE stage fails or when a method from an explicit `KNOWN_GAPS` array is missing. Task 2 empties `KNOWN_GAPS` to zero entries. cd neode-ui && node --check mock-backend.js && node scripts/mock-rpc-parity.mjs - `cd neode-ui && node --check mock-backend.js` exits 0. - `cd neode-ui && node scripts/mock-rpc-parity.mjs` exits 0 and its output contains the alias round-trip assertion result. - `grep -c "case 'mesh.contacts-list'" neode-ui/mock-backend.js` equals 1. - `grep -c "case 'mesh.contacts-save'" neode-ui/mock-backend.js` equals 1. - `grep -c 'MOCK_BACKEND_PORT' neode-ui/mock-backend.js` is at least 1. - `grep -c 'typed_messages.rs' neode-ui/mock-backend.js` is at least 2 (the pre-existing transport-advice citation plus the new contacts citation). - `node -e "process.exit(require('./neode-ui/package.json').scripts['test:mock-parity'] ? 0 : 1)"` exits 0. - Killing the harness leaves no stray listener: `cd neode-ui && node scripts/mock-rpc-parity.mjs && node scripts/mock-rpc-parity.mjs` exits 0 twice in a row. Peer aliasing works end-to-end on the demo and a single command proves it, with the remaining method gaps enumerated by name. Task 2: Close the remaining ten UI-called methods with no mock handler neode-ui/mock-backend.js, neode-ui/scripts/mock-rpc-parity.mjs - The STATIC-stage output from Task 1 — the authoritative live list. As of planning it is: `mesh.clear-all`, `mesh.schedule-message`, `mesh.list-scheduled`, `mesh.cancel-scheduled`, `mesh.assistant-status`, `mesh.assistant-configure`, `federation.nodes`, `federation.dwn-status`, `federation.notify-did-change`, `federation.cancel-request`. - `core/archipelago/src/api/rpc/dispatcher.rs` lines 390-440 — the real handler names each of these methods dispatches to, so the mock mirrors the right handler. - `neode-ui/mock-backend.js` — the existing `federation.list-nodes`, `federation.list-pending-requests`, `federation.approve-request`, and `federation.reject-request` cases, for the response shapes the sibling federation methods must match. Add a case for each remaining method, mirroring the real handler's response shape (read the Rust handler named by the dispatcher before writing each one) and citing it in a comment the way the contacts handlers do. Behavioral requirements, not bare acknowledgements: `mesh.clear-all` empties the session `dynamic` message array; `mesh.schedule-message` pushes into a new session `scheduled` bucket and returns the created entry's id; `mesh.list-scheduled` returns that bucket; `mesh.cancel-scheduled` removes by id and reports whether an entry was actually removed; `federation.nodes` returns the same node array `federation.list-nodes` returns (the UI treats them as aliases); `federation.cancel-request` removes the request from the pending-requests bucket the existing approve/reject cases operate on. Then set the harness's `KNOWN_GAPS` array to empty so the STATIC stage exits non-zero on ANY UI-called method without a mock case, and extend the LIVE stage with one assertion per newly stateful method that has observable state: schedule a message then list it and assert it is present; cancel it and assert it is gone; clear-all then read `mesh.messages` and assert the dynamic messages are gone. cd neode-ui && node --check mock-backend.js && node scripts/mock-rpc-parity.mjs - `cd neode-ui && node scripts/mock-rpc-parity.mjs` exits 0 with zero reported missing methods. - `cd neode-ui && grep -c 'KNOWN_GAPS' scripts/mock-rpc-parity.mjs` is at least 1 and the array literal it is assigned is empty. - Each of the ten method names appears exactly once as a `case '':` in `mock-backend.js`. - Deliberately deleting one `case` line makes `node scripts/mock-rpc-parity.mjs` exit non-zero (fail-first proof); restore the line afterward and record the check in the SUMMARY. The demo answers every mesh and federation RPC the UI calls, and the parity harness is proven to fail when it does not. Task 3: Make the message-mutation stubs mutate demo state neode-ui/mock-backend.js, neode-ui/scripts/mock-rpc-parity.mjs - `neode-ui/mock-backend.js` — the ack-only stub block covering `mesh.send-reaction`, `mesh.send-reply`, `mesh.send-read-receipt`, `mesh.edit-message`, `mesh.delete-message`, `mesh.forward-message`, `mesh.send-channel` (currently a shared bare-acknowledgement case), and the `mesh.send-content-inline` case above it for the message-object shape pushed into `currentStore().mesh.dynamic`. - `core/archipelago/src/api/rpc/mesh/typed_messages.rs` lines 637-976 — reply / reaction / read-receipt / forward handlers, and lines 1065-1180 — edit / delete. Note the stable `sender_pubkey` + `sender_seq` message key these operate on, not the local `id`. - `core/archipelago/src/mesh/types.rs` lines 139-180 — the `MeshMessage` field set the demo objects must match (`id`, `direction`, `peer_contact_id`, `peer_name`, `plaintext`, `timestamp`, `delivered`, `encrypted`, `transport`, `message_type`, `typed_payload`, `sender_pubkey`, `sender_seq`). - `neode-ui/src/views/Mesh.vue` and `neode-ui/src/stores/mesh.ts` — how the UI reads reactions, edited text, and deleted markers, so the mutated shape is the one that renders. Split the shared acknowledgement case into individual cases that mutate `currentStore().mesh.dynamic`: `mesh.send-reaction` — locate the target message by the same key the daemon uses and append or toggle the emoji in its reactions collection. `mesh.send-reply` — push a new message whose payload carries the replied-to message key, so the UI renders the quote block. `mesh.send-read-receipt` — mark the target message read. `mesh.edit-message` — replace the target's text and set the edited marker the UI reads. `mesh.delete-message` — apply the same deletion representation the daemon applies (tombstone marker vs removal — read the handler and mirror it, do not choose independently). `mesh.forward-message` — push a copy addressed to the destination peer. `mesh.send-channel` — push a channel-addressed message. Leave `mesh.refresh` and `mesh.reboot-radio` as acknowledgements — the daemon's handlers have no message-store effect either, so mirroring means leaving them alone. Add a comment on that pair stating why they remain acknowledgements, so a later reader does not "fix" them into divergence. Extend the harness's LIVE stage: send a message, react to it, and assert `mesh.messages` shows the reaction; edit it and assert the text changed and the edited marker is set; delete it and assert the daemon-matching representation; forward it and assert a copy exists for the destination peer. cd neode-ui && node --check mock-backend.js && node scripts/mock-rpc-parity.mjs - `cd neode-ui && node scripts/mock-rpc-parity.mjs` exits 0 with the reaction, edit, delete, and forward assertions all reported as passing. - `grep -c "case 'mesh.send-reaction':" neode-ui/mock-backend.js` equals 1 and that case is no longer part of a shared fall-through group with `mesh.refresh`. - `grep -c 'typed_messages.rs' neode-ui/mock-backend.js` is at least 4 (each mirrored family cites its daemon source). - `cd neode-ui && npm run build` exits 0 (the mock is dev-only, but the build must not regress). Reactions, replies, edits, deletes, and forwards render on the demo exactly as on a real node, proven by the live harness. ## Planner Assumptions (flagged, unresolved) - **FED-04 / spec-less probe, category `unclassified`:** the probe could not classify an edge for FED-04, and no acceptance criterion was invented for it. The parity harness covers the *known* drift class (missing handler, non-mutating handler); it does NOT cover response-shape drift where a mock case exists and returns a differently-shaped success object than the daemon. That residual class is surfaced here rather than silently dropped, and is a candidate finding for the FED-03 review in plan 01-07. ## Trust Boundaries | Boundary | Description | |----------|-------------| | browser → mock backend `/rpc/v1` | Developer-local demo surface; accepts unauthenticated JSON-RPC on a loopback-bound dev port | | harness child process → mock backend | The parity script spawns and drives the mock on an ephemeral port | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-01-05 | Spoofing | mock backend impersonating real daemon behavior in a way that hides a real-node bug | medium | mitigate | Every mirrored handler cites the daemon file and line range it mirrors; the parity harness asserts observable state transitions, not acknowledgements | | T-01-06 | Information Disclosure | mock backend binding a non-loopback interface on a developer machine | low | accept | Pre-existing `0.0.0.0` bind is unchanged by this plan; the mock serves only synthetic demo data and ships in no release artifact | | T-01-07 | Tampering | the parity harness leaving an orphaned server process holding a port | low | mitigate | The child is killed in a `finally` block and the acceptance criteria require two consecutive clean runs | | T-01-SC | Tampering | npm/pip/cargo installs | high | mitigate | No packages are added by this plan — the harness uses only Node built-ins (`node:child_process`, `fetch`, `node:fs`). If any dependency becomes necessary, stop and run the Package Legitimacy Gate before installing | - `cd neode-ui && node scripts/mock-rpc-parity.mjs` — green, zero missing methods. - `cd neode-ui && npm run build` — green. - Fail-first proof recorded: deleting a `case` line makes the harness exit non-zero. - Zero mesh.*/federation.* methods called by the UI lack a mock handler. - Peer aliasing, reactions, replies, edits, deletes, and forwards all change demo state and render. - A single command reproduces the parity verdict and is proven fail-first. Create `.planning/phases/01-federation-mesh-hardening/01-02-SUMMARY.md` when done. Commit staged by explicit path only (a second agent shares this tree), then `git push gitea-ai main`.