721 lines
31 KiB
Python
721 lines
31 KiB
Python
|
|
#!/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())
|