mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-19 07:59:34 +00:00
Add host browser profile mode setting
Default Bring Your Own Browser mode to the existing browser profile while exposing a clean Agent profile option in Browser settings with a clear warning for existing-profile access. Forward the selected profile mode through the connector browser runtime, tolerate legacy config modules and old saved configs, and update regression coverage for the new payload shape.
This commit is contained in:
parent
02838f7698
commit
0a8aaee9ac
8 changed files with 145 additions and 8 deletions
|
|
@ -6,7 +6,7 @@ import plugins._a0_connector.api.v1.base as connector_base
|
|||
|
||||
|
||||
_PRIVACY_NOTICE = (
|
||||
"For GDPR/content policy, visit Agent Zero WebUI > Browser settings to choose "
|
||||
"For Browser model-use settings, visit Agent Zero WebUI > Browser settings to choose "
|
||||
"Local models only, Warn when using cloud, or Allow."
|
||||
)
|
||||
|
||||
|
|
@ -24,6 +24,15 @@ def _normalize_requested_backend(value: object) -> str:
|
|||
return ""
|
||||
|
||||
|
||||
def _normalize_profile_mode(value: object) -> str:
|
||||
normalized = _string(value).lower().replace("-", "_").replace(" ", "_")
|
||||
if normalized in {"agent", "clean", "clean_agent", "a0", "dedicated"}:
|
||||
return "agent"
|
||||
if normalized in {"existing", "user", "personal", "current"}:
|
||||
return "existing"
|
||||
return ""
|
||||
|
||||
|
||||
def _runtime_label(value: str) -> str:
|
||||
if value == "host_required":
|
||||
return "Bring Your Own Browser"
|
||||
|
|
@ -59,12 +68,30 @@ class BrowserRuntime(connector_base.ProtectedConnectorApiHandler):
|
|||
mimetype="application/json",
|
||||
)
|
||||
settings["runtime_backend"] = runtime_backend
|
||||
if "host_browser_profile_mode" in input or "profile_mode" in input:
|
||||
profile_mode = _normalize_profile_mode(
|
||||
input.get("host_browser_profile_mode", input.get("profile_mode"))
|
||||
)
|
||||
if not profile_mode:
|
||||
return Response(
|
||||
response='{"error":"host_browser_profile_mode must be existing or agent"}',
|
||||
status=400,
|
||||
mimetype="application/json",
|
||||
)
|
||||
settings["host_browser_profile_mode"] = profile_mode
|
||||
settings["host_browser_profile_mode"] = (
|
||||
_normalize_profile_mode(settings.get("host_browser_profile_mode")) or "existing"
|
||||
)
|
||||
self._save_browser_config(project_name, settings)
|
||||
|
||||
runtime_backend = settings.get("runtime_backend") or "container"
|
||||
profile_mode = _normalize_profile_mode(settings.get("host_browser_profile_mode")) or "existing"
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"runtime_backend": settings["runtime_backend"],
|
||||
"label": _runtime_label(settings["runtime_backend"]),
|
||||
"runtime_backend": runtime_backend,
|
||||
"host_browser_profile_mode": profile_mode,
|
||||
"label": _runtime_label(runtime_backend),
|
||||
"project_name": project_name,
|
||||
"agent_profile": "",
|
||||
"privacy_notice": _PRIVACY_NOTICE,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ runtime_backend: "container"
|
|||
# - allow: allow without warning.
|
||||
host_browser_privacy_policy: "enforce_local"
|
||||
|
||||
# Host-browser profile preference:
|
||||
# - existing: use the user's authorized existing browser profile when available.
|
||||
# - agent: use a clean A0-controlled browser profile on the host.
|
||||
host_browser_profile_mode: "existing"
|
||||
|
||||
# Optional _model_config preset used by Browser-owned model helpers.
|
||||
# Empty uses the effective Main Model.
|
||||
model_preset: ""
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ DEFAULT_HOMEPAGE_KEY = "default_homepage"
|
|||
AUTOFOCUS_ACTIVE_PAGE_KEY = "autofocus_active_page"
|
||||
RUNTIME_BACKEND_KEY = "runtime_backend"
|
||||
HOST_BROWSER_PRIVACY_POLICY_KEY = "host_browser_privacy_policy"
|
||||
HOST_BROWSER_PROFILE_MODE_KEY = "host_browser_profile_mode"
|
||||
RUNTIME_BACKENDS = {"container", "host_required"}
|
||||
HOST_BROWSER_PRIVACY_POLICIES = {"enforce_local", "warn", "allow"}
|
||||
HOST_BROWSER_PROFILE_MODES = {"existing", "agent"}
|
||||
BASE_BROWSER_ARGS = [
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
|
|
@ -110,6 +112,11 @@ def normalize_browser_config(settings: dict[str, Any] | None) -> dict[str, Any]:
|
|||
allowed=HOST_BROWSER_PRIVACY_POLICIES,
|
||||
default="enforce_local",
|
||||
),
|
||||
HOST_BROWSER_PROFILE_MODE_KEY: _normalize_choice(
|
||||
raw.get(HOST_BROWSER_PROFILE_MODE_KEY, "existing"),
|
||||
allowed=HOST_BROWSER_PROFILE_MODES,
|
||||
default="existing",
|
||||
),
|
||||
MODEL_PRESET_KEY: _normalize_model_preset(raw.get(MODEL_PRESET_KEY, "")),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,10 +34,7 @@ from plugins._a0_connector.helpers.ws_runtime import (
|
|||
select_host_browser_target_sid,
|
||||
store_pending_browser_op,
|
||||
)
|
||||
from plugins._browser.helpers.config import (
|
||||
HOST_BROWSER_PRIVACY_POLICY_KEY,
|
||||
get_browser_config,
|
||||
)
|
||||
from plugins._browser.helpers import config as browser_config
|
||||
from plugins._browser.helpers.url import normalize_url
|
||||
|
||||
|
||||
|
|
@ -47,6 +44,17 @@ HOST_BROWSER_SCREENSHOT_DIR = ("tmp", "browser", "host-screenshots")
|
|||
CONTENT_HELPER_PATH = Path(__file__).resolve().parents[1] / "assets" / "browser-page-content.js"
|
||||
MAX_ARTIFACT_SIZE_BYTES = 25 * 1024 * 1024
|
||||
BASE64_DECODE_CHARS_PER_CHUNK = 64 * 1024
|
||||
HOST_BROWSER_PRIVACY_POLICY_KEY = getattr(
|
||||
browser_config,
|
||||
"HOST_BROWSER_PRIVACY_POLICY_KEY",
|
||||
"host_browser_privacy_policy",
|
||||
)
|
||||
HOST_BROWSER_PROFILE_MODE_KEY = getattr(
|
||||
browser_config,
|
||||
"HOST_BROWSER_PROFILE_MODE_KEY",
|
||||
"host_browser_profile_mode",
|
||||
)
|
||||
get_browser_config = browser_config.get_browser_config
|
||||
_LOCAL_PROVIDERS = {"ollama", "lm_studio"}
|
||||
_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", "host.docker.internal"}
|
||||
_SENSITIVE_ACTIONS = {"content", "detail", "evaluate", "screenshot", "screenshot_file"}
|
||||
|
|
@ -79,6 +87,7 @@ class ConnectorBrowserRuntime:
|
|||
"op_id": str(uuid.uuid4()),
|
||||
"context_id": self.context_id,
|
||||
"action": action,
|
||||
"profile_mode": self._host_browser_profile_mode(),
|
||||
}
|
||||
|
||||
if action == "open":
|
||||
|
|
@ -188,6 +197,7 @@ class ConnectorBrowserRuntime:
|
|||
return normalized_calls
|
||||
|
||||
async def _dispatch(self, payload: dict[str, Any]) -> Any:
|
||||
payload.setdefault("profile_mode", self._host_browser_profile_mode())
|
||||
self._enforce_privacy(payload)
|
||||
sid = self._select_sid()
|
||||
if not sid:
|
||||
|
|
@ -207,6 +217,7 @@ class ConnectorBrowserRuntime:
|
|||
"op_id": str(uuid.uuid4()),
|
||||
"context_id": self.context_id,
|
||||
"action": "ensure",
|
||||
"profile_mode": self._host_browser_profile_mode(),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -214,6 +225,11 @@ class ConnectorBrowserRuntime:
|
|||
|
||||
return await self._send_browser_op(sid, self._with_content_helper(sid, payload))
|
||||
|
||||
def _host_browser_profile_mode(self) -> str:
|
||||
config = get_browser_config(self.agent)
|
||||
mode = str(config.get(HOST_BROWSER_PROFILE_MODE_KEY) or "existing").strip().lower()
|
||||
return "agent" if mode == "agent" else "existing"
|
||||
|
||||
def _with_content_helper(self, sid: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
metadata = host_browser_metadata_for_sid(sid) or {}
|
||||
if str(metadata.get("content_helper_sha256") or "").strip().lower() == _content_helper_sha256():
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const BROWSER_EXTENSIONS_API = "/plugins/_browser/extensions";
|
|||
const BROWSER_STATUS_API = "/plugins/_browser/status";
|
||||
const RUNTIME_BACKENDS = new Set(["container", "host_required"]);
|
||||
const HOST_PRIVACY_POLICIES = new Set(["enforce_local", "warn", "allow"]);
|
||||
const HOST_PROFILE_MODES = new Set(["existing", "agent"]);
|
||||
|
||||
function normalizePathList(value) {
|
||||
const source = Array.isArray(value)
|
||||
|
|
@ -32,6 +33,11 @@ function ensureConfig(config) {
|
|||
HOST_PRIVACY_POLICIES,
|
||||
"enforce_local",
|
||||
);
|
||||
config.host_browser_profile_mode = normalizeChoice(
|
||||
config.host_browser_profile_mode,
|
||||
HOST_PROFILE_MODES,
|
||||
"existing",
|
||||
);
|
||||
config.model_preset = String(config.model_preset || "").trim();
|
||||
delete config.model;
|
||||
return config;
|
||||
|
|
@ -139,6 +145,12 @@ export const store = createStore("browserConfig", {
|
|||
return "Local Models Only";
|
||||
},
|
||||
|
||||
hostBrowserProfileModeLabel() {
|
||||
const value = this.config?.host_browser_profile_mode || "existing";
|
||||
if (value === "agent") return "Clean Agent Profile";
|
||||
return "Existing Browser Profile";
|
||||
},
|
||||
|
||||
async loadHostBrowserStatus() {
|
||||
if (this.hostBrowserStatusLoading) return;
|
||||
this.hostBrowserStatusLoading = true;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,34 @@
|
|||
</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="browser-config-field"
|
||||
x-show="$store.browserConfig.config.runtime_backend === 'host_required'"
|
||||
>
|
||||
<span class="browser-config-field-label">Host browser profile</span>
|
||||
<select x-model="$store.browserConfig.config.host_browser_profile_mode">
|
||||
<option value="existing">Existing browser profile</option>
|
||||
<option value="agent">Clean Agent profile</option>
|
||||
</select>
|
||||
<span
|
||||
class="browser-config-field-help"
|
||||
x-show="$store.browserConfig.config.host_browser_profile_mode === 'agent'"
|
||||
>
|
||||
Uses a separate A0-controlled local profile on this computer.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="browser-config-warning"
|
||||
x-show="$store.browserConfig.config.runtime_backend === 'host_required' && $store.browserConfig.config.host_browser_profile_mode !== 'agent'"
|
||||
>
|
||||
<span class="material-symbols-outlined">warning</span>
|
||||
<span>
|
||||
Existing profile lets the agent interact with the browser instance you authorize,
|
||||
including signed-in sites, cookies, tabs, downloads, and page content.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label class="browser-config-field">
|
||||
<span class="browser-config-field-label">Page content access</span>
|
||||
<select x-model="$store.browserConfig.config.host_browser_privacy_policy">
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ def test_browser_config_normalizes_extension_paths(tmp_path):
|
|||
"autofocus_active_page": True,
|
||||
"runtime_backend": "container",
|
||||
"host_browser_privacy_policy": "enforce_local",
|
||||
"host_browser_profile_mode": "existing",
|
||||
"model_preset": "",
|
||||
}
|
||||
|
||||
|
|
@ -169,11 +170,13 @@ def test_browser_config_normalizes_host_backend_and_privacy_policy():
|
|||
{
|
||||
"runtime_backend": "host-required",
|
||||
"host_browser_privacy_policy": "warn",
|
||||
"host_browser_profile_mode": "agent",
|
||||
}
|
||||
)
|
||||
|
||||
assert config["runtime_backend"] == "host_required"
|
||||
assert config["host_browser_privacy_policy"] == "warn"
|
||||
assert config["host_browser_profile_mode"] == "agent"
|
||||
assert (
|
||||
normalize_browser_config({"runtime_backend": "host_when_available"})["runtime_backend"]
|
||||
== "host_required"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
|
@ -188,6 +189,22 @@ def test_host_browser_privacy_detects_local_model(monkeypatch):
|
|||
assert _agent_uses_local_chat_model(_agent()) is True
|
||||
|
||||
|
||||
def test_connector_runtime_tolerates_legacy_config_module(monkeypatch):
|
||||
import plugins._browser.helpers.config as browser_config
|
||||
import plugins._browser.helpers.connector_runtime as connector_runtime_module
|
||||
|
||||
original = getattr(browser_config, "HOST_BROWSER_PROFILE_MODE_KEY", None)
|
||||
monkeypatch.delattr(browser_config, "HOST_BROWSER_PROFILE_MODE_KEY", raising=False)
|
||||
|
||||
reloaded = importlib.reload(connector_runtime_module)
|
||||
|
||||
assert reloaded.HOST_BROWSER_PROFILE_MODE_KEY == "host_browser_profile_mode"
|
||||
|
||||
if original is not None:
|
||||
monkeypatch.setattr(browser_config, "HOST_BROWSER_PROFILE_MODE_KEY", original, raising=False)
|
||||
importlib.reload(connector_runtime_module)
|
||||
|
||||
|
||||
def test_host_browser_privacy_blocks_cloud_content(monkeypatch):
|
||||
import plugins._browser.helpers.connector_runtime as connector_runtime_module
|
||||
from plugins._model_config.helpers import model_config
|
||||
|
|
@ -210,7 +227,14 @@ def test_host_browser_privacy_blocks_cloud_content(monkeypatch):
|
|||
runtime._enforce_privacy({"action": "content"})
|
||||
|
||||
|
||||
def test_connector_runtime_normalizes_host_navigation_payloads():
|
||||
def test_connector_runtime_normalizes_host_navigation_payloads(monkeypatch):
|
||||
import plugins._browser.helpers.connector_runtime as connector_runtime_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
connector_runtime_module,
|
||||
"get_browser_config",
|
||||
lambda agent=None: {"host_browser_profile_mode": "existing"},
|
||||
)
|
||||
runtime = ConnectorBrowserRuntime("ctx-host", _agent("ctx-host"))
|
||||
|
||||
open_payload = runtime._payload_for_call("open", "localhost:3000")
|
||||
|
|
@ -236,6 +260,20 @@ def test_connector_runtime_normalizes_host_navigation_payloads():
|
|||
assert multi_payload["calls"][1]["url"] == "http://127.0.0.1:8000/path"
|
||||
assert multi_payload["calls"][2]["calls"][0]["url"] == "https://nested.example/"
|
||||
assert multi_payload["calls"][3] == {"action": "content", "browser_id": 1}
|
||||
assert open_payload["profile_mode"] == "existing"
|
||||
|
||||
|
||||
def test_connector_runtime_forwards_host_profile_mode(monkeypatch):
|
||||
import plugins._browser.helpers.connector_runtime as connector_runtime_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
connector_runtime_module,
|
||||
"get_browser_config",
|
||||
lambda agent=None: {"host_browser_profile_mode": "agent"},
|
||||
)
|
||||
runtime = ConnectorBrowserRuntime("ctx-host", _agent("ctx-host"))
|
||||
|
||||
assert runtime._payload_for_call("open", "example.com")["profile_mode"] == "agent"
|
||||
|
||||
|
||||
def test_host_browser_artifacts_materialize_inside_multi_results(monkeypatch, tmp_path):
|
||||
|
|
@ -367,6 +405,7 @@ def test_connector_runtime_ensures_preparable_host_browser_before_action(monkeyp
|
|||
|
||||
assert result == {"id": 1, "state": {"runtime": "host"}}
|
||||
assert [payload["action"] for payload in emitted] == ["ensure", "open"]
|
||||
assert [payload["profile_mode"] for payload in emitted] == ["existing", "existing"]
|
||||
assert "__spaceBrowserPageContent__" in emitted[0]["content_helper"]["source"]
|
||||
assert "capture" in emitted[0]["content_helper"]["required_apis"]
|
||||
assert emitted[0]["content_helper"]["sha256"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue