summaryrefslogtreecommitdiffstats
path: root/docs/MESHBAY_NODE_PROTOCOL.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-02 10:51:19 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-02 10:51:19 +0200
commit754387590fa1754436b4648f969915888c6f6c9e (patch)
tree53c833eeb4042e8ce5f96b882b23471daa9d9394 /docs/MESHBAY_NODE_PROTOCOL.md
parentc928547ca6e402bfe5e06bb59d55ac91e6822cd0 (diff)
downloadmeshbay-754387590fa1754436b4648f969915888c6f6c9e.tar.gz
refactor(mnp): remove ten operator messages no client sent0.17
node_status, node_settings_set, roster_read, denylist_read, denylist_clear, node_reload and the signed gek_rotate, member_unpin, transfer_limits, group_detach leave MNP 6.0; the Node page and the CLI do this work over loopback. Their ops keep their tests, moved to the ops level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/MESHBAY_NODE_PROTOCOL.md')
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md64
1 files changed, 22 insertions, 42 deletions
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index cdcc2c0..0b5213c 100644
--- a/docs/MESHBAY_NODE_PROTOCOL.md
+++ b/docs/MESHBAY_NODE_PROTOCOL.md
@@ -162,7 +162,7 @@ Codes in use:
| Upload | `upload_not_sealed`, `no_group_key`, `lease_not_granted`, `upload_incomplete`, `bad_chunk_encoding`, `bad_chunk_index`, `invalid_filename`, `no_roots`, `no_such_root`, `no_writable_root`, `root_read_only`, `root_unavailable`, `no_such_directory`, `already_exists`, `not_started`, `too_large` (§11.4) |
| Directories | `root_read_only`, `root_unavailable` (§11.5) |
| Chat | `chat_too_large`, `chat_rate_limited` (§11.7) |
-| Operator controls | `not_operator`, `too_many_pending`, `too_large` (§10.4) |
+| Operator controls | `too_many_pending`, `too_large` (§10.4) |
| Metadata | `transcode_not_applicable`, `tmdb_search_rate_limited` (§11.9) |
| Moderation | `content_blocked` — a file the hub's content blocklist names, in a public group (§11.3) |
@@ -1156,8 +1156,6 @@ broadcast, every connected peer in the group learns the change without reconnect
| `invite_link_create` | `link:<group_id>`, the session's group | operator only | `invite_link_result{code, invite_id, expires_at, group_id}` | no — the code is shown once |
| `invite_cancel` | `invite_id` (32 hex) | operator only | `ack{detail: "invite_cancelled", invite_id}` | no |
| `member_revoke` | `user_id` | operator | `member_revoke_ack` | no |
-| `member_unpin` | `user_id` | operator | `member_unpin_ack{user_id}` | no |
-| `gek_rotate` | `group_id` | operator | `gek_rotate_ack{group_id, authorized_members, note}` | no |
| `apps_enabled` | the app set | operator | `apps_enabled_ack{apps}` | yes |
| `set_scan_settings` | the interval/debounce pair | operator | `set_scan_settings_ack{...}` | yes |
| `tmdb_config` | `{token, language}` — `token` is `null` (unchanged), `""` (clear) or `sha256:<hex>` of the token, never the token | operator | `tmdb_config_ack{token_customized, language}` | yes (never the token) |
@@ -1171,9 +1169,7 @@ broadcast, every connected peer in the group learns the change without reconnect
| `chat_directory` | the path | operator | `chat_directory_ack{path}` | yes |
| `chat_link_preview` | `on\|off` | operator | `chat_link_preview_ack{enabled}` | yes |
| `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_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 (its `writable` flag), not a switch over the group. A single
@@ -1212,18 +1208,14 @@ Their *storage* keys survive on the node — `Roster.LEGACY_DIR_KEYS` still read
operator's disk rather than on the wire, and a node upgraded into this has to find its
own configuration.
-A second family of operator messages is **not** signed: `node_status`, `roster_read`,
-`denylist_read`, `denylist_clear`, `node_settings_set`, `node_reload`. These are gated
-by `_operator_device()`, which asks two things: the authenticated session's `user_id`
-is the account the node records as its own (`node_user_id`, `is_node_admin()`), **and**
-the device on this connection has proved, with `device_hello` (§9.4), a key the roster
-holds as an operator. Anything else is refused with code `not_operator`. The first
-alone would be a claim in a token the hub issued, and a hub that can name the operator
-is a hub that can be one; the second is what it cannot forge, since it holds no user
-keys. Three of them only read; the other three run through the same `ops` entry points
-as the CLI and the loopback admin API. The distinction from the signed table above:
-a signed op proves possession of an operator key for *this* operation, while these
-prove it once per connection, through the device the connection identified.
+**The node's own controls are not MNP messages.** Its status (every group, with each
+root's absolute path), settings, roster, denylist and reload; rotating a group key;
+forgetting a pinned identity; the per-member transfer caps; no longer hosting a group —
+the Node page in the desktop application and the CLI do these over the loopback API, on
+the operator's own machine. Until 6.0 MNP carried them too (`node_status`,
+`node_settings_set`, `roster_read`, `denylist_read`, `denylist_clear`, `node_reload`,
+`gek_rotate`, `member_unpin`, `transfer_limits`, `group_detach`), and no client ever
+sent one: a door nobody calls is an untested way in (§10.5).
**The subject covers everything the node acts on.** The signature covers `op`, the
node, the group, the subject, the nonce and the time — nothing else of the request —
@@ -1242,16 +1234,14 @@ operations (`too_many_pending` beyond), each at most 64 KiB of subject and paylo
Rules that hold across the table:
* **No MNP message can activate a group key.** The rule (I2) targets key material
- arriving from outside, not the instruction: `gek_rotate` and `chat_epoch` are allowed
- precisely because the node generates the new key itself with its own CSPRNG. Initial
- `gek-init` stays local — with no group key there is no completed session to carry a
- signed op anyway.
+ arriving from outside, not the instruction: `chat_epoch` is allowed precisely
+ because the node generates the new key itself with its own CSPRNG. Initialising and
+ rotating the group key stay local (loopback, CLI).
* **There is no operation by which key material reaches the node** (§12). The node
wraps for a key the recipient has proved possession of, so no such message is needed —
and a path that does not exist cannot be mis-authorized, which is I10 applied to a
message instead of a version.
-* An operator cannot revoke or unpin **themselves** over the connection their pin
- authorizes.
+* An operator cannot revoke **themselves** over the connection their pin authorizes.
* Rotation is what actually removes a revoked member's access. Revocation stops the
node serving the *next* key; the ex-member still holds the current one, and content
they already downloaded stays readable. The ack says so in words.
@@ -1419,7 +1409,7 @@ The control plane is not covered either — see §14.2.
**Nonce collision, since `upload` is the first purpose with volume.** One subkey per
purpose and a fresh 96-bit random nonce per message: at one message per 48 KiB chunk,
2³² chunks is 200 TB uploaded under a single GEK before the collision probability
-reaches 2⁻³², and `gek_rotate` exists. Deriving the nonce from the payload instead
+reaches 2⁻³², and the group key can be rotated. Deriving the nonce from the payload instead
would be worse, not better — two chunks of identical bytes are ordinary in a file.
**Failure is fatal, never degraded** (I8). A client that cannot open an index message
@@ -1477,9 +1467,9 @@ The **lease** is that object, and every transfer runs under one.
**Caps.** Node-wide, 8 concurrent per kind by default; per member per group, 2 by
default. A group with no value of its own gets the default, never "unlimited": reading
an absent setting as no limit would leave the node-wide cap as the only control, which
-is the situation leases exist to end. The per-group value is a signed operator
-operation (`transfer_limits`, §10.4, bounded to 1–32; zero is refused, because a member
-who may not transfer at all is a member the operator revokes). The node-wide values are
+is the situation leases exist to end. The per-group value is set on the node's machine
+(`ops.set_transfer_limits`, loopback and CLI; bounded to 1–32, zero is refused, because
+a member who may not transfer at all is a member the operator revokes). The node-wide values are
daemon settings. A member's own cap rides on every `transfer_state`, so the interface can
say "2 of your 2 slots are busy" rather than draw a spinner that explains nothing.
@@ -1946,7 +1936,7 @@ collide immediately. The design degrades correctly into the deployment that exis
chain-based one would not have.
**Epochs.** A new epoch is opened when the set of devices that may read *future*
-messages shrinks — member revoke, member unpin, device revoke, `gek_rotate` — and by
+messages shrinks — member revoke, member unpin, device revoke — and by
hand with the signed `chat_epoch` op (§10.4). Old epochs are kept and still delivered to
current members, which is what keeps history readable to the people who could already
read it. The epoch key is wrapped under the group key **at delivery**, never stored
@@ -2132,15 +2122,12 @@ it back (§3.5).
| `admin_response` | C→N | auth | the operator's signature over that transcript |
| `client_diag` | C→N | auth | the video player's own view of a stream, written to the node's log beside its own (a stream event at INFO, the periodic state at DEBUG); the node acts on none of it and sends no reply |
| `member_revoke` / `_ack` | C→N / N→C | signed | stop serving the key to someone |
-| `member_unpin` / `_ack` | C→N / N→C | signed | forget a pinned identity |
-| `transfer_limits` / `_ack` | C→N / N⇒C | signed | per-member transfer caps for this group |
| `chat_epoch` / `_ack` | C→N / N⇒C | signed | open a new chat epoch by hand |
| `app_directories` / `_ack` | C→N / N⇒C | signed | one application's folders, keyed by app name |
| `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_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 |
| `set_scan_settings` / `_ack` | C→N / N⇒C | signed | reconcile interval and debounce |
| `tmdb_config` / `_ack` | C→N / N⇒C | signed | node-wide TMDB token and language |
@@ -2149,13 +2136,6 @@ it back (§3.5).
| `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_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 |
-| `denylist_clear` / `_ack` | C→N / N→C | auth (operator) | remove entries |
-| `node_settings_set` / `_ack` | C→N / N→C | auth (operator) | change daemon settings |
-| `node_reload` / `_ack` | C→N / N→C | auth (operator) | re-read `node.toml` |
| `error` | N→C | any | refusal, with `detail` and optionally `code`, `req_id`, and the `upload_id` / `tr` / `file_id` it is about |
| `ack` | N→C | auth | generic acknowledgement (chat, keypair bundle store) |
@@ -2210,7 +2190,8 @@ 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
+everything else it does still works; the floor stays at 4.0. The same version removes
+ten operator messages no client ever sent (§10.4, "The node's own controls"). 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.
@@ -2353,9 +2334,8 @@ walks through the gate meant to stop it.
file chunks, stream segments, uploads, the chat keys and the group roster. It does not
cover the admin and configuration acks (`app_directories_ack`, `root_*_ack` and the
rest), which carry the same folder names the sealed index carries; the media-metadata replies (`media_meta_resp`, `music_meta_resp`,
- `link_preview_resp`), which carry titles, artists and synopses; `node_status_ack`,
- which carries absolute paths on the operator's disk to an operator session; the
- identity replies (`roster_read_ack`, `device_list_result`); or `invite_result` and
+ `link_preview_resp`), which carry titles, artists and synopses; the identity reply
+ `device_list_result`; or `invite_result` and
`invite_link_result`, which carry a pairing code. All are inside DTLS/TLS and none reaches the hub, but none is
behind the group key.
* **Transfer messages are in clear on purpose**, and that is a deliberate line rather