Spaces:
Paused
Paused
File size: 6,829 Bytes
6778532 dc03fa5 6778532 dc03fa5 6778532 dc03fa5 6778532 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | """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())
|