Compare commits

...
Author SHA1 Message Date
leeguooooo 4f630e29ad chore(release): 0.27.0-fork.20 — per-session tab groups (ab-connect 0.4.0)
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
2026-06-09 21:17:54 +09:00
leeguooooo d232763ff7 feat(connect): per-session Chrome tab groups on the shared real browser
Shared browser, separate tab groups: when an agent drives the user's real Chrome
via ab-connect, every tab it opens lands in a Chrome tab group named after its
--session (stable color per name). Each agent's tabs stay visually separated from
other agents' and from the user's own (ungrouped) tabs. Visibility is NOT
restricted — all agents still see all tabs (per design).

- CreateTargetParams gains an optional non-CDP `agentGroup` hint (skip-if-none),
  so a strict real-Chrome endpoint never receives it
- BrowserManager.agent_group(): Some(session) only when ws_url == the live
  ab-connect relay URL (never on launched/direct CDP); DAEMON_SESSION set at
  daemon start supplies the name; emitted at all createTarget sites (transient
  storage target stays None)
- ab-connect: +tabGroups permission; Target.createTarget reads agentGroup and
  groups the new tab (create/reuse by title, deterministic color), best-effort
- extension 0.3.0 -> 0.4.0; re-signed crx + zip (id unchanged)

Needs the v0.4.0 extension reloaded + a build with this change to take effect.
2026-06-09 21:17:53 +09:00
leeguooooo 85f4635358 chore(release): 0.27.0-fork.19 — new extension id (Web Store signing key) + store-aware install
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Transport (native messaging + extension connect) works today via Load unpacked.
Silent force-install is pending the Chrome Web Store listing going live (off-store
force-install is [BLOCKED] on unmanaged Chrome 149).
2026-06-09 19:20:45 +09:00
leeguooooo 726e9d4ea3 chore(store): add listing screenshot (force-add past png ignore) 2026-06-09 19:16:04 +09:00
leeguooooo ff8a340269 chore(store): add 1280x800 listing screenshot + bake in GitHub Pages privacy URL 2026-06-09 19:15:49 +09:00
leeguooooo c1fa237183 chore: add .nojekyll for GitHub Pages (serve privacy policy as-is) 2026-06-09 19:10:32 +09:00
leeguooooo f6b21461e9 feat(connect): pivot extension install to Chrome Web Store path
Verified on Chrome 149 (unmanaged macOS): a force-install policy pointing at a
SELF-HOSTED crx is tagged [BLOCKED] in chrome://policy ("Error, Warning") — Chrome
refuses off-Web-Store force-installs on non-cloud-managed browsers. So the
self-hosted-crx approach cannot work on consumer Chrome; the extension must ship
via the Chrome Web Store (same reason codex/claude do).

- UPDATE_URL -> Chrome Web Store update endpoint; add STORE_URL (one-click Add to
  Chrome) as the guaranteed path + headless fallback
- install instructions now offer: A) one-click store link, B) silent profile
  force-install (works once published), with Load-unpacked as the pre-publish stopgap
- build extensions/ab-connect.zip (CWS upload package; manifest "key" kept so the
  published id stays ciiljdlhdpfckdcfkphgmfalanpdejep)
- extensions/store/{SUBMISSION.html,privacy.html}: full listing copy, permission
  justifications (debugger is the review-sensitive one), privacy policy
- drop dead self-hosted extensions/updates.xml; pack-extension.sh now builds the zip

Not released yet — force-install only works after the store listing is Published.
2026-06-09 19:03:10 +09:00
leeguooooo e8ef57bf00 feat(connect): force-install ab-connect via Chrome config profile (no Load-unpacked GUI)
Chrome 149 killed every GUI-free way to load an *unpacked* extension into the
real profile: --load-extension removed in Chrome 142 (incl. the
--disable-features workaround), local-.crx external install blocked on macOS
since Chrome 44, remote-debugging-port killed in Chrome 136. So agents were
stuck automating the chrome://extensions Load-unpacked native file dialog —
unworkable.

`extension install` now writes a macOS configuration profile that force-installs
the signed .crx from a hosted update_url (ExtensionInstallForcelist policy). One
approval in System Settings (a single fixed Install button — cua-driver-friendly,
unlike a file dialog) → Chrome force-installs + auto-updates the extension on next
launch. No token, no per-use confirmation, and binary-install users no longer
need the extensions/ folder (crx is fetched from the URL).

- pin a stable signing key; new extension id ciiljdlhdpfckdcfkphgmfalanpdejep
- ship signed extensions/ab-connect.crx + extensions/updates.xml (raw GH host)
- scripts/pack-extension.sh re-signs with the stable key; .secrets/*.pem ignored
- uninstall removes the profile file + prints `profiles remove` hint
2026-06-09 18:27:51 +09:00
19 changed files with 550 additions and 18 deletions
+4
View File
@@ -67,3 +67,7 @@ docs/package-lock.json
# next
.next/
out/
# extension signing key (never commit) + local-only id record
.secrets/
*.pem
View File
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser-stealth"
version = "0.27.0-fork.18"
version = "0.27.0-fork.20"
dependencies = [
"aes-gcm",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.27.0-fork.18"
version = "0.27.0-fork.20"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
+141 -9
View File
@@ -20,8 +20,27 @@ use std::path::PathBuf;
pub const HOST_NAME: &str = "com.agent_browser.connect";
/// Stable id of the `ab-connect` extension, pinned by the `key` in its
/// manifest.json. Chrome only lets that extension talk to this host.
pub const EXTENSION_ID: &str = "bdoiejojpjogcjojeladhioioijhgade";
/// manifest.json (and the signing key of the published `.crx`). Chrome only lets
/// that extension talk to this host, and the force-install policy references it.
pub const EXTENSION_ID: &str = "ciiljdlhdpfckdcfkphgmfalanpdejep";
/// Update URL the force-install policy points at. MUST be the Chrome Web Store
/// endpoint: Chrome 149 tags any **off-Web-Store** force-installed extension
/// `[BLOCKED]` on an unmanaged browser (verified on macOS — chrome://policy shows
/// `[BLOCKED]…` / "Error, Warning"). Self-hosting a `.crx` therefore does NOT
/// work on consumer Chrome; the extension must be published to the Web Store, and
/// then this policy force-installs it silently (Web Store extensions are allowed).
pub const UPDATE_URL: &str = "https://clients2.google.com/service/update2/crx";
/// Public Web Store listing — the guaranteed one-click "Add to Chrome" path,
/// and the fallback when the force-install profile can't be approved headlessly.
pub const STORE_URL: &str = "https://chromewebstore.google.com/detail/ciiljdlhdpfckdcfkphgmfalanpdejep";
/// Stable identifiers for the generated Chrome configuration profile, so a
/// re-install replaces (rather than duplicates) it in System Settings.
const PROFILE_ID: &str = "work.pwtk.agent-browser.ab-connect";
const PROFILE_UUID: &str = "A1B2C3D4-AB00-4CCE-9E10-AAAABBBBCCCC";
const PROFILE_PAYLOAD_UUID: &str = "A1B2C3D4-AB01-4CCE-9E10-DDDDEEEEFFFF";
/// `agent-browser extension <install|uninstall|status>` (local; no daemon).
/// `args` is the cleaned argv including the leading "extension".
@@ -31,18 +50,40 @@ pub fn run_connect(args: &[String], json: bool) {
if uninstall {
let removed = remove_host_manifests();
report(json, true, &format!("removed {removed} native-host manifest(s)"));
let profile_removed = remove_force_install_profile();
if json {
report(json, true, &format!("removed {removed} native-host manifest(s)"));
} else {
println!("✓ removed {removed} native-host manifest(s).");
if profile_removed {
println!("✓ removed ~/.agent-browser/ab-connect.mobileconfig");
}
if cfg!(target_os = "macos") {
println!(
" To fully remove the extension, delete the \"agent-browser connect\" profile\n\
in System Settings → Profiles (or run: profiles remove -identifier {PROFILE_ID})."
);
}
}
return;
}
if install {
let no_open = args.iter().any(|a| a == "--no-open");
match install_native_host() {
Ok(paths) => {
let profile = install_force_install_profile(no_open);
if json {
println!(
"{}",
serde_json::to_string(&serde_json::json!({
"success": true,
"data": { "installed": paths, "extensionId": EXTENSION_ID }
"data": {
"installed": paths,
"extensionId": EXTENSION_ID,
"profile": profile.as_ref().ok().map(|p| p.display().to_string()),
"profileError": profile.as_ref().err(),
"updateUrl": UPDATE_URL,
}
}))
.unwrap_or_default()
);
@@ -51,11 +92,30 @@ pub fn run_connect(args: &[String], json: bool) {
for p in &paths {
println!(" {p}");
}
println!(
"\nNext: load the ab-connect extension in Chrome (chrome://extensions →\n\
Developer mode → Load unpacked → extensions/ab-connect), then this host\n\
is reachable with no token and no per-use confirmation."
);
match profile {
Ok(path) => {
println!("\n✓ Chrome force-install profile written:\n {}", path.display());
if cfg!(target_os = "macos") {
println!(
"\nGet the extension into Chrome (one-time). Either:\n\
A) One click: open {STORE_URL}\n and press \"Add to Chrome\".\n\
B) Silent: approve the profile, then restart Chrome —\n \
System Settings → General → Device Management → double-click\n \
\"agent-browser connect\" → Install. Chrome then force-installs +\n \
auto-updates it (no token, no per-use confirmation).\n\
Both need the extension published to the Web Store; until then use\n \
chrome://extensions → Developer mode → Load unpacked → extensions/ab-connect."
);
}
}
Err(e) => {
println!("\n! could not write the force-install profile: {e}");
println!(
" Fallback: load extensions/ab-connect via chrome://extensions →\n\
Developer mode → Load unpacked."
);
}
}
}
}
Err(e) => report(json, false, &format!("install failed: {e}")),
@@ -136,6 +196,78 @@ fn install_native_host() -> Result<Vec<String>, String> {
Ok(written)
}
/// Write a Chrome configuration profile that force-installs `ab-connect` from
/// [`UPDATE_URL`], and (unless `no_open`) `open` it so the user approves it once
/// in System Settings. Returns the profile path. macOS only — elsewhere it
/// returns an error and the caller prints the manual fallback.
fn install_force_install_profile(no_open: bool) -> Result<PathBuf, String> {
if !cfg!(target_os = "macos") {
return Err("force-install profile is macOS-only; on Linux set Chrome's \
ExtensionInstallForcelist policy JSON, or Load unpacked from chrome://extensions"
.into());
}
let home = dirs::home_dir().ok_or("no home dir")?;
let ab_dir = home.join(".agent-browser");
std::fs::create_dir_all(&ab_dir).map_err(|e| e.to_string())?;
let path = ab_dir.join("ab-connect.mobileconfig");
std::fs::write(&path, force_install_mobileconfig()).map_err(|e| e.to_string())?;
if !no_open {
// `open` queues the profile in System Settings for one-time approval.
let _ = std::process::Command::new("open").arg(&path).status();
}
Ok(path)
}
/// The `.mobileconfig` payload: a user-scope Chrome policy that force-installs
/// the extension by id from our hosted update manifest. User scope installs
/// without admin — just a one-time approval click.
fn force_install_mobileconfig() -> String {
let forcelist = format!("{EXTENSION_ID};{UPDATE_URL}");
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key><string>com.google.Chrome</string>
<key>PayloadVersion</key><integer>1</integer>
<key>PayloadIdentifier</key><string>{PROFILE_ID}.chrome</string>
<key>PayloadUUID</key><string>{PROFILE_PAYLOAD_UUID}</string>
<key>PayloadEnabled</key><true/>
<key>PayloadDisplayName</key><string>agent-browser connect (Chrome)</string>
<key>ExtensionInstallForcelist</key>
<array>
<string>{forcelist}</string>
</array>
</dict>
</array>
<key>PayloadType</key><string>Configuration</string>
<key>PayloadVersion</key><integer>1</integer>
<key>PayloadIdentifier</key><string>{PROFILE_ID}</string>
<key>PayloadUUID</key><string>{PROFILE_UUID}</string>
<key>PayloadDisplayName</key><string>agent-browser connect</string>
<key>PayloadDescription</key><string>Force-installs the agent-browser connect extension so agent-browser can drive your logged-in Chrome. No token, no per-use confirmation.</string>
<key>PayloadOrganization</key><string>agent-browser-stealth</string>
<key>PayloadScope</key><string>User</string>
<key>PayloadRemovalDisallowed</key><false/>
</dict>
</plist>
"#
)
}
/// Remove the generated `.mobileconfig` file (the profile itself is removed by
/// the user from System Settings, or via `profiles remove`).
fn remove_force_install_profile() -> bool {
dirs::home_dir()
.map(|h| h.join(".agent-browser").join("ab-connect.mobileconfig"))
.filter(|p| p.exists())
.map(|p| std::fs::remove_file(&p).is_ok())
.unwrap_or(false)
}
fn remove_host_manifests() -> usize {
let mut n = 0;
for dir in native_messaging_dirs() {
+32
View File
@@ -12,6 +12,11 @@ use super::cdp::lightpanda::{launch_lightpanda, LightpandaLaunchOptions, Lightpa
use super::cdp::types::*;
use super::element::{resolve_element_object_id, RefMap};
/// The daemon's session name, set once at daemon start. Names the Chrome tab
/// group that abs-created tabs land in when driving the user's real Chrome via
/// the `ab-connect` extension, so each agent/session gets its own group.
pub static DAEMON_SESSION: std::sync::OnceLock<String> = std::sync::OnceLock::new();
// ---------------------------------------------------------------------------
// Launch validation
// ---------------------------------------------------------------------------
@@ -568,12 +573,14 @@ impl BrowserManager {
if page_targets.is_empty() {
// Create a new tab
let agent_group = self.agent_group();
let result: CreateTargetResult = self
.client
.send_command_typed(
"Target.createTarget",
&CreateTargetParams {
url: "about:blank".to_string(),
agent_group,
},
None,
)
@@ -958,12 +965,14 @@ impl BrowserManager {
return Ok(());
}
let agent_group = self.agent_group();
let result: CreateTargetResult = self
.client
.send_command_typed(
"Target.createTarget",
&CreateTargetParams {
url: "about:blank".to_string(),
agent_group,
},
None,
)
@@ -1072,6 +1081,27 @@ impl BrowserManager {
self.pages.iter().any(|p| p.label.as_deref() == Some(label))
}
/// Chrome tab-group name for tabs this manager creates, or `None` when not
/// driving the user's real Chrome via the `ab-connect` extension relay.
///
/// Grouping only makes sense on the shared real browser (one Chrome, many
/// agents): each session's tabs go into its own group. On a launched / direct
/// CDP browser the endpoint is strict, so we must NOT send the custom param —
/// hence `None` there. We detect the relay by matching our `ws_url` against
/// the live relay URL the native-messaging host published.
fn agent_group(&self) -> Option<String> {
let via_relay = crate::connect::relay_url().as_deref() == Some(self.ws_url.as_str());
if !via_relay {
return None;
}
let name = DAEMON_SESSION.get().map(String::as_str).unwrap_or("default");
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
pub async fn tab_new(
&mut self,
url: Option<&str>,
@@ -1096,12 +1126,14 @@ impl BrowserManager {
let target_url = url.unwrap_or("about:blank");
let agent_group = self.agent_group();
let result: CreateTargetResult = self
.client
.send_command_typed(
"Target.createTarget",
&CreateTargetParams {
url: target_url.to_string(),
agent_group,
},
None,
)
+6
View File
@@ -141,6 +141,12 @@ pub struct SetDiscoverTargetsParams {
#[serde(rename_all = "camelCase")]
pub struct CreateTargetParams {
pub url: String,
/// Non-CDP hint consumed only by the `ab-connect` extension: the Chrome
/// tab-group name to drop the new tab into (per-session grouping on the
/// shared real Chrome). `None` on the normal CDP path so a strict real-Chrome
/// endpoint never receives an unknown parameter.
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_group: Option<String>,
}
#[derive(Debug, Deserialize)]
+4
View File
@@ -17,6 +17,10 @@ use super::state;
use super::stream::StreamServer;
pub async fn run_daemon(session: &str) {
// Record this daemon's session so tabs it opens on the shared real Chrome
// (via the ab-connect extension) land in a per-session Chrome tab group.
let _ = super::browser::DAEMON_SESSION.set(session.to_string());
let socket_dir = get_daemon_socket_dir();
if !socket_dir.exists() {
let _ = fs::create_dir_all(&socket_dir);
+2
View File
@@ -119,6 +119,8 @@ async fn collect_storage_via_temp_target(
"Target.createTarget",
&CreateTargetParams {
url: "about:blank".to_string(),
// Transient internal target (storage collection) — never grouped.
agent_group: None,
},
None,
)
Binary file not shown.
Binary file not shown.
+47
View File
@@ -28,6 +28,46 @@ const tabs = new Map()
const sessionToTab = new Map()
/** child (OOPIF/worker) sessionId -> tabId */
const childSessionToTab = new Map()
/** tab-group name -> chrome tabGroups id (best-effort cache) */
const groupIdByName = new Map()
// Deterministic color per group name so a given session keeps the same color.
const GROUP_COLORS = ['blue', 'cyan', 'green', 'yellow', 'orange', 'red', 'pink', 'purple', 'grey']
function colorForName(name) {
let h = 0
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0
return GROUP_COLORS[h % GROUP_COLORS.length]
}
// Put a freshly-created tab into the agent/session's own Chrome tab group, so
// each agent's tabs are visually separated (from each other and from the user's
// own tabs) on the shared real browser. Best-effort: grouping failures never
// break tab creation.
async function groupTabInto(tabId, name) {
if (!name || !chrome.tabGroups || !chrome.tabs.group) return
const tab = await chrome.tabs.get(tabId).catch(() => null)
if (!tab) return
let gid = groupIdByName.get(name)
if (gid != null) {
const ok = await chrome.tabGroups.get(gid).then(() => true).catch(() => false)
if (!ok) {
gid = null
groupIdByName.delete(name)
}
}
if (gid == null) {
// Reuse a same-titled group already in this window (survives SW restarts).
const found = await chrome.tabGroups.query({ windowId: tab.windowId, title: name }).catch(() => [])
if (found && found[0]) gid = found[0].id
}
if (gid == null) {
gid = await chrome.tabs.group({ tabIds: tabId })
await chrome.tabGroups.update(gid, { title: name, color: colorForName(name) }).catch(() => {})
} else {
await chrome.tabs.group({ groupId: gid, tabIds: tabId }).catch(() => {})
}
groupIdByName.set(name, gid)
}
function postToHost(msg) {
try {
@@ -121,6 +161,13 @@ async function handleForwardCdpCommand(msg) {
if (!tab.id) throw new Error('createTarget: no tab id')
await new Promise((r) => setTimeout(r, 100))
const t = await attachTab(tab.id)
// Per-session tab grouping (non-CDP hint from the daemon). Best-effort.
const group = typeof params?.agentGroup === 'string' ? params.agentGroup.trim() : ''
if (group) {
try {
await groupTabInto(tab.id, group)
} catch {}
}
return { targetId: t.targetId }
}
if (method === 'Target.closeTarget') {
+18 -5
View File
@@ -1,16 +1,29 @@
{
"manifest_version": 3,
"name": "agent-browser connect",
"version": "0.2.0",
"version": "0.4.0",
"description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPGcSmMx7dSfq9gRBDbQuAgx/+TEavrDxP/4jLa2+Ycexf/FEmq1MN8gAHoTRjSyp66YKD+1qI1CF6bk0rH5ZtxpRO7DYRTUPcsA1IpHbgEn5mppx3YxNGZilfkEZyrxdhBqUIzq3J74+/kpZzEVsO+DQTbAZSsFfdUkoCb5mbJid2VQYeqeBnYGAhbpGvN1P99jdT9EA1nKINb3ji6tLobCpyQ1fjf2uWm4mUirWkkF/nbUFVFEAh33Q/IYZmtUHgDYea5LsM9xH4KAG2kxMvFGj6vHR39sZd5/+gnvwScTcItUWQ9lFIyWYiwrSB25Lu0FshfllevXUFrG5vrvRwIDAQAB",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"permissions": ["debugger", "tabs", "nativeMessaging", "storage", "alarms", "webNavigation"],
"background": { "service_worker": "background.js", "type": "module" },
"action": { "default_title": "agent-browser connect" }
"permissions": [
"debugger",
"tabs",
"tabGroups",
"nativeMessaging",
"storage",
"alarms",
"webNavigation"
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_title": "agent-browser connect"
}
}
+131
View File
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chrome Web Store 提交指南 — agent-browser connect</title>
<style>
:root{--fg:#1a1a1a;--muted:#5c5c5c;--accent:#2563eb;--warn:#b45309;--ok:#15803d;--border:#e2e2e2;--bg:#fff;--code:#f5f5f7}
*{box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--fg);background:var(--bg);max-width:880px;margin:0 auto;padding:48px 24px;line-height:1.65}
header{border-bottom:2px solid var(--fg);padding-bottom:16px;margin-bottom:24px}
h1{font-size:1.7rem;margin:0 0 4px}
.sub{color:var(--muted)}
h2{font-size:1.2rem;margin:34px 0 10px;border-left:3px solid var(--accent);padding-left:10px}
h3{font-size:1rem;margin:20px 0 6px}
code{background:var(--code);padding:1px 5px;border-radius:4px;font-size:.88em}
pre{background:var(--code);border:1px solid var(--border);border-radius:8px;padding:12px 14px;overflow:auto;font-size:.86rem;white-space:pre-wrap}
table{border-collapse:collapse;width:100%;margin:12px 0;font-size:.92rem}
th,td{border:1px solid var(--border);padding:8px 10px;text-align:left;vertical-align:top}
th{background:var(--code)}
ol li,ul li{margin:6px 0}
.warn{background:#fffbeb;border:1px solid #fde68a;border-left:4px solid var(--warn);padding:12px 14px;border-radius:6px;margin:16px 0}
.ok{background:#f0fdf4;border:1px solid #bbf7d0;border-left:4px solid var(--ok);padding:12px 14px;border-radius:6px;margin:16px 0}
.field{font-weight:600;color:var(--accent)}
footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--border);color:var(--muted);font-size:.85rem}
</style>
</head>
<body>
<header>
<h1>Chrome Web Store 提交指南</h1>
<div class="sub">agent-browser connect · 上传包 <code>extensions/ab-connect.zip</code> · id 锁定为 <code>ciiljdlhdpfckdcfkphgmfalanpdejep</code></div>
</header>
<p>为什么必须走商店:实测 Chrome 149 在<strong>非企业托管</strong>的 Mac 上,会把"非 Web Store"的 force-install 扩展直接标成 <code>[BLOCKED]</code>。商店扩展不受此限。这也是 codex / claude 扩展都发商店的原因。</p>
<div class="warn">
<strong>评审风险(务必知道):</strong> 本扩展用了 <code>debugger</code> 权限,这是 Chrome Web Store 审核最严的权限之一。理由必须写清楚"只在用户本机、用户主动发指令时驱动用户自己的标签页,无远程服务器"。类似工具(如 claude-in-chrome)能过审,但可能被多问一轮、审核时间偏长(几天到一两周)。
</div>
<h2>一、前置(你来做,一次性)</h2>
<ol>
<li>用一个 Google 账号登录 <code>https://chrome.google.com/webstore/devconsole</code></li>
<li>首次需付 <strong>$5</strong> 一次性开发者注册费</li>
<li>(隐私政策需要一个公开 URL,见第四节 —— 我可以帮你开 GitHub Pages 托管 <code>privacy.html</code>)</li>
</ol>
<h2>二、上传</h2>
<ol>
<li>devconsole → <span class="field">New item</span> → 上传 <code>extensions/ab-connect.zip</code></li>
<li>上传后确认分配到的 Item ID = <code>ciiljdlhdpfckdcfkphgmfalanpdejep</code>(因为 manifest 里保留了 <code>key</code>,id 会被锁成这个,native messaging 的 allowed_origins 才对得上)。<strong>若 id 不是这个,告诉我,我重签。</strong></li>
</ol>
<h2>三、商店信息(直接复制以下文案)</h2>
<h3>名称 / Name</h3>
<pre>agent-browser connect</pre>
<h3>简介 / Summary(≤132 字符)</h3>
<pre>Let your own agent-browser CLI drive your logged-in Chrome — a local automation bridge. No remote server, no token.</pre>
<h3>详细描述 / Description</h3>
<pre>agent-browser connect is the in-browser half of the open-source agent-browser CLI. It lets the
command-line tool you installed on this same computer automate the Chrome you're already logged
into — opening pages, clicking, filling forms, reading the DOM — driven entirely by you.
How it works
- The extension talks ONLY to the local agent-browser CLI over Chrome native messaging (a local
inter-process channel — no network socket, no token, no remote server).
- When you run an automation command, the extension relays Chrome DevTools Protocol operations to
the tab you target, then returns the result to the CLI.
Privacy
- No analytics, no trackers, no data collection.
- Nothing is sent to any remote server. The only message peer is the local CLI.
- Source is open (Apache-2.0): https://github.com/leeguooooo/agent-browser-stealth
You need the agent-browser CLI installed and paired (run: agent-browser extension install) for this
extension to do anything.</pre>
<h3>类别 / Category</h3>
<pre>Developer Tools</pre>
<h3>语言 / Language</h3>
<pre>English</pre>
<h2>四、隐私实践(Privacy practices 标签页 —— 必填)</h2>
<h3>Single purpose(单一用途)</h3>
<pre>Bridge the user's locally-installed agent-browser CLI to their own logged-in Chrome so the CLI can
automate pages the user is working with, entirely on the user's machine and at the user's command.</pre>
<h3>各权限理由 / Permission justifications</h3>
<table>
<tr><th>权限</th><th>理由(复制到对应输入框)</th></tr>
<tr><td class="field">debugger</td><td>Attaches the Chrome DevTools Protocol to the user's own active tab so the paired local agent-browser CLI can automate it (navigate, click, read DOM) only while the user is running a command. Commands arrive solely from the local CLI via native messaging; there is no remote endpoint.</td></tr>
<tr><td class="field">tabs</td><td>Enumerate and target the correct open tab to attach automation to.</td></tr>
<tr><td class="field">nativeMessaging</td><td>The sole communication channel: a local native-messaging connection to the agent-browser CLI installed on the same machine. No network is used.</td></tr>
<tr><td class="field">storage</td><td>Persist small local pairing/configuration state for the extension.</td></tr>
<tr><td class="field">alarms</td><td>Keep the MV3 service worker alive during longer automation sessions.</td></tr>
<tr><td class="field">webNavigation</td><td>Detect page loads/navigations so automation can wait for the right moment before acting.</td></tr>
<tr><td class="field">host permissions(若被问)</td><td>The extension declares none; tab access is mediated through the debugger attach the user initiates.</td></tr>
</table>
<h3>数据用途勾选 / Data usage</h3>
<ul>
<li>不勾选任何"collects user data"类别。</li>
<li>三个合规声明全部勾选可以为真:不卖数据 / 不挪作无关用途 / 不用于判断信用资质。</li>
<li><span class="field">Privacy policy URL</span>:填 <code>privacy.html</code> 的公开地址(见下)。</li>
</ul>
<h2>五、隐私政策 URL</h2>
<p>商店要求一个公开可访问的隐私政策地址。GitHub Pages <strong>已开启</strong>,直接填这个(渲染好看):</p>
<pre>https://leeguooooo.github.io/agent-browser-stealth/extensions/store/privacy.html</pre>
<p>(部署需 1–2 分钟生效。raw 备用直链:<code>https://raw.githubusercontent.com/leeguooooo/agent-browser-stealth/main/extensions/store/privacy.html</code>。)</p>
<h2>六、截图 / Screenshots(至少 1 张,1280×800 或 640×400</h2>
<p>可以截一张 CLI + Chrome 并排的演示图。<em>需要的话我用 cua-driver 截一张合规尺寸的图给你。</em></p>
<h2>七、提交后</h2>
<ol>
<li>提交审核 → 等几天。审核通过且状态变 <em>Published</em> 后告诉我。</li>
<li>我会把 <code>extension install</code> 的 force-install <code>update_url</code> 切到商店地址并发布新 fork;之后用户 <code>extension install</code> → 批准一次描述文件 → 静默装好(商店扩展不再 <code>[BLOCKED]</code>);或者用户在商店页一键 <span class="field">Add to Chrome</span></li>
</ol>
<div class="ok">
<strong>今天的临时可用方案:</strong> 在你这台 Mac 上 <code>chrome://extensions</code> → 打开开发者模式 → Load unpacked → 选 <code>extensions/ab-connect</code>,30 秒手动装一次,native messaging + <code>extension connect</code> 立即可用。等商店过审再切静默路径。
</div>
<footer>agent-browser-stealth · 提交包与文案随扩展版本更新;改扩展后重跑 <code>scripts/pack-extension.sh</code> 并重打 <code>ab-connect.zip</code></footer>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy — agent-browser connect</title>
<style>
:root{
--fg:#1a1a1a; --muted:#5c5c5c; --accent:#2563eb; --border:#e2e2e2; --bg:#fff; --code:#f5f5f5;
}
*{box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
color:var(--fg);background:var(--bg);max-width:820px;margin:0 auto;padding:48px 24px;line-height:1.65}
header{border-bottom:2px solid var(--fg);padding-bottom:16px;margin-bottom:28px}
h1{font-size:1.7rem;margin:0 0 4px}
.sub{color:var(--muted);font-size:.95rem}
h2{font-size:1.15rem;margin:32px 0 8px;border-left:3px solid var(--accent);padding-left:10px}
code{background:var(--code);padding:1px 5px;border-radius:4px;font-size:.88em}
table{border-collapse:collapse;width:100%;margin:12px 0;font-size:.92rem}
th,td{border:1px solid var(--border);padding:8px 10px;text-align:left;vertical-align:top}
th{background:var(--code)}
.key{font-weight:600;color:var(--accent)}
footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--border);color:var(--muted);font-size:.85rem}
strong{color:var(--fg)}
</style>
</head>
<body>
<header>
<h1>Privacy Policy — agent-browser connect</h1>
<div class="sub">Chrome extension (id <code>ciiljdlhdpfckdcfkphgmfalanpdejep</code>) · Last updated 2026-06-09</div>
</header>
<p><strong>Summary: this extension collects no personal data, contains no analytics or
trackers, and sends nothing to any remote server.</strong> It is a local bridge that lets the
user's own <code>agent-browser</code> command-line tool, running on the same computer, drive the
user's logged-in Chrome.</p>
<h2>What the extension does</h2>
<p>agent-browser connect pairs Chrome with the locally-installed <code>agent-browser</code> CLI over
Chrome <em>native messaging</em> (a local inter-process channel; no network socket, no token). When
the user issues an automation command in the CLI, the extension relays Chrome DevTools Protocol
operations to the tab the user targets. Everything happens on the user's machine, initiated by the
user.</p>
<h2>Data collection &amp; use</h2>
<table>
<tr><th>Category</th><th>Collected?</th><th>Detail</th></tr>
<tr><td class="key">Personally identifiable information</td><td>No</td><td>Never read, stored, or transmitted.</td></tr>
<tr><td class="key">Browsing history</td><td>No</td><td>Not collected. Page content is acted on transiently only while the user is running an automation command, and is never stored or sent off-device.</td></tr>
<tr><td class="key">Authentication / cookies / credentials</td><td>No</td><td>Not read or exported by the extension.</td></tr>
<tr><td class="key">Analytics / telemetry</td><td>No</td><td>The extension contains no analytics, tracking, or crash-reporting code.</td></tr>
<tr><td class="key">Remote transmission</td><td>No</td><td>The extension's only message peer is the local <code>agent-browser</code> CLI via native messaging. It makes no outbound network requests of its own.</td></tr>
</table>
<h2>Permissions &amp; why they are needed</h2>
<table>
<tr><th>Permission</th><th>Purpose</th></tr>
<tr><td class="key">debugger</td><td>Attach the Chrome DevTools Protocol to the user's own tab so the local CLI can automate it, only while the user is actively running a command.</td></tr>
<tr><td class="key">tabs</td><td>Enumerate and target the correct open tab to automate.</td></tr>
<tr><td class="key">nativeMessaging</td><td>The local transport to the paired <code>agent-browser</code> CLI — the extension's sole communication channel.</td></tr>
<tr><td class="key">storage</td><td>Persist small local pairing/state values.</td></tr>
<tr><td class="key">alarms</td><td>Keep the MV3 service worker alive during longer automation sessions.</td></tr>
<tr><td class="key">webNavigation</td><td>Detect page loads so automation can wait for the right moment.</td></tr>
</table>
<h2>Data sharing</h2>
<p>None. No data is sold, shared, or transferred to third parties. There are no third parties — the
extension talks only to a program the user installed on the same computer.</p>
<h2>Contact</h2>
<p>Source code, issues, and contact: <code>https://github.com/leeguooooo/agent-browser-stealth</code></p>
<footer>
agent-browser connect is open source (Apache-2.0). This policy applies to the extension only.
</footer>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

+45
View File
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<style>
html,body{margin:0;width:1280px;height:800px;overflow:hidden;
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text",sans-serif;
background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);color:#e2e8f0}
.wrap{display:flex;flex-direction:column;height:100%;padding:56px 64px;box-sizing:border-box}
h1{font-size:46px;margin:0 0 6px;font-weight:700;letter-spacing:-.5px;color:#fff}
.tag{font-size:21px;color:#94a3b8;margin:0 0 32px;font-weight:400}
.accent{color:#38bdf8}
.term{background:#0b1220;border:1px solid #334155;border-radius:14px;
box-shadow:0 24px 60px rgba(0,0,0,.45);overflow:hidden;flex:1;display:flex;flex-direction:column}
.bar{background:#1e293b;padding:13px 18px;display:flex;gap:9px;align-items:center;border-bottom:1px solid #334155}
.dot{width:13px;height:13px;border-radius:50%}
.r{background:#ff5f56}.y{background:#ffbd2e}.g{background:#27c93f}
.bartitle{color:#64748b;font-size:14px;margin-left:12px;font-family:ui-monospace,monospace}
pre{margin:0;padding:26px 30px;font-family:ui-monospace,"SF Mono",Menlo,monospace;
font-size:19.5px;line-height:1.72;flex:1}
.p{color:#38bdf8}.c{color:#f1f5f9;font-weight:600}.o{color:#94a3b8}.ok{color:#4ade80}.dim{color:#475569}
.foot{display:flex;gap:40px;margin-top:30px;font-size:18px;color:#cbd5e1}
.foot b{color:#fff}
.pill{display:inline-block;background:#0c4a6e;color:#7dd3fc;font-size:15px;padding:4px 13px;
border-radius:999px;margin-left:14px;vertical-align:middle;font-weight:600}
</style></head>
<body><div class="wrap">
<h1>agent-browser&nbsp;connect <span class="pill">local · no token · no remote</span></h1>
<p class="tag">Let your own <span class="accent">agent-browser</span> CLI drive the Chrome you're already logged into.</p>
<div class="term">
<div class="bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="bartitle">zsh — agent-browser</span></div>
<pre><span class="p">$</span> <span class="c">agent-browser extension install</span>
<span class="ok"></span> <span class="o">native-messaging host installed (com.agent_browser.connect)</span>
<span class="ok"></span> <span class="o">extension ready — add it from the Chrome Web Store</span>
<span class="p">$</span> <span class="c">agent-browser open</span> <span class="o">"https://mail.google.com"</span> <span class="dim"># your logged-in tab</span>
<span class="p">$</span> <span class="c">agent-browser snapshot -i</span> <span class="dim"># read the page</span>
<span class="p">$</span> <span class="c">agent-browser click</span> <span class="o">@e42</span> <span class="dim"># act on it</span>
<span class="ok"></span> <span class="o">driving your real session — no re-login, no confirmation</span>
</pre>
</div>
<div class="foot">
<span>🔌 <b>Native messaging</b> — local only</span>
<span>🧩 <b>chrome.debugger</b> — on your command</span>
<span>🔓 <b>Open source</b> · Apache-2.0</span>
</div>
</div></body></html>
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "agent-browser-stealth",
"version": "0.27.0-fork.18",
"description": "Browser automation CLI for AI agents stealth fork with anti-detection",
"version": "0.27.0-fork.20",
"description": "Browser automation CLI for AI agents \u2014 stealth fork with anti-detection",
"type": "module",
"packageManager": "pnpm@11.1.3",
"files": [
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
# Build the Chrome Web Store upload package extensions/ab-connect.zip (and a signed
# extensions/ab-connect.crx for reference) from extensions/ab-connect, keeping the
# extension id constant via the stable signing key + manifest "key".
#
# The id MUST stay ciiljdlhdpfckdcfkphgmfalanpdejep so the native-messaging
# allowed_origins and the force-install policy keep matching. The id is pinned by
# the "key" field in manifest.json (kept in the uploaded zip on purpose).
#
# The private key lives at .secrets/ab-connect.pem and is git-ignored.
#
# After changing the extension:
# 1. bump "version" in extensions/ab-connect/manifest.json
# 2. run this script
# 3. commit extensions/ab-connect.zip (+ .crx) + manifest.json
# 4. upload ab-connect.zip to the Web Store (see extensions/store/SUBMISSION.html)
set -e
cd "$(dirname "$0")/.."
KEY=.secrets/ab-connect.pem
EXT=extensions/ab-connect
CHROME="${CHROME_BIN:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}"
# Web Store upload package (zip of the unpacked extension, dotfiles excluded).
rm -f extensions/ab-connect.zip
( cd "$EXT" && zip -rq ../ab-connect.zip . -x '.*' )
[ -f extensions/ab-connect.zip ] || { echo "error: zip failed" >&2; exit 1; }
# Signed crx (reference / non-store force-install for managed setups).
if [ -f "$KEY" ]; then
rm -f extensions/ab-connect.crx
"$CHROME" --pack-extension="$PWD/$EXT" --pack-extension-key="$PWD/$KEY" >/dev/null 2>&1 || true
ID=$(openssl rsa -in "$KEY" -pubout -outform DER 2>/dev/null \
| openssl dgst -sha256 -binary | xxd -p -c256 | head -c32 | tr '0-9a-f' 'a-p')
echo "extension id: $ID"
else
echo "note: $KEY missing — built zip only (no crx)."
fi
echo "packed extensions/ab-connect.zip"
echo "manifest version: $(grep -o '"version"[^,]*' "$EXT/manifest.json" | head -1)"