Files
box/bin/muse-chat-api.py
T
2026-10-03 18:04:57 +00:00

201 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
# GOLDEN PATH: container -> VM (34.139.37.135) -> bl (100.123.153.75) -> netns -> browser -> agent
# This API is the bridge. Every call traverses 4 hops. Respect the path.
Multi-account muse.ai chat API with approval handling.
Approvals: The browser may show permission dialogs (e.g., "Allow pip to share
information with 34.139.37.135?"). The API detects these and handles them:
- Known-safe (our infrastructure IPs): auto-approve
- Unknown: raise APPROVAL_NEEDED, operator decides via chat
Usage:
muse-chat-api.py --account <agent> send "message"
muse-chat-api.py --account <agent> messages [n]
muse-chat-api.py --account <agent> wait [timeout]
muse-chat-api.py --account <agent> approvals # check pending approvals
"""
import json, urllib.request, websocket, time, sys, argparse
ACCOUNTS = {
"muse": ("muse", "http://127.0.0.1:9410/json/list"),
"pip": ("pip", "http://127.0.0.1:9420/json/list"),
"646": ("646", "http://10.201.202.2:9353/json/list"),
}
# IPs we trust for auto-approval (our infrastructure)
TRUSTED_IPS = {
"34.139.37.135", # VM (gateway)
"100.123.153.75", # bl (main compute)
"100.81.31.9", # VM tailnet
}
def get_page(node, cdp_url):
with urllib.request.urlopen(cdp_url, timeout=5) as r:
ts = json.load(r)
pages = [t for t in ts if t.get('type') == 'page']
if not pages:
print("ERROR: No page found", file=sys.stderr)
sys.exit(1)
return pages[0]
def ev(ws, expr, await_p=False):
ws.send(json.dumps({
"id": 1, "method": "Runtime.evaluate",
"params": {"expression": expr, "returnByValue": True, "awaitPromise": await_p}
}))
resp = json.loads(ws.recv())
return resp.get('result', {}).get('result', {}).get('value')
def check_approvals(ws):
"""
Check for browser permission dialogs.
Returns list of (dialog_text, is_trusted, action_taken).
"""
result = ev(ws, """(() => {
const dialogs = [];
// Look for permission prompts (common patterns)
const body = document.body.innerText;
// Check for "Allow ... to share" pattern
if (body.includes('Allow') && body.includes('to share')) {
// Find the dialog
const els = [...document.querySelectorAll('*')].filter(el => {
const t = el.innerText || '';
return t.includes('Allow') && t.includes('to share') && t.length < 500;
});
for (const el of els.slice(0,3)) {
dialogs.push(el.innerText.slice(0,200));
}
}
// Check for other permission patterns
const perm_btns = [...document.querySelectorAll('button')].filter(b => {
const t = (b.innerText||'').toLowerCase();
return t.includes('allow') || t.includes('deny') || t.includes('block');
});
if (perm_btns.length >= 2 && dialogs.length === 0) {
// Might be a permission dialog
const parent = perm_btns[0].closest('div');
if (parent) dialogs.push(parent.innerText.slice(0,200));
}
return JSON.stringify(dialogs);
})()""")
try:
dialogs = json.loads(result) if result else []
except:
dialogs = []
actions = []
for d in dialogs:
# Extract IP if present
import re
ips = re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', d)
is_trusted = any(ip in TRUSTED_IPS for ip in ips)
if is_trusted:
# Auto-approve: click "Allow once" or "Allow"
clicked = ev(ws, """(async()=>{
const b = [...document.querySelectorAll('button')].find(x=>{
const t = (x.innerText||'').toLowerCase();
return t.includes('allow once') || t === 'allow';
});
if (b) { b.click(); return 'clicked:'+b.innerText.slice(0,20); }
return 'NOTFOUND';
})()""", True)
actions.append((d[:80], True, clicked))
else:
actions.append((d[:80], False, "APPROVAL_NEEDED"))
return actions
def cmd_approvals(ws):
"""Check and handle pending approvals."""
actions = check_approvals(ws)
if not actions:
print("No pending approvals")
return
for dialog, trusted, action in actions:
print(f"Dialog: {dialog}")
print(f" Trusted: {trusted}, Action: {action}")
if not trusted:
print(" APPROVAL_NEEDED: Manual review required")
sys.exit(2)
def cmd_send(ws, message):
# Check approvals first
actions = check_approvals(ws)
for dialog, trusted, action in actions:
if not trusted:
print(f"APPROVAL_NEEDED: {dialog[:80]}", file=sys.stderr)
sys.exit(2)
msg_esc = message.replace('\\', '\\\\').replace('`', '\\`').replace('$', '\\$')
result = ev(ws, f"""(async()=>{{
const input = document.querySelector('[contenteditable="true"]') ||
document.querySelector('textarea[placeholder*="Message"]') ||
[...document.querySelectorAll('div[role="textbox"]')][0];
if (!input) return 'NOINPUT';
input.focus();
document.execCommand('insertText', false, `{msg_esc}`);
await new Promise(r=>setTimeout(r,500));
const send = [...document.querySelectorAll('button')].find(b=>
b.getAttribute('aria-label')&&b.getAttribute('aria-label').toLowerCase().includes('send')
);
if (send) {{ send.click(); return 'sent'; }}
const ke = new KeyboardEvent('keydown', {{key:'Enter', code:'Enter', bubbles:true}});
input.dispatchEvent(ke);
return 'enter-sent';
}})()""", True)
print(result)
def cmd_messages(ws, n=5):
# Check approvals first (non-blocking)
check_approvals(ws)
result = ev(ws, f"""(() => {{
const ps = [...document.querySelectorAll('p')].slice(-{n*2}).map(p=>p.innerText.slice(0,200));
return ps.join('\\n---\\n');
}})()""")
print(result)
def cmd_wait(ws, timeout=30):
print(f"Waiting {timeout}s for response...")
# Check approvals periodically during wait
for i in range(timeout // 5):
actions = check_approvals(ws)
for dialog, trusted, action in actions:
if not trusted:
print(f"APPROVAL_NEEDED: {dialog[:80]}", file=sys.stderr)
sys.exit(2)
time.sleep(5)
cmd_messages(ws, 2)
def main():
p = argparse.ArgumentParser()
p.add_argument('--account', required=True, choices=list(ACCOUNTS.keys()),
help='Agent name (matches node, profile, ACCOUNTS.md)')
p.add_argument('command', choices=['send', 'messages', 'wait', 'approvals'])
p.add_argument('arg', nargs='?', default=None)
args = p.parse_args()
node, cdp_url = ACCOUNTS[args.account]
page = get_page(node, cdp_url)
ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=15)
try:
if args.command == 'send':
if not args.arg:
print("ERROR: send requires a message", file=sys.stderr)
sys.exit(1)
cmd_send(ws, args.arg)
elif args.command == 'messages':
n = int(args.arg) if args.arg else 5
cmd_messages(ws, n)
elif args.command == 'wait':
t = int(args.arg) if args.arg else 30
cmd_wait(ws, t)
elif args.command == 'approvals':
cmd_approvals(ws)
finally:
ws.close()
if __name__ == '__main__':
main()