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 /packages/meshbay-common | |
| 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 'packages/meshbay-common')
5 files changed, 34 insertions, 51 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py index b68e828..c5c04a5 100644 --- a/packages/meshbay-common/src/meshbay_common/__init__.py +++ b/packages/meshbay-common/src/meshbay_common/__init__.py @@ -260,5 +260,12 @@ __version__ = "0.17.0" # a refusal, across the break. The break is confined to them, so the floor stays # at 4.0: everything else a 4.x peer does still works, and nothing is left # unsigned on either side — no node accepts the old subjects. -MNP_VERSION = "5.0" +# +# 6.0 (2026-10-02) is a MAJOR — three signed operations are removed: `root_add`, +# `root_update` and `group_attach`. What of the operator's disk is shared, and +# whether members may write there, is decided on the node's own machine (the +# desktop application over loopback, behind a native dialog, or the CLI), never +# over the wire. A 5.x client that sends one gets no answer, as for any unknown +# type; everything else it does still works, so the floor stays at 4.0. +MNP_VERSION = "6.0" MHP_VERSION = "0.1" diff --git a/packages/meshbay-common/src/meshbay_common/adminop.py b/packages/meshbay-common/src/meshbay_common/adminop.py index 4379519..11100b5 100644 --- a/packages/meshbay-common/src/meshbay_common/adminop.py +++ b/packages/meshbay-common/src/meshbay_common/adminop.py @@ -101,7 +101,6 @@ OP_TMDB_REMATCH = "tmdb_rematch" # the lesson was already learned once). Signed for the same reason as # tmdb_enabled. OP_MUSICBRAINZ_ENABLED = "musicbrainz_enabled" -OP_ROOT_ADD = "root_add" OP_ROOT_REMOVE = "root_remove" # One op for every application's directories. The subject is # "<app>:<comma-joined sorted paths>" so what the operator is shown before @@ -122,11 +121,16 @@ OP_SEARCH_LISTED = "search_listed" # There is no op for *enabling* chat encryption. It is not a setting — MNP 2.0 # has no plaintext chat to fall back to. OP_CHAT_EPOCH = "chat_epoch" -OP_ROOT_UPDATE = "root_update" OP_ROOT_EJECT = "root_eject" OP_ROOT_PLUG = "root_plug" -OP_GROUP_ATTACH = "group_attach" OP_GROUP_DETACH = "group_detach" +# OP_ROOT_ADD, OP_ROOT_UPDATE and OP_GROUP_ATTACH are gone (MNP 6.0). Each one +# chose what of the operator's disk is shared and who may write there, and a +# signature proves only that the operator's key signed — in a browser, through +# code the hub serves; on the desktop, through a renderer that parses content +# from nodes. Sharing a folder, hosting a group and opening a folder to writes +# are done on the node's own machine (loopback, behind a native dialog) or with +# the CLI, and a message that does not exist cannot be mis-authorized. # OP_GEK_BUNDLE_STORE is gone. Members no longer hand the node key material at # all: the node holds the GEK and wraps it itself, for a key the recipient proved # they hold (see `join.py` and docs/MESHBAY_DESIGN.md §3.4). The operation existed @@ -159,17 +163,6 @@ def secret_digest(value: str | None) -> str | None: return "sha256:" + hashlib.sha256(value.encode()).hexdigest() -def root_add_subject(path: str, name: str, kind: str, writable: bool, - removable: bool) -> str: - return structured_subject({"path": path, "name": name, "kind": kind, - "writable": writable, "removable": removable}) - - -def group_attach_subject(name: str, shared_dir: str, writable: bool) -> str: - return structured_subject({"name": name, "shared_dir": shared_dir, - "writable": writable}) - - def invite_create_subject(user_id: str, username: str) -> str: return structured_subject({"user_id": user_id, "username": username}) diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py index af2d93b..2c7e301 100644 --- a/packages/meshbay-common/src/meshbay_common/protocol.py +++ b/packages/meshbay-common/src/meshbay_common/protocol.py @@ -224,8 +224,6 @@ class MNP: INVITE_LINK_RESULT = "invite_link_result" # node → operator: link code + handle, once NODE_STATUS = "node_status" # operator → node: list all groups + roots NODE_STATUS_ACK = "node_status_ack" # node → operator: full status - ROOT_ADD = "root_add" # operator → node: add a directory to a group - ROOT_ADD_ACK = "root_add_ack" # node → operator: confirmed ROOT_REMOVE = "root_remove" # operator → node: remove a root from a group ROOT_REMOVE_ACK = "root_remove_ack" # node → operator: confirmed # One message for every application's directories, keyed by the app's own @@ -256,8 +254,6 @@ class MNP: # key without having to reconnect. CHAT_EPOCH = "chat_epoch" CHAT_EPOCH_ACK = "chat_epoch_ack" - ROOT_UPDATE = "root_update" # operator → node: a root's flags - ROOT_UPDATE_ACK = "root_update_ack" ROOT_EJECT = "root_eject" # operator → node: mark removable root as ejected ROOT_EJECT_ACK = "root_eject_ack" ROOT_PLUG = "root_plug" # operator → node: re-enable an ejected root @@ -276,8 +272,6 @@ class MNP: DENYLIST_READ_ACK = "denylist_read_ack" DENYLIST_CLEAR = "denylist_clear" # operator → node: remove denylist entry(ies) DENYLIST_CLEAR_ACK = "denylist_clear_ack" - GROUP_ATTACH = "group_attach" # operator → node: host a new group - GROUP_ATTACH_ACK = "group_attach_ack" GROUP_DETACH = "group_detach" # operator → node: stop hosting a group GROUP_DETACH_ACK = "group_detach_ack" NODE_SETTINGS_SET = "node_settings_set" # operator → node: change daemon settings diff --git a/packages/meshbay-common/tests/test_admin_subject_parity.py b/packages/meshbay-common/tests/test_admin_subject_parity.py index 7a229a9..59deaab 100644 --- a/packages/meshbay-common/tests/test_admin_subject_parity.py +++ b/packages/meshbay-common/tests/test_admin_subject_parity.py @@ -18,9 +18,7 @@ from pathlib import Path import pytest from meshbay_common.adminop import ( - group_attach_subject, invite_create_subject, - root_add_subject, secret_digest, structured_subject, tmdb_config_subject, @@ -34,16 +32,13 @@ pytestmark = pytest.mark.skipif( reason="node or crypto.js unavailable — parity cannot be checked", ) -ROOT_ADD = [ - ("/srv/Films", "", "generic", False, False), - ("/srv/Films", "Films", "video", True, True), - ("C:\\Users\\me\\Share", "Partagé", "photo", True, False), - ('/srv/a "quoted", odd:name|x', "名前", "audio", False, True), - ("/srv/tab\there\nnewline\x01ctl", "é", "generic", True, False), -] -GROUP_ATTACH = [ - ("photos", "/srv/photos", True), - ("famille-été", "/mnt/disque externe/Photos", False), +# The canonical JSON itself, on the values that are hard to get identical: +# separators, quotes, control characters, non-ASCII, and null/""/false. +STRUCTURED = [ + {"path": "/srv/Films", "name": "", "writable": False, "n": None}, + {"path": "C:\\Users\\me\\Share", "name": "Partagé", "kind": "photo"}, + {"path": '/srv/a "quoted", odd:name|x', "name": "名前", "removable": True}, + {"path": "/srv/tab\there\nnewline\x01ctl", "name": "é", "z": "", "a": 0}, ] INVITE_CREATE = [ ("0f8fad5b-d9cb-469f-a165-70867728950e", ""), @@ -59,13 +54,12 @@ _HARNESS = r""" const fs = require('fs'); globalThis.window = {}; const src = fs.readFileSync(process.argv[2], 'utf8'); -const M = new Function(src + '\nreturn { rootAddSubject, groupAttachSubject, ' +const M = new Function(src + '\nreturn { adminSubject, ' + 'inviteCreateSubject, tmdbConfigSubject };')(); const v = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')); (async () => { const out = { - root_add: v.root_add.map((a) => M.rootAddSubject(...a)), - group_attach: v.group_attach.map((a) => M.groupAttachSubject(...a)), + structured: v.structured.map((f) => M.adminSubject(f)), invite_create: v.invite_create.map((a) => M.inviteCreateSubject(...a)), tmdb_config: [], }; @@ -80,7 +74,7 @@ def js(tmp_path_factory): d = tmp_path_factory.mktemp("subject-parity") (d / "harness.js").write_text(_HARNESS, encoding="utf-8") (d / "vectors.json").write_text(json.dumps({ - "root_add": ROOT_ADD, "group_attach": GROUP_ATTACH, + "structured": STRUCTURED, "invite_create": INVITE_CREATE, "tmdb_config": TMDB_CONFIG, }), encoding="utf-8") proc = subprocess.run( @@ -95,14 +89,9 @@ def _bytes(s: str) -> bytes: return s.encode("utf-8") -@pytest.mark.parametrize("i,args", list(enumerate(ROOT_ADD))) -def test_root_add_subject_parity(i, args, js): - assert _bytes(js["root_add"][i]) == _bytes(root_add_subject(*args)) - - -@pytest.mark.parametrize("i,args", list(enumerate(GROUP_ATTACH))) -def test_group_attach_subject_parity(i, args, js): - assert _bytes(js["group_attach"][i]) == _bytes(group_attach_subject(*args)) +@pytest.mark.parametrize("i,fields", list(enumerate(STRUCTURED))) +def test_structured_subject_parity(i, fields, js): + assert _bytes(js["structured"][i]) == _bytes(structured_subject(fields)) @pytest.mark.parametrize("i,args", list(enumerate(INVITE_CREATE))) @@ -116,13 +105,13 @@ def test_tmdb_config_subject_parity(i, args, js): def test_every_value_changes_the_subject(): - base = ("/srv/Films", "Films", "video", False, False) - variants = {root_add_subject(*base)} - for i, other in enumerate(("/srv/Other", "Other", "audio", True, True)): + base = ("0f8fad5b-d9cb-469f-a165-70867728950e", "Someone") + variants = {invite_create_subject(*base)} + for i, other in enumerate(("6a2f41a3-c54c-fce8-32d2-0324e1c32e22", "Somebody")): args = list(base) args[i] = other - variants.add(root_add_subject(*args)) - assert len(variants) == 6 + variants.add(invite_create_subject(*args)) + assert len(variants) == 3 def test_unchanged_cleared_and_set_are_three_subjects(): diff --git a/packages/meshbay-common/tests/test_js_python_parity.py b/packages/meshbay-common/tests/test_js_python_parity.py index f75d60a..bb52742 100644 --- a/packages/meshbay-common/tests/test_js_python_parity.py +++ b/packages/meshbay-common/tests/test_js_python_parity.py @@ -668,7 +668,7 @@ _KIND_FIELDS = { "chat": {"groupId": "g" * 32, "epoch": 4, "nonce": base64.b64encode(b"\x01" * 12).decode(), "ct": base64.b64encode(b"ciphertext").decode()}, - "admin": {"op": "root_add", "nodePk": "Tk9ERVBL", "groupId": "g" * 32, + "admin": {"op": "root_remove", "nodePk": "Tk9ERVBL", "groupId": "g" * 32, "subject": '{"path":"/café"}', "nonce": _NONCE, "ts": 1_700_000_000}, } |