feat(box): wire tmux tallies, auto-approvals, and onboard connects into CLI and API
- bin/box-ctl.py: wire tmux-tally, tmux-auto-status, tmux-auto-toggle, tmux-auto-once, and onboard-connects actions with idempotent allowlists - bin/exec-constrained.py: register tmux.tally, tmux.auto_status, and onboard.connects ops for HTTPS execution - bin/super-cli.py: wire box tmux dispatch to approver, add cmd_run/cmd_watch, and json unread formatting - .agents/skills/box/SKILL.md: document tmux worker tally and auto-approval capabilities
This commit is contained in:
@@ -32,8 +32,8 @@ Add `--json` to any command for machine-readable output when parsing results in
|
|||||||
- `box lookup summary|fleet|threads|unread|approvals` — seamless one-shot lookups.
|
- `box lookup summary|fleet|threads|unread|approvals` — seamless one-shot lookups.
|
||||||
- `box job list` / `box job log` — scheduled jobs and execution events.
|
- `box job list` / `box job log` — scheduled jobs and execution events.
|
||||||
- `box harvest status` / `box followup list` — harvest watermarks / pending nudges.
|
- `box harvest status` / `box followup list` — harvest watermarks / pending nudges.
|
||||||
- `box muse-choices on|off|status|logs|reconcile` — Muse TUI A/B/C auto-answer daemon switch, state, and per-pane logs.
|
- `box muse-choices on|off|status|logs|reconcile` — Muse TUI auto-answer daemon switch, state, and per-pane logs (default on; `off` is the box-command opt-out).
|
||||||
- `box runtime list|send|launch` — Muse CLI tmux runtimes: live state + approval posture, send-keys input, auto-approved launches.
|
- `box runtime list|send|launch|layout|spread` — Muse CLI tmux runtimes: live state + approval posture, send-keys input, auto-approved launches, pane-geometry layout + spread for squeezed panes.
|
||||||
- `box tmux tally` / `box tmux auto [status|on|off|watch|once|logs|match]` — multi-socket Tmux worker tally, regex auto-approver daemon & guardrails.
|
- `box tmux tally` / `box tmux auto [status|on|off|watch|once|logs|match]` — multi-socket Tmux worker tally, regex auto-approver daemon & guardrails.
|
||||||
- `box onboard connects` / `box onboard-tui` — fleet & client onboarding inventory, CDP ports, OTP salvage & 4-surface TUI.
|
- `box onboard connects` / `box onboard-tui` — fleet & client onboarding inventory, CDP ports, OTP salvage & 4-surface TUI.
|
||||||
- `box invite status|code <node>|redeem <node> <CODE>` / `box usage [--node N]` — invite codes and usage limits.
|
- `box invite status|code <node>|redeem <node> <CODE>` / `box usage [--node N]` — invite codes and usage limits.
|
||||||
|
|||||||
+1008
-81
File diff suppressed because it is too large
Load Diff
+1023
-3
File diff suppressed because it is too large
Load Diff
+280
-1
@@ -70,6 +70,7 @@ DEFAULT_AGENT_SIDECHATS = {
|
|||||||
"pip": "646-pip-coord",
|
"pip": "646-pip-coord",
|
||||||
"muse": "muse tasks",
|
"muse": "muse tasks",
|
||||||
}
|
}
|
||||||
|
MUSE_TMUX_LOG_DIR = NETVM_ROOT / "logs" / "tmux"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ANSI Color & Formatting Engine
|
# ANSI Color & Formatting Engine
|
||||||
@@ -1082,6 +1083,96 @@ def cmd_runtime(args):
|
|||||||
print(" %s watcher %s" % (badge_ok("STARTED"), c_cyan(s)))
|
print(" %s watcher %s" % (badge_ok("STARTED"), c_cyan(s)))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
elif action == "layout":
|
||||||
|
sock = getattr(args, "socket", None)
|
||||||
|
rows = mcw.all_runtime_rows([sock] if sock else None)
|
||||||
|
minimum = {"width": mcw.MIN_APPROVAL_WIDTH,
|
||||||
|
"height": mcw.MIN_APPROVAL_HEIGHT}
|
||||||
|
if as_json:
|
||||||
|
print(json.dumps({"ok": True, "runtimes": rows,
|
||||||
|
"minimum": minimum}, indent=2))
|
||||||
|
return
|
||||||
|
print(c_bold("\n=== RUNTIME LAYOUT ===\n"))
|
||||||
|
print(c_dim(" Minimum for reliable approvals: %dx%d\n" % (
|
||||||
|
minimum["width"], minimum["height"])))
|
||||||
|
if not rows:
|
||||||
|
print(c_dim(" No panes found."))
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
headers = ["SOCKET", "SESSION", "PANE", "DIMS", "GEO", "CMD"]
|
||||||
|
table = []
|
||||||
|
for r in rows:
|
||||||
|
dims = ("%dx%d" % (r["width"], r["height"])
|
||||||
|
if r["width"] is not None else "-")
|
||||||
|
if not r["is_muse"]:
|
||||||
|
geo = badge_dim("-")
|
||||||
|
elif r["squeezed"]:
|
||||||
|
geo = badge_err("SQUEEZED")
|
||||||
|
else:
|
||||||
|
geo = badge_ok("OK")
|
||||||
|
table.append([os.path.basename(r["socket"]),
|
||||||
|
"%s:%s" % (r["session"], r["window"]),
|
||||||
|
r["pane"], dims, geo, (r["cmd"] or "")[:26]])
|
||||||
|
print_table(headers, table)
|
||||||
|
targets = mcw.spread_targets(rows)
|
||||||
|
if targets:
|
||||||
|
print(c_yellow(" %d squeezed runtime(s): %s" % (
|
||||||
|
len(targets), ", ".join(
|
||||||
|
"%s:%s" % (t["session"], t["pane"])
|
||||||
|
for t in targets))))
|
||||||
|
print(c_dim(" Run `box runtime spread` to break them into "
|
||||||
|
"own windows."))
|
||||||
|
print()
|
||||||
|
|
||||||
|
elif action == "spread":
|
||||||
|
sock = getattr(args, "socket", None)
|
||||||
|
session = getattr(args, "session", None)
|
||||||
|
dry_run = getattr(args, "dry_run", False)
|
||||||
|
rows = mcw.all_runtime_rows([sock] if sock else None)
|
||||||
|
if session:
|
||||||
|
rows = [r for r in rows if r["session"] == session]
|
||||||
|
targets = mcw.spread_targets(rows)
|
||||||
|
if dry_run:
|
||||||
|
if as_json:
|
||||||
|
print(json.dumps({"ok": True, "dry_run": True,
|
||||||
|
"targets": targets}, indent=2))
|
||||||
|
return
|
||||||
|
print(c_bold("\n=== RUNTIME SPREAD (dry-run) ===\n"))
|
||||||
|
if not targets:
|
||||||
|
print(c_dim(" No squeezed runtimes; nothing to spread."))
|
||||||
|
for t in targets:
|
||||||
|
print(" Would break %s %s:%s (%dx%d) into own "
|
||||||
|
"window" % (t["socket"], t["session"], t["pane"],
|
||||||
|
t["width"], t["height"]))
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
spread, failed = [], []
|
||||||
|
for t in targets:
|
||||||
|
name = "spread-%s" % t["pane"]
|
||||||
|
r = mcw._tmux(t["socket"], "break-pane", "-s", t["pane"],
|
||||||
|
"-n", name, timeout=15)
|
||||||
|
if r.returncode == 0:
|
||||||
|
spread.append("%s:%s" % (t["session"], t["pane"]))
|
||||||
|
else:
|
||||||
|
failed.append({
|
||||||
|
"pane": "%s:%s" % (t["session"], t["pane"]),
|
||||||
|
"error": (r.stderr or r.stdout or "").strip()
|
||||||
|
or "tmux error"})
|
||||||
|
if as_json:
|
||||||
|
print(json.dumps({"ok": not failed, "spread": spread,
|
||||||
|
"failed": failed}, indent=2))
|
||||||
|
return
|
||||||
|
print(c_bold("\n=== RUNTIME SPREAD ===\n"))
|
||||||
|
if not targets:
|
||||||
|
print(c_dim(" No squeezed runtimes; nothing to spread."))
|
||||||
|
for s in spread:
|
||||||
|
print(" %s spread %s (watchers follow pane ids)" % (
|
||||||
|
badge_ok("MOVED"), c_cyan(s)))
|
||||||
|
for f in failed:
|
||||||
|
print(" %s %s: %s" % (
|
||||||
|
badge_err("FAILED"), f["pane"], f["error"]))
|
||||||
|
print()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if as_json:
|
if as_json:
|
||||||
print(json.dumps({"ok": False,
|
print(json.dumps({"ok": False,
|
||||||
@@ -5100,10 +5191,35 @@ def cmd_passkey_info(args):
|
|||||||
|
|
||||||
|
|
||||||
def _lookup_unreads(args):
|
def _lookup_unreads(args):
|
||||||
|
data = collect_fleet_data()
|
||||||
|
as_json = getattr(args, "json", False)
|
||||||
|
if as_json:
|
||||||
|
nodes_out = []
|
||||||
|
for item in data:
|
||||||
|
n = item.get("node")
|
||||||
|
title = item.get("title", "")
|
||||||
|
unread_cnt = 0
|
||||||
|
if "(1)" in title or "(2)" in title or "(3)" in title:
|
||||||
|
m = re.search(r"\((\d+)\)", title)
|
||||||
|
unread_cnt = int(m.group(1)) if m else 1
|
||||||
|
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"
|
||||||
|
nodes_out.append({
|
||||||
|
"node": n,
|
||||||
|
"unread": unread_cnt,
|
||||||
|
"title": title,
|
||||||
|
"thread": thread_info,
|
||||||
|
"approval_pending": bool(item.get("approval_pending")),
|
||||||
|
})
|
||||||
|
print(json.dumps({"ok": True, "nodes": nodes_out}))
|
||||||
|
return
|
||||||
|
|
||||||
print("\n" + c_bold("=== FLEET UNREAD / ACTIVITY STATUS ===") + "\n")
|
print("\n" + c_bold("=== FLEET UNREAD / ACTIVITY STATUS ===") + "\n")
|
||||||
headers = ["NODE", "UNREAD COUNT", "ACTIVE PAGE / TITLE", "LAST SEEN / THREAD"]
|
headers = ["NODE", "UNREAD COUNT", "ACTIVE PAGE / TITLE", "LAST SEEN / THREAD"]
|
||||||
rows = []
|
rows = []
|
||||||
data = collect_fleet_data()
|
|
||||||
for item in data:
|
for item in data:
|
||||||
n = item["node"]
|
n = item["node"]
|
||||||
title = item.get("title", "")
|
title = item.get("title", "")
|
||||||
@@ -5157,6 +5273,8 @@ def cmd_lookup(args):
|
|||||||
cmd_fleet_status(args)
|
cmd_fleet_status(args)
|
||||||
elif sub in ("threads", "sidechats"):
|
elif sub in ("threads", "sidechats"):
|
||||||
cmd_thread_list(args)
|
cmd_thread_list(args)
|
||||||
|
elif sub in ("unread", "unreads"):
|
||||||
|
_lookup_unreads(args)
|
||||||
elif sub in ("approvals", "approval"):
|
elif sub in ("approvals", "approval"):
|
||||||
cmd_approvals(args)
|
cmd_approvals(args)
|
||||||
elif sub in ("docs", "doc", "surfaces", "sentence", "regex", "parse"):
|
elif sub in ("docs", "doc", "surfaces", "sentence", "regex", "parse"):
|
||||||
@@ -5169,6 +5287,159 @@ def cmd_lookup(args):
|
|||||||
print(f"Unknown lookup target '{sub}'. Choose from: fleet, threads, unread, approvals, key, docs", file=sys.stderr)
|
print(f"Unknown lookup target '{sub}'. Choose from: fleet, threads, unread, approvals, key, docs", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_tmux_name(name: str) -> str:
|
||||||
|
cleaned = re.sub(r"[^a-zA-Z0-9_-]", "-", name).strip("-")
|
||||||
|
cleaned = re.sub(r"-+", "-", cleaned)
|
||||||
|
return cleaned or "run"
|
||||||
|
|
||||||
|
|
||||||
|
def _write_watch_script(session: str) -> tuple[str, str]:
|
||||||
|
watch_session = f"{session}-watch"
|
||||||
|
script_path = f"/tmp/{watch_session}.sh"
|
||||||
|
content = f"""#!/bin/bash
|
||||||
|
target="{session}"
|
||||||
|
exit_file="/tmp/{session}.exit"
|
||||||
|
max=200
|
||||||
|
count=0
|
||||||
|
|
||||||
|
while [ $count -lt $max ]; do
|
||||||
|
if ! tmux -S /tmp/tmux-muse.sock has-session -t "$target" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
pane_content=$(tmux -S /tmp/tmux-muse.sock capture-pane -p -t "$target" 2>/dev/null)
|
||||||
|
if echo "$pane_content" | grep -q "› 1. Yes, proceed"; then
|
||||||
|
tmux -S /tmp/tmux-muse.sock send-keys -t "$target" "1" Enter
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
count=$((count+1))
|
||||||
|
done
|
||||||
|
touch "$exit_file"
|
||||||
|
"""
|
||||||
|
p = Path(script_path)
|
||||||
|
p.write_text(content, encoding="utf-8")
|
||||||
|
os.chmod(script_path, 0o755)
|
||||||
|
return watch_session, script_path
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_run(args):
|
||||||
|
"""Run a headless muse-code session in tmux on /tmp/tmux-muse.sock."""
|
||||||
|
if not shutil.which("tmux"):
|
||||||
|
print("tmux not found", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
if not shutil.which("muse-code"):
|
||||||
|
print("muse-code not found", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
prompt = getattr(args, "prompt", None)
|
||||||
|
prompt_file = getattr(args, "prompt_file", None)
|
||||||
|
if not prompt and not prompt_file:
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
print("Error: prompt or --prompt-file required", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
prompt = sys.stdin.read()
|
||||||
|
if not prompt.strip():
|
||||||
|
print("Error: empty prompt", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
raw_session = getattr(args, "session", None) or f"run-{int(time.time())}"
|
||||||
|
session = _sanitize_tmux_name(raw_session)
|
||||||
|
auto_approve = getattr(args, "auto_approve", False)
|
||||||
|
|
||||||
|
prompt_path = f"/tmp/{session}.prompt"
|
||||||
|
if prompt:
|
||||||
|
Path(prompt_path).write_text(prompt if prompt.endswith("\n") else prompt + "\n", encoding="utf-8")
|
||||||
|
elif prompt_file:
|
||||||
|
prompt_path = prompt_file
|
||||||
|
|
||||||
|
wrapper_path = f"/tmp/{session}.sh"
|
||||||
|
cmd_parts = [
|
||||||
|
"cd /home/super/Projects/NetVM",
|
||||||
|
f"muse-code --prompt-file {prompt_path}",
|
||||||
|
]
|
||||||
|
if getattr(args, "provider", None):
|
||||||
|
cmd_parts.append(f"--provider {args.provider}")
|
||||||
|
if getattr(args, "model", None):
|
||||||
|
cmd_parts.append(f"--model {args.model}")
|
||||||
|
if getattr(args, "effort", None):
|
||||||
|
cmd_parts.append(f"--reasoning-effort {args.effort}")
|
||||||
|
if getattr(args, "permission_profile", None):
|
||||||
|
cmd_parts.append(f"--permission-profile {args.permission_profile}")
|
||||||
|
if getattr(args, "trust_workspace", False):
|
||||||
|
cmd_parts.append("--trust-workspace")
|
||||||
|
if getattr(args, "approval_mode", None):
|
||||||
|
cmd_parts.append(f"--approval-mode {args.approval_mode}")
|
||||||
|
|
||||||
|
full_cmd = " && ".join(cmd_parts)
|
||||||
|
wrapper_content = f"#!/bin/bash\n{full_cmd}\ntouch /tmp/{session}.exit\n"
|
||||||
|
Path(wrapper_path).write_text(wrapper_content, encoding="utf-8")
|
||||||
|
os.chmod(wrapper_path, 0o755)
|
||||||
|
|
||||||
|
MUSE_TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
log_path = MUSE_TMUX_LOG_DIR / f"{session}.log"
|
||||||
|
|
||||||
|
tmux_cmd = [
|
||||||
|
"tmux", "-S", "/tmp/tmux-muse.sock",
|
||||||
|
"new-session", "-d", "-s", session,
|
||||||
|
f"{wrapper_path} 2>&1 | tee {log_path}"
|
||||||
|
]
|
||||||
|
res = subprocess.run(tmux_cmd)
|
||||||
|
if res.returncode != 0:
|
||||||
|
print(f"Error launching tmux session: {res.stderr or 'failed'}", file=sys.stderr)
|
||||||
|
sys.exit(res.returncode or 1)
|
||||||
|
|
||||||
|
print(f"session: {session}")
|
||||||
|
print(f"attach: tmux -S /tmp/tmux-muse.sock attach -t {session}")
|
||||||
|
|
||||||
|
if auto_approve:
|
||||||
|
watch_session, watch_script = _write_watch_script(session)
|
||||||
|
watch_log = MUSE_TMUX_LOG_DIR / f"{watch_session}.log"
|
||||||
|
w_cmd = [
|
||||||
|
"tmux", "-S", "/tmp/tmux-muse.sock",
|
||||||
|
"new-session", "-d", "-s", watch_session,
|
||||||
|
f"{watch_script} 2>&1 | tee {watch_log}"
|
||||||
|
]
|
||||||
|
w_res = subprocess.run(w_cmd)
|
||||||
|
if w_res.returncode == 0:
|
||||||
|
print(f"watcher: {watch_session}")
|
||||||
|
print(f"watcher-log: {watch_log}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_watch(args):
|
||||||
|
"""Spawn an auto-approve watcher on an existing tmux session."""
|
||||||
|
if not shutil.which("tmux"):
|
||||||
|
print("tmux not found", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
raw_session = getattr(args, "session", None)
|
||||||
|
if not raw_session:
|
||||||
|
print("Error: session name required", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
session = _sanitize_tmux_name(raw_session)
|
||||||
|
|
||||||
|
has_res = subprocess.run(["tmux", "-S", "/tmp/tmux-muse.sock", "has-session", "-t", session])
|
||||||
|
if has_res.returncode != 0:
|
||||||
|
print(f"Error: session '{session}' does not exist", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
watch_session, watch_script = _write_watch_script(session)
|
||||||
|
MUSE_TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
watch_log = MUSE_TMUX_LOG_DIR / f"{watch_session}.log"
|
||||||
|
|
||||||
|
w_cmd = [
|
||||||
|
"tmux", "-S", "/tmp/tmux-muse.sock",
|
||||||
|
"new-session", "-d", "-s", watch_session,
|
||||||
|
f"{watch_script} 2>&1 | tee {watch_log}"
|
||||||
|
]
|
||||||
|
w_res = subprocess.run(w_cmd)
|
||||||
|
if w_res.returncode != 0:
|
||||||
|
print(f"Error starting watcher: {w_res.stderr or 'failed'}", file=sys.stderr)
|
||||||
|
sys.exit(w_res.returncode or 1)
|
||||||
|
|
||||||
|
print(f"watching: {session}")
|
||||||
|
print(f"watcher: {watch_session}")
|
||||||
|
print(f"watcher-log: {watch_log}")
|
||||||
|
|
||||||
|
|
||||||
def cmd_docs_dispatch(args):
|
def cmd_docs_dispatch(args):
|
||||||
"""Bridge 'box docs' and 'super docs' directly to bin/docs-lookup.py."""
|
"""Bridge 'box docs' and 'super docs' directly to bin/docs-lookup.py."""
|
||||||
d_args = getattr(args, "docs_args", []) or []
|
d_args = getattr(args, "docs_args", []) or []
|
||||||
@@ -5384,6 +5655,14 @@ def build_parser():
|
|||||||
p_rt_launch.add_argument("--dry-run", action="store_true", help="Print the launch plan without creating")
|
p_rt_launch.add_argument("--dry-run", action="store_true", help="Print the launch plan without creating")
|
||||||
p_rt_launch.add_argument("muse_args", nargs=argparse.REMAINDER, default=[], help="Extra muse args after --")
|
p_rt_launch.add_argument("muse_args", nargs=argparse.REMAINDER, default=[], help="Extra muse args after --")
|
||||||
|
|
||||||
|
p_rt_layout = rt_sub.add_parser("layout", parents=[common], help="Show pane geometry vs approval minimums")
|
||||||
|
p_rt_layout.add_argument("--socket", default=None, help="Only this tmux socket")
|
||||||
|
|
||||||
|
p_rt_spread = rt_sub.add_parser("spread", parents=[common], help="Break squeezed runtimes into own windows")
|
||||||
|
p_rt_spread.add_argument("--socket", default=None, help="Only this tmux socket")
|
||||||
|
p_rt_spread.add_argument("--session", default=None, help="Only this tmux session")
|
||||||
|
p_rt_spread.add_argument("--dry-run", action="store_true", help="Print the spread plan without moving panes")
|
||||||
|
|
||||||
# Domain: INVITE
|
# Domain: INVITE
|
||||||
p_invite = subparsers.add_parser("invite", parents=[common], help="Muse.ai invite codes: find per-agent codes and redeem")
|
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)")
|
p_invite.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node (status)")
|
||||||
|
|||||||
Reference in New Issue
Block a user