diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-02 10:20:09 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-02 10:20:09 +0200 |
| commit | e4f61771131be635b9e81a19203a00707b4b19df (patch) | |
| tree | d80e4edafbeade3c27137e6753140e6585a26b9b /docs | |
| parent | e941cc4c39c38a12220153ea572bd4c7bb92fde0 (diff) | |
| download | meshbay-e4f61771131be635b9e81a19203a00707b4b19df.tar.gz | |
feat(mnp): sharing a folder is decided on the node's machine only (MNP 6.0)
root_add, root_update and group_attach leave MNP: adding a directory and
switching writable/removable go through the loopback API (native dialog in
the desktop app) or the CLI. The operator's Settings tab still lists the
roots from any browser, read-only. The desktop app refuses to sign those
ops; a loopback flag change now reaches open pages (publish_roots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 21 | ||||
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 38 | ||||
| -rw-r--r-- | docs/USERGUIDE.md | 5 |
3 files changed, 49 insertions, 15 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index eec354a..e145ed4 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -16,7 +16,7 @@ > them — it names the invariant that holds today, not the incident that produced > it. §13 is the register of those labels. > -> Wire versions at the time of writing: **MNP 5.0** (oldest peer accepted 4.0), +> Wire versions at the time of writing: **MNP 6.0** (oldest peer accepted 4.0), > **MHP 0.1**, packages **0.17.0**. The normative source for the wire format is > `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol > serves, not its byte layout. @@ -99,7 +99,7 @@ opens them. │ └─────────┘ MHP 0.1 │ signalling (SDP/ICE, <1 KB), presence, revocation push MHP │ - ┌────┴────┐ MNP 5.0 ┌──────────┐ + ┌────┴────┐ MNP 6.0 ┌──────────┐ │ node │◄──────── WebRTC DataChannel / QUIC ──────────►│ client │ └─────────┘ index, file chunks, streams, chat, admin └──────────┘ holds the files browser SPA or desktop @@ -1451,8 +1451,8 @@ checks the version its peer declared and **branches on none of it**. **The floor is not necessarily the current version, and what is added above it is why.** It is `MNP_MIN_SUPPORTED` in `handshake.py`, and it is the last MAJOR that had to refuse at the handshake: 3.1–3.4 were added above the 3.0 floor without moving it, -and 5.0, a MAJOR confined to four signed operations that a peer across the break -refuses to sign, sits above the 4.0 floor. So a +and 5.0 and 6.0 — MAJORs confined to a few signed operations, four whose subjects +changed and then three removed — sit above the 4.0 floor. So a peer can be reachable and still not do something the current version can, and the client has to cope with that — **by reading the peer's own answer, never by comparing version numbers**. @@ -1541,7 +1541,17 @@ Five consequences, none optional: - `writable = true` means any group member may upload there. Several roots may be writable and none need be — a fully read-only group is valid. -The operator toggles this with a signed op. + +**What widens the sharing is decided on the node's own machine.** Adding a root, +hosting a group over a directory, and switching `writable` or `removable` go through +the loopback API (in the desktop application, behind a native dialog for anything +that shares a folder or opens one to writes) or the CLI — never over MNP, since 6.0. +A signed op proves that the operator's key signed, not that they meant it: in a +browser that key is driven by code the hub serves (T3), and in the desktop +application by a renderer that parses content from nodes. Either could otherwise +have shared any folder on the machine, writable, from anywhere. The operator still +sees every root and its flags from any browser; removing, ejecting and plugging stay +signed ops, because they narrow what is shared or restore what already was. > **There is one answer to "may this member write", and it is the root.** A single > flag over the group cannot express "this library is published read-only and that @@ -3802,6 +3812,7 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | **The Create group wizard calls two hooks after an early return** | `CreateGroupWizard` (`create-group-page.js`) returns during node detection, before its `useRef`/`useEffect` for provisioning, so the hook count changes between renders. Preact tolerates a list that grows, and nothing is known to break; `test_hook_ordering.py` checks declaration order, not this. Found while tracing the frozen-fields report, which had another cause (`ask.js`) | | **A node key is read from the terminal or the desktop client, never a browser** | **Accepted.** `meshbay-node status` on the node's own machine and Node → Overview in the desktop client are the two places the key can be read; the Node page is Electron-only, because `platform.node` resolves to "not available" without the bridge, and no hub route exposes the key. The create-group wizard links it automatically over that same bridge, so the manual paste in **Profile → Link Node** exists for the operator who runs the node from a terminal and the hub from a browser — who has a terminal by definition. Anyone linking a node is already at a shell prompt, so a browser-reachable copy would buy nothing and widen what the hub knows about the node | | **Listing a group's folders walks every root on the event loop** | `index_sync_message` (`transport/wire.py`) builds its `dirs` field with `list_dirs`, an `rglob("*")` over every root, and nothing sends it off the loop: the WebRTC `index_sync` handler, the daemon's index push and QUIC all call it inline. So each index request from any member is a directory walk of the whole library that every other peer on the node waits behind. `test_disk_io_off_loop.py` never saw it, because it reads the transport's own modules and the walk is one call away in `wire.py`. Found by widening what that test reads, not by a symptom | +| **A loopback eject or plug reaches open pages late** | `ops.eject_root` and `ops.plug_root` flip the live set and tell nobody; over MNP the broadcast `root_eject_ack` / `root_plug_ack` is what moves every open table. So an eject made from the desktop application or the CLI shows on members' pages only with the next index push — for a plug, the end of its rescan; for an eject, whatever changes next. `ops.update_root` had the same silence and now calls `DirectoryIndexer.publish_roots`; the same call belongs in these two. Found while moving the `writable`/`removable` switches to the loopback door (MNP 6.0) | | **The transcoded-seek test passes without transcoding** | `test_a_transcoded_video_keeps_accurate_seeking` (`test_stream_seek_audio_alignment.py`) forces the re-encode branch by swapping the module's `BROWSER_INCOMPATIBLE_VIDEO_CODECS`, then checks only that the result has no audio gap. The copy path also leaves no gap on that clip, so pointing the swap at a module the streaming code does not read still passes: the test cannot tell that the branch it is named after never ran. It should assert the re-encode happened (the `re-encoding` log line, or the encoder in the ffmpeg argv). Found by breaking the swap on purpose while moving the streaming code | --- diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index e7fce90..d03b7eb 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -1,6 +1,6 @@ # MeshBay Node Protocol (MNP) -**Wire version:** `5.0` — `meshbay_common/__init__.py` (`MNP_VERSION`) +**Wire version:** `6.0` — `meshbay_common/__init__.py` (`MNP_VERSION`) **Oldest peer accepted:** `4.0` — `handshake.py` (`MNP_MIN_SUPPORTED`) **Normative implementation:** `meshbay-common` (`protocol.py`, `handshake.py`, `groupbox.py`, `chatbox.py`, `adminop.py`, `join.py`, `device.py`, `crypto.py`, @@ -1165,9 +1165,7 @@ broadcast, every connected peer in the group learns the change without reconnect | `tmdb_override` | `file_id=..,tmdb_id=..,media_type=..` | operator | `tmdb_override_ack{file_id, tmdb_id, media_type}` | yes | | `tmdb_rematch` | `file_id=..` | operator | `tmdb_rematch_ack{file_id}` | yes | | `musicbrainz_enabled` | `enabled` | operator | `musicbrainz_enabled_ack{enabled}` | yes | -| `root_add` | `{path, name, kind, writable, removable}` | operator | `root_add_ack` | no | | `root_remove` | the root name | operator | `root_remove_ack` | no | -| `root_update` | `<root>:rw=on\|off,rem=on\|off` | operator | `root_update_ack` | yes | | `root_eject`, `root_plug` | the root name | operator | `root_eject_ack` / `root_plug_ack` | yes | | `app_directories` | `<app>:<dir>,<dir>,...` | operator | `app_directories_ack{app, dirs}` | yes | | `chat_directory` | the path | operator | `chat_directory_ack{path}` | yes | @@ -1175,14 +1173,28 @@ broadcast, every connected peer in the group learns the change without reconnect | `search_listed` | `on\|off` | operator | `search_listed_ack{listed}` | yes | | `transfer_limits` | `d=<n>,u=<n>` | operator | `transfer_limits_ack{limits}` | yes | | `chat_epoch` | `group_id` | operator | `chat_epoch_ack{epoch}` | yes | -| `group_attach` | `{name, shared_dir, writable}` | operator | `group_attach_ack` | no | | `group_detach` | the group name | operator | `group_detach_ack` | no | **Upload policy is not in this table**, and that is the design: whether a member may -write is a property of each root (`root_update`), not a switch over the group. A single +write is a property of each root (its `writable` flag), not a switch over the group. A single group-wide flag cannot express "this library is published read-only and that folder is a drop box", which is the ordinary arrangement. +**Nothing in this table widens what the node shares**, and that is the design too. +Adding a directory to a group, hosting a new group over a directory, and switching +a root's `writable` or `removable` flag are done on the node's own machine — the +desktop application over the loopback API, which asks in a native dialog before it +shares a folder or opens one to writes, or the CLI — and never over MNP. Until 6.0 +they were the signed ops `root_add`, `group_attach` and `root_update`. A signature +proves that the operator's key signed, not that the operator meant it: in a browser +that key is driven by code the hub serves (T3), and in the desktop application by a +renderer that parses content from nodes. Either could have shared any folder on the +operator's machine, writable, from anywhere. What is left here narrows (`root_remove`, +`root_eject`) or restores what the operator already shared (`root_plug`). The +operator still sees the table from any browser: it rides in `index_sync` and +`index_delta`, which is also how a change made on the node's machine reaches every +open page. + `app_directories` is the **only** way an application's folders are set: one message for every application, keyed by the app's own registry name, so adding an application adds no message type, no signed op and no handler. @@ -2128,7 +2140,6 @@ it back (§3.5). | `chat_directory` / `_ack` | C→N / N⇒C | signed | where chat attachments are written | | `chat_link_preview` / `_ack` | C→N / N⇒C | signed | whether the node unfurls posted links | | `search_listed` / `_ack` | C→N / N⇒C | signed | whether members' cross-group Search lists this group | -| `root_update` / `_ack` | C→N / N⇒C | signed | a root's `writable` / `removable` flags | | `root_eject` / `_ack`, `root_plug` / `_ack` | C→N / N⇒C | signed | take a removable root offline, put it back | | `gek_rotate` / `_ack` | C→N / N→C | signed | node generates a new group key | | `apps_enabled` / `_ack` | C→N / N⇒C | signed | which group apps are shown | @@ -2138,8 +2149,8 @@ it back (§3.5). | `tmdb_override` / `_ack` | C→N / N⇒C | signed | correct a wrong automatic match | | `tmdb_rematch` / `_ack` | C→N / N⇒C | signed | drop one file's cached match | | `musicbrainz_enabled` / `_ack` | C→N / N⇒C | signed | per-group MusicBrainz on/off | -| `root_add` / `_ack`, `root_remove` / `_ack` | C→N / N→C | signed | add or remove a shared directory | -| `group_attach` / `_ack`, `group_detach` / `_ack` | C→N / N→C | signed | start or stop hosting a group | +| `root_remove` / `_ack` | C→N / N→C | signed | remove a shared directory | +| `group_detach` / `_ack` | C→N / N→C | signed | stop hosting a group | | `node_status` / `_ack` | C→N / N→C | auth (operator) | all groups, roots, daemon state | | `roster_read` / `_ack` | C→N / N→C | auth (operator) | pinned identities and members | | `denylist_read` / `_ack` | C→N / N→C | auth (operator) | current refusals | @@ -2182,7 +2193,7 @@ message: ## 13. Versioning and compatibility -MNP versions independently of the package version. Current: **`5.0`**; oldest peer +MNP versions independently of the package version. Current: **`6.0`**; oldest peer accepted: **`4.0`**. 4.0 is the floor: a member presents a short-lived node-audience token bound to one node @@ -2197,6 +2208,13 @@ selection, per-account blobs. four fail with a refusal and everything else works; no node accepts the old subjects, so nothing is left unsigned on either side. That is why the floor did not move. +6.0 is a MAJOR that removes three signed operations — `root_add`, `root_update`, +`group_attach` — rather than changing any (§10.4: nothing over MNP widens what a node +shares). A 5.x client that sends one gets no answer, as for any unknown type, and +everything else it does still works; the floor stays at 4.0. The desktop application +also refuses to sign them, so a node older than 6.0 cannot be driven into them by a +script in its page either. + The two numbers are separate on purpose. `MNP_VERSION` says what this build speaks; `MNP_MIN_SUPPORTED` says what it will talk to, and moving the second is a decision about whether an older peer can still do anything useful: @@ -2414,7 +2432,7 @@ LP(x) = uint32be(len(x)) || x every field, no exceptions | Constant | Value | Source | |---|---|---| -| `MNP_VERSION` | `5.0` | `meshbay_common/__init__.py` | +| `MNP_VERSION` | `6.0` | `meshbay_common/__init__.py` | | `MNP_MIN_SUPPORTED` | `4.0` | `handshake.py` | | `MNP_AUD` / `HUB_API_AUD` | `meshbay:mnp` / `meshbay:hub-api` | `tokens.py` | | MNP token lifetime | 900 s | `meshbay-hub/auth.py` (`issue_mnp_token`) | diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index e85b260..d631804 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -437,6 +437,11 @@ list you build yourself with invitation codes. Two things follow from that: | **The CLI**, over SSH | no browser needed, and it works while the daemon is stopped | | **A paired browser** | the group's Settings and Members tabs, and the Node page in the desktop application | +Sharing is decided at the node. Adding a directory, and switching it read-write +or removable, work only on the machine running the node — the desktop +application, or `meshbay-node root`. From any other browser the group's +Settings tab still lists the directories and their switches, read-only. + On a headless server the CLI is the only path, and it covers everything you need to run a group. One gap is known: removing **one** device of one member is only doable from the interface (§7). Start with: |