fix(extension): default to high-risk mode by disabling page bridge

This commit is contained in:
leeguooooo
2026-03-04 11:46:44 +09:00
parent ae22bda46c
commit 2d0d18f0d9
6 changed files with 2981 additions and 148 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser-stealth"
version = "0.16.1-fork.0"
version = "0.16.1-fork.3"
dependencies = [
"aes-gcm",
"async-trait",
+342
View File
@@ -2,6 +2,316 @@
const REQUEST_TYPE = 'AB_TAB_GROUP_REQUEST';
const RESPONSE_TYPE = 'AB_TAB_GROUP_RESPONSE';
const CONTENT_EVENT_TYPE = 'AB_CONTENT_EVENT';
const CONTENT_EXECUTE_ACTION = 'AB_CONTENT_EXECUTE_ACTION';
const CONTENT_GET_DOM_STATE = 'AB_CONTENT_GET_DOM_STATE';
const CONTENT_PING = 'AB_CONTENT_PING';
const PAGE_BRIDGE_EVENT = 'AB_PAGE_BRIDGE_EVENT';
const STORAGE_OPTIONS_KEY = 'abExtensionOptionsV1';
const mutationState = {
total: 0,
recent: [],
observerReady: false,
};
function pushMutationSummary(entry) {
mutationState.total += 1;
mutationState.recent.push({
...entry,
timestamp: Date.now(),
});
if (mutationState.recent.length > 40) {
mutationState.recent.splice(0, mutationState.recent.length - 40);
}
}
function serializeValue(value, depth = 0) {
if (value === null || typeof value === 'undefined') return value;
if (typeof value === 'string') return value.slice(0, 300);
if (typeof value === 'number' || typeof value === 'boolean') return value;
if (value instanceof Error) return `${value.name}: ${value.message}`;
if (depth > 2) return '[depth-limit]';
if (Array.isArray(value)) {
return value.slice(0, 10).map((item) => serializeValue(item, depth + 1));
}
if (typeof value === 'object') {
const out = {};
for (const [key, entry] of Object.entries(value).slice(0, 15)) {
out[key] = serializeValue(entry, depth + 1);
}
return out;
}
return String(value).slice(0, 300);
}
function sendRuntimeEvent(kind, payload) {
try {
chrome.runtime.sendMessage({
type: CONTENT_EVENT_TYPE,
kind,
payload: serializeValue(payload),
url: window.location.href,
title: document.title,
timestamp: Date.now(),
});
} catch {
// Ignore runtime channel errors.
}
}
function getPageBridgeEnabled() {
return new Promise((resolve) => {
try {
chrome.storage.local.get([STORAGE_OPTIONS_KEY], (result) => {
if (chrome.runtime.lastError) {
resolve(false);
return;
}
const rawOptions = result?.[STORAGE_OPTIONS_KEY];
resolve(Boolean(rawOptions && typeof rawOptions === 'object' && rawOptions.pageBridgeEnabled === true));
});
} catch {
resolve(false);
}
});
}
async function installPageBridge() {
// Receives events emitted by the injected page-world hook script.
const bridgeListener = (event) => {
if (event.source !== window) return;
const data = event.data;
if (!data || data.type !== PAGE_BRIDGE_EVENT) return;
sendRuntimeEvent(data.kind || 'page-event', data.payload || {});
};
window.addEventListener('message', bridgeListener);
const parent = document.documentElement || document.head || document.body;
if (!parent) return;
if (!(await getPageBridgeEnabled())) {
sendRuntimeEvent('lifecycle', {
event: 'bridge-disabled-default',
});
return;
}
// Use external extension script instead of inline text to reduce CSP conflicts.
const script = document.createElement('script');
script.src = chrome.runtime.getURL('page-bridge.js');
script.async = false;
script.dataset.abBridgeEvent = PAGE_BRIDGE_EVENT;
script.onload = () => script.remove();
script.onerror = () => {
sendRuntimeEvent('lifecycle', {
event: 'bridge-load-failed',
host: window.location.hostname,
});
script.remove();
};
parent.appendChild(script);
}
function ensureMutationObserver() {
if (mutationState.observerReady) return;
if (!document.documentElement) return;
const observer = new MutationObserver((records) => {
const summary = {
records: records.length,
addedNodes: 0,
removedNodes: 0,
};
for (const record of records.slice(0, 40)) {
summary.addedNodes += record.addedNodes?.length || 0;
summary.removedNodes += record.removedNodes?.length || 0;
}
pushMutationSummary(summary);
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style', 'hidden', 'disabled', 'aria-hidden'],
});
mutationState.observerReady = true;
}
function toSimpleNode(element) {
if (!element || typeof element !== 'object') return null;
const node = {
tag: element.tagName?.toLowerCase() || 'unknown',
id: element.id || undefined,
className: typeof element.className === 'string' ? element.className.slice(0, 120) : '',
role: element.getAttribute?.('role') || undefined,
name:
element.getAttribute?.('aria-label') ||
element.getAttribute?.('name') ||
element.getAttribute?.('placeholder') ||
'',
text: (element.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 160),
disabled: element.disabled === true,
hidden: element.hidden === true,
};
return node;
}
function collectInteractiveElements(root, limit = 80) {
const selector = [
'a[href]',
'button',
'input',
'select',
'textarea',
'summary',
'[role="button"]',
'[role="link"]',
'[tabindex]'
].join(',');
const out = [];
const nodes = root.querySelectorAll(selector);
for (const element of nodes) {
if (out.length >= limit) break;
out.push(toSimpleNode(element));
}
return out.filter(Boolean);
}
function collectDomState(options = {}) {
const selector = typeof options.selector === 'string' ? options.selector.trim() : '';
const root = selector ? document.querySelector(selector) : document.body || document.documentElement;
if (!root) {
return {
ok: false,
error: selector ? `selector-not-found: ${selector}` : 'root-not-found',
};
}
const textPreview = (root.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 1000);
const interactiveOnly = options.interactiveOnly === true;
const interactiveElements = collectInteractiveElements(root, options.maxNodes || 80);
const dom = {
href: window.location.href,
title: document.title,
readyState: document.readyState,
selector: selector || null,
rootTag: root.tagName?.toLowerCase() || 'unknown',
textPreview,
interactiveCount: interactiveElements.length,
interactiveElements,
mutation: {
total: mutationState.total,
recent: mutationState.recent.slice(-10),
},
capturedAt: Date.now(),
};
if (interactiveOnly) {
dom.textPreview = '';
}
return {
ok: true,
state: dom,
};
}
function queryElement(selector) {
if (typeof selector !== 'string' || selector.trim().length === 0) {
throw new Error('selector is required');
}
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
return element;
}
function focusElement(element) {
if (typeof element.focus === 'function') {
element.focus({ preventScroll: false });
}
}
function dispatchInputEvents(element) {
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
async function executeAction(command, args = {}) {
switch (command) {
case 'click': {
const element = queryElement(args.selector);
focusElement(element);
element.click();
return { ok: true, action: command, selector: args.selector };
}
case 'fill': {
const element = queryElement(args.selector);
if (!('value' in element)) {
throw new Error(`Element is not fillable: ${args.selector}`);
}
focusElement(element);
element.value = typeof args.value === 'string' ? args.value : String(args.value || '');
dispatchInputEvents(element);
return { ok: true, action: command, selector: args.selector, valueLength: element.value.length };
}
case 'press': {
const key = typeof args.key === 'string' && args.key.trim().length > 0 ? args.key.trim() : 'Enter';
let target;
if (typeof args.selector === 'string' && args.selector.trim().length > 0) {
target = queryElement(args.selector);
focusElement(target);
} else {
target = document.activeElement || document.body;
}
const down = new KeyboardEvent('keydown', { key, bubbles: true });
const up = new KeyboardEvent('keyup', { key, bubbles: true });
target.dispatchEvent(down);
target.dispatchEvent(up);
return { ok: true, action: command, key };
}
case 'eval': {
if (typeof args.expression !== 'string' || args.expression.trim().length === 0) {
throw new Error('expression is required');
}
const fn = new Function(`return (${args.expression});`);
const result = fn();
return { ok: true, action: command, result: serializeValue(result) };
}
case 'snapshot': {
return {
ok: true,
action: command,
...collectDomState({
selector: args.selector,
interactiveOnly: args.interactiveOnly === true,
maxNodes: args.maxNodes,
}),
};
}
default:
throw new Error(`Unknown content action: ${command}`);
}
}
ensureMutationObserver();
installPageBridge();
window.addEventListener('message', (event) => {
if (event.source !== window) {
return;
@@ -80,4 +390,36 @@
);
}
});
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (!message || typeof message !== 'object') return;
if (message.type === CONTENT_PING) {
sendResponse({
ok: true,
href: window.location.href,
title: document.title,
readyState: document.readyState,
});
return;
}
if (message.type === CONTENT_GET_DOM_STATE) {
sendResponse(collectDomState(message.options || {}));
return;
}
if (message.type === CONTENT_EXECUTE_ACTION) {
executeAction(message.command, message.args || {})
.then((result) => sendResponse(result))
.catch((error) => {
sendResponse({
ok: false,
action: message.command,
error: error instanceof Error ? error.message : String(error),
});
});
return true;
}
});
})();
+9
View File
@@ -3,6 +3,9 @@
"name": "agent-browser-stealth",
"version": "0.2.0",
"description": "Session-aware tab grouping and coordination for CDP-driven agent-browser workflows.",
"icons": {
"128": "icons/icon.svg"
},
"permissions": ["tabs", "tabGroups", "downloads", "storage", "sidePanel", "alarms"],
"host_permissions": ["<all_urls>"],
"background": {
@@ -21,5 +24,11 @@
"run_at": "document_start",
"match_about_blank": true
}
],
"web_accessible_resources": [
{
"resources": ["page-bridge.js"],
"matches": ["<all_urls>"]
}
]
}
+133
View File
@@ -0,0 +1,133 @@
(() => {
if (window.__AB_STEALTH_BRIDGE_INSTALLED__) return;
window.__AB_STEALTH_BRIDGE_INSTALLED__ = true;
const currentScript = document.currentScript;
const TYPE = currentScript?.dataset?.abBridgeEvent || 'AB_PAGE_BRIDGE_EVENT';
const post = (kind, payload) => {
try {
window.postMessage({ type: TYPE, kind, payload, timestamp: Date.now() }, '*');
} catch {
// Ignore post failures.
}
};
const serializeArg = (value, depth = 0) => {
if (value === null || typeof value === 'undefined') return value;
if (typeof value === 'string') return value.slice(0, 250);
if (typeof value === 'number' || typeof value === 'boolean') return value;
if (value instanceof Error) return `${value.name}: ${value.message}`;
if (depth > 2) return '[depth-limit]';
if (Array.isArray(value)) return value.slice(0, 10).map((item) => serializeArg(item, depth + 1));
if (typeof value === 'object') {
const out = {};
const entries = Object.entries(value).slice(0, 12);
for (const [k, v] of entries) {
out[k] = serializeArg(v, depth + 1);
}
return out;
}
return String(value).slice(0, 250);
};
const patchConsoleMethod = (name) => {
const original = console[name];
if (typeof original !== 'function') return;
console[name] = function patchedConsole(...args) {
post('console', {
level: name,
args: args.map((arg) => serializeArg(arg)),
});
return original.apply(this, args);
};
};
patchConsoleMethod('error');
patchConsoleMethod('warn');
window.addEventListener('error', (event) => {
post('console', {
level: 'error',
message: event.message,
source: event.filename,
line: event.lineno,
column: event.colno,
});
});
window.addEventListener('unhandledrejection', (event) => {
post('console', {
level: 'error',
message: 'Unhandled rejection',
reason: serializeArg(event.reason),
});
});
if (typeof window.fetch === 'function') {
const originalFetch = window.fetch.bind(window);
window.fetch = async (...args) => {
const startedAt = Date.now();
const requestInfo = args[0];
const requestInit = args[1] || {};
const method = requestInit.method || 'GET';
const url = typeof requestInfo === 'string' ? requestInfo : requestInfo?.url || '';
try {
const response = await originalFetch(...args);
post('network', {
transport: 'fetch',
method,
url,
status: response.status,
ok: response.ok,
durationMs: Date.now() - startedAt,
});
return response;
} catch (error) {
post('network', {
transport: 'fetch',
method,
url,
error: serializeArg(error),
durationMs: Date.now() - startedAt,
});
throw error;
}
};
}
if (typeof window.XMLHttpRequest === 'function') {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function patchedOpen(method, url, ...rest) {
this.__abRequestMeta = {
method: typeof method === 'string' ? method : 'GET',
url: typeof url === 'string' ? url : String(url || ''),
startedAt: Date.now(),
};
return originalOpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.send = function patchedSend(...args) {
this.addEventListener('loadend', () => {
const meta = this.__abRequestMeta || {};
post('network', {
transport: 'xhr',
method: meta.method || 'GET',
url: meta.url || '',
status: this.status,
ok: this.status >= 200 && this.status < 400,
durationMs: Date.now() - (meta.startedAt || Date.now()),
});
});
return originalSend.apply(this, args);
};
}
post('lifecycle', {
event: 'bridge-installed',
href: location.href,
});
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff