feat(hybrid-gateway): integrate muse-cli with Cloudflare netns isolation, symmetric sidechat routing, and 646-pip sync unblock

This commit is contained in:
operator
2026-10-04 22:54:01 +00:00
parent 3d5fbe5aeb
commit 1a271b1bbd
60 changed files with 5068 additions and 55 deletions
+125 -1
View File
@@ -16,6 +16,9 @@ Usage:
muse-chat-api.py --account <agent> wait [timeout]
muse-chat-api.py --account <agent> approvals # check pending approvals
muse-chat-api.py --account <agent> upload <file> [--message txt] [--dry-run]
Exit codes: 0 ok, 1 error, 2 APPROVAL_NEEDED (human decision),
3 DOM_NOT_READY (browser not in expected chat state - safe to retry).
"""
import json, urllib.request, websocket, time, sys, argparse, importlib.util
import os
@@ -146,6 +149,120 @@ def check_approvals(ws):
return actions
# Exit code for "browser DOM not ready" — distinct from 1 (generic error)
# and 2 (APPROVAL_NEEDED, needs a human). DOM_NOT_READY is safe to retry
# after a few seconds: the React app may still be hydrating (S3 partial
# load) or the Warp tunnel may have just hiccupped. dm.py treats any
# non-"sent" send output as a failed attempt and retries, so this is a
# fail-closed retry signal, not a crash.
DOM_NOT_READY_EXIT = 3
# Shimmer spans (span.animate-pulse-light) exist in the settled state too
# (2 observed — avatar/media skeleton slots, not load progress). Only a
# mass-skeleton count indicates a stuck/broken render. Heuristic threshold
# from docs/DOM-EDGE-STATES.md 1.1; the settled-state signature below is
# the primary readiness signal.
SHIMMER_STORM_THRESHOLD = 30
def chat_ready_probe(ws):
"""One-shot DOM readiness probe. Returns a dict of observed state, or
{} if the page could not be evaluated at all."""
result = ev1(ws, """(() => {
const q = s => { try { return document.querySelector(s); } catch(e) { return null; } };
const qa = s => { try { return document.querySelectorAll(s); } catch(e) { return []; } };
const title = document.title || '';
const url = window.location.href || '';
const switcher = !!q('[data-testid="hatch-chat-switcher-trigger"]');
const buttons = qa('button').length;
const msglog = !!q('div[role="log"]');
const composer = !!(q('[contenteditable="true"]') ||
q('textarea[placeholder*="Message"]') ||
q('div[role="textbox"]'));
const shimmer = qa('span.animate-pulse-light').length;
let alertText = '';
try {
alertText = [...qa('[role="alert"]')]
.filter(e => e.tagName !== 'SCRIPT' && (e.innerText||'').trim().length > 0)
.slice(0, 2).map(e => e.innerText.slice(0, 160)).join(' | ');
} catch(e) {}
let onLine = true;
try { onLine = navigator.onLine; } catch(e) {}
return JSON.stringify({title, url, switcher, buttons, msglog,
composer, shimmer, alertText, onLine});
})()""")
try:
return json.loads(result) if result else {}
except Exception:
return {}
def classify_chat_state(p):
"""Classify a readiness probe. Returns (state, diagnosis). READY is the
only state a send may proceed from. Reference: docs/DOM-EDGE-STATES.md.
Note: 'Chat \u2014' is the 'Chat \u2014 <identity>' title prefix."""
if not p:
return ("NO_PROBE",
"CDP evaluate returned nothing - page blank, navigating, or CDP wedged")
title = p.get("title", "")
url = p.get("url", "")
if title == "muse.ai":
return ("LANDING",
"browser is on the muse.ai landing page, not in chat (state: LANDING)")
if not p.get("onLine", True):
return ("OFFLINE",
"navigator.onLine is false - browser reports no network")
if p.get("alertText"):
return ("REAL_ERROR",
"page shows an error banner: %s" % p["alertText"][:160])
if "/thread/new" in url:
# Legitimate only right after `sidechat create`: the new-thread
# composer is up and the title has settled. A stripped /thread/new
# (no title, no composer) is the S8 broken state.
if p.get("composer") and "Chat \u2014" in title:
return ("READY", "new-thread composer state (/thread/new)")
return ("S8_STRIPPED",
"stripped /thread/new page - no composer/title (state: S8)")
switcher = p.get("switcher", False)
buttons = p.get("buttons", 0)
if title == "Muse" and (not switcher or buttons < 30):
return ("S3_PARTIAL",
"React still hydrating: title 'Muse', %d buttons, switcher=%s (state: S3)"
% (buttons, switcher))
if not ("Chat \u2014" in title and switcher and buttons > 30 and p.get("msglog")):
return ("NOT_SETTLED",
"settled-state signature not met (title=%r switcher=%s buttons=%d msglog=%s)" %
(title[:40], switcher, buttons, p.get("msglog", False)))
if p.get("shimmer", 0) > SHIMMER_STORM_THRESHOLD:
return ("SHIMMER_STORM",
"mass skeleton render: %d shimmer spans (settled has ~2)" % p["shimmer"])
if not p.get("composer"):
return ("COMPOSER_MISSING",
"chat settled but no composer input found - send has nowhere to type")
return ("READY", "settled chat state (title=%r)" % title[:40])
def assert_chat_ready(ws, context="send"):
"""Fail-closed React-readiness gate ("matching chromebox").
Call before any DOM mutation (send, upload). On mismatch prints a
structured diagnosis to stderr and exits DOM_NOT_READY_EXIT (3) - a
retryable signal, distinct from APPROVAL_NEEDED (2, needs a human).
S3 partial load gets one 5s grace re-probe (transient render phase);
everything else fails immediately.
"""
probe = chat_ready_probe(ws)
state, detail = classify_chat_state(probe)
if state == "S3_PARTIAL":
time.sleep(5)
probe = chat_ready_probe(ws)
state, detail = classify_chat_state(probe)
if state != "READY":
print("DOM_NOT_READY [%s] %s" % (state, detail), file=sys.stderr)
print("DOM_NOT_READY probe: %s" % json.dumps(probe), file=sys.stderr)
sys.exit(DOM_NOT_READY_EXIT)
return probe
def cmd_approvals(ws):
"""Check and handle pending approvals."""
actions = check_approvals(ws)
@@ -160,7 +277,12 @@ def cmd_approvals(ws):
sys.exit(2)
def cmd_send(ws, message):
# Check approvals first
# React-readiness gate ("matching chromebox"): fail closed if the
# browser is not in the expected chat state (landing page,
# partial load, partition). Exit 3 = safe to retry, unlike
# APPROVAL_NEEDED (2, needs a human).
assert_chat_ready(ws, context="send")
# Check approvals
actions = check_approvals(ws)
for dialog, trusted, action in actions:
if not trusted:
@@ -479,6 +601,8 @@ def cmd_upload(ws, filepath, message=None, dry_run=False):
if not os.path.isfile(filepath):
print("ERROR: file not found: %s" % filepath, file=sys.stderr)
sys.exit(1)
# React-readiness gate ("matching chromebox") - see cmd_send.
assert_chat_ready(ws, context="upload")
actions = check_approvals(ws)
for dialog, trusted, action in actions:
if not trusted: