aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/daemon.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py66
1 files changed, 54 insertions, 12 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 848fc9f..6fafc74 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -98,8 +98,9 @@ class _WsSender:
# ── Daemon ────────────────────────────────────────────────────────────────────
class NodeDaemon:
- def __init__(self, config: Config):
+ def __init__(self, config: Config, config_path: Path = DEFAULT_CONFIG_PATH):
self._config = config
+ self._config_path = config_path
self._state: dict = {
"status": "starting",
"hub_url": config.hub.url,
@@ -135,6 +136,9 @@ class NodeDaemon:
# 2. Start admin UI early (so operator can copy node key before hub login)
self._state["pk_node_ed25519"] = keys.pk_ed25519_b64
self._state["config"] = self._config
+ # Where it came from, so `group add` appends to the file this process
+ # actually read rather than guessing at the default.
+ self._state["config_path"] = str(self._config_path)
# Per-run token for the local admin UI (11.5.3). Not a password: it keeps
# other local processes and rebound browser pages out of an API that can
# re-initialise group keys.
@@ -579,7 +583,7 @@ class NodeDaemon:
# ── CLI helpers ───────────────────────────────────────────────────────────────
def _daemon_api(cfg: Config, path: str, method: str = "GET",
- timeout: int = 30) -> dict:
+ timeout: int = 30, body: dict | None = None) -> dict:
"""
Call the daemon's loopback API.
@@ -602,15 +606,23 @@ def _daemon_api(cfg: Config, path: str, method: str = "GET",
url = (f"http://127.0.0.1:{cfg.node.ui_port}{path}"
f"{sep}t={token_file.read_text().strip()}")
try:
- req = urllib.request.Request(url, method=method)
+ data = _json.dumps(body).encode() if body is not None else None
+ req = urllib.request.Request(
+ url, method=method, data=data,
+ headers={"Content-Type": "application/json"} if data else {})
with urllib.request.urlopen(req, timeout=timeout) as r:
return _json.loads(r.read())
except urllib.error.HTTPError as e:
- body = e.read().decode()[:300]
+ raw = e.read().decode()[:600]
try:
- detail = _json.loads(body).get("error", body)
+ parsed = _json.loads(raw)
+ detail = parsed.get("error", raw)
+ # Endpoints that refuse a name offer the ones that would work; a bare
+ # "no such group" leaves the operator guessing at a UUID.
+ for row in parsed.get("available", []):
+ detail += f"\n {row.get('name', ''):<24} {row.get('id', '')}"
except Exception:
- detail = body
+ detail = raw
print(f"failed: {detail}")
sys.exit(1)
except Exception as e:
@@ -663,15 +675,20 @@ def main() -> None:
parser = argparse.ArgumentParser(description="MeshBay Node daemon")
parser.add_argument("command", nargs="?",
choices=["init", "status", "ui", "gek-init", "operator",
- "member", "calibrate-argon2"],
+ "member", "group", "calibrate-argon2"],
help="init: write example config | status: node state and keys "
"| ui: print the admin UI URL | operator pair: pair a "
"browser with this node | member list|invite|revoke|unpin "
- "| calibrate-argon2: benchmark")
+ "| group add <name> --dir <path>: host another of your "
+ "groups | calibrate-argon2: benchmark")
parser.add_argument("subcommand", nargs="?",
- help="'pair' for operator; list|invite|revoke|unpin for member")
+ help="'pair' for operator; list|invite|revoke|unpin for "
+ "member; 'add' for group")
parser.add_argument("target", nargs="?",
- help="username, for member invite|revoke|unpin")
+ help="username for member invite|revoke|unpin; "
+ "group name for group add")
+ parser.add_argument("--dir", default=None,
+ help="shared directory, for group add")
parser.add_argument("--config", type=Path, default=None,
help="Config file path")
parser.add_argument("--group", default=None,
@@ -681,7 +698,8 @@ def main() -> None:
args = parser.parse_args()
# Query commands print a report; library logging would interleave with it.
- quiet = args.command in ("status", "ui", "gek-init", "operator", "member")
+ quiet = args.command in ("status", "ui", "gek-init", "operator", "member",
+ "group")
logging.basicConfig(
level=logging.ERROR if quiet else getattr(logging, args.log_level),
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
@@ -868,6 +886,30 @@ def main() -> None:
print(f" ! {err}")
return
+ if args.command == "group":
+ if args.subcommand != "add":
+ print("usage: meshbay-node group add <name> --dir <path>")
+ sys.exit(1)
+ if not args.target or not args.dir:
+ print("usage: meshbay-node group add <name> --dir <path>")
+ print()
+ print("The group must already exist on the hub and be yours. This")
+ print("only tells the node to host it, and picks the directory.")
+ sys.exit(1)
+
+ cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
+ out = _daemon_api(cfg, "/api/groups/attach", method="POST",
+ body={"name": args.target, "shared_dir": args.dir})
+ print(f"{out['name']} ({out['group_id'][:8]}) added to {out['config']}")
+ print(f" shared_dir {out['shared_dir']}")
+ print()
+ print("Restart the daemon so it picks the group up, then give it a key:")
+ print(f" meshbay-node gek-init --group {out['name']}")
+ print()
+ print("The key is this group's own — members of your other groups cannot")
+ print("read it, and joining one says nothing about the other.")
+ return
+
if args.command == "operator":
if args.subcommand != "pair":
print("usage: meshbay-node operator pair")
@@ -916,7 +958,7 @@ def main() -> None:
print("Error: hub.username not set in config. Run: meshbay-node init")
sys.exit(1)
- daemon = NodeDaemon(cfg)
+ daemon = NodeDaemon(cfg, Path(args.config or DEFAULT_CONFIG_PATH))
asyncio.run(daemon.run())