docs(netvm): document watchdog, swarm-worker, and alert relay in README; track approvals CLI
This commit is contained in:
+204
-4
@@ -261,6 +261,18 @@ def collect_fleet_data() -> list:
|
||||
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"],
|
||||
@@ -272,6 +284,8 @@ def collect_fleet_data() -> list:
|
||||
"title": tab["title"],
|
||||
"url": tab["url"],
|
||||
"queue_depth": q_depth,
|
||||
"approval_pending": approval_pending,
|
||||
"approval_detail": approval_detail,
|
||||
})
|
||||
return results
|
||||
|
||||
@@ -285,9 +299,13 @@ def cmd_fleet_status(args):
|
||||
|
||||
headers = ["NODE", "STATUS", "PEER IP:PORT", "LATENCY", "QUEUE", "ACTIVE PAGE / THREAD"]
|
||||
rows = []
|
||||
has_any_approval = False
|
||||
for item in data:
|
||||
# Status calculation
|
||||
if item["proc_alive"] and item["cdp_ok"]:
|
||||
if item.get("approval_pending"):
|
||||
status = badge_warn("APPROVAL_REQ")
|
||||
has_any_approval = True
|
||||
elif item["proc_alive"] and item["cdp_ok"]:
|
||||
status = badge_ok("ACTIVE")
|
||||
elif item["proc_alive"] and not item["cdp_ok"]:
|
||||
status = badge_warn("CDP_DOWN")
|
||||
@@ -299,7 +317,10 @@ def cmd_fleet_status(args):
|
||||
|
||||
# Format title/URL nicely
|
||||
title = item["title"]
|
||||
if "thread/" in item["url"]:
|
||||
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]
|
||||
@@ -313,11 +334,13 @@ def cmd_fleet_status(args):
|
||||
f"{item['peer_ip']}:{item['cdp_port']}",
|
||||
lat,
|
||||
q,
|
||||
title[:45]
|
||||
title[:50]
|
||||
])
|
||||
|
||||
print_table(headers, rows)
|
||||
print("\n" + c_dim(" Commands: super fleet watch | super fleet restart <node> | super fleet cdp <node>") + "\n")
|
||||
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 restart <node> | box approvals [check|allow|auto]") + "\n")
|
||||
|
||||
def cmd_fleet_watch(args):
|
||||
interval = getattr(args, "interval", 2)
|
||||
@@ -372,6 +395,146 @@ def cmd_fleet_cdp(args):
|
||||
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: 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"):
|
||||
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
|
||||
|
||||
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("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 == "UNREACHABLE":
|
||||
badge = badge_dim("OFFLINE")
|
||||
target = "-"
|
||||
purp = c_dim("CDP unreachable")
|
||||
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 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} UNTRUSTED approval(s) require manual review: 'box approvals allow <node> --force' or 'box approvals deny <node>'."))
|
||||
else:
|
||||
print(c_cyan(" All pending approvals are for trusted infrastructure. Run 'box approvals auto' to resolve."))
|
||||
print()
|
||||
else:
|
||||
print("\n" + c_green(" ✔ All agent approval queues clear. No agents blocked.") + "\n")
|
||||
|
||||
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)
|
||||
res = approvals.allow_node_approval(node, always=always, force=force)
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
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)
|
||||
res = approvals.deny_node_approval(node)
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
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."))
|
||||
|
||||
def resolve_sender(args) -> str:
|
||||
explicit = getattr(args, "from_agent", None)
|
||||
if explicit and explicit != DEFAULT_SENDER:
|
||||
@@ -3602,6 +3765,41 @@ def build_parser():
|
||||
p_fleet.add_argument("node", nargs="?", default=None, help="Target node (for restart / cdp)")
|
||||
p_fleet.add_argument("--interval", type=int, default=2, help="Watch refresh interval in seconds")
|
||||
|
||||
# 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_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_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_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_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_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")
|
||||
|
||||
# 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")
|
||||
@@ -4285,6 +4483,8 @@ def main():
|
||||
cmd_swarm_prune(args)
|
||||
else:
|
||||
p_swarm.print_help()
|
||||
elif args.domain in ("approvals", "approval"):
|
||||
cmd_approvals(args)
|
||||
elif args.domain in ("deploy", "subagent"):
|
||||
if args.domain == "subagent":
|
||||
args.action = "subagent"
|
||||
|
||||
Reference in New Issue
Block a user