Archipelago — open-source initial import
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
## 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 |
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: End-to-end — alias a mesh peer on the demo and it sticks, proven by a parity harness</name>
|
||||
<files>neode-ui/mock-backend.js, neode-ui/scripts/mock-rpc-parity.mjs, neode-ui/package.json</files>
|
||||
<read_first>
|
||||
- `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.
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd neode-ui && node --check mock-backend.js && node scripts/mock-rpc-parity.mjs</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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.
|
||||
</acceptance_criteria>
|
||||
<done>Peer aliasing works end-to-end on the demo and a single command proves it, with the remaining method gaps enumerated by name.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Close the remaining ten UI-called methods with no mock handler</name>
|
||||
<files>neode-ui/mock-backend.js, neode-ui/scripts/mock-rpc-parity.mjs</files>
|
||||
<read_first>
|
||||
- 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.
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd neode-ui && node --check mock-backend.js && node scripts/mock-rpc-parity.mjs</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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 '<method>':` 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.
|
||||
</acceptance_criteria>
|
||||
<done>The demo answers every mesh and federation RPC the UI calls, and the parity harness is proven to fail when it does not.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Make the message-mutation stubs mutate demo state</name>
|
||||
<files>neode-ui/mock-backend.js, neode-ui/scripts/mock-rpc-parity.mjs</files>
|
||||
<read_first>
|
||||
- `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.
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd neode-ui && node --check mock-backend.js && node scripts/mock-rpc-parity.mjs</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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).
|
||||
</acceptance_criteria>
|
||||
<done>Reactions, replies, edits, deletes, and forwards render on the demo exactly as on a real node, proven by the live harness.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## 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>
|
||||
|
||||
<verification>
|
||||
- `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.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
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`.
|
||||
</output>
|
||||
Reference in New Issue
Block a user