diff --git a/.gitignore b/.gitignore
index a9a9569..78f6d42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@ node_modules/
.output/
.output-e2e/
.output-store/
+.output-advanced/
+.output-e2e-advanced/
.wxt/
dist/
coverage/
diff --git a/AGENTS.md b/AGENTS.md
index 1def44b..719cdb0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -18,6 +18,7 @@ pnpm typecheck
pnpm test # unit + integration (Vitest)
pnpm test:e2e # builds, then runs Playwright against the unpacked build
pnpm build # .output/chrome-mv3
+pnpm build:advanced # .output-advanced/chrome-mv3, adds the `debugger` permission (zip with `pnpm zip:advanced`)
pnpm zip # sideload zip (keeps the pinned extension key)
pnpm zip:store # Chrome Web Store zip (manifest without `key`)
```
@@ -54,6 +55,7 @@ Rules:
- The side panel never reads the active page or the person's other tabs. There are two opt-in exceptions, both off by default:
- The "Ask Dewee" page script sends the text the person selected (never form fields) when they click its button, and only as an editable draft.
- "Connected browser" (self-hosted only) lets an agent read and act only in tabs it opened itself, only on exact origins the person allowed for that agent in the panel, only until the grant expires or is revoked, and only after the person granted Chrome host permission for those origins. Password, hidden, payment and one-time-code values are never read or typed, page content is never stored, and Pause/Stop/Revoke work locally. See [docs/browser-control-threat-model.md](docs/browser-control-threat-model.md).
+- The `debugger` permission exists only in the Advanced build profile. Its adapter attaches only to agent-owned tabs, only while a command runs and the person has Advanced mode on, and calls only the fixed CDP method list in `src/browser/adapters/cdp/cdp-client.ts`. Never add `Runtime.evaluate`, `Network`, `Storage`, cookie or `Target` methods, a browser-level target, a remote debugging port, or a CDP method or params taken from a command. Screenshots are viewport-only, never stored, and never fall back to another path.
## Code layout
diff --git a/README.md b/README.md
index c252dd0..5d78306 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,8 @@ Chat with your Dewee agents, with Super Agent pinned first, in Chrome's side pan
- **Workspaces and agents:** pick a workspace, search agents, and keep separate sessions per agent.
- **Reliable chat:** streaming, stop, retry, history, new chat. If the connection drops, the panel checks the server instead of sending your message twice.
- **Credential cards:** secrets stay in the form and go only to Dewee's respond endpoint. Nothing secret is stored by the extension.
-- **Connected browser (self-hosted, opt-in):** turn on *Connected browser* in Settings, register this browser, and allow chosen agents on chosen sites for a limited time. Agents can then open, read, and act in tabs they open themselves, never in your other tabs. Risky clicks wait for your approval, passwords and payment fields are never read or typed, and *Pause*, *Stop* and *Revoke* are always one click away.
+- **Connected browser (self-hosted, opt-in):** turn on *Connected browser* in Settings, register this browser, and allow chosen agents on chosen sites for a limited time. Agents can then open, read, and act in tabs they open themselves, never in your other tabs. Risky clicks wait for your approval, passwords and payment fields are never read or typed, and *Pause*, *Stop* and *Revoke* are always one click away. Agent tabs carry an "AI" badge, and using one yourself pauses the agents.
+- **Advanced build (sideload, opt-in):** `pnpm build:advanced` adds Chrome's `debugger` permission. After you turn on *Advanced mode* in the browser drawer, agents read pages through the accessibility tree, use real input and can take viewport screenshots, in their own tabs only. It shares the extension ID with the standard build, so install one or the other. See the [threat model](docs/browser-control-threat-model.md#advanced-mode-debugger).
- **Pending badge**, English and Vietnamese, light/dark/system theme, keyboard shortcuts.
## Install (development build)
diff --git a/docs/api-contract.md b/docs/api-contract.md
index eae31f6..6e90d31 100644
--- a/docs/api-contract.md
+++ b/docs/api-contract.md
@@ -197,7 +197,7 @@ What the extension adds on its side of the contract:
- Every command is checked again locally before it runs: grant present, not expired or revoked, agent in the grant, capability, exact origin of both the tab's current page and any requested URL, restricted pages (every `about:` page included, so `open about:blank` is refused), owned tab, Chrome host permission for that exact host, and local Pause/Stop.
- `ack` is sent after validation and before any work; exactly one `browser.command.result` follows. A command id is never executed twice while the service worker lives; after a worker restart this relies on the Gateway not resending a dispatched command. Results are not queued across a reconnect; commands in flight when the socket drops are cancelled locally, and the Gateway's deadline rules apply. A command runs for at most 120 s locally, whatever its deadline.
- `type` with `submit`, `press` with `Enter`, and clicks on high-risk elements wait for the person's "Allow once" in the side panel and fail with `approval_denied` otherwise; if the element changed while the person decided, they fail with `stale_ref`.
-- `type` is refused (`sensitive_field`) for password, PIN, payment-card and one-time-code fields, detected by input type, `autocomplete` (`cc-*`, `one-time-code`, `current-password`, `new-password`) and name/id hints; the snapshot redacts their values. `screenshot` answers `unsupported_capability` (no advanced build yet).
+- `type` is refused (`sensitive_field`) for password, PIN, payment-card and one-time-code fields, detected by input type, `autocomplete` (`cc-*`, `one-time-code`, `current-password`, `new-password`) and name/id hints; the snapshot redacts their values. `screenshot` answers `unsupported_capability` in the Standard build and while Advanced mode is off; in the Advanced build with Advanced mode on it returns a viewport JPEG of at most 1.5 MiB, or `debugger_unavailable` when another debugger holds the tab.
- Grants and devices are read by `grant_id` / `device_id`, also accepting `id`, since v1 does not pin the object shape beyond those fields.
- A command whose grant is revoked while it runs, or that arrives for a revoked grant, fails with `grant_revoked`. Page results (`open`, `navigate`, `snapshot`, `click`, `type`, `screenshot`) always carry the page `url`; a result without a valid one is reported as `internal`, or as `outcome_unknown` after an interaction (the action may have run). A refresh (`attach`, `browser.grant.list`) that no longer lists a grant cancels its in-flight commands, approvals included, with `grant_revoked`.
- The person's last Pause/Stop/Resume is kept per profile and device in `storage.local` and re-sent after every `attach` (`browser.control.state`), so a Chrome restart, a profile switch or a Gateway restart cannot lift a Pause. `active` is re-sent only when the person's last choice was Resume; with no stored choice nothing is sent and the Gateway keeps its state. A stored choice that cannot be read leaves the panel paused locally and sends nothing (see [browser-control-threat-model.md](browser-control-threat-model.md)).
diff --git a/docs/architecture.md b/docs/architecture.md
index b0f71bc..ecb89ca 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -57,16 +57,21 @@ Gateway ──/ws──► service worker
executor.ts policy re-check before every side effect, then the action
policy.ts grant, agent, capability, exact origin, restricted pages, owned tab, host permission
tabs.ts owned tabs t1, t2…, generation per document, single writer, user-input hold
+ engine-router.ts Standard or debugger per action; refs stay with the engine that issued them
chrome-api.ts ──► chrome.tabs + chrome.scripting.executeScript({ func: pageRun, args })
snapshot.ts (bundled page runtime, isolated world)
-side panel ◄─port "browser-control"─ state, approval cards; Pause/Resume/Stop, enroll, grants
+ adapters/cdp/ ──► chrome.debugger (Advanced build only, opt-in, owned tabs, method whitelist)
+ tab-marker.ts "AI" badge and title on agent tabs
+side panel ◄─port "browser-control"─ state, approval cards, agent tabs, timeline; Pause/Resume/Stop, enroll, grants, Advanced mode
```
- **Lifecycle.** The socket exists only while the "Connected browser" preference is on, the active profile is a self-hosted Gateway and it holds an API key. A profile switch, sign-out or turning the preference off closes it, cancels running commands, denies open approvals and forgets grants and owned tabs. Dewee Cloud shows "not available yet" and opens nothing. A one-minute `chrome.alarms` alarm revives a suspended worker, which reconnects and re-attaches with the stored device id; the 20 s heartbeat keeps it alive while connected (Chrome 116+).
-- **Stored.** `storage.local`: the server-generated device id and name, the ids of grants created here and the person's last Pause/Stop/Resume, per profile (not secrets; `storage.local` stays locked to trusted contexts), so Pause/Stop survives a Chrome restart and a profile switch. `storage.session`: owned-tab handles, so a worker restart keeps them. Never stored: the API key outside the credential store, page content, snapshots, typed text, screenshots.
+- **Stored.** `storage.local`: the server-generated device id and name, the ids of grants created here and the person's last Pause/Stop/Resume, per profile (not secrets; `storage.local` stays locked to trusted contexts), so Pause/Stop survives a Chrome restart and a profile switch. `storage.session`: owned-tab handles, so a worker restart keeps them, and the Advanced mode opt-in. Never stored: the API key outside the credential store, page content, snapshots, typed text, screenshots.
- **Commands.** Each `browser.command` is validated strictly (unknown keys, actions or keys for `press` are refused), acknowledged, executed and answered with exactly one result. A command id is executed at most once. Results are never queued across a reconnect: a dropped link cancels the commands it carried. An interrupted command that may already have changed the page reports `outcome_unknown`.
-- **Pages.** Only `chrome.tabs` and `chrome.scripting.executeScript` with the bundled `pageRun` function (func + args, never a code string), top frame only. Same-origin iframes are read only when their origin is granted; cross-origin iframes are omitted. No `tabs` and no `debugger` permission; screenshots answer `unsupported_capability`.
-- **The person stays in charge.** High-risk clicks and `type` with `submit` wait for an "Allow once" card in an open side panel; no panel or a denial gives `approval_denied`. Pause and Stop cancel what is running and refuse new commands (Stop also releases the agent's tabs). A click or key press by the person in an owned tab cancels the agent's command there and holds the tab for 5 s.
+- **Pages.** Only `chrome.tabs` and `chrome.scripting.executeScript` with the bundled `pageRun` function (func + args, never a code string), top frame only. Same-origin iframes are read only when their origin is granted; cross-origin iframes are omitted. No `tabs` permission. The Standard build has no `debugger` permission; screenshots answer `unsupported_capability`.
+- **Build profiles.** One codebase, two builds. Standard (`pnpm build`) is the default. Advanced (`DEWEE_PROFILE=advanced`, `pnpm build:advanced`) adds the `debugger` permission and the CDP adapter; the `__DEWEE_ADVANCED__` define drops that adapter from Standard bundles. A device enrolls with what the build can do (`["standard"]` or `["standard","advanced"]`) and each attach announces what is usable now: `advanced` only while the person has Advanced mode on.
+- **Advanced mode.** Off by default, per browser session, turned on in the drawer. `EngineRouter` sends snapshot, describe, highlight, click, type and press to the CDP adapter (accessibility tree, `Input.dispatch*`) and screenshots only there; scroll, wait, find and tab actions stay on the page script. The adapter attaches to the owned tab when a debugger command starts and detaches when it ends, and on Stop, Pause, revoke, expiry, disconnect, opt-in off and tab close. If attaching fails (DevTools open), it answers `debugger_unavailable`, or uses the Standard path for snapshot and press when the person allowed that fallback. Same-process frames on granted origins are read and driven; cross-site frames are left out and blacked out in screenshots. Details: [browser-control-threat-model.md](browser-control-threat-model.md#advanced-mode-debugger).
+- **The person stays in charge.** High-risk clicks and `type` with `submit` wait for an "Allow once" card in an open side panel; no panel or a denial gives `approval_denied`. Pause and Stop cancel what is running and refuse new commands (Stop also releases the agent's tabs). A click or key press by the person in an owned tab cancels the agent's command there, holds the tab for 5 s and pauses control until Resume. Agent tabs carry an "AI" badge; the drawer lists them with agent, site and expiry, shows the active command's agent, channel and site scope, and keeps a short in-memory timeline of outcomes. Before a click the target is highlighted briefly.
## Decisions
diff --git a/docs/browser-control-threat-model.md b/docs/browser-control-threat-model.md
index 409ac72..a8aee4b 100644
--- a/docs/browser-control-threat-model.md
+++ b/docs/browser-control-threat-model.md
@@ -18,20 +18,38 @@ The person's signed-in web sessions. An agent that can read and click in a tab c
| Threat | Control |
|---|---|
| A compromised or buggy Gateway sends commands or grants the person did not allow | Grants are not taken on the Gateway's word. The extension uses a grant only if this browser created it (its id is remembered in `storage.local` when the person creates it), it names this device, every origin in it is an exact origin, and, on creation, its origins match what the person asked for. Any other grant from attach, list or events is ignored. Every command is then re-checked locally against that grant: present, unexpired, unrevoked; agent listed; capability; exact origin of the tab's current page and of any requested URL; owned tab. Before every page action Chrome must hold host permission for that exact host (`permissions.getAll`); a broad pattern held for another feature, such as Ask Dewee's `https://*/*`, does not count. |
-| Code injection from the wire | No remote code, `eval` or CDP. Actions are a fixed list validated with Zod; the page function (`pageRun`) is bundled and receives only data (op, ref, generation, text). Selectors and scripts are never accepted. The page function is injected only after `storage.local` is confirmed locked to trusted extension contexts. |
+| Code injection from the wire | No remote code or `eval`; no CDP in the Standard build, and in the Advanced build only the fixed method list below. Actions are a fixed list validated with Zod; the page function (`pageRun`) is bundled and receives only data (op, ref, generation, text). Selectors and scripts are never accepted. The page function is injected only after `storage.local` is confirmed locked to trusted extension contexts. |
| Agent reaches the person's other tabs | Agents drive only tabs they opened ("owned tabs"), addressed by opaque handles (`t1`, `t2`); raw tab ids never leave the extension. `tabs` lists only owned tabs. The extension has no `tabs` permission, so other tabs' URLs and titles are invisible to it. A tab Chrome creates after the command was cancelled (Stop, revoke, profile switch) is closed, never registered. |
| Navigation or redirect to an ungranted site | The URL is checked before and after every navigation and click; a page that lands outside the grant fails the command, and every later command on that tab is refused while it stays there. `navigate` also needs the page currently in the tab to be in the grant. `chrome://`, extension pages, the Chrome Web Store, `file:`, `data:`, `javascript:` and every `about:` page (`about:blank` included, since a blank tab has no granted origin) are always refused. |
| Secrets read from the page | The snapshot redacts (`value=[redacted]`) any sensitive field, whatever role the page gives it: `type=password`; `autocomplete` `cc-*`, `one-time-code`, `current-password` or `new-password` (so a password revealed by a "show password" button stays redacted); a name or id containing a password, PIN, OTP, MFA or verification-code word (`userPin`, `otp_code`); payment card, CVC, expiry and IBAN hints. Hidden and `aria-hidden` nodes, hidden inputs, and frames from an origin outside the grant are left out. |
-| Agent types into a secret field | `type` into any field the rule above calls sensitive is refused (`sensitive_field`), and `Backspace` never edits one. |
+| Agent types into a secret field | `type` into any field the rule above calls sensitive is refused (`sensitive_field`), and no key at all (`press`, not only `Backspace`) is sent while focus is on one. The focused element is found through open shadow roots. Keys are refused (`origin_not_granted`) while focus is on a frame element, or inside a frame that is not same-origin and granted. |
+| A step fails after input reached the page | A click, `type` or `press` that fails after its first input event (mouse, key, inserted text, or an injected script that may have run) is reported as `outcome_unknown`, never as a specific error, so the agent re-snapshots instead of retrying blindly. A refusal before any input keeps its specific code. Both engines follow this rule. |
| Destructive or irreversible actions | `type` with `submit`, `press Enter` (which submits forms and sends chat messages), and clicks on elements named like submit, publish, delete, pay or similar wait for a per-action "Allow once" in the side panel. The card names the agent, the site and the target element, and for `type` with `submit` shows the start of the text to be sent (clipped, held only while the card is open, never stored or logged). With no side panel open the action is denied, never auto-approved. After approval the element (or the focused element, for Enter) is described again; if its role, name or sensitivity changed, the command fails with `stale_ref`. |
| Stale refs act on the wrong element | Refs belong to one snapshot generation; navigation or a large DOM change bumps it and old refs fail with `stale_ref`. Clicks check that the element is visible, enabled and actually under the click point. |
-| Agent fights the person for the tab | Best effort: real clicks and key presses by the person in an owned tab cancel the agent's running command there and hold new ones for 5 s (`busy`). The watcher listens in the capture phase on the page's window; a page can still stop the event before it arrives, so this is a courtesy, not a security boundary. One command at a time per tab. |
+| Agent fights the person for the tab | Best effort: real clicks and key presses by the person in an owned tab cancel the agent's running command there, hold new ones for 5 s (`busy`) and pause control (a persisted Pause; the panel says why and the person presses Resume). Input the debugger itself dispatches is not taken for the person's (it is ignored while a debugger `Input` command runs and for 600 ms after; the page and focus reads between those commands are not covered, so the person's input there still takes the tab back). The watcher listens in the capture phase on the page's window; a page can still stop the event before it arrives, so this is a courtesy, not a security boundary. One command at a time per tab. |
| Person loses control | Pause, Stop and Revoke work locally even if the Gateway does not cooperate: Pause/Stop refuse commands locally (`device_paused`), cancel the running ones and deny pending approvals, including commands waiting for text; Stop also releases owned tabs. Revoking a grant or the device, or a refresh that no longer lists a grant, cancels in-flight commands under it, including ones waiting for approval. The person's last Pause/Stop/Resume is kept per profile and device in `storage.local`, so it survives a Chrome restart and a profile switch, and is re-sent to the Gateway after every attach. Fail-safe: `active` is re-sent only when the person's last choice was Resume; with no stored choice nothing is sent and the Gateway keeps whatever state it holds; if the stored choice cannot be read or is damaged, the panel shows Paused, commands are refused locally and nothing is sent, so a Pause is never lifted by accident and the person can press Resume. |
| Replayed side effects | A command id runs at most once while the service worker lives (seen ids are kept in memory). After a worker restart, not running a command twice relies on the Gateway, which does not resend a dispatched command. Results are not replayed across reconnects; a lost result becomes `outcome_unknown` on the Gateway and the agent must re-snapshot. A command runs for at most 120 s locally, whatever deadline it carries. |
-| Page data persisted | Snapshots, typed text and page content are never stored. `storage.local` keeps only the device id and name, the ids of grants created here and the Pause/Stop/Resume choice, per profile; `storage.session` keeps the tab handle map. Storage stays locked to trusted extension contexts. |
+| Page data persisted | Snapshots, screenshots, typed text and page content are never stored. `storage.local` keeps only the device id and name, the ids of grants created here and the Pause/Stop/Resume choice, per profile; `storage.session` keeps the tab handle map and, in the Advanced build, the debugger opt-in. The drawer's action timeline lives in memory and holds only agent, action, domain and outcome code. Storage stays locked to trusted extension contexts. |
| Master token or key leak | Only the personal API key is used, only in the `connect` frame to the profile's own Gateway origin. It is never logged or put in a URL. |
| A web page talks to the worker | Content scripts in web pages may send only the Ask Dewee selection and the owned-tab input notice; `browser.*` requests from a web page (or any non-extension sender) are refused. |
-| Over-broad Chrome permission | No new install-time permission. Host permission is requested per exact origin at grant time, never ``, and only an exact-host permission satisfies the per-action check. |
+| Over-broad Chrome permission | The Standard build adds no install-time permission. Host permission is requested per exact origin at grant time, never ``, and only an exact-host permission satisfies the per-action check. The Advanced build adds only `debugger` (see below). |
+
+## Advanced mode (debugger)
+
+A separate build profile, Advanced (`pnpm build:advanced`, `pnpm zip:advanced`), adds the `debugger` permission so agents can read the accessibility tree, dispatch real input and take screenshots. The Standard build has no `debugger` permission and no debugger code (the adapter is dropped at build time; a unit test pins the manifest permissions and an e2e test checks the built manifest). Code: `src/browser/adapters/cdp/`, `src/browser/engine-router.ts`.
+
+| Threat | Control |
+|---|---|
+| The debugger used without the person knowing | Off by default. The person turns it on in the drawer ("Advanced mode") for this browser session only (`storage.session`; a Chrome restart turns it off). Chrome shows its own "is debugging this browser" bar while attached; cancelling that bar turns the opt-in off. The Gateway is told `advanced` only while the opt-in is on, so it refuses screenshot commands otherwise. |
+| Attaching wider than needed | Only `chrome.debugger.attach({ tabId })` on a tab the agent owns, never a browser-level target, another tab, an extension page or a remote debugging port. It attaches when a debugger command starts and detaches when that command ends. It detaches at once on Stop, Pause, grant revoke or expiry (a timer, not the next command), disconnect, profile switch, opt-in off and tab close; a detach asked for while an attach is still in progress detaches as soon as that attach completes, and the command fails with the policy's reason (`device_paused`, `grant_revoked`, `grant_expired`, ...) or `cancelled`, never by going the Standard way. An attach Chrome does not answer within 10 s fails, and is detached if Chrome completes it later. On a worker restart any tab still attached from before is detached. A cancelled command (Stop, deadline) stops between debugger steps and stops waiting for a pending one. |
+| Arbitrary CDP from the wire | Commands never carry a CDP method or params. The adapter calls one `send` wrapper that refuses any method outside a fixed list (unit-tested): `Accessibility.getFullAXTree`/`getPartialAXTree`, `DOM.enable`/`getDocument`/`describeNode`/`getContentQuads`/`getNodeForLocation`/`focus`/`scrollIntoViewIfNeeded`, `Page.getFrameTree`/`getLayoutMetrics`/`captureScreenshot`, `Input.dispatchMouseEvent`/`dispatchKeyEvent`/`insertText`, `Overlay.enable`/`highlightNode`/`hideHighlight`. No `Runtime.evaluate`, no `Network`, `Storage`, cookies, `Fetch` or `Target`. |
+| Policy bypass through the debugger | The same executor runs before the adapter: grant, agent, capability, exact origin, owned tab, host permission, approvals for risky actions. In addition the adapter asks the session before attaching and before every command (opt-in on, control active, owned tab, live grant, tab URL not restricted and in the grant; Chrome's host permission is read at attach and once per command for the tab's origin, and read again after any refusal or origin change), checks the main frame's origin again after reading and before every input, and checks each element's frame origin. A navigation during a command fails it (`origin_not_granted` or `stale_ref`). |
+| Secrets read through the accessibility tree | The AX snapshot uses the same `[eN] role "name"` format, generations and redaction as the page script: the sensitive-field rule is shared code (`src/browser/sensitive.ts`) applied to each node's DOM attributes, so a sensitive field is `value=[redacted]` whatever role the page gives it. An accessibility node with no matching DOM record is treated as sensitive. `type` into a sensitive field is refused, and no key is sent while focus is on one. Focus is read from the accessibility tree of the main frame and of each same-process child frame; Chrome's frame tree for the tab never lists out-of-process (cross-site) frames, and focus inside one shows in no tree read here. So when no tree shows focus (as in a background tab, where agent tabs open: Chrome then does not mark the page itself focused, though a focused element inside it is still marked), the key goes to the main document only if the DOM, read through closed shadow roots too, shows that every frame owner (`iframe`, `frame`, `object`, `embed`, any element holding a frame) holds a document Chrome returned to this session from the granted main page's origin. Otherwise focus may be inside a frame outside the grant and the key is refused (`origin_not_granted`, with that reason); this means pages with a cross-site frame anywhere (ads, chat or payment widgets) take no keys while nothing in the page shows focus. The key is also refused if a listed frame tree could not be read or there are too many frames, if focus is on a frame element, or if it is in a frame outside the grant. After focusing for `type`, the target must still hold focus and be non-sensitive, before the text and again before `Enter`. |
+| Secrets in screenshots | Viewport only (`captureBeyondViewport: false`), JPEG, at most 1.5 MiB after a quality and size step-down; nothing larger is sent. Sensitive fields and every frame owner (including `object` and `embed`) that does not hold a document same-origin with the granted main page and readable by the debugger (every cross-origin frame, granted or not, and every out-of-process frame) are painted black before encoding. Their boxes are measured before and after the capture; if the viewport or any box moved, it retries once and then refuses. A box lookup that fails (other than an element with no box), or more than 64 areas to black out, refuses the screenshot rather than send a part unredacted. The image goes only into the command result; it is never stored, logged or kept in the timeline. Screenshots never fall back to the Standard path. |
+| Frames | Same-process frames whose origin is in the grant are read and driven. Cross-site (out-of-process) frames and frames from origins outside the grant are left out of the snapshot, refused for actions and keys, and blacked out in screenshots. |
+| Debugger busy (DevTools or another extension) | Attaching fails with `debugger_unavailable`. If the person allowed it (the "read and act the standard way" box, on by default) snapshot and `press` go the Standard way, but only when the command is not cancelled and the policy check, run again at that point, still allows it; refs then belong to that engine, and a ref from the other engine is `stale_ref`. Screenshots never fall back. |
+
+Residual risks of Advanced mode: debugger input is trusted input, so sites that accept only trusted events will accept the agent's clicks; the accessibility tree can expose text a page script would have treated as hidden when the page labels it badly; Chrome's debugging bar is the only browser-level indication beyond the extension's own tab badge. The focus check and the key it allows are separate debugger commands, and CDP cannot make them atomic: a page script (a timer or a focus handler) can move focus in between, so a key may land on another element of the same granted page, including a secret field, or into a frame the page moves focus into in that moment (Chrome delivers a key to whatever holds focus when it arrives). Focus already inside a cross-site frame, including one in a closed shadow root or an `