13 KiB
DOM: Approval & Permission Dialogs
Box is the main surface. All operator work goes through Box (box.muse-dev.online). The web UI,
boxCLI, and agents share the same API endpoints. No UI-only powers.
Companion to DOM-APPROVALS-SPEC.md (behavioral contract: detection →
classification → handling → exit codes). This doc is the DOM surface
reference: what the dialogs look like in the tree, which selectors find
them, how check_approvals works today, and what's fragile.
Related: DOM-EDGE-STATES.md §5.2, DOM-PAGE-STRUCTURE.md §3,
DOM-INDEX.md §4f.
1. Observed specimen (real, from docs/pip-approval-dialog.png)
Captured on pip's node during onboarding. This is a Muse platform dialog rendered inside the chat DOM (bottom-anchored card over the message stream) — not a browser-chrome permission bubble.
┌─────────────────────────────────────────────────────────────┐
│ 🌐 Allow pip to share information with 34.139.37.135? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ pip wants to connect to a remote server to complete │ │ (expandable detail, chevron)
│ │ onboarding. ⌄ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ [ Allow once ] [ Always allow this site ] [ Deny ] │
└─────────────────────────────────────────────────────────────┘
| Field | Observed value |
|---|---|
| Title text | Allow pip to share information with 34.139.37.135? |
| Title pattern | Allow <agent> to share information with <IP>? |
| Detail text | pip wants to connect to a remote server to complete onboarding. |
| Detail pattern | <agent> wants to <action> to <purpose>. (collapsible) |
| Buttons (left→right) | Allow once (primary/blue), Always allow this site, Deny |
| Icon | globe/network glyph preceding the title |
Trigger class: the agent attempted an outbound network action (connecting to a remote server). Other trigger classes are unobserved — geolocation, camera/mic, notifications, clipboard, and download prompts have not been captured on fleet nodes yet (see §8 induction recipe).
2. Dialog DOM structure (as known)
No [role="dialog"] or [role="alertdialog"] elements have been observed in
settled-state probes (DOM-PAGE-STRUCTURE.md §3.2). The dialog is found
by text, not by structure — this is the central fragility of the current
implementation.
Known structural facts (Verified 2026-10-06 live capture on fleet):
- The dialog is in-DOM (React-rendered), so
Runtime.evaluatesees it; no shadow-DOM piercing needed. - Container:
div[data-testid="hatch-inline-approval-card"]withdata-hatch-approval-surface="panel". - Header:
div[data-testid="approval-panel-header"]. - Primary Allow action:
button[data-hatch-approval-primary-action="true"]("Allow once"). - Dropdown options:
button[data-slot="dropdown-menu-trigger"](contains "Always allow"). - Deny action:
<button>with innerText "Deny". - Secondary/Background queued approvals surface:
div[data-testid="hatch-inline-approval-card"][data-hatch-background-approval-surface="true"]with<p class="text-footnote text-text-secondary">N tasks need review</p>and<button data-pel-click="chat_background_approval_review">Review</button>.
Confirmed Live Selectors (2026-10-06):
// Active approval panel container
'div[data-testid="hatch-inline-approval-card"]'
// Panel header
'[data-testid="approval-panel-header"]'
// Primary action button ("Allow once")
'button[data-hatch-approval-primary-action="true"]'
// Background review surface ("N tasks need review" / "A task needs review")
'[data-hatch-background-approval-surface="true"]'
// Background review button trigger
'[data-pel-click="chat_background_approval_review"]'
Queued Background Task Reviews ("N tasks need review")
When multiple scheduled tasks or background operations trigger approval prompts concurrently (e.g. opm with scheduled Fleet Health Monitor runs):
- Muse renders the first approval card inline over chat.
- Below it, Muse docks a background approval banner:
<div data-testid="hatch-inline-approval-card" data-hatch-background-approval-surface="true">stating"N tasks need review"with a"Review"button (data-pel-click="chat_background_approval_review"). - Allowing or denying the active approval immediately advances the queue: the next queued task pops into the active inline panel, decrementing the background count (e.g. from 2 down to "A task needs review" to clear).
- Automated approval engines must inspect both the active card and
[data-hatch-background-approval-surface="true"]to confirm whether agents remain blocked.
Troubleshooting Blocked Agents
- In
muse-tui: Type/blocked(or press[a]/F2) from any view to open the Approvals & Blocked Tasks Drawer. Usej/kor arrow keys to navigate between blocked nodes; press[1]to Allow,[2]Always,[3]Deny,[R]Proceed,[X]Dismiss. Actions target the highlighted agent. - In CLI: Run
box blockedorbox approvalsto view fleet approval status; usebox approvals allow <node>to approve.
3. How check_approvals works today
Location: bin/muse-chat-api.py, check_approvals(ws) (~line 70).
Returns [(dialog_text, is_trusted, action_taken)].
Detection (two passes, JS evaluated via CDP Runtime.evaluate):
- If
document.body.innerTextcontains bothAllowandto share: collect elements whoseinnerTextcontains both and is < 500 chars (first 3, text sliced to 200 chars). - Fallback: if no dialog found yet and ≥ 2
<button>s have text matchingallow/deny/block, take the first button's closestdivtext (200 chars).
Classification (Python side):
- Extract IPv4s with
\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b. - No IP → ignored entirely (added 2026-10-04 to fix false positives where chat content mentioning "allow" blocked sends).
- IP in
TRUSTED_IPS→ trusted → auto-approve. Else → untrusted.
Handling:
- Trusted: click the button whose text contains
allow once, else the button whose text is exactlyallow. Recordsclicked:<text>orNOTFOUND. - Untrusted: no click. Action =
APPROVAL_NEEDED.
Trusted set (TRUSTED_IPS):
TRUSTED_IPS = {
"34.139.37.135", # VM (gateway)
"100.123.153.75", # bl (main compute)
"100.81.31.9", # VM tailnet
}
Enforcement points (from DOM-APPROVALS-SPEC.md):
| Command | Behavior |
|---|---|
send |
Gates: untrusted dialog → APPROVAL_NEEDED on stderr, exit 2, message NOT sent |
upload |
Gates (same as send) |
messages |
Observes only, never gates |
wait |
Re-checks every 5s; untrusted → exit 2 |
approvals |
Reports; never gates |
4. Deliberate safety properties (do not regress)
- Least-privilege auto-approve. The click finder prefers
allow onceoverallow. The observed dialog also offersAlways allow this site— the finder can never select it:"always allow this site"contains neitherallow onceas a targeted match… more precisely, the predicate ist.includes('allow once') || t === 'allow'(lowercased), and"always allow this site" === "allow"is false while"always allow this site".includes("allow once")is false. Persistent grants are never auto-clicked. Keep it that way. - Fail closed on untrusted IPs. Unknown IP → exit 2, human decides.
- No click without a dialog. Buttons are only clicked after a dialog was positively identified.
5. Known gaps & fragilities (input to the approvals work)
- Text-only detection, no structural anchor. If the platform rewords
the dialog (
Allow … to share→Grant … access), detection silently stops working. Next live capture should confirm or deny[role="dialog"]/data-testidhooks and prefer them when present. - False-negative trade (2026-10-04). Dialogs without an IPv4 are now
ignored, not escalated. A real non-IP prompt (e.g.
Allow pip to share your location?, camera/mic) would pass silently. Consider: escalate-on-unknown-pattern vs. ignore — currently ignore. This is the largest open semantics question. - IP regex is loose.
\d{1,3}matches999.999.999.999and version strings like1.2.3.4in chat content. A stricter octet check (0–255) plus context (is the IP inside the dialog element, not just the 200-char slice?) would cut false trust. - Full-DOM scan per call.
document.querySelectorAll('*')with aninnerTextfilter on every element runs on eachsend. Fine today; scope to overlay/portal regions if it ever shows up in profiles. .click()vs trusted input.DOM-INDEX.mdnotes Radix controls often need trusted CDP pointer events, not JS.click(). If the dialog buttons are Radix-based, auto-approve may silently no-op (recordsclicked:but nothing happens). Verify with a trusted-IP dialog: does the dialog actually dismiss after auto-approve?- Fallback ordering quirk. The ≥2-buttons fallback only runs when pass 1 found nothing. A page with both a share-dialog and unrelated allow/deny buttons elsewhere takes the share-dialog path — correct — but a non-share permission dialog on a page that also contains the words "Allow"/"to share" somewhere in chat content could misattribute. The <500-char element filter mitigates; not proven.
- Check-then-act race.
sendchecks once, then sends. A dialog appearing between check and send is unhandled until the next command.wait's 5s re-check is the only mitigation. - No deny path. Automation can approve (trusted) or escalate (untrusted) but never deny. A spurious trusted-IP dialog can only be approved or left for a human. If denial becomes a needed primitive, it must be an explicit, logged, human-gated action — never automatic.
- Truncation. Dialog text is sliced to 200 chars at capture, 80 in
the action record. Enough for the IP check; lossy for forensics.
Consider logging the full dialog text to the audit trail on
APPROVAL_NEEDED.
6. Induction recipe (to reproduce an approval state)
Not executed by the documenting subagent (no live-browser capability). Run by a browser-capable operator. Fleet nodes on bl only — never the user's personal browser.
Option A — browser permission prompt (deterministic):
- Pick a node (pip is a good candidate; CDP 9420 in
warp-pip). - CDP
Page.navigatetohttps://permission.site/(Google's benign permission-test page; the established navigate pattern is inbin/chat-state-check.py:229/bin/meta-acct.py:46). - Trigger one permission (Notification is the least invasive).
- Capture: full dialog DOM via
Runtime.evaluate— record tag names, classes,data-testids,roles, button texts and order, and whether the dialog lives in a portal/root container vs. inline. - Clean up: click Deny. Confirm the dialog dismisses and the chat
page is unaffected. Navigate back to
https://muse.ai/and verify the agent session is intact (messagesreturns content).
Option B — platform approval via chat (uses existing tooling):
- Through the chat API, ask the agent to perform an action that needs a platform grant (e.g. "download this URL", "share your screen"). The specimen in §1 arose exactly this way (outbound connection during onboarding).
- Less deterministic than Option A; useful for capturing platform (not browser-chrome) dialog variants.
Capture checklist for either option:
- Screenshot (like
docs/pip-approval-dialog.png) outerHTMLof the dialog container (first 2KB)- Confirm/deny:
[role="dialog"],[role="alertdialog"],data-testid - Button list in DOM order with exact innerText
- Whether
check_approvalsdetects it (runmuse-chat-api.py approvals) - Auto-approve behavior if trusted IP (does the dialog actually dismiss?)
Safety rules: deny/dismiss when done; never click allow on anything untrusted; never run induction on the user's personal browser; leave the node's chat session exactly as found.
7. Cross-references
DOM-APPROVALS-SPEC.md— the behavioral contract (detection, classification, handling, exit codes, operator flow). Read first.DOM-PAGE-STRUCTURE.md§3 — earlier writeup ofcheck_approvalsmechanics and selector patterns.DOM-EDGE-STATES.md§5.2 — approval dialogs as the one real "banner" class; false-positive traps for text matching.DOM-INDEX.md§4f — quick-reference checklist entry.bin/muse-chat-api.py—check_approvals(ws)(~line 70),TRUSTED_IPS(line 47), enforcement insend/upload/wait.docs/pip-approval-dialog.png— the §1 specimen screenshot.