feat(protocol): implement unified protocol_muse package, wheel caching, and rebuild recovery

This commit is contained in:
operator
2026-10-10 15:38:25 +00:00
parent 6c2efb2d3d
commit a4237527f1
20 changed files with 746 additions and 7 deletions
@@ -0,0 +1,15 @@
"""protocol_muse — Unified Muse Noise Protocol & Chromebox Gateway Client."""
from .client import MuseClient
from .gateway import NoiseGateway
from .fallback import GatewayFallback
from .queue import TaskItem, QueueSnapshot
__version__ = "0.1.0"
__all__ = [
"MuseClient",
"NoiseGateway",
"GatewayFallback",
"TaskItem",
"QueueSnapshot",
]
@@ -0,0 +1,70 @@
"""Unified MuseClient implementation."""
from typing import Dict, Any, Optional, List
from .gateway import NoiseGateway
from .fallback import GatewayFallback
from .queue import QueueSnapshot
class MuseClient:
"""Unified client for the NetVM Muse fleet.
Tries the primary Noise WebSocket gateway first, seamlessly falling back
to the Chromebox HTTPS gateway on error or timeout.
"""
def __init__(self, node: str = "muse", gateway_url: Optional[str] = None, token: Optional[str] = None):
self.node = node
self.gateway = NoiseGateway(node=node)
self.fallback = GatewayFallback(endpoint=gateway_url, token=token)
def list_threads(self) -> List[Dict[str, Any]]:
"""List active threads for this node."""
threads, err = self.gateway.list_threads()
if threads is not None:
return threads
# Fallback to HTTPS gateway
res = self.fallback.execute_op("chat.sidechats", {"agent": self.node})
if res.get("ok"):
try:
import json
return json.loads(res.get("stdout", "[]"))
except Exception:
return [{"raw": res.get("stdout", "")}]
return []
def send_message(self, message: str, thread_id: Optional[str] = None) -> Dict[str, Any]:
"""Send message via primary gateway or fallback."""
res, err = self.gateway.send_message(message=message, thread_id=thread_id)
if res is not None:
return res
# Fallback to HTTPS gateway
params = {"agent": self.node, "message": message}
if thread_id:
params["target"] = thread_id
return self.fallback.execute_op("chat.send", params)
def send_dm(self, to: str, message: str, target: str = "main", tags: Optional[List[str]] = None) -> Dict[str, Any]:
"""Send signed peer DM via HTTPS gateway."""
params = {
"agent": self.node,
"to": to,
"message": message,
"target": target,
"tags": tags or [],
}
return self.fallback.execute_op("dm.send", params)
def read_dms(self, target: str = "main", n: int = 5) -> Dict[str, Any]:
"""Read recent DMs via HTTPS gateway."""
params = {
"agent": self.node,
"target": target,
"n": n,
}
return self.fallback.execute_op("dm.read", params)
def get_queue(self) -> QueueSnapshot:
"""Query fleet task queue status."""
data = self.fallback.get_queue()
return QueueSnapshot.from_dict(data)
@@ -0,0 +1,61 @@
"""HTTPS fallback transport to Chromebox Gateway."""
import json
import os
import ssl
import urllib.request
import urllib.error
from typing import Dict, Any, Optional
class GatewayFallback:
"""Communicates with the Chromebox HTTPS gateway."""
def __init__(self, endpoint: Optional[str] = None, token: Optional[str] = None):
self.endpoint = endpoint or os.environ.get("CHROMEBOX_GATEWAY_URL", "https://100.123.153.75:8445")
self.token = token or self._resolve_token()
self._ctx = ssl.create_default_context()
self._ctx.check_hostname = False
self._ctx.verify_mode = ssl.CERT_NONE
def _resolve_token(self) -> str:
# Check env first
if "MUSE_GATEWAY_TOKEN" in os.environ:
return os.environ["MUSE_GATEWAY_TOKEN"]
# Check master token
master_path = os.path.expanduser("~/.exec-server-token")
if os.path.exists(master_path):
try:
with open(master_path) as f:
return f.read().strip()
except Exception:
pass
return ""
def _request(self, path: str, method: str = "GET", data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = f"{self.endpoint.rstrip('/')}{path}"
body_bytes = json.dumps(data).encode("utf-8") if data is not None else None
headers = {"Content-Type": "application/json"}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
req = urllib.request.Request(url, data=body_bytes, headers=headers, method=method)
try:
with urllib.request.urlopen(req, context=self._ctx, timeout=15) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
try:
err_data = json.loads(e.read().decode("utf-8"))
return {"ok": False, "status": e.code, "error": err_data}
except Exception:
return {"ok": False, "status": e.code, "error": str(e)}
except Exception as e:
return {"ok": False, "error": str(e)}
def check_health(self) -> Dict[str, Any]:
return self._request("/health", method="GET")
def get_queue(self) -> Dict[str, Any]:
return self._request("/api/v1/queue", method="GET")
def execute_op(self, op: str, params: Dict[str, Any]) -> Dict[str, Any]:
payload = {"op": op, "params": params}
return self._request("/api/v1/op", method="POST", data=payload)
@@ -0,0 +1,61 @@
"""Noise WebSocket protocol gateway transport."""
import os
import sys
import subprocess
import json
from typing import Dict, Any, Optional, List, Tuple
class NoiseGateway:
"""Primary fast gateway transport connecting via Noise protocol."""
def __init__(self, node: str = "muse"):
self.node = node
self.netvm_bin = os.path.expanduser("~/Projects/NetVM/bin")
def _call_hybrid(self, func_name: str, *args, **kwargs) -> Tuple[Any, Optional[str]]:
"""Try calling muse_hybrid directly if available."""
try:
if self.netvm_bin not in sys.path:
sys.path.insert(0, self.netvm_bin)
import muse_hybrid
func = getattr(muse_hybrid, func_name, None)
if func:
return func(self.node, *args, **kwargs)
except Exception as e:
return None, str(e)
return None, "muse_hybrid not found"
def list_threads(self) -> Tuple[Optional[List[Dict[str, Any]]], Optional[str]]:
res, err = self._call_hybrid("get_threads")
if res is not None:
return res, None
# CLI fallback via muse-cli-node
cli_path = os.path.join(self.netvm_bin, "muse-cli-node")
if os.path.exists(cli_path):
try:
proc = subprocess.run([cli_path, self.node, "threads", "--json"],
capture_output=True, text=True, timeout=10)
if proc.returncode == 0:
return json.loads(proc.stdout), None
except Exception as e:
return None, str(e)
return None, err or "gateway unavailable"
def send_message(self, message: str, thread_id: Optional[str] = None) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
res, err = self._call_hybrid("send_message", message=message, thread_id=thread_id)
if res is not None:
return res, None
cli_path = os.path.join(self.netvm_bin, "muse-cli-node")
if os.path.exists(cli_path):
try:
cmd = [cli_path, self.node, "send", message]
if thread_id:
cmd += ["--thread", thread_id]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if proc.returncode == 0:
return {"ok": True, "output": proc.stdout}, None
except Exception as e:
return None, str(e)
return None, err or "gateway unavailable"
@@ -0,0 +1,45 @@
"""Task queue model for protocol_muse."""
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional
@dataclass
class TaskItem:
name: str
queue: str # "pending", "claimed", "done"
owner: Optional[str] = None
age_s: Optional[float] = None
@dataclass
class QueueSnapshot:
ok: bool
tasks: List[TaskItem] = field(default_factory=list)
counts: Dict[str, int] = field(default_factory=lambda: {"pending": 0, "claimed": 0, "done": 0})
@property
def pending_count(self) -> int:
return self.counts.get("pending", 0)
@property
def claimed_count(self) -> int:
return self.counts.get("claimed", 0)
@property
def done_count(self) -> int:
return self.counts.get("done", 0)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "QueueSnapshot":
tasks = []
for t in data.get("tasks", []):
tasks.append(TaskItem(
name=t.get("name", ""),
queue=t.get("queue", "pending"),
owner=t.get("owner"),
age_s=t.get("age_s"),
))
counts = data.get("counts", {
"pending": sum(1 for t in tasks if t.queue == "pending"),
"claimed": sum(1 for t in tasks if t.queue == "claimed"),
"done": sum(1 for t in tasks if t.queue == "done"),
})
return cls(ok=bool(data.get("ok", True)), tasks=tasks, counts=counts)