diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/daemon.py | 66 |
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()) |