feat(box): passkey fetch, agent key-approval flow, unified lookups, tmux agent UX
- box passkey [show|fetch] (+ muse passkey): documents VM-only passkey (/srv/box/passkey.txt, fallback /etc/netvm/passkey.txt on 34.139.37.135), probes VM over SSH with graceful fallback; --json supported. No secrets on bl. - approvals: request_key_approval / check_node_key_request; KEY_APPROVAL status surfaced in `box approvals check`; allow/deny resolve + audit to box-ctl.jsonl; never auto-approved. New `box approvals request-key <node> --reason`. - box lookup (summary|fleet|threads|unread|approvals|key|docs) and docs-lookup engine with lookup_internal/ database (docs_internal symlink). - muse-tmux: non-TTY attach falls back to scrollback capture; prune NameError fix. - box/muse passthrough for tmux/muse/docs; thread list/view alias + prefix resolve. - Docs: AGENTS.md, AGENT-TOOLING.md, BOX-WEB-SURFACE-GUIDE.md, README. - Tests: key-approval + passkey tests; sync stale sidechat UUIDs and manifest name. - .gitignore runtime trackers (subagent-sessions, conversation-nudge-tracker).
This commit is contained in:
+461
-71
@@ -48,11 +48,55 @@ TRUSTED_IPS = {
|
||||
"34.139.37.135", # VM (gateway)
|
||||
"100.123.153.75", # bl (main compute)
|
||||
"100.81.31.9", # VM tailnet
|
||||
"1.1.1.1", # Cloudflare DNS
|
||||
"1.0.0.1", # Cloudflare DNS
|
||||
}
|
||||
|
||||
# Trusted infrastructure domains safe for automated approval
|
||||
TRUSTED_DOMAINS = {
|
||||
"muse-dev.online",
|
||||
}
|
||||
|
||||
|
||||
def is_trusted_target(target: str, card_text: str = "") -> bool:
|
||||
"""Check if the extracted approval target is trusted infrastructure.
|
||||
|
||||
Fail-closed: only the parsed target (IP or hostname) is evaluated. Free-form
|
||||
card text is deliberately NOT substring-matched, since an untrusted request
|
||||
could mention a trusted domain in its purpose string. `card_text` is kept
|
||||
for signature compatibility.
|
||||
"""
|
||||
if not target:
|
||||
return False
|
||||
target = target.strip().lower().rstrip(".")
|
||||
if target in TRUSTED_IPS:
|
||||
return True
|
||||
for dom in TRUSTED_DOMAINS:
|
||||
if target == dom or target.endswith("." + dom):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
REDACT_PATTERNS = [
|
||||
(re.compile(r"Bearer\s+[A-Za-z0-9._~+/-]+=*", re.IGNORECASE), "Bearer [REDACTED]"),
|
||||
(re.compile(r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,}", re.IGNORECASE), "[JWT-REDACTED]"),
|
||||
(re.compile(r"(?i)\b(api[_-]?key|token|secret|password|auth|passkey)\s*[:=]\s*(['\"]?)([A-Za-z0-9_\-\.]{4,})\2"), r"\1: \2[REDACTED]\2"),
|
||||
(re.compile(r"-----BEGIN [A-Z ]+ PRIVATE KEY-----[\s\S]*?-----END [A-Z ]+ PRIVATE KEY-----"), "[PRIVATE-KEY-REDACTED]"),
|
||||
]
|
||||
|
||||
|
||||
def redact_sensitive(text: str) -> str:
|
||||
"""Mask credentials, tokens, and passkeys in text."""
|
||||
if not text or not isinstance(text, str):
|
||||
return text
|
||||
out = text
|
||||
for pattern, repl in REDACT_PATTERNS:
|
||||
out = pattern.sub(repl, out)
|
||||
return out
|
||||
|
||||
|
||||
def log_box_ctl(action: str, name: str = None, caller: str = "box-approvals", extra: dict = None):
|
||||
"""Log an audit event to box-ctl.jsonl."""
|
||||
"""Log an audit event to box-ctl.jsonl with secret redaction."""
|
||||
try:
|
||||
rec = {
|
||||
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
@@ -61,13 +105,76 @@ def log_box_ctl(action: str, name: str = None, caller: str = "box-approvals", ex
|
||||
"caller": caller,
|
||||
}
|
||||
if extra:
|
||||
rec.update(extra)
|
||||
clean_extra = {}
|
||||
for k, v in extra.items():
|
||||
if isinstance(v, str):
|
||||
clean_extra[k] = redact_sensitive(v)
|
||||
else:
|
||||
clean_extra[k] = v
|
||||
rec.update(clean_extra)
|
||||
with open(CTL_LOG, "a") as f:
|
||||
f.write(json.dumps(rec) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def request_key_approval(node: str, reason: str = "", caller: str = "agent") -> dict:
|
||||
"""Register a key/passkey approval request for a node in box-ctl.jsonl."""
|
||||
reason = reason or "Operator passkey access requested"
|
||||
log_box_ctl(
|
||||
"key-approval-request",
|
||||
name=node,
|
||||
caller=caller,
|
||||
extra={"reason": reason},
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"node": node,
|
||||
"status": "KEY_APPROVAL_REQUESTED",
|
||||
"reason": reason,
|
||||
"caller": caller,
|
||||
"note": "Request recorded in audit log. Operator can approve via 'box approvals allow <node>'."
|
||||
}
|
||||
|
||||
|
||||
def check_node_key_request(node: str) -> dict:
|
||||
"""Check if node has an active unfulfilled key approval request in box-ctl.jsonl."""
|
||||
if not CTL_LOG.exists():
|
||||
return None
|
||||
latest_req = None
|
||||
resolved = False
|
||||
try:
|
||||
with open(CTL_LOG, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if rec.get("name") != node:
|
||||
continue
|
||||
act = rec.get("action")
|
||||
if act == "key-approval-request":
|
||||
latest_req = rec
|
||||
resolved = False
|
||||
elif act in ("key-approval-allow", "key-approval-deny", "approval-allow", "approval-deny"):
|
||||
resolved = True
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if latest_req and not resolved:
|
||||
return {
|
||||
"node": node,
|
||||
"reason": latest_req.get("reason", "Operator passkey access requested"),
|
||||
"requested_at": latest_req.get("ts", ""),
|
||||
"caller": latest_req.get("caller", ""),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def get_node_connection_info(node: str) -> dict:
|
||||
"""Return peer_ip, cdp_port, and netns for a given node."""
|
||||
if netvm_registry:
|
||||
@@ -94,8 +201,8 @@ def get_node_connection_info(node: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def get_cdp_ws(node: str, timeout: float = 3.0):
|
||||
"""Connect to the node's active browser page over CDP WebSocket."""
|
||||
def get_node_pages(node: str, timeout: float = 3.0) -> list:
|
||||
"""Return all active page targets for a node."""
|
||||
if websocket is None:
|
||||
raise RuntimeError("websocket-client library is required")
|
||||
|
||||
@@ -125,13 +232,20 @@ def get_cdp_ws(node: str, timeout: float = 3.0):
|
||||
pages = [t for t in tabs if t.get("type") == "page"]
|
||||
if not pages:
|
||||
raise ConnectionError(f"No active page found for node {node}")
|
||||
return pages
|
||||
|
||||
ws_url = pages[0].get("webSocketDebuggerUrl")
|
||||
|
||||
def get_cdp_ws(node: str, page_idx: int = 0, timeout: float = 3.0):
|
||||
"""Connect to a node's browser page over CDP WebSocket."""
|
||||
pages = get_node_pages(node, timeout=timeout)
|
||||
if page_idx >= len(pages):
|
||||
page_idx = 0
|
||||
target_page = pages[page_idx]
|
||||
ws_url = target_page.get("webSocketDebuggerUrl")
|
||||
if not ws_url:
|
||||
raise ConnectionError(f"No webSocketDebuggerUrl for node {node}")
|
||||
|
||||
ws = websocket.create_connection(ws_url, timeout=timeout)
|
||||
return ws, pages[0]
|
||||
return ws, target_page
|
||||
|
||||
|
||||
def cdp_evaluate(ws, js_expr: str, await_promise: bool = False, timeout: float = 3.0):
|
||||
@@ -233,6 +347,16 @@ JS_INSPECT_APPROVALS = """(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Task rows in the Activity sidebar waiting on human input
|
||||
// (e.g. "Launch 5 OPM Sub-Agents\\nAsked for input to start the launch\\n5:53 pm").
|
||||
const inputWaits = [];
|
||||
for (const b of document.querySelectorAll('button.rounded-10, a.rounded-10')) {
|
||||
const lines = (b.innerText || '').split('\\n').map(s => s.trim()).filter(Boolean);
|
||||
if (lines.length >= 2 && /^(asked for (input|details)|waiting for (your )?(input|reply|approval)|needs your input)/i.test(lines[1])) {
|
||||
inputWaits.push({ task: lines[0], status: lines[1], when: lines[2] || '' });
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
has_pending: !!activeCard && (hasAllowOnce || hasDeny),
|
||||
card_text: cardText.slice(0, 1000),
|
||||
@@ -240,16 +364,40 @@ JS_INSPECT_APPROVALS = """(() => {
|
||||
has_allow_once: hasAllowOnce,
|
||||
has_always_allow: hasAlwaysAllow,
|
||||
has_deny: hasDeny,
|
||||
history: historyBadges.slice(0, 5)
|
||||
history: historyBadges.slice(0, 5),
|
||||
input_waits: inputWaits.slice(0, 10)
|
||||
});
|
||||
})()"""
|
||||
|
||||
|
||||
def inspect_node_approvals(node: str) -> dict:
|
||||
"""Inspect a node for active browser approval prompts."""
|
||||
"""Inspect a node across all open page targets for active approval prompts and input waits."""
|
||||
try:
|
||||
ws, page = get_cdp_ws(node, timeout=2.5)
|
||||
pages = get_node_pages(node, timeout=2.5)
|
||||
except Exception as e:
|
||||
key_req = check_node_key_request(node)
|
||||
if key_req:
|
||||
return {
|
||||
"node": node,
|
||||
"status": "KEY_APPROVAL",
|
||||
"has_pending": True,
|
||||
"title": f"Passkey requested: {key_req.get('reason')}",
|
||||
"purpose": key_req.get("reason"),
|
||||
"ip": "34.139.37.135",
|
||||
"target": "34.139.37.135 (VM passkey)",
|
||||
"is_trusted": True,
|
||||
"buttons": ["Allow", "Deny"],
|
||||
"has_allow_once": True,
|
||||
"has_always_allow": False,
|
||||
"has_deny": True,
|
||||
"raw_text": f"Agent on node {node} requested passkey: {key_req.get('reason')}",
|
||||
"history": [],
|
||||
"input_waits": [],
|
||||
"page_title": "",
|
||||
"page_url": "",
|
||||
"ws_url": "",
|
||||
"key_request": key_req,
|
||||
}
|
||||
return {
|
||||
"node": node,
|
||||
"status": "UNREACHABLE",
|
||||
@@ -257,66 +405,140 @@ def inspect_node_approvals(node: str) -> dict:
|
||||
"has_pending": False,
|
||||
}
|
||||
|
||||
try:
|
||||
val_str = cdp_evaluate(ws, JS_INSPECT_APPROVALS, timeout=3.0)
|
||||
ws.close()
|
||||
if not val_str or not isinstance(val_str, str):
|
||||
return {
|
||||
"node": node,
|
||||
"status": "CLEAR",
|
||||
"has_pending": False,
|
||||
"page_title": page.get("title", ""),
|
||||
"page_url": page.get("url", ""),
|
||||
}
|
||||
all_input_waits = []
|
||||
first_page = pages[0]
|
||||
last_err = None
|
||||
|
||||
data = json.loads(val_str)
|
||||
has_pending = data.get("has_pending", False)
|
||||
card_text = data.get("card_text", "")
|
||||
|
||||
# Parse details
|
||||
ip = None
|
||||
m_ip = re.search(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", card_text)
|
||||
if m_ip:
|
||||
ip = m_ip.group(0)
|
||||
|
||||
# Extract title and purpose summary
|
||||
lines = [line.strip() for line in card_text.split("\n") if line.strip()]
|
||||
title = lines[0] if lines else "Permission request"
|
||||
purpose = lines[1] if len(lines) > 1 else ""
|
||||
|
||||
is_trusted = False
|
||||
if ip:
|
||||
is_trusted = ip in TRUSTED_IPS
|
||||
|
||||
return {
|
||||
"node": node,
|
||||
"status": "PENDING" if has_pending else "CLEAR",
|
||||
"has_pending": has_pending,
|
||||
"title": title,
|
||||
"purpose": purpose,
|
||||
"ip": ip,
|
||||
"is_trusted": is_trusted,
|
||||
"buttons": data.get("buttons", []),
|
||||
"has_allow_once": data.get("has_allow_once", False),
|
||||
"has_always_allow": data.get("has_always_allow", False),
|
||||
"has_deny": data.get("has_deny", False),
|
||||
"raw_text": card_text,
|
||||
"history": data.get("history", []),
|
||||
"page_title": page.get("title", ""),
|
||||
"page_url": page.get("url", ""),
|
||||
}
|
||||
except Exception as e:
|
||||
for page in pages:
|
||||
ws_url = page.get("webSocketDebuggerUrl")
|
||||
if not ws_url:
|
||||
continue
|
||||
ws = None
|
||||
try:
|
||||
ws = websocket.create_connection(ws_url, timeout=2.0)
|
||||
val_str = cdp_evaluate(ws, JS_INSPECT_APPROVALS, timeout=2.5)
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
ws = None
|
||||
if not val_str or not isinstance(val_str, str):
|
||||
continue
|
||||
data = json.loads(val_str)
|
||||
if data.get("input_waits"):
|
||||
all_input_waits.extend(data["input_waits"])
|
||||
|
||||
if data.get("has_pending"):
|
||||
card_text = data.get("card_text", "")
|
||||
ip = None
|
||||
target = None
|
||||
m_t = re.search(
|
||||
r"(?:connection to|share information with|contact|connect to)\s+([A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9])",
|
||||
card_text,
|
||||
)
|
||||
if m_t:
|
||||
target = m_t.group(1)
|
||||
m_ip = re.search(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", target or card_text)
|
||||
if m_ip:
|
||||
ip = m_ip.group(0)
|
||||
if not target:
|
||||
target = ip
|
||||
if not target:
|
||||
m_domain = re.search(r"\b([a-zA-Z0-9-]+\.)+(?:online|com|net|org|io|dev)\b", card_text)
|
||||
if m_domain:
|
||||
target = m_domain.group(0)
|
||||
|
||||
lines = [line.strip() for line in card_text.split("\n") if line.strip()]
|
||||
title = redact_sensitive(lines[0] if lines else "Permission request")
|
||||
purpose = redact_sensitive(lines[1] if len(lines) > 1 else "")
|
||||
|
||||
is_trusted = is_trusted_target(target or ip, card_text)
|
||||
|
||||
return {
|
||||
"node": node,
|
||||
"status": "PENDING",
|
||||
"has_pending": True,
|
||||
"title": title,
|
||||
"purpose": purpose,
|
||||
"ip": ip,
|
||||
"target": target or ip or "-",
|
||||
"is_trusted": is_trusted,
|
||||
"buttons": data.get("buttons", []),
|
||||
"has_allow_once": data.get("has_allow_once", False),
|
||||
"has_always_allow": data.get("has_always_allow", False),
|
||||
"has_deny": data.get("has_deny", False),
|
||||
"raw_text": redact_sensitive(card_text),
|
||||
"history": data.get("history", []),
|
||||
"input_waits": all_input_waits,
|
||||
"page_title": page.get("title", ""),
|
||||
"page_url": page.get("url", ""),
|
||||
"ws_url": ws_url,
|
||||
}
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
if ws:
|
||||
try:
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Deduplicate input waits by task name
|
||||
unique_waits = []
|
||||
seen_tasks = set()
|
||||
for w in all_input_waits:
|
||||
t_name = redact_sensitive(w.get("task", ""))
|
||||
status_text = redact_sensitive(w.get("status", ""))
|
||||
if t_name not in seen_tasks:
|
||||
seen_tasks.add(t_name)
|
||||
unique_waits.append({
|
||||
"task": t_name,
|
||||
"status": status_text,
|
||||
"when": w.get("when", ""),
|
||||
})
|
||||
|
||||
key_req = check_node_key_request(node)
|
||||
if key_req:
|
||||
return {
|
||||
"node": node,
|
||||
"status": "ERROR",
|
||||
"error": str(e),
|
||||
"has_pending": False,
|
||||
"status": "KEY_APPROVAL",
|
||||
"has_pending": True,
|
||||
"title": f"Passkey requested: {key_req.get('reason')}",
|
||||
"purpose": key_req.get("reason"),
|
||||
"ip": "34.139.37.135",
|
||||
"target": "34.139.37.135 (VM passkey)",
|
||||
"is_trusted": True,
|
||||
"buttons": ["Allow", "Deny"],
|
||||
"has_allow_once": True,
|
||||
"has_always_allow": False,
|
||||
"has_deny": True,
|
||||
"raw_text": f"Agent on node {node} requested passkey: {key_req.get('reason')}",
|
||||
"history": [],
|
||||
"input_waits": unique_waits,
|
||||
"page_title": first_page.get("title", ""),
|
||||
"page_url": first_page.get("url", ""),
|
||||
"ws_url": first_page.get("webSocketDebuggerUrl", ""),
|
||||
"key_request": key_req,
|
||||
}
|
||||
|
||||
status = "INPUT_WAIT" if unique_waits else ("ERROR" if last_err and not first_page else "CLEAR")
|
||||
return {
|
||||
"node": node,
|
||||
"status": status,
|
||||
"has_pending": False,
|
||||
"title": "No pending approvals",
|
||||
"purpose": "",
|
||||
"ip": None,
|
||||
"target": "-",
|
||||
"is_trusted": False,
|
||||
"buttons": [],
|
||||
"has_allow_once": False,
|
||||
"has_always_allow": False,
|
||||
"has_deny": False,
|
||||
"raw_text": "",
|
||||
"history": [],
|
||||
"input_waits": unique_waits,
|
||||
"page_title": first_page.get("title", ""),
|
||||
"page_url": first_page.get("url", ""),
|
||||
"ws_url": first_page.get("webSocketDebuggerUrl", ""),
|
||||
}
|
||||
|
||||
|
||||
def check_fleet_approvals(nodes: list = None) -> list:
|
||||
"""Check approval status across the fleet."""
|
||||
@@ -333,6 +555,28 @@ def allow_node_approval(node: str, always: bool = False, force: bool = False, ca
|
||||
if not info.get("has_pending"):
|
||||
return {"ok": False, "node": node, "error": "No pending approval dialog found on node"}
|
||||
|
||||
if info.get("status") == "KEY_APPROVAL":
|
||||
key_req = info.get("key_request") or check_node_key_request(node) or {}
|
||||
reason = key_req.get("reason", info.get("purpose", ""))
|
||||
log_box_ctl(
|
||||
"key-approval-allow",
|
||||
name=node,
|
||||
caller=caller,
|
||||
extra={
|
||||
"reason": reason,
|
||||
"forced": force,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"node": node,
|
||||
"type": "key_approval",
|
||||
"decision": "allow",
|
||||
"dismissed": True,
|
||||
"reason": reason,
|
||||
"title": info.get("title"),
|
||||
}
|
||||
|
||||
if not info.get("is_trusted") and not force:
|
||||
target = info.get("ip") or "unrecognized target"
|
||||
return {
|
||||
@@ -343,7 +587,11 @@ def allow_node_approval(node: str, always: bool = False, force: bool = False, ca
|
||||
}
|
||||
|
||||
try:
|
||||
ws, _ = get_cdp_ws(node, timeout=3.0)
|
||||
ws_url = info.get("ws_url")
|
||||
if ws_url:
|
||||
ws = websocket.create_connection(ws_url, timeout=3.0)
|
||||
else:
|
||||
ws, _ = get_cdp_ws(node, timeout=3.0)
|
||||
except Exception as e:
|
||||
return {"ok": False, "node": node, "error": f"Failed to connect to CDP: {e}"}
|
||||
|
||||
@@ -351,11 +599,19 @@ def allow_node_approval(node: str, always: bool = False, force: bool = False, ca
|
||||
if always:
|
||||
js_click = """(() => {
|
||||
const btns = Array.from(document.querySelectorAll('button'));
|
||||
const btn = btns.find(b => (b.innerText||'').toLowerCase().includes('always allow'));
|
||||
let btn = btns.find(b => (b.innerText||'').toLowerCase().includes('always allow'));
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return 'CLICKED_ALWAYS';
|
||||
}
|
||||
btn = document.querySelector('button[data-hatch-approval-primary-action="true"]');
|
||||
if (!btn) {
|
||||
btn = btns.find(b => (b.innerText||'').toLowerCase().includes('allow once') || (b.innerText||'').trim().toLowerCase() === 'allow');
|
||||
}
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return 'CLICKED_PRIMARY_FALLBACK';
|
||||
}
|
||||
return 'NOT_FOUND';
|
||||
})()"""
|
||||
else:
|
||||
@@ -422,13 +678,38 @@ def allow_node_approval(node: str, always: bool = False, force: bool = False, ca
|
||||
|
||||
|
||||
def deny_node_approval(node: str, caller: str = "box-approvals") -> dict:
|
||||
"""Deny a pending approval on a node (click 'Deny')."""
|
||||
"""Deny a pending approval on a node (click 'Deny' or deny key request)."""
|
||||
info = inspect_node_approvals(node)
|
||||
if not info.get("has_pending"):
|
||||
return {"ok": False, "node": node, "error": "No pending approval dialog found on node"}
|
||||
|
||||
if info.get("status") == "KEY_APPROVAL":
|
||||
key_req = info.get("key_request") or check_node_key_request(node) or {}
|
||||
reason = key_req.get("reason", info.get("purpose", ""))
|
||||
log_box_ctl(
|
||||
"key-approval-deny",
|
||||
name=node,
|
||||
caller=caller,
|
||||
extra={
|
||||
"reason": reason,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"node": node,
|
||||
"type": "key_approval",
|
||||
"decision": "deny",
|
||||
"dismissed": True,
|
||||
"reason": reason,
|
||||
"title": info.get("title"),
|
||||
}
|
||||
|
||||
try:
|
||||
ws, _ = get_cdp_ws(node, timeout=3.0)
|
||||
ws_url = info.get("ws_url")
|
||||
if ws_url:
|
||||
ws = websocket.create_connection(ws_url, timeout=3.0)
|
||||
else:
|
||||
ws, _ = get_cdp_ws(node, timeout=3.0)
|
||||
except Exception as e:
|
||||
return {"ok": False, "node": node, "error": f"Failed to connect to CDP: {e}"}
|
||||
|
||||
@@ -481,8 +762,8 @@ def deny_node_approval(node: str, caller: str = "box-approvals") -> dict:
|
||||
return {"ok": False, "node": node, "error": str(e)}
|
||||
|
||||
|
||||
def auto_approve_fleet(nodes: list = None, caller: str = "box-approvals") -> dict:
|
||||
"""Scan fleet nodes and automatically approve any requests to TRUSTED_IPS."""
|
||||
def auto_approve_fleet(nodes: list = None, always: bool = True, caller: str = "box-approvals") -> dict:
|
||||
"""Scan fleet nodes and automatically approve any requests to TRUSTED_IPS with Always Allow."""
|
||||
fleet = check_fleet_approvals(nodes)
|
||||
approved = []
|
||||
untrusted = []
|
||||
@@ -491,12 +772,16 @@ def auto_approve_fleet(nodes: list = None, caller: str = "box-approvals") -> dic
|
||||
for item in fleet:
|
||||
node = item["node"]
|
||||
if item.get("has_pending"):
|
||||
if item.get("is_trusted"):
|
||||
res = allow_node_approval(node, caller=caller)
|
||||
if item.get("status") == "KEY_APPROVAL":
|
||||
# Key approvals require explicit operator decision, never auto-approve
|
||||
untrusted.append(item)
|
||||
elif item.get("is_trusted"):
|
||||
res = allow_node_approval(node, always=always, caller=caller)
|
||||
approved.append({
|
||||
"node": node,
|
||||
"target_ip": item.get("ip"),
|
||||
"title": item.get("title"),
|
||||
"decision": "always" if always else "allow_once",
|
||||
"res": res,
|
||||
})
|
||||
else:
|
||||
@@ -510,3 +795,108 @@ def auto_approve_fleet(nodes: list = None, caller: str = "box-approvals") -> dic
|
||||
"untrusted_pending": untrusted,
|
||||
"clear_nodes": clear,
|
||||
}
|
||||
|
||||
|
||||
def reply_node_task(node: str, message: str, allow_main_chat: bool = False, caller: str = "box-approvals") -> dict:
|
||||
"""Send an operator reply into the waiting agent's thread to answer an input prompt."""
|
||||
info = inspect_node_approvals(node)
|
||||
page_url = info.get("page_url", "")
|
||||
page_title = info.get("page_title", "")
|
||||
ws_url = info.get("ws_url")
|
||||
|
||||
# Check if target is Main Chat
|
||||
# Main chat signatures: not sidechat and (no /thread/ or title contains 'Chat —')
|
||||
is_main = "sidechat" not in page_url.lower() and ("/thread/" not in page_url or "chat —" in page_title.lower())
|
||||
if is_main and not allow_main_chat:
|
||||
return {
|
||||
"ok": False,
|
||||
"node": node,
|
||||
"error": "Active thread appears to be Main Chat. Refusing reply by sidechat-first policy. Pass --allow-main-chat to confirm intentional operator override.",
|
||||
"page_title": page_title,
|
||||
"page_url": page_url,
|
||||
}
|
||||
|
||||
try:
|
||||
if ws_url:
|
||||
ws = websocket.create_connection(ws_url, timeout=3.0)
|
||||
else:
|
||||
ws, _ = get_cdp_ws(node, timeout=3.0)
|
||||
except Exception as e:
|
||||
return {"ok": False, "node": node, "error": f"Failed to connect to CDP: {e}"}
|
||||
|
||||
try:
|
||||
msg_esc = message.replace('\\', '\\\\').replace('`', '\\`').replace('$', '\\$').replace('"', '\\"')
|
||||
js_type_and_send = f"""(async() => {{
|
||||
const input = document.querySelector('[contenteditable="true"]') ||
|
||||
document.querySelector('textarea[placeholder*="Message"]') ||
|
||||
document.querySelector('textarea');
|
||||
if (!input) return 'NO_INPUT';
|
||||
input.focus();
|
||||
document.execCommand('insertText', false, "{msg_esc}");
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
const sendBtn = Array.from(document.querySelectorAll('button')).find(b => {{
|
||||
const a = (b.getAttribute('aria-label') || '').toLowerCase();
|
||||
const t = (b.innerText || '').toLowerCase();
|
||||
return a.includes('send') || t === 'send';
|
||||
}});
|
||||
if (sendBtn && !sendBtn.disabled) {{
|
||||
sendBtn.click();
|
||||
return 'CLICKED_SEND';
|
||||
}}
|
||||
const ke = new KeyboardEvent('keydown', {{key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true}});
|
||||
input.dispatchEvent(ke);
|
||||
return 'ENTER_SENT';
|
||||
}})()"""
|
||||
send_res = cdp_evaluate(ws, js_type_and_send, await_promise=True, timeout=5.0)
|
||||
ws.close()
|
||||
|
||||
log_box_ctl(
|
||||
"approval-reply",
|
||||
name=node,
|
||||
caller=caller,
|
||||
extra={
|
||||
"message_len": len(message),
|
||||
"page_url": page_url,
|
||||
"allow_main_chat": allow_main_chat,
|
||||
"send_res": send_res,
|
||||
},
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"node": node,
|
||||
"send_result": send_res,
|
||||
"page_title": page_title,
|
||||
"page_url": page_url,
|
||||
}
|
||||
except Exception as e:
|
||||
try:
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": False, "node": node, "error": str(e)}
|
||||
|
||||
|
||||
def dismiss_node_task(node: str, caller: str = "box-approvals") -> dict:
|
||||
"""Close any open task modal dialog or popup on a node."""
|
||||
try:
|
||||
ws, _ = get_cdp_ws(node, timeout=3.0)
|
||||
except Exception as e:
|
||||
return {"ok": False, "node": node, "error": f"Failed to connect to CDP: {e}"}
|
||||
|
||||
try:
|
||||
js_dismiss = """(() => {
|
||||
const close = document.querySelector('[aria-label="Close"], button[data-slot="dialog-close"]');
|
||||
if (close) { close.click(); return 'CLICKED_CLOSE'; }
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true}));
|
||||
return 'ESCAPE_SENT';
|
||||
})()"""
|
||||
res = cdp_evaluate(ws, js_dismiss, timeout=2.0)
|
||||
ws.close()
|
||||
return {"ok": True, "node": node, "result": res}
|
||||
except Exception as e:
|
||||
try:
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": False, "node": node, "error": str(e)}
|
||||
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/super/Projects/NetVM/bin/docs-lookup.py
|
||||
Executable
+606
@@ -0,0 +1,606 @@
|
||||
#!/usr/bin/env python3
|
||||
"""docs-lookup.py — Unified Lookup & Regex Passing Tool for docs_internal/.
|
||||
|
||||
Provides high-speed queries, regex passing, grammar validation, and surface
|
||||
lookups for autonomous agents and operators interfacing with NetVM, Box,
|
||||
and box.muse-dev.online.
|
||||
|
||||
Usage:
|
||||
docs-lookup.py search <query>
|
||||
docs-lookup.py surfaces [name]
|
||||
docs-lookup.py sentence [name]
|
||||
docs-lookup.py regex [name] [--test "<string>"]
|
||||
docs-lookup.py parse "<string>"
|
||||
docs-lookup.py get <collection> [key]
|
||||
docs-lookup.py cli [domain]
|
||||
docs-lookup.py overview
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
NETVM_ROOT = Path(__file__).resolve().parent.parent
|
||||
LOOKUP_INTERNAL = NETVM_ROOT / "lookup_internal"
|
||||
if not LOOKUP_INTERNAL.exists() and (NETVM_ROOT / "docs_internal").exists():
|
||||
LOOKUP_INTERNAL = NETVM_ROOT / "docs_internal"
|
||||
DOCS_INTERNAL = LOOKUP_INTERNAL
|
||||
|
||||
USE_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
||||
|
||||
def _c(code: str, text: str) -> str:
|
||||
return f"\033[{code}m{text}\033[0m" if USE_COLOR else str(text)
|
||||
|
||||
def c_bold(s: str) -> str: return _c("1", s)
|
||||
def c_dim(s: str) -> str: return _c("2", s)
|
||||
def c_green(s: str) -> str: return _c("32", s)
|
||||
def c_red(s: str) -> str: return _c("31", s)
|
||||
def c_yellow(s: str) -> str: return _c("33", s)
|
||||
def c_blue(s: str) -> str: return _c("34", s)
|
||||
def c_cyan(s: str) -> str: return _c("36", s)
|
||||
def c_magenta(s: str) -> str: return _c("35", s)
|
||||
|
||||
|
||||
def load_json_file(filename: str) -> Dict[str, Any]:
|
||||
"""Safely load a JSON file from docs_internal."""
|
||||
p = DOCS_INTERNAL / filename
|
||||
if not p.is_file():
|
||||
return {}
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error reading {p}: {e}", file=sys.stderr)
|
||||
return {}
|
||||
|
||||
|
||||
def load_manifest() -> Dict[str, Any]:
|
||||
return load_json_file("manifest.json")
|
||||
|
||||
|
||||
def load_sentence_structures() -> Dict[str, Any]:
|
||||
return load_json_file("sentence_structure.json")
|
||||
|
||||
|
||||
def load_regex_patterns() -> Dict[str, Any]:
|
||||
return load_json_file("regex_patterns.json")
|
||||
|
||||
|
||||
def load_assistive_surfaces() -> Dict[str, Any]:
|
||||
return load_json_file("assistive_surfaces.json")
|
||||
|
||||
|
||||
def load_cli_tools() -> Dict[str, Any]:
|
||||
return load_json_file("cli_tools.json")
|
||||
|
||||
|
||||
def load_fleet_nodes() -> Dict[str, Any]:
|
||||
return load_json_file("fleet_nodes.json")
|
||||
|
||||
|
||||
def compile_pattern(pat_entry: Dict[str, Any]) -> Tuple[Optional[re.Pattern], Optional[str]]:
|
||||
"""Compile a regex entry with its configured flags."""
|
||||
raw_pat = pat_entry.get("pattern", "")
|
||||
flag_names = pat_entry.get("flags", [])
|
||||
flags = 0
|
||||
for fn in flag_names:
|
||||
if hasattr(re, fn):
|
||||
flags |= getattr(re, fn)
|
||||
try:
|
||||
return re.compile(raw_pat, flags), None
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core Lookup Actions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def handle_overview(json_mode: bool = False):
|
||||
manifest = load_manifest()
|
||||
if json_mode:
|
||||
print(json.dumps(manifest, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold("\n=== docs_internal — Internal Agent & Operator Lookup Database ==="))
|
||||
print(c_dim(f"Location: {DOCS_INTERNAL} | Host: {manifest.get('host', 'box.muse-dev.online')}"))
|
||||
print(f"{manifest.get('description', '')}\n")
|
||||
|
||||
print(c_cyan("Available Collections:"))
|
||||
collections = manifest.get("collections", [])
|
||||
for col in collections:
|
||||
cid = col.get("id")
|
||||
name = col.get("name")
|
||||
desc = col.get("description")
|
||||
jfile = col.get("json_file")
|
||||
mfile = col.get("md_file")
|
||||
print(f" • {c_bold(cid):<20} {c_green(name)}")
|
||||
print(f" {c_dim(desc)}")
|
||||
print(f" {c_dim('Files:')} {c_yellow(jfile)} | {c_yellow(mfile)}")
|
||||
print()
|
||||
|
||||
print(c_dim("Query commands: super docs [search|surfaces|sentence|regex|parse|get|cli]"))
|
||||
|
||||
|
||||
def handle_search(query: str, json_mode: bool = False):
|
||||
q = query.lower()
|
||||
results = []
|
||||
|
||||
# Search in all JSON files
|
||||
for jpath in sorted(DOCS_INTERNAL.glob("*.json")):
|
||||
try:
|
||||
data = json.loads(jpath.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def recurse_search(obj, path=""):
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
subpath = f"{path}.{k}" if path else k
|
||||
if q in str(k).lower():
|
||||
results.append({
|
||||
"file": jpath.name,
|
||||
"type": "json_key",
|
||||
"path": subpath,
|
||||
"match": str(k),
|
||||
"preview": str(v)[:160]
|
||||
})
|
||||
recurse_search(v, subpath)
|
||||
elif isinstance(obj, list):
|
||||
for idx, item in enumerate(obj):
|
||||
recurse_search(item, f"{path}[{idx}]")
|
||||
elif isinstance(obj, str):
|
||||
if q in obj.lower():
|
||||
results.append({
|
||||
"file": jpath.name,
|
||||
"type": "json_value",
|
||||
"path": path,
|
||||
"match": obj[:120],
|
||||
"preview": obj[:240]
|
||||
})
|
||||
|
||||
recurse_search(data)
|
||||
|
||||
# Search in Markdown files
|
||||
for mpath in sorted(DOCS_INTERNAL.glob("*.md")):
|
||||
try:
|
||||
content = mpath.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
continue
|
||||
lines = content.splitlines()
|
||||
for idx, line in enumerate(lines, 1):
|
||||
if q in line.lower():
|
||||
results.append({
|
||||
"file": mpath.name,
|
||||
"type": "markdown",
|
||||
"line": idx,
|
||||
"match": line.strip(),
|
||||
"preview": line.strip()
|
||||
})
|
||||
|
||||
if json_mode:
|
||||
print(json.dumps({"query": query, "count": len(results), "results": results}, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold(f"\nSearch results for '{query}' ({len(results)} matches):"))
|
||||
if not results:
|
||||
print(c_dim(" (no matching entries found)"))
|
||||
return
|
||||
|
||||
for r in results[:40]:
|
||||
if r["type"] == "markdown":
|
||||
print(f" [{c_yellow(r['file'])}:{c_cyan(str(r['line']))}] {r['match']}")
|
||||
else:
|
||||
print(f" [{c_blue(r['file'])}:{c_magenta(r['path'])}] {r['preview']}")
|
||||
|
||||
|
||||
def handle_surfaces(view_name: Optional[str] = None, json_mode: bool = False):
|
||||
data = load_assistive_surfaces()
|
||||
views = data.get("views", {})
|
||||
|
||||
if view_name:
|
||||
key = view_name.lower().strip()
|
||||
v = views.get(key)
|
||||
if not v:
|
||||
for k, val in views.items():
|
||||
if key in k or key in val.get("name", "").lower():
|
||||
v = val
|
||||
key = k
|
||||
break
|
||||
if not v:
|
||||
err = {"error": f"Surface '{view_name}' not found", "available": list(views.keys())}
|
||||
if json_mode:
|
||||
print(json.dumps(err, indent=2))
|
||||
else:
|
||||
print(c_red(f"Error: Surface '{view_name}' not found. Available: {', '.join(views.keys())}"))
|
||||
sys.exit(1)
|
||||
|
||||
if json_mode:
|
||||
print(json.dumps({key: v}, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold(f"\n=== Assistive Surface: {v.get('name')} (`{key}`) ==="))
|
||||
print(c_dim(f"Host: {data.get('host')} | Tab ID: {v.get('tab_id')}"))
|
||||
print(f"{c_cyan('DOM Tab Selector:')} {c_yellow(str(v.get('dom_tab_selector')))}")
|
||||
print(f"{c_cyan('DOM Pane Selector:')} {c_yellow(str(v.get('dom_pane_selector')))}")
|
||||
|
||||
elements = v.get("key_elements", {})
|
||||
if elements:
|
||||
print(c_bold("\nKey DOM Elements / Selectors:"))
|
||||
for el_name, sel in elements.items():
|
||||
print(f" • {c_magenta(el_name):<20} {c_green(sel)}")
|
||||
|
||||
endpoints = v.get("api_endpoints", [])
|
||||
if endpoints:
|
||||
print(c_bold("\nAssociated REST API Endpoints:"))
|
||||
for ep in endpoints:
|
||||
print(f" • {c_bold(ep.get('method'))} {c_cyan(ep.get('path'))}")
|
||||
print(f" {c_dim(ep.get('description'))}")
|
||||
if ep.get("curl_example"):
|
||||
print(f" {c_dim('curl:')} {c_yellow(ep.get('curl_example'))}")
|
||||
|
||||
recipe = v.get("assistive_recipe")
|
||||
if recipe:
|
||||
print(c_bold("\nAssistive Recipe:"))
|
||||
print(f" {recipe}")
|
||||
print()
|
||||
return
|
||||
|
||||
# All views
|
||||
if json_mode:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold(f"\n=== Assistive Surfaces for {data.get('host', 'box.muse-dev.online')} ==="))
|
||||
print(c_dim(f"Operator PIN: {data.get('auth', {}).get('pin')} | Session Cookie: {data.get('auth', {}).get('cookie_name')}"))
|
||||
print()
|
||||
|
||||
for k, v in views.items():
|
||||
name = v.get("name", k)
|
||||
tab_sel = v.get("dom_tab_selector") or "(modal/overlay)"
|
||||
eps = [f"{ep.get('method')} {ep.get('path')}" for ep in v.get("api_endpoints", [])]
|
||||
ep_summary = ", ".join(eps) if eps else "(no direct endpoint)"
|
||||
print(f" • {c_bold(k):<16} {c_cyan(name):<30} {c_yellow(tab_sel)}")
|
||||
print(f" {c_dim('API:')} {ep_summary}")
|
||||
if v.get("assistive_recipe"):
|
||||
print(f" {c_dim('Hint:')} {v.get('assistive_recipe')[:100]}...")
|
||||
print()
|
||||
|
||||
|
||||
def handle_sentence(name: Optional[str] = None, json_mode: bool = False):
|
||||
data = load_sentence_structures()
|
||||
structs = data.get("structures", {})
|
||||
|
||||
if name:
|
||||
key = name.lower().strip()
|
||||
s = structs.get(key)
|
||||
if not s:
|
||||
for k, val in structs.items():
|
||||
if key in k or key in val.get("name", "").lower() or key in val.get("protocol_tag", "").lower():
|
||||
s = val
|
||||
key = k
|
||||
break
|
||||
if not s:
|
||||
err = {"error": f"Sentence structure '{name}' not found", "available": list(structs.keys())}
|
||||
if json_mode:
|
||||
print(json.dumps(err, indent=2))
|
||||
else:
|
||||
print(c_red(f"Error: Sentence structure '{name}' not found. Available: {', '.join(structs.keys())}"))
|
||||
sys.exit(1)
|
||||
|
||||
if json_mode:
|
||||
print(json.dumps({key: s}, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold(f"\n=== Protocol Structure: {s.get('name')} (`{key}`) ==="))
|
||||
print(f"{c_cyan('Tag:')} {c_bold(s.get('protocol_tag'))}")
|
||||
print(f"{c_cyan('Template:')} {c_green(s.get('template'))}")
|
||||
print(f"{c_cyan('Lifecycle:')} {c_yellow(s.get('lifecycle_transition', ''))}")
|
||||
print(f"\n{c_bold('Description:')}\n {s.get('description')}")
|
||||
print(f"\n{c_bold('Required Fields:')} {', '.join(s.get('required_fields', []))}")
|
||||
if s.get("optional_fields"):
|
||||
print(f"{c_bold('Optional Fields:')} {', '.join(s.get('optional_fields', []))}")
|
||||
print(f"\n{c_bold('Example:')}\n {c_cyan(s.get('example'))}")
|
||||
print(f"\n{c_bold('Reply Expectation:')}\n {s.get('reply_expectation')}")
|
||||
print()
|
||||
return
|
||||
|
||||
if json_mode:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold("\n=== Agent Sentence Structures & Conversational Contracts ==="))
|
||||
print(c_dim("Format rules enforced by response-harvester and self_main_loop.\n"))
|
||||
|
||||
for k, s in structs.items():
|
||||
tag = s.get("protocol_tag", "")
|
||||
desc = s.get("description", "")
|
||||
print(f" • {c_bold(k):<18} {c_green(tag):<25} {s.get('name')}")
|
||||
print(f" {c_dim(desc)}")
|
||||
print(f" {c_dim('Template:')} {c_cyan(s.get('template', ''))}")
|
||||
print()
|
||||
|
||||
|
||||
def handle_regex(name: Optional[str] = None, test_str: Optional[str] = None, json_mode: bool = False):
|
||||
data = load_regex_patterns()
|
||||
pats = data.get("patterns", {})
|
||||
|
||||
if name:
|
||||
key = name.lower().strip()
|
||||
p = pats.get(key)
|
||||
if not p:
|
||||
for k, val in pats.items():
|
||||
if key in k or key in val.get("name", "").lower():
|
||||
p = val
|
||||
key = k
|
||||
break
|
||||
if not p:
|
||||
err = {"error": f"Regex pattern '{name}' not found", "available": list(pats.keys())}
|
||||
if json_mode:
|
||||
print(json.dumps(err, indent=2))
|
||||
else:
|
||||
print(c_red(f"Error: Pattern '{name}' not found. Available: {', '.join(pats.keys())}"))
|
||||
sys.exit(1)
|
||||
|
||||
compiled, comp_err = compile_pattern(p)
|
||||
|
||||
test_result = None
|
||||
if test_str is not None:
|
||||
if compiled:
|
||||
m = compiled.search(test_str)
|
||||
test_result = {
|
||||
"matched": bool(m),
|
||||
"match_span": m.span() if m else None,
|
||||
"matched_text": m.group(0) if m else None,
|
||||
"named_groups": m.groupdict() if m else {},
|
||||
"groups": list(m.groups()) if m else []
|
||||
}
|
||||
else:
|
||||
test_result = {"matched": False, "error": comp_err}
|
||||
|
||||
if json_mode:
|
||||
out = {key: p, "compiled_ok": bool(compiled)}
|
||||
if test_str is not None:
|
||||
out["test_evaluation"] = test_result
|
||||
print(json.dumps(out, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold(f"\n=== Regex Pattern: {p.get('name')} (`{key}`) ==="))
|
||||
print(f"{c_cyan('Pattern:')} {c_yellow(p.get('pattern'))}")
|
||||
print(f"{c_cyan('Flags:')} {', '.join(p.get('flags', [])) or '(none)'}")
|
||||
print(f"{c_cyan('Description:')} {p.get('description')}")
|
||||
print(f"{c_cyan('Usage:')} {c_dim(p.get('usage', ''))}")
|
||||
|
||||
ng = p.get("named_groups", {})
|
||||
if ng:
|
||||
print(c_bold("\nNamed Groups:"))
|
||||
for gname, gdesc in ng.items():
|
||||
print(f" • {c_magenta(gname):<16} {gdesc}")
|
||||
|
||||
if test_str is not None:
|
||||
print(c_bold("\nTest Execution Result:"))
|
||||
print(f" Input: {c_dim(test_str)}")
|
||||
if test_result.get("matched"):
|
||||
print(f" Verdict: {c_green('✔ MATCHED')}")
|
||||
print(f" Matched Text: {c_cyan(test_result['matched_text'])}")
|
||||
if test_result["named_groups"]:
|
||||
print(f" Extracted Tokens:")
|
||||
for k_grp, v_grp in test_result["named_groups"].items():
|
||||
print(f" - {c_magenta(k_grp)}: {c_yellow(str(v_grp))}")
|
||||
else:
|
||||
print(f" Verdict: {c_red('✖ NO MATCH')}")
|
||||
if comp_err:
|
||||
print(f" Compile Error: {comp_err}")
|
||||
print()
|
||||
return
|
||||
|
||||
# List patterns
|
||||
if json_mode:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold("\n=== Master Regex Patterns & Passing Dictionary ==="))
|
||||
print(c_dim("Patterns tested and calibrated across response-harvester, dm, and loop engines.\n"))
|
||||
|
||||
for k, p in pats.items():
|
||||
print(f" • {c_bold(k):<18} {c_green(p.get('name'))}")
|
||||
print(f" {c_dim('Pattern:')} {c_yellow(p.get('pattern'))}")
|
||||
print(f" {c_dim(p.get('description'))}")
|
||||
print()
|
||||
|
||||
|
||||
def handle_parse(candidate_str: str, json_mode: bool = False):
|
||||
"""Pass candidate_str through all registered regexes and extract tokens."""
|
||||
data = load_regex_patterns()
|
||||
pats = data.get("patterns", {})
|
||||
|
||||
matches = []
|
||||
for k, p in pats.items():
|
||||
compiled, err = compile_pattern(p)
|
||||
if not compiled:
|
||||
continue
|
||||
m = compiled.search(candidate_str)
|
||||
if m:
|
||||
matches.append({
|
||||
"pattern_key": k,
|
||||
"pattern_name": p.get("name"),
|
||||
"matched_text": m.group(0),
|
||||
"named_groups": m.groupdict(),
|
||||
"span": m.span()
|
||||
})
|
||||
|
||||
if json_mode:
|
||||
print(json.dumps({
|
||||
"input": candidate_str,
|
||||
"matched_patterns_count": len(matches),
|
||||
"matches": matches
|
||||
}, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold(f"\n=== Regex Parse Analysis ==="))
|
||||
print(f"Input: {c_dim(candidate_str)}\n")
|
||||
|
||||
if not matches:
|
||||
print(c_yellow(" ⚠ No registered regex pattern matched this string."))
|
||||
return
|
||||
|
||||
print(c_green(f"Matched {len(matches)} pattern(s):"))
|
||||
for match in matches:
|
||||
print(f"\n • Pattern: {c_bold(match['pattern_name'])} (`{c_cyan(match['pattern_key'])}`)")
|
||||
print(f" Matched Chunk: {c_yellow(match['matched_text'])}")
|
||||
ng = match["named_groups"]
|
||||
if ng:
|
||||
print(f" Extracted Tokens:")
|
||||
for gk, gv in ng.items():
|
||||
print(f" - {c_magenta(gk)}: {c_green(str(gv))}")
|
||||
print()
|
||||
|
||||
|
||||
def handle_get(collection: str, key: Optional[str] = None, json_mode: bool = False):
|
||||
col_map = {
|
||||
"manifest": "manifest.json",
|
||||
"sentence": "sentence_structure.json",
|
||||
"sentence_structure": "sentence_structure.json",
|
||||
"regex": "regex_patterns.json",
|
||||
"regex_patterns": "regex_patterns.json",
|
||||
"surfaces": "assistive_surfaces.json",
|
||||
"assistive_surfaces": "assistive_surfaces.json",
|
||||
"cli": "cli_tools.json",
|
||||
"cli_tools": "cli_tools.json",
|
||||
"fleet": "fleet_nodes.json",
|
||||
"fleet_nodes": "fleet_nodes.json",
|
||||
}
|
||||
|
||||
fname = col_map.get(collection.lower().strip())
|
||||
if not fname:
|
||||
print(c_red(f"Error: Unknown collection '{collection}'. Available: {', '.join(col_map.keys())}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
data = load_json_file(fname)
|
||||
if key:
|
||||
# Check top level or primary container
|
||||
val = None
|
||||
for primary in ["structures", "patterns", "views", "domains", "nodes", "collections"]:
|
||||
if primary in data and isinstance(data[primary], dict) and key in data[primary]:
|
||||
val = data[primary][key]
|
||||
break
|
||||
if val is None and key in data:
|
||||
val = data[key]
|
||||
if val is None:
|
||||
print(c_red(f"Error: Key '{key}' not found in {fname}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
data = {key: val}
|
||||
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
def handle_cli(domain: Optional[str] = None, json_mode: bool = False):
|
||||
data = load_cli_tools()
|
||||
domains = data.get("domains", {})
|
||||
|
||||
if domain:
|
||||
d = domains.get(domain.lower().strip())
|
||||
if not d:
|
||||
print(c_red(f"Error: CLI domain '{domain}' not found. Available: {', '.join(domains.keys())}"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if json_mode:
|
||||
print(json.dumps({domain: d}, indent=2))
|
||||
return
|
||||
print(c_bold(f"\n=== CLI Tool Domain: super {domain} / box {domain} ==="))
|
||||
print(f"Summary: {d.get('summary')}\n")
|
||||
for sub in d.get("subcommands", []):
|
||||
print(f" • {c_green(sub['cmd'])}")
|
||||
print(f" {c_dim(sub['desc'])}\n")
|
||||
return
|
||||
|
||||
if json_mode:
|
||||
print(json.dumps(data, indent=2))
|
||||
return
|
||||
|
||||
print(c_bold("\n=== Unified NetVM & Box CLI Tool Catalog ==="))
|
||||
print(c_dim("Powered by super-cli.py and bin/muse wrapper.\n"))
|
||||
for dom_k, dom_v in domains.items():
|
||||
print(f" • {c_bold(dom_k):<16} {c_cyan(dom_v.get('summary'))}")
|
||||
for sub in dom_v.get("subcommands", [])[:2]:
|
||||
print(f" - {c_dim(sub['cmd'])}")
|
||||
print()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI Argument Parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_parser():
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--json", action="store_true", help="Output machine-readable JSON")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="docs-lookup — Internal Agent & Operator Documentation Query Engine",
|
||||
parents=[common],
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
sub = parser.add_subparsers(dest="action")
|
||||
|
||||
p_search = sub.add_parser("search", parents=[common], help="Full-text search across docs_internal")
|
||||
p_search.add_argument("query", help="Search keyword or phrase")
|
||||
|
||||
p_surfaces = sub.add_parser("surfaces", parents=[common], help="Lookup assistive surfaces for box.muse-dev.online")
|
||||
p_surfaces.add_argument("name", nargs="?", default=None, help="Surface or tab name")
|
||||
|
||||
p_sentence = sub.add_parser("sentence", parents=[common], help="Lookup agent sentence structures & protocols")
|
||||
p_sentence.add_argument("name", nargs="?", default=None, help="Structure name (e.g. work_order, result)")
|
||||
|
||||
p_regex = sub.add_parser("regex", parents=[common], help="Lookup and test regex patterns")
|
||||
p_regex.add_argument("name", nargs="?", default=None, help="Pattern name (e.g. work_order, verb)")
|
||||
p_regex.add_argument("--test", dest="test_str", default=None, help="Test string to evaluate against pattern")
|
||||
|
||||
p_parse = sub.add_parser("parse", parents=[common], help="Parse an agent utterance through all regex patterns")
|
||||
p_parse.add_argument("string", help="String/utterance to parse")
|
||||
|
||||
p_get = sub.add_parser("get", parents=[common], help="Query raw JSON collection and key")
|
||||
p_get.add_argument("collection", help="Collection name (sentence, regex, surfaces, cli, fleet)")
|
||||
p_get.add_argument("key", nargs="?", default=None, help="Optional specific key")
|
||||
|
||||
p_cli = sub.add_parser("cli", parents=[common], help="Lookup CLI commands and syntax")
|
||||
p_cli.add_argument("domain", nargs="?", default=None, help="CLI domain (fleet, dm, job, etc.)")
|
||||
|
||||
p_overview = sub.add_parser("overview", parents=[common], help="Database overview and collections manifest")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
act = args.action
|
||||
json_mode = getattr(args, "json", False)
|
||||
|
||||
if not act or act == "overview":
|
||||
handle_overview(json_mode)
|
||||
elif act == "search":
|
||||
handle_search(args.query, json_mode)
|
||||
elif act == "surfaces":
|
||||
handle_surfaces(args.name, json_mode)
|
||||
elif act == "sentence":
|
||||
handle_sentence(args.name, json_mode)
|
||||
elif act == "regex":
|
||||
handle_regex(args.name, args.test_str, json_mode)
|
||||
elif act == "parse":
|
||||
handle_parse(args.string, json_mode)
|
||||
elif act == "get":
|
||||
handle_get(args.collection, args.key, json_mode)
|
||||
elif act == "cli":
|
||||
handle_cli(args.domain, json_mode)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+32
-30
@@ -49,7 +49,7 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
import ssl
|
||||
|
||||
# ---------------------------------------------------------------- config
|
||||
@@ -139,33 +139,34 @@ def check_token(token):
|
||||
|
||||
|
||||
def check_nonce(nonce):
|
||||
now = time.time()
|
||||
fresh = []
|
||||
try:
|
||||
with open(NONCE_FILE) as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
n, t = parts
|
||||
try:
|
||||
if now - float(t) < 2 * SIG_MAX_SKEW:
|
||||
fresh.append((n, t))
|
||||
except ValueError:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if any(n == nonce for n, _ in fresh):
|
||||
return False
|
||||
fresh.append((nonce, str(now)))
|
||||
try:
|
||||
with open(NONCE_FILE, 'w') as f:
|
||||
for n, t in fresh:
|
||||
f.write(f'{n} {t}\n')
|
||||
os.chmod(NONCE_FILE, 0o600)
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
with _nonce_lock:
|
||||
now = time.time()
|
||||
fresh = []
|
||||
try:
|
||||
with open(NONCE_FILE) as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
n, t = parts
|
||||
try:
|
||||
if now - float(t) < 2 * SIG_MAX_SKEW:
|
||||
fresh.append((n, t))
|
||||
except ValueError:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if any(n == nonce for n, _ in fresh):
|
||||
return False
|
||||
fresh.append((nonce, str(now)))
|
||||
try:
|
||||
with open(NONCE_FILE, 'w') as f:
|
||||
for n, t in fresh:
|
||||
f.write(f'{n} {t}\n')
|
||||
os.chmod(NONCE_FILE, 0o600)
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_signature(identity, payload, signature):
|
||||
@@ -364,7 +365,7 @@ def _dm_read_validate(raw):
|
||||
def _dm_read_build(a):
|
||||
return [sys.executable, os.path.join(BIN_DIR, 'dm.py'), 'read',
|
||||
'--agent', a['agent'], '--target', a['target'],
|
||||
'--limit', str(a['limit'])]
|
||||
'--n', str(a['limit'])]
|
||||
|
||||
|
||||
def _job_run_validate(raw):
|
||||
@@ -1294,6 +1295,7 @@ def permitted(ident, op):
|
||||
# ------------------------------------------------------- audit log
|
||||
|
||||
_audit_lock = threading.Lock()
|
||||
_nonce_lock = threading.Lock()
|
||||
|
||||
|
||||
def audit(entry):
|
||||
@@ -1501,7 +1503,7 @@ def main():
|
||||
BIN_DIR, JOBS_DIR = args.bin_dir, args.jobs_dir
|
||||
WORK_DIR = args.work_dir
|
||||
|
||||
server = HTTPServer((args.host, args.port), Handler)
|
||||
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
||||
|
||||
cert_file = args.cert_file
|
||||
key_file = args.key_file
|
||||
|
||||
@@ -185,22 +185,29 @@ HEALTHY_AGENTS=""
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Agent approval blockage detection (catches agents held up on approvals) ---
|
||||
# --- Agent approval blockage & input wait detection ---
|
||||
"$BIN/netvm-registry.py" 2>/dev/null | while IFS=: read -r node port; do
|
||||
[ -n "$node" ] || continue
|
||||
cond="approval:$node"
|
||||
pending_info=$(python3 -c "
|
||||
import sys
|
||||
node_data=$(python3 -c "
|
||||
import sys, json
|
||||
sys.path.insert(0, '$BIN')
|
||||
import approvals
|
||||
info = approvals.inspect_node_approvals('$node')
|
||||
if info.get('has_pending'):
|
||||
print(f\"{info.get('ip') or 'unknown'}|{info.get('title') or ''}\")
|
||||
" 2>/dev/null || true)
|
||||
out = {
|
||||
'has_pending': info.get('has_pending', False),
|
||||
'target': info.get('target') or info.get('ip') or 'unknown',
|
||||
'title': info.get('title') or '',
|
||||
'waits': info.get('input_waits') or []
|
||||
}
|
||||
print(json.dumps(out))
|
||||
" 2>/dev/null || echo '{"has_pending":false,"target":"unknown","title":"","waits":[]}')
|
||||
|
||||
if [ -n "$pending_info" ]; then
|
||||
# 1. Egress permission dialog
|
||||
cond="approval:$node"
|
||||
has_pending=$(python3 -c "import json,sys; print(1 if json.loads(sys.argv[1]).get('has_pending') else 0)" "$node_data" 2>/dev/null || echo 0)
|
||||
if [ "$has_pending" = "1" ]; then
|
||||
failing=1
|
||||
target="${pending_info%%|*}"
|
||||
target=$(python3 -c "import json,sys; print(json.loads(sys.argv[1]).get('target','unknown'))" "$node_data" 2>/dev/null || echo unknown)
|
||||
detail="Agent $node held up on browser approval for $target"
|
||||
else
|
||||
failing=0
|
||||
@@ -220,6 +227,37 @@ if info.get('has_pending'):
|
||||
log "$cond still critical x$fails — re-page suppressed"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 2. Sidebar task waiting on human input
|
||||
cond_in="input_wait:$node"
|
||||
wait_summary=$(python3 -c "
|
||||
import json,sys
|
||||
w = json.loads(sys.argv[1]).get('waits', [])
|
||||
if w:
|
||||
print('; '.join(f\"{item.get('task')}: {item.get('status')}\" for item in w)[:120])
|
||||
" "$node_data" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$wait_summary" ]; then
|
||||
failing_in=1
|
||||
detail_in="Agent $node task waiting for human input: $wait_summary"
|
||||
else
|
||||
failing_in=0
|
||||
detail_in="Agent $node tasks running"
|
||||
fi
|
||||
injected "$cond_in" && failing_in=1
|
||||
read -r action_in fails_in < <(state_machine "$cond_in" "$failing_in")
|
||||
case "$action_in" in
|
||||
ALERT_FIRST|ALERT_REALERT)
|
||||
emit_record "ALERT" "$cond_in" "$detail_in" "$fails_in"
|
||||
echo "$cond_in" >> "$STATE_DIR/.alerts.tmp"
|
||||
;;
|
||||
RECOVERY)
|
||||
emit_record "RECOVERY" "$cond_in" "$detail_in" "$fails_in"
|
||||
;;
|
||||
SUPPRESSED)
|
||||
log "$cond_in still critical x$fails_in — re-page suppressed"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Warp partition detection (2026-10-04) ---
|
||||
|
||||
+12
-2
@@ -632,7 +632,7 @@ def diagnose_breaks() -> list:
|
||||
if app.get("has_pending"):
|
||||
node = app["node"]
|
||||
is_trusted = app.get("is_trusted", False)
|
||||
ip = app.get("ip") or "unknown target"
|
||||
ip = app.get("target") or app.get("ip") or "unknown target"
|
||||
breaks.append({
|
||||
"type": "approval_blocked",
|
||||
"severity": "WARNING" if is_trusted else "CRITICAL",
|
||||
@@ -641,6 +641,16 @@ def diagnose_breaks() -> list:
|
||||
"detail": f"Agent {node} is held up on browser approval for {ip}",
|
||||
"remedy": f"Run 'box approvals auto' or 'box approvals allow {node}'."
|
||||
})
|
||||
for w in app.get("input_waits") or []:
|
||||
node = app["node"]
|
||||
breaks.append({
|
||||
"type": "input_wait",
|
||||
"severity": "WARNING",
|
||||
"component": f"node:{node}",
|
||||
"agent": node,
|
||||
"detail": f"Agent {node} task '{w.get('task')}' is waiting: {w.get('status')} ({w.get('when')})",
|
||||
"remedy": f"Open {node}'s task and answer it, or 'box approvals check --node {node}'."
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -758,7 +768,7 @@ def remediate_breaks(dry_run=False) -> dict:
|
||||
if app.get("has_pending") and app.get("is_trusted"):
|
||||
node = app["node"]
|
||||
if not dry_run:
|
||||
approvals.allow_node_approval(node, caller="loop-remediate")
|
||||
approvals.allow_node_approval(node, always=True, caller="loop-remediate")
|
||||
remediated.append({
|
||||
"type": "approval_auto_allowed",
|
||||
"agent": node,
|
||||
|
||||
@@ -503,6 +503,26 @@ def main():
|
||||
elif job.get("target"):
|
||||
target = job.get("target").strip()
|
||||
|
||||
# Pre-dispatch approval & input check (auto-approve trusted; warn if blocked)
|
||||
if not dry_run:
|
||||
try:
|
||||
import approvals
|
||||
app_info = approvals.inspect_node_approvals(agent)
|
||||
if app_info.get("has_pending"):
|
||||
if app_info.get("is_trusted"):
|
||||
print(f"Pre-dispatch: auto-approving trusted request for {agent} ({app_info.get('target')})")
|
||||
approvals.allow_node_approval(agent, always=True, caller="job-dispatch")
|
||||
else:
|
||||
print(f"Warning: Agent '{agent}' has untrusted pending approval ({app_info.get('target')}). Dispatch may stall.", file=sys.stderr)
|
||||
log_event("job_dispatch_approval_blocked", {"job_id": job_id, "agent": agent, "target": app_info.get("target")})
|
||||
elif app_info.get("status") == "INPUT_WAIT":
|
||||
waits = app_info.get("input_waits", [])
|
||||
w_desc = "; ".join(w.get("task", "") for w in waits)[:80]
|
||||
print(f"Notice: Agent '{agent}' has task waiting for input ({w_desc}).", file=sys.stderr)
|
||||
log_event("job_dispatch_agent_input_wait", {"job_id": job_id, "agent": agent, "waits": w_desc})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Sidechat-first policy (2026-10-04): refuse to dispatch to main chat
|
||||
# unless the job explicitly opts in. Never fall back to main silently.
|
||||
allow_main = bool(job.get("allow_main_chat")) or args.allow_main_chat
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/super/Projects/NetVM/bin/docs-lookup.py
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""lookup_engine.py — Shared Zero-Downtime Hot-Reloading Pattern & Schema Engine.
|
||||
|
||||
Provides authoritative runtime access to lookup_internal/ databases for
|
||||
daemons (response-harvester, self_main_loop, job-dispatch), CLI commands,
|
||||
and agents.
|
||||
|
||||
Features:
|
||||
- Dynamic mtime-based zero-downtime hot reloading of compiled regex patterns.
|
||||
- Pre-flight soft validation of outbound agent sentences and work orders.
|
||||
- Direct helper functions for core protocol regexes (RESULT, VERB, TOOL, etc.).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
NETVM_ROOT = Path(__file__).resolve().parent.parent
|
||||
LOOKUP_INTERNAL = NETVM_ROOT / "lookup_internal"
|
||||
if not LOOKUP_INTERNAL.exists() and (NETVM_ROOT / "docs_internal").exists():
|
||||
LOOKUP_INTERNAL = NETVM_ROOT / "docs_internal"
|
||||
|
||||
# In-memory cache structures with modification timestamps
|
||||
_CACHE_MTIMES: Dict[str, float] = {}
|
||||
_RAW_CACHE: Dict[str, Any] = {}
|
||||
_COMPILED_PATTERNS: Dict[str, re.Pattern] = {}
|
||||
|
||||
# Fallback hardcoded regexes in case files are missing or unreadable
|
||||
_FALLBACK_RESULT_RE = re.compile(r"\[RESULT\s+([A-Za-z0-9_/-]+)\]\s*(.*?)(?=\[RESULT\s|\Z)", re.S)
|
||||
_FALLBACK_VERB_RE = re.compile(r"\[(ACK|CLAIM|RESULT|DECLINE|NO-ACTION)\s+([A-Za-z0-9_/-]+)\]")
|
||||
_FALLBACK_TOOL_RE = re.compile(r"\[(TOOL|EXEC)\s+([a-zA-Z0-9_.-]+)\s+(\{.*?\})\]", re.S)
|
||||
_FALLBACK_CONTRACT_FOOTER = (
|
||||
"Reply: [ACK id] seen | [CLAIM id] mine | "
|
||||
"[RESULT id] done | [DECLINE id] | [NO-ACTION id]."
|
||||
)
|
||||
|
||||
|
||||
def load_lookup_json(filename: str) -> Dict[str, Any]:
|
||||
"""Load JSON from lookup_internal/ with mtime-based caching."""
|
||||
target_path = LOOKUP_INTERNAL / filename
|
||||
if not target_path.is_file():
|
||||
return {}
|
||||
|
||||
try:
|
||||
current_mtime = os.path.getmtime(target_path)
|
||||
except OSError:
|
||||
return _RAW_CACHE.get(filename, {})
|
||||
|
||||
if filename in _RAW_CACHE and _CACHE_MTIMES.get(filename) == current_mtime:
|
||||
return _RAW_CACHE[filename]
|
||||
|
||||
try:
|
||||
with open(target_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
_RAW_CACHE[filename] = data
|
||||
_CACHE_MTIMES[filename] = current_mtime
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"[lookup_engine] Warning: Error reading {target_path}: {e}", file=sys.stderr)
|
||||
return _RAW_CACHE.get(filename, {})
|
||||
|
||||
|
||||
def _refresh_compiled_patterns_if_needed():
|
||||
"""Checks regex_patterns.json mtime and recompiles if changed."""
|
||||
global _COMPILED_PATTERNS
|
||||
data = load_lookup_json("regex_patterns.json")
|
||||
patterns_data = data.get("patterns", {})
|
||||
|
||||
target_path = LOOKUP_INTERNAL / "regex_patterns.json"
|
||||
current_mtime = _CACHE_MTIMES.get("regex_patterns.json", 0.0)
|
||||
compiled_mtime = _CACHE_MTIMES.get("_compiled_patterns_mtime", 0.0)
|
||||
|
||||
if current_mtime == compiled_mtime and _COMPILED_PATTERNS:
|
||||
return
|
||||
|
||||
new_compiled = {}
|
||||
for key, entry in patterns_data.items():
|
||||
raw_pat = entry.get("pattern", "")
|
||||
flag_names = entry.get("flags", [])
|
||||
flags = 0
|
||||
for fn in flag_names:
|
||||
if hasattr(re, fn):
|
||||
flags |= getattr(re, fn)
|
||||
try:
|
||||
new_compiled[key] = re.compile(raw_pat, flags)
|
||||
except Exception as e:
|
||||
print(f"[lookup_engine] Warning: Failed to compile pattern '{key}': {e}", file=sys.stderr)
|
||||
|
||||
_COMPILED_PATTERNS = new_compiled
|
||||
_CACHE_MTIMES["_compiled_patterns_mtime"] = current_mtime
|
||||
|
||||
|
||||
def get_compiled_pattern(name: str) -> Optional[re.Pattern]:
|
||||
"""Retrieve a compiled pattern by name, hot-reloading if the database was modified."""
|
||||
_refresh_compiled_patterns_if_needed()
|
||||
return _COMPILED_PATTERNS.get(name)
|
||||
|
||||
|
||||
def get_all_compiled_patterns() -> Dict[str, re.Pattern]:
|
||||
"""Retrieve all compiled patterns with automatic hot-reloading."""
|
||||
_refresh_compiled_patterns_if_needed()
|
||||
return dict(_COMPILED_PATTERNS)
|
||||
|
||||
|
||||
def get_result_regex() -> re.Pattern:
|
||||
"""Return the canonical [RESULT ...] regex."""
|
||||
p = get_compiled_pattern("result")
|
||||
return p if p is not None else _FALLBACK_RESULT_RE
|
||||
|
||||
|
||||
def get_verb_regex() -> re.Pattern:
|
||||
"""Return the canonical [VERB ...] regex (ACK|CLAIM|RESULT|DECLINE|NO-ACTION)."""
|
||||
p = get_compiled_pattern("verb")
|
||||
return p if p is not None else _FALLBACK_VERB_RE
|
||||
|
||||
|
||||
def get_tool_regex() -> re.Pattern:
|
||||
"""Return the canonical [TOOL ...] regex."""
|
||||
p = get_compiled_pattern("tool_call")
|
||||
return p if p is not None else _FALLBACK_TOOL_RE
|
||||
|
||||
|
||||
def get_contract_footer() -> str:
|
||||
"""Return standard contract footer string."""
|
||||
struct_data = load_lookup_json("sentence_structure.json")
|
||||
cf = struct_data.get("structures", {}).get("contract_footer", {})
|
||||
return cf.get("example") or _FALLBACK_CONTRACT_FOOTER
|
||||
|
||||
|
||||
def parse_agent_utterance(text: str) -> List[Dict[str, Any]]:
|
||||
"""Pass text through all registered patterns and extract matched tokens."""
|
||||
_refresh_compiled_patterns_if_needed()
|
||||
patterns_data = load_lookup_json("regex_patterns.json").get("patterns", {})
|
||||
matches = []
|
||||
|
||||
for key, compiled in _COMPILED_PATTERNS.items():
|
||||
m = compiled.search(text)
|
||||
if m:
|
||||
entry = patterns_data.get(key, {})
|
||||
matches.append({
|
||||
"pattern_key": key,
|
||||
"pattern_name": entry.get("name", key),
|
||||
"matched_text": m.group(0),
|
||||
"named_groups": m.groupdict(),
|
||||
"span": m.span()
|
||||
})
|
||||
return matches
|
||||
|
||||
|
||||
def validate_outbound_sentence(text: str) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||
"""Pre-flight check for outbound messages sent via CLI.
|
||||
|
||||
Returns:
|
||||
(is_valid, matched_kind, warning_or_hint)
|
||||
"""
|
||||
cleaned = text.strip()
|
||||
# If message starts with bracketed protocol marker
|
||||
if cleaned.startswith("["):
|
||||
marker = cleaned.split("]")[0] + "]"
|
||||
upper_marker = marker.upper()
|
||||
|
||||
if upper_marker.startswith("[WO:") or upper_marker.startswith("[WORKORDER:"):
|
||||
wo_pat = get_compiled_pattern("work_order")
|
||||
if wo_pat and not wo_pat.search(cleaned):
|
||||
hint = (
|
||||
"Notice: Message starts with a Work Order marker but does not match canonical structure.\n"
|
||||
" Expected format: [WO:<id>] [from <sender>] <title> — <body>\n"
|
||||
" Example: [WO:7fce46e0] [from super] Audit endpoints — Check GET /api/stats\n"
|
||||
" Hint: Query 'box lookup sentence work_order' for full spec."
|
||||
)
|
||||
return False, "work_order", hint
|
||||
return True, "work_order", None
|
||||
|
||||
if upper_marker.startswith("[ACK:") or upper_marker.startswith("[ACK "):
|
||||
ack_pat = get_compiled_pattern("ack")
|
||||
verb_pat = get_compiled_pattern("verb")
|
||||
if (ack_pat and not ack_pat.search(cleaned)) and (verb_pat and not verb_pat.search(cleaned)):
|
||||
hint = (
|
||||
"Notice: Message looks like an ACK but deviates from standard syntax.\n"
|
||||
" Expected format: [ACK:<id>] [from <sender>] or [ACK <id>]\n"
|
||||
" Example: [ACK:7fce46e0] [from 646]"
|
||||
)
|
||||
return False, "ack", hint
|
||||
return True, "ack", None
|
||||
|
||||
if upper_marker.startswith("[RESULT"):
|
||||
res_pat = get_result_regex()
|
||||
if not res_pat.search(cleaned):
|
||||
hint = (
|
||||
"Notice: Message starts with [RESULT] but deviates from standard syntax.\n"
|
||||
" Expected format: [RESULT <job_id>] <status> <summary>\n"
|
||||
" Example: [RESULT 7fce46e0] OK Task completed successfully"
|
||||
)
|
||||
return False, "result", hint
|
||||
return True, "result", None
|
||||
|
||||
return True, "plain_message", None
|
||||
@@ -16,20 +16,28 @@ VALID_ACCOUNTS=("muse" "pip" "646" "opm" "def" "dev")
|
||||
show_usage() {
|
||||
echo "Usage: muse <account> <command> [arguments...]"
|
||||
echo " muse -a <account> <command> [arguments...]"
|
||||
echo " muse <global-command> [arguments...]"
|
||||
echo ""
|
||||
echo "Available accounts:"
|
||||
for acct in "${VALID_ACCOUNTS[@]}"; do
|
||||
echo " • $acct"
|
||||
done
|
||||
echo ""
|
||||
echo "Common commands:"
|
||||
echo "Global lookups & tools:"
|
||||
echo " tmux [args...] Manage shared Muse tmux sessions (new, send, capture, ls, kill, prune)"
|
||||
echo " status Fleet overview & node vitality"
|
||||
echo " threads List registered threads and sidechats across fleet"
|
||||
echo " unread View unread counts across all agents"
|
||||
echo " lookup [subcommand] Unified lookup (fleet, threads, unread, approvals, key)"
|
||||
echo " passkey (or key) View passkey location (VM-only), PIN, & agent approval protocol"
|
||||
echo ""
|
||||
echo "Per-account commands:"
|
||||
echo " chat [--thread <id>] Launch interactive conversational shell / REPL"
|
||||
echo " tmux <cmd> [args...] Manage shared Muse tmux sessions (new, send, capture, ls, kill)"
|
||||
echo " status Check agent status, sessions, and unread"
|
||||
echo " threads List active threads and sidechats"
|
||||
echo " status Check account status, sessions, and unread"
|
||||
echo " threads List active threads and sidechats for account"
|
||||
echo " history --thread <id> View message history"
|
||||
echo " send --thread <id> msg Send message to an agent"
|
||||
echo " unread View unread counts"
|
||||
echo " unread View unread counts for account"
|
||||
echo ""
|
||||
echo "Authentication & Cookie Management:"
|
||||
echo " Cookies are isolated per-node in ~/.config/muse-cli/<account>/"
|
||||
@@ -37,6 +45,40 @@ show_usage() {
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Direct top-level global actions that do not require an account
|
||||
if [[ $# -gt 0 ]]; then
|
||||
case "$1" in
|
||||
tmux)
|
||||
shift
|
||||
exec python3 "$NETVM_BIN/muse-tmux.py" "$@"
|
||||
;;
|
||||
passkey|key)
|
||||
shift
|
||||
exec python3 "$NETVM_BIN/super-cli.py" passkey "$@"
|
||||
;;
|
||||
lookup|lookups)
|
||||
shift
|
||||
exec python3 "$NETVM_BIN/super-cli.py" lookup "$@"
|
||||
;;
|
||||
fleet)
|
||||
shift
|
||||
exec python3 "$NETVM_BIN/super-cli.py" fleet "$@"
|
||||
;;
|
||||
threads)
|
||||
shift
|
||||
exec python3 "$NETVM_BIN/super-cli.py" thread list "$@"
|
||||
;;
|
||||
unread)
|
||||
shift
|
||||
exec python3 "$NETVM_BIN/super-cli.py" lookup unread "$@"
|
||||
;;
|
||||
status)
|
||||
shift
|
||||
exec python3 "$NETVM_BIN/super-cli.py" fleet status "$@"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
ACCOUNT=""
|
||||
POSITIONAL=()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Socket location: /tmp/tmux-muse.sock (shared across fleet agents & super).
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import subprocess
|
||||
import argparse
|
||||
|
||||
@@ -181,6 +182,13 @@ def cmd_attach(args):
|
||||
session = args.session
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
|
||||
# Graceful non-interactive fallback for autonomous agents
|
||||
if not sys.stdin.isatty():
|
||||
sys.stderr.write(f"Notice: Non-interactive terminal (no tty). Capturing recent scrollback for '{session}':\n\n")
|
||||
setattr(args, "lines", getattr(args, "lines", 30) or 30)
|
||||
return cmd_capture(args)
|
||||
|
||||
if node:
|
||||
cmd = ["/home/super/Projects/NetVM/bin/netvm-exec.sh", node, "--", TMUX_BIN, "-S", f"/tmp/tmux-{node}.sock", "attach", "-t", session]
|
||||
os.execv(cmd[0], cmd)
|
||||
|
||||
+11
-4
@@ -280,10 +280,17 @@ def make_digest_id(agent):
|
||||
# recursive box->agent->box discipline: post the RESULT back in this thread
|
||||
# (box records it and dispatches the next chained step); never DM the next
|
||||
# agent directly. ~166 chars, within the 600-char digest budget.
|
||||
CONTRACT_FOOTER = ("Reply: [ACK id] seen | [CLAIM id] mine | "
|
||||
"[RESULT id] done | [DECLINE id] | [NO-ACTION id]. "
|
||||
"Report back here. Box dispatches the next step; "
|
||||
"do not DM the next agent directly.")
|
||||
try:
|
||||
import lookup_engine
|
||||
HAS_LOOKUP_ENGINE = True
|
||||
except ImportError:
|
||||
HAS_LOOKUP_ENGINE = False
|
||||
|
||||
_DEFAULT_CONTRACT_FOOTER = ("Reply: [ACK id] seen | [CLAIM id] mine | "
|
||||
"[RESULT id] done | [DECLINE id] | [NO-ACTION id]. "
|
||||
"Report back here. Box dispatches the next step; "
|
||||
"do not DM the next agent directly.")
|
||||
CONTRACT_FOOTER = lookup_engine.get_contract_footer() if HAS_LOOKUP_ENGINE else _DEFAULT_CONTRACT_FOOTER
|
||||
|
||||
|
||||
def send_prompt(sender, agent, sidechat, digest):
|
||||
|
||||
+520
-19
@@ -403,7 +403,7 @@ def cmd_approvals(args):
|
||||
action = getattr(args, "app_action", None) or "check"
|
||||
node = getattr(args, "node", None)
|
||||
|
||||
if action in ("check", "list"):
|
||||
if action in ("check", "list", "inspect"):
|
||||
nodes = [node] if node else VALID_NODES
|
||||
fleet = approvals.check_fleet_approvals(nodes)
|
||||
if getattr(args, "json", False):
|
||||
@@ -415,6 +415,7 @@ def cmd_approvals(args):
|
||||
rows = []
|
||||
pending_count = 0
|
||||
untrusted_count = 0
|
||||
input_wait_count = 0
|
||||
|
||||
for it in fleet:
|
||||
n = it["node"]
|
||||
@@ -422,12 +423,28 @@ def cmd_approvals(args):
|
||||
if st == "PENDING":
|
||||
pending_count += 1
|
||||
badge = badge_err("PENDING") if not it.get("is_trusted") else badge_warn("PENDING")
|
||||
target = it.get("ip") or "-"
|
||||
target = it.get("target") or it.get("ip") or "-"
|
||||
purp = (it.get("purpose") or it.get("title") or "-")[:50]
|
||||
trust = c_green("TRUSTED") if it.get("is_trusted") else c_red("UNTRUSTED")
|
||||
btns = " ".join([f"[{b}]" for b in it.get("buttons", [])])
|
||||
if not it.get("is_trusted"):
|
||||
untrusted_count += 1
|
||||
elif st == "KEY_APPROVAL":
|
||||
pending_count += 1
|
||||
badge = badge_warn("KEY_REQ")
|
||||
target = it.get("target") or "VM passkey"
|
||||
purp = (it.get("purpose") or it.get("title") or "-")[:50]
|
||||
trust = c_cyan("OPERATOR")
|
||||
btns = " ".join([f"[{b}]" for b in it.get("buttons", ["Allow", "Deny"])])
|
||||
untrusted_count += 1
|
||||
elif st == "INPUT_WAIT":
|
||||
waits = it.get("input_waits") or []
|
||||
badge = badge_warn("INPUT")
|
||||
target = "-"
|
||||
purp = "; ".join(f"{w.get('task')}: {w.get('status')}" for w in waits)[:60]
|
||||
trust = "-"
|
||||
btns = c_dim("answer in task")
|
||||
input_wait_count += 1
|
||||
elif st == "UNREACHABLE":
|
||||
badge = badge_dim("OFFLINE")
|
||||
target = "-"
|
||||
@@ -451,16 +468,51 @@ def cmd_approvals(args):
|
||||
|
||||
print_table(headers, rows)
|
||||
|
||||
if input_wait_count > 0:
|
||||
print("\n" + c_yellow(f" ⚠ {input_wait_count} node(s) have tasks waiting for human input (not auto-resolvable)."))
|
||||
if pending_count > 0:
|
||||
print("\n" + c_yellow(f" ⚠ {pending_count} pending approval(s) detected across fleet."))
|
||||
if untrusted_count > 0:
|
||||
print(c_red(f" ⚠ {untrusted_count} UNTRUSTED approval(s) require manual review: 'box approvals allow <node> --force' or 'box approvals deny <node>'."))
|
||||
print(c_red(f" ⚠ {untrusted_count} approval(s) require manual review / key grant: 'box approvals allow <node>' or 'box approvals deny <node>'."))
|
||||
else:
|
||||
print(c_cyan(" All pending approvals are for trusted infrastructure. Run 'box approvals auto' to resolve."))
|
||||
print()
|
||||
else:
|
||||
elif input_wait_count == 0:
|
||||
print("\n" + c_green(" ✔ All agent approval queues clear. No agents blocked.") + "\n")
|
||||
|
||||
# Verbose or inspect breakdown
|
||||
is_verbose = getattr(args, "verbose", False) or action == "inspect"
|
||||
if is_verbose:
|
||||
print(c_bold("\n=== DETAILED NODE INSPECTION ==="))
|
||||
for it in fleet:
|
||||
n = it["node"]
|
||||
if it.get("has_pending") or it.get("input_waits") or action == "inspect":
|
||||
print(f"\n [{c_bold(n)}] Status: {it.get('status')} | Page: {c_cyan(it.get('page_url') or '-')}")
|
||||
if it.get("has_pending"):
|
||||
print(f" Egress Target: {it.get('target') or it.get('ip')} ({'TRUSTED' if it.get('is_trusted') else 'UNTRUSTED'})")
|
||||
print(f" Prompt Title : {it.get('title')}")
|
||||
if it.get("purpose"):
|
||||
print(f" Purpose : {it.get('purpose')}")
|
||||
print(f" Buttons : {', '.join(it.get('buttons') or [])}")
|
||||
if it.get("input_waits"):
|
||||
print(f" Tasks Awaiting Human Input ({len(it['input_waits'])}):")
|
||||
for w in it["input_waits"]:
|
||||
print(f" • {c_bold(w.get('task'))}")
|
||||
print(f" Status: {w.get('status')} ({w.get('when')})")
|
||||
# Correlate with active subagents on this node
|
||||
try:
|
||||
import subagent_tracker
|
||||
active_subs = subagent_tracker.get_active_sessions(n)
|
||||
if active_subs:
|
||||
print(f" Active Subagents on Node ({len(active_subs)}):")
|
||||
for sub in active_subs[-3:]:
|
||||
s_id = sub.get("session_id", "")[:8]
|
||||
s_title = sub.get("title") or "subagent"
|
||||
print(f" • [{s_id}] {c_bold(s_title)} (spawned: {sub.get('spawned_at', '-')})")
|
||||
except Exception:
|
||||
pass
|
||||
print()
|
||||
|
||||
elif action in ("allow", "approve"):
|
||||
if not node:
|
||||
print(c_red("Error: Must specify node for allow. e.g. 'box approvals allow 646'"), file=sys.stderr)
|
||||
@@ -472,8 +524,11 @@ def cmd_approvals(args):
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
decision_str = "Always allow this site" if always else "Allow once"
|
||||
print(c_green(f"✔ Approved request on node '{node}' ({decision_str}). Dialog dismissed: {res.get('dismissed')}."))
|
||||
if res.get("type") == "key_approval":
|
||||
print(c_green(f"✔ Approved operator key request for node '{node}'. Granted and logged to audit trail."))
|
||||
else:
|
||||
decision_str = "Always allow this site" if always else "Allow once"
|
||||
print(c_green(f"✔ Approved request on node '{node}' ({decision_str}). Dialog dismissed: {res.get('dismissed')}."))
|
||||
else:
|
||||
print(c_red(f"✖ Failed to approve on node '{node}': {res.get('error')}"))
|
||||
sys.exit(2 if "Untrusted" in res.get("error", "") else 1)
|
||||
@@ -487,7 +542,10 @@ def cmd_approvals(args):
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
print(c_green(f"✔ Denied request on node '{node}'. Dialog dismissed: {res.get('dismissed')}."))
|
||||
if res.get("type") == "key_approval":
|
||||
print(c_green(f"✔ Denied operator key request for node '{node}'. Denied and logged to audit trail."))
|
||||
else:
|
||||
print(c_green(f"✔ Denied request on node '{node}'. Dialog dismissed: {res.get('dismissed')}."))
|
||||
else:
|
||||
print(c_red(f"✖ Failed to deny on node '{node}': {res.get('error')}"))
|
||||
sys.exit(1)
|
||||
@@ -535,6 +593,50 @@ def cmd_approvals(args):
|
||||
except KeyboardInterrupt:
|
||||
print("\n" + c_dim("Exited watch mode."))
|
||||
|
||||
elif action == "reply":
|
||||
if not node:
|
||||
print(c_red("Error: Must specify node for reply. e.g. 'box approvals reply pip \"proceed\"'"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
message = getattr(args, "message", "")
|
||||
allow_main = getattr(args, "allow_main_chat", False)
|
||||
res = approvals.reply_node_task(node, message, allow_main_chat=allow_main)
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
print(c_green(f"✔ Dispatched reply to node '{node}': \"{message}\" (thread: {res.get('page_url')})"))
|
||||
else:
|
||||
print(c_red(f"✖ Failed to reply to node '{node}': {res.get('error')}"))
|
||||
sys.exit(2 if "Main Chat" in res.get("error", "") else 1)
|
||||
|
||||
elif action == "dismiss":
|
||||
if not node:
|
||||
print(c_red("Error: Must specify node for dismiss. e.g. 'box approvals dismiss pip'"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
res = approvals.dismiss_node_task(node)
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
if res.get("ok"):
|
||||
print(c_green(f"✔ Dismissed task popup on node '{node}' ({res.get('result')})."))
|
||||
else:
|
||||
print(c_red(f"✖ Failed to dismiss task popup on node '{node}': {res.get('error')}"))
|
||||
sys.exit(1)
|
||||
|
||||
elif action in ("request-key", "request_key"):
|
||||
if not node:
|
||||
print(c_red("Error: Must specify node for request-key. e.g. 'box approvals request-key 646'"), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
reason = getattr(args, "reason", "Operator passkey access requested")
|
||||
caller = os.environ.get("BOX_CALLER") or getattr(args, "from_agent", "super")
|
||||
res = approvals.request_key_approval(node, reason=reason, caller=caller)
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(res, indent=2))
|
||||
return
|
||||
print(c_green(f"✔ Registered passkey approval request for node '{node}'."))
|
||||
print(f" Reason: {c_cyan(reason)}")
|
||||
print(c_dim(f" Operator can approve with: box approvals allow {node}"))
|
||||
|
||||
def resolve_sender(args) -> str:
|
||||
explicit = getattr(args, "from_agent", None)
|
||||
if explicit and explicit != DEFAULT_SENDER:
|
||||
@@ -864,6 +966,15 @@ def cmd_dm_send(args):
|
||||
sys.exit(1)
|
||||
print(c_yellow(" ⚠ [CHAT POLICY OVERRIDE] Main Chat targeted with --allow-main-chat. Keep interaction minimal."))
|
||||
|
||||
# Soft pre-flight grammar check against lookup_internal
|
||||
try:
|
||||
import lookup_engine
|
||||
is_val, kind, hint = lookup_engine.validate_outbound_sentence(message)
|
||||
if not is_val and hint:
|
||||
print(c_yellow(f"\n ⚠ [GRAMMAR NOTICE]\n {hint}\n"), file=sys.stderr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
should_sign = (sender == "super") and not getattr(args, "no_sign", False)
|
||||
mid = None
|
||||
if should_sign:
|
||||
@@ -929,7 +1040,8 @@ def cmd_dm_wo(args):
|
||||
|
||||
wo_id = hashlib.sha256(f"{sender}-{recipient}-{title}-{time.time()}".encode()).hexdigest()[:8]
|
||||
prefix = "[URGENT] " if priority == "urgent" else ""
|
||||
wo_content = f"{prefix}[WO:{wo_id}] {title} — {body}"
|
||||
wo_content = f"{prefix}[WO:{wo_id}] [from {sender}] {title} — {body}"
|
||||
|
||||
|
||||
should_sign = (sender == "super") and not getattr(args, "no_sign", False)
|
||||
if should_sign:
|
||||
@@ -1338,7 +1450,31 @@ def cmd_dm_files(args):
|
||||
# Domain: THREAD (Chat Oversight via box-chat.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
def cmd_thread_list(args):
|
||||
agent = args.agent
|
||||
agent = getattr(args, "agent", None)
|
||||
if not agent:
|
||||
print("\n" + c_bold("=== FLEET REGISTERED SIDECHATS & THREAD MAPPINGS ===") + "\n")
|
||||
sc_file = NETVM_ROOT / "job-sidechats.json"
|
||||
rows = []
|
||||
headers = ["TARGET / ALIAS", "AGENT", "THREAD UUID", "PURPOSE / NOTES"]
|
||||
if sc_file.exists():
|
||||
try:
|
||||
with open(sc_file, "r") as f:
|
||||
data = json.load(f)
|
||||
for k, v in sorted(data.items()):
|
||||
if isinstance(v, dict):
|
||||
ag = v.get("agent", "-")
|
||||
uuid = v.get("thread_uuid") or v.get("uuid") or "-"
|
||||
desc = v.get("description") or v.get("purpose") or "-"
|
||||
rows.append([c_cyan(k), c_bold(ag), uuid, desc[:45]])
|
||||
elif isinstance(v, str):
|
||||
rows.append([c_cyan(k), "-", v, "-"])
|
||||
except Exception as e:
|
||||
print(c_red(f"Error reading job-sidechats.json: {e}"))
|
||||
print_table(headers, rows)
|
||||
print("\n" + c_dim(" To view specific thread: box thread view <agent> <uuid|alias>") + "\n")
|
||||
print(c_dim(" To list agent active sessions: box thread list <agent>") + "\n")
|
||||
return
|
||||
|
||||
threads = []
|
||||
|
||||
def is_noise(title):
|
||||
@@ -1401,6 +1537,29 @@ def cmd_thread_view(args):
|
||||
limit = getattr(args, "limit", 15)
|
||||
messages = []
|
||||
|
||||
# 1. Resolve alias or name if known
|
||||
try:
|
||||
import dm
|
||||
resolved = dm.resolve_sidechat_target(thread_id, agent)
|
||||
if resolved and resolved != thread_id:
|
||||
thread_id = resolved
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. If short UUID prefix (6-12 hex chars), resolve against agent's thread list
|
||||
if re.fullmatch(r"[0-9a-fA-F]{6,12}", str(thread_id).strip()):
|
||||
try:
|
||||
import muse_hybrid
|
||||
gw_threads, _ = muse_hybrid.get_threads(agent)
|
||||
if gw_threads:
|
||||
for t in gw_threads:
|
||||
sid = t.get("session_id", "")
|
||||
if sid.lower().startswith(str(thread_id).strip().lower()):
|
||||
thread_id = sid
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fast path: try fast headless gateway via muse_hybrid (isolated per-node WARP egress)
|
||||
try:
|
||||
import muse_hybrid
|
||||
@@ -3735,14 +3894,278 @@ def cmd_swarm_prune(args):
|
||||
sys.stdout.write(res.stdout)
|
||||
|
||||
|
||||
def cmd_passkey_info(args):
|
||||
"""Display operator passkey reference and agent approval architecture, or fetch from VM."""
|
||||
action = getattr(args, "action", "show") or "show"
|
||||
is_json = getattr(args, "json", False)
|
||||
|
||||
if action in ("fetch", "get"):
|
||||
vm_host = "34.139.37.135"
|
||||
vm_paths = ["/srv/box/passkey.txt", "/etc/netvm/passkey.txt"]
|
||||
cmd = [
|
||||
"ssh",
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "ConnectTimeout=3",
|
||||
f"super@{vm_host}",
|
||||
f"cat {vm_paths[0]} 2>/dev/null || cat {vm_paths[1]} 2>/dev/null"
|
||||
]
|
||||
key_content = ""
|
||||
fetch_err = None
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||||
if res.returncode == 0 and res.stdout.strip():
|
||||
key_content = res.stdout.strip()
|
||||
else:
|
||||
fetch_err = res.stderr.strip() or "Empty output or authentication required"
|
||||
except subprocess.TimeoutExpired:
|
||||
fetch_err = "SSH connection timed out"
|
||||
except Exception as e:
|
||||
fetch_err = str(e)
|
||||
|
||||
if key_content:
|
||||
if is_json:
|
||||
print(json.dumps({
|
||||
"ok": True,
|
||||
"fetched": True,
|
||||
"passkey": key_content,
|
||||
"vm_host": vm_host,
|
||||
"path": vm_paths[0],
|
||||
"operator_pin": "3128",
|
||||
"surface": "https://box.muse-dev.online/"
|
||||
}, indent=2))
|
||||
return
|
||||
print("\n" + c_bold("=== OPERATOR PASSKEY (FETCHED FROM VM) ===") + "\n")
|
||||
print(f" {c_bold('Passkey Content')}: {c_green(key_content)}")
|
||||
print(f" {c_bold('Source Host')} : {c_cyan(vm_host)} ({vm_paths[0]})")
|
||||
print(f" {c_bold('Web Surface')} : https://box.muse-dev.online/ (PIN: 3128)\n")
|
||||
return
|
||||
else:
|
||||
if is_json:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"fetched": False,
|
||||
"vm_host": vm_host,
|
||||
"path": vm_paths[0],
|
||||
"fallback_path": vm_paths[1],
|
||||
"operator_pin": "3128",
|
||||
"surface": "https://box.muse-dev.online/",
|
||||
"error": f"Could not fetch passkey directly: {fetch_err}",
|
||||
"note": "Passkey is stored in a single .txt file on the VM (34.139.37.135) only, NOT on BL (100.123.153.75).",
|
||||
"operator_command": f"ssh super@{vm_host} 'cat {vm_paths[0]}'",
|
||||
"agent_approval_command": "box approvals request-key <node> --reason '<reason>'"
|
||||
}, indent=2))
|
||||
return
|
||||
print("\n" + c_bold("=== VM PASSKEY FETCH PROBE ===") + "\n")
|
||||
print(f" {c_bold('Target VM Host')} : {c_cyan(vm_host)}")
|
||||
print(f" {c_bold('Target Paths')} : {vm_paths[0]} (fallback: {vm_paths[1]})")
|
||||
print(f" {c_bold('Probe Result')} : {c_yellow('Direct SSH access unavailable (' + (fetch_err or 'auth required') + ')')}\n")
|
||||
print(c_bold(" Location Reality (VM vs BL):"))
|
||||
print(f" • {c_yellow('VM (' + vm_host + ')')} : Key material lives in a {c_bold('single text file (.txt)')} on the VM.")
|
||||
print(f" • {c_red('BL (100.123.153.75)')} : {c_bold('NO passkey / secret material exists on the dedicated BL.')}\n")
|
||||
print(f" {c_bold('Console PIN')} : {c_green('3128')} (sets session cookie: {c_dim('ops_session')} at {c_cyan('https://box.muse-dev.online/')})\n")
|
||||
print(c_bold(" Operator Access:"))
|
||||
print(f" ssh super@{vm_host} 'cat {vm_paths[0]}'\n")
|
||||
print(c_bold(" Agent UX / Approval Request:"))
|
||||
print(f" Agents must not hunt BL. If passkey authorization is needed, request it via:")
|
||||
print(f" {c_cyan('box approvals request-key <node> --reason \"<reason>\"')}\n")
|
||||
return
|
||||
|
||||
if is_json:
|
||||
print(json.dumps({
|
||||
"ok": True,
|
||||
"surface": "https://box.muse-dev.online/",
|
||||
"operator_pin": "3128",
|
||||
"session_cookie": "ops_session",
|
||||
"key_location": {
|
||||
"host": "Google Cloud VM (34.139.37.135)",
|
||||
"canonical_path": "/srv/box/passkey.txt",
|
||||
"fallback_path": "/etc/netvm/passkey.txt",
|
||||
"note": "Stored in a single text file (.txt) on the VM — NOT on the dedicated BL compute node (100.123.153.75)",
|
||||
"credstore_path": "/etc/netvm/meta-credentials/store.age",
|
||||
"credstore_key": "/etc/netvm/meta-credentials/.age-key"
|
||||
},
|
||||
"operator_fetch_cmd": "ssh super@34.139.37.135 'cat /srv/box/passkey.txt'",
|
||||
"agent_approval_protocol": {
|
||||
"rule": "Agents do not hold administrative passkeys or credentials directly. Secrets never reside on bl.",
|
||||
"request_flow": "1. Agent signals APPROVAL_REQUEST: key=<reason> or runs 'box approvals request-key <node>'.\n2. Operator verifies request.\n3. Operator retrieves key/PIN from VM single text file (.txt) or submits OTP.\n4. Operator approves via 'box approvals allow <node>'.",
|
||||
"request_command": "box approvals request-key <node> --reason '<reason>'",
|
||||
"sidechats": ["646 tasks", "pip tasks", "#jobs", "heartbeat"]
|
||||
}
|
||||
}, indent=2))
|
||||
return
|
||||
|
||||
print("\n" + c_bold("=== OPERATOR PASSKEY & KEY MATERIAL ARCHITECTURE ===") + "\n")
|
||||
print(f" {c_bold('Box Front-Door Console')}: {c_cyan('https://box.muse-dev.online/')}")
|
||||
print(f" {c_bold('Operator PIN / Passkey')}: {c_green('3128')} (sets session cookie: {c_dim('ops_session')})\n")
|
||||
|
||||
print(c_bold(" Location Reality (VM vs BL):"))
|
||||
print(f" • {c_yellow('VM (34.139.37.135)')} : Key material lives in a {c_bold('single text file (.txt)')} on the VM.")
|
||||
print(f" - Canonical path: {c_cyan('/srv/box/passkey.txt')}")
|
||||
print(f" - Fallback path : {c_cyan('/etc/netvm/passkey.txt')}")
|
||||
print(f" - Fetch command : {c_dim('box passkey fetch')}")
|
||||
print(f" • {c_red('BL (100.123.153.75)')} : {c_bold('NO passkey / secret material exists on the dedicated BL.')}")
|
||||
print(f" • Credstore on VM : /etc/netvm/meta-credentials/store.age (age-encrypted, root 600)")
|
||||
print(f" • Age Key on VM : /etc/netvm/meta-credentials/.age-key\n")
|
||||
|
||||
print(c_bold(" Agent UX & Operator Approval Flow:"))
|
||||
print(f" 1. {c_cyan('Never hunt on bl')} : Agents must never attempt to grep or locate passkeys on bl.")
|
||||
print(f" 2. {c_cyan('Signal Approval')} : If an agent requires key access or operator privilege:")
|
||||
print(f" - Run: {c_yellow('box approvals request-key <node> --reason \"<reason>\"')}")
|
||||
print(f" - Or emit {c_yellow('APPROVAL_NEEDED: <details>')} in task sidechat")
|
||||
print(f" - Or exit with code {c_yellow('2')} (per INFRA.md convention)")
|
||||
print(f" 3. {c_cyan('Operator Action')} : Operator consults the single .txt file on the VM to authenticate")
|
||||
print(f" and approves via {c_dim('box approvals allow <node>')}.")
|
||||
print(f" 4. {c_cyan('Target Sidechats')} : Use dedicated sidechats ({c_dim('646 tasks, pip tasks, #jobs, heartbeat')}).\n")
|
||||
|
||||
|
||||
|
||||
def _lookup_unreads(args):
|
||||
print("\n" + c_bold("=== FLEET UNREAD / ACTIVITY STATUS ===") + "\n")
|
||||
headers = ["NODE", "UNREAD COUNT", "ACTIVE PAGE / TITLE", "LAST SEEN / THREAD"]
|
||||
rows = []
|
||||
data = collect_fleet_data()
|
||||
for item in data:
|
||||
n = item["node"]
|
||||
title = item.get("title", "")
|
||||
unread = "0"
|
||||
if "(1)" in title or "(2)" in title or "(3)" in title:
|
||||
m = re.search(r"\((\d+)\)", title)
|
||||
unread = c_yellow(m.group(1)) if m else c_yellow("1+")
|
||||
elif item.get("approval_pending"):
|
||||
unread = c_yellow("APPROVAL")
|
||||
|
||||
thread_info = "-"
|
||||
if "thread/" in item.get("url", ""):
|
||||
thread_info = item["url"].split("thread/")[-1][:12]
|
||||
elif item.get("url") == "https://muse.ai/":
|
||||
thread_info = "home"
|
||||
|
||||
rows.append([c_bold(n), unread, title[:45], thread_info])
|
||||
print_table(headers, rows)
|
||||
print("\n" + c_dim(" To view unread messages: box thread view <node> <thread>") + "\n")
|
||||
|
||||
|
||||
def cmd_lookup(args):
|
||||
"""Seamless unified lookup across nodes, threads, unreads, approvals, and keys."""
|
||||
lookup_args = getattr(args, "lookup_args", []) or []
|
||||
if "--json" in lookup_args:
|
||||
setattr(args, "json", True)
|
||||
sub = getattr(args, "target", None)
|
||||
if not sub or sub in ("all", "summary"):
|
||||
print("\n" + c_bold("=== SEAMLESS FLEET LOOKUP SUMMARY ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n"))
|
||||
# 1. Fleet status
|
||||
cmd_fleet_status(args)
|
||||
# 2. Approvals summary
|
||||
try:
|
||||
import approvals
|
||||
app_list = approvals.check_fleet_approvals(VALID_NODES)
|
||||
pending = [a for a in app_list if a.get("status") == "PENDING"]
|
||||
if pending:
|
||||
print(c_yellow(f" ⚠ {len(pending)} pending approval(s): {', '.join(a['node'] for a in pending)} (run: box approvals)"))
|
||||
else:
|
||||
print(c_green(" ✔ Approval Queues: All clean"))
|
||||
except Exception:
|
||||
pass
|
||||
# 3. Key note
|
||||
print(c_dim(" ℹ Passkey: Stored in single .txt on VM (34.139.37.135), not bl. (run: box passkey)"))
|
||||
print()
|
||||
return
|
||||
|
||||
if sub in ("key", "passkey", "auth"):
|
||||
cmd_passkey_info(args)
|
||||
elif sub in ("fleet", "nodes"):
|
||||
cmd_fleet_status(args)
|
||||
elif sub in ("threads", "sidechats"):
|
||||
cmd_thread_list(args)
|
||||
elif sub in ("approvals", "approval"):
|
||||
cmd_approvals(args)
|
||||
elif sub in ("docs", "doc", "surfaces", "sentence", "regex", "parse"):
|
||||
extra = getattr(args, "lookup_args", []) or []
|
||||
sub_args = [sub] if sub not in ("docs", "doc") else []
|
||||
cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + sub_args + extra
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
else:
|
||||
print(f"Unknown lookup target '{sub}'. Choose from: fleet, threads, unread, approvals, key, docs", file=sys.stderr)
|
||||
|
||||
|
||||
def cmd_docs_dispatch(args):
|
||||
"""Bridge 'box docs' and 'super docs' directly to bin/docs-lookup.py."""
|
||||
d_args = getattr(args, "docs_args", []) or []
|
||||
cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + d_args
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
|
||||
|
||||
def cmd_tmux_dispatch(args):
|
||||
"""Bridge 'box tmux' commands directly to bin/muse-tmux.py."""
|
||||
tmux_bin = str(BIN_DIR / "muse-tmux.py")
|
||||
subcmd = args.tmux_action or "list"
|
||||
cmd = [sys.executable, tmux_bin, subcmd] + (args.tmux_args or [])
|
||||
t_args = getattr(args, "tmux_args", []) or []
|
||||
if not t_args:
|
||||
t_args = ["list"]
|
||||
cmd = [sys.executable, tmux_bin] + t_args
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
|
||||
|
||||
def cmd_muse_dispatch(args):
|
||||
"""Bridge 'box muse' commands to muse-cli-node or interactive REPL / multi-node lookups."""
|
||||
m_args = getattr(args, "muse_args", []) or []
|
||||
if not m_args:
|
||||
cmd = [str(BIN_DIR / "muse"), "--help"]
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
|
||||
first = m_args[0]
|
||||
|
||||
# If first is 'tmux', dispatch to tmux
|
||||
if first == "tmux":
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + m_args[1:]
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
|
||||
# If first is a cross-fleet query
|
||||
if first in ("status", "fleet"):
|
||||
cmd_fleet_status(args)
|
||||
return
|
||||
|
||||
if first in ("passkey", "key"):
|
||||
cmd_passkey_info(args)
|
||||
return
|
||||
|
||||
if first in ("lookup", "lookups"):
|
||||
setattr(args, "target", m_args[1] if len(m_args) > 1 else None)
|
||||
cmd_lookup(args)
|
||||
return
|
||||
|
||||
if first in ("threads", "sidechats"):
|
||||
cmd_thread_list(args)
|
||||
return
|
||||
|
||||
if first in ("unread", "unreads"):
|
||||
_lookup_unreads(args)
|
||||
return
|
||||
|
||||
# If first is a valid node
|
||||
if first in VALID_NODES:
|
||||
node = first
|
||||
subargs = m_args[1:]
|
||||
if not subargs:
|
||||
subargs = ["status"]
|
||||
if subargs[0] == "chat":
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-chat-repl.py"), node] + subargs[1:]
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
cmd = [str(BIN_DIR / "muse-cli-node"), node] + subargs
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
|
||||
# Fallback to bin/muse wrapper
|
||||
cmd = [str(BIN_DIR / "muse")] + m_args
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
|
||||
|
||||
def build_parser():
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--json", action="store_true", help="Output machine-readable JSON")
|
||||
@@ -3775,9 +4198,14 @@ def build_parser():
|
||||
|
||||
p_app_check = app_sub.add_parser("check", parents=[common], help="Check fleet approval states")
|
||||
p_app_check.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||||
p_app_check.add_argument("-v", "--verbose", action="store_true", help="Show detailed task prompts, thread URLs, and card text")
|
||||
|
||||
p_app_list = app_sub.add_parser("list", parents=[common], help="Alias for 'check'")
|
||||
p_app_list.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||||
p_app_list.add_argument("-v", "--verbose", action="store_true", help="Show detailed task prompts, thread URLs, and card text")
|
||||
|
||||
p_app_inspect = app_sub.add_parser("inspect", parents=[common], help="Inspect detailed approval and input-wait status on a node")
|
||||
p_app_inspect.add_argument("node", choices=VALID_NODES, help="Target node to inspect")
|
||||
|
||||
p_app_allow = app_sub.add_parser("allow", parents=[common], help="Approve pending browser request")
|
||||
p_app_allow.add_argument("node", choices=VALID_NODES, help="Target node to approve")
|
||||
@@ -3800,6 +4228,18 @@ def build_parser():
|
||||
p_app_watch.add_argument("--auto", action="store_true", help="Automatically approve trusted requests as they appear")
|
||||
p_app_watch.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node")
|
||||
|
||||
p_app_reply = app_sub.add_parser("reply", parents=[common], help="Reply to a waiting agent task/prompt in its active thread")
|
||||
p_app_reply.add_argument("node", choices=VALID_NODES, help="Target node to reply to")
|
||||
p_app_reply.add_argument("message", help="Message or answer to dispatch")
|
||||
p_app_reply.add_argument("--allow-main-chat", action="store_true", help="Explicit override if the active thread is Main Chat")
|
||||
|
||||
p_app_dismiss = app_sub.add_parser("dismiss", parents=[common], help="Dismiss any open task dialog/popup on a node")
|
||||
p_app_dismiss.add_argument("node", choices=VALID_NODES, help="Target node to dismiss dialog on")
|
||||
|
||||
p_app_req_key = app_sub.add_parser("request-key", parents=[common], help="Request operator passkey/key approval for an agent")
|
||||
p_app_req_key.add_argument("node", choices=VALID_NODES, help="Target node requesting key")
|
||||
p_app_req_key.add_argument("--reason", default="Passkey authentication required", help="Reason for key request")
|
||||
|
||||
# Domain: DM
|
||||
p_dm = subparsers.add_parser("dm", parents=[common], help="Inter-agent DMs, work orders ([WO]), acks, live log tail")
|
||||
dm_sub = p_dm.add_subparsers(dest="action")
|
||||
@@ -3878,8 +4318,8 @@ def build_parser():
|
||||
p_thread = subparsers.add_parser("thread", parents=[common], help="Inspect agent main chats, sidechats, and scrollbacks")
|
||||
thread_sub = p_thread.add_subparsers(dest="action")
|
||||
|
||||
p_thread_list = thread_sub.add_parser("list", parents=[common], help="List active threads for an agent")
|
||||
p_thread_list.add_argument("agent", choices=VALID_NODES, help="Agent node to inspect")
|
||||
p_thread_list = thread_sub.add_parser("list", parents=[common], help="List active threads for an agent or all fleet sidechats")
|
||||
p_thread_list.add_argument("agent", nargs="?", default=None, choices=VALID_NODES + [None], help="Agent node to inspect (default: all registered fleet sidechats)")
|
||||
|
||||
p_thread_view = thread_sub.add_parser("view", parents=[common], help="View recent messages from an agent's thread")
|
||||
p_thread_view.add_argument("agent", choices=VALID_NODES, help="Agent node to inspect")
|
||||
@@ -4255,17 +4695,74 @@ def build_parser():
|
||||
|
||||
# Domain: TMUX (Headless background tmux sessions with automatic logging)
|
||||
p_tmux = subparsers.add_parser("tmux", parents=[common], help="Manage headless background tmux sessions on /tmp/tmux-muse.sock")
|
||||
p_tmux.add_argument("tmux_action", nargs="?", default="list", choices=["list", "ls", "new", "send", "send-keys", "capture", "cap", "tail", "kill", "prune", "attach"], help="tmux action (default: list)")
|
||||
p_tmux.add_argument("tmux_args", nargs=argparse.REMAINDER, help="Arguments passed directly to muse-tmux.py")
|
||||
|
||||
# Domain: muse (fast headless gateway via muse-cli-node with isolated per-node Cloudflare WARP egress)
|
||||
p_muse = subparsers.add_parser("muse", parents=[common], help="Direct headless gateway client (muse-cli-node)")
|
||||
p_muse.add_argument("node", choices=VALID_NODES, help="Target agent node")
|
||||
p_muse.add_argument("muse_args", nargs=argparse.REMAINDER, help="Arguments passed directly to muse-cli-node")
|
||||
p_muse.add_argument("muse_args", nargs=argparse.REMAINDER, help="Arguments passed to muse-cli-node or fleet lookups")
|
||||
|
||||
# Domain: LOOKUP (Unified seamless lookups across nodes, threads, unread, approvals, key, docs)
|
||||
p_lookup = subparsers.add_parser("lookup", aliases=["lookups"], parents=[common], help="Seamless fleet lookups (summary, fleet, threads, unread, approvals, key, docs)")
|
||||
p_lookup.add_argument("target", nargs="?", default="summary", choices=["summary", "fleet", "nodes", "threads", "sidechats", "unread", "unreads", "approvals", "key", "passkey", "docs", "doc", "surfaces", "sentence", "regex", "parse"], help="Lookup target")
|
||||
p_lookup.add_argument("lookup_args", nargs=argparse.REMAINDER, help="Additional arguments passed to lookup engine")
|
||||
|
||||
# Domain: PASSKEY (Operator passkey location & agent approval protocol)
|
||||
p_passkey = subparsers.add_parser("passkey", aliases=["key"], parents=[common], help="Display operator passkey location (VM-only), PIN, & agent approval protocol")
|
||||
p_passkey.add_argument("action", nargs="?", default="show", choices=["show", "fetch", "get"], help="Passkey action: 'show' details or 'fetch' from VM")
|
||||
|
||||
# Domain: DOCS (Internal agent lookups, surfaces, sentence grammar, and regex engine)
|
||||
p_docs = subparsers.add_parser("docs", aliases=["doc"], parents=[common], help="Agent lookups, surfaces for box.muse-dev.online, sentence grammar & regex")
|
||||
p_docs.add_argument("docs_args", nargs=argparse.REMAINDER, help="Arguments passed directly to docs-lookup.py")
|
||||
|
||||
return parser
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == "tmux":
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + (sys.argv[2:] or ["list"])
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
elif sys.argv[1] in ("docs", "doc"):
|
||||
cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + sys.argv[2:]
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
elif sys.argv[1] == "muse":
|
||||
m_args = sys.argv[2:]
|
||||
if not m_args:
|
||||
cmd = [str(BIN_DIR / "muse"), "--help"]
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
if m_args[0] == "tmux":
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + (m_args[1:] or ["list"])
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
if m_args[0] in ("status", "fleet"):
|
||||
sys.argv = [sys.argv[0], "fleet", "status"]
|
||||
elif m_args[0] in ("passkey", "key"):
|
||||
sys.argv = [sys.argv[0], "passkey"] + m_args[1:]
|
||||
elif m_args[0] in ("lookup", "lookups"):
|
||||
sys.argv = [sys.argv[0], "lookup"] + m_args[1:]
|
||||
elif m_args[0] in ("threads", "sidechats"):
|
||||
sys.argv = [sys.argv[0], "thread", "list"] + m_args[1:]
|
||||
elif m_args[0] in ("unread", "unreads"):
|
||||
sys.argv = [sys.argv[0], "lookup", "unread"]
|
||||
elif m_args[0] in VALID_NODES:
|
||||
node = m_args[0]
|
||||
sub = m_args[1:]
|
||||
if not sub:
|
||||
sub = ["status"]
|
||||
if sub[0] == "chat":
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-chat-repl.py"), node] + sub[1:]
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
cmd = [str(BIN_DIR / "muse-cli-node"), node] + sub
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
else:
|
||||
cmd = [str(BIN_DIR / "muse")] + m_args
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
|
||||
parser = build_parser()
|
||||
if len(sys.argv) == 1:
|
||||
# Default behavior with no arguments: show fleet status
|
||||
@@ -4489,12 +4986,16 @@ def main():
|
||||
if args.domain == "subagent":
|
||||
args.action = "subagent"
|
||||
cmd_deploy(args)
|
||||
elif args.domain in ("lookup", "lookups"):
|
||||
cmd_lookup(args)
|
||||
elif args.domain in ("passkey", "key"):
|
||||
cmd_passkey_info(args)
|
||||
elif args.domain == "tmux":
|
||||
cmd_tmux_dispatch(args)
|
||||
elif args.domain == "muse":
|
||||
cmd = [str(BIN_DIR / "muse-cli-node"), args.node] + (args.muse_args or [])
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
cmd_muse_dispatch(args)
|
||||
elif args.domain in ("docs", "doc"):
|
||||
cmd_docs_dispatch(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user