Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default defineConfig({
sidebar: [
{ label: "Install", slug: "install" },
{ label: "Rules reference", slug: "rules" },
{ label: "Debug trace", slug: "debug-trace" },
{ label: "Use with browser-use", slug: "browser-use" },
{ label: "Use with Browserbase (Python)", slug: "browserbase-python" },
{ label: "Use with OpenClaw", slug: "openclaw" },
Expand Down
10 changes: 10 additions & 0 deletions docs/src/content/docs/browser-use.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,16 @@ If none of those markers appear, the extension is not attached:
upload step — there's no way to add the extension to an already-running
session.

## Investigating a false positive

When a shield rule hides, masks, or rewrites something it shouldn't have, turn
on the debug-trace recorder and pull the per-tab mutation record via
`window.__abs_dumpTrace()` through the browser-use session's `evaluate` (or any
raw-CDP `Runtime.evaluate`). Each entry tells you which rule fired, what it
matched, and the `outerHTML` before and after the mutation — enough to reproduce
the diagnosis offline. See [Debug trace](/agent-browser-shield/debug-trace/) for
the recipes and event schema.

## Why not browser-use's built-in `Browser()`?

`Browser()` launches a fresh Chromium under browser-use's control and does not
Expand Down
10 changes: 10 additions & 0 deletions docs/src/content/docs/browserbase-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ Any Python framework that accepts a CDP URL works the same way — pass
`session.connect_url` to browser-use, LangChain's browser tools, or your own
agent runner.

## Investigating a false positive

When a shield rule hides, masks, or rewrites something it shouldn't have, turn
on the debug-trace recorder and pull the per-tab mutation record via
`window.__abs_dumpTrace()` over CDP. Each entry tells you which rule fired, what
it matched, and the `outerHTML` before and after the mutation — enough to
reproduce the diagnosis offline. See
[Debug trace](/agent-browser-shield/debug-trace/) for the Playwright + raw-CDP
recipes and event schema.

## Brief the agent on what the shield changes

The extension rewrites the DOM — masked text becomes `[PII masked]`, dark
Expand Down
129 changes: 129 additions & 0 deletions docs/src/content/docs/debug-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
title: Debug trace
description: Investigate false positives — record every rule-driven mutation the shield makes and scrape the trace from the popup or over CDP via window.__abs_dumpTrace.
---

Turn on the debug-trace recorder when you need to investigate a **false
positive** — a rule hid, masked, or rewrote content that should have been left
alone, and you want to know which rule fired, what it matched, and what the page
looked like before and after. The recorder captures every rule-driven mutation
(selector, mutation kind, before/after `outerHTML`, segment id) and persists it
to IndexedDB at the extension origin so you can pull the record after the fact
and reproduce the diagnosis offline.

Typical scenarios:

- **An agent failed a task and you suspect the shield got in the way.** Compare
the trace to what the agent's accessibility-tree dump showed —
rule-application entries tell you exactly which DOM nodes were rewritten
between the page load and the agent's read.
- **A user reported "the page looks broken."** The trace's `outerHTML`
before/after pair shows the exact mutation; you can replay it on a clean page
locally to confirm the false positive and craft a regression test.
- **You're tuning a new rule and want a footprint report.** Run the page through
the recorder, dump the trace, and review every selector match before deciding
whether the rule's scope is right.

The recorder is **off by default**. Turn it on for the runs you want to inspect;
leave it off everywhere else (the recorder has both a runtime cost and exposes a
`window.__abs_*` global, see [Caveats](#caveats)).

## Turning the recorder on

Two paths, depending on how the extension was installed:

- **Build-time default** — pass `debugTrace: true` in your overrides file when
running `bun run build --defaults <file>`. See the
[`debugTrace`](/agent-browser-shield/install/#build-time-defaults) bullet on
Install. Right for CDP-driven harnesses (Browserbase, Hermes, browser-use,
OpenClaw) that ship the extension with the recorder always on so no human has
to flip a toggle per session.
- **Popup toggle** — open the extension popup and flip **Debug trace**. Right
for Chrome Web Store installs where the user enables tracing on demand. The
toggle persists in `chrome.storage` for the profile. Pages already open when
the toggle flips need a reload before the `window.__abs_dumpTrace()` bridge
appears (the dynamic content-script registration only takes effect on
subsequent navigations).

Both paths flip the same underlying `debugTraceStorage` value — the bridge
behaves identically either way.

## Retrieving the trace

### From the popup

Open the extension popup on the tab you want to inspect and click **Export**.
The popup downloads a JSONL file with one stored event per line, ordered
chronologically across all frames for that tab.

### From CDP

When the recorder is on, the extension exposes `window.__abs_dumpTrace()` in the
top frame's MAIN world. The async function returns the same stored-event shape
as the JSONL export, scoped to the calling tab. Any CDP client can call it via
`Runtime.evaluate`.

#### Playwright

```python
trace = page.evaluate("async () => await window.__abs_dumpTrace()")
```

#### Raw CDP

```python
result = client.send("Runtime.evaluate", {
"expression": "(async () => await window.__abs_dumpTrace())()",
"awaitPromise": True,
"returnByValue": True,
})
entries = result["result"]["value"]
```

#### Feature-detection

```python
present = page.evaluate("typeof window.__abs_dumpTrace === 'function'")
```

The function is absent on tabs where the recorder is off and on builds where the
bridge was never registered (no `debugTrace: true` default and no popup toggle
flip).

## Event shape

Each entry returned by the dump (and each JSONL line in the export) matches the
schema in
[`extension/data/debug-trace.schema.json`](https://github.com/pixiebrix/agent-browser-shield/blob/main/extension/data/debug-trace.schema.json).
The three top-level entry types are:

- **`segment`** — bookkeeping marker emitted at initial load, route changes,
modal opens, and large mutation bursts. Subsequent rule-application entries
carry the active segment id so consumers can group events by user-visible
phase.
- **`rule-application`** — a single rule-driven mutation. Carries the rule id,
mutation kind (`hide` / `mask` / `strip` / `sanitize` / `flag` / `embed`), the
matched selector, and `outerHTML` before/after the mutation. CSS-only matches
(the rule installed a stylesheet but didn't write to the element) set
`cssOnly: true` and have identical before/after HTML.
- **`navigation`** — emitted by the background service worker on every top-level
loading transition. Lets a single trace span multiple page loads in the same
tab.

The store caps each tab at 2000 events; older events are dropped first.

## Caveats

- **Top frame only.** `window.__abs_dumpTrace()` is exposed only on the top
frame. The response includes events from every frame on the tab (each carries
its `frameId`), so a CDP caller asking from the top frame still sees the full
picture.
- **The page can see it too.** When the bridge is installed, any page-world
script in the tab can call `window.__abs_dumpTrace()`. Don't enable the
recorder on a CWS profile you use for browsing untrusted sites.
- **Reload required for already-open tabs.** Toggling the popup switch on
doesn't retroactively expose the bridge on tabs you already had open. The next
navigation in that tab picks it up.
- **Recorder cost.** When on, the recorder serializes `outerHTML` around every
rule mutation. Cheap on most pages, but visibly slower on chatty SPAs that
re-render constantly. Leave the recorder off in production.
10 changes: 10 additions & 0 deletions docs/src/content/docs/hermes-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ shield is loaded in the Chrome instance running against
default profile. `/browser status` from the Hermes CLI confirms the CDP
endpoint.

## Investigating a false positive

When a shield rule hides, masks, or rewrites something it shouldn't have, turn
on the debug-trace recorder and pull the per-tab mutation record via
`window.__abs_dumpTrace()` over the same CDP connection Hermes is using. Each
entry tells you which rule fired, what it matched, and the `outerHTML` before
and after the mutation — enough to reproduce the diagnosis offline. See
[Debug trace](/agent-browser-shield/debug-trace/) for the recipes and event
schema.

## Why not Hermes' default browser providers?

Hermes' first-party browser providers — Browserbase via its own session manager,
Expand Down
15 changes: 8 additions & 7 deletions docs/src/content/docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,14 @@ set of reserved keys is also accepted for non-rule build-time toggles:

- `debugTrace` (boolean, default **off**) — start with the dev-mode debug-trace
recorder enabled. The recorder captures every rule-driven mutation (selector,
before/after `outerHTML`, segment id) and persists it to IndexedDB at the
extension origin so the popup's **Export** button can dump a JSONL trace.
Enable in builds you ship to automation harnesses (CDP-driven browsers,
Browserbase sessions) where you want post-hoc visibility into what the
extension touched without a human flipping the popup toggle. The export shape
is documented in
[`extension/data/debug-trace.schema.json`](https://github.com/pixiebrix/agent-browser-shield/blob/main/extension/data/debug-trace.schema.json).
before/after `outerHTML`, segment id) to IndexedDB and exposes
`window.__abs_dumpTrace()` for CDP-driven harnesses to scrape — for
investigating false positives (a rule hid, masked, or rewrote something it
shouldn't have) after the fact. Enable in builds you ship to automation
harnesses (Browserbase, Hermes, browser-use, OpenClaw) so the trace is on
every session without a human flipping the popup toggle. See
[Debug trace](/agent-browser-shield/debug-trace/) for the retrieval recipes
and event schema.

The file may be partial; rules not listed keep the committed default. Unknown
keys (neither a registered rule id nor a reserved key) and non-boolean values
Expand Down
10 changes: 10 additions & 0 deletions docs/src/content/docs/openclaw.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ If none of those markers appear, the extension is not attached:
- *Browserbase:* confirm the session was created with the `extensionId` from
step 2 — there's no way to add the extension to an already-running session.

## Investigating a false positive

When a shield rule hides, masks, or rewrites something it shouldn't have, turn
on the debug-trace recorder and pull the per-tab mutation record via
`window.__abs_dumpTrace()` over the same CDP connection OpenClaw is using. Each
entry tells you which rule fired, what it matched, and the `outerHTML` before
and after the mutation — enough to reproduce the diagnosis offline. See
[Debug trace](/agent-browser-shield/debug-trace/) for the recipes and event
schema.

## Why not OpenClaw's managed profile?

OpenClaw's `openclaw` profile launches a dedicated Chromium under its own
Expand Down
8 changes: 8 additions & 0 deletions extension/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ async function build(): Promise<void> {
// on. See `lib/shadow-root-probe-source.ts` and
// `lib/shadow-root-probe-registration.ts`.
join(SRC, "shadow-root-probe.ts"),
// Standalone main-world bundle registered by the background worker
// whenever the debug-trace toggle is on. Exposes
// `window.__abs_dumpTrace()` for CDP-driven harnesses to scrape
// the IDB-backed trace mid-flow via `Runtime.evaluate` without
// needing the popup's Export button. See
// `lib/dump-trace-bridge-source.ts` and
// `lib/dump-trace-bridge-registration.ts`.
join(SRC, "dump-trace-bridge.ts"),
],
outdir: DIST,
target: "browser",
Expand Down
1 change: 1 addition & 0 deletions extension/knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"src/webdriver-probe.ts",
"src/checkout-checkbox-defense.ts",
"src/shadow-root-probe.ts",
"src/dump-trace-bridge.ts",
"eslint-rules/index.js"
],
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.ts", "eslint-rules/**/*.js"],
Expand Down
33 changes: 33 additions & 0 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import { debugTraceStorage } from "./lib/debug-trace";
import {
appendEvent as appendDebugTraceEvent,
clearTab as clearDebugTraceTab,
getEventsForTab as getDebugTraceForTab,
} from "./lib/debug-trace-store";
import type {
DebugTraceEntry,
DebugTraceEventMessage,
DetectionKind,
DetectionPayload,
GetTabDebugTraceResponse,
GetTabDetectionsRequest,
GetTabDetectionsResponse,
GetTabRuleCountsRequest,
GetTabRuleCountsResponse,
RuleCountEntry,
RuleDetectionMessage,
} from "./lib/detection-messages";
import { startDumpTraceBridgeRegistration } from "./lib/dump-trace-bridge-registration";
import { subscribeEnforcementEnabled } from "./lib/enforcement";
import { startClassifyPortListener } from "./lib/llm-background";
import { log } from "./lib/log";
Expand Down Expand Up @@ -434,6 +437,28 @@ chrome.runtime.onMessage.addListener(
return undefined;
}

if (message.type === "get-tab-debug-trace") {
const tabId = sender.tab?.id;
if (typeof tabId !== "number") {
return undefined;
}
// Async response — the page-world bridge is awaiting via
// postMessage round-trip, so a missing reply hangs the caller
// (up to its 10s timeout). `return true` keeps the message
// channel open until `sendResponse` fires.
void getDebugTraceForTab(tabId)
.then((entries) => {
const response: GetTabDebugTraceResponse = { entries };
sendResponse(response);
})
.catch((error: unknown) => {
log.error("get-tab-debug-trace IDB read failed", { error });
const response: GetTabDebugTraceResponse = { entries: [] };
sendResponse(response);
});
return true;
}

if (message.type === "debug-trace-event") {
const tabId = sender.tab?.id;
const frameId = sender.frameId;
Expand Down Expand Up @@ -522,3 +547,11 @@ startCheckoutCheckboxDefenseRegistration();
// `rules/closed-shadow-root-annotate.ts` rely on. See
// `lib/shadow-root-probe-registration.ts`.
startShadowRootProbeRegistration();

// Register/unregister the page-world `__abs_dumpTrace` bridge whenever
// the debug-trace toggle flips. Exposes `window.__abs_dumpTrace()` for
// CDP-driven harnesses to scrape the IDB-backed trace mid-flow without
// the popup's Export button. See `lib/dump-trace-bridge-registration.ts`
// for the lifecycle and `lib/dump-trace-bridge-source.ts` for the
// page-world implementation.
startDumpTraceBridgeRegistration();
8 changes: 8 additions & 0 deletions extension/src/content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2026 PixieBrix, Inc.
// Licensed under PolyForm Shield 1.0.0 — see LICENSE.

import { startDumpTraceContentBridge } from "./lib/dump-trace-content-bridge";
import { isTopFrame } from "./lib/frame";
import { log } from "./lib/log";
import { startOptionsBadge } from "./lib/options-badge";
Expand Down Expand Up @@ -39,4 +40,11 @@ startSegmentTracker();

if (isTopFrame()) {
startOptionsBadge();
// Isolated-world half of the `window.__abs_dumpTrace` bridge. The
// page-world half is registered separately by background when the
// debug-trace toggle is on; this listener is idle until a page
// actually calls the function (which only exists when the page-world
// half is registered). The returned unsubscribe handle is only used
// by tests — production runs for the document lifetime.
void startDumpTraceContentBridge();
}
18 changes: 18 additions & 0 deletions extension/src/dump-trace-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) 2026 PixieBrix, Inc.
// Licensed under PolyForm Shield 1.0.0 — see LICENSE.

// Build entrypoint for the page-world dump-trace bridge. Registered
// dynamically by the background worker via
// `chrome.scripting.registerContentScripts` with `world: "MAIN"` and
// `runAt: "document_start"` whenever the debug-trace toggle is on.
// Exposes `window.__abs_dumpTrace()` for CDP clients to scrape the
// extension's IndexedDB-backed trace mid-flow.
//
// Kept tiny on purpose — anything imported here ships into every
// page-world heap when the bridge is registered. Pull only from
// `lib/dump-trace-bridge-source.ts`, which is also kept
// dependency-free.

import { installDumpTraceBridge } from "./lib/dump-trace-bridge-source";

installDumpTraceBridge.call(globalThis as unknown as Window);
Loading
Loading