aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-10 17:23:40 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-10 17:23:40 +0200
commit1dcedc77083b908b7b3b431bad679a4813884355 (patch)
treedb6ae06c05e5f337aeb42dde5c7bd2a7fe9444b5 /packages/meshbay-node
parent6964ff112fec47498ed778a491cf2a1490392c79 (diff)
downloadmeshbay-1dcedc77083b908b7b3b431bad679a4813884355.tar.gz
refactor(mnp)!: one answer to "may this member write", and it is the root
The group-wide `member_upload` switch is gone: the message, the signed operation, the field on the handshake ack, the `upload` alias on every root in the index payload, and the client's fallback path to it. Whether a member may write has been a property of each root for a while, and that is the model that survives: a single flag over the group cannot express "this library is published read-only and that folder is a drop box", which is the ordinary arrangement. What was left of the switch was a handler that logged a deprecation and acted on nothing, and a client that read `ack.member_upload` whenever the roots carried no `writable` — a second source for one question, with whichever the code consulted first deciding it. `roots.describe()` drops `upload` for the same reason: it was `writable` under an older name, and two names for one boolean is one too many. The paperclip now says "nowhere to write" rather than picking a root, in a group that has none writable. That is the honest answer; the fallback picked whatever came first and failed at send time. Node suite 1215 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/roots.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py22
-rw-r--r--packages/meshbay-node/tests/test_root_availability.py3
-rw-r--r--packages/meshbay-node/tests/test_root_writable_policy.py36
-rw-r--r--packages/meshbay-node/tests/test_roots.py17
5 files changed, 32 insertions, 50 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/roots.py b/packages/meshbay-node/src/meshbay_node/roots.py
index d288231..410fe68 100644
--- a/packages/meshbay-node/src/meshbay_node/roots.py
+++ b/packages/meshbay-node/src/meshbay_node/roots.py
@@ -371,9 +371,7 @@ class RootSet:
"available": r.available,
"writable": r.writable,
"removable": r.removable,
- "ejected": r.ejected,
- # Backward compat for MNP 1.0 clients
- "upload": r.writable}
+ "ejected": r.ejected}
# `with_paths` is for the operator's *own* channels only — the
# loopback API and the CLI reading it, both of which already
# require being on this machine with the run token. A member is
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 11412bf..ec7ef5d 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -67,7 +67,6 @@ from meshbay_common.adminop import (
OP_MEMBER_REVOKE,
OP_GEK_ROTATE,
OP_MEMBER_UNPIN,
- OP_MEMBER_UPLOAD,
OP_APPS_ENABLED,
OP_SET_SCAN_SETTINGS,
OP_TRANSFER_LIMITS,
@@ -570,8 +569,6 @@ class WebRTCPeerSession:
self._spawn(self._do_device_revoke(msg))
elif mtype == MNP.DEVICE_HELLO and self._nonce_node:
self._spawn(self._do_device_hello(msg))
- elif mtype == MNP.MEMBER_UPLOAD:
- self._do_member_upload(msg)
elif mtype == MNP.APPS_ENABLED:
self._do_apps_enabled(msg)
elif mtype == MNP.TRANSFER_LIMITS:
@@ -879,12 +876,6 @@ class WebRTCPeerSession:
# channel and nothing else.
config = {
"is_node_admin": self._is_node_admin(),
- # Backward compat for MNP 1.0 clients: computed from writable roots.
- # New clients read per-root writable from the index payload instead.
- "member_upload": any(
- r.get("writable") for r in
- (self._group_ctx().get("roots").describe()
- if self._group_ctx().get("roots") else [])),
# Which group "applications" to show. Absent/empty falls back to
# every registered one client-side, so a node that predates this
# setting (or one whose context has not loaded it yet) hides
@@ -2075,14 +2066,6 @@ class WebRTCPeerSession:
self._send({"type": MNP.MEMBER_UNPIN_ACK, "v": MNP_VERSION,
"user_id": user_id})
- def _do_member_upload(self, msg: dict) -> None:
- # Deprecated: upload control is now per-root via writable flag.
- # Old clients may still send this — acknowledge without acting.
- log.warning("Deprecated member_upload message received — use root "
- "writable/read-only instead")
- self._send({"type": MNP.MEMBER_UPLOAD_ACK, "v": MNP_VERSION,
- "allowed": True, "deprecated": True})
-
# Every "application" a group can show. Photos joins this set (and
# apps.js's registry, client-side) when it lands; nothing else about
# this handler changes. DEFAULT_APPS (roster.py) deliberately does not
@@ -5321,11 +5304,6 @@ class WebRTCPeerSession:
elif pending["op"] == OP_MEMBER_UNPIN:
self._spawn(
self._admin_exec_member_unpin(pending, transcript, sig_bytes))
- elif pending["op"] == OP_MEMBER_UPLOAD:
- log.warning("Deprecated OP_MEMBER_UPLOAD signed op — use root "
- "writable/read-only instead")
- self._send({"type": MNP.MEMBER_UPLOAD_ACK, "v": MNP_VERSION,
- "allowed": True, "deprecated": True})
elif pending["op"] == OP_APPS_ENABLED:
self._spawn(
self._admin_exec_apps_enabled(pending, transcript, sig_bytes))
diff --git a/packages/meshbay-node/tests/test_root_availability.py b/packages/meshbay-node/tests/test_root_availability.py
index d514dee..2d848c7 100644
--- a/packages/meshbay-node/tests/test_root_availability.py
+++ b/packages/meshbay-node/tests/test_root_availability.py
@@ -121,8 +121,7 @@ async def test_members_are_told_which_roots_are_unavailable(tmp_path):
idx = await _indexer(_roots(films))
assert idx.index.roots == [
{"name": "Films", "kind": "generic", "available": True,
- "writable": True, "removable": False, "ejected": False,
- "upload": True}]
+ "writable": True, "removable": False, "ejected": False}]
(films / "a.mkv").unlink()
films.rmdir()
diff --git a/packages/meshbay-node/tests/test_root_writable_policy.py b/packages/meshbay-node/tests/test_root_writable_policy.py
index 8345880..730e636 100644
--- a/packages/meshbay-node/tests/test_root_writable_policy.py
+++ b/packages/meshbay-node/tests/test_root_writable_policy.py
@@ -1,10 +1,11 @@
"""
Who may write to the operator's disk, now that RO/RW on the root decides it.
-This replaces `test_member_upload_policy.py`. The old model had two orthogonal
-controls — one root designated as the upload target, and a group-wide
-`member_upload` switch — and collapsed into one property per root: `writable`.
-The properties worth keeping from the old file survive the change unaltered:
+One property per root — `writable` — and no second control anywhere: not a
+group-wide switch, not a designated upload target. Two controls for one question
+is a question answered differently depending on which is read first.
+
+The properties this holds:
* the interface hiding a control is a courtesy to the people who are not
trying; **the node refusing is the part that holds** against someone who is.
@@ -220,25 +221,32 @@ async def test_a_request_with_nobody_to_authorize_it_is_refused(tmp_path):
assert [m for m in session.sent if m.get("type") == "error"]
-# ── The deprecated message must not still work ───────────────────────────────
+# ── There is no group-wide upload switch ─────────────────────────────────────
-async def test_the_old_member_upload_message_changes_nothing(tmp_path):
+async def test_no_message_can_reopen_uploads_for_a_whole_group(tmp_path):
"""
- MNP still parses `member_upload` so an old client gets an answer instead of
- a dropped request. What it must not do is act: this instruction could
- reopen uploads for a whole group, and a client old enough to send it is
- exactly one that knows nothing about read-only roots.
+ Whether a member may write is a property of each root, and there is no
+ second way to say it.
+
+ A group-wide switch is the thing this model replaced, and it cannot come
+ back by accident: the type does not exist, so a peer asking for it is a peer
+ the dispatcher logs and ignores. What must never happen is what a switch
+ would have allowed — one instruction turning a published, read-only library
+ into a writable one.
"""
+ assert not hasattr(MNP, "MEMBER_UPLOAD"), (
+ "a group-wide upload switch is back in the protocol")
+
session = _session(tmp_path, "member-1", writable=False)
session._has_admin_authority = lambda: True
issued = _capture_challenges(session)
- session._do_member_upload({"allowed": True})
+ session._dispatch_message({"type": "member_upload", "allowed": True})
- assert issued == [], "a deprecated instruction asked to be signed"
+ assert issued == [], "an unknown instruction asked to be signed"
assert session._ctx["roots"].roots[0].writable is False
- acks = [m for m in session.sent if m.get("type") == MNP.MEMBER_UPLOAD_ACK]
- assert acks and acks[0].get("deprecated") is True
+ assert not [m for m in session.sent if "upload" in str(m.get("type"))], (
+ "the node answered an instruction it does not implement")
# And the door is still shut.
_upload(session)
diff --git a/packages/meshbay-node/tests/test_roots.py b/packages/meshbay-node/tests/test_roots.py
index 505091b..9233180 100644
--- a/packages/meshbay-node/tests/test_roots.py
+++ b/packages/meshbay-node/tests/test_roots.py
@@ -272,27 +272,26 @@ def test_describe_reports_what_a_member_needs(tmp_path):
described = roots.describe()
assert described == [
{"name": "Media", "kind": "generic", "available": True,
- "writable": True, "removable": False, "ejected": False,
- "upload": True},
+ "writable": True, "removable": False, "ejected": False},
{"name": "Music", "kind": "audio", "available": True,
- "writable": False, "removable": True, "ejected": False,
- "upload": False},
+ "writable": False, "removable": True, "ejected": False},
]
# Deliberately no paths: a member is told what exists and whether it is
# readable, not where on the operator's disk it lives.
assert not any("path" in d for d in described)
-def test_describe_still_carries_upload_for_mnp_1_0_clients(tmp_path):
+def test_describe_names_a_root_and_never_a_path(tmp_path):
"""
- `upload` is `writable` under its old name, kept because an MNP 1.0 client
- reads no other field and would otherwise decide the group takes no uploads
- at all. It is derived, never stored — the two can never disagree.
+ What a member is told about a root: that it exists, and whether it is
+ readable and writable. Never where on the operator's disk it lives — that
+ is the operator's own view (`with_paths`), over their own channel.
"""
(tmp_path / "Media").mkdir()
roots = RootSet.build([_spec(tmp_path / "Media", writable=True)])
described = roots.describe()[0]
- assert described["upload"] == described["writable"] is True
+ assert set(described) == {"name", "kind", "available", "writable",
+ "removable", "ejected"}
# ── SAFE_UPLOAD_NAME ────────────────────────────────────────────────────────