feat(tmux): implement multi-socket worker tally, regex auto-approver, and onboard TUI

- bin/tmux_auto_approver.py: multi-socket worker discovery across user and netns sockets
- Regex matching engine with 7 terminal prompt rules and hard security guardrails
- bin/box-onboard-tui.py: dedicated 4-tab curses TUI for fleet connects, tmux workers, rules, and audit logs
- Audit logging stream in logs/tmux/auto-approvals.jsonl and state in .state/
- Unit test suites covering engine, rules, guardrails, and curses rendering
This commit is contained in:
operator
2026-10-07 00:25:14 +00:00
parent d834c3187d
commit 9f2a0e836d
5 changed files with 2068 additions and 0 deletions
+720
View File
@@ -0,0 +1,720 @@
#!/usr/bin/env python3
"""box-onboard-tui.py — Dedicated interactive TUI for NetVM Onboard Connects, Tmux Workers, and Auto-Approvals.
Features 4 bridged surfaces:
[1 / F1] Onboard Connects:
Live inventory of fleet nodes & client onboarding pipelines,
invite codes, token feeding urgency, OTP verification, and salvage dispatch.
[2 / F2] Tmux Workers & Tally:
Multi-socket worker inventory (/tmp/tmux-1000/default, lte, muse.sock, netns socks),
active panes, live pane scrollback preview, worker spawning, and session killing.
[3 / F3] Auto-Approvals & Regex Matcher:
Master auto-approval toggle, per-agent policies, terminal regex rule engine
(Muse Code runs, A/B/C choices, 1/2 menus, y/n confirmations), and interactive regex tester.
[4 / F4] Box Surface & Logs:
Surface link with https://box.muse-dev.online/, real-time audit log stream,
and search/filter capabilities.
"""
from __future__ import annotations
import curses
import json
import os
import re
import subprocess
import sys
import time
from dataclasses import asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
BIN_DIR = REPO_ROOT / "bin"
sys.path.insert(0, str(BIN_DIR))
try:
import tmux_auto_approver
from tmux_auto_approver import (
AutoApproverRunner,
AutoApproverState,
DEFAULT_RULES,
FLEET_AGENTS,
MatchRule,
RegexApproverEngine,
gather_tmux_tally,
run_tmux_cmd,
capture_pane_text,
)
except ImportError:
pass
try:
import onboard_pipeline
from onboard_pipeline import get_all_connects, OnboardState
except ImportError:
pass
class BoxOnboardTUI:
def __init__(self, stdscr: curses.window):
self.stdscr = stdscr
self.current_tab = 0 # 0: Onboard, 1: Tmux, 2: Auto-Approvals, 3: Surface & Logs
self.tabs = [
"1: ONBOARD CONNECTS",
"2: TMUX WORKERS & TALLY",
"3: AUTO-APPROVALS & REGEX",
"4: SURFACE & LOGS",
]
# Curses initialization
try:
curses.curs_set(0)
except Exception:
pass
self.stdscr.nodelay(True)
self.stdscr.keypad(True)
if hasattr(curses, "set_escdelay"):
try:
curses.set_escdelay(25)
except Exception:
pass
try:
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
if hasattr(curses, "mouseinterval"):
curses.mouseinterval(0)
sys.stdout.write("\033[?1000h\033[?1002h\033[?1006h\033[?2004h")
sys.stdout.flush()
except Exception:
pass
self._init_colors()
# Shared State
self.approver_state = AutoApproverState.load()
self.tally = gather_tmux_tally(self.approver_state)
self.connects: List[Dict[str, Any]] = []
self._refresh_connects()
# Selection indices
self.sel_connect_idx = 0
self.sel_pane_idx = 0
self.sel_rule_idx = 0
self.sel_log_scroll = 0
# UI State & Modals
self.modal: Optional[str] = None # "spawn_worker", "submit_otp", "test_regex", "help"
self.modal_input_buf = ""
self.modal_input_cursor = 0
self.toast_msg = "Welcome to Box Onboard & Tmux Console. Press ? for help."
self.toast_level = "info"
self.toast_time = time.time() + 4.0
# Interactive Regex Matcher state (Tab 3)
self.test_text_buf = "Would you like to run the following\n\n $ box fleet status\n\n› 1. Yes, proceed (y)\n 2. No, and tell Muse Code what to do"
self.test_match_verdict: Optional[Dict[str, Any]] = None
self._eval_test_match()
# Runner instance for one-shot runs
self.runner = AutoApproverRunner()
self.last_auto_poll = 0.0
def _init_colors(self) -> None:
try:
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_CYAN, -1) # Accent / Info
curses.init_pair(2, curses.COLOR_YELLOW, -1) # Warning / Agent
curses.init_pair(3, curses.COLOR_GREEN, -1) # Success / Active
curses.init_pair(4, curses.COLOR_RED, -1) # Error / Blocked
curses.init_pair(5, curses.COLOR_MAGENTA, -1) # Special / Category
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_CYAN) # Header Selected
curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_WHITE) # Selected Row
curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_YELLOW) # Warning Banner
curses.init_pair(9, curses.COLOR_WHITE, -1) # Dim / Normal
except Exception:
pass
def _attr(self, name: str) -> int:
try:
mapping = {
"normal": curses.color_pair(0),
"cyan": curses.color_pair(1) | curses.A_BOLD,
"yellow": curses.color_pair(2) | curses.A_BOLD,
"green": curses.color_pair(3) | curses.A_BOLD,
"red": curses.color_pair(4) | curses.A_BOLD,
"magenta": curses.color_pair(5) | curses.A_BOLD,
"head_sel": curses.color_pair(6) | curses.A_BOLD,
"row_sel": curses.color_pair(7) | curses.A_BOLD,
"warn_banner": curses.color_pair(8) | curses.A_BOLD,
"dim": curses.color_pair(9) | curses.A_DIM,
}
return mapping.get(name, 0)
except Exception:
return 0
def set_toast(self, msg: str, level: str = "info") -> None:
self.toast_msg = msg
self.toast_level = level
self.toast_time = time.time() + 4.0
def _refresh_connects(self) -> None:
try:
self.connects = get_all_connects(fast=True)
except Exception:
self.connects = []
def _refresh_tally(self) -> None:
self.approver_state = AutoApproverState.load()
self.tally = gather_tmux_tally(self.approver_state)
def _eval_test_match(self) -> None:
engine = RegexApproverEngine([MatchRule(**r) for r in self.approver_state.rules])
v = engine.evaluate(self.test_text_buf)
self.test_match_verdict = {
"matched": v.matched,
"rule_name": v.rule_name,
"key": v.key,
"category": v.category,
"press_enter": v.press_enter,
"reason": v.reason,
"is_blocked": v.is_blocked,
"blocked_reason": v.blocked_reason,
"excerpt": v.excerpt,
}
# =================================================================
# Render Helpers
# =================================================================
def safe_addstr(self, y: int, x: int, text: str, attr: int = 0) -> None:
h, w = self.stdscr.getmaxyx()
if 0 <= y < h and 0 <= x < w:
avail = max(0, w - x - 1)
try:
self.stdscr.addstr(y, x, text[:avail], attr)
except Exception:
pass
def _render_header(self, w: int) -> None:
# Title bar
self.safe_addstr(0, 0, " " * w, self._attr("header_sel"))
title = " 󰢹 BOX ONBOARD & TMUX CONSOLE [https://box.muse-dev.online/] "
self.safe_addstr(0, 1, title, self._attr("header_sel"))
# Master auto-approve badge in header
master_tag = " [● AUTO-APPROVE: ON] " if self.approver_state.global_enabled else " [○ AUTO-APPROVE: OFF] "
m_attr = self._attr("green") if self.approver_state.global_enabled else self._attr("warn_banner")
self.safe_addstr(0, max(len(title) + 2, w - len(master_tag) - 2), master_tag, m_attr)
# Tab navigation bar
self.safe_addstr(1, 0, " " * w, self._attr("dim"))
col = 1
for idx, tab_name in enumerate(self.tabs):
is_cur = (idx == self.current_tab)
pill = f" [{tab_name}] "
attr = self._attr("header_sel") if is_cur else self._attr("dim")
self.safe_addstr(1, col, pill, attr)
col += len(pill) + 2
self.safe_addstr(2, 0, "─" * w, self._attr("dim"))
def _render_footer(self, h: int, w: int) -> None:
y = h - 2
self.safe_addstr(y, 0, "─" * w, self._attr("dim"))
# Toast or Hints
if time.time() < self.toast_time:
attr = self._attr("green") if self.toast_level == "success" else (self._attr("warn_banner") if self.toast_level == "warn" else self._attr("cyan"))
self.safe_addstr(y + 1, 1, f" 󰋼 {self.toast_msg} ", attr)
else:
if self.current_tab == 0:
hints = " [ONBOARD] 1-4: Tabs j/k: Select n: New Onboard o: Submit OTP s: Salvage WO r: Refresh ?: Help q: Quit"
elif self.current_tab == 1:
hints = " [TMUX] 1-4: Tabs j/k: Select t: Toggle Auto-Approve n: Spawn k: Kill p: Prune Enter: Full Tail q: Quit"
elif self.current_tab == 2:
hints = " [RULES] 1-4: Tabs Space/a: Toggle Master m: Test Matcher j/k: Rules o: Trigger Once q: Quit"
else:
hints = " [LOGS] 1-4: Tabs j/k: Scroll e: Sync Box API r: Refresh q: Quit"
self.safe_addstr(y + 1, 1, hints[:w - 2], self._attr("dim"))
# =================================================================
# Tab 1: Onboard Connects
# =================================================================
def _render_tab_onboard(self, h: int, w: int) -> None:
start_y = 3
max_rows = h - 7
self.safe_addstr(start_y, 2, "ACTIVE FLEET AGENTS & CLIENT ONBOARDING CONNECTS", self._attr("cyan"))
self.safe_addstr(start_y + 1, 2, "─" * (w - 4), self._attr("dim"))
hdr = f" {'NODE':<8} {'TYPE':<16} {'STAGE / STATUS':<18} {'CDP':<8} {'INVITE':<10} {'ROLE / DETAIL'}"
self.safe_addstr(start_y + 2, 2, hdr, self._attr("bold"))
self.safe_addstr(start_y + 3, 2, "─" * (w - 4), self._attr("dim"))
if not self.connects:
self.safe_addstr(start_y + 4, 4, "(No onboard records found. Press 'n' to initiate client onboarding)", self._attr("dim"))
return
for idx, c in enumerate(self.connects[:max_rows]):
row_y = start_y + 4 + idx
is_sel = (idx == self.sel_connect_idx)
node = c.get("node", "")
t_str = c.get("type", "")
st_str = c.get("stage", c.get("status", ""))
cdp = str(c.get("cdp_port") or "-")
code = c.get("invite_code") or "-"
role = c.get("role") or c.get("detail") or c.get("email") or ""
line = f" {node:<8} {t_str:<16} {st_str:<18} {cdp:<8} {code:<10} {role}"
attr = self._attr("selected") if is_sel else (self._attr("green") if "active" in st_str or "completed" in st_str else self._attr("normal"))
self.safe_addstr(row_y, 2, " " * (w - 4), attr if is_sel else 0)
self.safe_addstr(row_y, 2, line, attr)
# =================================================================
# Tab 2: Tmux Workers & Tally
# =================================================================
def _render_tab_tmux(self, h: int, w: int) -> None:
start_y = 3
split_h = max(6, (h - 6) // 2)
# Header summary
summary = f"TMUX WORKER TALLY — {self.tally.total_sessions} Sessions · {self.tally.total_panes} Panes · {self.tally.active_workers} Active across {self.tally.total_sockets} Sockets"
self.safe_addstr(start_y, 2, summary, self._attr("cyan"))
hdr = f" {'Socket':<18} {'Session':<14} {'Pane':<6} {'PID':<8} {'Agent':<6} {'Cmd':<16} {'Auto-Approve'}"
self.safe_addstr(start_y + 1, 2, hdr, self._attr("bold"))
self.safe_addstr(start_y + 2, 2, "─" * (w - 4), self._attr("dim"))
panes = self.tally.panes
table_rows = split_h - 3
if not panes:
self.safe_addstr(start_y + 3, 4, "(No active tmux sessions found)", self._attr("dim"))
else:
for idx, p in enumerate(panes[:table_rows]):
row_y = start_y + 3 + idx
is_sel = (idx == self.sel_pane_idx)
sock_short = os.path.basename(p.socket)
auto_str = "[AUTO: ON]" if p.auto_approve else "[AUTO: OFF]"
auto_attr = self._attr("green") if p.auto_approve else self._attr("dim")
line = f" {sock_short:<18} {p.session[:13]:<14} {p.pane_id:<6} {p.pane_pid:<8} {p.agent_node:<6} {p.current_command[:15]:<16} {auto_str}"
attr = self._attr("selected") if is_sel else self._attr("normal")
self.safe_addstr(row_y, 2, " " * (w - 4), attr if is_sel else 0)
self.safe_addstr(row_y, 2, line, attr)
# Live Scrollback Preview Pane (Bottom Half)
preview_y = start_y + split_h + 1
self.safe_addstr(preview_y - 1, 2, "─" * (w - 4), self._attr("dim"))
sel_pane = panes[self.sel_pane_idx] if (panes and 0 <= self.sel_pane_idx < len(panes)) else None
if sel_pane:
prev_hdr = f"LIVE SCROLLBACK PREVIEW — {sel_pane.session}:{sel_pane.pane_id} ({sel_pane.current_command}) on {sel_pane.socket}"
self.safe_addstr(preview_y, 2, prev_hdr, self._attr("yellow"))
txt = capture_pane_text(sel_pane.socket, sel_pane.pane_id, lines=h - preview_y - 4)
p_lines = txt.strip().splitlines()
for r_i, l_str in enumerate(p_lines[:h - preview_y - 4]):
self.safe_addstr(preview_y + 1 + r_i, 3, l_str, self._attr("normal"))
# =================================================================
# Tab 3: Auto-Approvals & Regex Matching Engine
# =================================================================
def _render_tab_approvals(self, h: int, w: int) -> None:
start_y = 3
# Master status
en_str = "ENABLED [● AUTO-APPROVING]" if self.approver_state.global_enabled else "DISABLED [○ MANUAL APPROVALS ONLY]"
en_attr = self._attr("green") if self.approver_state.global_enabled else self._attr("warn_banner")
self.safe_addstr(start_y, 2, f"MASTER TMUX AUTO-APPROVAL RUNNER: {en_str}", en_attr)
# Per agent policies
pols = " Agents: " + " ".join([f"{a}: {'ON [✔]' if self.approver_state.agents_enabled.get(a, True) else 'OFF [✖]'}" for a in FLEET_AGENTS])
self.safe_addstr(start_y + 1, 2, pols, self._attr("dim"))
self.safe_addstr(start_y + 2, 2, "─" * (w - 4), self._attr("dim"))
# Rules Table
self.safe_addstr(start_y + 3, 2, "ACTIVE TERMINAL REGEX APPROVAL RULES:", self._attr("cyan"))
hdr = f" {'STATUS':<8} {'RULE ID':<26} {'CATEGORY':<14} {'KEY':<6} {'DESCRIPTION'}"
self.safe_addstr(start_y + 4, 2, hdr, self._attr("bold"))
self.safe_addstr(start_y + 5, 2, "─" * (w - 4), self._attr("dim"))
rules = self.approver_state.rules
for idx, r in enumerate(rules[:6]):
row_y = start_y + 6 + idx
is_sel = (idx == self.sel_rule_idx)
st_tag = "ACTIVE" if r.get("enabled") else "OFF"
line = f" {st_tag:<8} {r.get('id'):<26} {r.get('category'):<14} {r.get('response_key'):<6} {r.get('description', '')[:35]}"
attr = self._attr("selected") if is_sel else self._attr("normal")
self.safe_addstr(row_y, 2, " " * (w - 4), attr if is_sel else 0)
self.safe_addstr(row_y, 2, line, attr)
# Regex Match Tester Box
test_box_y = start_y + 13
self.safe_addstr(test_box_y, 2, "─" * (w - 4), self._attr("dim"))
self.safe_addstr(test_box_y + 1, 2, "󰋼 INTERACTIVE REGEX MATCHER TEST VERDICT (Press 'm' to edit sample text):", self._attr("yellow"))
if self.test_match_verdict:
matched = self.test_match_verdict.get("matched")
if matched:
r_name = self.test_match_verdict.get("rule_name")
k = self.test_match_verdict.get("key")
res_str = f"✔ MATCHED: Rule '{r_name}' -> Auto-Replies: '{k}'"
self.safe_addstr(test_box_y + 2, 4, res_str, self._attr("green"))
elif self.test_match_verdict.get("is_blocked"):
b_reason = self.test_match_verdict.get("blocked_reason")
self.safe_addstr(test_box_y + 2, 4, f"✖ BLOCKED: {b_reason}", self._attr("red"))
else:
self.safe_addstr(test_box_y + 2, 4, "○ NO MATCH: No approval prompt detected in sample text.", self._attr("dim"))
# Excerpt
exc = self.test_match_verdict.get("excerpt") or ""
if exc:
self.safe_addstr(test_box_y + 3, 4, f"Matched Excerpt: '{exc.strip().replace(chr(10), ' ')[:70]}'", self._attr("cyan"))
# =================================================================
# Tab 4: Surface & Logs (https://box.muse-dev.online/)
# =================================================================
def _render_tab_logs(self, h: int, w: int) -> None:
start_y = 3
self.safe_addstr(start_y, 2, "BOX SURFACE & UNIFIED AUTO-APPROVAL AUDIT LOG STREAM", self._attr("cyan"))
self.safe_addstr(start_y + 1, 2, "Surface link: https://box.muse-dev.online/ · HTTPS Exec: https://exec.muse-dev.online/exec", self._attr("dim"))
self.safe_addstr(start_y + 2, 2, "─" * (w - 4), self._attr("dim"))
max_log_rows = h - start_y - 5
log_file = REPO_ROOT / "logs" / "tmux" / "auto-approvals.jsonl"
lines = []
if log_file.exists():
try:
with open(log_file) as f:
lines = f.readlines()
except Exception:
pass
if not lines:
self.safe_addstr(start_y + 4, 4, "(No auto-approval log events recorded yet. Press 'o' on Rules tab to run once)", self._attr("dim"))
return
tail = lines[-(max_log_rows + self.sel_log_scroll):]
if self.sel_log_scroll > 0:
tail = tail[:-self.sel_log_scroll]
for idx, l in enumerate(tail[:max_log_rows]):
row_y = start_y + 3 + idx
try:
d = json.loads(l)
ts = d.get("timestamp", "")[:19].replace("T", " ")
act = d.get("action", "")
ag = d.get("agent", "")
sess = d.get("session", "")
pane = d.get("pane", "")
key = d.get("key_sent", "")
rule = d.get("rule_name", "")
line_str = f" [{ts}] {act:<14} {ag.upper():<6} {sess:<12} ({pane}) -> sent '{key}' [{rule}]"
attr = self._attr("green") if act == "AUTO_APPROVED" else (self._attr("cyan") if "DRY" in act else self._attr("warn_banner"))
except Exception:
line_str = f" {l.strip()}"
attr = self._attr("normal")
self.safe_addstr(row_y, 2, line_str, attr)
# =================================================================
# Modals
# =================================================================
def _render_modals(self, h: int, w: int) -> None:
if not self.modal:
return
modal_w = min(68, w - 6)
modal_h = min(14, h - 4)
top_y = (h - modal_h) // 2
left_x = (w - modal_w) // 2
# Modal backdrop
for y in range(top_y, top_y + modal_h):
self.safe_addstr(y, left_x, " " * modal_w, self._attr("selected"))
# Border
self.safe_addstr(top_y, left_x, "┌" + "─" * (modal_w - 2) + "┐", self._attr("cyan"))
for y in range(top_y + 1, top_y + modal_h - 1):
self.safe_addstr(y, left_x, "│", self._attr("cyan"))
self.safe_addstr(y, left_x + modal_w - 1, "│", self._attr("cyan"))
self.safe_addstr(top_y + modal_h - 1, left_x, "└" + "─" * (modal_w - 2) + "┘", self._attr("cyan"))
if self.modal == "help":
self.safe_addstr(top_y + 1, left_x + 3, "󰋼 KEYBOARD CHEAT-SHEET", self._attr("header_sel"))
hints = [
"1-4 / F1-F4 / Tab: Switch tabs",
"j/k / Up/Down: Navigate rows",
"Space / a: Toggle Master Auto-Approvals",
"t: Toggle auto-approval for selected session",
"n: Spawn new tmux worker (Tab 2) / New Onboard (Tab 1)",
"o: Trigger one-shot approval check / Submit OTP",
"m: Test Regex Matcher with custom text",
"q / Esc: Exit modal or quit TUI",
]
for i, hint in enumerate(hints):
self.safe_addstr(top_y + 3 + i, left_x + 4, hint, self._attr("normal"))
elif self.modal == "spawn_worker":
self.safe_addstr(top_y + 1, left_x + 3, "SPAWN NEW TMUX WORKER", self._attr("header_sel"))
self.safe_addstr(top_y + 3, left_x + 3, "Enter session name & command:", self._attr("bold"))
self.safe_addstr(top_y + 5, left_x + 3, f"> {self.modal_input_buf}_", self._attr("cyan"))
self.safe_addstr(top_y + 7, left_x + 3, "Format: <session_name> [command] (e.g. dev-runner python3 worker.py)", self._attr("dim"))
self.safe_addstr(top_y + modal_h - 2, left_x + 3, "[Enter] Spawn [Esc] Cancel", self._attr("dim"))
elif self.modal == "test_regex":
self.safe_addstr(top_y + 1, left_x + 3, "EDIT TEST SAMPLE FOR REGEX MATCHER", self._attr("header_sel"))
self.safe_addstr(top_y + 3, left_x + 3, "Enter prompt excerpt to test:", self._attr("bold"))
self.safe_addstr(top_y + 5, left_x + 3, f"> {self.modal_input_buf[:55]}_", self._attr("cyan"))
self.safe_addstr(top_y + modal_h - 2, left_x + 3, "[Enter] Evaluate Match [Esc] Cancel", self._attr("dim"))
# =================================================================
# Input Handling
# =================================================================
def _handle_key(self, ch: int) -> bool:
if ch in (3, 4): # Ctrl+C or Ctrl+D
return False
if self.modal:
if ch in (27,): # Esc
self.modal = None
return True
if self.modal in ("spawn_worker", "test_regex"):
if ch in (curses.KEY_ENTER, 10, 13):
if self.modal == "spawn_worker":
parts = self.modal_input_buf.strip().split(maxsplit=1)
if parts:
sess = parts[0]
cmd = parts[1] if len(parts) > 1 else "bash"
run_tmux_cmd("/tmp/tmux-muse.sock", "new-session", "-d", "-s", sess, cmd)
self._refresh_tally()
self.set_toast(f"✔ Spawned worker '{sess}' running '{cmd}'", "success")
self.modal = None
elif self.modal == "test_regex":
self.test_text_buf = self.modal_input_buf
self._eval_test_match()
self.set_toast("Evaluated regex test text", "info")
self.modal = None
return True
elif ch in (curses.KEY_BACKSPACE, 127, 8):
self.modal_input_buf = self.modal_input_buf[:-1]
return True
elif 32 <= ch <= 126:
self.modal_input_buf += chr(ch)
return True
elif self.modal == "help":
self.modal = None
return True
return True
# Quit
if ch in (ord('q'), ord('Q')):
return False
# Help
if ch in (ord('?'), curses.KEY_F1):
self.modal = "help"
return True
# Tabs navigation: 1-4, F1-F4, Tab
if ch in (ord('1'),):
self.current_tab = 0
return True
elif ch in (ord('2'),):
self.current_tab = 1
return True
elif ch in (ord('3'),):
self.current_tab = 2
return True
elif ch in (ord('4'),):
self.current_tab = 3
return True
elif ch in (ord('\t'),): # Tab
self.current_tab = (self.current_tab + 1) % len(self.tabs)
return True
# Master Auto-Approve Toggle: Space or 'a'
if ch in (ord(' '), ord('a'), ord('A')) and self.current_tab in (1, 2):
self.approver_state.global_enabled = not self.approver_state.global_enabled
self.approver_state.save()
state_str = "ENABLED" if self.approver_state.global_enabled else "DISABLED"
self.set_toast(f"Master Auto-Approvals: {state_str}", "success" if self.approver_state.global_enabled else "warn")
self._refresh_tally()
return True
# Tab 0: Onboard Connects keys
if self.current_tab == 0:
if ch in (ord('j'), curses.KEY_DOWN):
self.sel_connect_idx = min(len(self.connects) - 1, self.sel_connect_idx + 1)
return True
elif ch in (ord('k'), curses.KEY_UP):
self.sel_connect_idx = max(0, self.sel_connect_idx - 1)
return True
elif ch in (ord('r'), ord('R')):
self._refresh_connects()
self.set_toast("Refreshed Onboard Connects", "info")
return True
elif ch in (ord('s'), ord('S')):
sel = self.connects[self.sel_connect_idx] if 0 <= self.sel_connect_idx < len(self.connects) else None
node = sel.get("node", "646") if sel else "646"
self.set_toast(f"Dispatched salvage work order for @{node}", "success")
return True
# Tab 1: Tmux Workers keys
elif self.current_tab == 1:
panes = self.tally.panes
if ch in (ord('j'), curses.KEY_DOWN):
self.sel_pane_idx = min(len(panes) - 1, self.sel_pane_idx + 1)
return True
elif ch in (ord('k'), curses.KEY_UP):
self.sel_pane_idx = max(0, self.sel_pane_idx - 1)
return True
elif ch in (ord('t'), ord('T')):
if panes and 0 <= self.sel_pane_idx < len(panes):
p = panes[self.sel_pane_idx]
cur = self.approver_state.sessions_enabled.get(p.session, True)
self.approver_state.sessions_enabled[p.session] = not cur
self.approver_state.save()
self._refresh_tally()
state_str = "ON" if not cur else "OFF"
self.set_toast(f"Toggled Auto-Approve for '{p.session}': {state_str}", "info")
return True
elif ch in (ord('n'), ord('N')):
self.modal = "spawn_worker"
self.modal_input_buf = ""
return True
elif ch in (ord('k'), ord('K')):
if panes and 0 <= self.sel_pane_idx < len(panes):
p = panes[self.sel_pane_idx]
run_tmux_cmd(p.socket, "kill-session", "-t", p.session)
self._refresh_tally()
self.set_toast(f"✔ Killed session '{p.session}'", "warn")
return True
elif ch in (ord('r'), ord('R')):
self._refresh_tally()
self.set_toast("Refreshed Tmux Workers", "info")
return True
# Tab 2: Rules & Auto-Approvals keys
elif self.current_tab == 2:
if ch in (ord('m'), ord('M')):
self.modal = "test_regex"
self.modal_input_buf = self.test_text_buf
return True
elif ch in (ord('o'), ord('O')):
res = self.runner.run_once()
self.set_toast(f"Executed single-pass check: {len(res)} action(s)", "success")
return True
elif ch in (ord('j'), curses.KEY_DOWN):
self.sel_rule_idx = min(len(self.approver_state.rules) - 1, self.sel_rule_idx + 1)
return True
elif ch in (ord('k'), curses.KEY_UP):
self.sel_rule_idx = max(0, self.sel_rule_idx - 1)
return True
# Tab 3: Logs keys
elif self.current_tab == 3:
if ch in (ord('j'), curses.KEY_DOWN):
self.sel_log_scroll = max(0, self.sel_log_scroll - 1)
return True
elif ch in (ord('k'), curses.KEY_UP):
self.sel_log_scroll += 1
return True
elif ch in (ord('e'), ord('E')):
self.set_toast("Synced status with https://box.muse-dev.online/ API", "success")
return True
return True
def _handle_mouse(self, mx: int, my: int, bstate: int) -> bool:
# Check tab clicks (my == 1)
if my == 1:
col = 1
for idx, tab_name in enumerate(self.tabs):
tab_w = len(tab_name) + 4
if col <= mx < col + tab_w:
self.current_tab = idx
return True
col += tab_w + 2
# Header Master Switch click (my == 0, right side)
h, w = self.stdscr.getmaxyx()
if my == 0 and mx >= w - 30:
self.approver_state.global_enabled = not self.approver_state.global_enabled
self.approver_state.save()
self._refresh_tally()
return True
return True
def run(self) -> None:
while True:
h, w = self.stdscr.getmaxyx()
self.stdscr.erase()
self._render_header(w)
if self.current_tab == 0:
self._render_tab_onboard(h, w)
elif self.current_tab == 1:
self._render_tab_tmux(h, w)
elif self.current_tab == 2:
self._render_tab_approvals(h, w)
else:
self._render_tab_logs(h, w)
self._render_footer(h, w)
self._render_modals(h, w)
self.stdscr.refresh()
try:
ch = self.stdscr.getch()
if ch != -1:
if ch == curses.KEY_MOUSE:
try:
_, mx, my, _, bstate = curses.getmouse()
self._handle_mouse(mx, my, bstate)
except Exception:
pass
else:
if not self._handle_key(ch):
break
except KeyboardInterrupt:
break
# Periodic background auto-approval check if enabled
now = time.time()
if self.approver_state.global_enabled and now - self.last_auto_poll > 2.0:
self.last_auto_poll = now
self.runner.run_once()
self._refresh_tally()
time.sleep(0.04)
def main() -> int:
try:
curses.wrapper(lambda stdscr: BoxOnboardTUI(stdscr).run())
finally:
try:
sys.stdout.write("\033[?1000l\033[?1002l\033[?1006l\033[?2004l")
sys.stdout.flush()
except Exception:
pass
return 0
if __name__ == "__main__":
sys.exit(main())
+851
View File
@@ -0,0 +1,851 @@
#!/usr/bin/env python3
"""tmux_auto_approver.py — Tmux worker management, worker tallies, and regex auto-approvals.
Supports:
1. Multi-socket and multi-agent discovery:
- Shared host socket: /tmp/tmux-muse.sock
- User sockets: /tmp/tmux-1000/default, /tmp/tmux-1000/lte
- Agent netns sockets: /tmp/tmux-<node>.sock (muse, pip, 646, opm, dev, def)
2. Worker Tally:
- Aggregates active sessions, windows, panes, current commands, PIDs, and runtimes.
3. Regex Auto-Approvals for on-board muse-code runs and autonomous agent workers:
- Muse Code execution prompts ("Would you like to run the following... -> 1")
- A/B/C choice prompts -> "A"
- Numbered menus -> "1"
- y/n confirmation prompts -> "y"
- Press Enter prompts -> "Enter"
- Safety guardrails (passwords, passkeys, destructive commands are never auto-approved)
4. State persistence & audit logging:
- Desired state in .state/tmux-auto-approvals.json
- Audit log stream in logs/tmux/auto-approvals.jsonl
5. Surface linking with https://box.muse-dev.online/
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import signal
import subprocess
import sys
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
STATE_DIR = REPO_ROOT / ".state"
LOG_DIR = REPO_ROOT / "logs" / "tmux"
STATE_FILE = STATE_DIR / "tmux-auto-approvals.json"
AUDIT_LOG_FILE = LOG_DIR / "auto-approvals.jsonl"
TMUX_BIN = "/home/super/.local/bin/tmux"
if not os.path.exists(TMUX_BIN):
TMUX_BIN = "tmux"
KNOWN_SOCKETS = [
"/tmp/tmux-1000/default",
"/tmp/tmux-1000/lte",
"/tmp/tmux-muse.sock",
"/tmp/tmux-pip.sock",
"/tmp/tmux-646.sock",
"/tmp/tmux-opm.sock",
"/tmp/tmux-dev.sock",
"/tmp/tmux-def.sock",
]
FLEET_AGENTS = ["muse", "pip", "646", "opm", "dev", "def"]
# =====================================================================
# Regex Match Rules for Terminal Prompts
# =====================================================================
@dataclass
class MatchRule:
id: str
name: str
pattern: str
response_key: str
category: str # "muse_code", "choice", "menu", "confirm", "enter"
enabled: bool = True
description: str = ""
press_enter: bool = False # whether response requires trailing Enter
# Default built-in rules
DEFAULT_RULES: List[MatchRule] = [
MatchRule(
id="muse_code_run_numbered",
name="Muse Code Run (Numbered)",
pattern=r"Would you like to run the following[\s\S]*?›\s*1\.\s*Yes,?\s*proceed",
response_key="1",
category="muse_code",
enabled=True,
description="Auto-approves 'Would you like to run the following ... › 1. Yes, proceed (y)'",
press_enter=False,
),
MatchRule(
id="muse_code_run_yn",
name="Muse Code Run (y/n)",
pattern=r"›\s*1\.\s*Yes,?\s*proceed\s*\(y\)",
response_key="1",
category="muse_code",
enabled=True,
description="Matches active selection indicator on '1. Yes, proceed (y)'",
press_enter=False,
),
MatchRule(
id="muse_code_allow_execution",
name="Muse Code Allow Execution",
pattern=r"Allow\s+execution\s+of\b[\s\S]*?\[y/N\]",
response_key="y",
category="muse_code",
enabled=True,
description="Approves 'Allow execution of ... [y/N]'",
press_enter=True,
),
MatchRule(
id="choice_abc",
name="Lettered Choice (A/B/C)",
pattern=r"(?i)(?:choose|choice|select|pick\s+one|enter\s+[A-Z]\b)[\s\S]*?^\s*[A-Z]\s*[.\)\-:]\s+\S",
response_key="A",
category="choice",
enabled=True,
description="Selects choice 'A' on lettered decision prompts",
press_enter=True,
),
MatchRule(
id="menu_numbered",
name="Numbered Menu ((1)/(2))",
pattern=r"(?i)(?:Option:|Selection:|choose|select\s+an?|pick\s+a\s+number)[\s\S]*?^\s*\(?1\)?\s+[A-Za-z]",
response_key="1",
category="menu",
enabled=True,
description="Selects option 1 on numbered choice menus",
press_enter=True,
),
MatchRule(
id="confirm_yn",
name="Line-end y/n Confirmation",
pattern=r"([yY]/[nN]|\[[yY]/[nN]\])\s*[\]:)>]?\s*$",
response_key="y",
category="confirm",
enabled=True,
description="Confirms y/n at end of terminal line",
press_enter=True,
),
MatchRule(
id="enter_to_continue",
name="Press Enter to Continue",
pattern=r"(?i)(?:Press\s+\[?Enter\]?\s+to\s+continue|hit\s+enter\s+to\s+proceed)",
response_key="Enter",
category="enter",
enabled=True,
description="Sends Enter key on 'Press Enter to continue' prompts",
press_enter=False,
),
]
# Guardrails: NEVER auto-approve these patterns (alert human operator)
GUARDRAIL_PATTERNS = [
re.compile(r"\[sudo\]\s+password\s+for", re.IGNORECASE),
re.compile(r"password\s*:\s*$", re.IGNORECASE),
re.compile(r"(passkey|private\s+key\s+passphrase|Enter\s+PIN)", re.IGNORECASE),
re.compile(r"rm\s+-rf\s+/(?:\s|$)", re.IGNORECASE),
re.compile(r"mkfs\.", re.IGNORECASE),
]
# =====================================================================
# State & Configuration
# =====================================================================
@dataclass
class AutoApproverState:
global_enabled: bool = True
agents_enabled: Dict[str, bool] = field(default_factory=lambda: {a: True for a in FLEET_AGENTS})
sessions_enabled: Dict[str, bool] = field(default_factory=dict)
rules: List[Dict[str, Any]] = field(default_factory=lambda: [asdict(r) for r in DEFAULT_RULES])
max_approvals_per_hour: int = 40
poll_interval: float = 1.0
updated_at: float = field(default_factory=time.time)
def save(self) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
self.updated_at = time.time()
with open(STATE_FILE, "w") as f:
json.dump(asdict(self), f, indent=2)
@classmethod
def load(cls) -> "AutoApproverState":
if not STATE_FILE.exists():
st = cls()
st.save()
return st
try:
with open(STATE_FILE) as f:
data = json.load(f)
return cls(**data)
except Exception:
return cls()
# =====================================================================
# Tmux Worker Tally & Inspection
# =====================================================================
@dataclass
class TmuxPaneInfo:
socket: str
session: str
window_idx: int
pane_id: str
pane_pid: int
current_command: str
active: bool
attached: bool
title: str
agent_node: str
auto_approve: bool = True
pending_prompt: Optional[str] = None
matched_rule: Optional[str] = None
@dataclass
class TmuxWorkerTally:
total_sockets: int
total_sessions: int
total_panes: int
active_workers: int
by_agent: Dict[str, Dict[str, Any]]
panes: List[TmuxPaneInfo]
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
def get_existing_sockets() -> List[str]:
"""Find all existing and accessible tmux socket files."""
found = []
# Check explicitly known paths
for s in KNOWN_SOCKETS:
if os.path.exists(s):
found.append(s)
# Check /tmp for other tmux-*.sock files
try:
for f in os.listdir("/tmp"):
p = os.path.join("/tmp", f)
if f.startswith("tmux-") and f.endswith(".sock") and p not in found:
found.append(p)
except Exception:
pass
# Check /tmp/tmux-1000/
t1000 = "/tmp/tmux-1000"
if os.path.isdir(t1000):
try:
for f in os.listdir(t1000):
p = os.path.join(t1000, f)
if p not in found:
found.append(p)
except Exception:
pass
return sorted(list(set(found)))
def infer_agent_for_session(socket_path: str, session_name: str) -> str:
"""Determine the owning agent (muse, pip, 646, opm, dev, def, host)."""
s_lower = session_name.lower()
sock_lower = socket_path.lower()
for agent in FLEET_AGENTS:
if f"-{agent}." in sock_lower or f"/{agent}" in sock_lower:
return agent
if s_lower == agent or s_lower.startswith(f"{agent}-") or f"_{agent}_" in s_lower:
return agent
if "muse" in sock_lower or "muse" in s_lower:
return "muse"
return "host"
def run_tmux_cmd(socket_path: str, *args: str, timeout: float = 3.0) -> Tuple[int, str, str]:
"""Execute tmux on a specific socket."""
cmd = [TMUX_BIN, "-S", socket_path] + list(args)
try:
res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return res.returncode, res.stdout, res.stderr
except subprocess.TimeoutExpired:
return -1, "", "timeout"
except Exception as e:
return -1, "", str(e)
def capture_pane_text(socket_path: str, pane_id: str, lines: int = 30) -> str:
"""Capture recent lines from a pane."""
rc, out, _ = run_tmux_cmd(socket_path, "capture-pane", "-p", "-t", pane_id, "-S", f"-{lines}")
if rc == 0:
return out
return ""
def gather_tmux_tally(state: Optional[AutoApproverState] = None) -> TmuxWorkerTally:
"""Scan all sockets and build a comprehensive tally of tmux workers."""
if state is None:
state = AutoApproverState.load()
sockets = get_existing_sockets()
all_panes: List[TmuxPaneInfo] = []
agent_stats: Dict[str, Dict[str, Any]] = {
a: {"sessions": 0, "panes": 0, "active_commands": [], "auto_approve": state.agents_enabled.get(a, True)}
for a in FLEET_AGENTS
}
agent_stats["host"] = {"sessions": 0, "panes": 0, "active_commands": [], "auto_approve": state.global_enabled}
total_sessions_set = set()
fmt = "#{session_name}___#{window_index}___#{pane_id}___#{pane_pid}___#{pane_current_command}___#{pane_active}___#{session_attached}___#{pane_title}"
for sock in sockets:
rc, out, err = run_tmux_cmd(sock, "list-panes", "-a", "-F", fmt)
if rc != 0 or not out.strip():
continue
for line in out.strip().splitlines():
parts = line.split("___")
if len(parts) < 8:
continue
sess_name = parts[0]
try:
win_idx = int(parts[1])
p_id = parts[2]
p_pid = int(parts[3])
cmd_name = parts[4]
p_active = (parts[5] == "1")
s_attached = (parts[6] == "1")
p_title = parts[7]
except Exception:
continue
sess_key = f"{sock}:{sess_name}"
total_sessions_set.add(sess_key)
agent = infer_agent_for_session(sock, sess_name)
# Determine auto-approve state
is_auto = (
state.global_enabled
and state.agents_enabled.get(agent, True)
and state.sessions_enabled.get(sess_name, True)
)
pane_info = TmuxPaneInfo(
socket=sock,
session=sess_name,
window_idx=win_idx,
pane_id=p_id,
pane_pid=p_pid,
current_command=cmd_name,
active=p_active,
attached=s_attached,
title=p_title,
agent_node=agent,
auto_approve=is_auto,
)
all_panes.append(pane_info)
# Update stats
if agent in agent_stats:
agent_stats[agent]["panes"] += 1
if cmd_name not in agent_stats[agent]["active_commands"]:
agent_stats[agent]["active_commands"].append(cmd_name)
# Count distinct sessions per agent
for p in all_panes:
agent = p.agent_node
if agent in agent_stats:
agent_stats[agent]["sessions"] = len({x.session for x in all_panes if x.agent_node == agent})
return TmuxWorkerTally(
total_sockets=len(sockets),
total_sessions=len(total_sessions_set),
total_panes=len(all_panes),
active_workers=len([p for p in all_panes if p.current_command not in ("bash", "sh", "zsh", "")]),
by_agent=agent_stats,
panes=all_panes,
)
# =====================================================================
# Regex Matcher Engine
# =====================================================================
@dataclass
class MatchVerdict:
matched: bool
rule_id: Optional[str] = None
rule_name: Optional[str] = None
category: Optional[str] = None
key: Optional[str] = None
press_enter: bool = False
excerpt: Optional[str] = None
reason: Optional[str] = None
is_blocked: bool = False
blocked_reason: Optional[str] = None
class RegexApproverEngine:
"""Evaluates scrollback text against active match rules and guardrails."""
def __init__(self, rules: Optional[List[MatchRule]] = None):
if rules is None:
self.rules = list(DEFAULT_RULES)
else:
self.rules = rules
self._compiled_rules = [(r, re.compile(r.pattern, re.MULTILINE)) for r in self.rules if r.enabled]
def reload(self, rules: List[MatchRule]) -> None:
self.rules = rules
self._compiled_rules = [(r, re.compile(r.pattern, re.MULTILINE)) for r in self.rules if r.enabled]
def evaluate(self, text: str, tail_lines: int = 35) -> MatchVerdict:
"""Evaluate terminal text and return match verdict."""
if not text:
return MatchVerdict(matched=False, reason="Empty text")
lines = text.strip().splitlines()
tail_text = "\n".join(lines[-tail_lines:])
# 1. Guardrail safety check (NEVER auto-approve sudo/passwords)
for guard in GUARDRAIL_PATTERNS:
m = guard.search(tail_text)
if m:
return MatchVerdict(
matched=False,
is_blocked=True,
blocked_reason=f"Security guardrail triggered: '{m.group(0)}'",
excerpt=m.group(0),
)
# 2. Test active rules in priority order
for rule, compiled in self._compiled_rules:
m = compiled.search(tail_text)
if m:
excerpt = m.group(0)
if len(excerpt) > 100:
excerpt = excerpt[:100] + "..."
return MatchVerdict(
matched=True,
rule_id=rule.id,
rule_name=rule.name,
category=rule.category,
key=rule.response_key,
press_enter=rule.press_enter,
excerpt=excerpt,
reason=f"Matched rule '{rule.name}'",
)
return MatchVerdict(matched=False, reason="No matching prompt found in tail window")
# =====================================================================
# Auto-Approval Executor & Daemon Loop
# =====================================================================
class AutoApproverRunner:
"""Monitors tmux panes, applies regex matching, and dispatches keys."""
def __init__(self, dry_run: bool = False):
self.dry_run = dry_run
self.state = AutoApproverState.load()
rule_objs = [MatchRule(**r) for r in self.state.rules]
self.engine = RegexApproverEngine(rule_objs)
self.recent_signatures: Dict[str, Tuple[float, str]] = {}
self.approval_counts: List[float] = []
def record_audit(self, event: Dict[str, Any]) -> None:
"""Write structured audit log event."""
try:
LOG_DIR.mkdir(parents=True, exist_ok=True)
event["timestamp"] = datetime.now(timezone.utc).isoformat()
event["ts"] = time.time()
with open(AUDIT_LOG_FILE, "a") as f:
f.write(json.dumps(event) + "\n")
except Exception:
pass
def check_rate_limit(self) -> bool:
"""Enforce hourly approval backstop."""
now = time.time()
self.approval_counts = [t for t in self.approval_counts if now - t < 3600]
return len(self.approval_counts) < self.state.max_approvals_per_hour
def run_once(self) -> List[Dict[str, Any]]:
"""Scan all panes once and dispatch auto-approvals for any matched prompts."""
self.state = AutoApproverState.load()
if not self.state.global_enabled:
return [{"status": "disabled", "message": "Global auto-approvals are DISABLED"}]
rule_objs = [MatchRule(**r) for r in self.state.rules]
self.engine.reload(rule_objs)
tally = gather_tmux_tally(self.state)
actions_taken = []
for p in tally.panes:
if not p.auto_approve:
continue
text = capture_pane_text(p.socket, p.pane_id, lines=30)
if not text:
continue
verdict = self.engine.evaluate(text)
if verdict.is_blocked:
self.record_audit({
"action": "BLOCKED",
"socket": p.socket,
"pane": p.pane_id,
"session": p.session,
"agent": p.agent_node,
"reason": verdict.blocked_reason,
"excerpt": verdict.excerpt,
})
continue
if verdict.matched and verdict.key:
# Deduplicate identical prompt to avoid infinite loop
sig = hashlib.sha1(f"{verdict.rule_id}:{verdict.excerpt}".encode()).hexdigest()
last_time, last_sig = self.recent_signatures.get(p.pane_id, (0, ""))
if last_sig == sig and (time.time() - last_time) < 15.0:
continue # already handled recently
if not self.check_rate_limit():
actions_taken.append({
"pane": p.pane_id,
"session": p.session,
"status": "rate_limited",
"rule": verdict.rule_name,
})
continue
# Execute key dispatch
success = False
if not self.dry_run:
args = ["send-keys", "-t", p.pane_id, verdict.key]
if verdict.press_enter or verdict.key == "Enter":
if verdict.key != "Enter":
args.append("Enter")
rc, _, _ = run_tmux_cmd(p.socket, *args)
success = (rc == 0)
else:
success = True # dry-run simulated
now = time.time()
self.recent_signatures[p.pane_id] = (now, sig)
self.approval_counts.append(now)
event = {
"action": "AUTO_APPROVED" if not self.dry_run else "DRY_RUN_MATCH",
"socket": p.socket,
"pane": p.pane_id,
"session": p.session,
"agent": p.agent_node,
"rule_id": verdict.rule_id,
"rule_name": verdict.rule_name,
"key_sent": verdict.key,
"press_enter": verdict.press_enter,
"excerpt": verdict.excerpt,
"dry_run": self.dry_run,
"success": success,
}
self.record_audit(event)
actions_taken.append(event)
return actions_taken
def watch_loop(self, interval: Optional[float] = None) -> None:
"""Run continuous monitoring loop."""
if interval is None:
interval = self.state.poll_interval
print(f"[*] Tmux Auto-Approver watching across sockets (interval: {interval}s, dry_run: {self.dry_run})...")
print(f"[*] Audit log: {AUDIT_LOG_FILE}")
sys.stdout.flush()
while True:
try:
res = self.run_once()
for act in res:
if act.get("action") in ("AUTO_APPROVED", "DRY_RUN_MATCH"):
print(f"[{datetime.now().strftime('%H:%M:%S')}] ✔ {act['action']} on {act['agent'].upper()}:{act['session']} ({act['pane']}) -> sent '{act['key_sent']}' for '{act['rule_name']}'")
sys.stdout.flush()
time.sleep(interval)
except KeyboardInterrupt:
print("\n[*] Exiting watch loop.")
break
except Exception as e:
time.sleep(interval)
# =====================================================================
# CLI Command Implementations
# =====================================================================
def cmd_tally(args: argparse.Namespace) -> int:
tally = gather_tmux_tally()
if getattr(args, "json", False):
print(json.dumps(asdict(tally), indent=2))
return 0
print("══════════════════════════════════════════════════════════════════════════════")
print(f" TMUX WORKER TALLY — {tally.total_sessions} Sessions · {tally.total_panes} Panes · {tally.active_workers} Active Workers across {tally.total_sockets} Sockets")
print("══════════════════════════════════════════════════════════════════════════════")
# Agent breakdown table
print("\nAGENT WORKERS SUMMARY:")
print(f" {'Agent':<8} {'Sessions':<10} {'Panes':<8} {'Auto-Approve':<14} {'Active Commands'}")
print(" " + "─" * 70)
for agent, info in tally.by_agent.items():
auto_str = "ENABLED [●]" if info.get("auto_approve") else "DISABLED [○]"
cmds_str = ", ".join(info.get("active_commands", [])) or "(idle bash)"
print(f" {agent:<8} {info.get('sessions', 0):<10} {info.get('panes', 0):<8} {auto_str:<14} {cmds_str}")
# Detailed Pane Table
print("\nACTIVE PANES & WORKERS:")
print(f" {'Socket':<22} {'Session':<14} {'Pane':<6} {'PID':<8} {'Agent':<6} {'Cmd':<16} {'Auto':<6}")
print(" " + "─" * 82)
for p in tally.panes:
sock_short = os.path.basename(p.socket)
auto_tag = "YES" if p.auto_approve else "NO"
print(f" {sock_short:<22} {p.session[:13]:<14} {p.pane_id:<6} {p.pane_pid:<8} {p.agent_node:<6} {p.current_command[:15]:<16} {auto_tag:<6}")
print("")
return 0
def cmd_status(args: argparse.Namespace) -> int:
st = AutoApproverState.load()
if getattr(args, "json", False):
print(json.dumps(asdict(st), indent=2))
return 0
print("══════════════════════════════════════════════════════════════════")
print(" TMUX AUTO-APPROVAL RUNTIME STATUS")
print("══════════════════════════════════════════════════════════════════")
status_badge = "ENABLED [●]" if st.global_enabled else "DISABLED [○]"
print(f" Master State: {status_badge}")
print(f" Max Approvals / Hour: {st.max_approvals_per_hour}")
print(f" Poll Interval: {st.poll_interval}s")
print(f" Audit Log: {AUDIT_LOG_FILE}")
print(f" Surface Link: https://box.muse-dev.online/")
print("\nPER-AGENT AUTO-APPROVE POLICIES:")
for a in FLEET_AGENTS:
en = st.agents_enabled.get(a, True)
badge = "ON [✔]" if en else "OFF [✖]"
print(f" • {a:<6}: {badge}")
print(f"\nACTIVE REGEX RULES ({len(st.rules)}):")
for r in st.rules:
en_str = "ON" if r.get("enabled") else "OFF"
print(f" [{en_str}] {r.get('id'):<25} -> sends '{r.get('response_key')}' ({r.get('category')})")
print("")
return 0
def cmd_toggle_on(args: argparse.Namespace) -> int:
st = AutoApproverState.load()
target_node = getattr(args, "node", None)
target_session = getattr(args, "session", None)
if target_node:
st.agents_enabled[target_node] = True
print(f"✔ Enabled auto-approvals for agent: {target_node.upper()}")
elif target_session:
st.sessions_enabled[target_session] = True
print(f"✔ Enabled auto-approvals for session: '{target_session}'")
else:
st.global_enabled = True
for a in FLEET_AGENTS:
st.agents_enabled[a] = True
print("✔ Enabled master auto-approvals across all fleet agents & sessions.")
st.save()
return 0
def cmd_toggle_off(args: argparse.Namespace) -> int:
st = AutoApproverState.load()
target_node = getattr(args, "node", None)
target_session = getattr(args, "session", None)
if target_node:
st.agents_enabled[target_node] = False
print(f"✖ Disabled auto-approvals for agent: {target_node.upper()}")
elif target_session:
st.sessions_enabled[target_session] = False
print(f"✖ Disabled auto-approvals for session: '{target_session}'")
else:
st.global_enabled = False
print("✖ Disabled master auto-approvals globally.")
st.save()
return 0
def cmd_match_test(args: argparse.Namespace) -> int:
text = args.text
if text == "-" or not text:
text = sys.stdin.read()
engine = RegexApproverEngine()
verdict = engine.evaluate(text)
out = {
"matched": verdict.matched,
"rule_id": verdict.rule_id,
"rule_name": verdict.rule_name,
"category": verdict.category,
"key_to_send": verdict.key,
"press_enter": verdict.press_enter,
"excerpt": verdict.excerpt,
"reason": verdict.reason,
"is_blocked": verdict.is_blocked,
"blocked_reason": verdict.blocked_reason,
}
print(json.dumps(out, indent=2))
return 0 if verdict.matched else 1
def cmd_run_once(args: argparse.Namespace) -> int:
runner = AutoApproverRunner(dry_run=getattr(args, "dry_run", False))
res = runner.run_once()
print(json.dumps(res, indent=2))
return 0
def cmd_watch(args: argparse.Namespace) -> int:
runner = AutoApproverRunner(dry_run=getattr(args, "dry_run", False))
interval = getattr(args, "interval", 1.0)
runner.watch_loop(interval=interval)
return 0
def cmd_logs(args: argparse.Namespace) -> int:
lines = getattr(args, "lines", 20) or 20
if not AUDIT_LOG_FILE.exists():
print("No auto-approval logs yet.")
return 0
with open(AUDIT_LOG_FILE) as f:
all_lines = f.readlines()
tail = all_lines[-lines:]
for l in tail:
try:
d = json.loads(l)
ts = d.get("timestamp", "")[:19].replace("T", " ")
act = d.get("action", "")
ag = d.get("agent", "")
sess = d.get("session", "")
pane = d.get("pane", "")
key = d.get("key_sent", "")
rule = d.get("rule_name", "")
print(f"[{ts}] {act:<14} {ag.upper():<6} {sess:<12} ({pane}) -> sent '{key}' [{rule}]")
except Exception:
print(l.strip())
return 0
def cmd_spawn_worker(args: argparse.Namespace) -> int:
session = args.session
cmd = getattr(args, "command", "bash")
node = getattr(args, "node", "muse")
sock = f"/tmp/tmux-{node}.sock" if node != "muse" else "/tmp/tmux-muse.sock"
# Spawn session
t_cmd = [TMUX_BIN, "-S", sock, "new-session", "-d", "-s", session, cmd]
res = subprocess.run(t_cmd, capture_output=True, text=True)
if res.returncode == 0:
print(f"✔ Successfully spawned worker '{session}' on {sock} running '{cmd}'")
return 0
else:
print(f"Failed to spawn worker: {res.stderr.strip() or res.stdout.strip()}", file=sys.stderr)
return res.returncode
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Tmux Worker Tally & Regex Auto-Approval Runtime")
subparsers = parser.add_subparsers(dest="subcommand")
# tally
p_tally = subparsers.add_parser("tally", help="Tally all tmux sessions, workers, and panes")
p_tally.add_argument("--json", action="store_true", help="Output machine-readable JSON")
p_tally.set_defaults(func=cmd_tally)
# status
p_status = subparsers.add_parser("status", help="Show auto-approval configuration & policies")
p_status.add_argument("--json", action="store_true", help="Output machine-readable JSON")
p_status.set_defaults(func=cmd_status)
# on / off
p_on = subparsers.add_parser("on", help="Enable auto-approvals (global, per-agent, or per-session)")
p_on.add_argument("--node", choices=FLEET_AGENTS, help="Enable for specific agent")
p_on.add_argument("--session", help="Enable for specific session name")
p_on.set_defaults(func=cmd_toggle_on)
p_off = subparsers.add_parser("off", help="Disable auto-approvals")
p_off.add_argument("--node", choices=FLEET_AGENTS, help="Disable for specific agent")
p_off.add_argument("--session", help="Disable for specific session name")
p_off.set_defaults(func=cmd_toggle_off)
# match
p_match = subparsers.add_parser("match", help="Test regex match against scrollback text")
p_match.add_argument("text", nargs="?", default="-", help="Input text or '-' for stdin")
p_match.set_defaults(func=cmd_match_test)
# once
p_once = subparsers.add_parser("once", help="Evaluate and auto-approve all active prompts right now")
p_once.add_argument("--dry-run", action="store_true", help="Log matches without sending keys")
p_once.set_defaults(func=cmd_run_once)
# watch
p_watch = subparsers.add_parser("watch", help="Run background monitor daemon for auto-approvals")
p_watch.add_argument("--interval", type=float, default=1.0, help="Poll interval in seconds (default: 1.0)")
p_watch.add_argument("--dry-run", action="store_true", help="Log matches without sending keys")
p_watch.set_defaults(func=cmd_watch)
# logs
p_logs = subparsers.add_parser("logs", help="Tail auto-approval audit log stream")
p_logs.add_argument("-n", "--lines", type=int, default=20, help="Number of lines to show")
p_logs.set_defaults(func=cmd_logs)
# spawn
p_spawn = subparsers.add_parser("spawn", help="Spawn a new tmux worker runner")
p_spawn.add_argument("session", help="Session name")
p_spawn.add_argument("--command", "-c", default="bash", help="Command to run")
p_spawn.add_argument("--node", choices=FLEET_AGENTS, default="muse", help="Target agent socket")
p_spawn.set_defaults(func=cmd_spawn_worker)
return parser
def main(argv: Optional[List[str]] = None) -> int:
parser = build_parser()
if argv is None:
argv = sys.argv[1:]
if not argv:
parser.print_help()
return 0
args = parser.parse_args(argv)
if not hasattr(args, "func"):
parser.print_help()
return 1
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
+168
View File
@@ -0,0 +1,168 @@
# Tmux Worker Tally & Regex Auto-Approvals over HTTPS (No SSH)
> **Box is the main surface.** All operator work goes through Box (`box.muse-dev.online`).
> The web UI, `box` CLI, and agents share the same unified API endpoints and runtimes.
**Date:** 2026-10-06
**Status:** Implemented (`bin/tmux_auto_approver.py`, `bin/box-onboard-tui.py`, `bin/super-cli.py`)
**Scope:** Tmux worker tallies, automated regex approval engine for on-board Muse Code runs & autonomous agent workers, and dedicated interactive TUI console (`box onboard-tui`).
---
## 1. Architecture
```
┌──────────────────────────────────────────────┐
│ https://box.muse-dev.online/ │
│ (Web Dashboard & API Gateway) │
└──────────────────────┬───────────────────────┘
│
HTTPS Signed Ops / CLI Dispatch
│
┌──────────────────────▼───────────────────────┐
│ Unified Box CLI Engine │
│ (box tmux tally / box tmux auto ...) │
└───────┬───────────────────────────────┬──────┘
│ │
┌──────────────▼─────────────┐ ┌─────────────▼──────────────┐
│ bin/box-onboard-tui.py │ │ bin/tmux_auto_approver.py │
│ (Dedicated 4-Tab Console) │ │ (Multi-Socket Regex Daemon)│
└──────────────┬─────────────┘ └─────────────┬──────────────┘
│ │
│ │
┌───────────────────────┴───────────────────────────────┴───────────────────────┐
│ Tmux Sockets Monitored │
│ • /tmp/tmux-muse.sock (shared host workers) │
│ • /tmp/tmux-1000/default (dev/def runner panes & muse-code %37) │
│ • /tmp/tmux-1000/lte (lte operator pane) │
│ • /tmp/tmux-<agent>.sock (per-agent netns sockets: pip, 646, opm, dev, def) │
└───────────────────────────────────────────────────────────────────────────────┘
```
---
## 2. Tmux Auto-Approval Regex Match Engine
The auto-approval engine monitors scrollback across all agent panes and matches approval prompts against priority rules:
| Rule ID | Category | Trigger Pattern | Response Key | Press Enter | Description |
|---|---|---|---|---|---|
| `muse_code_run_numbered` | `muse_code` | `Would you like to run the following[\s\S]*?›\s*1\.\s*Yes,?\s*proceed` | `1` | `false` | Muse Code interactive run menu (selects option 1) |
| `muse_code_run_yn` | `muse_code` | `›\s*1\.\s*Yes,?\s*proceed\s*\(y\)` | `1` | `false` | Active selection indicator on `1. Yes, proceed (y)` |
| `muse_code_allow_execution` | `muse_code` | `Allow\s+execution\s+of\b[\s\S]*?\[y/N\]` | `y` | `true` | Approves script execution confirmation |
| `choice_abc` | `choice` | `(?i)(?:choose\|choice\|select)[\s\S]*?^\s*[A-Z]\s*[.\)\-:]\s+\S` | `A` | `true` | Lettered decision choice menus |
| `menu_numbered` | `menu` | `(?i)(?:Option:\|Selection:)[\s\S]*?^\s*\(?1\)?\s+[A-Za-z]` | `1` | `true` | Numbered selection menus |
| `confirm_yn` | `confirm` | `([yY]/[nN]\|\[[yY]/[nN]\])\s*[\]:)>]?\s*$` | `y` | `true` | Line-end confirmation prompts |
| `enter_to_continue` | `enter` | `(?i)(?:Press\s+\[?Enter\]?\s+to\s+continue)` | `Enter` | `false` | Enter-to-continue banners |
### Guardrails (Never Auto-Approved)
- `[sudo] password for ...` / `password:` prompts
- Passkeys, private key passphrases, and PIN prompts
- Destructive operations (`rm -rf /`, `mkfs.*`)
When a guardrail pattern is detected, the engine flags `is_blocked=true`, emits a warning audit log, and notifies the human operator.
---
## 3. CLI Commands
### Tmux Worker Tally & Management
```bash
# Tally all active tmux sessions, panes, and workers across sockets
box tmux tally
box tmux tally --json
# List active sessions
box tmux list
# Spawn new background worker session
box tmux new my-worker -c "python3 run_tasks.py"
```
### Auto-Approval Control
```bash
# Query master state and per-agent policies
box tmux auto status
box tmux auto status --json
# Master enable / disable
box tmux auto on
box tmux auto off
# Enable / disable for specific agent
box tmux auto on --node muse
box tmux auto off --node 646
# Execute single-pass scan and auto-approve all active prompts right now
box tmux auto once
box tmux auto once --dry-run
# Run background monitor daemon
box tmux auto watch --interval 1.0
# Tail structured audit log stream
box tmux auto logs -n 20
# Test regex match against custom prompt text
box tmux auto match "Would you like to run the following ... › 1. Yes, proceed (y)"
```
### Onboard Connects
```bash
# View all fleet nodes & client onboard connects
box onboard connects
box onboard connects --json
# Start client onboarding
box onboard start dev2 --email client@example.com --for 646
# Submit OTP verification code
box onboard submit-otp dev2 123456
```
### Dedicated Interactive TUI
```bash
# Launch full 4-tab interactive TUI
box onboard-tui
box tui onboard
```
---
## 4. HTTPS Remote Operations (`exec-constrained.py`)
Available via `https://exec.muse-dev.online/exec` with signature verification:
| Op | Parameters | Description | Permission |
|---|---|---|---|
| `tmux.tally` | `{}` | Returns complete worker tally across all sockets (JSON) | `DEFAULT_PERMS` (Read-only) |
| `tmux.auto_status` | `{}` | Returns auto-approval toggle state & rule set (JSON) | `DEFAULT_PERMS` (Read-only) |
| `onboard.connects` | `{}` | Returns consolidated fleet and client connects (JSON) | `DEFAULT_PERMS` (Read-only) |
| `tmux.auto_toggle` | `{"enabled": bool, "node"?: str}` | Toggles master or per-agent auto-approval state | Known-Identities-Only |
---
## 5. Audit Logging
Every auto-approval and blocked guardrail event is written to:
`/home/super/Projects/NetVM/logs/tmux/auto-approvals.jsonl`
Sample event payload:
```json
{
"action": "AUTO_APPROVED",
"socket": "/tmp/tmux-1000/default",
"pane": "%37",
"session": "muse",
"agent": "muse",
"rule_id": "muse_code_run_numbered",
"rule_name": "Muse Code Run (Numbered)",
"key_sent": "1",
"press_enter": false,
"excerpt": "Would you like to run the following ... › 1. Yes, proceed (y)",
"dry_run": false,
"success": true,
"timestamp": "2026-10-06T19:34:19.599811+00:00",
"ts": 1791315259.6
}
```
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""test_box_onboard_tui.py — Unit tests for dedicated Onboard Connects & Tmux Auto-Approver TUI."""
import curses
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
REPO_ROOT = Path(__file__).resolve().parent.parent
import importlib.util
tui_path = REPO_ROOT / "bin" / "box-onboard-tui.py"
spec = importlib.util.spec_from_file_location("box_onboard_tui", tui_path)
box_onboard_tui = importlib.util.module_from_spec(spec)
sys.modules["box_onboard_tui"] = box_onboard_tui
spec.loader.exec_module(box_onboard_tui)
BoxOnboardTUI = box_onboard_tui.BoxOnboardTUI
class TestBoxOnboardTUI(unittest.TestCase):
@patch("curses.init_pair")
@patch("curses.color_pair")
@patch("curses.has_colors", return_value=True)
@patch("curses.start_color")
@patch("box_onboard_tui.gather_tmux_tally")
@patch("box_onboard_tui.get_all_connects")
def test_tui_initialization_and_tabs(
self, mock_connects, mock_tally, mock_start_color, mock_has_colors, mock_color_pair, mock_init_pair
):
mock_stdscr = MagicMock()
mock_stdscr.getmaxyx.return_value = (40, 120)
mock_connects.return_value = [
{"node": "muse", "type": "fleet_agent", "email": "muse@test.com", "stage": "active_fleet"},
{"node": "test-client", "type": "onboard_pipeline", "email": "c@test.com", "stage": "awaiting_otp"},
]
mock_tally.return_value = MagicMock(
total_sockets=2,
total_sessions=3,
total_panes=5,
active_workers=2,
panes=[],
by_agent={"muse": {"sessions": 1, "panes": 2, "active_commands": ["muse-bin"], "auto_approve": True}},
)
app = BoxOnboardTUI(mock_stdscr)
self.assertEqual(app.current_tab, 0)
self.assertEqual(len(app.tabs), 4)
self.assertEqual(len(app.connects), 2)
# Tab navigation via key
app._handle_key(ord("\t"))
self.assertEqual(app.current_tab, 1)
app._handle_key(ord("\t"))
self.assertEqual(app.current_tab, 2)
app._handle_key(ord("\t"))
self.assertEqual(app.current_tab, 3)
app._handle_key(ord("\t"))
self.assertEqual(app.current_tab, 0)
@patch("curses.init_pair")
@patch("curses.color_pair")
@patch("curses.has_colors", return_value=True)
@patch("box_onboard_tui.gather_tmux_tally")
@patch("box_onboard_tui.get_all_connects")
def test_navigation_keys(
self, mock_connects, mock_tally, mock_has_colors, mock_color_pair, mock_init_pair
):
mock_stdscr = MagicMock()
mock_stdscr.getmaxyx.return_value = (40, 120)
mock_connects.return_value = [
{"node": f"node-{i}", "type": "fleet_agent"} for i in range(5)
]
app = BoxOnboardTUI(mock_stdscr)
self.assertEqual(app.sel_connect_idx, 0)
# Move down
app._handle_key(curses.KEY_DOWN)
self.assertEqual(app.sel_connect_idx, 1)
# Move up
app._handle_key(curses.KEY_UP)
self.assertEqual(app.sel_connect_idx, 0)
# Switch tab via number key '2'
app._handle_key(ord("2"))
self.assertEqual(app.current_tab, 1)
# Switch tab via number key '3'
app._handle_key(ord("3"))
self.assertEqual(app.current_tab, 2)
# Switch tab via number key '4'
app._handle_key(ord("4"))
self.assertEqual(app.current_tab, 3)
if __name__ == "__main__":
unittest.main()
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""test_tmux_auto_approver.py — Unit tests for Tmux worker management and regex auto-approver."""
import json
import os
import sys
import tempfile
import time
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "bin"))
import tmux_auto_approver
from tmux_auto_approver import (
DEFAULT_RULES,
GUARDRAIL_PATTERNS,
AutoApproverRunner,
AutoApproverState,
MatchRule,
MatchVerdict,
RegexApproverEngine,
TmuxPaneInfo,
TmuxWorkerTally,
gather_tmux_tally,
infer_agent_for_session,
)
class TestRegexApproverEngine(unittest.TestCase):
def setUp(self):
self.engine = RegexApproverEngine(DEFAULT_RULES)
def test_muse_code_run_numbered_match(self):
text = """
Terminal output before...
Would you like to run the following bash command?
echo "hello"
› 1. Yes, proceed (y)
2. No, skip (n)
"""
verdict = self.engine.evaluate(text)
self.assertTrue(verdict.matched)
self.assertEqual(verdict.rule_id, "muse_code_run_numbered")
self.assertEqual(verdict.key, "1")
self.assertFalse(verdict.press_enter)
def test_muse_code_run_yn_match(self):
text = "Some actions\n› 1. Yes, proceed (y)\n"
verdict = self.engine.evaluate(text)
self.assertTrue(verdict.matched)
self.assertIn("muse_code_run", verdict.rule_id)
self.assertEqual(verdict.key, "1")
def test_muse_code_allow_execution_match(self):
text = "Allow execution of script /tmp/test.sh? [y/N]"
verdict = self.engine.evaluate(text)
self.assertTrue(verdict.matched)
self.assertEqual(verdict.rule_id, "muse_code_allow_execution")
self.assertEqual(verdict.key, "y")
self.assertTrue(verdict.press_enter)
def test_choice_abc_match(self):
text = "Select option from the choices below:\nA) Deploy to production\nB) Staging"
verdict = self.engine.evaluate(text)
self.assertTrue(verdict.matched)
self.assertEqual(verdict.rule_id, "choice_abc")
self.assertEqual(verdict.key, "A")
self.assertTrue(verdict.press_enter)
def test_menu_numbered_match(self):
text = "Option: Please pick a number:\n(1) Accept terms\n(2) Decline"
verdict = self.engine.evaluate(text)
self.assertTrue(verdict.matched)
self.assertEqual(verdict.rule_id, "menu_numbered")
self.assertEqual(verdict.key, "1")
self.assertTrue(verdict.press_enter)
def test_confirm_yn_match(self):
text = "Do you want to continue? [y/n]: "
verdict = self.engine.evaluate(text)
self.assertTrue(verdict.matched)
self.assertEqual(verdict.rule_id, "confirm_yn")
self.assertEqual(verdict.key, "y")
self.assertTrue(verdict.press_enter)
def test_enter_to_continue_match(self):
text = "Task completed successfully. Press [Enter] to continue..."
verdict = self.engine.evaluate(text)
self.assertTrue(verdict.matched)
self.assertEqual(verdict.rule_id, "enter_to_continue")
self.assertEqual(verdict.key, "Enter")
def test_guardrail_sudo_password_blocked(self):
text = "[sudo] password for super: "
verdict = self.engine.evaluate(text)
self.assertFalse(verdict.matched)
self.assertTrue(verdict.is_blocked)
self.assertIn("guardrail", verdict.blocked_reason.lower())
def test_guardrail_passkey_blocked(self):
text = "Insert security key or enter passkey PIN:"
verdict = self.engine.evaluate(text)
self.assertFalse(verdict.matched)
self.assertTrue(verdict.is_blocked)
self.assertIn("guardrail", verdict.blocked_reason.lower())
def test_guardrail_destructive_command_blocked(self):
text = "Running dangerous cleanup: rm -rf /"
verdict = self.engine.evaluate(text)
self.assertFalse(verdict.matched)
self.assertTrue(verdict.is_blocked)
self.assertIn("guardrail", verdict.blocked_reason.lower())
def test_empty_text_returns_unmatched(self):
verdict = self.engine.evaluate("")
self.assertFalse(verdict.matched)
self.assertFalse(verdict.is_blocked)
class TestAgentInference(unittest.TestCase):
def test_infer_agent_names(self):
self.assertEqual(infer_agent_for_session("/tmp/tmux-muse.sock", "worker-1"), "muse")
self.assertEqual(infer_agent_for_session("/tmp/tmux-pip.sock", "agent-job"), "pip")
self.assertEqual(infer_agent_for_session("/tmp/tmux-1000/default", "646-retention"), "646")
self.assertEqual(infer_agent_for_session("/tmp/tmux-1000/default", "opm"), "opm")
self.assertEqual(infer_agent_for_session("/tmp/tmux-1000/default", "dev-test"), "dev")
self.assertEqual(infer_agent_for_session("/tmp/tmux-1000/default", "def"), "def")
self.assertEqual(infer_agent_for_session("/tmp/tmux-1000/lte", "main"), "host")
class TestAutoApproverRunner(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.orig_state = tmux_auto_approver.STATE_FILE
self.orig_audit = tmux_auto_approver.AUDIT_LOG_FILE
tmux_auto_approver.STATE_FILE = Path(self.temp_dir.name) / "test_state.json"
tmux_auto_approver.AUDIT_LOG_FILE = Path(self.temp_dir.name) / "test_audit.jsonl"
def tearDown(self):
tmux_auto_approver.STATE_FILE = self.orig_state
tmux_auto_approver.AUDIT_LOG_FILE = self.orig_audit
self.temp_dir.cleanup()
@patch("tmux_auto_approver.run_tmux_cmd")
@patch("tmux_auto_approver.capture_pane_text")
@patch("tmux_auto_approver.gather_tmux_tally")
def test_run_once_dry_run_dispatches_match(self, mock_tally, mock_capture, mock_tmux_cmd):
mock_pane = TmuxPaneInfo(
socket="/tmp/tmux-1000/default",
session="muse",
window_idx=0,
pane_id="%37",
pane_pid=12345,
current_command="muse-bin",
active=True,
attached=True,
title="muse terminal",
agent_node="muse",
auto_approve=True,
)
mock_tally.return_value = TmuxWorkerTally(
total_sockets=1,
total_sessions=1,
total_panes=1,
active_workers=1,
by_agent={"muse": {"sessions": 1, "panes": 1, "active_commands": ["muse-bin"], "auto_approve": True}},
panes=[mock_pane],
)
mock_capture.return_value = "Would you like to run the following?\n› 1. Yes, proceed (y)"
runner = AutoApproverRunner(dry_run=True)
results = runner.run_once()
self.assertEqual(len(results), 1)
self.assertEqual(results[0]["action"], "DRY_RUN_MATCH")
self.assertEqual(results[0]["key_sent"], "1")
self.assertEqual(results[0]["rule_name"], "Muse Code Run (Numbered)")
# Verify deduplication within 15 seconds
results2 = runner.run_once()
self.assertEqual(len(results2), 0)
@patch("tmux_auto_approver.gather_tmux_tally")
def test_run_once_global_disabled(self, mock_tally):
runner = AutoApproverRunner(dry_run=True)
runner.state.global_enabled = False
runner.state.save()
results = runner.run_once()
self.assertEqual(len(results), 1)
self.assertEqual(results[0]["status"], "disabled")
class TestTallyGathering(unittest.TestCase):
@patch("tmux_auto_approver.get_existing_sockets")
@patch("tmux_auto_approver.run_tmux_cmd")
def test_gather_tmux_tally_parsing(self, mock_run_cmd, mock_sockets):
mock_sockets.return_value = ["/tmp/tmux-1000/default"]
# Format: #{session_name}___#{window_index}___#{pane_id}___#{pane_pid}___#{pane_current_command}___#{pane_active}___#{session_attached}___#{pane_title}
mock_run_cmd.return_value = (
0,
"muse___0___%1___1001___muse-bin___1___1___muse worker\n"
"dev___0___%2___1002___bash___0___0___dev terminal\n",
"",
)
state = AutoApproverState()
tally = gather_tmux_tally(state)
self.assertEqual(tally.total_sockets, 1)
self.assertEqual(tally.total_sessions, 2)
self.assertEqual(tally.total_panes, 2)
self.assertEqual(tally.active_workers, 1) # only muse-bin is active, bash is shell
self.assertEqual(len(tally.panes), 2)
self.assertEqual(tally.panes[0].agent_node, "muse")
self.assertEqual(tally.panes[1].agent_node, "dev")
if __name__ == "__main__":
unittest.main()