4998ffddb6
- bin/muse-tui.py: * Parse raw SGR 1006 (\033[<btn;x;yM/m) and Xterm mouse escape sequences in _handle_escape_sequence fallback. * Multi-trigger context menus: Button 3, Button 2, Ctrl/Shift/Alt+Click, double-click, click on active item, or click [⚡] / [sid] target. * Render permanent [⚡] action target across all sidebar thread rows. * Separate focus partitions for FLEET AGENTS and SIDECHATS with partition-specific wheel scrolling and keyboard navigation (j/k, Enter, h/l). * Space key support in NORMAL mode to open context menus. * Active pane highlighting and updated footer hints. - bin/super-cli.py: * Add 'box tui' command dispatching directly to muse-tui.py --mode box. - tests: * Add unit tests in test_context_menus.py, test_focus_highlight.py, and test_main_nav.py (51/51 passing).
5932 lines
258 KiB
Python
Executable File
5932 lines
258 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""super-cli.py — Unified Off-Board Orchestrator CLI for NetVM & Box.
|
||
|
||
Empowers 'off-board super' to govern, task, and observe the fleet of
|
||
autonomous browser agents (muse, pip, 646, opm) on bl and interface
|
||
with the Google Cloud VM web surfaces.
|
||
|
||
Domains:
|
||
super muse-choices — Muse TUI A/B/C auto-answer daemon (on/off/status/logs)
|
||
super fleet — Node health, CDP status, active tabs, watch, restart, heal
|
||
super watchdog — Watchdog timers: status, trigger runs
|
||
super dm — Inter-agent DMs, work orders ([WO]), acks, live log tail
|
||
super thread — Inspect agent main chats, sidechats, and scrollbacks
|
||
super job — Manage scheduled jobs, systemd timers, triggers, logs
|
||
super web — Probe VM web surfaces (box.muse-dev.online), auth, sync
|
||
super loop — Intrinsic loop strategy, fleet loop health, break taxonomy, vars
|
||
super strat — Shortcut to inspect & configure loop modulation strategies
|
||
super vars — Shortcut to inspect & adjust runtime control variables
|
||
|
||
Usage:
|
||
super fleet [status|watch|restart|cdp|heal]
|
||
super watchdog [status|run <node|relay>]
|
||
super dm [send|wo|ack|log|tail]
|
||
super thread [list|view]
|
||
super job [list|status|run|log|create|enable|disable|delete]
|
||
super web [health|test-auth|sync]
|
||
super loop [status|health|breaks|strat|vars|close|nudge|sweep|harvest|remediate]
|
||
super strat [show|set|reset|eval]
|
||
super vars [list|get|set|reset|history|rollback]
|
||
"""
|
||
|
||
import argparse
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
import urllib.error
|
||
import urllib.request
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
# Paths
|
||
NETVM_ROOT = Path("/home/super/Projects/NetVM")
|
||
BIN_DIR = NETVM_ROOT / "bin"
|
||
JOBS_DIR = NETVM_ROOT / "jobs"
|
||
TRANSFERS_DIR = NETVM_ROOT / "transfers"
|
||
DM_LOG = NETVM_ROOT / "dm-log.jsonl"
|
||
JOB_LOG = NETVM_ROOT / "job-log.jsonl"
|
||
CTL_LOG = NETVM_ROOT / "box-ctl.jsonl"
|
||
REGISTRY_FILE = NETVM_ROOT / "NODES.md"
|
||
CHAT_HISTORY_LOG = NETVM_ROOT / "logs" / "chat-history.jsonl"
|
||
WATERMARKS_FILE = NETVM_ROOT / "siphon-watermarks.json"
|
||
FOLLOWUPS_FILE = NETVM_ROOT / "followups.json"
|
||
JOB_SIDECHATS_FILE = NETVM_ROOT / "job-sidechats.json"
|
||
RESPONSE_HARVESTER_PY = BIN_DIR / "response-harvester.py"
|
||
FOLLOWUP_SWEEPER_PY = BIN_DIR / "followup-sweeper.py"
|
||
PIPELINES_FILE = NETVM_ROOT / "pipelines.json"
|
||
JOB_DISPATCH_PY = BIN_DIR / "job-dispatch.py"
|
||
|
||
# Agent Constants
|
||
VALID_NODES = ["muse", "pip", "646", "opm", "def", "dev"]
|
||
DEFAULT_SENDER = "super"
|
||
DEFAULT_AGENT_SIDECHATS = {
|
||
"646": "646 tasks",
|
||
"opm": "heartbeat",
|
||
"pip": "646-pip-coord",
|
||
"muse": "muse tasks",
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# ANSI Color & Formatting Engine
|
||
# ---------------------------------------------------------------------------
|
||
USE_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
||
|
||
def _c(code: str, text: str) -> str:
|
||
return f"\033[{code}m{text}\033[0m" if USE_COLOR else str(text)
|
||
|
||
def c_bold(s: str) -> str: return _c("1", s)
|
||
def c_dim(s: str) -> str: return _c("2", s)
|
||
def c_green(s: str) -> str: return _c("32", s)
|
||
def c_red(s: str) -> str: return _c("31", s)
|
||
def c_yellow(s: str) -> str: return _c("33", s)
|
||
def c_blue(s: str) -> str: return _c("34", s)
|
||
def c_magenta(s: str) -> str: return _c("35", s)
|
||
def c_cyan(s: str) -> str: return _c("36", s)
|
||
|
||
def badge_ok(s: str = "UP") -> str: return c_green(f"● {s}")
|
||
def badge_warn(s: str = "WARN") -> str: return c_yellow(f"● {s}")
|
||
def badge_err(s: str = "DOWN") -> str: return c_red(f"● {s}")
|
||
def badge_dim(s: str) -> str: return c_dim(f"○ {s}")
|
||
|
||
def print_table(headers: list, rows: list, alignments: list = None):
|
||
"""Print an aligned tabular display with clean ANSI formatting."""
|
||
if not rows:
|
||
print(c_dim(" (no records found)"))
|
||
return
|
||
|
||
num_cols = len(headers)
|
||
alignments = alignments or ["left"] * num_cols
|
||
|
||
# Calculate max visual length per column (stripping ANSI escapes)
|
||
strip_ansi = re.compile(r"\033\[[0-9;]*m")
|
||
|
||
def vlen(s):
|
||
return len(strip_ansi.sub("", str(s)))
|
||
|
||
col_widths = [vlen(h) for h in headers]
|
||
for row in rows:
|
||
for i in range(num_cols):
|
||
val = str(row[i]) if i < len(row) else ""
|
||
col_widths[i] = max(col_widths[i], vlen(val))
|
||
|
||
# Header
|
||
hdr_parts = []
|
||
sep_parts = []
|
||
for i, h in enumerate(headers):
|
||
w = col_widths[i]
|
||
align = alignments[i]
|
||
pad = w - vlen(h)
|
||
if align == "right":
|
||
hdr_parts.append(" " * pad + c_bold(h))
|
||
else:
|
||
hdr_parts.append(c_bold(h) + " " * pad)
|
||
sep_parts.append("─" * w)
|
||
|
||
print(" " + " ".join(hdr_parts))
|
||
print(" " + c_dim(" ".join(sep_parts)))
|
||
|
||
# Rows
|
||
for row in rows:
|
||
row_parts = []
|
||
for i in range(num_cols):
|
||
val = str(row[i]) if i < len(row) else ""
|
||
w = col_widths[i]
|
||
align = alignments[i]
|
||
pad = max(0, w - vlen(val))
|
||
if align == "right":
|
||
row_parts.append(" " * pad + val)
|
||
else:
|
||
row_parts.append(val + " " * pad)
|
||
print(" " + " ".join(row_parts))
|
||
|
||
def parse_relative_time(ts_str: str) -> str:
|
||
"""Convert ISO timestamp to short relative time (e.g. 5m ago, 2h ago)."""
|
||
if not ts_str:
|
||
return "-"
|
||
try:
|
||
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
||
now = datetime.now(timezone.utc)
|
||
diff = int((now - dt).total_seconds())
|
||
if diff < 0:
|
||
return "just now"
|
||
if diff < 60:
|
||
return f"{diff}s ago"
|
||
if diff < 3600:
|
||
return f"{diff // 60}m ago"
|
||
if diff < 86400:
|
||
return f"{diff // 3600}h ago"
|
||
return f"{diff // 86400}d ago"
|
||
except Exception:
|
||
return ts_str[:16].replace("T", " ")
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Registry & Network Helpers
|
||
# ---------------------------------------------------------------------------
|
||
def get_node_network_info(node: str) -> dict:
|
||
"""Compute deterministic veth/peer IP and load pinned CDP port."""
|
||
tag = hashlib.sha256(node.encode()).hexdigest()[:8]
|
||
idx = int(tag[:3], 16) % 200 + 10
|
||
gw_ip = f"10.201.{idx}.1"
|
||
peer_ip = f"10.201.{idx}.2"
|
||
|
||
# Pinned ports from NODES.md registry
|
||
pinned_ports = {
|
||
"muse": 9410,
|
||
"pip": 9420,
|
||
"646": 9430,
|
||
"opm": 9440,
|
||
"def": 9450,
|
||
"dev": 9455,
|
||
}
|
||
cdp_port = pinned_ports.get(node, 9222 + int(tag[4:7], 16) % 2000)
|
||
|
||
return {
|
||
"node": node,
|
||
"netns": f"warp-{node}",
|
||
"tag": tag,
|
||
"gw_ip": gw_ip,
|
||
"peer_ip": peer_ip,
|
||
"cdp_port": cdp_port,
|
||
}
|
||
|
||
def probe_cdp_status(peer_ip: str, cdp_port: int, timeout: float = 1.5) -> dict:
|
||
"""Probe Chromium DevTools protocol on peer IP."""
|
||
url = f"http://{peer_ip}:{cdp_port}/json/version"
|
||
t0 = time.time()
|
||
try:
|
||
req = urllib.request.Request(url, headers={"User-Agent": "super-cli/1.0"})
|
||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||
elapsed_ms = int((time.time() - t0) * 1000)
|
||
if resp.status == 200:
|
||
data = json.load(resp)
|
||
return {
|
||
"ok": True,
|
||
"latency_ms": elapsed_ms,
|
||
"browser": data.get("Browser", "Chromium"),
|
||
}
|
||
except Exception as e:
|
||
return {"ok": False, "error": str(e), "latency_ms": None}
|
||
return {"ok": False, "error": "unknown error", "latency_ms": None}
|
||
|
||
def fetch_active_tab(peer_ip: str, cdp_port: int, timeout: float = 1.5) -> dict:
|
||
"""Retrieve title and URL of the active browser page."""
|
||
url = f"http://{peer_ip}:{cdp_port}/json/list"
|
||
try:
|
||
req = urllib.request.Request(url, headers={"User-Agent": "super-cli/1.0"})
|
||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||
tabs = json.load(resp)
|
||
pages = [t for t in tabs if t.get("type") == "page"]
|
||
if pages:
|
||
return {
|
||
"title": pages[0].get("title", ""),
|
||
"url": pages[0].get("url", ""),
|
||
}
|
||
except Exception:
|
||
pass
|
||
return {"title": "-", "url": "-"}
|
||
|
||
def check_process_alive(node: str) -> bool:
|
||
"""Check if Chromium process exists for the node's profile."""
|
||
try:
|
||
res = subprocess.run(
|
||
["pgrep", "-f", f"chrome-box/profiles/{node}"],
|
||
capture_output=True, text=True, timeout=2
|
||
)
|
||
return res.returncode == 0
|
||
except Exception:
|
||
return False
|
||
|
||
def get_queue_depth(node: str) -> int:
|
||
"""Check active flock tickets in /tmp/cdp-queue/<node>/."""
|
||
q_dir = Path(f"/tmp/cdp-queue/{node}")
|
||
if not q_dir.exists():
|
||
return 0
|
||
try:
|
||
tickets = [f for f in q_dir.iterdir() if f.name.startswith("ticket-")]
|
||
return len(tickets)
|
||
except Exception:
|
||
return 0
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: FLEET
|
||
# ---------------------------------------------------------------------------
|
||
def _host_evidence_for(results):
|
||
"""Host watchdog evidence, or None when unneeded/unavailable.
|
||
|
||
Queried only when at least one node is fully blind (both local
|
||
probes negative). Never raises: evidence must not break status.
|
||
"""
|
||
if not any(not r["proc_alive"] and not r["cdp_ok"] for r in results):
|
||
return None
|
||
try:
|
||
import host_evidence
|
||
return host_evidence.collect([r["node"] for r in results])
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def _resolve_statuses(results) -> None:
|
||
"""Attach effective status/source/evidence to each collected node.
|
||
|
||
proc_alive/cdp_ok keep their local-probe meaning; status is the
|
||
display verdict. Host evidence only decides the fully-blind pattern
|
||
(both local probes negative); live local signals always win.
|
||
"""
|
||
try:
|
||
import host_evidence
|
||
have_mod = True
|
||
except ImportError:
|
||
have_mod = False
|
||
ev_map = _host_evidence_for(results) if have_mod else None
|
||
for r in results:
|
||
ev = (ev_map or {}).get(r["node"]) if ev_map else None
|
||
if not r["proc_alive"] and r["cdp_ok"]:
|
||
# CDP answers: the browser is definitionally alive even when
|
||
# pgrep cannot see it (PID-blind shell, renamed profile path).
|
||
r["status"], r["source"] = "ACTIVE", "local"
|
||
elif r["proc_alive"] and not r["cdp_ok"]:
|
||
r["status"], r["source"] = "CDP_DOWN", "local"
|
||
elif r["proc_alive"] and r["cdp_ok"]:
|
||
r["status"], r["source"] = "ACTIVE", "local"
|
||
elif ev is None:
|
||
r["status"], r["source"] = "STOPPED", "local"
|
||
else:
|
||
r["status"], r["source"] = host_evidence.effective_status(
|
||
False, False, ev.get("browser", "unknown"),
|
||
ev.get("cdp", "unknown"))
|
||
r["evidence"] = ev
|
||
|
||
|
||
def collect_fleet_data() -> list:
|
||
results = []
|
||
for node in VALID_NODES:
|
||
info = get_node_network_info(node)
|
||
proc_alive = check_process_alive(node)
|
||
cdp = probe_cdp_status(info["peer_ip"], info["cdp_port"])
|
||
tab = fetch_active_tab(info["peer_ip"], info["cdp_port"]) if cdp["ok"] else {"title": "-", "url": "-"}
|
||
q_depth = get_queue_depth(node)
|
||
|
||
approval_pending = False
|
||
approval_detail = None
|
||
if cdp["ok"] and ("(1)" in tab.get("title", "") or "approval" in tab.get("title", "").lower()):
|
||
try:
|
||
import approvals
|
||
app_info = approvals.inspect_node_approvals(node)
|
||
if app_info.get("has_pending"):
|
||
approval_pending = True
|
||
approval_detail = app_info
|
||
except Exception:
|
||
pass
|
||
|
||
results.append({
|
||
"node": node,
|
||
"netns": info["netns"],
|
||
"peer_ip": info["peer_ip"],
|
||
"cdp_port": info["cdp_port"],
|
||
"proc_alive": proc_alive,
|
||
"cdp_ok": cdp["ok"],
|
||
"latency_ms": cdp["latency_ms"],
|
||
"title": tab["title"],
|
||
"url": tab["url"],
|
||
"queue_depth": q_depth,
|
||
"approval_pending": approval_pending,
|
||
"approval_detail": approval_detail,
|
||
})
|
||
_resolve_statuses(results)
|
||
return results
|
||
|
||
def cmd_fleet_status(args):
|
||
data = collect_fleet_data()
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "fleet": data}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold("=== NETVM FLEET STATUS ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n"))
|
||
|
||
headers = ["NODE", "STATUS", "PEER IP:PORT", "LATENCY", "QUEUE", "ACTIVE PAGE / THREAD"]
|
||
rows = []
|
||
has_any_approval = False
|
||
for item in data:
|
||
# Status calculation (effective status; see _resolve_statuses)
|
||
if item.get("approval_pending"):
|
||
status = badge_warn("APPROVAL_REQ")
|
||
has_any_approval = True
|
||
elif item.get("status") == "ACTIVE":
|
||
status = badge_ok("ACTIVE")
|
||
elif item.get("status") == "CDP_DOWN":
|
||
status = badge_warn("CDP_DOWN")
|
||
elif item.get("status") == "UNKNOWN":
|
||
status = badge_dim("UNKNOWN")
|
||
else:
|
||
status = badge_err("STOPPED")
|
||
|
||
lat = f"{item['latency_ms']}ms" if item["latency_ms"] is not None else "-"
|
||
q = str(item["queue_depth"]) if item["queue_depth"] > 0 else c_dim("idle")
|
||
|
||
# Format title/URL nicely
|
||
title = item["title"]
|
||
if item.get("approval_pending"):
|
||
target = item.get("approval_detail", {}).get("ip") or "request"
|
||
title = f"{c_yellow('[APPROVAL: ' + target + ']')} {title}"
|
||
elif "thread/" in item["url"]:
|
||
m = re.search(r"thread/([0-9a-fA-F-]+)", item["url"])
|
||
if m:
|
||
uuid_short = m.group(1)[:8]
|
||
title = f"{title} [{c_cyan(uuid_short)}]"
|
||
elif item["url"] == "https://muse.ai/":
|
||
title = f"{title} [{c_dim('home')}]"
|
||
|
||
rows.append([
|
||
c_bold(item["node"]),
|
||
status,
|
||
f"{item['peer_ip']}:{item['cdp_port']}",
|
||
lat,
|
||
q,
|
||
title[:50]
|
||
])
|
||
|
||
print_table(headers, rows)
|
||
if any(r.get("source") == "host-evidence" for r in data):
|
||
print(c_dim(" [*] status via host watchdog evidence (local probes blind in this shell)"))
|
||
if has_any_approval:
|
||
print("\n" + c_yellow(" ⚠ Agent(s) held up on browser approval. Run 'box approvals' to inspect/allow."))
|
||
print("\n" + c_dim(" Commands: super fleet watch | super fleet heal <node> | box approvals [check|allow|auto]") + "\n")
|
||
|
||
def cmd_fleet_watch(args):
|
||
interval = getattr(args, "interval", 2)
|
||
try:
|
||
while True:
|
||
# Clear terminal
|
||
sys.stdout.write("\033[2J\033[H")
|
||
sys.stdout.flush()
|
||
cmd_fleet_status(args)
|
||
print(c_dim(f" [Auto-refreshing every {interval}s. Press Ctrl+C to exit]"))
|
||
time.sleep(interval)
|
||
except KeyboardInterrupt:
|
||
print("\n" + c_dim("Exited watch mode."))
|
||
|
||
def cmd_fleet_restart(args):
|
||
node = args.node
|
||
if node not in VALID_NODES:
|
||
print(f"Error: Unknown node '{node}'. Valid: {', '.join(VALID_NODES)}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
print(f"Restarting node '{c_bold(node)}' via netvm-node-up.sh...")
|
||
cmd = ["sudo", str(BIN_DIR / "netvm-node-up.sh"), node]
|
||
res = subprocess.run(cmd, text=True)
|
||
if res.returncode == 0:
|
||
print(c_green(f"Node '{node}' brought up successfully."))
|
||
else:
|
||
print(c_red(f"Node restart exited with code {res.returncode}."))
|
||
sys.exit(res.returncode)
|
||
|
||
def cmd_fleet_cdp(args):
|
||
node = args.node
|
||
if node not in VALID_NODES:
|
||
print(f"Error: Unknown node '{node}'", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
info = get_node_network_info(node)
|
||
peer = info["peer_ip"]
|
||
port = info["cdp_port"]
|
||
|
||
if args.json:
|
||
print(json.dumps({
|
||
"node": node,
|
||
"peer_ip": peer,
|
||
"port": port,
|
||
"url": f"http://{peer}:{port}",
|
||
"ssh_forward": f"ssh -L {port}:{peer}:{port} super@100.123.153.75"
|
||
}, indent=2))
|
||
return
|
||
|
||
print(f"\n{c_bold('CDP Access for ' + node)}:")
|
||
print(f" Host Endpoint : {c_cyan(f'http://{peer}:{port}/json/version')}")
|
||
print(f" SSH Forward : {c_yellow(f'ssh -L {port}:{peer}:{port} super@100.123.153.75')}")
|
||
print(f" Local Connect : {c_dim(f'http://127.0.0.1:{port}')} (after forwarding)\n")
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: FLEET HEAL + WATCHDOG
|
||
# ---------------------------------------------------------------------------
|
||
SYSTEMD_SYSTEM_DIR = Path("/etc/systemd/system")
|
||
WATCHDOG_LOCK_TMPL = "/tmp/chromebox-watchdog-{node}.lock"
|
||
WARP_PROBE_URL = "https://1.1.1.1/cdn-cgi/trace"
|
||
CHROMEBOX_TIMER_TMPL = """[Unit]
|
||
Description=Run chromebox watchdog for {node} every 2 minutes
|
||
|
||
[Timer]
|
||
RandomizedDelaySec=30s
|
||
OnBootSec=2min
|
||
OnUnitActiveSec=2min
|
||
Unit=chromebox-watchdog@{node}.service
|
||
|
||
[Install]
|
||
WantedBy=timers.target
|
||
"""
|
||
|
||
|
||
def _sh(cmd, timeout=120, input_text=None):
|
||
"""Run cmd, capture output. Returns (returncode, combined_output)."""
|
||
try:
|
||
r = subprocess.run(cmd, capture_output=True, text=True,
|
||
timeout=timeout, input=input_text)
|
||
return r.returncode, ((r.stdout or "") + (r.stderr or "")).strip()
|
||
except subprocess.TimeoutExpired:
|
||
return 124, "timed out after %ds: %s" % (timeout, " ".join(cmd))
|
||
except OSError as e:
|
||
return 127, str(e)
|
||
|
||
|
||
def _heal_lock_step(node):
|
||
"""Clear a stale unwritable watchdog lock (blocks watchdog startup)."""
|
||
lock = Path(WATCHDOG_LOCK_TMPL.format(node=node))
|
||
if not lock.exists() or os.access(lock, os.W_OK):
|
||
return True, "lock writable or absent"
|
||
rc, out = _sh(["sudo", "rm", "-f", str(lock)], timeout=30)
|
||
if rc == 0:
|
||
return True, "removed stale lock %s" % lock
|
||
return False, "cannot remove %s: %s" % (lock, out)
|
||
|
||
|
||
def _heal_timer_step(node):
|
||
"""Install (if missing) and enable the node's watchdog timer."""
|
||
timer = "chromebox-watchdog-%s.timer" % node
|
||
path = SYSTEMD_SYSTEM_DIR / timer
|
||
if not path.exists():
|
||
rc, out = _sh(["sudo", "tee", str(path)], timeout=30,
|
||
input_text=CHROMEBOX_TIMER_TMPL.format(node=node))
|
||
if rc != 0:
|
||
return False, "cannot install %s: %s" % (timer, out)
|
||
rc, out = _sh(["sudo", "systemctl", "daemon-reload"], timeout=60)
|
||
if rc != 0:
|
||
return False, "daemon-reload failed: %s" % out
|
||
rc, out = _sh(["sudo", "systemctl", "enable", "--now", timer], timeout=60)
|
||
if rc != 0:
|
||
return False, "enable --now failed: %s" % out
|
||
return True, "timer installed+active"
|
||
|
||
|
||
def _heal_tunnel_step(node):
|
||
"""Restart the node's Warp tunnel + CDP relay via netvm-node-up.sh."""
|
||
rc, out = _sh(["sudo", str(BIN_DIR / "netvm-node-up.sh"), node], timeout=240)
|
||
last = out.strip().splitlines()[-1] if out.strip() else "(no output)"
|
||
if rc != 0:
|
||
return False, "netvm-node-up.sh rc=%d: %s" % (rc, last)
|
||
if "egress=unknown" in out or "no handshake yet" in out:
|
||
return False, last
|
||
return True, last
|
||
|
||
|
||
def _warp_egress_ok(node):
|
||
"""True when the node's netns reaches the warp probe URL."""
|
||
rc, out = _sh(["sudo", "ip", "netns", "exec", "warp-%s" % node,
|
||
"curl", "-sk", "--max-time", "10", WARP_PROBE_URL],
|
||
timeout=30)
|
||
if rc != 0:
|
||
tail = out.splitlines()[-1] if out else "curl failed"
|
||
return False, tail
|
||
for line in out.splitlines():
|
||
if line.startswith("ip="):
|
||
return True, line
|
||
return False, "no ip= line in probe response"
|
||
|
||
|
||
def cmd_fleet_heal(args):
|
||
node = args.node
|
||
if node not in VALID_NODES:
|
||
print(f"Error: Unknown node '{node}'. Valid: {', '.join(VALID_NODES)}", file=sys.stderr)
|
||
sys.exit(1)
|
||
as_json = getattr(args, "json", False)
|
||
info = get_node_network_info(node)
|
||
steps = []
|
||
|
||
def record(step, ok, detail):
|
||
steps.append({"step": step, "ok": ok, "detail": detail})
|
||
if not as_json:
|
||
mark = c_green("ok") if ok else c_red("FAIL")
|
||
print(f" [{mark}] {step}: {detail}")
|
||
|
||
if not as_json:
|
||
print(f"\n{c_bold('Healing node ' + node)} ({info['netns']}, {info['peer_ip']}:{info['cdp_port']})")
|
||
|
||
record("lock", *_heal_lock_step(node))
|
||
record("timer", *_heal_timer_step(node))
|
||
record("tunnel", *_heal_tunnel_step(node))
|
||
egress_ok, egress_detail = _warp_egress_ok(node)
|
||
record("egress", egress_ok, egress_detail)
|
||
relay = probe_cdp_status(info["peer_ip"], info["cdp_port"])
|
||
if relay["ok"]:
|
||
record("relay", True, "CDP answers in %sms" % relay["latency_ms"])
|
||
else:
|
||
record("relay", False, relay.get("error", "unreachable"))
|
||
|
||
verdict = "RECOVERED" if egress_ok else "DOWN"
|
||
if as_json:
|
||
print(json.dumps({"ok": egress_ok, "node": node,
|
||
"verdict": verdict, "steps": steps}, indent=2))
|
||
else:
|
||
print(f"\n Verdict: {c_green(verdict) if egress_ok else c_red(verdict)}")
|
||
if not egress_ok:
|
||
print(c_dim(" Tunnel still down after restart — if it persists, the Warp identity "
|
||
f"likely needs regenerating (human: netvm-new-identity.sh {node})"))
|
||
print()
|
||
sys.exit(0 if egress_ok else 1)
|
||
|
||
|
||
def _unit_state(unit):
|
||
"""(enabled, active) booleans for a systemd unit."""
|
||
rc_e, _ = _sh(["systemctl", "is-enabled", unit], timeout=15)
|
||
rc_a, _ = _sh(["systemctl", "is-active", unit], timeout=15)
|
||
return rc_e == 0, rc_a == 0
|
||
|
||
|
||
def cmd_watchdog_status(args):
|
||
as_json = getattr(args, "json", False)
|
||
try:
|
||
import host_evidence
|
||
ev = host_evidence.collect()
|
||
except Exception:
|
||
ev = {}
|
||
data = []
|
||
for node in VALID_NODES:
|
||
timer = "chromebox-watchdog-%s.timer" % node
|
||
enabled, active = _unit_state(timer)
|
||
node_ev = ev.get(node, {})
|
||
data.append({
|
||
"node": node,
|
||
"timer": timer,
|
||
"enabled": enabled,
|
||
"active": active,
|
||
"browser": node_ev.get("browser", "unknown"),
|
||
"browser_detail": node_ev.get("browser_detail", ""),
|
||
"cdp": node_ev.get("cdp", "unknown"),
|
||
"cdp_detail": node_ev.get("cdp_detail", ""),
|
||
})
|
||
relay_enabled, relay_active = _unit_state("cdp-relay-watchdog.timer")
|
||
relay = {"timer": "cdp-relay-watchdog.timer",
|
||
"enabled": relay_enabled, "active": relay_active}
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "nodes": data, "relay": relay}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold("=== WATCHDOG STATUS ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n"))
|
||
headers = ["NODE", "TIMER", "BROWSER", "CDP"]
|
||
rows = []
|
||
for item in data:
|
||
timer = badge_ok("active") if item["active"] else (
|
||
badge_warn("ENABLED") if item["enabled"] else badge_dim("missing"))
|
||
rows.append([c_bold(item["node"]), timer,
|
||
item["browser"], item["cdp"]])
|
||
print_table(headers, rows)
|
||
rmark = badge_ok("active") if relay["active"] else badge_dim("missing")
|
||
print(f" relay: {rmark} {c_dim('cdp-relay-watchdog.timer')}\n")
|
||
|
||
|
||
def cmd_watchdog_run(args):
|
||
target = args.target
|
||
as_json = getattr(args, "json", False)
|
||
if target == "relay":
|
||
unit = "cdp-relay-watchdog.service"
|
||
elif target in VALID_NODES:
|
||
unit = "chromebox-watchdog@%s.service" % target
|
||
else:
|
||
print("Error: Specify node or 'relay' (e.g. box watchdog run dev)", file=sys.stderr)
|
||
sys.exit(1)
|
||
rc, out = _sh(["sudo", "systemctl", "start", unit], timeout=180)
|
||
tail = out[-500:] if out else ""
|
||
if as_json:
|
||
print(json.dumps({"ok": rc == 0, "unit": unit, "detail": tail}, indent=2))
|
||
else:
|
||
if rc == 0:
|
||
print(c_green(f"Watchdog run triggered: {unit}"))
|
||
else:
|
||
print(c_red(f"Failed to start {unit}: {tail}"))
|
||
sys.exit(0 if rc == 0 else 1)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: APPROVALS
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_approvals(args):
|
||
import approvals
|
||
action = getattr(args, "app_action", None) or "check"
|
||
node = getattr(args, "node", None)
|
||
|
||
if action in ("check", "list", "inspect"):
|
||
nodes = [node] if node else VALID_NODES
|
||
fleet = approvals.check_fleet_approvals(nodes)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "approvals": fleet}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold("=== FLEET APPROVALS STATUS ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n"))
|
||
headers = ["NODE", "STATUS", "TARGET / IP", "PURPOSE / DETAILS", "TRUST", "ACTIONS"]
|
||
rows = []
|
||
pending_count = 0
|
||
untrusted_count = 0
|
||
input_wait_count = 0
|
||
|
||
for it in fleet:
|
||
n = it["node"]
|
||
st = it["status"]
|
||
if st == "PENDING":
|
||
pending_count += 1
|
||
badge = badge_err("PENDING") if not it.get("is_trusted") else badge_warn("PENDING")
|
||
target = it.get("target") or it.get("ip") or "-"
|
||
purp = (it.get("purpose") or it.get("title") or "-")[:50]
|
||
trust = c_green("TRUSTED") if it.get("is_trusted") else c_red("UNTRUSTED")
|
||
btns = " ".join([f"[{b}]" for b in it.get("buttons", [])])
|
||
if not it.get("is_trusted"):
|
||
untrusted_count += 1
|
||
elif st == "KEY_APPROVAL":
|
||
pending_count += 1
|
||
badge = badge_warn("KEY_REQ")
|
||
target = it.get("target") or "VM passkey"
|
||
purp = (it.get("purpose") or it.get("title") or "-")[:50]
|
||
trust = c_cyan("OPERATOR")
|
||
btns = " ".join([f"[{b}]" for b in it.get("buttons", ["Allow", "Deny"])])
|
||
untrusted_count += 1
|
||
elif st == "INPUT_WAIT":
|
||
waits = it.get("input_waits") or []
|
||
badge = badge_warn("INPUT")
|
||
target = "-"
|
||
purp = "; ".join(f"{w.get('task')}: {w.get('status')}" for w in waits)[:60]
|
||
trust = "-"
|
||
btns = c_dim("answer in task")
|
||
input_wait_count += 1
|
||
elif st == "UNREACHABLE":
|
||
if it.get("host_cdp_ok") is True:
|
||
badge = badge_dim("BLIND")
|
||
purp = c_dim("CDP ok on host; blind here")
|
||
elif it.get("host_cdp_ok") is False:
|
||
badge = badge_err("OFFLINE")
|
||
purp = c_dim("CDP down (host agrees)")
|
||
else:
|
||
badge = badge_dim("OFFLINE")
|
||
purp = c_dim("CDP unreachable")
|
||
target = "-"
|
||
trust = "-"
|
||
btns = "-"
|
||
elif st == "ERROR":
|
||
badge = badge_err("ERROR")
|
||
target = "-"
|
||
purp = it.get("error", "-")[:40]
|
||
trust = "-"
|
||
btns = "-"
|
||
else:
|
||
badge = badge_ok("CLEAR")
|
||
target = "-"
|
||
purp = c_dim("No pending approvals")
|
||
trust = "-"
|
||
btns = "-"
|
||
|
||
rows.append([c_bold(n), badge, target, purp, trust, btns])
|
||
|
||
print_table(headers, rows)
|
||
|
||
if input_wait_count > 0:
|
||
print("\n" + c_yellow(f" ⚠ {input_wait_count} node(s) have tasks waiting for human input (not auto-resolvable)."))
|
||
if pending_count > 0:
|
||
print("\n" + c_yellow(f" ⚠ {pending_count} pending approval(s) detected across fleet."))
|
||
if untrusted_count > 0:
|
||
print(c_red(f" ⚠ {untrusted_count} approval(s) require manual review / key grant: 'box approvals allow <node>' or 'box approvals deny <node>'."))
|
||
else:
|
||
print(c_cyan(" All pending approvals are for trusted infrastructure. Run 'box approvals auto' to resolve."))
|
||
print()
|
||
elif input_wait_count == 0:
|
||
unreach = [it for it in fleet if it.get("status") == "UNREACHABLE"]
|
||
blind_ok = [it["node"] for it in unreach
|
||
if it.get("host_cdp_ok") is True]
|
||
blind_unknown = [it["node"] for it in unreach
|
||
if it.get("host_cdp_ok") is None]
|
||
if blind_ok:
|
||
print("\n" + c_dim(" ? %d node(s) blind from this shell "
|
||
"(CDP ok on host); queues unverified: %s."
|
||
% (len(blind_ok), ", ".join(blind_ok))) + "\n")
|
||
if blind_unknown:
|
||
print("\n" + c_dim(" ? %d node(s) unreachable, host evidence "
|
||
"inconclusive: %s."
|
||
% (len(blind_unknown),
|
||
", ".join(blind_unknown))) + "\n")
|
||
if not blind_ok and not blind_unknown:
|
||
print("\n" + c_green(" ✔ All agent approval queues clear. No agents blocked.") + "\n")
|
||
|
||
# Verbose or inspect breakdown
|
||
is_verbose = getattr(args, "verbose", False) or action == "inspect"
|
||
if is_verbose:
|
||
print(c_bold("\n=== DETAILED NODE INSPECTION ==="))
|
||
for it in fleet:
|
||
n = it["node"]
|
||
if it.get("has_pending") or it.get("input_waits") or action == "inspect":
|
||
print(f"\n [{c_bold(n)}] Status: {it.get('status')} | Page: {c_cyan(it.get('page_url') or '-')}")
|
||
if it.get("has_pending"):
|
||
print(f" Egress Target: {it.get('target') or it.get('ip')} ({'TRUSTED' if it.get('is_trusted') else 'UNTRUSTED'})")
|
||
print(f" Prompt Title : {it.get('title')}")
|
||
if it.get("purpose"):
|
||
print(f" Purpose : {it.get('purpose')}")
|
||
print(f" Buttons : {', '.join(it.get('buttons') or [])}")
|
||
if it.get("input_waits"):
|
||
print(f" Tasks Awaiting Human Input ({len(it['input_waits'])}):")
|
||
for w in it["input_waits"]:
|
||
print(f" • {c_bold(w.get('task'))}")
|
||
print(f" Status: {w.get('status')} ({w.get('when')})")
|
||
# Correlate with active subagents on this node
|
||
try:
|
||
import subagent_tracker
|
||
active_subs = subagent_tracker.get_active_sessions(n)
|
||
if active_subs:
|
||
print(f" Active Subagents on Node ({len(active_subs)}):")
|
||
for sub in active_subs[-3:]:
|
||
s_id = sub.get("session_id", "")[:8]
|
||
s_title = sub.get("title") or "subagent"
|
||
print(f" • [{s_id}] {c_bold(s_title)} (spawned: {sub.get('spawned_at', '-')})")
|
||
except Exception:
|
||
pass
|
||
print()
|
||
|
||
elif action in ("allow", "approve"):
|
||
if not node:
|
||
print(c_red("Error: Must specify node for allow. e.g. 'box approvals allow 646'"), file=sys.stderr)
|
||
sys.exit(1)
|
||
always = getattr(args, "always", False)
|
||
force = getattr(args, "force", False)
|
||
message = getattr(args, "message", None)
|
||
allow_main_chat = getattr(args, "allow_main_chat", False)
|
||
res = approvals.allow_node_approval(node, always=always, force=force, message=message,
|
||
allow_main_chat=allow_main_chat)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if res.get("ok"):
|
||
if res.get("type") == "key_approval":
|
||
notified = "agent notified" if res.get("notified") else "⚠ agent NOT notified (follow up manually)"
|
||
print(c_green(f"✔ Approved operator key request for node '{node}'. Logged to audit trail. {notified}."))
|
||
else:
|
||
decision_str = "Always allow this site" if always else "Allow once"
|
||
print(c_green(f"✔ Approved request on node '{node}' ({decision_str}). Dialog dismissed: {res.get('dismissed')}."))
|
||
else:
|
||
print(c_red(f"✖ Failed to approve on node '{node}': {res.get('error')}"))
|
||
sys.exit(2 if "Untrusted" in res.get("error", "") else 1)
|
||
|
||
elif action == "deny":
|
||
if not node:
|
||
print(c_red("Error: Must specify node for deny. e.g. 'box approvals deny 646'"), file=sys.stderr)
|
||
sys.exit(1)
|
||
message = getattr(args, "message", None)
|
||
allow_main_chat = getattr(args, "allow_main_chat", False)
|
||
res = approvals.deny_node_approval(node, message=message, allow_main_chat=allow_main_chat)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if res.get("ok"):
|
||
if res.get("type") == "key_approval":
|
||
notified = "agent notified" if res.get("notified") else "⚠ agent NOT notified (follow up manually)"
|
||
print(c_green(f"✔ Denied operator key request for node '{node}'. Logged to audit trail. {notified}."))
|
||
else:
|
||
print(c_green(f"✔ Denied request on node '{node}'. Dialog dismissed: {res.get('dismissed')}."))
|
||
else:
|
||
print(c_red(f"✖ Failed to deny on node '{node}': {res.get('error')}"))
|
||
sys.exit(1)
|
||
|
||
elif action == "auto":
|
||
nodes = [node] if node else VALID_NODES
|
||
res = approvals.auto_approve_fleet(nodes)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
approved = res.get("auto_approved", [])
|
||
untrusted = res.get("untrusted_pending", [])
|
||
if approved:
|
||
print("\n" + c_green(f"✔ Auto-approved {len(approved)} trusted request(s):"))
|
||
for a in approved:
|
||
print(f" • {c_bold(a['node'])}: {a.get('target_ip')} — {a.get('title')}")
|
||
if untrusted:
|
||
print("\n" + c_yellow(f"⚠ {len(untrusted)} untrusted request(s) require manual decision:"))
|
||
for u in untrusted:
|
||
print(f" • {c_bold(u['node'])}: {u.get('ip')} — {u.get('title')} (run: box approvals allow {u['node']} --force)")
|
||
if not approved and not untrusted:
|
||
print(c_green("✔ All nodes clear. No approvals pending."))
|
||
print()
|
||
if untrusted:
|
||
sys.exit(2)
|
||
|
||
elif action == "watch":
|
||
interval = getattr(args, "interval", 2)
|
||
auto_mode = getattr(args, "auto", False)
|
||
try:
|
||
while True:
|
||
sys.stdout.write("\033[2J\033[H")
|
||
sys.stdout.flush()
|
||
if auto_mode:
|
||
auto_res = approvals.auto_approve_fleet(nodes=[node] if node else VALID_NODES)
|
||
if auto_res.get("auto_approved"):
|
||
for a in auto_res["auto_approved"]:
|
||
print(c_green(f"[AUTO-APPROVED] {a['node']}: {a.get('target_ip')}"))
|
||
# Print status
|
||
setattr(args, "app_action", "check")
|
||
cmd_approvals(args)
|
||
auto_label = c_cyan(" [AUTO-APPROVE ENABLED]") if auto_mode else ""
|
||
print(c_dim(f" [Watching every {interval}s{auto_label}. Press Ctrl+C to exit]"))
|
||
time.sleep(interval)
|
||
except KeyboardInterrupt:
|
||
print("\n" + c_dim("Exited watch mode."))
|
||
|
||
elif action == "reply":
|
||
if not node:
|
||
print(c_red("Error: Must specify node for reply. e.g. 'box approvals reply pip \"proceed\"'"), file=sys.stderr)
|
||
sys.exit(1)
|
||
message = getattr(args, "message", "")
|
||
allow_main = getattr(args, "allow_main_chat", False)
|
||
res = approvals.reply_node_task(node, message, allow_main_chat=allow_main)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if res.get("ok"):
|
||
print(c_green(f"✔ Dispatched reply to node '{node}': \"{message}\" (thread: {res.get('page_url')})"))
|
||
else:
|
||
print(c_red(f"✖ Failed to reply to node '{node}': {res.get('error')}"))
|
||
sys.exit(2 if "Main Chat" in res.get("error", "") else 1)
|
||
|
||
elif action == "dismiss":
|
||
if not node:
|
||
print(c_red("Error: Must specify node for dismiss. e.g. 'box approvals dismiss pip'"), file=sys.stderr)
|
||
sys.exit(1)
|
||
res = approvals.dismiss_node_task(node)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if res.get("ok"):
|
||
print(c_green(f"✔ Dismissed task popup on node '{node}' ({res.get('result')})."))
|
||
else:
|
||
print(c_red(f"✖ Failed to dismiss task popup on node '{node}': {res.get('error')}"))
|
||
sys.exit(1)
|
||
elif action in ("clear", "clear-all", "clear_all"):
|
||
target_node = node
|
||
if action in ("clear-all", "clear_all"):
|
||
target_node = None
|
||
res = approvals.clear_node_waits(target_node)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
cleared = res.get("total_cleared", 0)
|
||
target_desc = f"node '{target_node}'" if target_node else "all fleet nodes"
|
||
print(c_green(f"✔ Cleared {cleared} input wait(s) on {target_desc}."))
|
||
|
||
elif action in ("request-key", "request_key"):
|
||
if not node:
|
||
print(c_red("Error: Must specify node for request-key. e.g. 'box approvals request-key 646'"), file=sys.stderr)
|
||
sys.exit(1)
|
||
reason = getattr(args, "reason", "Operator passkey access requested")
|
||
caller = os.environ.get("BOX_CALLER") or getattr(args, "from_agent", "super")
|
||
res = approvals.request_key_approval(node, reason=reason, caller=caller)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
print(c_green(f"✔ Registered passkey approval request for node '{node}'."))
|
||
print(f" Reason: {c_cyan(reason)}")
|
||
print(c_dim(f" Operator can approve with: box approvals allow {node}"))
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: MUSE-CHOICES (Muse TUI A/B/C auto-answer daemon)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_muse_choices(args):
|
||
import muse_choice_watcher as mcw
|
||
action = getattr(args, "mc_action", None) or "status"
|
||
as_json = getattr(args, "json", False)
|
||
caller = os.environ.get("BOX_CALLER") or getattr(args, "from_agent", None) or "super"
|
||
|
||
if action == "on":
|
||
dry_run = getattr(args, "dry_run", False)
|
||
state = mcw.set_enabled(True, dry_run=dry_run, by=caller)
|
||
res = mcw.reconcile()
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "desired": state, "reconcile": res}, indent=2))
|
||
return
|
||
mode = "DRY-RUN (logging only)" if dry_run else c_yellow("LIVE (typing A into Muse panes)")
|
||
print(c_green("\n✔ Muse choice auto-answers ON.") + f" [{mode}]\n")
|
||
for s in res["started"]:
|
||
print(f" {badge_ok('STARTED')} {c_cyan(s)}")
|
||
for s in res["already"]:
|
||
print(f" {badge_dim('RUNNING')} {s}")
|
||
for s in res.get("failed") or []:
|
||
print(f" {badge_err('FAILED')} {s}")
|
||
if not res["started"] and not res["already"] and not res.get("failed"):
|
||
print(c_dim(" No Muse panes found on known tmux sockets."))
|
||
print()
|
||
|
||
elif action == "off":
|
||
state = mcw.set_enabled(False, by=caller)
|
||
stopped = mcw.stop_all()
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "desired": state, "stopped": stopped}, indent=2))
|
||
return
|
||
print(c_green("\n✔ Muse choice auto-answers OFF."))
|
||
for r in stopped:
|
||
if r.get("status") == "stopped-orphan":
|
||
label = "(orphan %s:%s)" % (r.get("socket"), r.get("pane"))
|
||
else:
|
||
label = r.get("pidfile")
|
||
print(f" {badge_dim('STOPPED')} {label} (pid {r.get('pid')})")
|
||
if not stopped:
|
||
print(c_dim(" No watchers were running."))
|
||
print()
|
||
|
||
elif action == "reconcile":
|
||
res = mcw.reconcile()
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "reconcile": res}, indent=2))
|
||
return
|
||
print(f"\n Enabled: {c_bold(str(res['enabled']))} Dry-run: {res['dry_run']}")
|
||
for s in res["started"]:
|
||
print(f" {badge_ok('STARTED')} {c_cyan(s)}")
|
||
for s in res["stopped"]:
|
||
print(f" {badge_dim('STOPPED')} {s}")
|
||
for s in res["pruned"]:
|
||
print(f" {badge_dim('PRUNED')} {s}")
|
||
for s in res.get("failed") or []:
|
||
print(f" {badge_err('FAILED')} {s}")
|
||
if not res["started"] and not res["stopped"] and not res["pruned"] and not res.get("failed"):
|
||
print(c_dim(" Already in desired state."))
|
||
print()
|
||
|
||
elif action == "logs":
|
||
sock = getattr(args, "socket", None)
|
||
pane = getattr(args, "pane", None)
|
||
n = getattr(args, "n", 20)
|
||
path = mcw.logfile_for(sock, pane)
|
||
try:
|
||
with open(path) as f:
|
||
lines = f.readlines()
|
||
except OSError:
|
||
print(c_red(f"Error: no log at {path}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
if as_json:
|
||
recs = []
|
||
for line in lines[-n:]:
|
||
try:
|
||
recs.append(json.loads(line))
|
||
except Exception:
|
||
recs.append({"raw": line.rstrip("\n")})
|
||
print(json.dumps({"ok": True, "log": path, "records": recs}, indent=2))
|
||
return
|
||
print(c_bold(f"\n=== {path} (last {n}) ===\n"))
|
||
for line in lines[-n:]:
|
||
print(line.rstrip("\n"))
|
||
print()
|
||
|
||
else: # status
|
||
desired = mcw.get_desired()
|
||
watchers = mcw.status_all()
|
||
answers = mcw.recent_answers(5)
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "desired": desired,
|
||
"watchers": watchers,
|
||
"recent_answers": answers}, indent=2))
|
||
return
|
||
state_badge = badge_ok("ON") if desired["enabled"] else badge_dim("OFF")
|
||
dry = " (dry-run)" if desired.get("dry_run") else ""
|
||
print("\n" + c_bold("=== MUSE CHOICE AUTO-ANSWERS ===") + f" {state_badge}{dry}")
|
||
upd = desired.get("updated_at") or "-"
|
||
by = desired.get("updated_by") or "-"
|
||
print(c_dim(f" Desired state: enabled={desired['enabled']} dry_run={desired.get('dry_run')} updated={upd} by={by}\n"))
|
||
if watchers:
|
||
headers = ["WATCHER", "STATUS", "PID", "LOG BYTES"]
|
||
rows = []
|
||
for w in watchers:
|
||
if w.get("orphan"):
|
||
badge = badge_warn("ORPHAN")
|
||
name = "(orphan %s:%s)" % (w.get("socket"), w.get("pane"))
|
||
else:
|
||
badge = badge_ok("ALIVE") if w["alive"] else badge_err("DEAD")
|
||
name = w["pidfile"]
|
||
rows.append([name, badge, str(w["pid"] or "-"),
|
||
str(w["log_bytes"] if w["log_bytes"] is not None else "-")])
|
||
print_table(headers, rows)
|
||
else:
|
||
print(c_dim(" No watcher state files."))
|
||
if answers:
|
||
print(c_bold("\n Recent answers (box audit feed):"))
|
||
for a in answers:
|
||
opts = "; ".join(a.get("options") or [])[:80]
|
||
kind = a.get("kind") or "-"
|
||
key = a.get("key") or "-"
|
||
print(f" • {c_dim(a.get('ts', ''))} {c_bold(a.get('name', ''))} {kind}/{key} sig={a.get('sig')} ok={a.get('ok')}")
|
||
print(f" {c_dim(opts)}")
|
||
else:
|
||
print(c_dim("\n No answers recorded in box audit feed."))
|
||
print()
|
||
|
||
def resolve_sender(args) -> str:
|
||
explicit = getattr(args, "from_agent", None)
|
||
if explicit and explicit != DEFAULT_SENDER:
|
||
return explicit
|
||
caller_env = os.environ.get("BOX_CALLER")
|
||
if caller_env and caller_env in ("muse", "pip", "646", "opm", "super"):
|
||
return caller_env
|
||
return explicit or DEFAULT_SENDER
|
||
|
||
def sign_message(sender: str, message: str, key_path: Path = None, msg_id: str = None) -> tuple:
|
||
"""Signs message with SSH key (namespace 'dm') using ssh-keygen -Y sign.
|
||
Returns (signed_raw_wire_format, msg_id)."""
|
||
import tempfile
|
||
key_path = key_path or Path.home() / ".ssh" / "id_ed25519"
|
||
if not key_path.exists():
|
||
raise FileNotFoundError(f"Signing key not found: {key_path}")
|
||
|
||
mid = msg_id or hashlib.sha256(f"{sender}-{time.time()}-{os.urandom(8).hex()}".encode()).hexdigest()[:8]
|
||
payload = f"[from:{sender}] [id:{mid}]\n\n{message}"
|
||
|
||
with tempfile.TemporaryDirectory() as td:
|
||
payf = Path(td) / "payload"
|
||
with open(payf, "w") as f:
|
||
f.write(payload)
|
||
|
||
res = subprocess.run(
|
||
["ssh-keygen", "-Y", "sign", "-f", str(key_path), "-n", "dm", str(payf)],
|
||
capture_output=True, text=True, timeout=10
|
||
)
|
||
if res.returncode != 0:
|
||
raise RuntimeError(f"ssh-keygen sign failed: {res.stderr}")
|
||
|
||
sigf = Path(td) / "payload.sig"
|
||
with open(sigf, "r") as f:
|
||
sigblock = f.read().strip()
|
||
|
||
signed_wire = f"[from:{sender}] [id:{mid}]\n\n{message}\n\n{sigblock}"
|
||
return signed_wire, mid
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: INVITE
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_invite(args):
|
||
import invite
|
||
from invite_handler import salvage_blocked_node
|
||
action = getattr(args, "invite_action", None) or "status"
|
||
node = getattr(args, "node", None)
|
||
as_json = getattr(args, "json", False)
|
||
|
||
if action in ("status", "list"):
|
||
nodes = [node] if node else VALID_NODES
|
||
res = invite.fleet_invite_status(nodes)
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "agents": res}, indent=2))
|
||
return
|
||
print("\n" + c_bold("=== FLEET INVITE STATUS ===") + "\n")
|
||
headers = ["NODE", "CODE", "REDEEMED", "USES LEFT", "USED", "REWARD"]
|
||
rows = []
|
||
for n in nodes:
|
||
it = res.get(n, {})
|
||
if not it.get("ok"):
|
||
rows.append([c_bold(n), c_red("ERROR"), "-", "-", "-",
|
||
(it.get("error") or "")[:40]])
|
||
continue
|
||
rows.append([c_bold(n), c_cyan(it.get("code") or "-"),
|
||
"yes" if it.get("has_redeemed") else c_green("no"),
|
||
str(it.get("uses_remaining")
|
||
if it.get("uses_remaining") is not None else "-"),
|
||
str(it.get("use_count")
|
||
if it.get("use_count") is not None else "-"),
|
||
((it.get("reward") or {}).get("title") or "-")[:40]])
|
||
print_table(headers, rows)
|
||
print()
|
||
elif action in ("code", "find"):
|
||
try:
|
||
res = invite.get_invite(node)
|
||
except invite.InviteError as e:
|
||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||
sys.exit(2)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
elif res.get("ok"):
|
||
print(f" {c_bold(node)} invite code: {c_cyan(res['code'])}")
|
||
if res.get("uses_remaining") is not None:
|
||
print(f" Uses remaining: {res['uses_remaining']}")
|
||
print(f" Source: {res.get('source', 'api')}")
|
||
else:
|
||
print(c_red(f" Error: {res.get('error')}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
elif action == "redeem":
|
||
code = getattr(args, "code", None)
|
||
try:
|
||
res = invite.redeem_invite(node, code)
|
||
except invite.InviteError as e:
|
||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||
sys.exit(2)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
elif res.get("ok"):
|
||
msg = f" ✔ Redeemed {res['code']} on {node}."
|
||
if res.get("detail"):
|
||
msg += f" {res['detail']}"
|
||
print(c_green(msg))
|
||
else:
|
||
print(c_red(f" ✘ Redeem failed on {node}: "
|
||
f"{res.get('reason') or res.get('error')}"),
|
||
file=sys.stderr)
|
||
if res.get("detail"):
|
||
print(c_dim(f" {res['detail']}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
elif action == "salvage":
|
||
target = getattr(args, "node", "646") or "646"
|
||
res = salvage_blocked_node(target, helper_node=getattr(args, "helper", None))
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
print(f"\n{c_bold('=== SALVAGE REPORT FOR ' + target.upper() + ' ===')}")
|
||
code = res.get("invite_code") or res.get("code_redeemed")
|
||
print(f" Invite Code to Credit: {c_cyan(str(code))}")
|
||
if res.get("success"):
|
||
print(c_green(f" Status: SUCCESS! Redeemed on {res.get('helper_node')}. 1 billion tokens credited to {target}!"))
|
||
else:
|
||
diag = res.get("error") or (res.get("redemption_result") or {}).get("detail") or "Redemption failed on helper peer"
|
||
print(c_yellow(f" Diagnosis: {diag}"))
|
||
action_hint = res.get("share_instruction") or f"Redeem code '{code}' via onboarding pipeline to grant 1B tokens to {target}."
|
||
print(f" Action: {action_hint}")
|
||
print(f" Command: {c_cyan('box onboard start <new_node> --email <client_email> --for ' + target)}")
|
||
print()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: USAGE
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_usage(args):
|
||
import invite
|
||
node = getattr(args, "node", None) or getattr(args, "node_pos", None)
|
||
as_json = getattr(args, "json", False)
|
||
nodes = [node] if node else VALID_NODES
|
||
res = invite.fleet_usage(nodes)
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "agents": res}, indent=2))
|
||
return
|
||
print("\n" + c_bold("=== FLEET USAGE LIMITS ===") + "\n")
|
||
headers = ["NODE", "WEEKLY RESET", "WEEKLY USED", "ADDITIONAL",
|
||
"ADD LEFT", "REDEEMED"]
|
||
rows = []
|
||
has_blocked = False
|
||
for n in nodes:
|
||
it = res.get(n, {})
|
||
if not it.get("ok"):
|
||
rows.append([c_bold(n), c_red("ERROR"), "-", "-",
|
||
(it.get("error") or "")[:30], "-"])
|
||
continue
|
||
wu_val = it.get("weekly_used_pct")
|
||
wu = ("%d%%" % wu_val) if wu_val is not None else "-"
|
||
au_val = it.get("additional_used_pct")
|
||
au = ("%d%% used" % au_val) if au_val is not None else "-"
|
||
add_left = it.get("additional_left") or "-"
|
||
redeemed_val = it.get("has_redeemed")
|
||
redeemed_str = "yes" if redeemed_val else (c_green("no") if redeemed_val is False else "-")
|
||
if (wu_val is not None and wu_val >= 100 and au_val is not None and au_val >= 100) or ("0 tokens left" in add_left):
|
||
has_blocked = True
|
||
n_disp = c_red(f"● {n}")
|
||
else:
|
||
n_disp = c_bold(n)
|
||
rows.append([n_disp, it.get("weekly_reset") or "-",
|
||
wu, "%s / %s" % (it.get("additional_expires") or "-",
|
||
au),
|
||
add_left, redeemed_str])
|
||
print_table(headers, rows)
|
||
if has_blocked:
|
||
print("\n" + c_yellow(" ⚠ One or more agents have reached usage limits. Run 'box invite salvage <node>' to resolve.") + "\n")
|
||
else:
|
||
print()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: SETTINGS
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_settings(args):
|
||
from settings_rpa import SettingsRPA
|
||
action = getattr(args, "settings_action", "usage") or "usage"
|
||
node = getattr(args, "node", "646") or "646"
|
||
as_json = getattr(args, "json", False)
|
||
|
||
with SettingsRPA(node) as rpa:
|
||
if action == "usage":
|
||
usage = rpa.read_usage()
|
||
if as_json:
|
||
print(json.dumps(usage.to_dict(), indent=2))
|
||
return
|
||
status_badge = badge_err("BLOCKED") if usage.is_blocked else badge_ok("ACTIVE")
|
||
redeemed_badge = c_green("No (Eligible to redeem)") if not usage.has_redeemed else "Yes (Already redeemed)"
|
||
print(f"\n{c_bold('=== Settings RPA Usage: ' + node + ' ===')}")
|
||
print(f" Plan: {usage.plan}")
|
||
print(f" Weekly Limit: {usage.weekly_reset_text} ({c_bold(str(usage.weekly_percent_used) + '%')} used)")
|
||
print(f" Extra Tokens: {usage.extra_tokens_remaining} ({c_bold(str(usage.extra_percent_used) + '%')} used)")
|
||
print(f" Has Redeemed: {redeemed_badge}")
|
||
print(f" Status: {status_badge}\n")
|
||
elif action == "check-redeem":
|
||
info = rpa.check_redeem_entrypoint()
|
||
print(json.dumps(info, indent=2))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: ONBOARD (Agent-driven Onboarding & Invite Salvage Pipeline)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_onboard(args):
|
||
import onboard_pipeline
|
||
action = getattr(args, "onboard_action", "start") or "start"
|
||
as_json = getattr(args, "json", False)
|
||
|
||
if action == "start":
|
||
node = args.node
|
||
email = args.email
|
||
for_agent = getattr(args, "for_agent", None)
|
||
code = getattr(args, "code", None)
|
||
acct_name = getattr(args, "account_name", None)
|
||
|
||
res = onboard_pipeline.start_onboarding(node, email, beneficiary_node=for_agent, invite_code=code, account_name=acct_name)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if res.get("ok"):
|
||
print(f"\n{c_bold('=== ONBOARDING PIPELINE STARTED: ' + node.upper() + ' ===')}")
|
||
print(f" Target Node: {c_bold(node)}")
|
||
print(f" Client Email: {email}")
|
||
print(f" Beneficiary: @{res.get('beneficiary_node')} ({c_cyan(str(res.get('invite_code_queued')))})")
|
||
print(f" Status: {c_yellow(res.get('status', 'in_progress').upper())}")
|
||
print(f" Next Step: {res.get('message')}\n")
|
||
else:
|
||
print(c_red(f"\n✘ Onboarding failed: {res.get('error')}\n"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
elif action == "submit-otp":
|
||
node = args.node
|
||
otp = args.otp
|
||
email = getattr(args, "email", None)
|
||
|
||
res = onboard_pipeline.submit_onboarding_otp(node, otp, email=email)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if res.get("ok"):
|
||
print(f"\n{c_green('=== ONBOARDING & REDEMPTION COMPLETE ===')}")
|
||
print(f" Node: {c_bold(node)}")
|
||
print(f" Beneficiary: @{res.get('beneficiary_node')}")
|
||
print(f" Token Grant: +1,000,000,000 Muse tokens each")
|
||
print(f" Message: {c_green(res.get('message'))}\n")
|
||
else:
|
||
print(c_red(f"\n✘ OTP submission failed: {res.get('error')}\n"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
elif action == "status":
|
||
node = args.node
|
||
state = onboard_pipeline.OnboardState.load(node)
|
||
if not state:
|
||
print(c_red(f"No onboarding record found for node '{node}'"), file=sys.stderr)
|
||
sys.exit(1)
|
||
if as_json:
|
||
print(json.dumps(onboard_pipeline.asdict(state), indent=2))
|
||
return
|
||
print(f"\n{c_bold('=== ONBOARDING STATUS: @' + state.node.upper() + ' ===')}")
|
||
print(f" Email: {state.email}")
|
||
print(f" Current Stage: {c_cyan(state.stage)}")
|
||
print(f" Beneficiary Agent:@{state.beneficiary_node or '-'} (Code: {c_cyan(str(state.invite_code or '-'))})")
|
||
if state.detail:
|
||
print(f" Detail: {state.detail}")
|
||
if state.redemption_result:
|
||
rr = state.redemption_result
|
||
r_st = c_green("SUCCESS") if rr.get("ok") else c_red("FAILED")
|
||
print(f" Redemption: {r_st}")
|
||
print()
|
||
|
||
elif action == "salvage-wo":
|
||
node = getattr(args, "node", "646") or "646"
|
||
target_chat = getattr(args, "target", "646 tasks") or "646 tasks"
|
||
res = onboard_pipeline.issue_salvage_work_order(node, to_sidechat=target_chat)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if res.get("ok"):
|
||
print(c_green(f"\n✔ Cryptographically signed salvage work order dispatched for @{node} -> {target_chat}\n"))
|
||
else:
|
||
print(c_red(f"\n✘ Failed to dispatch work order: {res}\n"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
elif action == "feed-matrix":
|
||
matrix = onboard_pipeline.calculate_feeding_weights()
|
||
if as_json:
|
||
print(json.dumps({"ok": True, "feed_matrix": matrix}, indent=2))
|
||
return
|
||
print("\n" + c_bold("=== FLEET FEEDING & TOKEN ALLOCATION MATRIX ==="))
|
||
print(c_dim(" (Ranked by: Urgency + Work Done Over Time + Job Amount + Role Criticality)\n"))
|
||
headers = ["RANK", "AGENT", "ROLE", "FEED SCORE", "STATUS", "JOBS", "WORK DONE", "CODE"]
|
||
rows = []
|
||
for idx, r in enumerate(matrix, 1):
|
||
st = c_red("BLOCKED") if r["is_blocked"] else (c_yellow("LIMIT") if r["status"] == "LIMIT_REACHED" else c_green("ACTIVE"))
|
||
rows.append([
|
||
f"#{idx}",
|
||
c_bold(r["node"]),
|
||
r["role"],
|
||
c_cyan(str(r["feeding_weight"]) + " pts"),
|
||
st,
|
||
str(r["jobs_count"]),
|
||
f"{r['work_done_msgs']} msgs",
|
||
c_bold(r["code"]),
|
||
])
|
||
print_table(headers, rows)
|
||
print()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: CHROMEBOX (agent browser settings-menu toggles)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_chromebox(args):
|
||
from hatch_menu import toggles as menu_toggles
|
||
action = getattr(args, "chrome_action", None)
|
||
if action != "permissions":
|
||
print(c_red(" Error: expected 'permissions' (box chromebox "
|
||
"permissions <node> list|get|set|describe)"),
|
||
file=sys.stderr)
|
||
sys.exit(2)
|
||
node = getattr(args, "node", None)
|
||
as_json = getattr(args, "json", False)
|
||
perm_action = getattr(args, "perm_action", None) or "list"
|
||
|
||
if perm_action == "list":
|
||
tab = getattr(args, "tab", None)
|
||
try:
|
||
res = menu_toggles.list_toggles(node, tab=tab)
|
||
except menu_toggles.MenuError as e:
|
||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||
sys.exit(2)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
return
|
||
if not res.get("ok"):
|
||
print(c_red(f" Error: {res.get('error')}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
print("\n" + c_bold(f"=== CHROMEBOX TOGGLES: {node} ===") + "\n")
|
||
rows = []
|
||
for name in sorted(res.get("toggles", {})):
|
||
val = res["toggles"][name]
|
||
rows.append([name, c_red("-") if val is None else str(val)])
|
||
print_table(["TOGGLE", "VALUE"], rows)
|
||
print()
|
||
elif perm_action == "get":
|
||
name = getattr(args, "toggle", None)
|
||
try:
|
||
res = menu_toggles.get_toggle(node, name)
|
||
except menu_toggles.MenuError as e:
|
||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||
sys.exit(2)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
elif res.get("ok"):
|
||
print(f" {c_bold(name)} = {c_cyan(res['value'])}")
|
||
else:
|
||
print(c_red(f" Error: {res.get('error')}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
elif perm_action == "set":
|
||
name = getattr(args, "toggle", None)
|
||
value = getattr(args, "value", None)
|
||
try:
|
||
res = menu_toggles.set_toggle(node, name, value)
|
||
except menu_toggles.MenuError as e:
|
||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||
sys.exit(2)
|
||
if as_json:
|
||
print(json.dumps(res, indent=2))
|
||
elif res.get("ok"):
|
||
print(c_green(f" ✔ {name} = {res['value']} on {node} "
|
||
f"(readback verified)."))
|
||
else:
|
||
print(c_red(f" ✘ Set failed on {node}: {res.get('error')}"),
|
||
file=sys.stderr)
|
||
sys.exit(1)
|
||
elif perm_action == "describe":
|
||
tab = getattr(args, "tab", None)
|
||
try:
|
||
res = menu_toggles.describe_tab(node, tab)
|
||
except menu_toggles.MenuError as e:
|
||
print(c_red(f" Error: {e}"), file=sys.stderr)
|
||
sys.exit(2)
|
||
print(json.dumps(res, indent=2))
|
||
else:
|
||
print(c_red(f" Error: unknown action {perm_action!r}"),
|
||
file=sys.stderr)
|
||
sys.exit(2)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: DM (Direct Messaging & Work Orders)
|
||
# ---------------------------------------------------------------------------
|
||
def wait_for_reply(recipient: str, target: str, msg_id: str = None, timeout: int = 60) -> str:
|
||
"""Wait for recipient agent to reply, displaying an animated spinner."""
|
||
spinner_chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||
t0 = time.time()
|
||
idx = 0
|
||
dm_pos = DM_LOG.stat().st_size if DM_LOG.exists() else 0
|
||
found_reply = None
|
||
chat_hist_file = NETVM_ROOT / "logs" / "chat-history.jsonl"
|
||
hist_pos = chat_hist_file.stat().st_size if chat_hist_file.exists() else 0
|
||
|
||
sys.stdout.write("\n")
|
||
try:
|
||
while time.time() - t0 < timeout:
|
||
spin = spinner_chars[idx % len(spinner_chars)]
|
||
elapsed = int(time.time() - t0)
|
||
sys.stdout.write(f"\r {c_cyan(spin)} Awaiting response from {c_bold(recipient)}/{target}... ({elapsed}s/{timeout}s) [Ctrl+C to stop waiting]")
|
||
sys.stdout.flush()
|
||
idx += 1
|
||
|
||
# 1. Check DM_LOG for return message from recipient
|
||
if DM_LOG.exists() and DM_LOG.stat().st_size > dm_pos:
|
||
with open(DM_LOG, "r") as f:
|
||
f.seek(dm_pos)
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
d = json.loads(line)
|
||
ev_type = d.get("type", "")
|
||
sender = d.get("agent", "")
|
||
if sender == recipient and ev_type in ("sent", "send_done", "verified"):
|
||
m = d.get("msg") or d.get("body")
|
||
if m and (not msg_id or msg_id not in m):
|
||
found_reply = m
|
||
break
|
||
except Exception:
|
||
pass
|
||
dm_pos = f.tell()
|
||
|
||
if found_reply:
|
||
break
|
||
|
||
# 2. Check chat-history.jsonl (from harvester if active)
|
||
if chat_hist_file.exists() and chat_hist_file.stat().st_size > hist_pos:
|
||
with open(chat_hist_file, "r") as f:
|
||
f.seek(hist_pos)
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
d = json.loads(line)
|
||
if d.get("agent") == recipient:
|
||
m = d.get("text") or d.get("msg")
|
||
if m and (not msg_id or msg_id not in m) and not m.startswith("[from:super]"):
|
||
found_reply = m
|
||
break
|
||
except Exception:
|
||
pass
|
||
hist_pos = f.tell()
|
||
|
||
if found_reply:
|
||
break
|
||
|
||
# 3. Periodically poll thread messages via dm.py read every ~2.5s
|
||
if idx % 12 == 0:
|
||
try:
|
||
read_cmd = ["python3", str(BIN_DIR / "dm.py"), "read", "--agent", recipient, "--target", target, "--n", "3"]
|
||
r_read = subprocess.run(read_cmd, capture_output=True, text=True, timeout=8)
|
||
if r_read.returncode == 0 and r_read.stdout:
|
||
for block in r_read.stdout.split("\n---\n"):
|
||
block_s = block.strip()
|
||
if block_s and (not msg_id or f"[id:{msg_id}]" not in block_s) and not block_s.startswith("[from:super]"):
|
||
found_reply = block_s
|
||
break
|
||
except Exception:
|
||
pass
|
||
|
||
if found_reply:
|
||
break
|
||
|
||
time.sleep(0.2)
|
||
|
||
sys.stdout.write("\r" + " " * 85 + "\r")
|
||
sys.stdout.flush()
|
||
|
||
if found_reply:
|
||
print(f"\n{c_bold(c_cyan('[' + recipient + ']:'))}")
|
||
for l in found_reply.splitlines():
|
||
print(f" {l}")
|
||
print()
|
||
return found_reply
|
||
else:
|
||
print(c_yellow(f"\n (No response received from {recipient} within {timeout}s)\n"))
|
||
return None
|
||
|
||
except KeyboardInterrupt:
|
||
sys.stdout.write("\r" + " " * 85 + "\r")
|
||
sys.stdout.flush()
|
||
print(c_dim("\n (Stopped waiting for response)\n"))
|
||
return None
|
||
|
||
def get_available_targets(agent: str) -> list:
|
||
"""Find known sidechats and targets for an agent."""
|
||
targets = []
|
||
# 1. From job-sidechats.json
|
||
if (NETVM_ROOT / "job-sidechats.json").exists():
|
||
try:
|
||
with open(NETVM_ROOT / "job-sidechats.json") as f:
|
||
sc_data = json.load(f)
|
||
for k, v in sc_data.items():
|
||
if isinstance(v, dict):
|
||
if v.get("agent") == agent:
|
||
targets.append({"name": k, "type": "sidechat", "desc": f"registered coordination ({k})"})
|
||
elif agent in k:
|
||
targets.append({"name": k, "type": "sidechat", "desc": f"registered sidechat ({k})"})
|
||
except Exception:
|
||
pass
|
||
|
||
# 2. Add well-known agent sidechats
|
||
if agent == "646":
|
||
targets.append({"name": "646 tasks", "type": "sidechat", "desc": "primary task & check-in sidechat"})
|
||
targets.append({"name": "646-opm-work", "type": "sidechat", "desc": "opm work sidechat"})
|
||
elif agent == "opm":
|
||
targets.append({"name": "heartbeat", "type": "sidechat", "desc": "heartbeat & health sidechat"})
|
||
elif agent == "pip":
|
||
targets.append({"name": "646-pip-coord", "type": "sidechat", "desc": "pip & 646 coordination sidechat"})
|
||
|
||
# Deduplicate by name
|
||
seen = set()
|
||
unique_targets = []
|
||
for t in targets:
|
||
if t["name"] not in seen:
|
||
seen.add(t["name"])
|
||
unique_targets.append(t)
|
||
|
||
# Add Main Chat last with caution label
|
||
unique_targets.append({
|
||
"name": "main",
|
||
"type": "main",
|
||
"desc": "Main Chat (CAUTION: avoid per CHAT_POLICY.md)"
|
||
})
|
||
return unique_targets
|
||
|
||
def _render_chat_history(agent: str, target: str, limit: int = 5):
|
||
"""Fetch and display recent chat messages from thread."""
|
||
try:
|
||
read_cmd = ["python3", str(BIN_DIR / "dm.py"), "read", "--agent", agent, "--target", target, "--n", str(limit)]
|
||
r = subprocess.run(read_cmd, capture_output=True, text=True, timeout=12)
|
||
if r.returncode == 0 and r.stdout:
|
||
blocks = [b.strip() for b in r.stdout.split("\n---\n") if b.strip()]
|
||
if blocks:
|
||
print(c_dim("--- Recent Messages ---"))
|
||
for b in blocks[-limit:]:
|
||
if b.startswith("[from:super]"):
|
||
print(f"{c_green('[super]:')}")
|
||
elif f"[from:{agent}]" in b or f"from {agent}" in b:
|
||
print(f"{c_cyan('[' + agent + ']:')}")
|
||
else:
|
||
print(f"{c_dim('[message]:')}")
|
||
for line in b.splitlines()[:5]:
|
||
print(f" {line}")
|
||
print(c_dim("-----------------------\n"))
|
||
except Exception:
|
||
pass
|
||
|
||
def cmd_dm_chat(args):
|
||
agent = args.agent
|
||
target = getattr(args, "target", None)
|
||
timeout = getattr(args, "timeout", 60) or 60
|
||
|
||
print("\n" + c_bold(f"=== INTERACTIVE CONVERSATION SESSION: {c_cyan(agent.upper())} ===") + "\n")
|
||
|
||
# Target selection if not provided
|
||
if not target:
|
||
available = get_available_targets(agent)
|
||
print(c_bold("Select chat target:") + c_dim(" (Prefer sidechats to keep Main Chat clean)\n"))
|
||
for idx, t in enumerate(available, 1):
|
||
tag = c_green("[Sidechat - RECOMMENDED]") if t["type"] == "sidechat" else c_yellow("[Main Chat - USE WITH CAUTION]")
|
||
print(f" [{idx}] {c_bold(t['name']):<18} {tag} {c_dim(t['desc'])}")
|
||
print()
|
||
|
||
try:
|
||
choice = input(f"Choose target [1-{len(available)}] (default: 1): ").strip()
|
||
if not choice:
|
||
target = available[0]["name"]
|
||
else:
|
||
choice_idx = int(choice) - 1
|
||
if 0 <= choice_idx < len(available):
|
||
target = available[choice_idx]["name"]
|
||
else:
|
||
target = available[0]["name"]
|
||
except (ValueError, KeyboardInterrupt):
|
||
target = available[0]["name"]
|
||
|
||
print(f"\nEntering chat session with {c_bold(agent)} on {c_cyan(target)}...")
|
||
if target == "main":
|
||
print(c_yellow(" ⚠ WARNING: Main Chat targeted. Per CHAT_POLICY.md, keep interactions minimal."))
|
||
print(c_dim(" Commands: /exit (or /quit), /refresh (reload history), /switch (change target), /wo <title> <body>\n"))
|
||
|
||
# Initial history render
|
||
_render_chat_history(agent, target, limit=5)
|
||
|
||
# REPL loop
|
||
while True:
|
||
try:
|
||
prompt_str = f"{c_green('super')} ({c_cyan(target)}) > "
|
||
line = input(prompt_str).strip()
|
||
if not line:
|
||
continue
|
||
|
||
# Handle slash commands
|
||
if line in ("/exit", "/quit", ":q"):
|
||
print(c_dim("\nExited chat session.\n"))
|
||
break
|
||
elif line in ("/refresh", "/history"):
|
||
_render_chat_history(agent, target, limit=10)
|
||
continue
|
||
elif line == "/switch":
|
||
available = get_available_targets(agent)
|
||
print(c_bold("\nSelect new target:"))
|
||
for idx, t in enumerate(available, 1):
|
||
print(f" [{idx}] {t['name']} ({t['desc']})")
|
||
try:
|
||
c = input(f"Choose target [1-{len(available)}]: ").strip()
|
||
if c and int(c) - 1 in range(len(available)):
|
||
target = available[int(c) - 1]["name"]
|
||
print(f"Switched target to {c_cyan(target)}.\n")
|
||
_render_chat_history(agent, target, limit=5)
|
||
except Exception:
|
||
pass
|
||
continue
|
||
elif line.startswith("/wo "):
|
||
parts = line[4:].split(" ", 1)
|
||
wo_title = parts[0]
|
||
wo_body = parts[1] if len(parts) > 1 else wo_title
|
||
wo_id = hashlib.sha256(f"super-{agent}-{wo_title}-{time.time()}".encode()).hexdigest()[:8]
|
||
wo_content = f"[WO:{wo_id}] {wo_title} — {wo_body}"
|
||
signed_wire, mid = sign_message("super", wo_content, msg_id=wo_id)
|
||
print(c_dim(f"Issuing Work Order [WO:{wo_id}]..."))
|
||
r = subprocess.run([
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", "super", "--to", agent, "--target", target,
|
||
"--raw", signed_wire
|
||
], capture_output=True, text=True)
|
||
if r.returncode == 0:
|
||
print(c_green(f"✔ Work Order [WO:{wo_id}] delivered."))
|
||
wait_for_reply(agent, target, msg_id=wo_id, timeout=timeout)
|
||
else:
|
||
print(c_red(f"Delivery failed: {r.stderr or r.stdout}"))
|
||
continue
|
||
|
||
# Standard message
|
||
signed_wire, mid = sign_message("super", line)
|
||
r = subprocess.run([
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", "super", "--to", agent, "--target", target,
|
||
"--raw", signed_wire
|
||
], capture_output=True, text=True)
|
||
|
||
if r.returncode == 0:
|
||
print(c_green(f"✔ Delivered & verified [{mid}]."))
|
||
wait_for_reply(agent, target, msg_id=mid, timeout=timeout)
|
||
else:
|
||
print(c_red(f"Delivery failed: {r.stderr or r.stdout}"))
|
||
|
||
except (KeyboardInterrupt, EOFError):
|
||
print(c_dim("\nExited chat session.\n"))
|
||
break
|
||
|
||
def cmd_dm_send(args):
|
||
sender = resolve_sender(args)
|
||
recipient = args.to
|
||
target = getattr(args, "target", "main")
|
||
message = args.message
|
||
wait = getattr(args, "wait", False)
|
||
timeout = getattr(args, "timeout", 60) or 60
|
||
allow_main = getattr(args, "allow_main_chat", False)
|
||
|
||
if target == "main":
|
||
if not allow_main:
|
||
print(c_red("ERROR: Targeting Main Chat is blocked by CHAT_POLICY.md to prevent chat degradation."), file=sys.stderr)
|
||
print(c_dim(" To send routine task/payloads, use a sidechat: --target '<sidechat>'.\n To override intentionally, pass --allow-main-chat."), file=sys.stderr)
|
||
sys.exit(1)
|
||
print(c_yellow(" ⚠ [CHAT POLICY OVERRIDE] Main Chat targeted with --allow-main-chat. Keep interaction minimal."))
|
||
|
||
# Soft pre-flight grammar check against lookup_internal
|
||
try:
|
||
import lookup_engine
|
||
is_val, kind, hint = lookup_engine.validate_outbound_sentence(message)
|
||
if not is_val and hint:
|
||
print(c_yellow(f"\n ⚠ [GRAMMAR NOTICE]\n {hint}\n"), file=sys.stderr)
|
||
except Exception:
|
||
pass
|
||
|
||
should_sign = (sender == "super") and not getattr(args, "no_sign", False)
|
||
mid = None
|
||
if should_sign:
|
||
try:
|
||
signed_wire, mid = sign_message(sender, message)
|
||
cmd = [
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", sender,
|
||
"--to", recipient,
|
||
"--target", target,
|
||
"--raw",
|
||
signed_wire
|
||
]
|
||
print(f"Dispatching Cryptographically Signed DM [{c_green('verified from:' + sender)}] -> [{c_bold(recipient)}/{target}]...")
|
||
res = subprocess.run(cmd)
|
||
if res.returncode != 0:
|
||
sys.exit(res.returncode)
|
||
if wait:
|
||
wait_for_reply(recipient, target, msg_id=mid, timeout=timeout)
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
print(f"Signing notice: {e}, sending standard DM...", file=sys.stderr)
|
||
|
||
cmd = [
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", sender,
|
||
"--to", recipient,
|
||
"--target", target,
|
||
]
|
||
if getattr(args, "expect_reply", False):
|
||
cmd.append("--expect-reply")
|
||
if getattr(args, "reply_timeout", None):
|
||
cmd.extend(["--reply-timeout", str(args.reply_timeout)])
|
||
cmd.append(message)
|
||
|
||
print(f"Dispatching DM [{c_cyan('from:' + sender)}] -> [{c_bold(recipient)}/{target}]...")
|
||
res = subprocess.run(cmd)
|
||
if res.returncode != 0:
|
||
sys.exit(res.returncode)
|
||
if wait:
|
||
wait_for_reply(recipient, target, msg_id=mid, timeout=timeout)
|
||
sys.exit(0)
|
||
|
||
def cmd_dm_wo(args):
|
||
sender = resolve_sender(args)
|
||
recipient = args.to
|
||
target = getattr(args, "target", None)
|
||
if not target:
|
||
target = DEFAULT_AGENT_SIDECHATS.get(recipient, "main")
|
||
title = args.title
|
||
body = args.body
|
||
priority = getattr(args, "priority", "routine")
|
||
wait = getattr(args, "wait", True)
|
||
timeout = getattr(args, "timeout", 60) or 60
|
||
allow_main = getattr(args, "allow_main_chat", False)
|
||
|
||
if target == "main":
|
||
if not allow_main:
|
||
print(c_red("ERROR: Work Orders must target sidechats per CHAT_POLICY.md."), file=sys.stderr)
|
||
print(c_dim(" Defaulting to sidechat recommended. To override, pass --allow-main-chat."), file=sys.stderr)
|
||
sys.exit(1)
|
||
print(c_yellow(" ⚠ [CHAT POLICY OVERRIDE] Work Order targeted to Main Chat with --allow-main-chat."))
|
||
|
||
wo_id = hashlib.sha256(f"{sender}-{recipient}-{title}-{time.time()}".encode()).hexdigest()[:8]
|
||
prefix = "[URGENT] " if priority == "urgent" else ""
|
||
wo_content = f"{prefix}[WO:{wo_id}] [from {sender}] {title} — {body}"
|
||
|
||
|
||
should_sign = (sender == "super") and not getattr(args, "no_sign", False)
|
||
if should_sign:
|
||
try:
|
||
signed_wire, mid = sign_message(sender, wo_content, msg_id=wo_id)
|
||
cmd = [
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", sender,
|
||
"--to", recipient,
|
||
"--target", target,
|
||
"--raw",
|
||
signed_wire
|
||
]
|
||
print(f"Issuing Cryptographically Signed Work Order {c_green('[WO:' + wo_id + ']')} [{c_green('verified from:' + sender)}] -> [{c_bold(recipient)}/{c_cyan(target)}]...")
|
||
res = subprocess.run(cmd)
|
||
if res.returncode != 0:
|
||
sys.exit(res.returncode)
|
||
if wait:
|
||
wait_for_reply(recipient, target, msg_id=wo_id, timeout=timeout)
|
||
sys.exit(0)
|
||
except Exception as e:
|
||
print(f"Signing notice: {e}, sending standard work order...", file=sys.stderr)
|
||
|
||
cmd = [
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", sender,
|
||
"--to", recipient,
|
||
"--target", target,
|
||
"--expect-reply",
|
||
wo_content
|
||
]
|
||
|
||
print(f"Issuing Work Order {c_cyan('[WO:' + wo_id + ']')} [{c_cyan('from:' + sender)}] -> [{c_bold(recipient)}/{c_cyan(target)}]...")
|
||
res = subprocess.run(cmd)
|
||
if res.returncode != 0:
|
||
sys.exit(res.returncode)
|
||
if wait:
|
||
wait_for_reply(recipient, target, msg_id=wo_id, timeout=timeout)
|
||
sys.exit(0)
|
||
|
||
def cmd_dm_ack(args):
|
||
sender = resolve_sender(args)
|
||
recipient = args.to
|
||
target = getattr(args, "target", "main")
|
||
ref_id = args.id
|
||
|
||
ack_message = f"[ACK:{ref_id}]"
|
||
cmd = [
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", sender,
|
||
"--to", recipient,
|
||
"--target", target,
|
||
ack_message
|
||
]
|
||
|
||
print(f"Sending Acknowledgment {c_green('[ACK:' + ref_id + ']')} -> [{c_bold(recipient)}/{target}]...")
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
def cmd_dm_verify(args):
|
||
cmd = ["python3", str(BIN_DIR / "dm.py"), "verify-sig"]
|
||
if getattr(args, "agent", None):
|
||
cmd.extend(["--agent", args.agent])
|
||
if getattr(args, "target", None):
|
||
cmd.extend(["--target", args.target])
|
||
if getattr(args, "message", None):
|
||
cmd.append(args.message)
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
def cmd_dm_log(args):
|
||
n = getattr(args, "n", 25)
|
||
filter_agent = getattr(args, "agent", None)
|
||
filter_text = getattr(args, "filter", None)
|
||
|
||
if not DM_LOG.exists():
|
||
print(c_dim("dm-log.jsonl not found."))
|
||
return
|
||
|
||
entries = []
|
||
with open(DM_LOG, "r") as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
data = json.loads(line)
|
||
if filter_agent and (data.get("agent") != filter_agent and data.get("to") != filter_agent):
|
||
continue
|
||
if filter_text:
|
||
if filter_text.lower() not in json.dumps(data).lower():
|
||
continue
|
||
entries.append(data)
|
||
except Exception:
|
||
continue
|
||
|
||
entries = entries[-n:]
|
||
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "entries": entries}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold(f"=== RECENT INTER-AGENT DMs (last {len(entries)}) ===") + "\n")
|
||
|
||
headers = ["TIME", "TYPE", "FROM -> TO", "TARGET", "STATUS / DETAIL"]
|
||
rows = []
|
||
for item in entries:
|
||
t_rel = parse_relative_time(item.get("ts", ""))
|
||
ev_type = item.get("type", "unknown")
|
||
|
||
# Colorize type badges
|
||
if ev_type == "sent" or ev_type == "send_done":
|
||
badge = c_green("SENT")
|
||
elif ev_type == "verified":
|
||
badge = c_green("✔ VERIFIED")
|
||
elif "fail" in ev_type:
|
||
badge = c_red("FAIL")
|
||
elif ev_type == "retry":
|
||
badge = c_yellow("RETRY")
|
||
elif ev_type == "send_start":
|
||
badge = c_blue("START")
|
||
else:
|
||
badge = c_dim(ev_type[:8].upper())
|
||
|
||
from_to = f"{item.get('agent', '-')} -> {item.get('to', '-')}"
|
||
target = item.get("target", "-")
|
||
|
||
msg = item.get("msg") or item.get("reason") or item.get("error") or ""
|
||
if not msg:
|
||
if ev_type == "retry":
|
||
msg = f"attempt {item.get('attempt', '?')} (id: {item.get('id', '-')})"
|
||
elif ev_type == "failed":
|
||
msg = f"delivery failed (id: {item.get('id', '-')}, verified: {item.get('verified', False)})"
|
||
elif ev_type == "sent":
|
||
msg = f"delivered (id: {item.get('id', '-')}, verified: {item.get('verified', True)})"
|
||
elif ev_type == "verified":
|
||
msg = f"confirmed in DOM (id: {item.get('id', '-')})"
|
||
elif ev_type == "send_done":
|
||
msg = f"completed send (id: {item.get('id', '-')})"
|
||
elif ev_type == "alias_resolved":
|
||
msg = f"resolved {item.get('target')} -> {item.get('thread_uuid', '')[:8]}"
|
||
elif ev_type == "nav_ok":
|
||
msg = f"navigated to thread {item.get('thread_uuid', '')[:8]}"
|
||
elif item.get("id"):
|
||
msg = f"id: {item.get('id')}"
|
||
msg_clean = " ".join(msg.split())
|
||
|
||
# Highlight protocol tokens
|
||
if "[WO:" in msg_clean:
|
||
msg_clean = re.sub(r"\[WO:([a-f0-9]+)\]", lambda m: c_magenta(m.group(0)), msg_clean)
|
||
if "[ACK:" in msg_clean:
|
||
msg_clean = re.sub(r"\[ACK:([a-f0-9]+)\]", lambda m: c_green(m.group(0)), msg_clean)
|
||
if "[JOB" in msg_clean:
|
||
msg_clean = re.sub(r"\[JOB ([^\]]+)\]", lambda m: c_cyan(m.group(0)), msg_clean)
|
||
if "[RESULT" in msg_clean:
|
||
msg_clean = re.sub(r"\[RESULT ([^\]]+)\]", lambda m: c_yellow(m.group(0)), msg_clean)
|
||
|
||
rows.append([
|
||
t_rel,
|
||
badge,
|
||
from_to,
|
||
target[:16],
|
||
msg_clean[:60] if msg_clean else c_dim("-")
|
||
])
|
||
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" Commands: super dm tail | super dm send --to <agent> \"msg\" | super dm wo --to <agent> --title \"T\" \"body\"") + "\n")
|
||
|
||
def cmd_dm_tail(args):
|
||
filter_text = getattr(args, "filter", None)
|
||
print(c_bold("=== STREAMING DM LOG (Ctrl+C to stop) ===") + "\n")
|
||
|
||
if not DM_LOG.exists():
|
||
print(c_red("Error: dm-log.jsonl does not exist."))
|
||
return
|
||
|
||
with open(DM_LOG, "r") as f:
|
||
# Seek to end
|
||
f.seek(0, os.SEEK_END)
|
||
try:
|
||
while True:
|
||
line = f.readline()
|
||
if not line:
|
||
time.sleep(0.5)
|
||
continue
|
||
try:
|
||
data = json.loads(line.strip())
|
||
if filter_text and filter_text.lower() not in line.lower():
|
||
continue
|
||
ts = parse_relative_time(data.get("ts", ""))
|
||
ev_type = data.get("type", "")
|
||
sender = data.get("agent", "")
|
||
to = data.get("to", "")
|
||
msg = data.get("msg") or data.get("reason") or data.get("error") or ""
|
||
print(f"[{ts}] {c_cyan(ev_type.upper()):<12} {sender} -> {to}: {msg[:90]}")
|
||
except Exception:
|
||
pass
|
||
except KeyboardInterrupt:
|
||
print("\n" + c_dim("Tail stopped."))
|
||
|
||
TRANSFERS_REGISTRY_FILE = TRANSFERS_DIR / ".transfers-registry.json"
|
||
|
||
def _load_transfers_registry() -> dict:
|
||
if TRANSFERS_REGISTRY_FILE.exists():
|
||
try:
|
||
with open(TRANSFERS_REGISTRY_FILE, "r") as f:
|
||
return json.load(f)
|
||
except Exception:
|
||
return {}
|
||
return {}
|
||
|
||
def _save_transfers_registry(reg: dict):
|
||
TRANSFERS_DIR.mkdir(parents=True, exist_ok=True)
|
||
with open(TRANSFERS_REGISTRY_FILE, "w") as f:
|
||
json.dump(reg, f, indent=2)
|
||
|
||
def cmd_dm_send_file(args):
|
||
file_path = Path(args.file).resolve()
|
||
if not file_path.exists() or not file_path.is_file():
|
||
print(c_red(f"Error: File not found: {file_path}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
recipient = args.to
|
||
target = getattr(args, "target", None)
|
||
if not target:
|
||
target = DEFAULT_AGENT_SIDECHATS.get(recipient, "main")
|
||
|
||
if target == "main":
|
||
print(c_yellow(" ⚠ [CHAT POLICY NOTE] Targeting Main Chat. Per CHAT_POLICY.md, avoid using Main Chat for routine payloads."))
|
||
|
||
note = getattr(args, "note", None)
|
||
wait = getattr(args, "wait", False)
|
||
timeout = getattr(args, "timeout", 60) or 60
|
||
|
||
# 1. Compute checksum and file stats
|
||
file_stat = file_path.stat()
|
||
size_bytes = file_stat.st_size
|
||
size_kb = size_bytes / 1024.0
|
||
|
||
hasher = hashlib.sha256()
|
||
with open(file_path, "rb") as f:
|
||
while chunk := f.read(65536):
|
||
hasher.update(chunk)
|
||
sha256_full = hasher.hexdigest()
|
||
sha256_short = sha256_full[:12]
|
||
|
||
# 2. Stage file in transfers directory
|
||
agent_transfers_dir = TRANSFERS_DIR / recipient
|
||
agent_transfers_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
ts_prefix = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
dest_filename = f"{ts_prefix}_{file_path.name}"
|
||
dest_path = agent_transfers_dir / dest_filename
|
||
shutil.copy2(file_path, dest_path)
|
||
|
||
# 3. Construct protocol payload pointer
|
||
file_id = hashlib.sha256(f"{recipient}-{dest_filename}-{time.time()}".encode()).hexdigest()[:8]
|
||
|
||
# Save to transfers registry
|
||
reg = _load_transfers_registry()
|
||
reg[file_id] = {
|
||
"file_id": file_id,
|
||
"recipient": recipient,
|
||
"original_name": file_path.name,
|
||
"original_path": str(file_path),
|
||
"staged_path": str(dest_path),
|
||
"sha256": sha256_full,
|
||
"size_bytes": size_bytes,
|
||
"size_kb": f"{size_kb:.1f} KB",
|
||
"staged_at": datetime.now(timezone.utc).isoformat(),
|
||
"note": note,
|
||
"sender": "super",
|
||
"target": target
|
||
}
|
||
_save_transfers_registry(reg)
|
||
|
||
lines = [
|
||
f"[FILE:{file_id}] {file_path.name} ({size_kb:.1f} KB, sha256:{sha256_short})",
|
||
f"Path: {dest_path}",
|
||
]
|
||
if note:
|
||
lines.append(f"Note: {note}")
|
||
|
||
payload_message = "\n".join(lines)
|
||
|
||
# 4. Dispatch cryptographically signed DM to recipient sidechat
|
||
signed_wire, mid = sign_message("super", payload_message, msg_id=file_id)
|
||
print(f"Staged file {c_bold(file_path.name)} -> {c_dim(str(dest_path))}")
|
||
print(f"Dispatching File Pointer {c_green('[FILE:' + file_id + ']')} [{c_green('verified from:super')}] -> [{c_bold(recipient)}/{c_cyan(target)}]...")
|
||
|
||
cmd = [
|
||
"python3", str(BIN_DIR / "dm.py"), "send",
|
||
"--agent", "super",
|
||
"--to", recipient,
|
||
"--target", target,
|
||
"--raw",
|
||
signed_wire
|
||
]
|
||
res = subprocess.run(cmd)
|
||
if res.returncode != 0:
|
||
print(c_red(f"File notification delivery failed with code {res.returncode}"), file=sys.stderr)
|
||
sys.exit(res.returncode)
|
||
|
||
print(c_green(f"✔ File notification delivered to {recipient}/{target}."))
|
||
|
||
if wait:
|
||
wait_for_reply(recipient, target, msg_id=file_id, timeout=timeout)
|
||
|
||
def cmd_dm_files(args):
|
||
subaction = getattr(args, "files_action", "list")
|
||
filter_agent = getattr(args, "agent", None)
|
||
|
||
if subaction == "clean":
|
||
older_than_days = getattr(args, "older_than", 7)
|
||
cutoff_sec = time.time() - (older_than_days * 86400)
|
||
reg = _load_transfers_registry()
|
||
removed_count = 0
|
||
retained_reg = {}
|
||
|
||
# Scan files on disk
|
||
for ad in (TRANSFERS_DIR.iterdir() if TRANSFERS_DIR.exists() else []):
|
||
if not ad.is_dir() or ad.name.startswith("."):
|
||
continue
|
||
if filter_agent and ad.name != filter_agent:
|
||
continue
|
||
for fp in list(ad.iterdir()):
|
||
if fp.is_file() and fp.stat().st_mtime < cutoff_sec:
|
||
try:
|
||
fp.unlink()
|
||
removed_count += 1
|
||
except Exception as e:
|
||
print(c_red(f"Error removing {fp}: {e}"))
|
||
|
||
for fid, rec in reg.items():
|
||
staged = Path(rec.get("staged_path", ""))
|
||
if staged.exists():
|
||
retained_reg[fid] = rec
|
||
_save_transfers_registry(retained_reg)
|
||
|
||
print(c_green(f"✔ Cleaned {removed_count} payload file(s) older than {older_than_days} day(s)."))
|
||
return
|
||
|
||
if not TRANSFERS_DIR.exists():
|
||
print(c_dim("No transferred files found (transfers/ directory is empty)."))
|
||
return
|
||
|
||
reg = _load_transfers_registry()
|
||
records = []
|
||
agent_dirs = [TRANSFERS_DIR / filter_agent] if filter_agent else sorted(TRANSFERS_DIR.iterdir())
|
||
|
||
for ad in agent_dirs:
|
||
if not ad.is_dir() or ad.name.startswith("."):
|
||
continue
|
||
agent_name = ad.name
|
||
for fp in sorted(ad.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True):
|
||
if not fp.is_file() or fp.name.startswith("."):
|
||
continue
|
||
st = fp.stat()
|
||
size_kb = f"{st.st_size / 1024:.1f} KB"
|
||
mtime = datetime.fromtimestamp(st.st_mtime, tz=timezone.utc).isoformat()
|
||
t_rel = parse_relative_time(mtime)
|
||
|
||
clean_name = fp.name
|
||
m = re.match(r"^\d{8}_\d{6}_(.+)$", fp.name)
|
||
if m:
|
||
clean_name = m.group(1)
|
||
|
||
# Match in registry if available
|
||
reg_entry = next((r for r in reg.values() if r.get("staged_path") == str(fp)), {})
|
||
file_id = reg_entry.get("file_id", "-")
|
||
note = reg_entry.get("note", "")
|
||
|
||
records.append({
|
||
"agent": agent_name,
|
||
"file_id": file_id,
|
||
"filename": clean_name,
|
||
"staged_name": fp.name,
|
||
"path": str(fp),
|
||
"size_kb": size_kb,
|
||
"mtime": mtime,
|
||
"time_rel": t_rel,
|
||
"note": note
|
||
})
|
||
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "files": records}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold(f"=== TRANSFERRED PAYLOADS & STAGED FILES ({len(records)}) ===") + "\n")
|
||
headers = ["AGENT", "FILE ID", "FILENAME", "SIZE", "TRANSFERRED", "NOTE", "STAGED PATH"]
|
||
rows = []
|
||
for r in records:
|
||
rows.append([
|
||
c_cyan(r["agent"]),
|
||
c_green(r["file_id"]) if r["file_id"] != "-" else c_dim("-"),
|
||
c_bold(r["filename"]),
|
||
r["size_kb"],
|
||
r["time_rel"],
|
||
r["note"][:20] if r["note"] else c_dim("-"),
|
||
c_dim(r["path"])
|
||
])
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" Commands: super dm send-file --to <agent> <path> | super dm files clean [--older-than N]") + "\n")
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: THREAD (Chat Oversight via box-chat.py)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_thread_list(args):
|
||
agent = getattr(args, "agent", None)
|
||
if not agent:
|
||
print("\n" + c_bold("=== FLEET REGISTERED SIDECHATS & THREAD MAPPINGS ===") + "\n")
|
||
sc_file = NETVM_ROOT / "job-sidechats.json"
|
||
rows = []
|
||
headers = ["TARGET / ALIAS", "AGENT", "THREAD UUID", "PURPOSE / NOTES"]
|
||
if sc_file.exists():
|
||
try:
|
||
with open(sc_file, "r") as f:
|
||
data = json.load(f)
|
||
for k, v in sorted(data.items()):
|
||
if isinstance(v, dict):
|
||
ag = v.get("agent", "-")
|
||
uuid = v.get("thread_uuid") or v.get("uuid") or "-"
|
||
desc = v.get("description") or v.get("purpose") or "-"
|
||
rows.append([c_cyan(k), c_bold(ag), uuid, desc[:45]])
|
||
elif isinstance(v, str):
|
||
rows.append([c_cyan(k), "-", v, "-"])
|
||
except Exception as e:
|
||
print(c_red(f"Error reading job-sidechats.json: {e}"))
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" To view specific thread: box thread view <agent> <uuid|alias>") + "\n")
|
||
print(c_dim(" To list agent active sessions: box thread list <agent>") + "\n")
|
||
return
|
||
|
||
threads = []
|
||
|
||
def is_noise(title):
|
||
t = (title or "").lower().strip()
|
||
return bool(re.search(r"generate.*(chat|session)?.*title", t))
|
||
|
||
# Fast path: try fast headless gateway via muse_hybrid (isolated per-node WARP egress)
|
||
try:
|
||
import muse_hybrid
|
||
gw_threads, gw_err = muse_hybrid.get_threads(agent)
|
||
if gw_threads and not gw_err:
|
||
for t in gw_threads:
|
||
if is_noise(t.get("title")):
|
||
continue
|
||
threads.append({
|
||
"id": t.get("session_id", ""),
|
||
"kind": "thread" if t.get("thread") else "chat",
|
||
"title": t.get("title") or "(no title)",
|
||
"participants": [agent],
|
||
"last_message_at": t.get("updated", "")
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
# Fallback to box-chat.py / CDP if gateway returned no threads
|
||
if not threads:
|
||
cmd = ["python3", str(BIN_DIR / "box-chat.py"), "thread-list", agent]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
try:
|
||
data = json.loads(res.stdout)
|
||
raw_threads = data.get("threads", []) if data.get("ok") else []
|
||
threads = [t for t in raw_threads if not is_noise(t.get("title"))]
|
||
except Exception:
|
||
threads = []
|
||
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "threads": threads}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold(f"=== ACTIVE THREADS FOR {agent.upper()} ({len(threads)}) ===") + "\n")
|
||
|
||
headers = ["ID / UUID", "KIND", "TITLE", "PARTICIPANTS", "LAST ACTIVE"]
|
||
rows = []
|
||
for t in threads:
|
||
tid = t.get("id", "")
|
||
tid_disp = c_cyan(tid[:12]) if tid != "main" else c_bold("main")
|
||
kind = t.get("kind", "")
|
||
title = t.get("title", "")
|
||
parts = ", ".join(t.get("participants", []))
|
||
last_act = parse_relative_time(t.get("last_message_at", ""))
|
||
|
||
rows.append([tid_disp, kind, title[:35], parts, last_act])
|
||
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(f" View thread: super thread view {agent} <id>") + "\n")
|
||
|
||
def cmd_thread_view(args):
|
||
agent = args.agent
|
||
thread_id = args.thread_id
|
||
limit = getattr(args, "limit", 15)
|
||
messages = []
|
||
|
||
# 1. Resolve alias or name if known
|
||
try:
|
||
import dm
|
||
resolved = dm.resolve_sidechat_target(thread_id, agent)
|
||
if resolved and resolved != thread_id:
|
||
thread_id = resolved
|
||
except Exception:
|
||
pass
|
||
|
||
# 2. If short UUID prefix (6-12 hex chars), resolve against agent's thread list
|
||
if re.fullmatch(r"[0-9a-fA-F]{6,12}", str(thread_id).strip()):
|
||
try:
|
||
import muse_hybrid
|
||
gw_threads, _ = muse_hybrid.get_threads(agent)
|
||
if gw_threads:
|
||
for t in gw_threads:
|
||
sid = t.get("session_id", "")
|
||
if sid.lower().startswith(str(thread_id).strip().lower()):
|
||
thread_id = sid
|
||
break
|
||
except Exception:
|
||
pass
|
||
|
||
# Fast path: try fast headless gateway via muse_hybrid (isolated per-node WARP egress)
|
||
try:
|
||
import muse_hybrid
|
||
gw_msgs, gw_err = muse_hybrid.get_history(agent, thread_id=thread_id, limit=limit)
|
||
if gw_msgs and not gw_err:
|
||
for m in gw_msgs:
|
||
role = m.get("role", "unknown")
|
||
messages.append({
|
||
"from": {"name": role, "role": role},
|
||
"text": m.get("text", "")
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
# Fallback to box-chat.py / CDP if gateway returned no messages
|
||
if not messages:
|
||
cmd = ["python3", str(BIN_DIR / "box-chat.py"), "thread-messages", agent, thread_id, "--limit", str(limit)]
|
||
try:
|
||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||
data = json.loads(res.stdout)
|
||
messages = data.get("messages", []) if data.get("ok") else []
|
||
except Exception:
|
||
messages = []
|
||
if not messages:
|
||
print(c_red(f"Error viewing thread {thread_id}: no messages returned"))
|
||
return
|
||
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "messages": messages}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold(f"=== THREAD {thread_id} ({agent}) — {len(messages)} messages ===") + "\n")
|
||
|
||
for msg in messages:
|
||
sender_info = msg.get("from", {})
|
||
sender_name = sender_info.get("name") or sender_info.get("role") or "unknown"
|
||
text = msg.get("text", "")
|
||
|
||
# Colorize sender
|
||
if sender_name in ("human", "super", "operator-main"):
|
||
sender_tag = c_yellow(f"[{sender_name}]")
|
||
else:
|
||
sender_tag = c_cyan(f"[{sender_name}]")
|
||
|
||
print(f"{sender_tag}:")
|
||
for line in text.split("\n"):
|
||
print(f" {line}")
|
||
print()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: JOB (Systemd Timers & Job Definitions)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_job_list(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-list"]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
||
try:
|
||
data = json.loads(res.stdout)
|
||
except Exception:
|
||
print(c_red("Failed to load jobs:") + f"\n{res.stdout}")
|
||
return
|
||
|
||
if args.json:
|
||
print(json.dumps(data, indent=2))
|
||
return
|
||
|
||
jobs = data.get("jobs", [])
|
||
print("\n" + c_bold(f"=== DEFINED JOBS ({len(jobs)}) ===") + "\n")
|
||
|
||
headers = ["NAME", "AGENT", "SCHEDULE", "TIMEOUT", "DESCRIPTION"]
|
||
rows = []
|
||
for j in jobs:
|
||
sched = j.get("schedule", "-")
|
||
sched_disp = c_green(sched) if sched != "manual" else c_dim("manual")
|
||
rows.append([
|
||
c_bold(j.get("name", "")),
|
||
j.get("agent", "-"),
|
||
sched_disp,
|
||
f"{j.get('timeout', '-')}s",
|
||
(j.get("description") or "-")[:50]
|
||
])
|
||
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" Commands: super job show <name> | super job status <name> | super job run <name> | super job create <name>") + "\n")
|
||
|
||
def cmd_job_show(args):
|
||
name = args.name
|
||
# 1. Fetch job definition via box-ctl
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-get", name]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
try:
|
||
jdata = json.loads(res.stdout)
|
||
except Exception:
|
||
print(c_red(f"Failed to query job '{name}': {res.stdout or res.stderr}"))
|
||
return
|
||
|
||
if not jdata.get("ok"):
|
||
print(c_red(f"Error: {jdata.get('error', 'Job not found')}"))
|
||
return
|
||
|
||
job = jdata.get("job", {})
|
||
|
||
# 2. Fetch timer status
|
||
tcmd = ["python3", str(BIN_DIR / "box-ctl.py"), "timer-status", name]
|
||
tres = subprocess.run(tcmd, capture_output=True, text=True)
|
||
tdata = None
|
||
try:
|
||
parsed_t = json.loads(tres.stdout)
|
||
if parsed_t.get("ok"):
|
||
tdata = parsed_t
|
||
except Exception:
|
||
pass
|
||
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "job": job, "timer": tdata}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold(f"=== JOB DEFINITION: {name} ===") + "\n")
|
||
print(f" Description : {job.get('description', '-')}")
|
||
print(f" Target Agent: {c_cyan(job.get('agent', '-'))}")
|
||
sched = job.get('schedule', '-')
|
||
print(f" Schedule : {c_green(sched) if sched != 'manual' else c_dim('manual')}")
|
||
print(f" Timeout : {job.get('timeout', 300)}s")
|
||
print(f" On Failure : {job.get('on_failure', 'alert')}")
|
||
if job.get("chain_next"):
|
||
print(f" Chain Next : {job.get('chain_next')}")
|
||
if job.get("dm_target"):
|
||
print(f" DM Target : {job.get('dm_target')}")
|
||
|
||
sidechat = job.get("sidechat")
|
||
if sidechat and any(sidechat.values()):
|
||
print(f"\n {c_bold('Sidechat Config:')}")
|
||
print(f" Create : {sidechat.get('create', False)}")
|
||
if sidechat.get("name_template"):
|
||
print(f" Name Template : {sidechat.get('name_template')}")
|
||
if sidechat.get("reuse_key"):
|
||
print(f" Reuse Key : {sidechat.get('reuse_key')}")
|
||
|
||
followup = job.get("followup")
|
||
if followup:
|
||
print(f"\n {c_bold('Follow-up Policy:')}")
|
||
print(f" Expect Reply : {followup.get('expect_reply', False)}")
|
||
print(f" Timeout : {followup.get('timeout', '-')}")
|
||
print(f" Nudges : {followup.get('nudges', 0)}")
|
||
if followup.get("escalate"):
|
||
print(f" Escalate To : {followup.get('escalate')}")
|
||
if followup.get("route"):
|
||
print(f" Route : {followup.get('route')}")
|
||
|
||
print(f"\n {c_bold('Prompt Template:')}")
|
||
prompt = job.get("prompt_template", "")
|
||
for line in prompt.splitlines():
|
||
print(f" {c_dim(line)}")
|
||
|
||
print(f"\n {c_bold('Systemd Timer Status:')}")
|
||
if tdata:
|
||
unit = tdata.get("unit", f"job-{name}.timer")
|
||
act = badge_ok() if tdata.get("active") else badge_err("INACTIVE")
|
||
en = str(tdata.get("enabled"))
|
||
cal = tdata.get("oncalendar") or "-"
|
||
last_run = tdata.get("last_run") or "-"
|
||
next_run = tdata.get("next_run") or "-"
|
||
res_str = tdata.get("last_result") or "-"
|
||
print(f" Unit File : {unit}")
|
||
print(f" Active : {act}")
|
||
print(f" Enabled : {en}")
|
||
print(f" Schedule : {c_cyan(cal)}")
|
||
print(f" Last Run : {last_run}")
|
||
print(f" Next Run : {next_run}")
|
||
print(f" Result : {res_str}")
|
||
else:
|
||
print(f" {badge_dim('NOT CONFIGURED')} (Run 'super job enable {name}' to activate)")
|
||
print()
|
||
|
||
def cmd_job_status(args):
|
||
name = getattr(args, "name", None)
|
||
if name:
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "timer-status", name]
|
||
else:
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "timer-list"]
|
||
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
try:
|
||
data = json.loads(res.stdout)
|
||
except Exception:
|
||
print(c_red("Failed to query timers:") + f"\n{res.stdout}")
|
||
return
|
||
|
||
if args.json:
|
||
print(json.dumps(data, indent=2))
|
||
return
|
||
|
||
if name:
|
||
if not data.get("ok"):
|
||
print(c_red(f"Error querying timer '{name}': {data.get('error', 'Unknown error')}"))
|
||
return
|
||
print(f"\n{c_bold('Timer Status: ' + name)}")
|
||
print(f" Unit File : {data.get('unit')}")
|
||
print(f" Active : {badge_ok() if data.get('active') else badge_err('INACTIVE')}")
|
||
print(f" Enabled : {data.get('enabled')}")
|
||
print(f" Schedule : {c_cyan(data.get('oncalendar', '-'))}")
|
||
print(f" Last Run : {data.get('last_run') or '-'}")
|
||
print(f" Next Run : {data.get('next_run') or '-'}")
|
||
print(f" Result : {data.get('last_result') or '-'}\n")
|
||
else:
|
||
timers = data.get("timers", [])
|
||
print("\n" + c_bold(f"=== ACTIVE TIMERS ({len(timers)}) ===") + "\n")
|
||
headers = ["NAME", "UNIT", "ACTIVE", "ENABLED"]
|
||
rows = []
|
||
for t in timers:
|
||
act = badge_ok() if t.get("active") else badge_err("INACTIVE")
|
||
rows.append([c_bold(t.get("name", "")), t.get("unit", ""), act, str(t.get("enabled"))])
|
||
print_table(headers, rows)
|
||
print()
|
||
|
||
def cmd_job_create(args):
|
||
name = args.name
|
||
if not re.match(r"^[a-z0-9-]{1,64}$", name):
|
||
print(c_red("Error: Job name must match ^[a-z0-9-]{1,64}$"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
job_file = JOBS_DIR / f"{name}.json"
|
||
existed = job_file.exists()
|
||
update_mode = getattr(args, "update", False)
|
||
if existed and not update_mode:
|
||
print(c_red(f"Error: Job '{name}' already exists. Use --update to modify existing job."), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
file_arg = getattr(args, "file", None)
|
||
if file_arg:
|
||
try:
|
||
if file_arg == "-":
|
||
raw_data = json.load(sys.stdin)
|
||
else:
|
||
with open(file_arg, "r") as f:
|
||
raw_data = json.load(f)
|
||
job_dict = raw_data
|
||
job_dict["name"] = name
|
||
except Exception as e:
|
||
print(c_red(f"Error loading JSON from '{file_arg}': {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
else:
|
||
# Check required fields
|
||
if not args.agent:
|
||
print(c_red("Error: --agent is required when not using --file."), file=sys.stderr)
|
||
sys.exit(1)
|
||
if not args.prompt:
|
||
print(c_red("Error: --prompt is required when not using --file."), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
job_dict = {
|
||
"name": name,
|
||
"agent": args.agent,
|
||
"schedule": args.schedule or "manual",
|
||
"description": args.description or f"Job {name}",
|
||
"prompt_template": args.prompt,
|
||
"timeout": args.timeout or 300,
|
||
"on_failure": getattr(args, "on_failure", "alert") or "alert",
|
||
"chain_next": getattr(args, "chain_next", None),
|
||
}
|
||
if getattr(args, "sidechat_create", False) or getattr(args, "sidechat_name", None) or getattr(args, "reuse_key", None):
|
||
job_dict["sidechat"] = {
|
||
"create": bool(getattr(args, "sidechat_create", False) or getattr(args, "sidechat_name", None) or getattr(args, "reuse_key", None)),
|
||
}
|
||
if getattr(args, "sidechat_name", None):
|
||
job_dict["sidechat"]["name_template"] = args.sidechat_name
|
||
if getattr(args, "reuse_key", None):
|
||
job_dict["sidechat"]["reuse_key"] = args.reuse_key
|
||
|
||
if getattr(args, "dm_target", None):
|
||
job_dict["dm_target"] = args.dm_target
|
||
|
||
# Call box-ctl.py job-put
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-put", name]
|
||
res = subprocess.run(cmd, input=json.dumps(job_dict), capture_output=True, text=True)
|
||
try:
|
||
resp = json.loads(res.stdout)
|
||
except Exception:
|
||
print(c_red(f"Failed to save job via box-ctl: {res.stdout or res.stderr}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if not resp.get("ok"):
|
||
print(c_red(f"Error saving job: {resp.get('error', 'unknown error')}"), file=sys.stderr)
|
||
if "detail" in resp:
|
||
print(c_dim(f"Detail: {json.dumps(resp['detail'])}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
action_label = "Updated" if existed else "Created"
|
||
print(c_green(f"✔ {action_label} job '{name}' (saved to jobs/{name}.json and committed to git)"))
|
||
|
||
# Manage timer
|
||
schedule = job_dict.get("schedule", "manual")
|
||
no_enable = getattr(args, "no_enable", False)
|
||
|
||
timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer"
|
||
timer_existed = timer_unit.exists()
|
||
|
||
if schedule != "manual" and not no_enable:
|
||
if timer_existed:
|
||
subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-delete", name, "--keep-job"],
|
||
capture_output=True, text=True)
|
||
r_timer = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-create", name],
|
||
capture_output=True, text=True)
|
||
try:
|
||
tresp = json.loads(r_timer.stdout)
|
||
if tresp.get("ok"):
|
||
print(c_green(f"✔ Systemd timer active: job-{name}.timer ({tresp.get('oncalendar')})"))
|
||
else:
|
||
print(c_yellow(f"⚠ Timer could not be activated: {tresp.get('error')}"))
|
||
except Exception:
|
||
print(c_yellow(f"⚠ Timer creation output: {r_timer.stdout or r_timer.stderr}"))
|
||
elif timer_existed and schedule == "manual":
|
||
subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-delete", name, "--keep-job"],
|
||
capture_output=True, text=True)
|
||
print(c_dim("Systemd timer removed because schedule is set to 'manual'."))
|
||
|
||
def cmd_job_enable(args):
|
||
name = args.name
|
||
job_file = JOBS_DIR / f"{name}.json"
|
||
if not job_file.exists():
|
||
print(c_red(f"Error: Job definition jobs/{name}.json not found."), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
try:
|
||
jdata = json.loads(job_file.read_text())
|
||
if jdata.get("schedule") == "manual":
|
||
print(c_red(f"Error: Cannot enable timer for job '{name}' with schedule 'manual'."), file=sys.stderr)
|
||
print(c_dim("Update schedule to a valid cron pattern first using 'super job create --update'."), file=sys.stderr)
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(c_red(f"Error reading job definition: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer"
|
||
if not timer_unit.exists():
|
||
print(f"Timer unit not found for '{name}'. Creating systemd timer...")
|
||
res = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-create", name],
|
||
capture_output=True, text=True)
|
||
else:
|
||
print(f"Activating existing timer for '{name}'...")
|
||
res = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-enable", name],
|
||
capture_output=True, text=True)
|
||
|
||
try:
|
||
data = json.loads(res.stdout)
|
||
if data.get("ok"):
|
||
print(c_green(f"✔ Timer job-{name}.timer enabled and active."))
|
||
else:
|
||
print(c_red(f"Error enabling timer: {data.get('error', 'unknown error')}"))
|
||
sys.exit(1)
|
||
except Exception:
|
||
print(res.stdout or res.stderr)
|
||
|
||
def cmd_job_disable(args):
|
||
name = args.name
|
||
timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer"
|
||
if not timer_unit.exists():
|
||
print(c_yellow(f"Timer job-{name}.timer is not currently installed."))
|
||
return
|
||
|
||
print(f"Disabling timer for '{name}'...")
|
||
res = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-disable", name],
|
||
capture_output=True, text=True)
|
||
subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-stop", name],
|
||
capture_output=True, text=True)
|
||
try:
|
||
data = json.loads(res.stdout)
|
||
if data.get("ok"):
|
||
print(c_green(f"✔ Timer job-{name}.timer disabled and stopped."))
|
||
else:
|
||
print(c_red(f"Error disabling timer: {data.get('error', 'unknown error')}"))
|
||
sys.exit(1)
|
||
except Exception:
|
||
print(res.stdout or res.stderr)
|
||
|
||
def cmd_job_delete(args):
|
||
name = args.name
|
||
job_file = JOBS_DIR / f"{name}.json"
|
||
timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer"
|
||
|
||
if not job_file.exists() and not timer_unit.exists():
|
||
print(c_red(f"Error: Job '{name}' not found."), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
force = getattr(args, "force", False)
|
||
yes = getattr(args, "yes", False)
|
||
|
||
if not (yes or force):
|
||
prompt_msg = f"Are you sure you want to delete job '{c_bold(name)}' and its timer? [y/N]: "
|
||
try:
|
||
choice = input(prompt_msg).strip().lower()
|
||
if choice not in ("y", "yes"):
|
||
print(c_dim("Deletion aborted."))
|
||
return
|
||
except KeyboardInterrupt:
|
||
print("\n" + c_dim("Deletion aborted."))
|
||
return
|
||
|
||
# 1. Delete timer if exists
|
||
if timer_unit.exists():
|
||
print(f"Removing systemd timer job-{name}.timer...")
|
||
r_timer = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-delete", name, "--keep-job"],
|
||
capture_output=True, text=True)
|
||
try:
|
||
tdata = json.loads(r_timer.stdout)
|
||
if not tdata.get("ok") and not force:
|
||
print(c_red(f"Error deleting timer: {tdata.get('error')}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
except Exception:
|
||
pass
|
||
|
||
# 2. Delete job file
|
||
if job_file.exists():
|
||
print(f"Removing job definition jobs/{name}.json...")
|
||
r_job = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "job-delete", name, "--force"],
|
||
capture_output=True, text=True)
|
||
try:
|
||
jdata = json.loads(r_job.stdout)
|
||
if jdata.get("ok"):
|
||
print(c_green(f"✔ Job '{name}' and its systemd units deleted successfully."))
|
||
else:
|
||
print(c_red(f"Error deleting job: {jdata.get('error')}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
except Exception:
|
||
print(r_job.stdout or r_job.stderr)
|
||
|
||
def cmd_job_run(args):
|
||
name = args.name
|
||
follow = getattr(args, "follow", False)
|
||
|
||
job_file = JOBS_DIR / f"{name}.json"
|
||
if not job_file.exists():
|
||
print(c_red(f"Error: Job definition jobs/{name}.json not found."), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
print(f"Triggering job dispatch for '{c_bold(name)}'...")
|
||
|
||
if not follow:
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-trigger", name]
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
# Follow mode: open log files and track from current end position
|
||
dm_pos = DM_LOG.stat().st_size if DM_LOG.exists() else 0
|
||
job_pos = JOB_LOG.stat().st_size if JOB_LOG.exists() else 0
|
||
|
||
# Trigger job in background
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-trigger", name]
|
||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||
|
||
print(c_dim(f"Following execution logs for '{name}' (Press Ctrl+C to exit)..."))
|
||
|
||
start_time = time.time()
|
||
dispatched = False
|
||
completed = False
|
||
|
||
try:
|
||
while time.time() - start_time < 90: # 90s max follow
|
||
if proc.poll() is not None and not dispatched:
|
||
out_txt, err_txt = proc.communicate()
|
||
try:
|
||
pdata = json.loads(out_txt)
|
||
if not pdata.get("ok"):
|
||
print(c_red(f"Trigger failed: {pdata.get('error')}"))
|
||
return
|
||
else:
|
||
dispatched = True
|
||
except Exception:
|
||
pass
|
||
|
||
if JOB_LOG.exists() and JOB_LOG.stat().st_size > job_pos:
|
||
with open(JOB_LOG, "r") as f:
|
||
f.seek(job_pos)
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
d = json.loads(line)
|
||
if d.get("job_name") == name or name in str(d.get("job_id", "")):
|
||
ev = d.get("type") or d.get("event") or "event"
|
||
badge = c_green("DISPATCH") if "dispatched" in ev else (
|
||
c_red("FAILED") if "fail" in ev else c_cyan(ev.upper())
|
||
)
|
||
print(f" {badge} [{c_bold(name)}] {d.get('target', '')} {d.get('error', '')}")
|
||
if "failed" in ev or "timeout" in ev:
|
||
completed = True
|
||
except Exception:
|
||
pass
|
||
job_pos = f.tell()
|
||
|
||
if DM_LOG.exists() and DM_LOG.stat().st_size > dm_pos:
|
||
with open(DM_LOG, "r") as f:
|
||
f.seek(dm_pos)
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
d = json.loads(line)
|
||
msg = d.get("msg") or d.get("error") or ""
|
||
if name in msg or "[RESULT" in msg or "[WO" in msg:
|
||
ev_type = d.get("type", "")
|
||
sender = d.get("agent", "")
|
||
to = d.get("to", "")
|
||
badge = c_green("VERIFIED") if ev_type == "verified" else c_cyan(ev_type.upper())
|
||
print(f" {badge} DM {sender} -> {to}: {msg[:100]}")
|
||
if "[RESULT" in msg:
|
||
completed = True
|
||
except Exception:
|
||
pass
|
||
dm_pos = f.tell()
|
||
|
||
if completed:
|
||
print(c_green(f"✔ Job execution completed."))
|
||
break
|
||
|
||
time.sleep(0.5)
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n" + c_dim("Stopped following."))
|
||
|
||
def cmd_job_log(args):
|
||
n = getattr(args, "n", 20)
|
||
filter_name = getattr(args, "name", None)
|
||
|
||
if not JOB_LOG.exists():
|
||
print(c_dim("job-log.jsonl not found."))
|
||
return
|
||
|
||
entries = []
|
||
with open(JOB_LOG, "r") as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
try:
|
||
d = json.loads(line)
|
||
if filter_name and d.get("job_name") != filter_name and filter_name not in d.get("job_id", ""):
|
||
continue
|
||
entries.append(d)
|
||
except Exception:
|
||
continue
|
||
|
||
entries = entries[-n:]
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "entries": entries}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold(f"=== RECENT JOB ACTIVITY (last {len(entries)}) ===") + "\n")
|
||
headers = ["TIME", "EVENT", "JOB ID", "AGENT", "DETAIL"]
|
||
rows = []
|
||
for item in entries:
|
||
t_rel = parse_relative_time(item.get("ts", ""))
|
||
ev = item.get("event", "-")
|
||
badge = c_green("DISPATCH") if "dispatched" in ev else c_dim(ev[:12])
|
||
job_id = item.get("job_id", "-")
|
||
agent = item.get("agent", "-")
|
||
detail = item.get("target") or item.get("error") or ""
|
||
|
||
rows.append([t_rel, badge, job_id[:25], agent, detail])
|
||
|
||
print_table(headers, rows)
|
||
print()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: WEB (Google Cloud VM Web Surfaces Probe)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_web_health(args):
|
||
endpoints = [
|
||
{"name": "Box Console (Front-Door)", "url": "https://box.muse-dev.online/"},
|
||
{"name": "Board Service", "url": "https://board.muse-dev.online/"},
|
||
{"name": "VM Reverse Proxy (sslip)", "url": "https://34-139-37-135.sslip.io/"},
|
||
{"name": "Box Timers API", "url": "https://box.muse-dev.online/api/box/timers"},
|
||
]
|
||
|
||
print("\n" + c_bold("=== GOOGLE CLOUD VM WEB SURFACES HEALTH (34.139.37.135) ===") + "\n")
|
||
|
||
results = []
|
||
headers = ["SURFACE", "ENDPOINT", "STATUS", "LATENCY", "SERVER / VIA"]
|
||
rows = []
|
||
|
||
for ep in endpoints:
|
||
t0 = time.time()
|
||
try:
|
||
req = urllib.request.Request(
|
||
ep["url"],
|
||
headers={"User-Agent": "super-cli/1.0 (bl; health-probe)"}
|
||
)
|
||
with urllib.request.urlopen(req, timeout=4) as resp:
|
||
elapsed_ms = int((time.time() - t0) * 1000)
|
||
code = resp.status
|
||
via = resp.headers.get("Via") or resp.headers.get("Server") or "ok"
|
||
badge = badge_ok(f"{code}")
|
||
except urllib.error.HTTPError as e:
|
||
elapsed_ms = int((time.time() - t0) * 1000)
|
||
code = e.code
|
||
via = e.headers.get("Via") or e.headers.get("Server") or "-"
|
||
# 401/403 means auth works as intended
|
||
if code in (401, 403):
|
||
badge = badge_ok(f"{code} AUTH_GATED")
|
||
else:
|
||
badge = badge_warn(f"{code}")
|
||
except Exception as e:
|
||
elapsed_ms = None
|
||
code = "ERR"
|
||
via = str(e)[:25]
|
||
badge = badge_err("UNREACHABLE")
|
||
|
||
lat_disp = f"{elapsed_ms}ms" if elapsed_ms is not None else "-"
|
||
rows.append([c_bold(ep["name"]), ep["url"], badge, lat_disp, via[:25]])
|
||
results.append({"name": ep["name"], "url": ep["url"], "code": code, "latency_ms": elapsed_ms})
|
||
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" Operator note: box.muse-dev.online requires PIN or Bearer session cookie.") + "\n")
|
||
|
||
if args.json:
|
||
print(json.dumps({"ok": True, "results": results}, indent=2))
|
||
|
||
def cmd_web_test_auth(args):
|
||
print("\n" + c_bold("Testing Box API Authentication Gate:") + "\n")
|
||
url = "https://box.muse-dev.online/api/box/timers"
|
||
try:
|
||
req = urllib.request.Request(url, headers={"User-Agent": "super-cli/1.0"})
|
||
with urllib.request.urlopen(req, timeout=4) as resp:
|
||
print(c_yellow(f"Unexpected: Endpoint responded without auth: HTTP {resp.status}"))
|
||
except urllib.error.HTTPError as e:
|
||
body = e.read().decode("utf-8", errors="ignore")
|
||
if e.code in (401, 403):
|
||
print(f" Endpoint: {url}")
|
||
print(f" Response: {badge_ok(f'HTTP {e.code}')} (Protection active)")
|
||
print(f" Body : {c_dim(body.strip()[:100])}\n")
|
||
else:
|
||
print(c_warn(f"HTTP {e.code}: {body}"))
|
||
except Exception as e:
|
||
print(c_red(f"Error testing auth: {e}"))
|
||
|
||
def cmd_web_sync(args):
|
||
print("\n" + c_bold("=== DATA SYNC INTEGRITY (bl -> VM) ===") + "\n")
|
||
files = [
|
||
("DM Log", DM_LOG),
|
||
("Job Log", JOB_LOG),
|
||
("Control Audit", CTL_LOG),
|
||
]
|
||
headers = ["DATA STORE", "PATH", "SIZE", "RECORDS", "LAST MODIFIED"]
|
||
rows = []
|
||
for label, path in files:
|
||
if path.exists():
|
||
stat = path.stat()
|
||
size_kb = f"{stat.st_size / 1024:.1f} KB"
|
||
# Count lines
|
||
try:
|
||
with open(path, "r") as f:
|
||
lines = sum(1 for _ in f)
|
||
except Exception:
|
||
lines = 0
|
||
mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
|
||
mtime_rel = parse_relative_time(mtime)
|
||
rows.append([c_bold(label), str(path), size_kb, str(lines), mtime_rel])
|
||
else:
|
||
rows.append([c_bold(label), str(path), "-", "0", c_dim("missing")])
|
||
|
||
print_table(headers, rows)
|
||
print()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: CRED (Credentials & Onboarding Client)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_cred_initiate(args):
|
||
import cred_client
|
||
client = cred_client.CredClient()
|
||
res = client.initiate(args.node, args.email, service=getattr(args, "service", "muse"), account_name=getattr(args, "account_name", None))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
else:
|
||
st = res.get("status")
|
||
if st == "awaiting_otp":
|
||
print("\n" + c_yellow(f"[APPROVAL_NEEDED] Code sent to {args.email} for node '{args.node}'.") + "\n")
|
||
print(f" Submit OTP with: super cred submit-otp --node {args.node} --otp <code>\n")
|
||
sys.exit(2)
|
||
elif st == "active":
|
||
print("\n" + c_green(f"[SUCCESS] Node '{args.node}' is already authenticated and active.") + "\n")
|
||
else:
|
||
print(f"[{st.upper()}] {res.get('message') or res.get('detail')}")
|
||
sys.exit(res.get("code", 1))
|
||
|
||
def cmd_cred_submit_otp(args):
|
||
import cred_client
|
||
client = cred_client.CredClient()
|
||
res = client.submit_otp(args.node, args.otp, email=getattr(args, "email", None))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
else:
|
||
st = res.get("status")
|
||
if st == "active":
|
||
print("\n" + c_green(f"[SUCCESS] Node '{args.node}' successfully signed in!") + "\n")
|
||
else:
|
||
print(f"[{st.upper()}] {res.get('message') or res.get('detail')}")
|
||
sys.exit(res.get("code", 1))
|
||
|
||
def cmd_cred_status(args):
|
||
import cred_client
|
||
client = cred_client.CredClient()
|
||
res = client.status(args.node)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
else:
|
||
print("\n" + c_bold(f"=== CRED STATUS: {args.node} ===") + "\n")
|
||
print(f" Node : {c_bold(res['node'])}")
|
||
print(f" Email : {res['email']}")
|
||
print(f" Status : {res['status']}")
|
||
print(f" CDP Port : {res['cdp_port'] or '-'}")
|
||
alive_b = badge_ok("YES") if res['session_alive'] else badge_warn("NO")
|
||
print(f" Session Alive: {alive_b}")
|
||
if res["detail"]:
|
||
print(f" Detail : {c_dim(res['detail'])}")
|
||
print()
|
||
|
||
def cmd_cred_link_instagram(args):
|
||
import cred_client
|
||
client = cred_client.CredClient()
|
||
res = client.link_instagram(args.node, notify=getattr(args, "notify", False))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
else:
|
||
print("\n" + c_bold(f"=== INSTAGRAM LINKING: {args.node} ===") + "\n")
|
||
if res.get("error"):
|
||
print(c_err(f" Error: {res['error']}"))
|
||
sys.exit(1)
|
||
print(f" Tailscale Portal: {c_cyan(res['portal_url'])}")
|
||
print(f" Direct IP Portal: {c_cyan(res['portal_ip_url'])}")
|
||
oauth_preview = res['direct_oauth_url'][:75] + "..." if res.get('direct_oauth_url') else "-"
|
||
print(f" OAuth URL : {c_dim(oauth_preview)}")
|
||
if getattr(args, "notify", False):
|
||
n_badge = badge_ok("SENT") if res['email_notified'] else badge_err("FAILED")
|
||
print(f" Email Alert : {n_badge}")
|
||
print("\n" + c_yellow(" → Tap either Tailscale link from your phone/browser to complete Meta age verification.") + "\n")
|
||
|
||
def cmd_cred_meta_audit(args):
|
||
import cred_client
|
||
client = cred_client.CredClient()
|
||
res = client.audit_meta(args.node)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(res, indent=2))
|
||
else:
|
||
print("\n" + c_bold(f"=== META ACCOUNTS CENTER AUDIT: {args.node} ===") + "\n")
|
||
if res.get("error"):
|
||
print(c_red(f" Error: {res['error']}"))
|
||
sys.exit(1)
|
||
email_str = res.get('email') or c_dim('(none / phone-only)')
|
||
print(f" Meta Account Email: {c_cyan(email_str)}")
|
||
profiles = res.get("profiles", [])
|
||
print(f" Linked Profiles ({len(profiles)}):")
|
||
for p_info in profiles:
|
||
p_type = p_info.get("type", "unknown")
|
||
p_name = p_info.get("name", "unknown")
|
||
badge = c_green(f"[{p_type}]") if p_type == "instagram" else c_magenta(f"[{p_type}]")
|
||
print(f" - {badge} {c_bold(p_name)}")
|
||
print()
|
||
|
||
def cmd_cred_list(args):
|
||
import cred_client
|
||
client = cred_client.CredClient()
|
||
items = client.list_all()
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(items, indent=2))
|
||
else:
|
||
print("\n" + c_bold("=== FLEET CREDENTIAL & ONBOARDING STATUS ===") + "\n")
|
||
headers = ["NODE", "STATUS", "CDP", "ALIVE", "EMAIL"]
|
||
rows = []
|
||
for it in items:
|
||
alive_b = badge_ok("YES") if it["session_alive"] else badge_warn("NO")
|
||
rows.append([c_bold(it['node']), it['status'], str(it['cdp_port'] or '-'), alive_b, it['email']])
|
||
print_table(headers, rows)
|
||
print()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: HARVEST (Response & Readback Harvester)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_harvest_run(args):
|
||
cmd = [sys.executable, str(RESPONSE_HARVESTER_PY), "--once"]
|
||
if getattr(args, "agent", None):
|
||
cmd.extend(["--agent", args.agent])
|
||
if getattr(args, "dry_run", False):
|
||
cmd.append("--dry-run")
|
||
if getattr(args, "json", False):
|
||
cmd.append("--json")
|
||
try:
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if getattr(args, "json", False):
|
||
print(res.stdout.strip())
|
||
else:
|
||
if res.stdout.strip():
|
||
print(res.stdout.strip())
|
||
if res.stderr.strip():
|
||
print(c_yellow(res.stderr.strip()), file=sys.stderr)
|
||
except Exception as e:
|
||
print(c_red(f"Error running harvest: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
def cmd_harvest_status(args):
|
||
watermarks = {}
|
||
if WATERMARKS_FILE.exists():
|
||
try:
|
||
with open(WATERMARKS_FILE, "r") as f:
|
||
watermarks = json.load(f)
|
||
except Exception:
|
||
pass
|
||
|
||
sidechats = {}
|
||
if JOB_SIDECHATS_FILE.exists():
|
||
try:
|
||
with open(JOB_SIDECHATS_FILE, "r") as f:
|
||
sidechats = json.load(f)
|
||
except Exception:
|
||
pass
|
||
|
||
latest_seen = {}
|
||
if CHAT_HISTORY_LOG.exists():
|
||
try:
|
||
with open(CHAT_HISTORY_LOG, "r") as f:
|
||
for line in f:
|
||
if not line.strip(): continue
|
||
try:
|
||
rec = json.loads(line)
|
||
k = f"{rec.get('agent')}:{rec.get('thread_id')}"
|
||
latest_seen[k] = rec.get("ts")
|
||
except Exception:
|
||
continue
|
||
except Exception:
|
||
pass
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({
|
||
"watermarks": watermarks,
|
||
"sidechats": sidechats,
|
||
"latest_events": latest_seen
|
||
}, indent=2))
|
||
return
|
||
|
||
now_str = datetime.now().strftime("%H:%M:%S")
|
||
print(f"\n=== RESPONSE HARVESTER STATUS === ({now_str} local)\n")
|
||
|
||
headers = ["AGENT", "THREAD / ALIAS", "ID", "WATERMARK", "LAST HARVESTED", "STATUS"]
|
||
rows = []
|
||
|
||
for agent in VALID_NODES:
|
||
wm_main = watermarks.get(f"{agent}:main", "-")
|
||
last_ts = latest_seen.get(f"{agent}:main")
|
||
status_b = badge_ok("ACTIVE") if wm_main != "-" else badge_dim("IDLE")
|
||
rows.append([
|
||
c_bold(agent),
|
||
"Main Chat",
|
||
c_dim("main"),
|
||
wm_main[:16] if wm_main != "-" else c_dim("-"),
|
||
parse_relative_time(last_ts) if last_ts else c_dim("none"),
|
||
status_b
|
||
])
|
||
|
||
for key, val in sidechats.items():
|
||
if key.startswith("_"): continue
|
||
if isinstance(val, dict):
|
||
tid = val.get("thread_uuid") or val.get("uuid")
|
||
a = val.get("agent", "opm")
|
||
elif isinstance(val, str):
|
||
tid = val
|
||
a = "opm"
|
||
else:
|
||
continue
|
||
|
||
if a == agent and tid:
|
||
wm_sc = watermarks.get(f"{agent}:{tid}", "-")
|
||
last_sc_ts = latest_seen.get(f"{agent}:{tid}")
|
||
status_sc = badge_ok("ACTIVE") if wm_sc != "-" else badge_dim("IDLE")
|
||
rows.append([
|
||
c_dim(f" └─ {agent}"),
|
||
key,
|
||
c_dim(tid[:8] + "…"),
|
||
wm_sc[:16] if wm_sc != "-" else c_dim("-"),
|
||
parse_relative_time(last_sc_ts) if last_sc_ts else c_dim("none"),
|
||
status_sc
|
||
])
|
||
|
||
print_table(headers, rows)
|
||
print(c_dim("\n Commands: super harvest run | super harvest tail\n"))
|
||
|
||
def _print_harvest_line(line: str, filter_text: str = None):
|
||
try:
|
||
e = json.loads(line)
|
||
text = e.get("text", "")
|
||
if filter_text and filter_text.lower() not in text.lower():
|
||
return
|
||
ts = e.get("ts", "")[11:19]
|
||
agent = e.get("agent", "unknown")
|
||
author = e.get("author", "unknown")
|
||
thread = e.get("thread_name") or e.get("thread_id", "main")
|
||
|
||
auth_color = c_green if author == "assistant" else c_cyan
|
||
snippet = text.replace("\n", " ")[:120]
|
||
print(f" {c_dim(ts)} {c_bold(f'[{agent}:{thread}]')} {auth_color(author)}: {snippet}")
|
||
except Exception:
|
||
pass
|
||
|
||
def cmd_harvest_tail(args):
|
||
if not CHAT_HISTORY_LOG.exists():
|
||
print(c_dim(f"Chat history log empty ({CHAT_HISTORY_LOG})"))
|
||
return
|
||
print(c_bold(f"=== TAILING CHAT HARVEST STREAM ({CHAT_HISTORY_LOG.name}) === (Ctrl+C to stop)") + "\n")
|
||
try:
|
||
with open(CHAT_HISTORY_LOG, "r") as f:
|
||
lines = f.readlines()
|
||
for line in lines[-15:]:
|
||
_print_harvest_line(line, getattr(args, "filter", None))
|
||
|
||
f.seek(0, os.SEEK_END)
|
||
while True:
|
||
line = f.readline()
|
||
if line:
|
||
_print_harvest_line(line, getattr(args, "filter", None))
|
||
else:
|
||
time.sleep(0.5)
|
||
except KeyboardInterrupt:
|
||
print()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: FOLLOWUP (Deadline Tracking & Nudges)
|
||
# ---------------------------------------------------------------------------
|
||
def cmd_followup_list(args):
|
||
followups = {}
|
||
if FOLLOWUPS_FILE.exists():
|
||
try:
|
||
with open(FOLLOWUPS_FILE, "r") as f:
|
||
followups = json.load(f)
|
||
except Exception:
|
||
pass
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(followups, indent=2))
|
||
return
|
||
|
||
now_str = datetime.now().strftime("%H:%M:%S")
|
||
print(f"\n=== FOLLOW-UP TRACKING === ({now_str} local)\n")
|
||
|
||
headers = ["DM ID", "ROUTE", "TARGET", "NUDGES", "DEADLINE", "STATUS", "SNIPPET"]
|
||
rows = []
|
||
now = datetime.now(timezone.utc)
|
||
|
||
for dm_id, rec in sorted(followups.items(), key=lambda x: x[1].get("sent_at", ""), reverse=True):
|
||
st = rec.get("status", "pending")
|
||
if not getattr(args, "all", False) and st not in ("pending", "escalated"):
|
||
continue
|
||
|
||
route = f"{rec.get('sender', '?')} → {c_bold(rec.get('recipient', '?'))}"
|
||
target = rec.get("target", "main")
|
||
if len(target) > 18:
|
||
target = target[:17] + "…"
|
||
nudges = f"{rec.get('nudges_sent', 0)}/{rec.get('nudges_allowed', 2)}"
|
||
|
||
deadline_str = rec.get("deadline")
|
||
deadline_display = c_dim("-")
|
||
if deadline_str and st == "pending":
|
||
try:
|
||
dl_dt = datetime.fromisoformat(deadline_str.replace("Z", "+00:00"))
|
||
diff = int((dl_dt - now).total_seconds())
|
||
if diff > 0:
|
||
deadline_display = c_green(f"in {diff // 60}m")
|
||
else:
|
||
deadline_display = c_red(f"{abs(diff) // 60}m overdue")
|
||
except Exception:
|
||
deadline_display = deadline_str[:16]
|
||
|
||
if st == "pending":
|
||
status_badge = badge_warn("PENDING")
|
||
elif st == "escalated":
|
||
status_badge = badge_err("ESCALATED")
|
||
elif st == "resolved":
|
||
status_badge = badge_ok("RESOLVED")
|
||
else:
|
||
status_badge = badge_dim(st.upper())
|
||
|
||
snippet = rec.get("prompt_snippet") or rec.get("resolved_snippet") or ""
|
||
snippet = snippet.replace("\n", " ")[:35]
|
||
|
||
rows.append([
|
||
c_bold(dm_id[:8]),
|
||
route,
|
||
target,
|
||
nudges,
|
||
deadline_display,
|
||
status_badge,
|
||
c_dim(snippet)
|
||
])
|
||
|
||
print_table(headers, rows)
|
||
if not getattr(args, "all", False):
|
||
print(c_dim("\n (Showing pending & escalated only; use --all to view resolved records)"))
|
||
print(c_dim(" Commands: super followup sweep | super followup cancel <dm_id>\n"))
|
||
|
||
def cmd_followup_sweep(args):
|
||
cmd = [sys.executable, str(FOLLOWUP_SWEEPER_PY), "--once"]
|
||
if getattr(args, "dry_run", False):
|
||
cmd.append("--dry-run")
|
||
try:
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if res.stdout.strip():
|
||
print(res.stdout.strip())
|
||
if res.stderr.strip():
|
||
print(c_yellow(res.stderr.strip()), file=sys.stderr)
|
||
except Exception as e:
|
||
print(c_red(f"Error running sweeper: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
def cmd_followup_cancel(args):
|
||
if not FOLLOWUPS_FILE.exists():
|
||
print(c_red("No follow-ups found."), file=sys.stderr)
|
||
sys.exit(1)
|
||
try:
|
||
with open(FOLLOWUPS_FILE, "r") as f:
|
||
data = json.load(f)
|
||
except Exception as e:
|
||
print(c_red(f"Error reading follow-ups: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
target_id = args.dm_id.strip()
|
||
match_key = None
|
||
for k in data.keys():
|
||
if k == target_id or k.startswith(target_id):
|
||
match_key = k
|
||
break
|
||
|
||
if not match_key:
|
||
print(c_red(f"Follow-up ID '{target_id}' not found."), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
data[match_key]["status"] = "canceled"
|
||
data[match_key]["canceled_at"] = datetime.now(timezone.utc).isoformat()
|
||
tmp_path = f"{FOLLOWUPS_FILE}.tmp.{os.getpid()}"
|
||
with open(tmp_path, "w") as f:
|
||
json.dump(data, f, indent=2)
|
||
os.replace(tmp_path, FOLLOWUPS_FILE)
|
||
print(c_green(f"● Cancelled follow-up {match_key}."))
|
||
|
||
# Also stand down the VM-side nudge controller (request-sweeper.py),
|
||
# which tracks dm_followup records independently of bl's local file.
|
||
# Best-effort: the local cancel already succeeded, so a VM failure
|
||
# (unreachable, no record, bad response) must not fail the command.
|
||
_vm_followup_cancel(match_key)
|
||
|
||
|
||
def _vm_followup_cancel(dm_id):
|
||
"""POST /api/box/followups/cancel so the VM-side sweeper stops nudging.
|
||
|
||
Best-effort: never raises. Prints a one-line outcome.
|
||
"""
|
||
import json as _json
|
||
import os as _os
|
||
import subprocess as _sp
|
||
import time as _time
|
||
import urllib.parse as _up
|
||
import urllib.request as _ur
|
||
import urllib.error as _ue
|
||
|
||
box_api = _os.environ.get("BOX_API_BASE", "https://box.muse-dev.online")
|
||
key = _os.environ.get("BOX_SIGN_KEY",
|
||
_os.path.expanduser("~/.ssh/id_ed25519"))
|
||
endpoint = "followups/cancel"
|
||
ts_now = str(int(_time.time()))
|
||
payload = ("%s\n%s" % (ts_now, endpoint)).encode()
|
||
try:
|
||
pr = _sp.run(["ssh-keygen", "-Y", "sign", "-f", key, "-n", "box"],
|
||
input=payload, capture_output=True, timeout=15)
|
||
sig = pr.stdout.decode().strip()
|
||
if not sig:
|
||
raise RuntimeError("empty signature")
|
||
except Exception as e:
|
||
print(c_warn(" VM follow-up cancel skipped (signing failed: %s)" % e))
|
||
return
|
||
query = _up.urlencode({"identity": "bl", "ts": ts_now, "sig": sig})
|
||
url = "%s/api/box/followups/cancel?%s" % (box_api, query)
|
||
try:
|
||
req = _ur.Request(url,
|
||
data=_json.dumps({"dm_id": dm_id}).encode(),
|
||
headers={"Content-Type": "application/json",
|
||
"User-Agent": "super-cli/1.0 (bl)"},
|
||
method="POST")
|
||
with _ur.urlopen(req, timeout=30) as r:
|
||
resp = _json.load(r)
|
||
except _ue.HTTPError as e:
|
||
if e.code == 404:
|
||
print(c_dim(" VM: no dm_followup record (bl-only follow-up)."))
|
||
else:
|
||
try:
|
||
detail = e.read().decode("utf-8", errors="ignore")[:120]
|
||
except Exception:
|
||
detail = ""
|
||
print(c_warn(" VM cancel failed: HTTP %s %s" % (e.code, detail)))
|
||
return
|
||
except Exception as e:
|
||
print(c_warn(" VM cancel failed: %s" % e))
|
||
return
|
||
if resp.get("canceled"):
|
||
print(c_green(" VM: follow-up canceled (%s)."
|
||
% resp.get("request_id")))
|
||
else:
|
||
print(c_dim(" VM: %s." % resp.get("reason", "not canceled")))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: PIPELINE (Multi-Agent Workflow Pipelines)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def cmd_pipeline_run(args):
|
||
name = args.name.strip()
|
||
job_file = JOBS_DIR / f"{name}.json"
|
||
if not job_file.exists():
|
||
print(c_red(f"Error: Pipeline root job '{name}.json' not found in jobs/"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
import pipeline_engine
|
||
run_id = pipeline_engine.generate_pipeline_run_id(name)
|
||
entry = pipeline_engine.create_pipeline(name, run_id=run_id)
|
||
target = entry.get("target")
|
||
|
||
print(c_bold(f"\n=== LAUNCHING MULTI-AGENT PIPELINE [{name}] ==="))
|
||
print(f" Run ID: {c_cyan(run_id)}")
|
||
print(f" Target: {c_yellow(target)}")
|
||
print(f" Step 1: {c_bold(name)}\n")
|
||
|
||
cmd = [sys.executable, str(JOB_DISPATCH_PY), name, "--pipeline-run", run_id, "--step-n", "1"]
|
||
if getattr(args, "dry_run", False):
|
||
cmd.append("--dry-run")
|
||
|
||
res = subprocess.run(cmd)
|
||
if res.returncode == 0:
|
||
print(c_green(f"\n✔ Pipeline '{run_id}' dispatched step 1 successfully."))
|
||
print(c_dim(" Track with: super pipeline status | super pipeline history\n"))
|
||
else:
|
||
print(c_red(f"\n✖ Step 1 dispatch failed with code {res.returncode}"), file=sys.stderr)
|
||
sys.exit(res.returncode)
|
||
|
||
|
||
def cmd_pipeline_status(args):
|
||
import pipeline_engine
|
||
active = pipeline_engine.list_active_pipelines()
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(active, indent=2))
|
||
return
|
||
|
||
now_str = datetime.now().strftime("%H:%M:%S")
|
||
print(f"\n=== ACTIVE MULTI-AGENT PIPELINES === ({now_str} local)\n")
|
||
|
||
if not active:
|
||
print(c_dim(" (no active pipeline runs currently executing)"))
|
||
print(c_dim(" Launch one with: super pipeline run <job_name>\n"))
|
||
return
|
||
|
||
headers = ["RUN ID", "PIPELINE", "TARGET", "STEP", "CURRENT AGENT", "ACTIVE JOB ID", "STARTED", "STATUS"]
|
||
rows = []
|
||
|
||
for p in active:
|
||
run_id = p.get("run_id", "-")
|
||
p_name = p.get("pipeline_name", "-")
|
||
target = p.get("target", "-")
|
||
cur_step_n = p.get("current_step", 1)
|
||
steps = p.get("steps", [])
|
||
last_step = steps[-1] if steps else {}
|
||
agent = last_step.get("agent", "-")
|
||
job_id = last_step.get("job_id", "-")
|
||
created_at = p.get("created_at")
|
||
|
||
rows.append([
|
||
c_bold(run_id),
|
||
p_name,
|
||
c_yellow(target),
|
||
f"Step {cur_step_n}",
|
||
c_cyan(agent),
|
||
c_dim(job_id[:16] + "…") if len(job_id) > 16 else job_id,
|
||
parse_relative_time(created_at) if created_at else c_dim("-"),
|
||
badge_ok("RUNNING"),
|
||
])
|
||
|
||
print_table(headers, rows)
|
||
|
||
# Details on steps for active runs
|
||
for p in active:
|
||
print(c_bold(f"\n Active Pipeline Details [{p.get('run_id')}]:"))
|
||
for s in p.get("steps", []):
|
||
st = s.get("status", "unknown")
|
||
st_badge = badge_ok("COMPLETED") if st == "completed" else badge_warn("RUNNING") if st == "dispatched" else badge_err(st.upper())
|
||
res_snippet = s.get("result", "")
|
||
res_disp = f" → {c_dim(res_snippet[:70])}" if res_snippet else ""
|
||
print(f" • Step {s.get('step_n')}: {c_bold(s.get('job_name'))} ({s.get('agent')}) [{st_badge}]{res_disp}")
|
||
|
||
print(c_dim("\n Commands: super pipeline run <name> | super pipeline history\n"))
|
||
|
||
|
||
def cmd_pipeline_history(args):
|
||
import pipeline_engine
|
||
limit = getattr(args, "limit", 20) or 20
|
||
history = pipeline_engine.list_pipeline_history(limit=limit)
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(history, indent=2))
|
||
return
|
||
|
||
now_str = datetime.now().strftime("%H:%M:%S")
|
||
print(f"\n=== PIPELINE RUN HISTORY (Recent {len(history)}) === ({now_str} local)\n")
|
||
|
||
if not history:
|
||
print(c_dim(" (no pipeline history found)"))
|
||
return
|
||
|
||
headers = ["RUN ID", "PIPELINE", "TARGET", "STEPS", "STARTED", "COMPLETED", "STATUS"]
|
||
rows = []
|
||
|
||
for p in history:
|
||
run_id = p.get("run_id", "-")
|
||
p_name = p.get("pipeline_name", "-")
|
||
target = p.get("target", "-")
|
||
steps = p.get("steps", [])
|
||
step_summary = f"{len(steps)} step{'s' if len(steps) != 1 else ''}"
|
||
created_at = p.get("created_at")
|
||
completed_at = p.get("completed_at")
|
||
st = p.get("status", "unknown")
|
||
|
||
if st == "completed":
|
||
badge = badge_ok("COMPLETED")
|
||
elif st == "running":
|
||
badge = badge_warn("RUNNING")
|
||
else:
|
||
badge = badge_err("FAILED")
|
||
|
||
rows.append([
|
||
c_bold(run_id),
|
||
p_name,
|
||
c_yellow(target),
|
||
step_summary,
|
||
parse_relative_time(created_at) if created_at else c_dim("-"),
|
||
parse_relative_time(completed_at) if completed_at else c_dim("active"),
|
||
badge,
|
||
])
|
||
|
||
print_table(headers, rows)
|
||
print(c_dim("\n Commands: super pipeline status | super pipeline run <name>\n"))
|
||
|
||
|
||
def cmd_pipeline_stop(args):
|
||
import pipeline_engine
|
||
run_id = args.run_id.strip()
|
||
reason = getattr(args, "reason", "cancelled_by_operator") or "cancelled_by_operator"
|
||
entry = pipeline_engine.stop_pipeline(run_id, reason=reason)
|
||
if not entry:
|
||
print(c_red(f"Error: Pipeline run '{run_id}' not found."), file=sys.stderr)
|
||
sys.exit(1)
|
||
print(c_green(f"✔ Pipeline run '{entry.get('run_id')}' stopped/canceled."))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps(entry, indent=2))
|
||
|
||
|
||
def cmd_pipeline_prune(args):
|
||
import pipeline_engine
|
||
max_age = getattr(args, "max_age", 1) or 1
|
||
pruned = pipeline_engine.prune_pipelines(max_age_hours=max_age)
|
||
if pruned:
|
||
print(c_green(f"✔ Pruned {len(pruned)} stale pipeline runs (> {max_age}h): {', '.join(pruned)}"))
|
||
else:
|
||
print(c_dim(f"✔ No stale running pipelines found (> {max_age}h)."))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"pruned": pruned, "count": len(pruned)}, indent=2))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: DEPLOY (Multi-Agent Pipelines, Subagent Spawning, and Oversight)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def cmd_deploy(args):
|
||
action = getattr(args, "action", None)
|
||
|
||
if action == "subagent":
|
||
agent = args.agent
|
||
title = args.title or "subagent-task"
|
||
prompt = args.prompt
|
||
wait_val = getattr(args, "wait", 30)
|
||
wait = 30 if wait_val is None else int(wait_val)
|
||
|
||
print(c_bold(f"\n=== DEPLOYING SUBAGENT ON [{agent}] ==="))
|
||
print(f" Agent: {c_cyan(agent)}")
|
||
print(f" Title: {c_yellow(title)}")
|
||
print(f" Prompt: {c_dim(prompt[:90])}...\n")
|
||
|
||
import muse_hybrid
|
||
res, err = muse_hybrid.start_session(agent, title=title)
|
||
if err or not res:
|
||
print(c_err(f"✖ Failed to spawn subagent session: {err}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
session_id = res.get("session_id")
|
||
print(c_green(f"✔ Subagent session spawned: {session_id}"))
|
||
|
||
try:
|
||
import subagent_tracker
|
||
subagent_tracker.register_session(agent, session_id, title=title, prompt=prompt)
|
||
except Exception:
|
||
pass
|
||
|
||
if prompt:
|
||
print(f" Dispatching task prompt to subagent (waiting up to {wait}s)...")
|
||
send_res, send_err = muse_hybrid.send_message(agent, prompt, thread_id=session_id, wait=wait)
|
||
if send_err:
|
||
print(c_warn(f"Notice: {send_err}"))
|
||
else:
|
||
print(c_green("✔ Task prompt delivered."))
|
||
|
||
# Fetch fresh history to show assistant's initial work
|
||
msgs, _ = muse_hybrid.get_history(agent, thread_id=session_id, limit=5)
|
||
if msgs:
|
||
for m in msgs:
|
||
role = m.get("role")
|
||
if role == "assistant":
|
||
print(f"\n{c_bold('--- Subagent Response ---')}\n{m.get('text')}\n")
|
||
|
||
print(f" Oversee anytime with: {c_cyan(f'box thread view {agent} {session_id}')}\n")
|
||
return
|
||
|
||
# Pipeline deployment
|
||
pipe_name = getattr(args, "name", None)
|
||
if not pipe_name:
|
||
print(c_err("Error: Specify pipeline name (e.g. box deploy pipeline pipe-demo-step1)"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
args.name = pipe_name
|
||
cmd_pipeline_run(args)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domain: LOOP (Intrinsic Loop Strategy, Health, Break Taxonomy, and Control)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def cmd_loop_status(args):
|
||
agent = getattr(args, "agent", None)
|
||
status_filter = getattr(args, "status", None)
|
||
limit = getattr(args, "n", 30)
|
||
|
||
from gravity import reconstruct_loops
|
||
loops = reconstruct_loops(limit=limit, agent=agent, status_filter=status_filter)
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "loops": loops, "count": len(loops)}, indent=2))
|
||
return
|
||
|
||
prog = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "super"
|
||
if prog.endswith(".py"):
|
||
prog = "super"
|
||
|
||
print("\n" + c_bold(f"=== INTRINSIC LOOPS ({len(loops)} tracked) ===") + "\n")
|
||
headers = ["LOOP ID", "AGENT", "TARGET", "PURPOSE", "STATE", "AGE", "DEADLINE", "NUDGES", "DETAIL"]
|
||
rows = []
|
||
now = datetime.now(timezone.utc)
|
||
|
||
for l in loops:
|
||
lid = l.get("loop_id", "-")[:8]
|
||
ag = l.get("agent", "-")
|
||
tgt = l.get("target", "main")
|
||
if len(tgt) > 16:
|
||
tgt = tgt[:14] + ".."
|
||
purp = l.get("purpose", "-")
|
||
st = l.get("state", "LANDED")
|
||
if st == "FIRING":
|
||
st_badge = c_cyan("○ FIRING")
|
||
elif st == "LANDED":
|
||
st_badge = c_blue("● LANDED")
|
||
elif st == "SEEN":
|
||
st_badge = c_yellow("● SEEN")
|
||
elif st in ("ANSWERED", "CLOSED"):
|
||
st_badge = badge_ok(st)
|
||
elif st == "NUDGED":
|
||
st_badge = badge_warn("NUDGED")
|
||
elif st == "ESCALATED":
|
||
st_badge = badge_err("ESCALATED")
|
||
elif st == "BROKEN":
|
||
st_badge = badge_err("BROKEN")
|
||
else:
|
||
st_badge = badge_dim(st)
|
||
|
||
sent_rel = parse_relative_time(l.get("sent_at", ""))
|
||
dl_str = l.get("deadline", "")
|
||
if dl_str:
|
||
try:
|
||
dl_dt = datetime.fromisoformat(dl_str.replace("Z", "+00:00"))
|
||
diff_s = int((dl_dt - now).total_seconds())
|
||
if diff_s > 0:
|
||
dl_disp = c_green(f"in {diff_s // 60}m")
|
||
else:
|
||
dl_disp = c_red(f"{abs(diff_s) // 60}m ago")
|
||
except Exception:
|
||
dl_disp = dl_str[:10]
|
||
else:
|
||
dl_disp = c_dim("-")
|
||
|
||
nudges = f"{l.get('nudges_sent', 0)}/{l.get('nudges_allowed', 2)}"
|
||
detail = l.get("summary", "")[:30]
|
||
|
||
rows.append([c_bold(lid), ag, tgt, purp, st_badge, sent_rel, dl_disp, nudges, c_dim(detail)])
|
||
|
||
print_table(headers, rows)
|
||
print(c_dim(f"\n Commands: {prog} loop health | {prog} loop breaks | {prog} loop strat | {prog} loop vars | {prog} loop close <id>\n"))
|
||
|
||
|
||
def cmd_loop_health(args):
|
||
threshold = getattr(args, "threshold", None)
|
||
from gravity import get_fleet_loop_health
|
||
data = get_fleet_loop_health(threshold=threshold)
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, **data}, indent=2))
|
||
return
|
||
|
||
summary = data.get("summary", {})
|
||
t_val = summary.get("threshold", 0.5)
|
||
|
||
print("\n" + c_bold(f"=== INTRINSIC FLEET LOOP HEALTH (Threshold: {t_val:.2f}) ===") + "\n")
|
||
headers = ["AGENT", "STATUS", "HEALTH RATIO", "LANDED", "ANSWERED", "VERDICT"]
|
||
rows = []
|
||
|
||
for ag, info in sorted(data.get("agents", {}).items()):
|
||
st = info.get("status", "IDLE")
|
||
if st == "HEALTHY":
|
||
badge = badge_ok("HEALTHY")
|
||
verd = c_green("PASS")
|
||
elif st == "DEGRADED":
|
||
badge = badge_err("DEGRADED")
|
||
verd = c_red("FAIL")
|
||
else:
|
||
badge = badge_dim("IDLE")
|
||
verd = c_dim("NO TRAFFIC")
|
||
|
||
hpct = info.get("health_pct", "-")
|
||
landed = str(info.get("landed", 0))
|
||
answered = str(info.get("answered", 0))
|
||
rows.append([c_bold(ag), badge, hpct, landed, answered, verd])
|
||
|
||
print_table(headers, rows)
|
||
ov = summary.get("overall_health_pct", "-")
|
||
tot_l = summary.get("total_landed", 0)
|
||
tot_a = summary.get("total_answered", 0)
|
||
fleet_st = c_green("HEALTHY") if summary.get("healthy") else c_red("DEGRADED")
|
||
print(f"\n {c_bold('Overall Fleet Health')}: {ov} ({tot_a}/{tot_l} loops closed/answered) — {fleet_st}\n")
|
||
|
||
|
||
def cmd_loop_breaks(args):
|
||
from gravity import diagnose_breaks
|
||
breaks = diagnose_breaks()
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "breaks": breaks, "count": len(breaks)}, indent=2))
|
||
return
|
||
|
||
if not breaks:
|
||
print("\n" + c_green("✔ All intrinsic loops and tracking mechanisms healthy. No breaks detected.") + "\n")
|
||
return
|
||
|
||
print("\n" + c_bold(f"=== INTRINSIC LOOP BREAK DIAGNOSTICS ({len(breaks)} issues) ===") + "\n")
|
||
headers = ["SEVERITY", "BREAK TYPE", "COMPONENT / AGENT", "DIAGNOSIS", "REMEDIATION"]
|
||
rows = []
|
||
for b in breaks:
|
||
sev = badge_err(b.get("severity", "CRITICAL")) if b.get("severity") == "CRITICAL" else badge_warn("WARNING")
|
||
btype = b.get("type", "-")
|
||
target = b.get("component") or b.get("agent") or "-"
|
||
diag = b.get("detail", "")
|
||
rem = b.get("remedy", "")
|
||
rows.append([sev, c_bold(btype), target, diag, c_dim(rem)])
|
||
print_table(headers, rows)
|
||
print()
|
||
|
||
|
||
def cmd_loop_strat(args):
|
||
subact = getattr(args, "strat_action", None) or "show"
|
||
from modulate import get_all_strategies, set_strategy_override, reset_strategy_override, modulate, render_tags, InputType
|
||
|
||
prog = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "super"
|
||
if prog.endswith(".py"):
|
||
prog = "super"
|
||
|
||
if subact == "show":
|
||
strats = get_all_strategies()
|
||
filter_type = getattr(args, "type", None)
|
||
if filter_type:
|
||
strats = [s for s in strats if s.get("input_type") == filter_type.lower()]
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "strategies": strats}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold("=== INTRINSIC LOOP STRATEGY MATRIX (MODULATION) ===") + "\n")
|
||
headers = ["INPUT TYPE", "SUBTYPE", "AGENT", "TRACK", "PRIORITY", "TIMEOUT", "NUDGES", "ESCALATE", "SOURCE"]
|
||
rows = []
|
||
for s in strats:
|
||
itype = s.get("input_type", "")
|
||
sub = s.get("subtype") or "-"
|
||
ag = s.get("agent") or "-"
|
||
tr = str(s.get("track", "-"))
|
||
prio = s.get("priority", "normal")
|
||
if prio == "critical":
|
||
prio_disp = c_red("CRITICAL")
|
||
elif prio == "important":
|
||
prio_disp = c_yellow("IMPORTANT")
|
||
elif prio == "routine":
|
||
prio_disp = c_dim("ROUTINE")
|
||
else:
|
||
prio_disp = c_cyan(prio.upper())
|
||
|
||
timeout = f"{s.get('timeout_s', 0)}s"
|
||
nudges = str(s.get("nudges", 0))
|
||
esc = s.get("escalate") or "-"
|
||
src = c_yellow("OVERRIDE") if s.get("is_override") else c_dim("BUILTIN")
|
||
|
||
rows.append([c_bold(itype), sub, ag, tr, prio_disp, timeout, nudges, esc, src])
|
||
|
||
print_table(headers, rows)
|
||
print(c_dim(f"\n Modify: {prog} loop strat set <type> [--subtype S] [--agent A] [--priority P] [--timeout S] [--nudges N] [--escalate AGENT]\n"))
|
||
|
||
elif subact == "set":
|
||
itype = args.type
|
||
sub = getattr(args, "subtype", None)
|
||
agent = getattr(args, "agent", None)
|
||
res = set_strategy_override(
|
||
itype, sub, agent,
|
||
track=getattr(args, "track", None),
|
||
priority=getattr(args, "priority", None),
|
||
timeout_s=getattr(args, "timeout", None),
|
||
nudges=getattr(args, "nudges", None),
|
||
escalate=getattr(args, "escalate", None),
|
||
by="super"
|
||
)
|
||
target_desc = f"{itype}:{sub or '*'}:{agent or '*'}" if agent else f"{itype}:{sub or '*'}"
|
||
print(c_green(f"✔ Updated loop strategy override for '{target_desc}'."))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "strategy": res}, indent=2))
|
||
|
||
elif subact == "reset":
|
||
itype = args.type
|
||
sub = getattr(args, "subtype", None)
|
||
agent = getattr(args, "agent", None)
|
||
ok = reset_strategy_override(itype, sub, agent, by="super")
|
||
target_desc = f"{itype}:{sub or '*'}:{agent or '*'}" if agent else f"{itype}:{sub or '*'}"
|
||
if ok:
|
||
print(c_green(f"✔ Reset loop strategy override for '{target_desc}' to default."))
|
||
else:
|
||
print(c_yellow(f"No override was active for '{target_desc}'."))
|
||
|
||
elif subact == "eval":
|
||
itype_str = args.type
|
||
sub = getattr(args, "subtype", None)
|
||
agent = getattr(args, "agent", None)
|
||
actionable = getattr(args, "actionable", False)
|
||
try:
|
||
itype = InputType(itype_str.lower())
|
||
except ValueError:
|
||
itype = InputType.MANUAL
|
||
pol = modulate(itype, sub.upper() if sub else None, agent=agent, actionable=actionable)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "policy": pol.__dict__ if pol else None}, indent=2))
|
||
return
|
||
if pol:
|
||
print(f"\n{c_bold('Resolved Policy:')}")
|
||
print(f" Input Type : {pol.input_type.value}")
|
||
print(f" Subtype : {pol.subtype or '-'}")
|
||
print(f" Agent Scope: {agent or '-'}")
|
||
print(f" Priority : {pol.priority.value}")
|
||
print(f" Timeout : {pol.timeout_s}s")
|
||
print(f" Max Nudges : {pol.nudges}")
|
||
print(f" Escalate To: {pol.escalate or '-'}")
|
||
print(f" Rendered Tags: {c_cyan(render_tags(pol))}\n")
|
||
else:
|
||
print(f"\n{c_dim('Policy: Non-tracked input (no follow-up record created).')}\n")
|
||
|
||
|
||
def cmd_loop_vars(args):
|
||
subact = getattr(args, "vars_action", None) or "list"
|
||
from variables import Variables
|
||
v = Variables()
|
||
|
||
prog = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "super"
|
||
if prog.endswith(".py"):
|
||
prog = "super"
|
||
|
||
if subact == "list":
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "variables": v.all(), "schemas": {n: v.schema(n) for n in v.names()}}, indent=2))
|
||
return
|
||
|
||
names = v.names()
|
||
print("\n" + c_bold(f"=== RUNTIME CONTROL VARIABLES ({len(names)} registered) ===") + "\n")
|
||
headers = ["VARIABLE NAME", "VALUE", "DEFAULT", "UNIT", "RANGE", "DESCRIPTION"]
|
||
rows = []
|
||
|
||
for name in names:
|
||
val = v.get(name)
|
||
spec = v.schema(name)
|
||
d_val = spec.get("default")
|
||
unit = spec.get("unit", "-")
|
||
min_v = spec.get("min")
|
||
max_v = spec.get("max")
|
||
rng = f"{min_v}..{max_v}" if min_v is not None and max_v is not None else "-"
|
||
desc = spec.get("description", "")[:40]
|
||
|
||
val_str = str(val)
|
||
if val != d_val:
|
||
val_disp = c_cyan(f"{val_str} *")
|
||
else:
|
||
val_disp = val_str
|
||
|
||
rows.append([c_bold(name), val_disp, str(d_val), unit, rng, c_dim(desc)])
|
||
|
||
print_table(headers, rows)
|
||
print(c_dim(f"\n Adjust: {prog} loop vars set <name> <value> | Reset: {prog} loop vars reset <name> (* = modified)\n"))
|
||
|
||
elif subact == "get":
|
||
name = args.name
|
||
try:
|
||
val = v.get(name)
|
||
spec = v.schema(name)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "name": name, "value": val, "schema": spec}, indent=2))
|
||
return
|
||
print(f"\n{c_bold('Variable: ' + name)}")
|
||
print(f" Current Value : {c_cyan(str(val))}")
|
||
print(f" Default Value : {spec.get('default')}")
|
||
print(f" Type : {spec.get('type')}")
|
||
print(f" Unit : {spec.get('unit', '-')}")
|
||
if "min" in spec and "max" in spec:
|
||
print(f" Allowed Range : {spec.get('min')} .. {spec.get('max')}")
|
||
print(f" Description : {spec.get('description')}\n")
|
||
except Exception as e:
|
||
print(c_red(f"Error: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
elif subact == "set":
|
||
name = args.name
|
||
val_raw = args.value
|
||
try:
|
||
spec = v.schema(name)
|
||
vtype = spec.get("type", "str")
|
||
if vtype == "int":
|
||
val = int(val_raw)
|
||
elif vtype == "float":
|
||
val = float(val_raw)
|
||
elif vtype == "bool":
|
||
val = val_raw.lower() in ("true", "1", "yes")
|
||
else:
|
||
val = val_raw
|
||
new_val = v.set(name, val, by="super")
|
||
print(c_green(f"✔ Variable '{name}' updated to {new_val}."))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "name": name, "value": new_val}, indent=2))
|
||
except Exception as e:
|
||
print(c_red(f"Error setting variable: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
elif subact == "reset":
|
||
name = args.name
|
||
try:
|
||
def_val = v.reset(name, by="super")
|
||
print(c_green(f"✔ Variable '{name}' reset to default ({def_val})."))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "name": name, "value": def_val}, indent=2))
|
||
except Exception as e:
|
||
print(c_red(f"Error resetting variable: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
elif subact == "history":
|
||
name = getattr(args, "name", None)
|
||
limit = getattr(args, "limit", 20)
|
||
entries = v.history(name=name, limit=limit)
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "history": entries, "count": len(entries)}, indent=2))
|
||
return
|
||
if not entries:
|
||
print("\n" + c_dim(" (no variable change history found)") + "\n")
|
||
return
|
||
print("\n" + c_bold(f"=== VARIABLE MODIFICATION AUDIT HISTORY ({len(entries)} entries) ===") + "\n")
|
||
headers = ["TIMESTAMP", "VARIABLE", "ACTION", "OLD VALUE", "NEW VALUE", "CHANGED BY"]
|
||
rows = []
|
||
for e in entries:
|
||
act = e.get("action", "")
|
||
if act == "set":
|
||
act_badge = c_cyan("SET")
|
||
elif act == "reset":
|
||
act_badge = c_yellow("RESET")
|
||
elif act == "rollback":
|
||
act_badge = c_magenta("ROLLBACK")
|
||
else:
|
||
act_badge = act
|
||
rows.append([
|
||
e.get("ts", "")[:19].replace("T", " "),
|
||
c_bold(e.get("name", "")),
|
||
act_badge,
|
||
str(e.get("old_value", "-")),
|
||
c_bold(str(e.get("new_value", "-"))),
|
||
e.get("by", "-")
|
||
])
|
||
print_table(headers, rows)
|
||
print(c_dim(f"\n Rollback: {prog} loop vars rollback <name> [--revision REV]\n"))
|
||
|
||
elif subact == "rollback":
|
||
name = args.name
|
||
rev = getattr(args, "revision", None)
|
||
try:
|
||
new_val = v.rollback(name, revision=rev, by="super")
|
||
print(c_green(f"✔ Variable '{name}' rolled back to {new_val}."))
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, "name": name, "value": new_val}, indent=2))
|
||
except Exception as e:
|
||
print(c_red(f"Error rolling back variable: {e}"), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
def cmd_loop_close(args):
|
||
dm_id = args.dm_id.strip()
|
||
cmd = [sys.executable, str(BIN_DIR / "box-ctl.py"), "loop-resolve", dm_id]
|
||
if getattr(args, "note", None):
|
||
cmd.append(args.note)
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
try:
|
||
data = json.loads(res.stdout)
|
||
if data.get("ok"):
|
||
print(c_green(f"✔ Loop {dm_id} marked resolved/closed."))
|
||
else:
|
||
print(c_red(f"Error closing loop: {data.get('error')}"), file=sys.stderr)
|
||
except Exception:
|
||
print(res.stdout or res.stderr)
|
||
|
||
|
||
def cmd_loop_nudge(args):
|
||
dm_id = args.dm_id.strip()
|
||
if not FOLLOWUPS_FILE.exists():
|
||
print(c_red("followups.json not found."), file=sys.stderr)
|
||
return
|
||
try:
|
||
with open(FOLLOWUPS_FILE, "r") as f:
|
||
fdata = json.load(f)
|
||
rec = fdata.get(dm_id)
|
||
if not rec:
|
||
print(c_red(f"No pending follow-up found for ID '{dm_id}'."), file=sys.stderr)
|
||
return
|
||
rec["deadline"] = datetime.now(timezone.utc).isoformat()
|
||
with open(FOLLOWUPS_FILE, "w") as f:
|
||
json.dump(fdata, f, indent=2)
|
||
print(c_green(f"✔ Armed immediate nudge for loop {dm_id}. Running sweeper..."))
|
||
subprocess.run([sys.executable, str(FOLLOWUP_SWEEPER_PY), "--once"])
|
||
except Exception as e:
|
||
print(c_red(f"Error nudging loop: {e}"), file=sys.stderr)
|
||
|
||
|
||
def cmd_loop_sweep(args):
|
||
cmd_followup_sweep(args)
|
||
|
||
|
||
def cmd_loop_harvest(args):
|
||
cmd_harvest_run(args)
|
||
|
||
|
||
def cmd_loop_remediate(args):
|
||
dry_run = getattr(args, "dry_run", False)
|
||
from gravity import remediate_breaks
|
||
res = remediate_breaks(dry_run=dry_run)
|
||
|
||
if getattr(args, "json", False):
|
||
print(json.dumps({"ok": True, **res}, indent=2))
|
||
return
|
||
|
||
mode_str = c_yellow("DRY RUN (Simulated)") if dry_run else c_green("ACTIVE RECOVERY")
|
||
print("\n" + c_bold(f"=== PROGRESSIVE INTRINSIC LOOP REMEDIATION [{mode_str}] ===") + "\n")
|
||
|
||
remediated = res.get("remediated", [])
|
||
escalated = res.get("escalated", [])
|
||
|
||
if remediated:
|
||
print(c_bold(f"Auto-Remediated Soft Breakages ({len(remediated)}):"))
|
||
headers = ["ACTION", "LOOP ID", "AGENT", "REMEDIATION DETAIL"]
|
||
rows = []
|
||
for r in remediated:
|
||
act_badge = c_green("✔ " + r.get("action", ""))
|
||
rows.append([act_badge, c_bold(r.get("loop_id", "-")[:8]), r.get("agent", "-"), r.get("detail", "")])
|
||
print_table(headers, rows)
|
||
print()
|
||
else:
|
||
print(c_dim(" ✔ No soft breakages require remediation at this time.\n"))
|
||
|
||
if escalated:
|
||
print(c_red(c_bold(f"⚠ Escalated Hard Failures ({len(escalated)} - Manual Intervention Required):")))
|
||
headers = ["SEVERITY", "BREAK TYPE", "TARGET", "ISSUE DETAIL", "REMEDY"]
|
||
rows = []
|
||
for esc in escalated:
|
||
sev = badge_err("CRITICAL") if esc.get("severity") == "CRITICAL" else badge_warn("WARNING")
|
||
rows.append([sev, c_bold(esc.get("type", "-")), esc.get("agent") or esc.get("component") or "-", esc.get("detail", ""), c_dim(esc.get("remedy", ""))])
|
||
print_table(headers, rows)
|
||
print()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CLI Argument Parser Setup
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def cmd_ssh_mint(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-mint", args.name]
|
||
if args.force:
|
||
cmd.append("--force")
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print(c_green(f"✓ Minted ed25519 SSH keypair for '{args.name}'"))
|
||
print(f" Private key: {d.get('private_key')}")
|
||
print(f" Public key: {d.get('public_key')}")
|
||
print(f" Public key text: {d.get('public_key_content')}")
|
||
print(c_dim(" Registered into: ") + ", ".join(d.get("registered_in", [])))
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')} ({d.get('code')})"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_ssh_list(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-list"]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
keys = d.get("keys", [])
|
||
print("\n" + c_bold(f"=== MANAGED SSH KEYS ({len(keys)}) ===") + "\n")
|
||
headers = ["NAME", "PRIVATE KEY", "PUB STATUS", "PUBLIC KEY PREVIEW"]
|
||
rows = []
|
||
for k in keys:
|
||
pub_txt = k.get("public_key") or ""
|
||
preview = (pub_txt[:32] + "..." + pub_txt[-16:]) if len(pub_txt) > 48 else pub_txt
|
||
rows.append([c_cyan(k.get("name")), k.get("private_key"), c_green("✓ present") if k.get("has_public") else c_red("missing"), preview])
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" Mint new key: box ssh mint <name> [--force]") + "\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_ssh_show(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-show", args.name]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print(c_bold(f"=== SSH KEY DETAILS: {args.name} ==="))
|
||
print(f" Private key: {d.get('private_key')}")
|
||
print(f" Public key: {d.get('public_key')}")
|
||
print(f" Fingerprint: {d.get('fingerprint')}")
|
||
print(f"\n Public Key:\n {c_cyan(d.get('public_key_content'))}\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_ssh_ports(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-ports"]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
tunnels = d.get("tunnels", {})
|
||
jump = d.get("jump_host", "34.139.37.135")
|
||
print("\n" + c_bold(f"=== CONTAINER SSH & REVERSE TUNNEL REGISTRY (JUMP: {jump}) ===") + "\n")
|
||
headers = ["AGENT", "SSH PORT", "TERM PORT", "USER", "DIAL-IN COMMAND"]
|
||
rows = []
|
||
for name, info in tunnels.items():
|
||
port = info.get("port")
|
||
tport = info.get("terminal")
|
||
u = info.get("user", "hatch")
|
||
dial = f"ssh -J super@{jump} -p {port} {u}@localhost"
|
||
rows.append([c_cyan(name), str(port), str(tport), u, c_yellow(dial)])
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" To execute remote cmd: ssh -J super@<jump> -p <port> hatch@localhost '<cmd>'") + "\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_ssh_info(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-info", args.name]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
name = d.get("account", args.name)
|
||
port = d.get("port")
|
||
u = d.get("container_user", "hatch")
|
||
jump = d.get("jump_host", "34.139.37.135")
|
||
print("\n" + c_bold(f"=== SSH DIAL-IN FOR AGENT: {name} ===") + "\n")
|
||
print(f" Container User: {c_cyan(u)}")
|
||
print(f" Reverse SSH Port:{c_yellow(str(port))}")
|
||
print(f" GCP Jump Host: {jump}")
|
||
print(f" From VM directly: {c_green(d.get('direct_from_vm', ''))}")
|
||
print(f" Via Jump Host: {c_green(d.get('jump_command', ''))}")
|
||
print(f"\n Modify .md via SSH:")
|
||
print(f" {c_dim(d.get('cat_example', ''))}\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_audit(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-audit"] + (args.accounts or [])
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
agents = d.get("agents", {})
|
||
print("\n" + c_bold(f"=== MUSE AGENT .MD & OPERATIONAL DRIVE AUDIT ({len(agents)} AGENTS) ===") + "\n")
|
||
for acct, info in agents.items():
|
||
if not info.get("connected"):
|
||
print(c_red(f"[{acct.upper()}] ✗ Connection failed: {info.get('error')}"))
|
||
continue
|
||
score = info.get("drive_score", 0)
|
||
score_str = c_green(f"{score}/100 [HIGH DRIVE]") if score >= 75 else (c_yellow(f"{score}/100 [PARTIAL]") if score >= 50 else c_red(f"{score}/100 [LOW/STALE]"))
|
||
vm = info.get("vm_id", "unknown")
|
||
print(f"[{c_bold(acct.upper())}] Drive: {score_str} VM: {c_dim(vm)}")
|
||
headers = ["FILE", "STATUS", "SIZE", "MODIFIED"]
|
||
rows = []
|
||
for fname, finfo in info.get("files", {}).items():
|
||
exists = finfo.get("exists")
|
||
st = c_green("✓ present") if exists else c_red("✗ missing")
|
||
sz = f"{finfo.get('size', 0)} B"
|
||
mod = (finfo.get("modified") or "-")[:19]
|
||
rows.append([fname, st, sz, mod])
|
||
print_table(headers, rows)
|
||
issues = info.get("issues", [])
|
||
if issues:
|
||
print(c_dim(" Issues / Gaps:"))
|
||
for iss in issues:
|
||
print(c_yellow(f" • {iss}"))
|
||
print()
|
||
print(c_dim(" Fix agent drive: box md inject-drive <agent> | box md sync-all") + "\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_list(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-list", args.account, args.path]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
entries = d.get("entries", [])
|
||
print("\n" + c_bold(f"=== FILES IN {args.account}:{args.path or '/'} ({len(entries)}) ===") + "\n")
|
||
headers = ["NAME", "TYPE", "SIZE", "MODIFIED"]
|
||
rows = []
|
||
for e in entries:
|
||
t = e.get("type", "file")
|
||
name_colored = c_cyan(e.get("name")) if t == "directory" else e.get("name")
|
||
rows.append([name_colored, t, str(e.get("size", "-")), (e.get("modifiedAt") or "-")[:19]])
|
||
print_table(headers, rows)
|
||
print()
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_read(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-read", args.account, args.filename]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
text = d.get("text", "")
|
||
print(f"\n{c_bold('--- ' + args.account + ':' + args.filename + ' (' + str(len(text)) + ' chars) ---')}\n")
|
||
print(text)
|
||
print()
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_write(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-write", args.account, args.filename]
|
||
if args.file:
|
||
cmd.extend(["--file", args.file])
|
||
elif args.content:
|
||
cmd.extend(["--content", args.content])
|
||
else:
|
||
print(c_red("Error: specify --content or --file"))
|
||
return
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print(c_green(f"✓ Wrote {d.get('bytes_written')} bytes to {args.account}:{args.filename} via Hatch"))
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_diff(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-diff", args.account, args.filename]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
if d.get("identical"):
|
||
print(c_green(f"✓ {args.account}:{args.filename} matches local shared/operators/{args.filename} exactly."))
|
||
else:
|
||
print(f"\n{c_bold('=== DIFF: ' + args.account + ':' + args.filename + ' vs shared/operators/' + args.filename + ' ===')}\n")
|
||
diff = d.get("diff", "")
|
||
for line in diff.splitlines():
|
||
if line.startswith("+"):
|
||
print(c_green(line))
|
||
elif line.startswith("-"):
|
||
print(c_red(line))
|
||
elif line.startswith("@@"):
|
||
print(c_cyan(line))
|
||
else:
|
||
print(line)
|
||
print()
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_inject_drive(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-inject-drive", args.account]
|
||
if args.force:
|
||
cmd.append("--force")
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print("\n" + c_bold(f"=== INJECTED OPERATIONAL DRIVE INTO AGENT: {args.account.upper()} ===") + "\n")
|
||
for u in d.get("updates", []):
|
||
print(c_green(f" ✓ Injected {u.get('file'):26} ({u.get('bytes')} bytes)"))
|
||
print(f"\n{c_green('✓ Successfully activated SOUL.md, PROACTIVE_PREFERENCES.md, and HEARTBEAT.md!')}\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_sync_all(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-sync-all"]
|
||
if args.force:
|
||
cmd.append("--force")
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print("\n" + c_bold("=== FLEET-WIDE OPERATIONAL DRIVE INJECTION ===") + "\n")
|
||
results = d.get("results", {})
|
||
for acct, r in results.items():
|
||
if r.get("ok"):
|
||
files_str = ", ".join(u.get("file") for u in r.get("updates", []))
|
||
print(c_green(f" ✓ {acct.upper():6} drive injected ({files_str})"))
|
||
else:
|
||
print(c_red(f" ✗ {acct.upper():6} failed: {r.get('error')}"))
|
||
print(f"\n{c_green('✓ Fleet synchronization complete.')}\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_amend(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md", "amend", args.filename]
|
||
if args.file:
|
||
cmd.extend(["--file", args.file])
|
||
elif args.content:
|
||
cmd.extend(["--content", args.content])
|
||
if args.author:
|
||
cmd.extend(["--author", args.author])
|
||
if args.reason:
|
||
cmd.extend(["--reason", args.reason])
|
||
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
commit_info = f" (git commit: {d.get('commit')})" if d.get("commit") else ""
|
||
print(c_green(f"\n✓ Successfully amended shared/operators/{args.filename}{commit_info}"))
|
||
print(f" Author: {c_cyan(d.get('author'))}")
|
||
print(f" Bytes: {d.get('bytes_written')}\n")
|
||
print(c_dim(" Broadcast to fleet with: box md sync-all\n"))
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_append(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md", "append", args.filename, args.text]
|
||
if args.author:
|
||
cmd.extend(["--author", args.author])
|
||
if args.section:
|
||
cmd.extend(["--section", args.section])
|
||
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
commit_info = f" (git commit: {d.get('commit')})" if d.get("commit") else ""
|
||
print(c_green(f"\n✓ Successfully appended note to shared/operators/{args.filename}{commit_info}"))
|
||
print(f" Author: {c_cyan(d.get('author'))}\n")
|
||
print(c_dim(" Broadcast to fleet with: box md sync-all\n"))
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_pull(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md", "pull", args.account, args.filename]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print(c_green(f"✓ Pulled canonical shared/operators/{args.filename} into {args.account} container ({d.get('bytes_written')} B)"))
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_md_watchdog(args):
|
||
watchdog_py = BIN_DIR / "agent-drive-watchdog.py"
|
||
if args.sub_action == "status":
|
||
cmd = ["python3", str(watchdog_py), "--status"]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
print("\n" + c_bold("=== NETVM FLEET AGENT DRIVE WATCHDOG STATUS ===") + "\n")
|
||
print(f" Last Run: {c_cyan(d.get('last_run', 'never'))}")
|
||
st = d.get("agent_status", {})
|
||
headers = ["AGENT", "DRIVE SCORE", "STATUS", "LAST CHECKED", "ISSUES"]
|
||
rows = []
|
||
for acct, info in st.items():
|
||
score = info.get("score", 0)
|
||
score_str = c_green(f"{score}/100") if score == 100 else c_red(f"{score}/100")
|
||
stat = c_green("HIGH DRIVE") if info.get("status") == "HIGH_DRIVE" else c_red(info.get("status", "DEGRADED"))
|
||
dt = (info.get("last_checked") or "-")[:19]
|
||
iss = ", ".join(info.get("issues", [])) or c_dim("none")
|
||
rows.append([c_bold(acct.upper()), score_str, stat, dt, iss])
|
||
print_table(headers, rows)
|
||
|
||
# Show timer status
|
||
tr = subprocess.run(["systemctl", "--user", "is-active", "agent-drive-watchdog.timer"], capture_output=True, text=True)
|
||
t_stat = tr.stdout.strip()
|
||
t_badge = c_green("ACTIVE (every 10m)") if t_stat == "active" else c_yellow(t_stat)
|
||
print(f"\n Systemd Timer: {t_badge}\n")
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
elif args.sub_action == "run":
|
||
cmd = ["python3", str(watchdog_py), "--once"]
|
||
if getattr(args, "no_heal", False):
|
||
cmd.append("--no-heal")
|
||
print("\n" + c_bold("=== RUNNING FLEET AGENT DRIVE WATCHDOG CYCLE ===") + "\n")
|
||
subprocess.run(cmd)
|
||
print()
|
||
else:
|
||
print("Usage: box md watchdog {status|run}")
|
||
|
||
|
||
def cmd_swarm_list(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-list"]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
swarms = d.get("swarms", [])
|
||
print("\n" + c_bold(f"=== SWARMS ({len(swarms)}) ===") + "\n")
|
||
headers = ["ID", "STATUS", "LABEL", "TOTAL", "DONE", "RUNNING", "FAILED", "CREATED"]
|
||
rows = []
|
||
for sw in swarms:
|
||
st = sw.get("status")
|
||
st_color = c_green(st) if st == "completed" else (c_yellow(st) if st in ("running", "partial") else c_dim(st))
|
||
rows.append([
|
||
c_cyan(sw.get("swarm_id", "")[:18]),
|
||
st_color,
|
||
(sw.get("label") or "-")[:20],
|
||
str(sw.get("count", 0)),
|
||
str(sw.get("done", 0)),
|
||
str(sw.get("running", 0)),
|
||
str(sw.get("failed", 0)),
|
||
(sw.get("created_ts") or "")[:16]
|
||
])
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" Spawn swarm: box swarm spawn <count> <task> [--label L]") + "\n")
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_swarm_status(args):
|
||
sid = args.swarm_id
|
||
if not sid.startswith("sw-"):
|
||
sid = "sw-" + sid
|
||
# Support prefix matching if exact ID is truncated (e.g. 18 chars)
|
||
sw_file = Path(__file__).resolve().parent.parent / "swarms.json"
|
||
if sw_file.exists():
|
||
try:
|
||
import json
|
||
data = json.loads(sw_file.read_text(encoding="utf-8"))
|
||
if sid not in data:
|
||
matches = [k for k in data.keys() if k.startswith(sid)]
|
||
if len(matches) == 1:
|
||
sid = matches[0]
|
||
except Exception:
|
||
pass
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-status", sid]
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print(c_bold(f"\n=== SWARM STATUS: {args.swarm_id} ==="))
|
||
sw = d.get("swarm", {})
|
||
print(f" Status: {sw.get('status')}")
|
||
print(f" Counts: {d.get('counts')}")
|
||
slots = sw.get("slots", [])
|
||
print(f"\n Slots ({len(slots)}):")
|
||
for s in slots:
|
||
res_txt = s.get("result")
|
||
res_prev = (str(res_txt)[:50] + "...") if res_txt and len(str(res_txt)) > 50 else str(res_txt)
|
||
print(f" Slot {s.get('slot')}: [{s.get('status')}] agent={s.get('agent_id')} subagent={s.get('subagent_session_id')} res={res_prev}")
|
||
print()
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')}"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_swarm_spawn(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-spawn", str(args.count), args.task]
|
||
if args.label:
|
||
cmd.extend(["--label", args.label])
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print(c_green(f"✓ Spawned swarm: {d.get('swarm_id')} ({d.get('count')} slots)"))
|
||
else:
|
||
print(c_red(f"✗ Failed: {d.get('error')} ({d.get('code')})"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_swarm_prune(args):
|
||
cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-prune", "--stale-hours", str(args.stale_hours)]
|
||
if args.confirm:
|
||
cmd.append("--confirm")
|
||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||
if args.json:
|
||
sys.stdout.write(res.stdout)
|
||
return
|
||
try:
|
||
d = json.loads(res.stdout)
|
||
if d.get("ok"):
|
||
print(c_green(f"✓ Pruned {d.get('archived_count')} swarms older than {d.get('stale_hours')}h to 'archived'"))
|
||
else:
|
||
print(c_yellow(f"! {d.get('error')} (use --confirm)"))
|
||
except Exception:
|
||
sys.stdout.write(res.stdout)
|
||
|
||
|
||
def cmd_passkey_info(args):
|
||
"""Display operator passkey reference and agent approval architecture, or fetch from VM."""
|
||
action = getattr(args, "action", "show") or "show"
|
||
is_json = getattr(args, "json", False)
|
||
|
||
if action in ("fetch", "get"):
|
||
vm_host = "34.139.37.135"
|
||
vm_paths = ["/srv/box/passkey.txt", "/etc/netvm/passkey.txt"]
|
||
cmd = [
|
||
"ssh",
|
||
"-o", "BatchMode=yes",
|
||
"-o", "ConnectTimeout=3",
|
||
f"super@{vm_host}",
|
||
f"cat {vm_paths[0]} 2>/dev/null || cat {vm_paths[1]} 2>/dev/null"
|
||
]
|
||
key_content = ""
|
||
fetch_err = None
|
||
try:
|
||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||
if res.returncode == 0 and res.stdout.strip():
|
||
key_content = res.stdout.strip()
|
||
else:
|
||
fetch_err = res.stderr.strip() or "Empty output or authentication required"
|
||
except subprocess.TimeoutExpired:
|
||
fetch_err = "SSH connection timed out"
|
||
except Exception as e:
|
||
fetch_err = str(e)
|
||
|
||
if key_content:
|
||
if is_json:
|
||
print(json.dumps({
|
||
"ok": True,
|
||
"fetched": True,
|
||
"passkey": key_content,
|
||
"vm_host": vm_host,
|
||
"path": vm_paths[0],
|
||
"operator_pin": "3128",
|
||
"surface": "https://box.muse-dev.online/"
|
||
}, indent=2))
|
||
return
|
||
print("\n" + c_bold("=== OPERATOR PASSKEY (FETCHED FROM VM) ===") + "\n")
|
||
print(f" {c_bold('Passkey Content')}: {c_green(key_content)}")
|
||
print(f" {c_bold('Source Host')} : {c_cyan(vm_host)} ({vm_paths[0]})")
|
||
print(f" {c_bold('Web Surface')} : https://box.muse-dev.online/ (PIN: 3128)\n")
|
||
return
|
||
else:
|
||
if is_json:
|
||
print(json.dumps({
|
||
"ok": False,
|
||
"fetched": False,
|
||
"vm_host": vm_host,
|
||
"path": vm_paths[0],
|
||
"fallback_path": vm_paths[1],
|
||
"operator_pin": "3128",
|
||
"surface": "https://box.muse-dev.online/",
|
||
"error": f"Could not fetch passkey directly: {fetch_err}",
|
||
"note": "Passkey is stored in a single .txt file on the VM (34.139.37.135) only, NOT on BL (100.123.153.75).",
|
||
"operator_command": f"ssh super@{vm_host} 'cat {vm_paths[0]}'",
|
||
"agent_approval_command": "box approvals request-key <node> --reason '<reason>'"
|
||
}, indent=2))
|
||
return
|
||
print("\n" + c_bold("=== VM PASSKEY FETCH PROBE ===") + "\n")
|
||
print(f" {c_bold('Target VM Host')} : {c_cyan(vm_host)}")
|
||
print(f" {c_bold('Target Paths')} : {vm_paths[0]} (fallback: {vm_paths[1]})")
|
||
print(f" {c_bold('Probe Result')} : {c_yellow('Direct SSH access unavailable (' + (fetch_err or 'auth required') + ')')}\n")
|
||
print(c_bold(" Location Reality (VM vs BL):"))
|
||
print(f" • {c_yellow('VM (' + vm_host + ')')} : Key material lives in a {c_bold('single text file (.txt)')} on the VM.")
|
||
print(f" • {c_red('BL (100.123.153.75)')} : {c_bold('NO passkey / secret material exists on the dedicated BL.')}\n")
|
||
print(f" {c_bold('Console PIN')} : {c_green('3128')} (sets session cookie: {c_dim('ops_session')} at {c_cyan('https://box.muse-dev.online/')})\n")
|
||
print(c_bold(" Operator Access:"))
|
||
print(f" ssh super@{vm_host} 'cat {vm_paths[0]}'\n")
|
||
print(c_bold(" Agent UX / Approval Request:"))
|
||
print(f" Agents must not hunt BL. If passkey authorization is needed, request it via:")
|
||
print(f" {c_cyan('box approvals request-key <node> --reason \"<reason>\"')}\n")
|
||
return
|
||
|
||
if is_json:
|
||
print(json.dumps({
|
||
"ok": True,
|
||
"surface": "https://box.muse-dev.online/",
|
||
"operator_pin": "3128",
|
||
"session_cookie": "ops_session",
|
||
"key_location": {
|
||
"host": "Google Cloud VM (34.139.37.135)",
|
||
"canonical_path": "/srv/box/passkey.txt",
|
||
"fallback_path": "/etc/netvm/passkey.txt",
|
||
"note": "Stored in a single text file (.txt) on the VM — NOT on the dedicated BL compute node (100.123.153.75)",
|
||
"credstore_path": "/etc/netvm/meta-credentials/store.age",
|
||
"credstore_key": "/etc/netvm/meta-credentials/.age-key"
|
||
},
|
||
"operator_fetch_cmd": "ssh super@34.139.37.135 'cat /srv/box/passkey.txt'",
|
||
"agent_approval_protocol": {
|
||
"rule": "Agents do not hold administrative passkeys or credentials directly. Secrets never reside on bl.",
|
||
"request_flow": "1. Agent signals APPROVAL_REQUEST: key=<reason> or runs 'box approvals request-key <node>'.\n2. Operator verifies request.\n3. Operator retrieves key/PIN from VM single text file (.txt) or submits OTP.\n4. Operator approves via 'box approvals allow <node>'.",
|
||
"request_command": "box approvals request-key <node> --reason '<reason>'",
|
||
"sidechats": ["646 tasks", "pip tasks", "#jobs", "heartbeat"]
|
||
}
|
||
}, indent=2))
|
||
return
|
||
|
||
print("\n" + c_bold("=== OPERATOR PASSKEY & KEY MATERIAL ARCHITECTURE ===") + "\n")
|
||
print(f" {c_bold('Box Front-Door Console')}: {c_cyan('https://box.muse-dev.online/')}")
|
||
print(f" {c_bold('Operator PIN / Passkey')}: {c_green('3128')} (sets session cookie: {c_dim('ops_session')})\n")
|
||
|
||
print(c_bold(" Location Reality (VM vs BL):"))
|
||
print(f" • {c_yellow('VM (34.139.37.135)')} : Key material lives in a {c_bold('single text file (.txt)')} on the VM.")
|
||
print(f" - Canonical path: {c_cyan('/srv/box/passkey.txt')}")
|
||
print(f" - Fallback path : {c_cyan('/etc/netvm/passkey.txt')}")
|
||
print(f" - Fetch command : {c_dim('box passkey fetch')}")
|
||
print(f" • {c_red('BL (100.123.153.75)')} : {c_bold('NO passkey / secret material exists on the dedicated BL.')}")
|
||
print(f" • Credstore on VM : /etc/netvm/meta-credentials/store.age (age-encrypted, root 600)")
|
||
print(f" • Age Key on VM : /etc/netvm/meta-credentials/.age-key\n")
|
||
|
||
print(c_bold(" Agent UX & Operator Approval Flow:"))
|
||
print(f" 1. {c_cyan('Never hunt on bl')} : Agents must never attempt to grep or locate passkeys on bl.")
|
||
print(f" 2. {c_cyan('Signal Approval')} : If an agent requires key access or operator privilege:")
|
||
print(f" - Run: {c_yellow('box approvals request-key <node> --reason \"<reason>\"')}")
|
||
print(f" - Or emit {c_yellow('APPROVAL_NEEDED: <details>')} in task sidechat")
|
||
print(f" - Or exit with code {c_yellow('2')} (per INFRA.md convention)")
|
||
print(f" 3. {c_cyan('Operator Action')} : Operator consults the single .txt file on the VM to authenticate")
|
||
print(f" and approves via {c_dim('box approvals allow <node>')}.")
|
||
print(f" 4. {c_cyan('Target Sidechats')} : Use dedicated sidechats ({c_dim('646 tasks, pip tasks, #jobs, heartbeat')}).\n")
|
||
|
||
|
||
|
||
def _lookup_unreads(args):
|
||
print("\n" + c_bold("=== FLEET UNREAD / ACTIVITY STATUS ===") + "\n")
|
||
headers = ["NODE", "UNREAD COUNT", "ACTIVE PAGE / TITLE", "LAST SEEN / THREAD"]
|
||
rows = []
|
||
data = collect_fleet_data()
|
||
for item in data:
|
||
n = item["node"]
|
||
title = item.get("title", "")
|
||
unread = "0"
|
||
if "(1)" in title or "(2)" in title or "(3)" in title:
|
||
m = re.search(r"\((\d+)\)", title)
|
||
unread = c_yellow(m.group(1)) if m else c_yellow("1+")
|
||
elif item.get("approval_pending"):
|
||
unread = c_yellow("APPROVAL")
|
||
|
||
thread_info = "-"
|
||
if "thread/" in item.get("url", ""):
|
||
thread_info = item["url"].split("thread/")[-1][:12]
|
||
elif item.get("url") == "https://muse.ai/":
|
||
thread_info = "home"
|
||
|
||
rows.append([c_bold(n), unread, title[:45], thread_info])
|
||
print_table(headers, rows)
|
||
print("\n" + c_dim(" To view unread messages: box thread view <node> <thread>") + "\n")
|
||
|
||
|
||
def cmd_lookup(args):
|
||
"""Seamless unified lookup across nodes, threads, unreads, approvals, and keys."""
|
||
lookup_args = getattr(args, "lookup_args", []) or []
|
||
if "--json" in lookup_args:
|
||
setattr(args, "json", True)
|
||
sub = getattr(args, "target", None)
|
||
if not sub or sub in ("all", "summary"):
|
||
print("\n" + c_bold("=== SEAMLESS FLEET LOOKUP SUMMARY ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n"))
|
||
# 1. Fleet status
|
||
cmd_fleet_status(args)
|
||
# 2. Approvals summary
|
||
try:
|
||
import approvals
|
||
app_list = approvals.check_fleet_approvals(VALID_NODES)
|
||
pending = [a for a in app_list if a.get("status") == "PENDING"]
|
||
if pending:
|
||
print(c_yellow(f" ⚠ {len(pending)} pending approval(s): {', '.join(a['node'] for a in pending)} (run: box approvals)"))
|
||
else:
|
||
print(c_green(" ✔ Approval Queues: All clean"))
|
||
except Exception:
|
||
pass
|
||
# 3. Key note
|
||
print(c_dim(" ℹ Passkey: Stored in single .txt on VM (34.139.37.135), not bl. (run: box passkey)"))
|
||
print()
|
||
return
|
||
|
||
if sub in ("key", "passkey", "auth"):
|
||
cmd_passkey_info(args)
|
||
elif sub in ("fleet", "nodes"):
|
||
cmd_fleet_status(args)
|
||
elif sub in ("threads", "sidechats"):
|
||
cmd_thread_list(args)
|
||
elif sub in ("approvals", "approval"):
|
||
cmd_approvals(args)
|
||
elif sub in ("docs", "doc", "surfaces", "sentence", "regex", "parse"):
|
||
extra = getattr(args, "lookup_args", []) or []
|
||
sub_args = [sub] if sub not in ("docs", "doc") else []
|
||
cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + sub_args + extra
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
else:
|
||
print(f"Unknown lookup target '{sub}'. Choose from: fleet, threads, unread, approvals, key, docs", file=sys.stderr)
|
||
|
||
|
||
def cmd_docs_dispatch(args):
|
||
"""Bridge 'box docs' and 'super docs' directly to bin/docs-lookup.py."""
|
||
d_args = getattr(args, "docs_args", []) or []
|
||
cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + d_args
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
|
||
def cmd_tmux_dispatch(args):
|
||
"""Bridge 'box tmux' commands directly to bin/muse-tmux.py."""
|
||
tmux_bin = str(BIN_DIR / "muse-tmux.py")
|
||
t_args = getattr(args, "tmux_args", []) or []
|
||
if not t_args:
|
||
t_args = ["list"]
|
||
cmd = [sys.executable, tmux_bin] + t_args
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
|
||
def cmd_muse_dispatch(args):
|
||
"""Bridge 'box muse' commands to muse-cli-node or interactive REPL / multi-node lookups."""
|
||
m_args = getattr(args, "muse_args", []) or []
|
||
if not m_args:
|
||
cmd = [str(BIN_DIR / "muse"), "--help"]
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
first = m_args[0]
|
||
|
||
# If first is 'tmux', dispatch to tmux
|
||
if first == "tmux":
|
||
cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + m_args[1:]
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
# If first is a cross-fleet query
|
||
if first in ("status", "fleet"):
|
||
cmd_fleet_status(args)
|
||
return
|
||
|
||
if first in ("passkey", "key"):
|
||
cmd_passkey_info(args)
|
||
return
|
||
|
||
if first in ("lookup", "lookups"):
|
||
setattr(args, "target", m_args[1] if len(m_args) > 1 else None)
|
||
cmd_lookup(args)
|
||
return
|
||
|
||
if first in ("threads", "sidechats"):
|
||
cmd_thread_list(args)
|
||
return
|
||
|
||
if first in ("unread", "unreads"):
|
||
_lookup_unreads(args)
|
||
return
|
||
|
||
# If first is a valid node
|
||
if first in VALID_NODES:
|
||
node = first
|
||
subargs = m_args[1:]
|
||
if not subargs:
|
||
subargs = ["status"]
|
||
if subargs[0] == "chat":
|
||
cmd = [sys.executable, str(BIN_DIR / "muse-chat-repl.py"), node] + subargs[1:]
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
cmd = [str(BIN_DIR / "muse-cli-node"), node] + subargs
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
# Fallback to bin/muse wrapper
|
||
cmd = [str(BIN_DIR / "muse")] + m_args
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
|
||
def build_parser():
|
||
common = argparse.ArgumentParser(add_help=False)
|
||
common.add_argument("--json", action="store_true", help="Output machine-readable JSON")
|
||
|
||
prog_name = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "super"
|
||
if prog_name.endswith(".py"):
|
||
prog_name = "super"
|
||
|
||
parser = argparse.ArgumentParser(
|
||
prog=prog_name,
|
||
description=f"{prog_name} — Unified Orchestrator CLI for NetVM & Box",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
parents=[common]
|
||
)
|
||
subparsers = parser.add_subparsers(dest="domain", help="Orchestration Domain")
|
||
|
||
# Domain: FLEET
|
||
p_fleet = subparsers.add_parser("fleet", parents=[common], help="Node health, CDP status, active tabs, watch, restart, heal")
|
||
p_fleet.add_argument("action", nargs="?", default="status", choices=["status", "watch", "restart", "cdp", "heal"])
|
||
p_fleet.add_argument("node", nargs="?", default=None, help="Target node (for restart / cdp / heal)")
|
||
p_fleet.add_argument("--interval", type=int, default=2, help="Watch refresh interval in seconds")
|
||
|
||
# Domain: WATCHDOG
|
||
p_watchdog = subparsers.add_parser("watchdog", parents=[common], help="Watchdog timers: status, trigger runs")
|
||
p_watchdog.add_argument("action", nargs="?", default="status", choices=["status", "run"])
|
||
p_watchdog.add_argument("target", nargs="?", default=None, help="Target node (or 'relay') for run")
|
||
|
||
# Domain: APPROVALS
|
||
p_approvals = subparsers.add_parser("approvals", parents=[common], help="Inspect and handle agent browser & gateway approvals")
|
||
p_approval = subparsers.add_parser("approval", parents=[common], help="Alias for 'approvals'")
|
||
|
||
for p_app in (p_approvals, p_approval):
|
||
p_app.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||
app_sub = p_app.add_subparsers(dest="app_action")
|
||
|
||
p_app_check = app_sub.add_parser("check", parents=[common], help="Check fleet approval states")
|
||
p_app_check.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||
p_app_check.add_argument("-v", "--verbose", action="store_true", help="Show detailed task prompts, thread URLs, and card text")
|
||
|
||
p_app_list = app_sub.add_parser("list", parents=[common], help="Alias for 'check'")
|
||
p_app_list.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||
p_app_list.add_argument("-v", "--verbose", action="store_true", help="Show detailed task prompts, thread URLs, and card text")
|
||
|
||
p_app_inspect = app_sub.add_parser("inspect", parents=[common], help="Inspect detailed approval and input-wait status on a node")
|
||
p_app_inspect.add_argument("node", choices=VALID_NODES, help="Target node to inspect")
|
||
|
||
p_app_allow = app_sub.add_parser("allow", parents=[common], help="Approve pending browser request")
|
||
p_app_allow.add_argument("node", choices=VALID_NODES, help="Target node to approve")
|
||
p_app_allow.add_argument("--always", action="store_true", help="Click 'Always allow this site' instead of 'Allow once'")
|
||
p_app_allow.add_argument("--force", action="store_true", help="Force approval even if target is untrusted")
|
||
p_app_allow.add_argument("--message", default=None, help="Optional message/credential to deliver to the waiting agent")
|
||
p_app_allow.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there")
|
||
|
||
p_app_approve = app_sub.add_parser("approve", parents=[common], help="Alias for 'allow'")
|
||
p_app_approve.add_argument("node", choices=VALID_NODES, help="Target node to approve")
|
||
p_app_approve.add_argument("--always", action="store_true", help="Click 'Always allow this site' instead of 'Allow once'")
|
||
p_app_approve.add_argument("--force", action="store_true", help="Force approval even if target is untrusted")
|
||
p_app_approve.add_argument("--message", default=None, help="Optional message/credential to deliver to the waiting agent")
|
||
p_app_approve.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there")
|
||
|
||
p_app_deny = app_sub.add_parser("deny", parents=[common], help="Deny pending browser request")
|
||
p_app_deny.add_argument("node", choices=VALID_NODES, help="Target node to deny")
|
||
p_app_deny.add_argument("--message", default=None, help="Optional message to deliver to the waiting agent")
|
||
p_app_deny.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there")
|
||
|
||
p_app_auto = app_sub.add_parser("auto", parents=[common], help="Auto-approve all trusted requests across fleet")
|
||
p_app_auto.add_argument("--node", choices=VALID_NODES, default=None, help="Target node (or all nodes)")
|
||
|
||
p_app_watch = app_sub.add_parser("watch", parents=[common], help="Live watch pending approvals")
|
||
p_app_watch.add_argument("--interval", type=int, default=2, help="Watch refresh interval in seconds")
|
||
p_app_watch.add_argument("--auto", action="store_true", help="Automatically approve trusted requests as they appear")
|
||
p_app_watch.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||
|
||
p_app_reply = app_sub.add_parser("reply", parents=[common], help="Reply to a waiting agent task/prompt in its active thread")
|
||
p_app_reply.add_argument("node", choices=VALID_NODES, help="Target node to reply to")
|
||
p_app_reply.add_argument("message", help="Message or answer to dispatch")
|
||
p_app_reply.add_argument("--allow-main-chat", action="store_true", help="Explicit override if the active thread is Main Chat")
|
||
|
||
p_app_dismiss = app_sub.add_parser("dismiss", parents=[common], help="Dismiss any open task dialog/popup on a node")
|
||
p_app_dismiss.add_argument("node", choices=VALID_NODES, help="Target node to dismiss dialog on")
|
||
|
||
p_app_clear = app_sub.add_parser("clear", parents=[common], help="Clear and dismiss pending input waits on a node or all nodes")
|
||
p_app_clear.add_argument("node", nargs="?", choices=VALID_NODES, default=None, help="Target node (or omit for all nodes)")
|
||
|
||
p_app_clear_all = app_sub.add_parser("clear-all", parents=[common], help="Clear and dismiss all pending input waits across fleet")
|
||
|
||
p_app_req_key = app_sub.add_parser("request-key", parents=[common], help="Request operator passkey/key approval for an agent")
|
||
p_app_req_key.add_argument("node", choices=VALID_NODES, help="Target node requesting key")
|
||
p_app_req_key.add_argument("--reason", default="Passkey authentication required", help="Reason for key request")
|
||
|
||
# Domain: MUSE-CHOICES
|
||
p_mc = subparsers.add_parser("muse-choices", parents=[common], help="Muse TUI A/B/C choice auto-answer daemon (on/off/status/logs)")
|
||
mc_sub = p_mc.add_subparsers(dest="mc_action")
|
||
|
||
p_mc_on = mc_sub.add_parser("on", parents=[common], help="Enable auto-answers on all Muse panes")
|
||
p_mc_on.add_argument("--dry-run", action="store_true", help="Log answers instead of sending keys")
|
||
|
||
p_mc_off = mc_sub.add_parser("off", parents=[common], help="Disable auto-answers and stop all watchers")
|
||
|
||
p_mc_status = mc_sub.add_parser("status", parents=[common], help="Show desired state, watchers, recent answers")
|
||
|
||
p_mc_logs = mc_sub.add_parser("logs", parents=[common], help="Tail one pane watcher log")
|
||
p_mc_logs.add_argument("--socket", required=True, help="Tmux socket path (e.g. /tmp/tmux-1000/default)")
|
||
p_mc_logs.add_argument("--pane", required=True, help="Pane id (e.g. %%37)")
|
||
p_mc_logs.add_argument("-n", type=int, default=20, help="Lines to show (default: 20)")
|
||
|
||
p_mc_rec = mc_sub.add_parser("reconcile", parents=[common], help="Enforce desired state now (start missing / stop excess)")
|
||
|
||
# Domain: INVITE
|
||
p_invite = subparsers.add_parser("invite", parents=[common], help="Muse.ai invite codes: find per-agent codes and redeem")
|
||
p_invite.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node (status)")
|
||
inv_sub = p_invite.add_subparsers(dest="invite_action")
|
||
|
||
p_inv_status = inv_sub.add_parser("status", parents=[common], help="Invite code matrix across fleet (default)")
|
||
p_inv_status.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||
|
||
p_inv_list = inv_sub.add_parser("list", parents=[common], help="Alias for 'status'")
|
||
p_inv_list.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||
|
||
p_inv_code = inv_sub.add_parser("code", parents=[common], help="Show one agent's invite code")
|
||
p_inv_code.add_argument("node", choices=VALID_NODES, help="Target node")
|
||
|
||
p_inv_find = inv_sub.add_parser("find", parents=[common], help="Alias for 'code'")
|
||
p_inv_find.add_argument("node", choices=VALID_NODES, help="Target node")
|
||
|
||
p_inv_redeem = inv_sub.add_parser("redeem", parents=[common], help="Redeem an invite code on a node")
|
||
p_inv_redeem.add_argument("node", choices=VALID_NODES, help="Target node")
|
||
p_inv_redeem.add_argument("code", help="6-char invite code")
|
||
|
||
p_inv_salvage = inv_sub.add_parser("salvage", parents=[common], help="Salvage an out-of-tokens agent (defaults to 646)")
|
||
p_inv_salvage.add_argument("node", nargs="?", default="646", choices=VALID_NODES, help="Blocked agent node (default: 646)")
|
||
p_inv_salvage.add_argument("--helper", choices=VALID_NODES, help="Specific helper agent to redeem code")
|
||
|
||
# Domain: USAGE
|
||
p_usage = subparsers.add_parser("usage", parents=[common], help="Muse.ai usage limits per agent")
|
||
p_usage.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||
p_usage.add_argument("node_pos", nargs="?", default=None, choices=VALID_NODES, help="Optional node positional argument")
|
||
|
||
# Domain: SETTINGS
|
||
p_settings = subparsers.add_parser("settings", parents=[common], help="Settings menu RPA automation")
|
||
settings_sub = p_settings.add_subparsers(dest="settings_action")
|
||
p_set_usage = settings_sub.add_parser("usage", parents=[common], help="Extract usage via Settings RPA")
|
||
p_set_usage.add_argument("node", choices=VALID_NODES, default="646", nargs="?", help="Target node (default: 646)")
|
||
p_set_check = settings_sub.add_parser("check-redeem", parents=[common], help="Check if Redeem Invite Code is in General Settings")
|
||
p_set_check.add_argument("node", choices=VALID_NODES, default="646", nargs="?", help="Target node (default: 646)")
|
||
|
||
# Domain: ONBOARD
|
||
p_onboard = subparsers.add_parser("onboard", parents=[common], help="Agent onboarding & automated invite code salvage pipeline")
|
||
onboard_sub = p_onboard.add_subparsers(dest="onboard_action")
|
||
|
||
p_onb_start = onboard_sub.add_parser("start", parents=[common], help="Start onboarding pipeline (provisions infra + initiates auth)")
|
||
p_onb_start.add_argument("node", help="Target new node label (e.g. dev2, client1)")
|
||
p_onb_start.add_argument("--email", required=True, help="Client login email")
|
||
p_onb_start.add_argument("--for", dest="for_agent", choices=VALID_NODES, help="Beneficiary agent to unblock (defaults to most urgent)")
|
||
p_onb_start.add_argument("--code", help="Explicit 6-character invite code to redeem")
|
||
p_onb_start.add_argument("--account-name", help="Display name hint for multi-account selection")
|
||
|
||
p_onb_otp = onboard_sub.add_parser("submit-otp", parents=[common], help="Submit transient OTP and complete auto-redemption")
|
||
p_onb_otp.add_argument("node", help="Target node label")
|
||
p_onb_otp.add_argument("otp", help="6-digit verification code")
|
||
p_onb_otp.add_argument("--email", help="Client email (optional)")
|
||
|
||
p_onb_stat = onboard_sub.add_parser("status", parents=[common], help="Query onboarding pipeline state for a node")
|
||
p_onb_stat.add_argument("node", help="Target node label")
|
||
|
||
p_onb_wo = onboard_sub.add_parser("salvage-wo", parents=[common], help="Dispatch cryptographically signed salvage work order to sidechat")
|
||
p_onb_wo.add_argument("node", nargs="?", default="646", choices=VALID_NODES, help="Blocked agent node (default: 646)")
|
||
p_onb_wo.add_argument("--target", default="646 tasks", help="Target sidechat (default: 646 tasks)")
|
||
|
||
onboard_sub.add_parser("feed-matrix", parents=[common], help="Display all agents ranked by feeding weight, job volume, role, and work done over time")
|
||
|
||
# Domain: CHROMEBOX
|
||
p_chromebox = subparsers.add_parser("chromebox", parents=[common], help="Agent browser settings-menu toggles (chromebox RPA)")
|
||
chrome_sub = p_chromebox.add_subparsers(dest="chrome_action")
|
||
p_chrome_perm = chrome_sub.add_parser("permissions", parents=[common], help="Settings menu toggles (permissions + related tabs)")
|
||
p_chrome_perm.add_argument("node", choices=VALID_NODES, help="Target node")
|
||
perm_sub = p_chrome_perm.add_subparsers(dest="perm_action")
|
||
|
||
p_perm_list = perm_sub.add_parser("list", parents=[common], help="List toggle states (default)")
|
||
p_perm_list.add_argument("--tab", default=None, help="Only this settings tab")
|
||
|
||
p_perm_get = perm_sub.add_parser("get", parents=[common], help="Read one toggle")
|
||
p_perm_get.add_argument("toggle", help="Toggle address (e.g. permissions.web_access, permissions.websites:example.com)")
|
||
|
||
p_perm_set = perm_sub.add_parser("set", parents=[common], help="Set one toggle (verified with readback)")
|
||
p_perm_set.add_argument("toggle", help="Toggle address")
|
||
p_perm_set.add_argument("value", help="Value (on/off, Allow/Ask/Deny, auto_allow/always_ask, theme name)")
|
||
|
||
p_perm_desc = perm_sub.add_parser("describe", parents=[common], help="Full inventory of one settings tab (JSON)")
|
||
p_perm_desc.add_argument("tab", help="Settings tab name (e.g. Permissions)")
|
||
|
||
# Domain: DM
|
||
p_dm = subparsers.add_parser("dm", parents=[common], help="Inter-agent DMs, work orders ([WO]), acks, live log tail")
|
||
dm_sub = p_dm.add_subparsers(dest="action")
|
||
|
||
# super dm send
|
||
p_dm_send = dm_sub.add_parser("send", parents=[common], help="Send a direct message")
|
||
p_dm_send.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node")
|
||
p_dm_send.add_argument("--from", dest="from_agent", default=DEFAULT_SENDER, help="Sender identity (default: super)")
|
||
p_dm_send.add_argument("--target", default="main", help="Target chat/sidechat (default: main)")
|
||
p_dm_send.add_argument("--allow-main-chat", action="store_true", help="Explicit override allowing send directly to Main Chat")
|
||
p_dm_send.add_argument("-w", "--wait", action="store_true", help="Wait for recipient agent to reply")
|
||
p_dm_send.add_argument("-t", "--timeout", type=int, default=60, help="Wait timeout in seconds (default: 60)")
|
||
p_dm_send.add_argument("--expect-reply", action="store_true", help="Arm follow-up tracking")
|
||
p_dm_send.add_argument("--reply-timeout", type=int, default=None, help="Timeout in seconds")
|
||
p_dm_send.add_argument("--no-sign", action="store_true", help="Send without cryptographic SSH signature")
|
||
p_dm_send.add_argument("message", help="Message body")
|
||
|
||
# super dm chat
|
||
p_dm_chat = dm_sub.add_parser("chat", parents=[common], help="Interactive conversational chat session with an agent")
|
||
p_dm_chat.add_argument("agent", choices=VALID_NODES, help="Target agent to converse with")
|
||
p_dm_chat.add_argument("--target", default=None, help="Target sidechat or 'main'")
|
||
p_dm_chat.add_argument("-t", "--timeout", type=int, default=60, help="Reply timeout in seconds (default: 60)")
|
||
|
||
# super dm send-file
|
||
p_dm_sf = dm_sub.add_parser("send-file", parents=[common], help="Transfer a file to an agent and send a pointer in chat")
|
||
p_dm_sf.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node")
|
||
p_dm_sf.add_argument("--target", default=None, help="Target sidechat (default: agent primary task sidechat)")
|
||
p_dm_sf.add_argument("--note", default=None, help="Optional operator note or instructions")
|
||
p_dm_sf.add_argument("-w", "--wait", action="store_true", help="Wait for recipient agent to reply/acknowledge")
|
||
p_dm_sf.add_argument("-t", "--timeout", type=int, default=60, help="Reply timeout in seconds (default: 60)")
|
||
p_dm_sf.add_argument("file", help="Path to file to transfer")
|
||
|
||
# super dm files
|
||
p_dm_files = dm_sub.add_parser("files", parents=[common], help="List transferred files and staged payloads")
|
||
p_dm_files.add_argument("files_action", nargs="?", default="list", choices=["list", "clean"], help="Action: list (default) or clean")
|
||
p_dm_files.add_argument("--agent", choices=VALID_NODES, default=None, help="Filter by recipient agent")
|
||
p_dm_files.add_argument("--older-than", type=int, default=7, help="Retention cutoff in days for clean (default: 7)")
|
||
|
||
# super dm wo
|
||
p_dm_wo = dm_sub.add_parser("wo", parents=[common], help="Dispatch a structured Work Order ([WO:...])")
|
||
p_dm_wo.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node")
|
||
p_dm_wo.add_argument("--title", required=True, help="Work Order title")
|
||
p_dm_wo.add_argument("--priority", choices=["routine", "urgent"], default="routine", help="Priority level")
|
||
p_dm_wo.add_argument("--from", dest="from_agent", default=DEFAULT_SENDER, help="Sender identity (default: super)")
|
||
p_dm_wo.add_argument("--target", default=None, help="Target sidechat (default: agent primary task sidechat)")
|
||
p_dm_wo.add_argument("--allow-main-chat", action="store_true", help="Explicit override allowing work order in Main Chat")
|
||
p_dm_wo.add_argument("--no-wait", dest="wait", action="store_false", help="Do not wait for reply")
|
||
p_dm_wo.add_argument("-t", "--timeout", type=int, default=60, help="Wait timeout in seconds (default: 60)")
|
||
p_dm_wo.add_argument("--no-sign", action="store_true", help="Issue without cryptographic SSH signature")
|
||
p_dm_wo.add_argument("body", help="Work Order detailed description")
|
||
|
||
# super dm ack
|
||
p_dm_ack = dm_sub.add_parser("ack", parents=[common], help="Acknowledge a DM or Work Order")
|
||
p_dm_ack.add_argument("id", help="DM ID or Work Order ID")
|
||
p_dm_ack.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node")
|
||
p_dm_ack.add_argument("--from", dest="from_agent", default=DEFAULT_SENDER, help="Sender identity (default: super)")
|
||
p_dm_ack.add_argument("--target", default="main", help="Target chat (default: main)")
|
||
|
||
# super dm log
|
||
p_dm_log = dm_sub.add_parser("log", parents=[common], help="Display recent DM activity")
|
||
p_dm_log.add_argument("-n", type=int, default=25, help="Number of records to show")
|
||
p_dm_log.add_argument("--agent", choices=VALID_NODES, default=None, help="Filter by agent")
|
||
p_dm_log.add_argument("--filter", default=None, help="Search filter in text")
|
||
|
||
# super dm tail
|
||
p_dm_tail = dm_sub.add_parser("tail", parents=[common], help="Live stream incoming DMs")
|
||
p_dm_tail.add_argument("--filter", default=None, help="Search filter in text")
|
||
|
||
# super dm verify
|
||
p_dm_verify = dm_sub.add_parser("verify", parents=[common], help="Verify cryptographic signature on a DM")
|
||
p_dm_verify.add_argument("message", nargs="?", default=None, help="Message text containing signature block")
|
||
p_dm_verify.add_argument("--agent", choices=VALID_NODES, default=None, help="Scan agent's recent reads")
|
||
p_dm_verify.add_argument("--target", default=None, help="Scan target chat's recent reads")
|
||
|
||
# Domain: THREAD
|
||
p_thread = subparsers.add_parser("thread", parents=[common], help="Inspect agent main chats, sidechats, and scrollbacks")
|
||
thread_sub = p_thread.add_subparsers(dest="action")
|
||
|
||
p_thread_list = thread_sub.add_parser("list", parents=[common], help="List active threads for an agent or all fleet sidechats")
|
||
p_thread_list.add_argument("agent", nargs="?", default=None, choices=VALID_NODES + [None], help="Agent node to inspect (default: all registered fleet sidechats)")
|
||
|
||
p_thread_view = thread_sub.add_parser("view", parents=[common], help="View recent messages from an agent's thread")
|
||
p_thread_view.add_argument("agent", choices=VALID_NODES, help="Agent node to inspect")
|
||
p_thread_view.add_argument("thread_id", help="Thread UUID or 'main'")
|
||
p_thread_view.add_argument("--limit", type=int, default=15, help="Number of messages to retrieve")
|
||
|
||
# Domain: JOB
|
||
p_job = subparsers.add_parser("job", parents=[common], help="Manage scheduled jobs, systemd timers, triggers, logs")
|
||
job_sub = p_job.add_subparsers(dest="action")
|
||
|
||
p_job_list = job_sub.add_parser("list", parents=[common], help="List defined jobs")
|
||
|
||
p_job_show = job_sub.add_parser("show", parents=[common], help="Inspect full job definition and timer details")
|
||
p_job_show.add_argument("name", help="Job name")
|
||
|
||
p_job_status = job_sub.add_parser("status", parents=[common], help="Check timer status")
|
||
p_job_status.add_argument("name", nargs="?", default=None, help="Job/timer name")
|
||
|
||
p_job_create = job_sub.add_parser("create", parents=[common], help="Create or update a scheduled job")
|
||
p_job_create.add_argument("name", help="Job name (^[a-z0-9-]{1,64}$)")
|
||
p_job_create.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent")
|
||
p_job_create.add_argument("--schedule", default=None, help="Cron schedule (e.g. '*/15 * * * *' or 'manual')")
|
||
p_job_create.add_argument("--prompt", default=None, help="Prompt template")
|
||
p_job_create.add_argument("--description", default=None, help="Job description")
|
||
p_job_create.add_argument("--timeout", type=int, default=300, help="Timeout in seconds (60-3600, default: 300)")
|
||
p_job_create.add_argument("--on-failure", choices=["alert", "retry", "ignore"], default="alert", help="Failure policy")
|
||
p_job_create.add_argument("--chain-next", default=None, help="Next job name to chain on success")
|
||
p_job_create.add_argument("--sidechat-create", action="store_true", help="Dispatch in a sidechat")
|
||
p_job_create.add_argument("--sidechat-name", default=None, help="Sidechat name template")
|
||
p_job_create.add_argument("--reuse-key", default=None, help="Sidechat reuse key")
|
||
p_job_create.add_argument("--dm-target", default=None, help="Target chat or thread name")
|
||
p_job_create.add_argument("--file", default=None, help="Path to JSON file with job definition (or '-' for stdin)")
|
||
p_job_create.add_argument("--no-enable", action="store_true", help="Do not enable systemd timer immediately")
|
||
p_job_create.add_argument("--update", action="store_true", help="Allow updating an existing job")
|
||
|
||
p_job_enable = job_sub.add_parser("enable", parents=[common], help="Enable and activate a job's systemd timer")
|
||
p_job_enable.add_argument("name", help="Job name")
|
||
|
||
p_job_disable = job_sub.add_parser("disable", parents=[common], help="Stop and disable a job's systemd timer")
|
||
p_job_disable.add_argument("name", help="Job name")
|
||
|
||
p_job_delete = job_sub.add_parser("delete", parents=[common], help="Delete a job and its timer")
|
||
p_job_delete.add_argument("name", help="Job name")
|
||
p_job_delete.add_argument("-y", "--yes", action="store_true", help="Bypass confirmation prompt")
|
||
p_job_delete.add_argument("--force", action="store_true", help="Force deletion even if timer error")
|
||
|
||
p_job_run = job_sub.add_parser("run", parents=[common], help="Trigger a job immediately")
|
||
p_job_run.add_argument("name", help="Job name to trigger")
|
||
p_job_run.add_argument("-f", "--follow", action="store_true", help="Stream live dispatch and execution events")
|
||
|
||
p_job_log = job_sub.add_parser("log", parents=[common], help="View recent job execution events")
|
||
p_job_log.add_argument("-n", type=int, default=20, help="Number of events")
|
||
p_job_log.add_argument("name", nargs="?", default=None, help="Filter by job name")
|
||
|
||
# Domain: WEB
|
||
p_web = subparsers.add_parser("web", parents=[common], help="Probe VM web surfaces (box.muse-dev.online), auth, sync")
|
||
web_sub = p_web.add_subparsers(dest="action")
|
||
|
||
web_sub.add_parser("health", parents=[common], help="Check Google Cloud VM web surface health")
|
||
web_sub.add_parser("test-auth", parents=[common], help="Verify operator auth gating on the VM")
|
||
web_sub.add_parser("sync", parents=[common], help="Check local log size and sync integrity")
|
||
|
||
# Domain: CRED
|
||
p_cred = subparsers.add_parser("cred", parents=[common], help="Credential console API & client onboarding")
|
||
cred_sub = p_cred.add_subparsers(dest="action")
|
||
|
||
p_c_init = cred_sub.add_parser("initiate", parents=[common], help="Initiate onboarding flow for a node")
|
||
p_c_init.add_argument("--node", required=True, help="Node label (e.g. opm, pip, 646)")
|
||
p_c_init.add_argument("--email", required=True, help="Client login email")
|
||
p_c_init.add_argument("--service", default="muse", help="Service name (default: muse)")
|
||
p_c_init.add_argument("--account-name", default=None, help="Display name hint for multi-account selector")
|
||
|
||
p_c_otp = cred_sub.add_parser("submit-otp", parents=[common], help="Submit transient OTP verification code")
|
||
p_c_otp.add_argument("--node", required=True, help="Node label")
|
||
p_c_otp.add_argument("--otp", required=True, help="6-digit verification code")
|
||
p_c_otp.add_argument("--email", default=None, help="Client email (optional)")
|
||
|
||
p_c_stat = cred_sub.add_parser("status", parents=[common], help="Query onboarding & vitality status for a node")
|
||
p_c_stat.add_argument("--node", required=True, help="Node label")
|
||
|
||
p_c_link = cred_sub.add_parser("link-instagram", parents=[common], help="Generate Tailscale one-tap portal & OAuth link for age verification")
|
||
p_c_link.add_argument("--node", required=True, help="Node label")
|
||
p_c_link.add_argument("--notify", action="store_true", help="Send email alert to operator via local MTA")
|
||
|
||
p_c_audit = cred_sub.add_parser("meta-audit", parents=[common], help="Audit Meta Accounts Center for linked profiles and email")
|
||
p_c_audit.add_argument("--node", required=True, help="Node label")
|
||
|
||
cred_sub.add_parser("list", parents=[common], help="List all registered nodes and vitality statuses")
|
||
|
||
|
||
# Domain: SSH
|
||
p_ssh = subparsers.add_parser("ssh", parents=[common], help="Mint and manage fleet SSH signing/access keys")
|
||
ssh_sub = p_ssh.add_subparsers(dest="action")
|
||
|
||
p_s_mint = ssh_sub.add_parser("mint", parents=[common], help="Mint a new ed25519 SSH keypair and register in allowed_signers")
|
||
p_s_mint.add_argument("name", help="Identity/agent name (e.g. 646, pip, dev, canary)")
|
||
p_s_mint.add_argument("--force", action="store_true", help="Overwrite existing keypair")
|
||
|
||
p_s_list = ssh_sub.add_parser("list", parents=[common], help="List managed SSH keypairs and registration status")
|
||
p_s_show = ssh_sub.add_parser("show", parents=[common], help="Show public key content and fingerprint")
|
||
p_s_show.add_argument("name", help="Identity/agent name")
|
||
|
||
p_s_ports = ssh_sub.add_parser("ports", parents=[common], help="List container reverse tunnel port mappings")
|
||
p_s_info = ssh_sub.add_parser("info", parents=[common], help="Show SSH dial-in commands and reverse tunnel coordinates")
|
||
p_s_info.add_argument("name", help="Agent identity (e.g. 646, muse, pip)")
|
||
|
||
# Domain: MD (Hatch Agent Markdown & Operational Drive)
|
||
p_md = subparsers.add_parser("md", parents=[common], help="Manage agent .md system files & inject operational DRIVE via Hatch")
|
||
md_sub = p_md.add_subparsers(dest="action")
|
||
|
||
p_md_audit = md_sub.add_parser("audit", parents=[common], help="Audit .md files and DRIVE scores across fleet agents")
|
||
p_md_audit.add_argument("accounts", nargs="*", help="Optional account filter")
|
||
|
||
p_md_list = md_sub.add_parser("list", parents=[common], help="List files in agent container via Hatch")
|
||
p_md_list.add_argument("account", help="Agent account (e.g. muse, pip, 646, opm, def, dev)")
|
||
p_md_list.add_argument("path", nargs="?", default="", help="Subdirectory path")
|
||
|
||
p_md_read = md_sub.add_parser("read", parents=[common], help="Read an agent .md file via Hatch")
|
||
p_md_read.add_argument("account", help="Agent account")
|
||
p_md_read.add_argument("filename", help="Filename (e.g. SOUL.md, HEARTBEAT.md)")
|
||
|
||
p_md_write = md_sub.add_parser("write", parents=[common], help="Write an agent .md file via Hatch")
|
||
p_md_write.add_argument("account", help="Agent account")
|
||
p_md_write.add_argument("filename", help="Filename (e.g. SOUL.md)")
|
||
p_md_write.add_argument("--content", help="Text content to write")
|
||
p_md_write.add_argument("--file", help="Local file to copy from")
|
||
|
||
p_md_diff = md_sub.add_parser("diff", parents=[common], help="Diff container .md file against shared operator template")
|
||
p_md_diff.add_argument("account", help="Agent account")
|
||
p_md_diff.add_argument("filename", help="Filename (e.g. SOUL.md)")
|
||
|
||
p_md_drive = md_sub.add_parser("inject-drive", parents=[common], help="Inject high-drive operator files into an agent container")
|
||
p_md_drive.add_argument("account", help="Agent account")
|
||
p_md_drive.add_argument("--force", action="store_true", help="Force overwrite")
|
||
|
||
p_md_sync = md_sub.add_parser("sync-all", parents=[common], help="Inject high-drive operator files across all active fleet agents")
|
||
p_md_sync.add_argument("--force", action="store_true", help="Force overwrite")
|
||
|
||
p_md_amend = md_sub.add_parser("amend", parents=[common], help="Amend centralized shared operator file with validation and git commit")
|
||
p_md_amend.add_argument("filename", help="Filename (e.g. AGENTS.md, TOOLS.md)")
|
||
p_md_amend.add_argument("--content", help="New content")
|
||
p_md_amend.add_argument("--file", help="File with new content")
|
||
p_md_amend.add_argument("--author", default="operator", help="Author of amendment")
|
||
p_md_amend.add_argument("--reason", default="", help="Reason for amendment")
|
||
|
||
p_md_append = md_sub.add_parser("append", parents=[common], help="Safely append a lesson or note to a shared operator file")
|
||
p_md_append.add_argument("filename", help="Filename (e.g. AGENTS.md)")
|
||
p_md_append.add_argument("text", help="Text to append")
|
||
p_md_append.add_argument("--author", default="operator", help="Author of amendment")
|
||
p_md_append.add_argument("--section", default=None, help="Optional section header")
|
||
|
||
p_md_pull = md_sub.add_parser("pull", parents=[common], help="Pull canonical shared template into an agent container")
|
||
p_md_pull.add_argument("account", help="Agent account")
|
||
p_md_pull.add_argument("filename", help="Filename (e.g. SOUL.md)")
|
||
|
||
p_md_wd = md_sub.add_parser("watchdog", parents=[common], help="Fleet agent drive watchdog daemon & status")
|
||
p_md_wd.add_argument("sub_action", choices=["status", "run"], nargs="?", default="status", help="Watchdog action: status (default) or run (execute cycle)")
|
||
p_md_wd.add_argument("--no-heal", action="store_true", help="Audit only; do not auto-heal degraded agents")
|
||
|
||
# Domain: HARVEST
|
||
p_harvest = subparsers.add_parser("harvest", parents=[common], help="Readback & response harvesting engine")
|
||
harvest_sub = p_harvest.add_subparsers(dest="action")
|
||
p_h_run = harvest_sub.add_parser("run", parents=[common], help="Run a harvest cycle immediately")
|
||
p_h_run.add_argument("--agent", choices=VALID_NODES, default=None, help="Target specific agent")
|
||
p_h_run.add_argument("--dry-run", action="store_true", help="Scrape without persisting watermarks")
|
||
|
||
p_h_status = harvest_sub.add_parser("status", parents=[common], help="Display active watermarks and harvest metrics")
|
||
|
||
p_h_tail = harvest_sub.add_parser("tail", parents=[common], help="Live stream ingested chat messages")
|
||
p_h_tail.add_argument("--filter", default=None, help="Search filter in text")
|
||
|
||
# Domain: FOLLOWUP
|
||
p_followup = subparsers.add_parser("followup", parents=[common], help="Scheduled follow-up deadline tracking and nudges")
|
||
followup_sub = p_followup.add_subparsers(dest="action")
|
||
p_f_list = followup_sub.add_parser("list", parents=[common], help="List tracked follow-ups and deadlines")
|
||
p_f_list.add_argument("--all", action="store_true", help="Include resolved and historical records")
|
||
|
||
p_f_sweep = followup_sub.add_parser("sweep", parents=[common], help="Trigger deadline check and nudge dispatch immediately")
|
||
p_f_sweep.add_argument("--dry-run", action="store_true", help="Simulate sweep without sending DMs")
|
||
|
||
p_f_cancel = followup_sub.add_parser("cancel", parents=[common], help="Cancel a pending follow-up")
|
||
p_f_cancel.add_argument("dm_id", help="DM ID to cancel")
|
||
|
||
# Domain: LOOP (Intrinsic Loop Management)
|
||
p_loop = subparsers.add_parser("loop", parents=[common], help="Intrinsic loop strategy, health, break taxonomy, and variables")
|
||
loop_sub = p_loop.add_subparsers(dest="action")
|
||
|
||
p_l_status = loop_sub.add_parser("status", parents=[common], help="Show active and recent intrinsic loops")
|
||
p_l_status.add_argument("--agent", choices=VALID_NODES, default=None, help="Filter by agent")
|
||
p_l_status.add_argument("--status", choices=["pending", "active", "nudged", "escalated", "closed", "resolved", "all"], default=None, help="Filter by state")
|
||
p_l_status.add_argument("-n", type=int, default=30, help="Number of loops to display")
|
||
|
||
p_l_health = loop_sub.add_parser("health", parents=[common], help="Evaluate per-agent and fleet loop health metrics")
|
||
p_l_health.add_argument("--threshold", type=float, default=None, help="Health ratio threshold (default: 0.5)")
|
||
|
||
p_l_breaks = loop_sub.add_parser("breaks", parents=[common], help="Run break detection diagnostics across loops")
|
||
|
||
# loop strat
|
||
p_l_strat = loop_sub.add_parser("strat", parents=[common], help="Inspect and configure loop modulation strategies")
|
||
strat_sub = p_l_strat.add_subparsers(dest="strat_action")
|
||
p_ls_show = strat_sub.add_parser("show", parents=[common], help="Display active modulation strategy matrix")
|
||
p_ls_show.add_argument("type", nargs="?", default=None, help="Filter by input type (wake, job, siphon, manual, health, heartbeat)")
|
||
|
||
p_ls_set = strat_sub.add_parser("set", parents=[common], help="Override loop modulation strategy")
|
||
p_ls_set.add_argument("type", help="Input type (wake, job, siphon, manual, health, heartbeat)")
|
||
p_ls_set.add_argument("--subtype", default=None, help="Subtype (e.g. overdue, canary, alert, blocker)")
|
||
p_ls_set.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope")
|
||
p_ls_set.add_argument("--track", choices=["true", "false", "actionable", "always", "never"], default=None, help="Track mode")
|
||
p_ls_set.add_argument("--priority", choices=["routine", "normal", "important", "critical"], default=None, help="Priority rank")
|
||
p_ls_set.add_argument("--timeout", type=int, default=None, help="Timeout in seconds before nudge")
|
||
p_ls_set.add_argument("--nudges", type=int, default=None, help="Max automatic nudges")
|
||
p_ls_set.add_argument("--escalate", default=None, help="Escalation target identity (e.g. opm, user, none)")
|
||
|
||
p_ls_reset = strat_sub.add_parser("reset", parents=[common], help="Reset loop strategy override to builtin default")
|
||
p_ls_reset.add_argument("type", help="Input type")
|
||
p_ls_reset.add_argument("--subtype", default=None, help="Subtype")
|
||
p_ls_reset.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope")
|
||
|
||
p_ls_eval = strat_sub.add_parser("eval", parents=[common], help="Evaluate/simulate strategy resolution for input")
|
||
p_ls_eval.add_argument("type", help="Input type")
|
||
p_ls_eval.add_argument("--subtype", default=None, help="Subtype")
|
||
p_ls_eval.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope")
|
||
p_ls_eval.add_argument("--actionable", action="store_true", help="Actionable flag (for wake)")
|
||
|
||
# loop vars
|
||
p_l_vars = loop_sub.add_parser("vars", parents=[common], help="Inspect and adjust runtime control variables")
|
||
vars_sub = p_l_vars.add_subparsers(dest="vars_action")
|
||
vars_sub.add_parser("list", parents=[common], help="List all registered control variables")
|
||
|
||
p_lv_get = vars_sub.add_parser("get", parents=[common], help="Inspect a specific control variable")
|
||
p_lv_get.add_argument("name", help="Variable name")
|
||
|
||
p_lv_set = vars_sub.add_parser("set", parents=[common], help="Set a control variable value")
|
||
p_lv_set.add_argument("name", help="Variable name")
|
||
p_lv_set.add_argument("value", help="New value")
|
||
|
||
p_lv_reset = vars_sub.add_parser("reset", parents=[common], help="Reset a control variable to default")
|
||
p_lv_reset.add_argument("name", help="Variable name")
|
||
|
||
p_lv_hist = vars_sub.add_parser("history", parents=[common], help="Show variable modification audit history")
|
||
p_lv_hist.add_argument("name", nargs="?", default=None, help="Variable name")
|
||
p_lv_hist.add_argument("-n", "--limit", type=int, default=20, help="Number of records")
|
||
|
||
p_lv_rb = vars_sub.add_parser("rollback", parents=[common], help="Roll back a variable to a previous value")
|
||
p_lv_rb.add_argument("name", help="Variable name")
|
||
p_lv_rb.add_argument("--revision", default=None, help="Revision step (int) or timestamp")
|
||
|
||
p_l_remed = loop_sub.add_parser("remediate", parents=[common], help="Run progressive auto-remediation for soft breaks")
|
||
p_l_remed.add_argument("--dry-run", action="store_true", help="Simulate remediation without applying fixes")
|
||
|
||
p_l_close = loop_sub.add_parser("close", parents=[common], help="Manually close/resolve a pending loop")
|
||
p_l_close.add_argument("dm_id", help="DM ID or Loop ID")
|
||
p_l_close.add_argument("--note", default=None, help="Resolution note")
|
||
|
||
p_l_nudge = loop_sub.add_parser("nudge", parents=[common], help="Trigger immediate nudge for a pending loop")
|
||
p_l_nudge.add_argument("dm_id", help="DM ID or Loop ID")
|
||
|
||
p_l_sweep = loop_sub.add_parser("sweep", parents=[common], help="Run immediate sweep cycle")
|
||
p_l_sweep.add_argument("--dry-run", action="store_true", help="Simulate sweep without sending DMs")
|
||
|
||
p_l_harvest = loop_sub.add_parser("harvest", parents=[common], help="Run immediate harvest cycle")
|
||
p_l_harvest.add_argument("--dry-run", action="store_true", help="Scrape without persisting watermarks")
|
||
|
||
# Domain: PIPELINE
|
||
p_pipe = subparsers.add_parser("pipeline", parents=[common], help="Multi-agent workflow pipelines and chained handoffs")
|
||
pipe_sub = p_pipe.add_subparsers(dest="action")
|
||
|
||
p_pipe_run = pipe_sub.add_parser("run", parents=[common], help="Start a new multi-agent pipeline run")
|
||
p_pipe_run.add_argument("name", help="Root pipeline job name (e.g. pipe-demo-step1)")
|
||
p_pipe_run.add_argument("--dry-run", action="store_true", help="Simulate pipeline without sending DMs")
|
||
|
||
p_pipe_status = pipe_sub.add_parser("status", parents=[common], help="Display active pipeline runs and step states")
|
||
p_pipe_hist = pipe_sub.add_parser("history", parents=[common], help="Show historical pipeline runs and outcomes")
|
||
p_pipe_hist.add_argument("-n", "--limit", type=int, default=20, help="Number of records to display")
|
||
|
||
p_pipe_stop = pipe_sub.add_parser("stop", parents=[common], help="Cancel/stop an active pipeline run")
|
||
p_pipe_stop.add_argument("run_id", help="Pipeline run ID (prefix match supported)")
|
||
p_pipe_stop.add_argument("--reason", default="cancelled_by_operator", help="Cancellation reason")
|
||
|
||
p_pipe_prune = pipe_sub.add_parser("prune", parents=[common], help="Prune/timeout stale running pipelines")
|
||
p_pipe_prune.add_argument("--max-age", type=float, default=1.0, help="Max age in hours (default 1.0)")
|
||
|
||
# Top-level domain aliases: strat and vars
|
||
p_strat_alias = subparsers.add_parser("strat", parents=[common], help="Shortcut for 'loop strat'")
|
||
strat_alias_sub = p_strat_alias.add_subparsers(dest="strat_action")
|
||
p_sa_show = strat_alias_sub.add_parser("show", parents=[common], help="Display active modulation strategy matrix")
|
||
p_sa_show.add_argument("type", nargs="?", default=None, help="Filter by input type")
|
||
|
||
p_sa_set = strat_alias_sub.add_parser("set", parents=[common], help="Override loop modulation strategy")
|
||
p_sa_set.add_argument("type", help="Input type")
|
||
p_sa_set.add_argument("--subtype", default=None, help="Subtype")
|
||
p_sa_set.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope")
|
||
p_sa_set.add_argument("--track", choices=["true", "false", "actionable", "always", "never"], default=None, help="Track mode")
|
||
p_sa_set.add_argument("--priority", choices=["routine", "normal", "important", "critical"], default=None, help="Priority rank")
|
||
p_sa_set.add_argument("--timeout", type=int, default=None, help="Timeout in seconds before nudge")
|
||
p_sa_set.add_argument("--nudges", type=int, default=None, help="Max automatic nudges")
|
||
p_sa_set.add_argument("--escalate", default=None, help="Escalation target identity")
|
||
|
||
p_sa_reset = strat_alias_sub.add_parser("reset", parents=[common], help="Reset loop strategy override")
|
||
p_sa_reset.add_argument("type", help="Input type")
|
||
p_sa_reset.add_argument("--subtype", default=None, help="Subtype")
|
||
p_sa_reset.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope")
|
||
|
||
p_sa_eval = strat_alias_sub.add_parser("eval", parents=[common], help="Evaluate/simulate strategy resolution")
|
||
p_sa_eval.add_argument("type", help="Input type")
|
||
p_sa_eval.add_argument("--subtype", default=None, help="Subtype")
|
||
p_sa_eval.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope")
|
||
p_sa_eval.add_argument("--actionable", action="store_true", help="Actionable flag")
|
||
|
||
p_vars_alias = subparsers.add_parser("vars", parents=[common], help="Shortcut for 'loop vars'")
|
||
vars_alias_sub = p_vars_alias.add_subparsers(dest="vars_action")
|
||
vars_alias_sub.add_parser("list", parents=[common], help="List all control variables")
|
||
|
||
p_va_get = vars_alias_sub.add_parser("get", parents=[common], help="Inspect a specific control variable")
|
||
p_va_get.add_argument("name", help="Variable name")
|
||
|
||
p_va_set = vars_alias_sub.add_parser("set", parents=[common], help="Set a control variable value")
|
||
p_va_set.add_argument("name", help="Variable name")
|
||
p_va_set.add_argument("value", help="New value")
|
||
|
||
p_va_reset = vars_alias_sub.add_parser("reset", parents=[common], help="Reset a control variable to default")
|
||
p_va_reset.add_argument("name", help="Variable name")
|
||
|
||
p_va_hist = vars_alias_sub.add_parser("history", parents=[common], help="Show variable modification audit history")
|
||
p_va_hist.add_argument("name", nargs="?", default=None, help="Variable name")
|
||
p_va_hist.add_argument("-n", "--limit", type=int, default=20, help="Number of records")
|
||
|
||
p_va_rb = vars_alias_sub.add_parser("rollback", parents=[common], help="Roll back a variable to a previous value")
|
||
p_va_rb.add_argument("name", help="Variable name")
|
||
p_va_rb.add_argument("--revision", default=None, help="Revision step (int) or timestamp")
|
||
|
||
# Domain: deploy (Deploy multi-agent pipelines or subagent tasks with oversight)
|
||
p_deploy = subparsers.add_parser("deploy", parents=[common], help="Deploy multi-agent task or pipeline across nodes with live oversight")
|
||
deploy_sub = p_deploy.add_subparsers(dest="action")
|
||
|
||
p_dep_pipe = deploy_sub.add_parser("pipeline", parents=[common], help="Deploy multi-agent pipeline")
|
||
p_dep_pipe.add_argument("name", help="Pipeline root job name (e.g. pipe-demo-step1)")
|
||
p_dep_pipe.add_argument("--dry-run", action="store_true", help="Simulate pipeline without sending DMs")
|
||
|
||
p_dep_sub = deploy_sub.add_parser("subagent", parents=[common], help="Spawn sub-agent session and dispatch task")
|
||
p_dep_sub.add_argument("--agent", required=True, choices=VALID_NODES, help="Agent node to spawn subagent on")
|
||
p_dep_sub.add_argument("--title", default="subagent-task", help="Title for the subagent session")
|
||
p_dep_sub.add_argument("prompt", help="Task prompt for the subagent")
|
||
p_dep_sub.add_argument("--wait", type=int, default=30, help="Seconds to wait for subagent response")
|
||
|
||
|
||
# Domain: SWARM
|
||
p_swarm = subparsers.add_parser("swarm", parents=[common], help="Manage multi-agent swarm tasks and worker allocations")
|
||
swarm_sub = p_swarm.add_subparsers(dest="action")
|
||
|
||
p_sw_list = swarm_sub.add_parser("list", parents=[common], help="List all swarms and slot rollups")
|
||
p_sw_status = swarm_sub.add_parser("status", parents=[common], help="Show status and slot details for a swarm")
|
||
p_sw_status.add_argument("swarm_id", help="Swarm identifier (sw-...)")
|
||
|
||
p_sw_spawn = swarm_sub.add_parser("spawn", parents=[common], help="Spawn a new multi-slot worker swarm")
|
||
p_sw_spawn.add_argument("count", type=int, help="Number of worker slots (1-50)")
|
||
p_sw_spawn.add_argument("task", help="Task prompt for the swarm workers")
|
||
p_sw_spawn.add_argument("--label", default=None, help="Optional tracking label")
|
||
|
||
p_sw_prune = swarm_sub.add_parser("prune", parents=[common], help="Archive stale partial/terminal swarms")
|
||
p_sw_prune.add_argument("--stale-hours", type=float, default=6.0, help="Hours of inactivity before pruning (default: 6)")
|
||
p_sw_prune.add_argument("--confirm", action="store_true", help="Confirm execution of pruning")
|
||
|
||
# Domain: subagent (Direct alias for spawning & managing subagents)
|
||
p_subagent = subparsers.add_parser("subagent", parents=[common], help="Spawn sub-agent session and dispatch task")
|
||
subagent_sub = p_subagent.add_subparsers(dest="action")
|
||
p_sub_spawn = subagent_sub.add_parser("spawn", parents=[common], help="Spawn sub-agent session and dispatch task")
|
||
p_sub_spawn.add_argument("--agent", required=True, choices=VALID_NODES, help="Agent node to spawn subagent on")
|
||
p_sub_spawn.add_argument("--title", default="subagent-task", help="Title for the subagent session")
|
||
p_sub_spawn.add_argument("prompt", help="Task prompt for the subagent")
|
||
p_sub_spawn.add_argument("--wait", type=int, default=30, help="Seconds to wait for subagent response")
|
||
|
||
|
||
# Domain: TMUX (Headless background tmux sessions with automatic logging)
|
||
p_tmux = subparsers.add_parser("tmux", parents=[common], help="Manage headless background tmux sessions on /tmp/tmux-muse.sock")
|
||
p_tmux.add_argument("tmux_args", nargs=argparse.REMAINDER, help="Arguments passed directly to muse-tmux.py")
|
||
|
||
# Domain: muse (fast headless gateway via muse-cli-node with isolated per-node Cloudflare WARP egress)
|
||
p_muse = subparsers.add_parser("muse", parents=[common], help="Direct headless gateway client (muse-cli-node)")
|
||
p_muse.add_argument("muse_args", nargs=argparse.REMAINDER, help="Arguments passed to muse-cli-node or fleet lookups")
|
||
|
||
# Domain: LOOKUP (Unified seamless lookups across nodes, threads, unread, approvals, key, docs)
|
||
p_lookup = subparsers.add_parser("lookup", aliases=["lookups"], parents=[common], help="Seamless fleet lookups (summary, fleet, threads, unread, approvals, key, docs)")
|
||
p_lookup.add_argument("target", nargs="?", default="summary", choices=["summary", "fleet", "nodes", "threads", "sidechats", "unread", "unreads", "approvals", "key", "passkey", "docs", "doc", "surfaces", "sentence", "regex", "parse"], help="Lookup target")
|
||
p_lookup.add_argument("lookup_args", nargs=argparse.REMAINDER, help="Additional arguments passed to lookup engine")
|
||
|
||
# Domain: PASSKEY (Operator passkey location & agent approval protocol)
|
||
p_passkey = subparsers.add_parser("passkey", aliases=["key"], parents=[common], help="Display operator passkey location (VM-only), PIN, & agent approval protocol")
|
||
p_passkey.add_argument("action", nargs="?", default="show", choices=["show", "fetch", "get"], help="Passkey action: 'show' details or 'fetch' from VM")
|
||
|
||
# Domain: DOCS (Internal agent lookups, surfaces, sentence grammar, and regex engine)
|
||
p_docs = subparsers.add_parser("docs", aliases=["doc"], parents=[common], help="Agent lookups, surfaces for box.muse-dev.online, sentence grammar & regex")
|
||
p_docs.add_argument("docs_args", nargs=argparse.REMAINDER, help="Arguments passed directly to docs-lookup.py")
|
||
|
||
# Domain: TUI (Interactive full-screen Muse TUI & Box fleet console)
|
||
p_tui = subparsers.add_parser("tui", parents=[common], help="Interactive full-screen Muse TUI & Box fleet console")
|
||
p_tui.add_argument("tui_args", nargs=argparse.REMAINDER, help="Arguments passed directly to muse-tui.py")
|
||
|
||
return parser
|
||
|
||
def main():
|
||
if len(sys.argv) > 1:
|
||
if sys.argv[1] == "tui":
|
||
tui_args = sys.argv[2:]
|
||
cmd = [sys.executable, str(BIN_DIR / "muse-tui.py"), "--mode", "box"] + tui_args
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
elif sys.argv[1] == "tmux":
|
||
cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + (sys.argv[2:] or ["list"])
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
elif sys.argv[1] in ("docs", "doc"):
|
||
cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + sys.argv[2:]
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
elif sys.argv[1] == "muse":
|
||
m_args = sys.argv[2:]
|
||
if not m_args:
|
||
cmd = [str(BIN_DIR / "muse"), "--help"]
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
if m_args[0] == "tmux":
|
||
cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + (m_args[1:] or ["list"])
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
if m_args[0] in ("status", "fleet"):
|
||
sys.argv = [sys.argv[0], "fleet", "status"]
|
||
elif m_args[0] in ("passkey", "key"):
|
||
sys.argv = [sys.argv[0], "passkey"] + m_args[1:]
|
||
elif m_args[0] in ("lookup", "lookups"):
|
||
sys.argv = [sys.argv[0], "lookup"] + m_args[1:]
|
||
elif m_args[0] in ("threads", "sidechats"):
|
||
sys.argv = [sys.argv[0], "thread", "list"] + m_args[1:]
|
||
elif m_args[0] in ("unread", "unreads"):
|
||
sys.argv = [sys.argv[0], "lookup", "unread"]
|
||
elif m_args[0] in VALID_NODES:
|
||
node = m_args[0]
|
||
sub = m_args[1:]
|
||
if not sub:
|
||
sub = ["status"]
|
||
if sub[0] == "chat":
|
||
cmd = [sys.executable, str(BIN_DIR / "muse-chat-repl.py"), node] + sub[1:]
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
cmd = [str(BIN_DIR / "muse-cli-node"), node] + sub
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
else:
|
||
cmd = [str(BIN_DIR / "muse")] + m_args
|
||
res = subprocess.run(cmd)
|
||
sys.exit(res.returncode)
|
||
|
||
parser = build_parser()
|
||
if len(sys.argv) == 1:
|
||
# Default behavior with no arguments: show fleet status
|
||
sys.argv.append("fleet")
|
||
sys.argv.append("status")
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Route commands
|
||
if args.domain == "fleet":
|
||
act = getattr(args, "action", "status") or "status"
|
||
if act == "status":
|
||
cmd_fleet_status(args)
|
||
elif act == "watch":
|
||
cmd_fleet_watch(args)
|
||
elif act == "restart":
|
||
if not getattr(args, "node", None):
|
||
print("Error: Specify node to restart (e.g. super fleet restart muse)", file=sys.stderr)
|
||
sys.exit(1)
|
||
cmd_fleet_restart(args)
|
||
elif act == "cdp":
|
||
if not getattr(args, "node", None):
|
||
print("Error: Specify node (e.g. super fleet cdp muse)", file=sys.stderr)
|
||
sys.exit(1)
|
||
cmd_fleet_cdp(args)
|
||
elif act == "heal":
|
||
if not getattr(args, "node", None):
|
||
print("Error: Specify node to heal (e.g. super fleet heal dev)", file=sys.stderr)
|
||
sys.exit(1)
|
||
cmd_fleet_heal(args)
|
||
elif args.domain == "watchdog":
|
||
act = getattr(args, "action", "status") or "status"
|
||
if act == "status":
|
||
cmd_watchdog_status(args)
|
||
elif act == "run":
|
||
if not getattr(args, "target", None):
|
||
print("Error: Specify node or 'relay' (e.g. box watchdog run dev)", file=sys.stderr)
|
||
sys.exit(1)
|
||
cmd_watchdog_run(args)
|
||
elif args.domain == "dm":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "log":
|
||
cmd_dm_log(args)
|
||
elif act == "send":
|
||
cmd_dm_send(args)
|
||
elif act == "wo":
|
||
cmd_dm_wo(args)
|
||
elif act == "ack":
|
||
cmd_dm_ack(args)
|
||
elif act == "tail":
|
||
cmd_dm_tail(args)
|
||
elif act == "chat":
|
||
cmd_dm_chat(args)
|
||
elif act == "send-file":
|
||
cmd_dm_send_file(args)
|
||
elif act == "files":
|
||
cmd_dm_files(args)
|
||
elif act == "verify":
|
||
cmd_dm_verify(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "thread":
|
||
act = getattr(args, "action", None)
|
||
if act == "list":
|
||
cmd_thread_list(args)
|
||
elif act == "view":
|
||
cmd_thread_view(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "job":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "list":
|
||
cmd_job_list(args)
|
||
elif act == "show":
|
||
cmd_job_show(args)
|
||
elif act == "create":
|
||
cmd_job_create(args)
|
||
elif act == "enable":
|
||
cmd_job_enable(args)
|
||
elif act == "disable":
|
||
cmd_job_disable(args)
|
||
elif act == "delete":
|
||
cmd_job_delete(args)
|
||
elif act == "status":
|
||
cmd_job_status(args)
|
||
elif act == "run":
|
||
cmd_job_run(args)
|
||
elif act == "log":
|
||
cmd_job_log(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "web":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "health":
|
||
cmd_web_health(args)
|
||
elif act == "test-auth":
|
||
cmd_web_test_auth(args)
|
||
elif act == "sync":
|
||
cmd_web_sync(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "cred":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "list":
|
||
cmd_cred_list(args)
|
||
elif act == "initiate":
|
||
cmd_cred_initiate(args)
|
||
elif act == "submit-otp":
|
||
cmd_cred_submit_otp(args)
|
||
elif act == "status":
|
||
cmd_cred_status(args)
|
||
elif act == "link-instagram":
|
||
cmd_cred_link_instagram(args)
|
||
elif act == "meta-audit":
|
||
cmd_cred_meta_audit(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "ssh":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "list":
|
||
cmd_ssh_list(args)
|
||
elif act == "mint":
|
||
cmd_ssh_mint(args)
|
||
elif act == "show":
|
||
cmd_ssh_show(args)
|
||
elif act == "ports":
|
||
cmd_ssh_ports(args)
|
||
elif act == "info":
|
||
cmd_ssh_info(args)
|
||
else:
|
||
p_ssh.print_help()
|
||
elif args.domain == "md":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "audit":
|
||
cmd_md_audit(args)
|
||
elif act == "list":
|
||
cmd_md_list(args)
|
||
elif act == "read":
|
||
cmd_md_read(args)
|
||
elif act == "write":
|
||
cmd_md_write(args)
|
||
elif act == "diff":
|
||
cmd_md_diff(args)
|
||
elif act == "inject-drive":
|
||
cmd_md_inject_drive(args)
|
||
elif act == "sync-all":
|
||
cmd_md_sync_all(args)
|
||
elif act == "amend":
|
||
cmd_md_amend(args)
|
||
elif act == "append":
|
||
cmd_md_append(args)
|
||
elif act == "pull":
|
||
cmd_md_pull(args)
|
||
elif act == "watchdog":
|
||
cmd_md_watchdog(args)
|
||
else:
|
||
p_md.print_help()
|
||
elif args.domain == "harvest":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "status":
|
||
cmd_harvest_status(args)
|
||
elif act == "run":
|
||
cmd_harvest_run(args)
|
||
elif act == "tail":
|
||
cmd_harvest_tail(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "followup":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "list":
|
||
cmd_followup_list(args)
|
||
elif act == "sweep":
|
||
cmd_followup_sweep(args)
|
||
elif act == "cancel":
|
||
cmd_followup_cancel(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "pipeline":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "status":
|
||
cmd_pipeline_status(args)
|
||
elif act == "run":
|
||
cmd_pipeline_run(args)
|
||
elif act == "history":
|
||
cmd_pipeline_history(args)
|
||
elif act == "stop":
|
||
cmd_pipeline_stop(args)
|
||
elif act == "prune":
|
||
cmd_pipeline_prune(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "loop":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "status":
|
||
cmd_loop_status(args)
|
||
elif act == "health":
|
||
cmd_loop_health(args)
|
||
elif act == "breaks":
|
||
cmd_loop_breaks(args)
|
||
elif act == "strat":
|
||
cmd_loop_strat(args)
|
||
elif act == "vars":
|
||
cmd_loop_vars(args)
|
||
elif act == "close":
|
||
cmd_loop_close(args)
|
||
elif act == "nudge":
|
||
cmd_loop_nudge(args)
|
||
elif act == "sweep":
|
||
cmd_loop_sweep(args)
|
||
elif act == "harvest":
|
||
cmd_loop_harvest(args)
|
||
elif act == "remediate":
|
||
cmd_loop_remediate(args)
|
||
else:
|
||
parser.print_help()
|
||
elif args.domain == "strat":
|
||
cmd_loop_strat(args)
|
||
elif args.domain == "vars":
|
||
cmd_loop_vars(args)
|
||
elif args.domain == "swarm":
|
||
act = getattr(args, "action", None)
|
||
if not act or act == "list":
|
||
cmd_swarm_list(args)
|
||
elif act == "status":
|
||
cmd_swarm_status(args)
|
||
elif act == "spawn":
|
||
cmd_swarm_spawn(args)
|
||
elif act == "prune":
|
||
cmd_swarm_prune(args)
|
||
else:
|
||
p_swarm.print_help()
|
||
elif args.domain in ("approvals", "approval", "blocked"):
|
||
cmd_approvals(args)
|
||
elif args.domain == "muse-choices":
|
||
cmd_muse_choices(args)
|
||
elif args.domain == "invite":
|
||
cmd_invite(args)
|
||
elif args.domain == "usage":
|
||
cmd_usage(args)
|
||
elif args.domain == "settings":
|
||
cmd_settings(args)
|
||
elif args.domain == "onboard":
|
||
cmd_onboard(args)
|
||
elif args.domain == "chromebox":
|
||
cmd_chromebox(args)
|
||
elif args.domain in ("deploy", "subagent"):
|
||
if args.domain == "subagent":
|
||
args.action = "subagent"
|
||
cmd_deploy(args)
|
||
elif args.domain in ("lookup", "lookups"):
|
||
cmd_lookup(args)
|
||
elif args.domain in ("passkey", "key"):
|
||
cmd_passkey_info(args)
|
||
elif args.domain == "tmux":
|
||
cmd_tmux_dispatch(args)
|
||
elif args.domain == "muse":
|
||
cmd_muse_dispatch(args)
|
||
elif args.domain in ("docs", "doc"):
|
||
cmd_docs_dispatch(args)
|
||
else:
|
||
parser.print_help()
|
||
|
||
if __name__ == "__main__":
|
||
main()
|