summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/roots.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-06 22:20:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-06 22:20:46 +0200
commitd6e1cc19a09df35988f6a90c226c94f2fdf6b209 (patch)
tree422c8e5b9f380ecc803545a0f0e9a1b1bcc9648c /packages/meshbay-node/src/meshbay_node/roots.py
parentf3fb449f3a943096a2569dc383f2819a612bccd5 (diff)
downloadmeshbay-d6e1cc19a09df35988f6a90c226c94f2fdf6b209.tar.gz
fix(node): `root list` printed "?" for every path
`RootSet.describe()` feeds two audiences that want opposite things. The index payload goes to every member and has always deliberately carried no paths — a member is told what exists and whether it is readable, not that the library sits under someone's home directory. The loopback API answers the operator themselves, over a channel that already requires their machine and the run token, and the path is exactly what they asked for. The `root list` CLI I added in Phase 1 read `path` from the member form, so it printed a placeholder for every directory. Nothing caught it: the CLI reads a dict, the API returns a dict, and neither end states which keys it owes. `describe(with_paths=True)` is the operator's view and `list_groups` is the only caller. The shared-directories table has the same hole over MNP — the roots there come from the index payload — so its Path column now appears only when a path is actually present, rather than rendering a column of blanks. The test asserts both halves, because they pull opposite ways: one that only checked the operator sees paths would be satisfied by leaking them to every member. It reads the indexer's source for the member side, and compares the CLI's key reads against what the payload offers for the other — checked to fail in each direction independently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/roots.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/roots.py18
1 files changed, 16 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/roots.py b/packages/meshbay-node/src/meshbay_node/roots.py
index ffe801c..d288231 100644
--- a/packages/meshbay-node/src/meshbay_node/roots.py
+++ b/packages/meshbay-node/src/meshbay_node/roots.py
@@ -357,8 +357,14 @@ class RootSet:
"available" if live else "UNAVAILABLE", root.path)
return changed
- def describe(self) -> list[dict]:
- """Per-root state for the index payload and the admin UI."""
+ def describe(self, *, with_paths: bool = False) -> list[dict]:
+ """
+ Per-root state for the index payload and the admin UI.
+
+ Deliberately no paths by default: this is what every member receives.
+ `with_paths=True` is the operator's own view, over a channel that is
+ already theirs alone (loopback + run token).
+ """
out = []
for r in self.roots:
d: dict = {"name": r.name, "kind": r.kind,
@@ -368,6 +374,14 @@ class RootSet:
"ejected": r.ejected,
# Backward compat for MNP 1.0 clients
"upload": r.writable}
+ # `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
+ # told what exists and whether it is readable, never where on the
+ # operator's disk it lives, and the index payload every member
+ # receives must keep calling this without the flag.
+ if with_paths:
+ d["path"] = str(r.path)
out.append(d)
return out