feat(box): add invite code handler, Settings RPA, and agent onboarding pipeline

- Support invite code discovery in main chat and redemption in settings menu
- Add Settings RPA primitives with Radix UI mouse dispatch and retry polling for async DOM
- Attribute 'has_redeemed' binary from the Additional tokens ticker / entrypoint visibility
- Unblock agent @646 by repairing warp-def/dev tunnels and redeeming REDCJ7 via dev (+1B tokens)
- Implement 'box onboard' pipeline to provision infra, authenticate, and auto-redeem queued codes
- Add 'box onboard feed-matrix' ranking all agents by work done over time, job count, and role
- Register 'InputType.SALVAGE' in loop modulation (CRITICAL priority, 600s timeout, 3 nudges to opm)
- Ingest recurring balance audits into canonical HEARTBEAT.md and TOOLS.md
This commit is contained in:
operator
2026-10-06 19:33:17 +00:00
parent 4998ffddb6
commit b824f6d405
30 changed files with 4516 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
"""hatch_menu — modular muse.ai settings-menu navigation + toggles.
One module per part so a site change means patching one file:
mouse.py trusted input primitives (real click, escape, close)
dialog.py settings dialog open / tab nav / rows / back / text
controls.py generic radio/switch primitives (verify-then-fallback)
toggles.py toggle registry + sessions over controls and tabs
tabs/ one module per settings tab (uniform describe())
invite.py consumes this package; `box chromebox` exposes toggles.
"""
from hatch_menu.dialog import (
TAB_NAMES,
TABS,
click_row,
close_settings,
describe_rows,
dialog_present,
dialog_text,
go_back,
goto_tab,
open_settings,
)
from hatch_menu.mouse import MouseError, close, escape, real_click
from hatch_menu.toggles import (
MenuError,
describe_tab,
get_toggle,
list_toggles,
set_toggle,
)
__all__ = [
"TAB_NAMES",
"TABS",
"MenuError",
"MouseError",
"click_row",
"close",
"close_settings",
"describe_rows",
"describe_tab",
"dialog_present",
"dialog_text",
"escape",
"get_toggle",
"go_back",
"goto_tab",
"list_toggles",
"open_settings",
"real_click",
]
+260
View File
@@ -0,0 +1,260 @@
"""Generic control primitives (ws-level, no tab knowledge).
Radio/switch list + set with verify-then-fallback: synthetic click,
verify state flipped, else trusted real click, verify again. Tab
modules build their flows on these; toggles.py adds addressing.
"""
import time
from approvals import cdp_evaluate
from hatch_menu.mouse import real_click
JS_LIST_RADIOS = """(() => {
const d = document.querySelector('[role="dialog"]');
if (!d) return null;
return Array.from(d.querySelectorAll('input[type="radio"]')).map(r => {
let head = '', el = r.parentElement, depth = 0;
while (el && el !== d && depth < 6) {
const h = el.querySelector('h1,h2,h3,h4');
if (h && (h.innerText || '').trim()) {
head = h.innerText.trim().slice(0, 60);
break;
}
el = el.parentElement;
depth += 1;
}
const lab = r.closest('label');
return {value: r.value, checked: !!r.checked, heading: head,
aria: r.getAttribute('aria-label') || '',
label: lab ? (lab.innerText || '').trim().slice(0, 80) : ''};
});
})()"""
JS_CLICK_RADIO = """((heading, value) => {
const d = document.querySelector('[role="dialog"]');
if (!d) return 'NO_DIALOG';
const radios = Array.from(d.querySelectorAll('input[type="radio"]'));
const headOf = (r) => {
let el = r.parentElement, depth = 0;
while (el && el !== d && depth < 6) {
const h = el.querySelector('h1,h2,h3,h4');
if (h && (h.innerText || '').trim())
return h.innerText.trim().toLowerCase();
el = el.parentElement;
depth += 1;
}
return '';
};
const t = radios.find(r => headOf(r) === heading.toLowerCase()
&& r.value === value);
if (!t) return 'NO_MATCH';
t.click();
return 'CLICKED';
})('%s', '%s')"""
JS_RADIO_RECT = """((heading, value) => {
const d = document.querySelector('[role="dialog"]');
if (!d) return null;
const radios = Array.from(d.querySelectorAll('input[type="radio"]'));
const headOf = (r) => {
let el = r.parentElement, depth = 0;
while (el && el !== d && depth < 6) {
const h = el.querySelector('h1,h2,h3,h4');
if (h && (h.innerText || '').trim())
return h.innerText.trim().toLowerCase();
el = el.parentElement;
depth += 1;
}
return '';
};
const t = radios.find(r => headOf(r) === heading.toLowerCase()
&& r.value === value);
if (!t) return null;
const r = t.getBoundingClientRect();
return {x: r.x + r.width / 2, y: r.y + r.height / 2};
})('%s', '%s')"""
JS_CLICK_RADIO_ARIA = """((name) => {
const d = document.querySelector('[role="dialog"]');
if (!d) return 'NO_DIALOG';
const n = name.toLowerCase();
const t = Array.from(d.querySelectorAll('input[type="radio"]'))
.find(r => (r.getAttribute('aria-label') || '').toLowerCase() === n
|| (r.value || '').toLowerCase() === n);
if (!t) return 'NO_MATCH';
t.click();
return 'CLICKED';
})('%s')"""
JS_RADIO_ARIA_RECT = """((name) => {
const d = document.querySelector('[role="dialog"]');
if (!d) return null;
const n = name.toLowerCase();
const t = Array.from(d.querySelectorAll('input[type="radio"]'))
.find(r => (r.getAttribute('aria-label') || '').toLowerCase() === n
|| (r.value || '').toLowerCase() === n);
if (!t) return null;
const r = t.getBoundingClientRect();
return {x: r.x + r.width / 2, y: r.y + r.height / 2};
})('%s')"""
JS_LIST_SWITCHES = """(() => {
const d = document.querySelector('[role="dialog"]');
if (!d) return null;
return Array.from(d.querySelectorAll('[role="switch"]')).map(s => {
let el = s.parentElement, label = '', depth = 0;
while (el && el !== d && depth < 6) {
const t = (el.innerText || '').trim().replace(/\\s+/g, ' ');
if (t && t.length < 250) { label = t; break; }
el = el.parentElement;
depth += 1;
}
return {label: label.slice(0, 200),
aria: s.getAttribute('aria-label') || '',
checked: s.getAttribute('aria-checked') === 'true'};
});
})()"""
JS_CLICK_SWITCH = """((label) => {
const d = document.querySelector('[role="dialog"]');
if (!d) return 'NO_DIALOG';
const rowOf = (s) => {
let el = s.parentElement, depth = 0;
while (el && el !== d && depth < 6) {
const t = (el.innerText || '').trim();
if (t && t.length < 250) return t.toLowerCase();
el = el.parentElement;
depth += 1;
}
return '';
};
const hits = Array.from(d.querySelectorAll('[role="switch"]'))
.filter(s => rowOf(s).includes(label.toLowerCase())
|| (s.getAttribute('aria-label') || '').toLowerCase()
.includes(label.toLowerCase()));
if (!hits.length) return 'NO_MATCH';
if (hits.length > 1) return 'AMBIGUOUS';
hits[0].click();
return 'CLICKED';
})('%s')"""
JS_SWITCH_RECT = """((label) => {
const d = document.querySelector('[role="dialog"]');
if (!d) return null;
const rowOf = (s) => {
let el = s.parentElement, depth = 0;
while (el && el !== d && depth < 6) {
const t = (el.innerText || '').trim();
if (t && t.length < 250) return t.toLowerCase();
el = el.parentElement;
depth += 1;
}
return '';
};
const hits = Array.from(d.querySelectorAll('[role="switch"]'))
.filter(s => rowOf(s).includes(label.toLowerCase())
|| (s.getAttribute('aria-label') || '').toLowerCase()
.includes(label.toLowerCase()));
if (hits.length !== 1) return null;
const r = hits[0].getBoundingClientRect();
return {x: r.x + r.width / 2, y: r.y + r.height / 2};
})('%s')"""
def _eval(ws, js, timeout=8.0):
try:
return cdp_evaluate(ws, js, timeout=timeout)
except Exception:
return None
def list_radios(ws):
"""All dialog radios with heading/label/value/checked (or [])."""
return _eval(ws, JS_LIST_RADIOS) or []
def list_switches(ws):
"""All dialog switches with row label + checked (or [])."""
return _eval(ws, JS_LIST_SWITCHES) or []
def radio_state(ws, heading, value):
"""Checked state of one heading-grouped radio, None if absent."""
for r in list_radios(ws):
if (r.get("heading") or "").lower() == heading.lower() \
and r.get("value") == value:
return bool(r.get("checked"))
return None
def set_radio_by_heading(ws, heading, value):
"""Set a heading-grouped radio; verify, else trusted click, verify."""
if _eval(ws, JS_CLICK_RADIO % (heading, value)) == "CLICKED" \
and radio_state(ws, heading, value) is True:
return True
rect = _eval(ws, JS_RADIO_RECT % (heading, value))
if not rect or "x" not in rect:
return False
try:
real_click(ws, rect["x"], rect["y"])
except Exception:
return False
time.sleep(0.6)
return radio_state(ws, heading, value) is True
def radio_aria_state(ws, name):
"""Checked state of one aria-labeled radio, None if absent."""
n = name.lower()
for r in list_radios(ws):
if (r.get("aria") or "").lower() == n \
or (r.get("value") or "").lower() == n:
return bool(r.get("checked"))
return None
def set_radio_by_aria(ws, name):
"""Set an aria-labeled radio; verify, else trusted click, verify."""
if _eval(ws, JS_CLICK_RADIO_ARIA % name) == "CLICKED" \
and radio_aria_state(ws, name) is True:
return True
rect = _eval(ws, JS_RADIO_ARIA_RECT % name)
if not rect or "x" not in rect:
return False
try:
real_click(ws, rect["x"], rect["y"])
except Exception:
return False
time.sleep(0.6)
return radio_aria_state(ws, name) is True
def switch_state(ws, label):
"""Checked state of one label-matched switch, None if not unique."""
matches = [s for s in list_switches(ws)
if label.lower() in (s.get("label") or "").lower()
or label.lower() in (s.get("aria") or "").lower()]
if len(matches) != 1:
return None
return bool(matches[0].get("checked"))
def set_switch(ws, label, on):
"""Set a switch by row-label/aria match; no-op when already there."""
state = switch_state(ws, label)
if state is None:
return False
if state == bool(on):
return True
if _eval(ws, JS_CLICK_SWITCH % label) == "CLICKED" \
and switch_state(ws, label) is bool(on):
return True
rect = _eval(ws, JS_SWITCH_RECT % label)
if not rect or "x" not in rect:
return False
try:
real_click(ws, rect["x"], rect["y"])
except Exception:
return False
time.sleep(0.6)
return switch_state(ws, label) is bool(on)
+151
View File
@@ -0,0 +1,151 @@
"""Settings dialog navigation: open, tabs, rows, back, text.
Open is retried (single-shot opens flake ~1/4 live): each attempt
re-Escapes and re-drives the dock menu from scratch. Tab clicks are
idempotent (re-clicking the active tab is a harmless no-op), so goto
always clicks and reports the click result instead of guessing which
tab is active.
"""
import time
from approvals import cdp_evaluate
from hatch_menu.mouse import escape, real_click
TAB_NAMES = ["General", "Connectors", "Wallet", "Secure store",
"Permissions", "Messaging channels", "Devices",
"Data controls", "Help & support", "Legal info"]
TABS = TAB_NAMES # legacy alias
DOCK_MORE_TESTID = "hatch-dock-more"
JS_DOCK_RECT = ("(() => { const b = document.querySelector("
"'[data-testid=\"hatch-dock-more\"]'); if (!b) return null;"
" const r = b.getBoundingClientRect();"
" return {x: r.x + r.width/2, y: r.y + r.height/2}; })()")
JS_CLICK_SETTINGS_ITEM = ("(() => { const it = Array.from(document."
"querySelectorAll('[role=\"menuitem\"]')).find(el => el.getAttribute("
"'data-pel-click') === 'settings_nav_click');"
" if (!it) return 'NO_ITEM'; it.click(); return 'CLICKED'; })()")
JS_DIALOG_PRESENT = ("(() => !!document.querySelector('[role=\"dialog\"]'))()")
JS_DIALOG_TEXT = ("(() => { const d = document.querySelector("
"'[role=\"dialog\"]'); return d ? d.innerText : null; })()")
JS_GOTO_TAB_TMPL = ("(() => { const b = Array.from(document."
"querySelectorAll('[role=\"dialog\"] button')).find(x => "
"(x.innerText||'').trim() === '%s');"
" if (!b) return 'NO_TAB'; b.click(); return 'CLICKED'; })()")
JS_CLICK_ROW_TMPL = ("((name) => {"
" const d = document.querySelector('[role=\"dialog\"]');"
" if (!d) return 'NO_DIALOG';"
" const inNav = (el) => !!el.closest("
"'nav, [role=\"tablist\"], [role=\"navigation\"]');"
" const els = Array.from(d.querySelectorAll("
"'button, [role=\"button\"], a')).filter(e => !inNav(e));"
" const t = els.find(e => (e.innerText || '').trim()"
".toLowerCase().startsWith(name.toLowerCase()));"
" if (!t) return 'NO_ROW'; t.click(); return 'CLICKED'; })('%s')")
JS_DESCRIBE_ROWS = ("(() => {"
" const d = document.querySelector('[role=\"dialog\"]');"
" if (!d) return null;"
" const inNav = (el) => !!el.closest("
"'nav, [role=\"tablist\"], [role=\"navigation\"]');"
" return Array.from(d.querySelectorAll("
"'button, [role=\"button\"], a')).filter(e => !inNav(e))"
".map(e => { const lines = (e.innerText || '').trim().split('\\n');"
" return {name: (lines[0] || '').slice(0, 80),"
" detail: lines.slice(1).join(' / ').slice(0, 120)}; }); })()")
JS_GO_BACK = ("(() => { const d = document.querySelector("
"'[role=\"dialog\"]'); if (!d) return 'NO_DIALOG';"
" const b = Array.from(d.querySelectorAll('button')).find("
"x => (x.getAttribute('aria-label') || '') === 'Go back');"
" if (!b) return 'NO_BACK'; b.click(); return 'CLICKED'; })()")
def _eval(ws, js, timeout=8.0):
try:
return cdp_evaluate(ws, js, timeout=timeout)
except Exception:
return None
def dialog_present(ws):
"""True when a dialog is currently open."""
return bool(_eval(ws, JS_DIALOG_PRESENT, timeout=5.0))
def dialog_text(ws, timeout=8.0, limit=4000):
"""Inner text of the open dialog, or None."""
try:
text = cdp_evaluate(ws, JS_DIALOG_TEXT, timeout=timeout)
except Exception:
return None
if not text:
return None
return text[:limit]
def open_settings(ws, tries=3):
"""Open the Settings dialog via the dock menu. True when open."""
for _ in range(tries):
escape(ws)
time.sleep(0.5)
rect = _eval(ws, JS_DOCK_RECT, timeout=5.0)
if not rect or "x" not in rect:
continue
try:
real_click(ws, rect["x"], rect["y"])
except Exception:
continue
time.sleep(1.0)
if _eval(ws, JS_CLICK_SETTINGS_ITEM,
timeout=5.0) != "CLICKED":
continue
time.sleep(1.5)
if dialog_present(ws):
return True
return False
def goto_tab(ws, name, timeout=5.0):
"""Click a Settings tab by visible name. True when clicked."""
if _eval(ws, JS_GOTO_TAB_TMPL % name, timeout=timeout) != "CLICKED":
return False
time.sleep(0.8)
return True
def click_row(ws, name, tab=None, timeout=8.0):
"""Click a content row; verify the drill/expand opened. Bool."""
if tab is not None and not goto_tab(ws, tab, timeout=timeout):
return False
if _eval(ws, JS_CLICK_ROW_TMPL % name, timeout=timeout) != "CLICKED":
return False
time.sleep(1.0)
text = dialog_text(ws, timeout=timeout) or ""
return name.lower() in text.lower()
def describe_rows(ws, tab=None, timeout=8.0):
"""Inventory rows (name/detail) on a tab. [] when unreadable."""
if tab is not None and not goto_tab(ws, tab, timeout=timeout):
return []
return _eval(ws, JS_DESCRIBE_ROWS, timeout=timeout) or []
def go_back(ws):
"""Click the sub-page Go back button. True when clicked."""
if _eval(ws, JS_GO_BACK, timeout=5.0) != "CLICKED":
return False
time.sleep(0.8)
return True
def close_settings(ws):
"""Dismiss settings/popovers. Never raises."""
escape(ws)
+62
View File
@@ -0,0 +1,62 @@
"""Trusted input primitives for menu automation.
Radix triggers (dock menu, permission-mode choosers) ignore synthetic
JS clicks: they need real CDP Input.dispatchMouseEvent press+release.
"""
import json
import time
from approvals import _cdp_req_ids as _shared_cdp_ids, cdp_evaluate
ESCAPE_JS = ("(() => { document.dispatchEvent(new KeyboardEvent("
"'keydown', {key: 'Escape', code: 'Escape',"
" bubbles: true})); return 'ESC'; })()")
class MouseError(RuntimeError):
"""Trusted click failed (CDP transport or echo timeout)."""
def escape(ws):
"""Dismiss topmost popover/menu/dialog. Never raises."""
try:
cdp_evaluate(ws, ESCAPE_JS, timeout=3.0)
except Exception:
pass
def close(ws):
"""Close a CDP websocket. Never raises."""
try:
ws.close()
except Exception:
pass
def real_click(ws, x, y, timeout=5.0):
"""Trusted press+release at page coordinates.
Shares approvals' monotonic CDP id counter so ids stay unique on
the connection; matches responses by id like cdp_evaluate.
Raises MouseError on transport or echo-timeout failure.
"""
try:
for typ in ("mousePressed", "mouseReleased"):
req_id = next(_shared_cdp_ids)
ws.send(json.dumps({"id": req_id,
"method": "Input.dispatchMouseEvent",
"params": {"type": typ, "x": x, "y": y,
"button": "left",
"clickCount": 1}}))
deadline = time.time() + timeout
while time.time() < deadline:
resp = json.loads(ws.recv())
if resp.get("id") == req_id:
break
else:
raise MouseError("mouse echo timeout for %s" % typ)
except MouseError:
raise
except Exception as e:
raise MouseError("real click failed: %s: %s"
% (type(e).__name__, e))
+30
View File
@@ -0,0 +1,30 @@
"""One module per Settings tab. Uniform: TAB, describe(ws)."""
from hatch_menu.tabs import (
connectors,
data_controls,
devices,
general,
help_support,
legal,
messaging,
permissions,
secure_store,
wallet,
)
TAB_MODULES = {
"General": general,
"Connectors": connectors,
"Wallet": wallet,
"Secure store": secure_store,
"Permissions": permissions,
"Messaging channels": messaging,
"Devices": devices,
"Data controls": data_controls,
"Help & support": help_support,
"Legal info": legal,
}
__all__ = ["TAB_MODULES", "connectors", "data_controls", "devices",
"general", "help_support", "legal", "messaging",
"permissions", "secure_store", "wallet"]
+10
View File
@@ -0,0 +1,10 @@
"""Connectors tab: read-only inventory (search + per-app Connect/View)."""
from hatch_menu import dialog
TAB = "Connectors"
def describe(ws):
"""Row inventory + text excerpt (describe-only for now)."""
return {"rows": dialog.describe_rows(ws, TAB),
"text": (dialog.dialog_text(ws) or "")[:400]}
+52
View File
@@ -0,0 +1,52 @@
"""Data controls tab: model-improvement switch (read-only otherwise).
The switch row label is not yet pinned from recon, so resolution
prefers the single switch on the tab and falls back to keyword
match. Import/Delete rows are inventoried, never touched.
"""
from hatch_menu import controls, dialog
TAB = "Data controls"
_KEYWORDS = ("improv", "train", "model", "data", "usage")
def _resolve(ws):
"""The improvement switch dict, or None when not resolvable."""
if not dialog.goto_tab(ws, TAB):
return None
switches = controls.list_switches(ws)
if len(switches) == 1:
return switches[0]
for kw in _KEYWORDS:
for s in switches:
blob = ((s.get("label") or "") + " "
+ (s.get("aria") or "")).lower()
if kw in blob:
return s
return None
def ai_improvement(ws):
"""Improvement-switch state: True/False, None when unreadable."""
sw = _resolve(ws)
return None if sw is None else bool(sw.get("checked"))
def set_ai_improvement(ws, on):
"""Set the improvement switch; verify-then-fallback. Bool."""
sw = _resolve(ws)
if sw is None:
return False
label = (sw.get("aria") or "").strip() \
or (sw.get("label") or "")[:40].strip()
if not label:
return False
return controls.set_switch(ws, label, on)
def describe(ws):
"""Inventory: switch state + row names (import/delete read-only)."""
state = ai_improvement(ws)
return {"ai_improvement":
("on" if state else "off") if state is not None else None,
"rows": dialog.describe_rows(ws, TAB)}
+13
View File
@@ -0,0 +1,13 @@
"""Devices tab: read-only inventory (paired devices or empty state)."""
from hatch_menu import dialog
TAB = "Devices"
def describe(ws):
"""Row inventory + empty flag (describe-only for now)."""
text = dialog.dialog_text(ws) or ""
low = text.lower()
return {"rows": dialog.describe_rows(ws, TAB),
"empty": ("no devices" in low or "don't have" in low),
"text": text[:400]}
+95
View File
@@ -0,0 +1,95 @@
"""General tab: usage balances, theme picker, redeem entrypoint.
Usage parsing moved here from invite.py (single copy). All flows are
ws-level: sessions and error shaping live in toggles.py.
"""
import re
from approvals import cdp_evaluate
from hatch_menu import controls, dialog
TAB = "General"
THEME_VALUES = ("match", "default", "blue", "purple", "pink",
"orange", "green", "beige", "monochrome")
_FREE_RE = re.compile(r"\bfree plan\b", re.IGNORECASE)
_PCT_RE = re.compile(r"(\d+)%\s*used")
_RESET_RE = re.compile(r"resets?\s+on\s+([A-Z][a-z]+\s+\d{1,2})",
re.IGNORECASE)
_TOK_RE = re.compile(r"\(([0-9.,]+\s*[BMK]?)\s*tokens?\s+left\)",
re.IGNORECASE)
def parse_usage_text(text):
"""Parse a General-tab usage block into a balance dict.
Returns None for empty/unreadable text. Weekly fields stay None
when the block only carries additional-tokens rows.
"""
if not text or not text.strip():
return None
out = {"plan": None, "weekly": {"pct_used": None, "resets_on": None},
"additional": {"pct_used": None, "tokens_left": None,
"never_expires": False}}
if _FREE_RE.search(text):
out["plan"] = "free"
pcts = _PCT_RE.findall(text)
if pcts:
out["weekly"]["pct_used"] = int(pcts[0])
if len(pcts) > 1:
out["additional"]["pct_used"] = int(pcts[1])
m = _RESET_RE.search(text)
if m:
out["weekly"]["resets_on"] = m.group(1)
m = _TOK_RE.search(text)
if m:
out["additional"]["tokens_left"] = m.group(1).strip()
if "never expires" in text.lower():
out["additional"]["never_expires"] = True
return out
def _eval(ws, js, timeout=8.0):
try:
return cdp_evaluate(ws, js, timeout=timeout)
except Exception:
return None
def usage(ws):
"""Usage balances from General tab. Dict, or None when unreadable."""
if not dialog.goto_tab(ws, TAB):
return None
return parse_usage_text(dialog.dialog_text(ws))
def theme(ws):
"""Current theme radio value, or None when unreadable."""
if not dialog.goto_tab(ws, TAB):
return None
for r in controls.list_radios(ws):
if r.get("checked"):
return r.get("value")
return None
def set_theme(ws, value):
"""Set theme by aria-label; verify-then-fallback. Bool."""
if not dialog.goto_tab(ws, TAB):
return False
return controls.set_radio_by_aria(ws, value)
def redeem_row(ws):
"""True when the 'Redeem invite code' row is present (eligible)."""
if not dialog.goto_tab(ws, TAB):
return False
text = (dialog.dialog_text(ws) or "").lower()
return "redeem" in text and "invite code" in text
def describe(ws):
"""Full General inventory: usage, theme, redeem entrypoint."""
return {"usage": usage(ws), "theme": theme(ws),
"redeem_row": redeem_row(ws)}
+10
View File
@@ -0,0 +1,10 @@
"""Help & support tab: read-only inventory (help links)."""
from hatch_menu import dialog
TAB = "Help & support"
def describe(ws):
"""Row inventory + text excerpt (describe-only for now)."""
return {"rows": dialog.describe_rows(ws, TAB),
"text": (dialog.dialog_text(ws) or "")[:400]}
+10
View File
@@ -0,0 +1,10 @@
"""Legal info tab: read-only inventory (legal links)."""
from hatch_menu import dialog
TAB = "Legal info"
def describe(ws):
"""Row inventory + text excerpt (describe-only for now)."""
return {"rows": dialog.describe_rows(ws, TAB),
"text": (dialog.dialog_text(ws) or "")[:400]}
+10
View File
@@ -0,0 +1,10 @@
"""Messaging channels tab: read-only inventory (channel rows)."""
from hatch_menu import dialog
TAB = "Messaging channels"
def describe(ws):
"""Row inventory + text excerpt (describe-only for now)."""
return {"rows": dialog.describe_rows(ws, TAB),
"text": (dialog.dialog_text(ws) or "")[:400]}
+383
View File
@@ -0,0 +1,383 @@
"""Permissions tab: defaults radios, website modes, protocols, advanced.
Contracts live here (single copy): toggles.py references these
constants for addressing. Drills open sub-pages, read, and come back
via the back affordance with tab re-entry fallback. All flows are
ws-level; sessions and error shaping live in toggles.py.
"""
import re
import time
from approvals import cdp_evaluate
from hatch_menu import controls, dialog
from hatch_menu.mouse import escape, real_click
TAB = "Permissions"
ROOT_MARK = "Manage permissions"
CONNECTOR_HEADING = "Connector defaults"
WEB_HEADING = "Web access defaults"
DEFAULT_VALUES = ("auto_allow", "always_ask")
WEBSITE_MODES = ("Allow", "Ask", "Deny")
ADV_LABELS = {"transparent_proxy": "Transparent proxy",
"tls_interception": "TLS interception",
"sni_mismatch_rejection": "SNI mismatch rejection"}
PROTOCOL_SLUGS = {"Model Context Protocol servers (SSE)": "mcp-sse",
"Model Context Protocol servers (Streamable HTTP)":
"mcp-streamable",
"Agent Skills endpoints": "agent-skills",
"MCP Apps (UI extensions)": "mcp-apps",
"MCP remote OAuth": "mcp-oauth"}
JS_WEBSITES = """(() => {
const d = document.querySelector('[role="dialog"]');
if (!d) return null;
return Array.from(d.querySelectorAll('button')).filter(b =>
['allow', 'ask', 'deny'].includes((b.getAttribute('aria-label') || '')
.trim().toLowerCase())).map(b => {
let el = b.parentElement, host = '', depth = 0;
while (el && el !== d && depth < 6) {
const t = (el.innerText || '').trim().split('\\n')[0] || '';
if (t && t.includes('.') && t.length < 120) { host = t; break; }
el = el.parentElement;
depth += 1;
}
return {host: host, mode: (b.getAttribute('aria-label') || '').trim(),
x: b.getBoundingClientRect().x + b.getBoundingClientRect().width / 2,
y: b.getBoundingClientRect().y + b.getBoundingClientRect().height / 2};
});
})()"""
JS_MODE_MENU = """(() => {
return Array.from(document.querySelectorAll('[role="menuitem"]'))
.map(m => ({text: (m.innerText || '').trim()}));
})()"""
JS_CLICK_MODE = """((mode) => {
const m = Array.from(document.querySelectorAll('[role="menuitem"]'))
.find(el => (el.innerText || '').trim() === mode);
if (!m) return 'NO_MATCH';
m.click();
return 'CLICKED';
})('%s')"""
JS_MODE_RECT = """((mode) => {
const m = Array.from(document.querySelectorAll('[role="menuitem"]'))
.find(el => (el.innerText || '').trim() === mode);
if (!m) return null;
const r = m.getBoundingClientRect();
return {x: r.x + r.width / 2, y: r.y + r.height / 2};
})('%s')"""
JS_PROTO_ROWS = """(() => {
const d = document.querySelector('[role="dialog"]');
if (!d) return null;
return Array.from(d.querySelectorAll('[role="switch"]')).map(s => {
let el = s.parentElement, label = '', depth = 0;
while (el && el !== d && depth < 6) {
const t = (el.innerText || '').trim().replace(/\\s+/g, ' ');
if (t && t.length < 250) { label = t; break; }
el = el.parentElement;
depth += 1;
}
const r = s.getBoundingClientRect();
return {label: label.slice(0, 120),
checked: s.getAttribute('aria-checked') === 'true',
x: r.x + r.width / 2, y: r.y + r.height / 2};
});
})()"""
def _eval(ws, js, timeout=8.0):
try:
return cdp_evaluate(ws, js, timeout=timeout)
except Exception:
return None
def _slug(label):
"""Protocol slug: registry hit, else slugified, else None."""
if label in PROTOCOL_SLUGS:
return PROTOCOL_SLUGS[label]
clean = re.sub(r"[^a-z0-9]+", "-",
label.strip().lower()).strip("-")
return clean or None
def resolve_protocol(name):
"""Slug or label fragment -> row label, None when unresolvable."""
if not isinstance(name, str) or not name.strip():
return None
want = name.strip().lower()
for label, slug in PROTOCOL_SLUGS.items():
if want == slug or want == label.lower():
return label
return None
def _back_to_root(ws):
"""Back affordance, else tab re-entry; verify root text."""
dialog.go_back(ws)
if ROOT_MARK in (dialog.dialog_text(ws) or ""):
return True
if not dialog.goto_tab(ws, TAB):
return False
return ROOT_MARK in (dialog.dialog_text(ws) or "")
def defaults(ws):
"""Connector + web default values (each value or None)."""
if not dialog.goto_tab(ws, TAB):
return {"connector_defaults": None, "web_access": None}
heads = {CONNECTOR_HEADING.lower(): "connector_defaults",
WEB_HEADING.lower(): "web_access"}
vals = {CONNECTOR_HEADING.lower(): [], WEB_HEADING.lower(): []}
for r in controls.list_radios(ws):
h = (r.get("heading") or "").lower()
if h in vals and r.get("checked"):
vals[h].append(r.get("value"))
out = {}
for h, key in heads.items():
out[key] = vals[h][0] if len(vals[h]) == 1 else None
return out
def set_default(ws, which, value):
"""Set one defaults radio. Bool."""
if not dialog.goto_tab(ws, TAB):
return False
heading = CONNECTOR_HEADING if which == "connector_defaults" \
else WEB_HEADING
return controls.set_radio_by_heading(ws, heading, value)
def ensure_advanced(ws):
"""Expand Advanced network settings when collapsed. Bool."""
if not dialog.goto_tab(ws, TAB):
return False
labels = [s.get("label", "") for s in controls.list_switches(ws)]
if any("Transparent proxy" in lab for lab in labels):
return True
if not dialog.click_row(ws, "Advanced network settings", TAB):
return False
time.sleep(0.6)
labels = [s.get("label", "") for s in controls.list_switches(ws)]
return any("Transparent proxy" in lab for lab in labels)
def advanced(ws):
"""Advanced switch states {key: on/off/None}."""
if not ensure_advanced(ws):
return {k: None for k in ADV_LABELS}
out = {}
for key, label in ADV_LABELS.items():
state = None
for s in controls.list_switches(ws):
if label.lower() in (s.get("label") or "").lower():
state = bool(s.get("checked"))
break
out[key] = ("on" if state else "off") if state is not None \
else None
return out
def set_advanced(ws, key, on):
"""Set one advanced switch. Bool."""
if key not in ADV_LABELS or not ensure_advanced(ws):
return False
return controls.set_switch(ws, ADV_LABELS[key], on)
def _websites_raw(ws):
"""Drill into Websites; rows or None (stays on sub-page)."""
if not dialog.click_row(ws, "Websites", TAB):
return None
time.sleep(0.6)
return _eval(ws, JS_WEBSITES)
def websites(ws):
"""[{host, mode}] (back at root afterwards)."""
rows = _websites_raw(ws)
if rows is None:
return []
out = [{"host": r.get("host"), "mode": r.get("mode")} for r in rows]
_back_to_root(ws)
return out
def website_mode(ws, host):
"""Mode for one host, or None when absent/unreadable."""
for row in websites(ws):
if (row.get("host") or "").lower() == host.lower():
return row.get("mode")
return None
def set_website_mode(ws, host, mode):
"""Set one host mode via the mode chooser. Verify + readback. Bool."""
if mode not in WEBSITE_MODES:
return False
rows = _websites_raw(ws)
if rows is None:
return False
target = next((r for r in rows
if (r.get("host") or "").lower() == host.lower()),
None)
if target is None:
_back_to_root(ws)
return False
try:
real_click(ws, target["x"], target["y"])
except Exception:
_back_to_root(ws)
return False
time.sleep(0.8)
items = _eval(ws, JS_MODE_MENU) or []
texts = [(i.get("text") or "") for i in items]
if mode not in texts:
escape(ws)
_back_to_root(ws)
return False
if _eval(ws, JS_CLICK_MODE % mode) != "CLICKED":
escape(ws)
_back_to_root(ws)
return False
time.sleep(0.6)
rows = _eval(ws, JS_WEBSITES) or []
cur = next(((r.get("mode")) for r in rows
if (r.get("host") or "").lower() == host.lower()),
None)
if cur == mode:
_back_to_root(ws)
return True
rect = _eval(ws, JS_MODE_RECT % mode)
if rect and "x" in rect:
try:
real_click(ws, rect["x"], rect["y"])
except Exception:
pass
time.sleep(0.6)
rows = _eval(ws, JS_WEBSITES) or []
cur = next(((r.get("mode")) for r in rows
if (r.get("host") or "").lower() == host.lower()),
None)
if cur == mode:
_back_to_root(ws)
return True
escape(ws)
_back_to_root(ws)
return False
def _protocols_raw(ws):
"""Drill into protocols; rows or None (stays on sub-page)."""
if not dialog.click_row(ws, "Direct network protocols", TAB):
return None
time.sleep(0.6)
return _eval(ws, JS_PROTO_ROWS)
def protocols(ws):
"""[{slug, label, on}] (back at root afterwards)."""
rows = _protocols_raw(ws)
if rows is None:
return []
out = [{"slug": _slug(r.get("label", "")),
"label": r.get("label", ""),
"on": "on" if r.get("checked") else "off"} for r in rows]
_back_to_root(ws)
return out
def protocol_state(ws, label):
"""on/off for one protocol row label, None when absent."""
for row in protocols(ws):
if row.get("label") == label:
return row.get("on")
return None
def set_protocol(ws, label, on):
"""Set one protocol switch in place; readback before returning."""
rows = _protocols_raw(ws)
if rows is None:
return False
target = next((r for r in rows if r.get("label") == label), None)
if target is None:
_back_to_root(ws)
return False
want = bool(on)
if bool(target.get("checked")) == want:
_back_to_root(ws)
return True
try:
real_click(ws, target["x"], target["y"])
except Exception:
_back_to_root(ws)
return False
time.sleep(0.6)
rows = _eval(ws, JS_PROTO_ROWS) or []
cur = next((r for r in rows if r.get("label") == label), None)
ok = cur is not None and bool(cur.get("checked")) == want
_back_to_root(ws)
return ok
def manage_counts(ws):
"""Manageable-row summary counts (name -> count|None)."""
if not dialog.goto_tab(ws, TAB):
return {}
text = dialog.dialog_text(ws) or ""
out = {}
for name in ("Websites", "Connectors", "Scheduled tasks",
"Direct network protocols"):
m = re.search(re.escape(name) + r"\s*(\d+)", text)
out[name] = int(m.group(1)) if m else None
return out
def scheduled_tasks(ws):
"""[{name, cadence}] (back at root afterwards; empty when none)."""
if not dialog.click_row(ws, "Scheduled tasks", TAB):
return []
time.sleep(0.6)
text = dialog.dialog_text(ws) or ""
_back_to_root(ws)
rows = []
lines = [line.strip() for line in text.splitlines()
if line.strip()]
for i, line in enumerate(lines):
if re.search(r"\b(daily|weekly|hourly|every|min)\b", line,
re.IGNORECASE) and i > 0:
rows.append({"name": lines[i - 1], "cadence": line})
return rows
def all_toggles(ws):
"""Flat map of every settable Permissions toggle (for list)."""
out = {}
defs = defaults(ws)
out["permissions.connector_defaults"] = defs.get("connector_defaults")
out["permissions.web_access"] = defs.get("web_access")
adv = advanced(ws)
for key, val in adv.items():
out["permissions.advanced." + key] = val
for row in protocols(ws):
if row.get("slug"):
out["permissions.protocols:" + row["slug"]] = row.get("on")
for row in websites(ws):
if row.get("host"):
out["permissions.websites:" + row["host"]] = row.get("mode")
return out
def describe(ws):
"""Full Permissions inventory: defaults, counts, adv, rows."""
return {"defaults": defaults(ws), "counts": manage_counts(ws),
"advanced": advanced(ws), "websites": websites(ws),
"protocols": protocols(ws),
"scheduled_tasks": scheduled_tasks(ws)}
+10
View File
@@ -0,0 +1,10 @@
"""Secure store tab: read-only inventory (secret entries + add)."""
from hatch_menu import dialog
TAB = "Secure store"
def describe(ws):
"""Row inventory + text excerpt (describe-only for now)."""
return {"rows": dialog.describe_rows(ws, TAB),
"text": (dialog.dialog_text(ws) or "")[:400]}
+10
View File
@@ -0,0 +1,10 @@
"""Wallet tab: read-only inventory (payment methods + add row)."""
from hatch_menu import dialog
TAB = "Wallet"
def describe(ws):
"""Row inventory + text excerpt (describe-only for now)."""
return {"rows": dialog.describe_rows(ws, TAB),
"text": (dialog.dialog_text(ws) or "")[:400]}
+305
View File
@@ -0,0 +1,305 @@
"""Toggle registry + sessions over controls.py and tabs/*.
Every settable toggle has ONE address; contracts live in the tab
modules (single-copy per part), addressing here. Set flows read back
through a fresh session and never partially report success.
Caller errors (unknown node/toggle/value) raise MenuError before any
CDP traffic. Transport failures return {"ok": False, ...}.
"""
from approvals import VALID_NODES, get_cdp_ws
from hatch_menu import controls, dialog
from hatch_menu.mouse import close, escape
from hatch_menu.tabs import TAB_MODULES
_PERM = TAB_MODULES["Permissions"]
_DC = TAB_MODULES["Data controls"]
_GEN = TAB_MODULES["General"]
class MenuError(ValueError):
"""Caller error: unknown node, toggle, or value. Raised before CDP."""
TOGGLES = {
"permissions.connector_defaults": {
"tab": "Permissions", "kind": "radio-heading",
"heading": _PERM.CONNECTOR_HEADING,
"values": _PERM.DEFAULT_VALUES},
"permissions.web_access": {
"tab": "Permissions", "kind": "radio-heading",
"heading": _PERM.WEB_HEADING,
"values": _PERM.DEFAULT_VALUES},
"permissions.advanced.transparent_proxy": {
"tab": "Permissions", "kind": "adv-switch",
"label": _PERM.ADV_LABELS["transparent_proxy"],
"values": ("on", "off")},
"permissions.advanced.tls_interception": {
"tab": "Permissions", "kind": "adv-switch",
"label": _PERM.ADV_LABELS["tls_interception"],
"values": ("on", "off")},
"permissions.advanced.sni_mismatch_rejection": {
"tab": "Permissions", "kind": "adv-switch",
"label": _PERM.ADV_LABELS["sni_mismatch_rejection"],
"values": ("on", "off")},
"data_controls.ai_improvement": {
"tab": "Data controls", "kind": "switch",
"values": ("on", "off")},
"general.theme": {
"tab": "General", "kind": "radio-aria",
"values": _GEN.THEME_VALUES},
}
WEBSITE_PREFIX = "permissions.websites:"
PROTOCOL_PREFIX = "permissions.protocols:"
def _check_node(node):
if node not in VALID_NODES:
raise MenuError("unknown node: %r (valid: %s)"
% (node, ", ".join(VALID_NODES)))
def normalize_onoff(value):
"""on/off/true/false/1/0/yes/no -> bool. None when invalid."""
if isinstance(value, bool):
return value
if not isinstance(value, str):
return None
v = value.strip().lower()
if v in ("on", "true", "1", "yes"):
return True
if v in ("off", "false", "0", "no"):
return False
return None
def resolve_toggle(name):
"""Resolve a toggle address to its spec. Raises MenuError."""
if not isinstance(name, str) or not name:
raise MenuError("toggle name must be a non-empty string")
if name in TOGGLES:
spec = dict(TOGGLES[name])
spec["name"] = name
return spec
if name.startswith(WEBSITE_PREFIX) and len(name) > len(WEBSITE_PREFIX):
return {"name": name, "tab": "Permissions", "kind": "website",
"host": name[len(WEBSITE_PREFIX):],
"values": _PERM.WEBSITE_MODES}
if name.startswith(PROTOCOL_PREFIX) and len(name) > len(PROTOCOL_PREFIX):
label = _PERM.resolve_protocol(name[len(PROTOCOL_PREFIX):])
if label is None:
raise MenuError("unknown protocol: %r (see list_toggles)"
% (name,))
return {"name": name, "tab": "Permissions", "kind": "protocol",
"label": label, "values": ("on", "off")}
raise MenuError("unknown toggle: %r (see list_toggles)" % (name,))
def _session(node):
_check_node(node)
try:
ws, _ = get_cdp_ws(node)
except Exception as e:
raise MenuError("CDP unreachable for %s: %s" % (node, e))
return ws
def _normalize_value(spec, value):
kind = spec["kind"]
if kind == "website":
if not isinstance(value, str):
raise MenuError("mode must be one of %s"
% (spec["values"],))
for m in spec["values"]:
if value.strip().lower() == m.lower():
return m
raise MenuError("mode must be one of %s (got %r)"
% (spec["values"], value))
if kind in ("switch", "adv-switch", "protocol"):
b = normalize_onoff(value)
if b is None:
raise MenuError("value must be on/off (got %r)" % (value,))
return "on" if b else "off"
if kind == "radio-heading":
if value not in spec["values"]:
raise MenuError("%s must be one of %s (got %r)"
% (spec["name"], spec["values"], value))
return value
if kind == "radio-aria":
if not isinstance(value, str):
raise MenuError("theme must be one of %s" % (spec["values"],))
v = value.strip().lower()
if v in spec["values"]:
return v
raise MenuError("theme must be one of %s (got %r)"
% (spec["values"], value))
raise MenuError("cannot set kind %r" % (kind,))
def get_toggle(node, name):
"""Read one toggle. Returns {"ok", "node", "toggle", "value"}."""
spec = resolve_toggle(name)
_check_node(node)
try:
ws = _session(node)
except MenuError as e:
return {"ok": False, "node": node, "toggle": name,
"error": str(e)}
try:
if not dialog.open_settings(ws):
return {"ok": False, "node": node, "toggle": name,
"error": "settings dialog did not open"}
if not dialog.goto_tab(ws, spec["tab"]):
return {"ok": False, "node": node, "toggle": name,
"error": "tab did not open: %s" % spec["tab"]}
kind = spec["kind"]
if kind == "radio-heading":
vals = {r["value"]: r["checked"]
for r in controls.list_radios(ws)
if (r.get("heading") or "").lower()
== spec["heading"].lower()}
on = [v for v, c in vals.items() if c]
value = on[0] if len(on) == 1 else None
elif kind == "radio-aria":
vals = [(r.get("value"), r.get("checked"))
for r in controls.list_radios(ws)]
on = [v for v, c in vals if c]
value = on[0] if len(on) == 1 else None
elif kind == "switch":
state = _DC.ai_improvement(ws)
value = ("on" if state else "off") if state is not None \
else None
elif kind == "adv-switch":
_PERM.ensure_advanced(ws)
state = controls.switch_state(ws, spec["label"])
value = ("on" if state else "off") if state is not None \
else None
elif kind == "website":
value = _PERM.website_mode(ws, spec["host"])
elif kind == "protocol":
value = _PERM.protocol_state(ws, spec["label"])
else:
value = None
if value is None:
return {"ok": False, "node": node, "toggle": name,
"error": "toggle not readable (site changed?)"}
return {"ok": True, "node": node, "toggle": name, "value": value}
except Exception as e:
return {"ok": False, "node": node, "toggle": name,
"error": "%s: %s" % (type(e).__name__, e)}
finally:
escape(ws)
close(ws)
def set_toggle(node, name, value):
"""Set one toggle with readback. Never partially reports success."""
spec = resolve_toggle(name)
want = _normalize_value(spec, value)
_check_node(node)
try:
ws = _session(node)
except MenuError as e:
return {"ok": False, "node": node, "toggle": name,
"error": str(e)}
try:
if not dialog.open_settings(ws):
return {"ok": False, "node": node, "toggle": name,
"error": "settings dialog did not open"}
if not dialog.goto_tab(ws, spec["tab"]):
return {"ok": False, "node": node, "toggle": name,
"error": "tab did not open: %s" % spec["tab"]}
kind = spec["kind"]
if kind == "radio-heading":
ok = controls.set_radio_by_heading(ws, spec["heading"], want)
elif kind == "radio-aria":
ok = controls.set_radio_by_aria(ws, want)
elif kind == "switch":
ok = _DC.set_ai_improvement(ws, want == "on")
elif kind == "adv-switch":
_PERM.ensure_advanced(ws)
ok = controls.set_switch(ws, spec["label"], want == "on")
elif kind == "website":
ok = _PERM.set_website_mode(ws, spec["host"], want)
elif kind == "protocol":
ok = _PERM.set_protocol(ws, spec["label"], want == "on")
else:
ok = False
if not ok:
return {"ok": False, "node": node, "toggle": name,
"error": "set failed verification (site changed?)"}
readback = get_toggle(node, name)
if not readback.get("ok") or readback.get("value") != want:
return {"ok": False, "node": node, "toggle": name,
"error": "readback mismatch (want %r, got %r)"
% (want, readback.get("value"))}
return {"ok": True, "node": node, "toggle": name, "value": want}
except Exception as e:
return {"ok": False, "node": node, "toggle": name,
"error": "%s: %s" % (type(e).__name__, e)}
finally:
escape(ws)
close(ws)
def list_toggles(node, tab=None):
"""All toggle states, optionally filtered to one tab."""
_check_node(node)
if tab is not None and tab not in TAB_MODULES:
raise MenuError("unknown tab: %r (valid: %s)"
% (tab, sorted(TAB_MODULES)))
want = [tab] if tab else ["Permissions", "Data controls", "General"]
try:
ws = _session(node)
except MenuError as e:
return {"ok": False, "node": node, "error": str(e)}
try:
if not dialog.open_settings(ws):
return {"ok": False, "node": node,
"error": "settings dialog did not open"}
out = {}
if "Permissions" in want:
dialog.goto_tab(ws, "Permissions")
out.update(_PERM.all_toggles(ws))
if "Data controls" in want:
state = _DC.ai_improvement(ws)
out["data_controls.ai_improvement"] = \
("on" if state else "off") if state is not None else None
if "General" in want:
dialog.goto_tab(ws, "General")
out["general.theme"] = _GEN.theme(ws)
return {"ok": True, "node": node, "toggles": out}
except Exception as e:
return {"ok": False, "node": node,
"error": "%s: %s" % (type(e).__name__, e)}
finally:
escape(ws)
close(ws)
def describe_tab(node, tab):
"""Full inventory of one tab (debugging/patching aid)."""
_check_node(node)
if tab not in TAB_MODULES:
raise MenuError("unknown tab: %r (valid: %s)"
% (tab, sorted(TAB_MODULES)))
try:
ws = _session(node)
except MenuError as e:
return {"ok": False, "node": node, "tab": tab, "error": str(e)}
try:
if not dialog.open_settings(ws):
return {"ok": False, "node": node, "tab": tab,
"error": "settings dialog did not open"}
if not dialog.goto_tab(ws, tab):
return {"ok": False, "node": node, "tab": tab,
"error": "tab did not open"}
return {"ok": True, "node": node, "tab": tab,
"inventory": TAB_MODULES[tab].describe(ws)}
except Exception as e:
return {"ok": False, "node": node, "tab": tab,
"error": "%s: %s" % (type(e).__name__, e)}
finally:
escape(ws)
close(ws)