tommytracx's picture
v1.1.0: CPU-safe GPU detection, real GPU offload, hardened fallible calls, doctor command
dc03fa5 verified
Raw
History Blame Contribute Delete
6.83 kB
"""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())