---
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.