Spaces:
Paused
Paused
v1.1.0: CPU-safe GPU detection, real GPU offload, hardened fallible calls, doctor command
dc03fa5 verified | """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()) | |