feat: complete agent-browser-stealth extension controls

This commit is contained in:
leeguooooo
2026-03-03 12:33:46 +09:00
parent 870895e922
commit 8e2e4abce6
7 changed files with 234 additions and 9 deletions
+2 -1
View File
@@ -74,11 +74,12 @@ Install once in Chrome: load unpacked extension from `extensions/tab-group-cdp/`
### Extension Capabilities (`agent-browser-stealth`)
- Session window isolation: tabs are kept in their session window when possible.
- Configurable isolation controls: side panel can toggle `strictWindowIsolation` and cross-window activation guard.
- Session-aware grouping: deterministic group color, default session expanded, non-default sessions collapsed.
- Download archive routing: downloads from managed tabs are routed to `agent-browser-stealth/<session>/...`.
- Domain allowlist fallback: when allowlist is configured for a session, extension can force-block out-of-policy tabs to `about:blank`.
- Risk hints (debug only): suspicious host/TLD hints are returned via handshake and printed only when `AGENT_BROWSER_DEBUG=1`.
- Side panel console: view session/tab/group mapping, focus a session, keep only one session, clean empty groups, and edit session allowlist.
- Side panel console: view session/tab/group mapping, focus a session, keep only one session, clean empty groups, edit session allowlist, and toggle auto-clean.
## Stealth Architecture
+1
View File
@@ -150,6 +150,7 @@ CDP mode uses a browser extension handshake to group tabs.
- Extension side panel (`agent-browser-stealth`) also provides:
- Session window isolation and deterministic group colors.
- `Keep Only This`, `Focus`, `Clean Empty Groups` quick actions.
- Toggle switches for strict isolation / activation guard / auto-clean.
- Session allowlist editing (domain fallback to `about:blank` when violated).
- Download routing to `agent-browser-stealth/<session>/...`.
- Use `--tab-group` / `AGENT_BROWSER_TAB_GROUP` for base title.
+1 -1
View File
@@ -321,7 +321,7 @@ For tab grouping in CDP mode, grouping is best-effort through the extension hand
extension available => grouped by session; extension missing/unavailable => silent no-op.
With the `agent-browser-stealth` extension installed, the side panel also exposes
session window isolation controls, empty-group cleanup, and per-session allowlist policy editing.
session window isolation controls, activation guard toggles, empty-group cleanup, and per-session allowlist policy editing.
## Common Configurations
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "agent-browser-stealth",
"version": "0.2.0",
"description": "Session-aware tab grouping and coordination for CDP-driven agent-browser workflows.",
"permissions": ["tabs", "tabGroups", "downloads", "storage", "sidePanel"],
"permissions": ["tabs", "tabGroups", "downloads", "storage", "sidePanel", "alarms"],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "service-worker.js"
+168 -5
View File
@@ -4,22 +4,32 @@ const PANEL_CLOSE_OTHER_TABS = 'AB_PANEL_CLOSE_OTHER_SESSION_TABS';
const PANEL_FOCUS_SESSION = 'AB_PANEL_FOCUS_SESSION';
const PANEL_CLEAN_EMPTY_GROUPS = 'AB_PANEL_CLEAN_EMPTY_GROUPS';
const PANEL_SET_POLICY = 'AB_PANEL_SET_POLICY';
const PANEL_SET_OPTIONS = 'AB_PANEL_SET_OPTIONS';
const DEFAULT_GROUP_TITLE = 'Agent Browser Stealth';
const DOWNLOAD_ARCHIVE_ROOT = 'agent-browser-stealth';
const STORAGE_POLICY_KEY = 'abSessionPoliciesV1';
const STORAGE_OPTIONS_KEY = 'abExtensionOptionsV1';
const CLEANUP_ALARM_NAME = 'ab-clean-empty-groups';
const GROUP_COLORS = ['blue', 'green', 'pink', 'orange', 'purple', 'cyan', 'red', 'yellow'];
const RISKY_TLDS = new Set(['zip', 'mov', 'click', 'top', 'gq', 'tk', 'country']);
const RISKY_HOST_KEYWORDS = ['secure-login', 'account-verify', 'wallet-verify', 'airdrop-claim'];
const DEFAULT_EXTENSION_OPTIONS = {
strictWindowIsolation: true,
suppressCrossWindowActivation: true,
autoCleanEmptyGroups: true,
};
const sessionGroupCache = new Map();
const sessionWindowMap = new Map();
const tabSessionMap = new Map();
const tabMetaById = new Map();
const downloadEvents = [];
const sessionPolicies = new Map();
let extensionOptions = { ...DEFAULT_EXTENSION_OPTIONS };
let policyLoadPromise = loadPolicies();
let bootstrapPromise = bootstrapState();
function normalizeSession(session) {
if (typeof session !== 'string') return 'default';
@@ -141,6 +151,46 @@ async function loadPolicies() {
}
}
function normalizeOptions(raw) {
if (!raw || typeof raw !== 'object') {
return { ...DEFAULT_EXTENSION_OPTIONS };
}
return {
strictWindowIsolation: raw.strictWindowIsolation !== false,
suppressCrossWindowActivation: raw.suppressCrossWindowActivation !== false,
autoCleanEmptyGroups: raw.autoCleanEmptyGroups !== false,
};
}
async function loadOptions() {
try {
const result = await chrome.storage.local.get([STORAGE_OPTIONS_KEY]);
extensionOptions = normalizeOptions(result?.[STORAGE_OPTIONS_KEY]);
} catch {
extensionOptions = { ...DEFAULT_EXTENSION_OPTIONS };
}
}
async function persistOptions() {
await chrome.storage.local.set({ [STORAGE_OPTIONS_KEY]: extensionOptions });
}
async function setExtensionOptions(nextOptions) {
extensionOptions = {
...extensionOptions,
...normalizeOptions(nextOptions),
};
await persistOptions();
await syncCleanupAlarm();
return extensionOptions;
}
async function bootstrapState() {
await loadPolicies();
await loadOptions();
await syncCleanupAlarm();
}
async function persistPolicies() {
const serialized = {};
for (const [session, domains] of sessionPolicies.entries()) {
@@ -200,6 +250,11 @@ function removeWindowCaches(windowId) {
}
async function ensureSessionWindow(tabId, currentWindowId, session) {
if (!extensionOptions.strictWindowIsolation) {
sessionWindowMap.set(session, currentWindowId);
return currentWindowId;
}
let targetWindowId = sessionWindowMap.get(session);
if (typeof targetWindowId === 'number') {
@@ -420,6 +475,75 @@ async function cleanEmptyGroups() {
return { removedGroups, removedWindows };
}
async function syncCleanupAlarm() {
try {
await chrome.alarms.clear(CLEANUP_ALARM_NAME);
if (extensionOptions.autoCleanEmptyGroups) {
await chrome.alarms.create(CLEANUP_ALARM_NAME, { periodInMinutes: 1 });
}
} catch {
// Ignore alarms API failures.
}
}
async function enforceSessionWindowAffinity(tabId) {
if (!extensionOptions.suppressCrossWindowActivation) return { moved: false };
const session = getManagedSessionForTab(tabId);
if (!session) return { moved: false };
if (!extensionOptions.strictWindowIsolation) return { moved: false };
let tab;
try {
tab = await chrome.tabs.get(tabId);
} catch {
return { moved: false };
}
const mappedWindowId = sessionWindowMap.get(session);
if (typeof mappedWindowId !== 'number' || mappedWindowId === tab.windowId) {
if (typeof tab.windowId === 'number') {
sessionWindowMap.set(session, tab.windowId);
}
return { moved: false };
}
try {
await chrome.tabs.move(tabId, { windowId: mappedWindowId, index: -1 });
await chrome.tabs.update(tabId, { active: false }).catch(() => {});
return { moved: true, toWindowId: mappedWindowId };
} catch {
return { moved: false };
}
}
async function updateRiskBadge(tabId) {
let text = '';
let title = 'agent-browser-stealth';
const session = getManagedSessionForTab(tabId);
if (session) {
let tab;
try {
tab = await chrome.tabs.get(tabId);
} catch {
tab = undefined;
}
if (tab) {
const hints = collectRiskHints(tab.url, getSessionPolicy(session));
if (hints.length > 0) {
text = '!';
title = `Risk hints (${hints.length}): ${hints.join(', ')}`;
}
}
}
await chrome.action.setBadgeText({ text }).catch(() => {});
await chrome.action.setBadgeBackgroundColor({ color: '#dc2626' }).catch(() => {});
await chrome.action.setTitle({ title }).catch(() => {});
}
async function buildPanelState() {
const allTabs = await chrome.tabs.query({});
for (const tab of allTabs) {
@@ -492,6 +616,7 @@ async function buildPanelState() {
return {
extensionId: chrome.runtime.id,
options: { ...extensionOptions },
totals: {
sessions: sessions.length,
tabs: sessions.reduce((sum, session) => sum + session.tabs.length, 0),
@@ -502,7 +627,7 @@ async function buildPanelState() {
}
async function handleTabGroupRequest(message, sender) {
await policyLoadPromise;
await bootstrapPromise;
const tabId = sender.tab?.id;
const windowId = sender.tab?.windowId;
@@ -555,6 +680,11 @@ async function handleTabGroupRequest(message, sender) {
chrome.runtime.onInstalled.addListener(() => {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
bootstrapPromise = bootstrapState();
});
chrome.runtime.onStartup.addListener(() => {
bootstrapPromise = bootstrapState();
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
@@ -580,7 +710,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
}
if (type === PANEL_GET_STATE) {
buildPanelState()
bootstrapPromise
.then(() => buildPanelState())
.then((state) => sendResponse({ ok: true, state }))
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -620,7 +751,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
}
if (type === PANEL_SET_POLICY) {
setSessionPolicy(message.session, message.allowedDomains)
bootstrapPromise
.then(() => setSessionPolicy(message.session, message.allowedDomains))
.then(() => sendResponse({ ok: true }))
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -628,12 +760,28 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
return true;
}
if (type === PANEL_SET_OPTIONS) {
bootstrapPromise
.then(() => setExtensionOptions(message.options))
.then((options) => sendResponse({ ok: true, options }))
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
sendResponse({ ok: false, error: errorMessage });
});
return true;
}
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
updateTabMeta(tab);
const session = getManagedSessionForTab(tabId);
if (!session) return;
if (!session) {
if (changeInfo.status === 'complete' && tab.active === true) {
updateRiskBadge(tabId).catch(() => {});
}
return;
}
if (typeof tab.windowId === 'number') {
sessionWindowMap.set(session, tab.windowId);
@@ -641,9 +789,17 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
applySessionDomainFallback(tabId, session).catch(() => {});
if (tab.active === true) {
updateRiskBadge(tabId).catch(() => {});
}
}
});
chrome.tabs.onActivated.addListener((activeInfo) => {
enforceSessionWindowAffinity(activeInfo.tabId).catch(() => {});
updateRiskBadge(activeInfo.tabId).catch(() => {});
});
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
const session = getManagedSessionForTab(tabId);
tabSessionMap.delete(tabId);
@@ -659,6 +815,7 @@ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
if (remaining.length === 0) {
sessionWindowMap.delete(session);
}
cleanEmptyGroups().catch(() => {});
});
chrome.tabs.onDetached.addListener((tabId) => {
@@ -675,6 +832,12 @@ chrome.tabs.onAttached.addListener((tabId, attachInfo) => {
chrome.windows.onRemoved.addListener((windowId) => {
removeWindowCaches(windowId);
cleanEmptyGroups().catch(() => {});
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm?.name !== CLEANUP_ALARM_NAME) return;
cleanEmptyGroups().catch(() => {});
});
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
+60
View File
@@ -17,6 +17,7 @@ function createTag(text) {
function renderSummary(state) {
summaryEl.innerHTML = '';
const options = state.options || {};
const title = document.createElement('div');
title.innerHTML = `<strong>Overview</strong> · extensionId: <code>${state.extensionId}</code>`;
@@ -24,9 +25,68 @@ function renderSummary(state) {
tags.className = 'tags';
tags.appendChild(createTag(`sessions: ${state.totals.sessions}`));
tags.appendChild(createTag(`tabs: ${state.totals.tabs}`));
tags.appendChild(
createTag(`isolation: ${options.strictWindowIsolation === false ? 'off' : 'on'}`)
);
tags.appendChild(
createTag(`activation-guard: ${options.suppressCrossWindowActivation === false ? 'off' : 'on'}`)
);
tags.appendChild(
createTag(`auto-clean: ${options.autoCleanEmptyGroups === false ? 'off' : 'on'}`)
);
const optionActions = document.createElement('div');
optionActions.className = 'row-actions';
const isolationBtn = document.createElement('button');
isolationBtn.type = 'button';
isolationBtn.textContent =
options.strictWindowIsolation === false ? 'Enable Isolation' : 'Disable Isolation';
isolationBtn.addEventListener('click', async () => {
await send({
type: 'AB_PANEL_SET_OPTIONS',
options: { ...options, strictWindowIsolation: options.strictWindowIsolation === false },
});
await refresh();
});
const guardBtn = document.createElement('button');
guardBtn.type = 'button';
guardBtn.textContent =
options.suppressCrossWindowActivation === false ? 'Enable Guard' : 'Disable Guard';
guardBtn.addEventListener('click', async () => {
await send({
type: 'AB_PANEL_SET_OPTIONS',
options: {
...options,
suppressCrossWindowActivation: options.suppressCrossWindowActivation === false,
},
});
await refresh();
});
const autoCleanBtn = document.createElement('button');
autoCleanBtn.type = 'button';
autoCleanBtn.textContent =
options.autoCleanEmptyGroups === false ? 'Enable Auto-Clean' : 'Disable Auto-Clean';
autoCleanBtn.addEventListener('click', async () => {
await send({
type: 'AB_PANEL_SET_OPTIONS',
options: {
...options,
autoCleanEmptyGroups: options.autoCleanEmptyGroups === false,
},
});
await refresh();
});
optionActions.appendChild(isolationBtn);
optionActions.appendChild(guardBtn);
optionActions.appendChild(autoCleanBtn);
summaryEl.appendChild(title);
summaryEl.appendChild(tags);
summaryEl.appendChild(optionActions);
}
function renderSessions(state) {
+1 -1
View File
@@ -276,7 +276,7 @@ Notes:
- non-default session: `Agent Browser Stealth • <session>`
- Additional extension-side capabilities:
- Session window isolation + deterministic group colors.
- Side panel controls: Focus / Keep Only This / Clean Empty Groups.
- Side panel controls: Focus / Keep Only This / Clean Empty Groups + isolation/auto-clean toggles.
- Session allowlist policy editing and fallback blocking (`about:blank`).
- Download auto-routing to `agent-browser-stealth/<session>/...`.