"""CLI entry point: ``python -m thoxmesh_node``. Subcommands ----------- ``serve`` start a node (the default) ``keygen`` mint an Ed25519 device identity for pairing ``verify`` check config and mesh reachability without registering """ from __future__ import annotations import argparse import json import logging import os import sys from .config import detect_node_kind, load_config from .errors import NodeError from .signing import DeviceIdentity def _cmd_serve(args: argparse.Namespace) -> int: from .agent import run run(echo=args.echo, tunnel_provider=args.tunnel, block=True) return 0 def _cmd_keygen(args: argparse.Namespace) -> int: """Generate a device keypair. Only the public key leaves the node: it goes to the mesh owner, who attaches it to a ``meshstack_devices_v2`` row through a pairing session. The private seed stays with the node and is set as ``THOX_MESH_DEVICE_KEY``. """ identity = DeviceIdentity.generate(args.device_id or "00000000-0000-0000-0000-000000000000") payload = { "device_id": identity.device_id, "identity_public_key": identity.public_key_base64(), "private_seed_base64": identity.private_seed_base64(), "note": ( "Give identity_public_key to the mesh owner for pairing. Keep " "private_seed_base64 secret: set it as THOX_MESH_DEVICE_KEY. Never commit it." ), } json.dump(payload, sys.stdout, indent=2) sys.stdout.write("\n") return 0 def _cmd_doctor(args: argparse.Namespace) -> int: """Check the environment and report, without starting anything. Every check is individually guarded, because the whole point of this command is to explain a broken environment — it must not itself fall over on one. Exit 0 means the node can serve; CPU-only counts as healthy. """ from .gpu import environment_report lines: list[str] = [] errors = 0 def check(label: str, fn) -> None: nonlocal errors try: ok, detail = fn() except Exception as exc: # noqa: BLE001 - a failing check IS the finding lines.append(f" [ERROR] {label}: {type(exc).__name__}: {exc}") errors += 1 return lines.append(f" [{'ok ' if ok else 'WARN'}] {label}: {detail}") def _python(): return sys.version_info >= (3, 10), sys.version.split()[0] def _crypto(): import cryptography return True, f"cryptography {cryptography.__version__}" def _web(): import fastapi import uvicorn return True, f"fastapi {fastapi.__version__}, uvicorn {uvicorn.__version__}" def _llama(): try: import llama_cpp except ImportError: return False, "not installed - only the echo backend will work" return True, f"llama_cpp {getattr(llama_cpp, '__version__', 'unknown')}" def _hub(): try: import huggingface_hub except ImportError: return False, "not installed - cannot download models" has_token = bool(os.environ.get("HF_TOKEN")) state = "set" if has_token else "NOT SET (private THOX repos will fail)" return has_token, f"huggingface_hub {huggingface_hub.__version__}, HF_TOKEN {state}" def _gpu(): report = environment_report() if report["gpu_available"]: return True, f"{report['gpu_name']} - layers will be offloaded" # Not a failure: a CPU node is a full member of the mesh. return True, f"CPU-only ({report['gpu_detail']}) - supported" def _tunnel(): import shutil as _shutil return True, _shutil.which("cloudflared") or "not installed (downloaded on demand)" def _config(): config = load_config() return True, ( f"{config.model_id} via {config.transport}, " f"gpu_layers={config.n_gpu_layers}, ttl={config.ttl_seconds}s" ) for label, fn in ( ("python", _python), ("cryptography", _crypto), ("web stack", _web), ("llama-cpp-python", _llama), ("huggingface_hub", _hub), ("gpu", _gpu), ("cloudflared", _tunnel), ("config", _config), ): check(label, fn) print(f"thoxmesh_node doctor (v{_version()})") print("\n".join(lines)) if errors: print(f"\n{errors} check(s) errored - fix these before starting the node.") return 1 print("\nEnvironment looks serviceable.") return 0 def _version() -> str: from . import __version__ return __version__ def _cmd_verify(args: argparse.Namespace) -> int: """Print the resolved configuration without touching the mesh.""" config = load_config() print(json.dumps({ "node_kind": config.node_kind if config.node_kind != "unknown" else detect_node_kind(), "model_id": config.model_id, "alias": config.alias, "protocol": config.protocol, "transport": config.transport, "device_function_url": config.device_function_url, "controller_url": config.controller_url or None, "heartbeat_seconds": config.heartbeat_seconds, "ttl_seconds": config.ttl_seconds, "capabilities": config.capabilities, "public_url": config.public_url or "(resolved at runtime)", }, indent=2)) return 0 def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="thoxmesh_node", description="THOX mesh node agent") sub = parser.add_subparsers(dest="command") serve = sub.add_parser("serve", help="serve a model and join the mesh") serve.add_argument("--echo", action="store_true", help="use the deterministic echo backend") serve.add_argument("--tunnel", default="cloudflared", choices=["cloudflared", "ngrok"]) serve.set_defaults(func=_cmd_serve) keygen = sub.add_parser("keygen", help="generate an Ed25519 device identity") keygen.add_argument("--device-id", default="", help="device UUID issued by the mesh owner") keygen.set_defaults(func=_cmd_keygen) verify = sub.add_parser("verify", help="print resolved config") verify.set_defaults(func=_cmd_verify) doctor = sub.add_parser("doctor", help="check the environment and report problems") doctor.set_defaults(func=_cmd_doctor) args = parser.parse_args(argv) if not getattr(args, "func", None): args = parser.parse_args((argv or []) + ["serve"]) logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s") try: return args.func(args) except NodeError as exc: print(f"error: {exc}", file=sys.stderr) return 1 except KeyboardInterrupt: return 130 if __name__ == "__main__": raise SystemExit(main())