summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/indexer.py15
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops/roots.py12
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py3
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py168
-rw-r--r--packages/meshbay-node/tests/golden/dispatch.json1237
-rw-r--r--packages/meshbay-node/tests/test_admin_challenge_bounds.py45
-rw-r--r--packages/meshbay-node/tests/test_node_status.py58
-rw-r--r--packages/meshbay-node/tests/test_root_writable_policy.py38
-rw-r--r--packages/meshbay-node/tests/test_sharing_is_local_only.py109
10 files changed, 207 insertions, 1484 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
index 4b619d7..f0505f7 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
@@ -719,6 +719,21 @@ class DirectoryIndexer:
# The table now; the files when the scan ends.
await self.on_change(self)
+ async def publish_roots(self) -> None:
+ """
+ Tell every connected peer the table, after a root's flags were edited
+ in place (`ops.update_root`).
+
+ A reload compares the edited set with itself and finds nothing to do,
+ so without this a directory made writable from the operator's own
+ machine stayed read-only on every open page until something else
+ happened to push the index.
+ """
+ self._index.roots = self.roots.describe()
+ self._index.version = int(time.time())
+ if self.on_change:
+ await self.on_change(self)
+
def _holds(self, root: Root) -> bool:
return any(r.folded == root.folded and r.path == root.path for r in self.roots)
diff --git a/packages/meshbay-node/src/meshbay_node/ops/roots.py b/packages/meshbay-node/src/meshbay_node/ops/roots.py
index 480fe63..9dbade4 100644
--- a/packages/meshbay-node/src/meshbay_node/ops/roots.py
+++ b/packages/meshbay-node/src/meshbay_node/ops/roots.py
@@ -174,11 +174,14 @@ async def update_root(state: dict, group_id: str, root_name: str, *,
writable=match.writable, removable=match.removable)
# Update the live RootSet so GET /api/groups returns correct data
- # immediately, without waiting for the async reload to finish.
+ # immediately, without waiting for the async reload to finish — and the
+ # indexer's, which is normally the same object but need not be, since it
+ # is the one the table pushed to every peer is read from.
live_roots: RootSet | None = state.get("groups_ctx", {}).get(
group_id, {}).get("roots")
- if live_roots:
- for lr in live_roots.roots:
+ indexer = state.get("indexers", {}).get(group_id)
+ for rootset in {id(x): x for x in (live_roots, indexer and indexer.roots) if x}.values():
+ for lr in rootset.roots:
lr_name = lr.name or str(Path(lr.path).name)
if fold(lr_name) == target:
if writable is not None:
@@ -187,6 +190,9 @@ async def update_root(state: dict, group_id: str, root_name: str, *,
lr.removable = removable
break
+ if indexer:
+ await indexer.publish_roots()
+
# Built from config when there is no live set, never returned empty: an
# empty list is a *valid answer* meaning "this group has no directories",
# and the client cannot tell it from "the node could not say". It would
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py
index daaee62..738dbce 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admin.py
@@ -18,7 +18,6 @@ from meshbay_common.adminop import (
OP_DIR_DELETE,
OP_FILE_DELETE,
OP_GEK_ROTATE,
- OP_GROUP_ATTACH,
OP_GROUP_DETACH,
OP_INVITE_CANCEL,
OP_INVITE_CREATE,
@@ -26,11 +25,9 @@ from meshbay_common.adminop import (
OP_MEMBER_REVOKE,
OP_MEMBER_UNPIN,
OP_MUSICBRAINZ_ENABLED,
- OP_ROOT_ADD,
OP_ROOT_EJECT,
OP_ROOT_PLUG,
OP_ROOT_REMOVE,
- OP_ROOT_UPDATE,
OP_SEARCH_LISTED,
OP_SET_SCAN_SETTINGS,
OP_TMDB_CONFIG,
@@ -72,17 +69,14 @@ _ADMIN_EXECUTORS = {
OP_TMDB_OVERRIDE: "_admin_exec_tmdb_override",
OP_TMDB_REMATCH: "_admin_exec_tmdb_rematch",
OP_MUSICBRAINZ_ENABLED: "_admin_exec_musicbrainz_enabled",
- OP_ROOT_ADD: "_admin_exec_root_add",
OP_ROOT_REMOVE: "_admin_exec_root_remove",
OP_APP_DIRECTORIES: "_admin_exec_app_directories",
OP_CHAT_DIRECTORY: "_admin_exec_chat_directory",
OP_CHAT_LINK_PREVIEW: "_admin_exec_chat_link_preview",
OP_SEARCH_LISTED: "_admin_exec_search_listed",
OP_CHAT_EPOCH: "_admin_exec_chat_epoch",
- OP_ROOT_UPDATE: "_admin_exec_root_update",
OP_ROOT_EJECT: "_admin_exec_root_eject",
OP_ROOT_PLUG: "_admin_exec_root_plug",
- OP_GROUP_ATTACH: "_admin_exec_group_attach",
OP_GROUP_DETACH: "_admin_exec_group_detach",
}
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py
index ee2e3a1..1ba5445 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/dispatch.py
@@ -71,15 +71,12 @@ _HANDLERS = {
MNP.MEMBER_UNPIN: ("_do_member_unpin", INLINE),
MNP.GEK_ROTATE: ("_do_gek_rotate", INLINE),
MNP.NODE_STATUS: ("_do_node_status", SPAWNED),
- MNP.ROOT_ADD: ("_do_root_add", INLINE),
MNP.ROOT_REMOVE: ("_do_root_remove", INLINE),
- MNP.ROOT_UPDATE: ("_do_root_update", INLINE),
MNP.ROOT_EJECT: ("_do_root_eject", INLINE),
MNP.ROOT_PLUG: ("_do_root_plug", INLINE),
MNP.ROSTER_READ: ("_do_roster_read", SPAWNED),
MNP.DENYLIST_READ: ("_do_denylist_read", SPAWNED),
MNP.DENYLIST_CLEAR: ("_do_denylist_clear", SPAWNED),
- MNP.GROUP_ATTACH: ("_do_group_attach", INLINE),
MNP.GROUP_DETACH: ("_do_group_detach", INLINE),
MNP.NODE_SETTINGS_SET: ("_do_node_settings_set", SPAWNED),
MNP.NODE_RELOAD: ("_do_node_reload", SPAWNED),
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py
index f7bbbfa..acaa33f 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/node_ops.py
@@ -5,17 +5,12 @@ import logging
from meshbay_common import MNP_VERSION
from meshbay_common.adminop import (
- OP_GROUP_ATTACH,
OP_GROUP_DETACH,
- OP_ROOT_ADD,
OP_ROOT_EJECT,
OP_ROOT_PLUG,
OP_ROOT_REMOVE,
- OP_ROOT_UPDATE,
OP_SET_SCAN_SETTINGS,
OP_TRANSFER_LIMITS,
- group_attach_subject,
- root_add_subject,
)
from meshbay_common.protocol import MNP
@@ -235,52 +230,6 @@ class NodeOpsMixin:
log.error("denylist_clear failed: %s", e, exc_info=True)
self._send({"type": "error", "detail": "Internal error"})
- def _do_group_attach(self, msg: dict) -> None:
- name = str(msg.get("name", "")).strip()
- shared_dir = str(msg.get("shared_dir", "")).strip()
- if not name or not shared_dir:
- self._send({"type": "error", "detail": "Missing name or shared_dir"})
- return
- if not self._has_admin_authority():
- self._send({"type": "error", "detail": "No authorized key for this"})
- return
- # `upload_dir` is not read here any more, and a client still sending it
- # is ignored rather than obeyed: on load it forces every other root
- # read-only, which is the model the RO/RW one replaced. A second
- # writable directory is `root_add` with `writable`.
- writable = bool(msg.get("writable", True))
- # The directory being exposed is signed, not only the group's name.
- self._issue_admin_challenge(
- OP_GROUP_ATTACH, group_attach_subject(name, shared_dir, writable),
- payload={"name": name, "shared_dir": shared_dir, "writable": writable},
- group_id="")
-
- async def _admin_exec_group_attach(
- self, pending: dict, transcript: bytes, sig: bytes,
- ) -> None:
- if not await self._verify_admin_sig(transcript, sig):
- self._send({"type": "error", "detail": "Signature verification failed"})
- self._audit("admin_auth_failed",
- f"group_attach:{pending['subject'][:16]}")
- return
- p = pending.get("payload") or {}
- try:
- result = await self._run_op(
- ops.attach_group, p["name"], p["shared_dir"],
- writable=bool(p.get("writable", True)))
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- return
- self._audit("group_attach", pending["subject"])
- self._send({"type": MNP.GROUP_ATTACH_ACK, "v": MNP_VERSION, **result})
- state = self._ctx.get("daemon_state")
- reload_fn = state.get("reload_fn") if state else None
- if reload_fn:
- try:
- await reload_fn()
- except Exception as e:
- log.error("Reload after group_attach failed: %s", e)
-
def _do_group_detach(self, msg: dict) -> None:
name = str(msg.get("name", "")).strip()
if not name:
@@ -336,55 +285,6 @@ class NodeOpsMixin:
log.error("node_reload failed: %s", e, exc_info=True)
self._send({"type": "error", "detail": "Reload failed"})
- def _do_root_add(self, msg: dict) -> None:
- target_group = str(msg.get("group_id", "")).strip()
- path = str(msg.get("path", "")).strip()
- if not target_group or not path:
- self._send({"type": "error", "detail": "Missing group_id or path"})
- return
- if not self._has_admin_authority():
- self._send({"type": "error", "detail": "No authorized key for this"})
- return
- payload = {
- "group_id": target_group, "path": path,
- "name": str(msg.get("name", ""))[:128],
- "kind": str(msg.get("kind", "generic"))[:16],
- "writable": bool(msg.get("writable", msg.get("upload", False))),
- "removable": bool(msg.get("removable", False)),
- }
- # Everything the executor acts on is signed — `writable` decides whether
- # every member may write there. The group is in the transcript itself.
- self._issue_admin_challenge(
- OP_ROOT_ADD,
- root_add_subject(path, payload["name"], payload["kind"],
- payload["writable"], payload["removable"]),
- payload=payload, group_id=target_group)
-
- async def _admin_exec_root_add(
- self, pending: dict, transcript: bytes, sig: bytes,
- ) -> None:
- if not await self._verify_admin_sig(transcript, sig):
- self._send({"type": "error", "detail": "Signature verification failed"})
- self._audit("admin_auth_failed", f"root_add:{pending['subject'][:24]}")
- return
- p = pending["payload"]
- try:
- result = await self._run_op(
- ops.add_root, p["group_id"], p["path"],
- name=p.get("name", ""), kind=p.get("kind", "generic"),
- writable=p.get("writable", False),
- removable=p.get("removable", False))
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- return
- except Exception as e:
- log.error("root_add failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Internal error"})
- return
- self._audit("root_add", f"{p['path']}→{p['group_id'][:8]}")
- await self._retarget_indexer(p["group_id"])
- self._send({"type": MNP.ROOT_ADD_ACK, "v": MNP_VERSION, **result})
-
def _do_root_remove(self, msg: dict) -> None:
target_group = str(msg.get("group_id", "")).strip()
root_name = str(msg.get("root_name", "")).strip()
@@ -421,59 +321,6 @@ class NodeOpsMixin:
await self._retarget_indexer(p["group_id"])
self._send({"type": MNP.ROOT_REMOVE_ACK, "v": MNP_VERSION, **result})
- def _do_root_update(self, msg: dict) -> None:
- target_group = str(msg.get("group_id", self._group_id or "")).strip()
- root_name = str(msg.get("root_name", "")).strip()
- if not target_group or not root_name:
- self._send({"type": "error", "detail": "Missing group_id or root_name"})
- return
- if not self._has_admin_authority():
- self._send({"type": "error", "detail": "No authorized key for this"})
- return
- updates = []
- if "writable" in msg:
- updates.append(f"rw={'on' if msg['writable'] else 'off'}")
- if "removable" in msg:
- updates.append(f"rem={'on' if msg['removable'] else 'off'}")
- subject = f"{root_name}:{','.join(updates)}" if updates else root_name
- self._issue_admin_challenge(
- OP_ROOT_UPDATE, subject,
- payload={
- "group_id": target_group, "root_name": root_name,
- "writable": msg.get("writable"),
- "removable": msg.get("removable"),
- },
- group_id=target_group)
-
- async def _admin_exec_root_update(
- self, pending: dict, transcript: bytes, sig: bytes,
- ) -> None:
- if not await self._verify_admin_sig(transcript, sig):
- self._send({"type": "error", "detail": "Signature verification failed"})
- self._audit("admin_auth_failed",
- f"root_update:{pending['subject'][:24]}")
- return
- p = pending["payload"]
- try:
- result = await self._run_op(
- ops.update_root, p["group_id"], p["root_name"],
- writable=p.get("writable"), removable=p.get("removable"))
- except ops.OpError as e:
- self._send({"type": "error", "detail": e.message})
- return
- except Exception as e:
- log.error("root_update failed: %s", e, exc_info=True)
- self._send({"type": "error", "detail": "Internal error"})
- return
- self._audit("root_update", pending["subject"])
- await self._retarget_indexer(p["group_id"])
- notice = {"type": MNP.ROOT_UPDATE_ACK, "v": MNP_VERSION, **result}
- for uid, session in list(self._peer_registry().items()):
- try:
- session._send(notice)
- except Exception:
- pass
-
def _do_root_eject(self, msg: dict) -> None:
target_group = str(msg.get("group_id", self._group_id or "")).strip()
root_name = str(msg.get("root_name", "")).strip()
@@ -558,24 +405,21 @@ class NodeOpsMixin:
async def _retarget_indexer(self, group_id: str) -> None:
"""
- Pick up a root that was just added to or removed from node.toml.
+ Pick up a root that was just removed from node.toml.
Through the daemon's own reload, which is what the loopback API has
always done after the same operations (`ui/app.py`). This used to
re-point the indexer at `groups_ctx[gid]["roots"]` instead — the very
object the op had just edited — so `retarget` diffed a set against
- itself, found no new names, scanned nothing, and dropped nothing. A
- directory added over MNP reached node.toml and was invisible until a
- restart; one removed kept serving its files.
+ itself, found no new names, scanned nothing, and dropped nothing: a
+ directory removed over MNP kept serving its files until a restart.
Two front doors doing different things is the shape `ops.py` exists to
prevent, and this was it: the loopback path worked and the MNP path did
- not, which is why it survived until the operator added a directory from
- a browser.
+ not.
- Not awaited: a reload rescans, and a new library is minutes. The ack
- the caller sends carries the set the node is moving to, and the
- `index_sync` that follows the scan carries what it found.
+ Not awaited: a reload can be long. The ack the caller sends carries the
+ set the node is moving to.
"""
state = self._ctx.get("daemon_state")
if not state:
diff --git a/packages/meshbay-node/tests/golden/dispatch.json b/packages/meshbay-node/tests/golden/dispatch.json
index a7bb543..345b077 100644
--- a/packages/meshbay-node/tests/golden/dispatch.json
+++ b/packages/meshbay-node/tests/golden/dispatch.json
@@ -84,14 +84,6 @@
"_admin_exec_gek_rotate"
]
},
- "group_attach": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_admin_exec_group_attach"
- ]
- },
"group_detach": {
"audit": [],
"log": [],
@@ -160,14 +152,6 @@
],
"spawned": []
},
- "root_add": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_admin_exec_root_add"
- ]
- },
"root_eject": {
"audit": [],
"log": [],
@@ -192,14 +176,6 @@
"_admin_exec_root_remove"
]
},
- "root_update": {
- "audit": [],
- "log": [],
- "sent": [],
- "spawned": [
- "_admin_exec_root_update"
- ]
- },
"search_listed": {
"audit": [],
"log": [],
@@ -2068,7 +2044,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2391,7 +2367,7 @@
"subject": "gggggggggggggggggggggggggggggggg",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2410,7 +2386,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2429,7 +2405,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2448,7 +2424,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2718,7 +2694,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2732,7 +2708,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2746,7 +2722,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2760,7 +2736,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2774,7 +2750,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2788,7 +2764,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2802,7 +2778,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -2816,7 +2792,7 @@
"messages": [],
"req_id": 4242,
"type": "chat_hist_resp",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -8535,7 +8511,7 @@
"subject": "gggggggggggggggggggggggggggggggg",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -8554,7 +8530,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -8573,7 +8549,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -8592,7 +8568,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -8757,379 +8733,6 @@
"sent": [],
"spawned": []
},
- "group_attach | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | member | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing name or shared_dir",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | member | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | member | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing name or shared_dir",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "group_attach",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "{\"name\":\"['x']\",\"shared_dir\":\"['x']\",\"writable\":true}",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "group_attach | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "group_attach",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "{\"name\":\"7\",\"shared_dir\":\"7\",\"writable\":true}",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "group_attach | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "group_attach",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "{\"name\":\"x\",\"shared_dir\":\"x\",\"writable\":true}",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "group_attach_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_attach_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_attach_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_attach_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_attach_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_attach_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_attach_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "group_attach_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"group_detach | challenged | bare": {
"audit": [],
"log": [],
@@ -9300,7 +8903,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -9319,7 +8922,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -9338,7 +8941,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -11641,7 +11244,7 @@
"subject": "link:gggggggggggggggggggggggggggggggg",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -13740,7 +13343,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -13759,7 +13362,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -13778,7 +13381,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -14113,7 +13716,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -14132,7 +13735,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -14151,7 +13754,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16212,7 +15815,7 @@
"req_id": 4242,
"token": null,
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16227,7 +15830,7 @@
"x"
],
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16240,7 +15843,7 @@
"req_id": 4242,
"token": 7,
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16253,7 +15856,7 @@
"req_id": 4242,
"token": "x",
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16266,7 +15869,7 @@
"req_id": 4242,
"token": null,
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16281,7 +15884,7 @@
"x"
],
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16294,7 +15897,7 @@
"req_id": 4242,
"token": 7,
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16307,7 +15910,7 @@
"req_id": 4242,
"token": "x",
"type": "pong",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -16472,379 +16075,6 @@
"sent": [],
"spawned": []
},
- "root_add | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | member | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing group_id or path",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | member | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | member | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing group_id or path",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "['x']",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "root_add",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "{\"kind\":\"['x']\",\"name\":\"['x']\",\"path\":\"['x']\",\"removable\":true,\"writable\":true}",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "root_add | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "7",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "root_add",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "{\"kind\":\"7\",\"name\":\"7\",\"path\":\"7\",\"removable\":true,\"writable\":true}",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "root_add | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "x",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "root_add",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "{\"kind\":\"x\",\"name\":\"x\",\"path\":\"x\",\"removable\":true,\"writable\":true}",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "root_add_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_add_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_add_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_add_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_add_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_add_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_add_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_add_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_add_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"root_eject | challenged | bare": {
"audit": [],
"log": [],
@@ -17015,7 +16245,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17034,7 +16264,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17053,7 +16283,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17388,7 +16618,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17407,7 +16637,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17426,7 +16656,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17761,7 +16991,7 @@
"subject": "['x']",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17780,7 +17010,7 @@
"subject": "7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17799,7 +17029,7 @@
"subject": "x",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -17964,379 +17194,6 @@
"sent": [],
"spawned": []
},
- "root_update | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | member | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing group_id or root_name",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | member | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | member | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | member | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "No authorized key for this",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | operator | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Missing group_id or root_name",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update | operator | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "['x']",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "root_update",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "['x']:rw=on,rem=on",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "root_update | operator | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "7",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "root_update",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "7:rw=on,rem=on",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "root_update | operator | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "group_id": "x",
- "node_pk": "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=",
- "nonce": "<volatile>",
- "op": "root_update",
- "op_id": "<volatile>",
- "req_id": 4242,
- "subject": "x:rw=on,rem=on",
- "ts": "<volatile>",
- "type": "admin_challenge",
- "v": "5.0"
- }
- ],
- "spawned": []
- },
- "root_update_ack | challenged | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | challenged | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | challenged | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | challenged | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | fresh | bare": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | fresh | lists": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | fresh | numbers": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | fresh | strings": {
- "audit": [],
- "log": [],
- "sent": [
- {
- "detail": "Handshake required",
- "req_id": 4242,
- "type": "error"
- }
- ],
- "spawned": []
- },
- "root_update_ack | member | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_update_ack | member | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_update_ack | member | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_update_ack | member | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_update_ack | operator | bare": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_update_ack | operator | lists": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_update_ack | operator | numbers": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
- "root_update_ack | operator | strings": {
- "audit": [],
- "log": [
- "WARNING Unknown MNP message type on DataChannel: %s"
- ],
- "sent": [],
- "spawned": []
- },
"roster_read | challenged | bare": {
"audit": [],
"log": [],
@@ -21119,7 +19976,7 @@
"subject": "{\"language\":null,\"token\":null}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -21162,7 +20019,7 @@
"subject": "{\"language\":\"x\",\"token\":\"sha256:2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881\"}",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
@@ -23049,7 +21906,7 @@
"subject": "d=7,u=7",
"ts": "<volatile>",
"type": "admin_challenge",
- "v": "5.0"
+ "v": "6.0"
}
],
"spawned": []
diff --git a/packages/meshbay-node/tests/test_admin_challenge_bounds.py b/packages/meshbay-node/tests/test_admin_challenge_bounds.py
index fd3b2f1..9dcb750 100644
--- a/packages/meshbay-node/tests/test_admin_challenge_bounds.py
+++ b/packages/meshbay-node/tests/test_admin_challenge_bounds.py
@@ -3,8 +3,9 @@ What a connection may leave waiting for a signature (docs/MESHBAY_DESIGN.md §13
Anyone authenticated can ask for an admin challenge — the signature is checked
later — so a member who never answers must not make the node keep every request.
-Measured before the bound: 200 `root_add` of 1 MiB each from a plain member held
-200 pending operations and ~400 MiB for the life of the connection.
+Measured before the bound: 200 `root_add` (an op since removed) of 1 MiB each
+from a plain member held 200 pending operations and ~400 MiB for the life of
+the connection.
"""
import struct
@@ -48,23 +49,24 @@ def _member_session():
return s
-def _root_add(s, path: str) -> dict:
- s._dispatch_message({"type": "root_add", "group_id": GROUP, "path": path})
+def _ask(s, path: str) -> dict:
+ """A signed op whose subject is whatever the caller sends."""
+ s._dispatch_message({"type": "chat_directory", "path": path})
return s._channel.sent[-1]
def test_a_member_cannot_pile_up_challenges():
s = _member_session()
for i in range(MAX_PENDING_ADMIN_OPS):
- assert _root_add(s, f"/srv/{i}")["type"] == "admin_challenge"
- refused = _root_add(s, "/srv/one-too-many")
+ assert _ask(s, f"/srv/{i}")["type"] == "admin_challenge"
+ refused = _ask(s, "/srv/one-too-many")
assert refused["type"] == "error" and refused["code"] == "too_many_pending"
assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS
def test_an_oversized_request_is_not_kept():
s = _member_session()
- refused = _root_add(s, "x" * (MAX_ADMIN_OP_BYTES + 1))
+ refused = _ask(s, "x" * (MAX_ADMIN_OP_BYTES + 1))
assert refused["type"] == "error" and refused["code"] == "too_large"
assert s._admin_ops == {}
@@ -72,21 +74,21 @@ def test_an_oversized_request_is_not_kept():
def test_an_expired_challenge_frees_its_place():
s = _member_session()
for i in range(MAX_PENDING_ADMIN_OPS):
- _root_add(s, f"/srv/{i}")
+ _ask(s, f"/srv/{i}")
for pending in s._admin_ops.values():
pending["ts"] -= 10_000
- assert _root_add(s, "/srv/after-expiry")["type"] == "admin_challenge"
+ assert _ask(s, "/srv/after-expiry")["type"] == "admin_challenge"
assert len(s._admin_ops) == 1
def test_answering_a_challenge_frees_its_place():
s = _member_session()
for i in range(MAX_PENDING_ADMIN_OPS):
- _root_add(s, f"/srv/{i}")
+ _ask(s, f"/srv/{i}")
op_id = next(iter(s._admin_ops))
s._dispatch_message({"type": "admin_response", "op_id": op_id, "signature": "!!"})
assert len(s._admin_ops) == MAX_PENDING_ADMIN_OPS - 1
- assert _root_add(s, "/srv/next")["type"] == "admin_challenge"
+ assert _ask(s, "/srv/next")["type"] == "admin_challenge"
assert all(time.time() - p["ts"] < 5 for p in s._admin_ops.values())
@@ -95,27 +97,6 @@ def test_answering_a_challenge_frees_its_place():
# The signature covers the subject and nothing else of a request, so every value
# the executor acts on has to be in it.
-def test_root_add_signs_whether_members_may_write():
- from meshbay_common.adminop import root_add_subject
- s = _member_session()
- s._dispatch_message({"type": "root_add", "group_id": GROUP, "path": "/srv/drop",
- "name": "Drop", "writable": True, "removable": False})
- challenge = s._channel.sent[-1]
- assert challenge["subject"] == root_add_subject("/srv/drop", "Drop", "generic",
- True, False)
- assert challenge["subject"] != root_add_subject("/srv/drop", "Drop", "generic",
- False, False)
-
-
-def test_group_attach_signs_the_directory_it_exposes():
- from meshbay_common.adminop import group_attach_subject
- s = _member_session()
- s._dispatch_message({"type": "group_attach", "name": "photos",
- "shared_dir": "/home/me/Photos"})
- assert s._channel.sent[-1]["subject"] == group_attach_subject(
- "photos", "/home/me/Photos", True)
-
-
def test_invite_create_signs_the_name_it_records():
from meshbay_common.adminop import invite_create_subject
s = _member_session()
diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py
index bd63f8e..257c60c 100644
--- a/packages/meshbay-node/tests/test_node_status.py
+++ b/packages/meshbay-node/tests/test_node_status.py
@@ -1,9 +1,10 @@
"""
-node_status, root_add, root_remove over MNP.
+node_status and root_remove over MNP.
These test the D5 node management panel's server-side behaviour: the admin
identity check on node_status, the list_groups operation, and the root
-add/remove flows through the MNP handlers.
+removal flow through the MNP handlers. Adding a root is not an MNP message
+(MNP 6.0); `ops.add_root` is still tested here, as the loopback and CLI use it.
"""
import base64
@@ -478,32 +479,6 @@ async def test_remove_root_succeeds_with_two_roots(tmp_path):
assert cfg.roots[0].name == "dir1"
-# ── root_add MNP handler ───────────────────────────────────────────────────
-
-async def test_root_add_issues_challenge(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- session._do_root_add({"group_id": GROUP, "path": "/tmp/test"})
- msg = _last(session)
- assert msg["type"] == "admin_challenge"
-
-
-async def test_root_add_refuses_without_authority(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False)
- session._do_root_add({"group_id": GROUP, "path": "/tmp/test"})
- msg = _last(session)
- assert msg["type"] == "error"
- assert "authorized" in msg["detail"].lower()
-
-
-async def test_root_add_refuses_missing_fields(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- session._do_root_add({"group_id": GROUP})
- assert _last(session)["type"] == "error"
- assert "Missing" in _last(session)["detail"]
-
-
# ── root_remove MNP handler ────────────────────────────────────────────────
async def test_root_remove_issues_challenge(tmp_path, roster):
@@ -677,33 +652,6 @@ async def test_denylist_clear_refused_for_non_admin(tmp_path, roster):
assert msg["type"] == "error"
-# ── group_attach MNP handler ────────────────────────────────────────────
-
-async def test_group_attach_issues_challenge(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- session._do_group_attach({"name": "test-group", "shared_dir": "/tmp/share"})
- msg = _last(session)
- assert msg["type"] == "admin_challenge"
-
-
-async def test_group_attach_refused_without_authority(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=False)
- session._do_group_attach({"name": "test-group", "shared_dir": "/tmp/share"})
- msg = _last(session)
- assert msg["type"] == "error"
- assert "authorized" in msg["detail"].lower()
-
-
-async def test_group_attach_refuses_missing_fields(tmp_path, roster):
- session = await _session(tmp_path, roster, operator=True,
- node_user_id="grenet")
- session._do_group_attach({"name": "test-group"})
- msg = _last(session)
- assert msg["type"] == "error"
- assert "Missing" in msg["detail"]
-
-
# ── node_reload MNP handler ─────────────────────────────────────────────
async def test_node_reload_runs_for_admin(tmp_path, roster):
diff --git a/packages/meshbay-node/tests/test_root_writable_policy.py b/packages/meshbay-node/tests/test_root_writable_policy.py
index 0cd9af8..0dc5d6d 100644
--- a/packages/meshbay-node/tests/test_root_writable_policy.py
+++ b/packages/meshbay-node/tests/test_root_writable_policy.py
@@ -12,8 +12,9 @@ The properties this holds:
A member with an old tab open, or one speaking MNP directly, gets the same
answer. That half is pinned in `test_security_regressions.py`, next to the
overwrite properties it belongs with;
-* the setting is changed by a **signed** operator instruction, or it is a
- suggestion any member can undo;
+* the setting is changed **on the node's own machine** — the desktop
+ application over loopback, or the CLI — and never over MNP, where a signature
+ proves only that the operator's key signed (`test_sharing_is_local_only.py`);
* it is stored on the **node**, never the hub. A hub that could decide who
writes to the operator's disk would have authority over the node.
@@ -26,7 +27,7 @@ from pathlib import Path
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from meshbay_common.adminop import OP_ROOT_EJECT, OP_ROOT_PLUG, OP_ROOT_UPDATE
+from meshbay_common.adminop import OP_ROOT_EJECT, OP_ROOT_PLUG
from meshbay_common.crypto import generate_gek
from meshbay_common.protocol import MNP
from meshbay_node.indexer.group_index import GroupIndex
@@ -163,34 +164,6 @@ def _capture_challenges(session) -> list[tuple[str, str]]:
return issued
-async def test_changing_a_roots_flags_needs_a_signature(tmp_path):
- """The flags are not applied by the request — only by the signed response."""
- session = _session(tmp_path, "the-operator", operator="the-operator")
- issued = _capture_challenges(session)
-
- session._do_root_update({"group_id": "g" * 32, "root_name": "shared",
- "writable": False})
-
- assert [op for op, _ in issued] == [OP_ROOT_UPDATE]
- assert session._ctx["roots"].roots[0].writable is True, (
- "applied before it was signed")
-
-
-async def test_the_subject_names_the_outcome_not_the_operation(tmp_path):
- """
- The operator is shown the subject before signing, so it has to say what will
- be true afterwards. "shared" alone would have them authorize a change they
- cannot see the direction of.
- """
- session = _session(tmp_path, "op", operator="op")
- issued = _capture_challenges(session)
-
- session._do_root_update({"group_id": "g" * 32, "root_name": "shared",
- "writable": True, "removable": True})
-
- assert issued == [(OP_ROOT_UPDATE, "shared:rw=on,rem=on")]
-
-
async def test_eject_and_plug_are_signed_too(tmp_path):
"""
Hiding a group's whole library from every member is not a lesser act than
@@ -214,8 +187,7 @@ async def test_a_request_with_nobody_to_authorize_it_is_refused(tmp_path):
issued = _capture_challenges(session)
session._has_admin_authority = lambda: False
- session._do_root_update({"group_id": "g" * 32, "root_name": "shared",
- "writable": True})
+ session._do_root_eject({"group_id": "g" * 32, "root_name": "shared"})
assert issued == []
assert [m for m in session.sent if m.get("type") == "error"]
diff --git a/packages/meshbay-node/tests/test_sharing_is_local_only.py b/packages/meshbay-node/tests/test_sharing_is_local_only.py
new file mode 100644
index 0000000..cd550e4
--- /dev/null
+++ b/packages/meshbay-node/tests/test_sharing_is_local_only.py
@@ -0,0 +1,109 @@
+"""
+What of the operator's disk is shared, and who may write there, is decided on
+the node's own machine — never over MNP (MNP 6.0).
+
+A signed operation proves that the operator's key signed, not that the operator
+meant it: in a browser the key is driven by code the hub serves, and in the
+desktop application by a renderer that parses content from nodes. `root_add`,
+`root_update` and `group_attach` let either of them share any folder on the
+machine, or open one to writes, from anywhere. They are gone; the loopback API
+(behind a native dialog in the desktop application) and the CLI remain.
+
+And the consequence that has to hold for the operator's list to stay true: a
+flag changed through the loopback API reaches every connected page, which the
+MNP ack used to do.
+"""
+
+from dataclasses import asdict
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from meshbay_common import adminop
+from meshbay_common.protocol import MNP
+from meshbay_node import ops
+from meshbay_node.config import GroupConfig, RootSpec
+from meshbay_node.indexer.indexer import DirectoryIndexer
+from meshbay_node.roots import RootSet
+from meshbay_node.transport.webrtc.admin import _ADMIN_EXECUTORS
+from meshbay_node.transport.webrtc.dispatch import _HANDLERS
+from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from test_admin_challenge_bounds import _PC, _Channel
+
+GROUP = "g" * 32
+GONE = ("root_add", "root_update", "group_attach")
+
+
+@pytest.mark.parametrize("mtype", GONE)
+def test_no_message_widens_what_the_node_shares(mtype):
+ assert mtype not in _HANDLERS, f"{mtype} is dispatched again"
+ assert mtype not in _ADMIN_EXECUTORS, f"{mtype} can be executed again"
+ assert mtype not in vars(MNP).values(), f"{mtype} is back in the protocol"
+ assert mtype not in vars(adminop).values(), f"{mtype} is a signed op again"
+
+
+@pytest.mark.parametrize("mtype", GONE)
+def test_an_older_client_asking_is_issued_nothing_to_sign(mtype):
+ """A 5.x client still sends these. Nothing it sends may come back as a
+ challenge — a challenge is what a compromised page needs signed."""
+ ctx = {"sk_node": Ed25519PrivateKey.from_private_bytes(b"\x01" * 32),
+ "groups": {GROUP: {}}, "has_admin_authority": True}
+ s = WebRTCPeerSession(_PC(), ctx, peer_id="peer")
+ s._channel = _Channel()
+ s._audit = lambda *a, **k: None
+ s._user_id, s._group_id = "operator", GROUP
+
+ s._dispatch_message({"type": mtype, "group_id": GROUP, "path": "/home/someone",
+ "shared_dir": "/home/someone", "name": "x",
+ "root_name": "x", "writable": True})
+
+ assert s._admin_ops == {}
+ assert not [m for m in s._channel.sent if m.get("type") == "admin_challenge"]
+
+
+def _state(tmp_path: Path, indexer_roots: RootSet | None = None):
+ (tmp_path / "media").mkdir()
+ cfg = GroupConfig(id=GROUP, name="plop",
+ roots=[RootSpec(path=str(tmp_path / "media"), name="media")])
+ conf = tmp_path / "node.toml"
+ conf.write_text(
+ f'[[groups]]\nid = "{GROUP}"\nname = "plop"\n\n'
+ f' [[groups.roots]]\n path = "{(tmp_path / "media").as_posix()}"\n'
+ f' name = "media"\n', encoding="utf-8")
+ live = RootSet.build([asdict(r) for r in cfg.roots])
+ pushed: list[list[dict]] = []
+
+ async def on_change(indexer):
+ pushed.append(indexer.index.roots)
+
+ indexer = DirectoryIndexer(indexer_roots or live, GROUP,
+ Ed25519PrivateKey.generate(), None,
+ on_change=on_change)
+ state = {"config": SimpleNamespace(groups=[cfg]), "config_path": str(conf),
+ "groups_ctx": {GROUP: {"roots": live}},
+ "indexers": {GROUP: indexer}}
+ return state, pushed
+
+
+async def test_a_flag_changed_on_the_node_reaches_every_open_page(tmp_path):
+ state, pushed = _state(tmp_path)
+
+ await ops.update_root(state, GROUP, "media", writable=True)
+
+ assert pushed, "nothing was pushed — open pages keep the old flag"
+ assert pushed[-1][0]["writable"] is True
+
+
+async def test_the_pushed_table_is_right_when_the_indexer_holds_its_own_set(tmp_path):
+ """The indexer and the group context normally share one RootSet; when they
+ do not, the table peers receive is the indexer's, and must carry the flag."""
+ (tmp_path / "media").mkdir()
+ own = RootSet.build([{"path": str(tmp_path / "media"), "name": "media"}])
+ (tmp_path / "media").rmdir()
+ state, pushed = _state(tmp_path, indexer_roots=own)
+
+ await ops.update_root(state, GROUP, "media", removable=True)
+
+ assert pushed[-1][0]["removable"] is True
+ assert state["groups_ctx"][GROUP]["roots"].by_name("media").removable is True