mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-17 04:01:13 +00:00
Refine host browser routing and settings copy
Store and surface host-browser preparation and CDP endpoint metadata from A0 CLI. Let Browser runtime prepare candidate CLIs before the first action, and keep host-required errors more actionable. Simplify Host Browser settings language and document the Chrome remote-debugging consent flow.
This commit is contained in:
parent
4b3e2eb327
commit
d47207dfd7
7 changed files with 155 additions and 37 deletions
|
|
@ -51,7 +51,12 @@ to pair it with local-model enforcement for host-browser content.
|
|||
|
||||
1. Keep A0 CLI connected to the Agent Zero chat.
|
||||
|
||||
2. Optionally list or select a Chrome-family profile:
|
||||
2. If you want Agent Zero to use an already-open personal Chrome window, open
|
||||
`chrome://inspect/#remote-debugging` and click **Allow** for that browser
|
||||
instance. A0 CLI detects Chrome's local `DevToolsActivePort` file; status and
|
||||
profile checks do not connect to Chrome.
|
||||
|
||||
3. Optionally list or select a Chrome-family profile:
|
||||
|
||||
```bash
|
||||
/browser profile
|
||||
|
|
@ -60,12 +65,12 @@ to pair it with local-model enforcement for host-browser content.
|
|||
```
|
||||
|
||||
Chrome 136+ blocks Playwright remote debugging against the default personal
|
||||
Chrome data directory. In that case, choose the A0-controlled local profile
|
||||
(`chrome-a0 Default` for Google Chrome). Cookies and site data remain in that
|
||||
separate browser profile on the host, and the user may need to sign in there
|
||||
once.
|
||||
Chrome data directory. If Chrome's own Remote debugging consent path is not
|
||||
available, choose the A0-controlled local profile (`chrome-a0 Default` for
|
||||
Google Chrome). Cookies and site data remain in that separate browser profile on
|
||||
the host, and the user may need to sign in there once.
|
||||
|
||||
3. In Agent Zero WebUI, open Browser plugin settings and choose one of:
|
||||
4. In Agent Zero WebUI, open Browser plugin settings and choose one of:
|
||||
|
||||
- `container`: always use the Docker/server Playwright browser.
|
||||
- `host_when_available`: use the A0 CLI host browser when the subscribed CLI can provide it, otherwise fall back to container.
|
||||
|
|
@ -81,13 +86,15 @@ still useful for diagnostics and manual override:
|
|||
/browser relaunch
|
||||
```
|
||||
|
||||
4. If the selected Chrome profile is already open normally, A0 CLI reports
|
||||
5. If the selected Chrome profile is already open normally, A0 CLI reports
|
||||
`relaunch_required`. Close that browser and retry the agent request or run
|
||||
`/browser relaunch` manually.
|
||||
|
||||
The MVP uses Python Playwright against installed system Chrome, Chromium, or
|
||||
Edge. It does not require a Chrome extension, and it does not copy browser
|
||||
credentials, cookies, or profile data out of the browser profile.
|
||||
The local-profile launch path uses Python Playwright against installed system
|
||||
Chrome, Chromium, or Edge. The user-authorized Chrome remote debugging path uses
|
||||
A0 CLI's built-in DevTools Protocol helper instead, so users do not need to
|
||||
install Chrome DevTools MCP. A0 does not copy browser credentials, cookies, or
|
||||
profile data out of the browser profile.
|
||||
|
||||
Host-browser page content and screenshots are controlled by the Browser
|
||||
plugin's project-level policy:
|
||||
|
|
|
|||
|
|
@ -62,11 +62,13 @@ class ComputerUseMetadata:
|
|||
@dataclass(frozen=True)
|
||||
class HostBrowserMetadata:
|
||||
supported: bool
|
||||
can_prepare: bool
|
||||
enabled: bool
|
||||
status: str
|
||||
browser_family: str
|
||||
profile_label: str
|
||||
profile_path: str
|
||||
cdp_endpoint: str
|
||||
features: tuple[str, ...]
|
||||
support_reason: str
|
||||
updated_at: float
|
||||
|
|
@ -359,15 +361,18 @@ def store_sid_host_browser_metadata(sid: str, payload: dict[str, Any]) -> HostBr
|
|||
features = tuple(str(item).strip() for item in features_value if str(item).strip())
|
||||
else:
|
||||
features = ()
|
||||
support_reason = str(payload.get("support_reason", "") or "").strip()
|
||||
metadata = HostBrowserMetadata(
|
||||
supported=bool(payload.get("supported")),
|
||||
can_prepare=_host_browser_can_prepare(payload, features=features, support_reason=support_reason),
|
||||
enabled=bool(payload.get("supported")) and bool(payload.get("enabled")),
|
||||
status=str(payload.get("status", "") or "").strip(),
|
||||
browser_family=str(payload.get("browser_family", "") or "").strip(),
|
||||
profile_label=str(payload.get("profile_label", "") or "").strip(),
|
||||
profile_path=str(payload.get("profile_path", "") or "").strip(),
|
||||
cdp_endpoint=str(payload.get("cdp_endpoint", "") or "").strip(),
|
||||
features=features,
|
||||
support_reason=str(payload.get("support_reason", "") or "").strip(),
|
||||
support_reason=support_reason,
|
||||
updated_at=time.time(),
|
||||
)
|
||||
with _state_lock:
|
||||
|
|
@ -375,6 +380,25 @@ def store_sid_host_browser_metadata(sid: str, payload: dict[str, Any]) -> HostBr
|
|||
return metadata
|
||||
|
||||
|
||||
def _host_browser_can_prepare(
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
features: tuple[str, ...],
|
||||
support_reason: str,
|
||||
) -> bool:
|
||||
if "can_prepare" in payload:
|
||||
return bool(payload.get("can_prepare"))
|
||||
if "ensure" not in features:
|
||||
return False
|
||||
reason = support_reason.lower()
|
||||
return (
|
||||
"python playwright" in reason
|
||||
or "a0-controlled local profile" in reason
|
||||
or "chrome-a0" in reason
|
||||
or "remote debugging" in reason
|
||||
)
|
||||
|
||||
|
||||
def clear_sid_host_browser_metadata(sid: str) -> None:
|
||||
with _state_lock:
|
||||
_sid_host_browser_metadata.pop(sid, None)
|
||||
|
|
@ -387,11 +411,13 @@ def host_browser_metadata_for_sid(sid: str) -> dict[str, Any] | None:
|
|||
return None
|
||||
return {
|
||||
"supported": metadata.supported,
|
||||
"can_prepare": metadata.can_prepare,
|
||||
"enabled": metadata.enabled,
|
||||
"status": metadata.status,
|
||||
"browser_family": metadata.browser_family,
|
||||
"profile_label": metadata.profile_label,
|
||||
"profile_path": metadata.profile_path,
|
||||
"cdp_endpoint": metadata.cdp_endpoint,
|
||||
"features": list(metadata.features),
|
||||
"support_reason": metadata.support_reason,
|
||||
"updated_at": metadata.updated_at,
|
||||
|
|
@ -421,7 +447,7 @@ def select_host_browser_candidate_sid(context_id: str) -> str | None:
|
|||
fallback: str | None = None
|
||||
for sid in subscribers:
|
||||
metadata = _sid_host_browser_metadata.get(sid)
|
||||
if not metadata or not metadata.supported:
|
||||
if not metadata or not (metadata.supported or metadata.can_prepare):
|
||||
continue
|
||||
if metadata.enabled and metadata.status in {"ready", "active"}:
|
||||
return sid
|
||||
|
|
|
|||
|
|
@ -328,7 +328,8 @@ class ConnectorBrowserRuntime:
|
|||
for status in statuses:
|
||||
parts.append(
|
||||
f"sid={status.get('sid')} status={status.get('status')} "
|
||||
f"supported={status.get('supported')} enabled={status.get('enabled')} "
|
||||
f"supported={status.get('supported')} can_prepare={status.get('can_prepare')} "
|
||||
f"enabled={status.get('enabled')} "
|
||||
f"reason={status.get('support_reason') or 'none'}"
|
||||
)
|
||||
return "; ".join(parts)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ def _host_browser_status_detail(context_id: str) -> str:
|
|||
for status in statuses:
|
||||
parts.append(
|
||||
f"sid={status.get('sid')} supported={status.get('supported')} "
|
||||
f"enabled={status.get('enabled')} status={status.get('status') or 'unknown'} "
|
||||
f"can_prepare={status.get('can_prepare')} enabled={status.get('enabled')} "
|
||||
f"status={status.get('status') or 'unknown'} "
|
||||
f"reason={status.get('support_reason') or 'none'}"
|
||||
)
|
||||
return "; ".join(parts)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,32 @@ function normalizeBoolean(value, fallback = true) {
|
|||
return fallback;
|
||||
}
|
||||
|
||||
function hostBrowserFamilyLabel(value) {
|
||||
const family = String(value || "").trim().toLowerCase();
|
||||
const a0Profile = family.endsWith("-a0");
|
||||
const remoteDebugging = family.endsWith("-cdp");
|
||||
const base = a0Profile ? family.slice(0, -3) : remoteDebugging ? family.slice(0, -4) : family;
|
||||
const labels = {
|
||||
chrome: "Chrome",
|
||||
chromium: "Chromium",
|
||||
edge: "Edge",
|
||||
"edge-dev": "Edge Dev",
|
||||
};
|
||||
const label = labels[base] || "Host browser";
|
||||
if (remoteDebugging) return `${label} (allowed)`;
|
||||
return a0Profile ? `${label} (A0 profile)` : label;
|
||||
}
|
||||
|
||||
function hostBrowserStatusLabel(value) {
|
||||
const status = String(value || "").trim().toLowerCase();
|
||||
if (status === "active") return "open";
|
||||
if (status === "ready") return "ready";
|
||||
if (status === "disabled") return "will open on first use";
|
||||
if (status === "relaunch_required") return "close browser and retry";
|
||||
if (status === "unsupported") return "unavailable";
|
||||
return status || "ready";
|
||||
}
|
||||
|
||||
export const store = createStore("browserConfig", {
|
||||
config: null,
|
||||
extensionsList: [],
|
||||
|
|
@ -96,16 +122,16 @@ export const store = createStore("browserConfig", {
|
|||
|
||||
runtimeBackendLabel() {
|
||||
const value = this.config?.runtime_backend || "container";
|
||||
if (value === "host_when_available") return "Host When Available";
|
||||
if (value === "host_required") return "Host Required";
|
||||
return "Container";
|
||||
if (value === "host_when_available") return "Use Host When Ready";
|
||||
if (value === "host_required") return "Require Host Browser";
|
||||
return "Docker Browser";
|
||||
},
|
||||
|
||||
privacyPolicyLabel() {
|
||||
const value = this.config?.host_browser_privacy_policy || "enforce_local";
|
||||
if (value === "warn") return "Warn";
|
||||
if (value === "warn") return "Warn When Using Cloud";
|
||||
if (value === "allow") return "Allow";
|
||||
return "Enforce Local";
|
||||
return "Local Models Only";
|
||||
},
|
||||
|
||||
async loadHostBrowserStatus() {
|
||||
|
|
@ -127,12 +153,13 @@ export const store = createStore("browserConfig", {
|
|||
: [];
|
||||
const active = connectors.find((item) => item?.supported && item?.enabled);
|
||||
if (active) {
|
||||
const family = active.browser_family || "browser";
|
||||
const profile = active.profile_label ? ` / ${active.profile_label}` : "";
|
||||
return `${family}${profile}: ${active.status || "ready"}`;
|
||||
const profile = active.profile_label ? ` - ${active.profile_label}` : "";
|
||||
return `${hostBrowserFamilyLabel(active.browser_family)}${profile}: ${hostBrowserStatusLabel(active.status)}`;
|
||||
}
|
||||
if (connectors.length) return "A0 CLI connected, host browser disabled or unavailable";
|
||||
return "Waiting for A0 CLI";
|
||||
const preparable = connectors.find((item) => item?.can_prepare || item?.supported);
|
||||
if (preparable) return "A0 CLI connected - browser will open on first use";
|
||||
if (connectors.length) return "A0 CLI connected - host browser unavailable";
|
||||
return "Connect A0 CLI to use a host browser";
|
||||
},
|
||||
|
||||
hasPaths() {
|
||||
|
|
|
|||
|
|
@ -53,27 +53,27 @@
|
|||
<div class="browser-config-card">
|
||||
<div class="section-title">Host Browser</div>
|
||||
<div class="section-description">
|
||||
Route the existing Browser tool through A0 CLI. When host mode is selected, the first browser action can prepare the local browser automatically.
|
||||
Use Chrome, Edge, or Chromium on this computer. Keep A0 CLI connected; the browser opens when the agent first needs it.
|
||||
</div>
|
||||
|
||||
<label class="browser-config-field">
|
||||
<span class="browser-config-field-label">Backend mode</span>
|
||||
<span class="browser-config-field-label">Browser location</span>
|
||||
<select x-model="$store.browserConfig.config.runtime_backend">
|
||||
<option value="container">Container</option>
|
||||
<option value="host_when_available">Host when available</option>
|
||||
<option value="host_required">Host required</option>
|
||||
<option value="container">Docker browser</option>
|
||||
<option value="host_when_available">Use host when ready</option>
|
||||
<option value="host_required">Require host browser</option>
|
||||
</select>
|
||||
<span class="browser-config-field-help">The WebUI setting is the routing intent; CLI browser commands are available for diagnostics and manual override.</span>
|
||||
<span class="browser-config-field-help">Require host browser when pages must stay on this computer.</span>
|
||||
</label>
|
||||
|
||||
<label class="browser-config-field">
|
||||
<span class="browser-config-field-label">Host content policy</span>
|
||||
<span class="browser-config-field-label">Page content access</span>
|
||||
<select x-model="$store.browserConfig.config.host_browser_privacy_policy">
|
||||
<option value="enforce_local">Enforce local</option>
|
||||
<option value="warn">Warn</option>
|
||||
<option value="enforce_local">Local models only</option>
|
||||
<option value="warn">Warn when using cloud</option>
|
||||
<option value="allow">Allow</option>
|
||||
</select>
|
||||
<span class="browser-config-field-help">Local-model enforcement applies before host page content or screenshots are returned to the agent.</span>
|
||||
<span class="browser-config-field-help">Controls page text and screenshots from the host browser.</span>
|
||||
</label>
|
||||
|
||||
<div class="browser-config-note">
|
||||
|
|
|
|||
|
|
@ -72,6 +72,60 @@ def test_host_browser_candidate_selection_allows_disabled_supported_cli():
|
|||
ws_runtime.unregister_sid(sid)
|
||||
|
||||
|
||||
def test_host_browser_candidate_selection_allows_preparable_cli():
|
||||
sid = "sid-host-browser-preparable"
|
||||
context_id = "ctx-host-browser-preparable"
|
||||
ws_runtime.register_sid(sid)
|
||||
ws_runtime.subscribe_sid_to_context(sid, context_id)
|
||||
try:
|
||||
ws_runtime.store_sid_host_browser_metadata(
|
||||
sid,
|
||||
{
|
||||
"supported": False,
|
||||
"can_prepare": True,
|
||||
"enabled": False,
|
||||
"status": "unsupported",
|
||||
"browser_family": "chrome-a0",
|
||||
"profile_label": "Default",
|
||||
"features": ["ensure", "open"],
|
||||
"support_reason": "Python Playwright is not installed.",
|
||||
},
|
||||
)
|
||||
|
||||
assert ws_runtime.select_host_browser_target_sid(context_id) is None
|
||||
assert ws_runtime.select_host_browser_candidate_sid(context_id) == sid
|
||||
rows = ws_runtime.host_browser_metadata_for_context(context_id)
|
||||
assert rows[0]["can_prepare"] is True
|
||||
finally:
|
||||
ws_runtime.unregister_sid(sid)
|
||||
|
||||
|
||||
def test_host_browser_metadata_infers_preparable_legacy_cli():
|
||||
sid = "sid-host-browser-legacy-preparable"
|
||||
context_id = "ctx-host-browser-legacy-preparable"
|
||||
ws_runtime.register_sid(sid)
|
||||
ws_runtime.subscribe_sid_to_context(sid, context_id)
|
||||
try:
|
||||
ws_runtime.store_sid_host_browser_metadata(
|
||||
sid,
|
||||
{
|
||||
"supported": False,
|
||||
"enabled": False,
|
||||
"status": "unsupported",
|
||||
"browser_family": "chrome-a0",
|
||||
"profile_label": "Default",
|
||||
"features": ["ensure", "open"],
|
||||
"support_reason": "Python Playwright is not installed.",
|
||||
},
|
||||
)
|
||||
|
||||
rows = ws_runtime.host_browser_metadata_for_context(context_id)
|
||||
assert rows[0]["can_prepare"] is True
|
||||
assert ws_runtime.select_host_browser_candidate_sid(context_id) == sid
|
||||
finally:
|
||||
ws_runtime.unregister_sid(sid)
|
||||
|
||||
|
||||
def test_pending_browser_op_resolves_and_disconnect_fails():
|
||||
async def run() -> None:
|
||||
sid = "sid-browser-pending"
|
||||
|
|
@ -205,7 +259,7 @@ def test_host_browser_artifact_materialization_rejects_oversized_payload(monkeyp
|
|||
assert not list(tmp_path.rglob("shot.jpg"))
|
||||
|
||||
|
||||
def test_connector_runtime_ensures_disabled_host_browser_before_action(monkeypatch):
|
||||
def test_connector_runtime_ensures_preparable_host_browser_before_action(monkeypatch):
|
||||
async def run() -> None:
|
||||
import plugins._browser.helpers.connector_runtime as connector_runtime_module
|
||||
|
||||
|
|
@ -251,12 +305,14 @@ def test_connector_runtime_ensures_disabled_host_browser_before_action(monkeypat
|
|||
ws_runtime.store_sid_host_browser_metadata(
|
||||
sid,
|
||||
{
|
||||
"supported": True,
|
||||
"supported": False,
|
||||
"can_prepare": True,
|
||||
"enabled": False,
|
||||
"status": "disabled",
|
||||
"status": "unsupported",
|
||||
"browser_family": "chrome-a0",
|
||||
"profile_label": "Default",
|
||||
"features": ["ensure", "open"],
|
||||
"support_reason": "Python Playwright is not installed.",
|
||||
},
|
||||
)
|
||||
runtime = ConnectorBrowserRuntime(context_id, _agent(context_id))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue