Files
archy/.planning/phases/01-federation-mesh-hardening/01-02-PLAN.md
T

19 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-federation-mesh-hardening 02 execute 1
neode-ui/mock-backend.js
neode-ui/scripts/mock-rpc-parity.mjs
neode-ui/package.json
true
FED-04
truths prohibitions artifacts key_links
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
statement category
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 transparency
path provides min_lines
neode-ui/scripts/mock-rpc-parity.mjs Static UI-call vs mock-handler cross-reference plus a live RPC smoke sequence 60
path provides contains
neode-ui/mock-backend.js mesh.contacts-list/save, stateful message-mutation handlers, and the 10 previously-missing UI-called methods mesh.contacts-list
from to via pattern
neode-ui/scripts/mock-rpc-parity.mjs neode-ui/mock-backend.js spawns mock-backend.js on MOCK_BACKEND_PORT and posts a scripted JSON-RPC sequence 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.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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.<verb>'` / `'federation.<verb>'` string
literal, scan `mock-backend.js` for every `case '<method>':`, 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.

<threat_model>

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
</threat_model>
- `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.

<success_criteria>

  • 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. </success_criteria>
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`.