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.
Alessandro committed
May 9, 2026 at 16:25 UTC
0a8aaee9acfc693817e2e693d5974ac346d9196e
8 files changed
+145
-8
plugins/_a0_connector/api/v1/browser_runtime.py
+30
-3
@@ -6,7 +6,7 @@ import plugins._a0_connector.api.v1.base as connector_base
6
7
8
_PRIVACY_NOTICE = (
9
- "For GDPR/content policy, visit Agent Zero WebUI > Browser settings to choose "
9
+ "For Browser model-use settings, visit Agent Zero WebUI > Browser settings to choose "
10
"Local models only, Warn when using cloud, or Allow."
11
)
12
@@ -24,6 +24,15 @@ def _normalize_requested_backend(value: object) -> str:
24
return ""
25
26
27
+def _normalize_profile_mode(value: object) -> str:
28
+ normalized = _string(value).lower().replace("-", "_").replace(" ", "_")
29
+ if normalized in {"agent", "clean", "clean_agent", "a0", "dedicated"}:
30
+ return "agent"
31
+ if normalized in {"existing", "user", "personal", "current"}:
32
+ return "existing"
33
+ return ""
34
+
35
+
36
def _runtime_label(value: str) -> str:
37
if value == "host_required":
38
return "Bring Your Own Browser"
@@ -59,12 +68,30 @@ class BrowserRuntime(connector_base.ProtectedConnectorApiHandler):
68
mimetype="application/json",
69
)
70
settings["runtime_backend"] = runtime_backend
71
+ if "host_browser_profile_mode" in input or "profile_mode" in input:
72
+ profile_mode = _normalize_profile_mode(
73
+ input.get("host_browser_profile_mode", input.get("profile_mode"))
74
+ )
75
+ if not profile_mode:
76
+ return Response(
77
+ response='{"error":"host_browser_profile_mode must be existing or agent"}',
78
+ status=400,
79
+ mimetype="application/json",
80
+ )
81
+ settings["host_browser_profile_mode"] = profile_mode
82
+ settings["host_browser_profile_mode"] = (
83
+ _normalize_profile_mode(settings.get("host_browser_profile_mode")) or "existing"
84
+ )
85
self._save_browser_config(project_name, settings)
86
87
+ runtime_backend = settings.get("runtime_backend") or "container"
88
+ profile_mode = _normalize_profile_mode(settings.get("host_browser_profile_mode")) or "existing"
89
+
90
return {
91
"ok": True,
66
- "runtime_backend": settings["runtime_backend"],
67
- "label": _runtime_label(settings["runtime_backend"]),
92
+ "runtime_backend": runtime_backend,
93
+ "host_browser_profile_mode": profile_mode,
94
+ "label": _runtime_label(runtime_backend),
95
"project_name": project_name,
96
"agent_profile": "",
97
"privacy_notice": _PRIVACY_NOTICE,
plugins/_browser/default_config.yaml
+5
@@ -19,6 +19,11 @@ runtime_backend: "container"
19
# - allow: allow without warning.
20
host_browser_privacy_policy: "enforce_local"
21
22
+# Host-browser profile preference:
23
+# - existing: use the user's authorized existing browser profile when available.
24
+# - agent: use a clean A0-controlled browser profile on the host.
25
+host_browser_profile_mode: "existing"
26
+
27
# Optional _model_config preset used by Browser-owned model helpers.
28
# Empty uses the effective Main Model.
29
model_preset: ""
plugins/_browser/helpers/config.py
+7
@@ -13,8 +13,10 @@ DEFAULT_HOMEPAGE_KEY = "default_homepage"
13
AUTOFOCUS_ACTIVE_PAGE_KEY = "autofocus_active_page"
14
RUNTIME_BACKEND_KEY = "runtime_backend"
15
HOST_BROWSER_PRIVACY_POLICY_KEY = "host_browser_privacy_policy"
16
+HOST_BROWSER_PROFILE_MODE_KEY = "host_browser_profile_mode"
17
RUNTIME_BACKENDS = {"container", "host_required"}
18
HOST_BROWSER_PRIVACY_POLICIES = {"enforce_local", "warn", "allow"}
19
+HOST_BROWSER_PROFILE_MODES = {"existing", "agent"}
20
BASE_BROWSER_ARGS = [
21
"--no-sandbox",
22
"--disable-dev-shm-usage",
@@ -110,6 +112,11 @@ def normalize_browser_config(settings: dict[str, Any] | None) -> dict[str, Any]:
112
allowed=HOST_BROWSER_PRIVACY_POLICIES,
113
default="enforce_local",
114
),
115
+ HOST_BROWSER_PROFILE_MODE_KEY: _normalize_choice(
116
+ raw.get(HOST_BROWSER_PROFILE_MODE_KEY, "existing"),
117
+ allowed=HOST_BROWSER_PROFILE_MODES,
118
+ default="existing",
119
+ ),
120
MODEL_PRESET_KEY: _normalize_model_preset(raw.get(MODEL_PRESET_KEY, "")),
121
}
122
plugins/_browser/helpers/connector_runtime.py
+20
-4
@@ -34,10 +34,7 @@ from plugins._a0_connector.helpers.ws_runtime import (
34
select_host_browser_target_sid,
35
store_pending_browser_op,
36
)
37
-from plugins._browser.helpers.config import (
38
- HOST_BROWSER_PRIVACY_POLICY_KEY,
39
- get_browser_config,
40
-)
37
+from plugins._browser.helpers import config as browser_config
38
from plugins._browser.helpers.url import normalize_url
39
40
@@ -47,6 +44,17 @@ HOST_BROWSER_SCREENSHOT_DIR = ("tmp", "browser", "host-screenshots")
44
CONTENT_HELPER_PATH = Path(__file__).resolve().parents[1] / "assets" / "browser-page-content.js"
45
MAX_ARTIFACT_SIZE_BYTES = 25 * 1024 * 1024
46
BASE64_DECODE_CHARS_PER_CHUNK = 64 * 1024
47
+HOST_BROWSER_PRIVACY_POLICY_KEY = getattr(
48
+ browser_config,
49
+ "HOST_BROWSER_PRIVACY_POLICY_KEY",
50
+ "host_browser_privacy_policy",
51
+)
52
+HOST_BROWSER_PROFILE_MODE_KEY = getattr(
53
+ browser_config,
54
+ "HOST_BROWSER_PROFILE_MODE_KEY",
55
+ "host_browser_profile_mode",
56
+)
57
+get_browser_config = browser_config.get_browser_config
58
_LOCAL_PROVIDERS = {"ollama", "lm_studio"}
59
_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1", "host.docker.internal"}
60
_SENSITIVE_ACTIONS = {"content", "detail", "evaluate", "screenshot", "screenshot_file"}
@@ -79,6 +87,7 @@ class ConnectorBrowserRuntime:
87
"op_id": str(uuid.uuid4()),
88
"context_id": self.context_id,
89
"action": action,
90
+ "profile_mode": self._host_browser_profile_mode(),
91
}
92
93
if action == "open":
@@ -188,6 +197,7 @@ class ConnectorBrowserRuntime:
197
return normalized_calls
198
199
async def _dispatch(self, payload: dict[str, Any]) -> Any:
200
+ payload.setdefault("profile_mode", self._host_browser_profile_mode())
201
self._enforce_privacy(payload)
202
sid = self._select_sid()
203
if not sid:
@@ -207,6 +217,7 @@ class ConnectorBrowserRuntime:
217
"op_id": str(uuid.uuid4()),
218
"context_id": self.context_id,
219
"action": "ensure",
220
+ "profile_mode": self._host_browser_profile_mode(),
221
},
222
),
223
)
@@ -214,6 +225,11 @@ class ConnectorBrowserRuntime:
225
226
return await self._send_browser_op(sid, self._with_content_helper(sid, payload))
227
228
+ def _host_browser_profile_mode(self) -> str:
229
+ config = get_browser_config(self.agent)
230
+ mode = str(config.get(HOST_BROWSER_PROFILE_MODE_KEY) or "existing").strip().lower()
231
+ return "agent" if mode == "agent" else "existing"
232
+
233
def _with_content_helper(self, sid: str, payload: dict[str, Any]) -> dict[str, Any]:
234
metadata = host_browser_metadata_for_sid(sid) or {}
235
if str(metadata.get("content_helper_sha256") or "").strip().lower() == _content_helper_sha256():
plugins/_browser/webui/browser-config-store.js
+12
@@ -5,6 +5,7 @@ const BROWSER_EXTENSIONS_API = "/plugins/_browser/extensions";
5
const BROWSER_STATUS_API = "/plugins/_browser/status";
6
const RUNTIME_BACKENDS = new Set(["container", "host_required"]);
7
const HOST_PRIVACY_POLICIES = new Set(["enforce_local", "warn", "allow"]);
8
+const HOST_PROFILE_MODES = new Set(["existing", "agent"]);
9
10
function normalizePathList(value) {
11
const source = Array.isArray(value)
@@ -32,6 +33,11 @@ function ensureConfig(config) {
33
HOST_PRIVACY_POLICIES,
34
"enforce_local",
35
);
36
+ config.host_browser_profile_mode = normalizeChoice(
37
+ config.host_browser_profile_mode,
38
+ HOST_PROFILE_MODES,
39
+ "existing",
40
+ );
41
config.model_preset = String(config.model_preset || "").trim();
42
delete config.model;
43
return config;
@@ -139,6 +145,12 @@ export const store = createStore("browserConfig", {
145
return "Local Models Only";
146
},
147
148
+ hostBrowserProfileModeLabel() {
149
+ const value = this.config?.host_browser_profile_mode || "existing";
150
+ if (value === "agent") return "Clean Agent Profile";
151
+ return "Existing Browser Profile";
152
+ },
153
+
154
async loadHostBrowserStatus() {
155
if (this.hostBrowserStatusLoading) return;
156
this.hostBrowserStatusLoading = true;
plugins/_browser/webui/config.html
+28
@@ -35,6 +35,34 @@
35
</span>
36
</label>
37
38
+ <label
39
+ class="browser-config-field"
40
+ x-show="$store.browserConfig.config.runtime_backend === 'host_required'"
41
+ >
42
+ <span class="browser-config-field-label">Host browser profile</span>
43
+ <select x-model="$store.browserConfig.config.host_browser_profile_mode">
44
+ <option value="existing">Existing browser profile</option>
45
+ <option value="agent">Clean Agent profile</option>
46
+ </select>
47
+ <span
48
+ class="browser-config-field-help"
49
+ x-show="$store.browserConfig.config.host_browser_profile_mode === 'agent'"
50
+ >
51
+ Uses a separate A0-controlled local profile on this computer.
52
+ </span>
53
+ </label>
54
+
55
+ <div
56
+ class="browser-config-warning"
57
+ x-show="$store.browserConfig.config.runtime_backend === 'host_required' && $store.browserConfig.config.host_browser_profile_mode !== 'agent'"
58
+ >
59
+ <span class="material-symbols-outlined">warning</span>
60
+ <span>
61
+ Existing profile lets the agent interact with the browser instance you authorize,
62
+ including signed-in sites, cookies, tabs, downloads, and page content.
63
+ </span>
64
+ </div>
65
+
66
<label class="browser-config-field">
67
<span class="browser-config-field-label">Page content access</span>
68
<select x-model="$store.browserConfig.config.host_browser_privacy_policy">
tests/test_browser_agent_regressions.py
+3
@@ -155,6 +155,7 @@ def test_browser_config_normalizes_extension_paths(tmp_path):
155
"autofocus_active_page": True,
156
"runtime_backend": "container",
157
"host_browser_privacy_policy": "enforce_local",
158
+ "host_browser_profile_mode": "existing",
159
"model_preset": "",
160
}
161
@@ -169,11 +170,13 @@ def test_browser_config_normalizes_host_backend_and_privacy_policy():
170
{
171
"runtime_backend": "host-required",
172
"host_browser_privacy_policy": "warn",
173
+ "host_browser_profile_mode": "agent",
174
}
175
)
176
177
assert config["runtime_backend"] == "host_required"
178
assert config["host_browser_privacy_policy"] == "warn"
179
+ assert config["host_browser_profile_mode"] == "agent"
180
assert (
181
normalize_browser_config({"runtime_backend": "host_when_available"})["runtime_backend"]
182
== "host_required"
tests/test_host_browser_connector.py
+40
-1
@@ -1,6 +1,7 @@
1
from __future__ import annotations
2
3
import asyncio
4
+import importlib
5
import sys
6
from pathlib import Path
7
from types import SimpleNamespace
@@ -188,6 +189,22 @@ def test_host_browser_privacy_detects_local_model(monkeypatch):
189
assert _agent_uses_local_chat_model(_agent()) is True
190
191
192
+def test_connector_runtime_tolerates_legacy_config_module(monkeypatch):
193
+ import plugins._browser.helpers.config as browser_config
194
+ import plugins._browser.helpers.connector_runtime as connector_runtime_module
195
+
196
+ original = getattr(browser_config, "HOST_BROWSER_PROFILE_MODE_KEY", None)
197
+ monkeypatch.delattr(browser_config, "HOST_BROWSER_PROFILE_MODE_KEY", raising=False)
198
+
199
+ reloaded = importlib.reload(connector_runtime_module)
200
+
201
+ assert reloaded.HOST_BROWSER_PROFILE_MODE_KEY == "host_browser_profile_mode"
202
+
203
+ if original is not None:
204
+ monkeypatch.setattr(browser_config, "HOST_BROWSER_PROFILE_MODE_KEY", original, raising=False)
205
+ importlib.reload(connector_runtime_module)
206
+
207
+
208
def test_host_browser_privacy_blocks_cloud_content(monkeypatch):
209
import plugins._browser.helpers.connector_runtime as connector_runtime_module
210
from plugins._model_config.helpers import model_config
@@ -210,7 +227,14 @@ def test_host_browser_privacy_blocks_cloud_content(monkeypatch):
227
runtime._enforce_privacy({"action": "content"})
228
229
213
-def test_connector_runtime_normalizes_host_navigation_payloads():
230
+def test_connector_runtime_normalizes_host_navigation_payloads(monkeypatch):
231
+ import plugins._browser.helpers.connector_runtime as connector_runtime_module
232
+
233
+ monkeypatch.setattr(
234
+ connector_runtime_module,
235
+ "get_browser_config",
236
+ lambda agent=None: {"host_browser_profile_mode": "existing"},
237
+ )
238
runtime = ConnectorBrowserRuntime("ctx-host", _agent("ctx-host"))
239
240
open_payload = runtime._payload_for_call("open", "localhost:3000")
@@ -236,6 +260,20 @@ def test_connector_runtime_normalizes_host_navigation_payloads():
260
assert multi_payload["calls"][1]["url"] == "http://127.0.0.1:8000/path"
261
assert multi_payload["calls"][2]["calls"][0]["url"] == "https://nested.example/"
262
assert multi_payload["calls"][3] == {"action": "content", "browser_id": 1}
263
+ assert open_payload["profile_mode"] == "existing"
264
+
265
+
266
+def test_connector_runtime_forwards_host_profile_mode(monkeypatch):
267
+ import plugins._browser.helpers.connector_runtime as connector_runtime_module
268
+
269
+ monkeypatch.setattr(
270
+ connector_runtime_module,
271
+ "get_browser_config",
272
+ lambda agent=None: {"host_browser_profile_mode": "agent"},
273
+ )
274
+ runtime = ConnectorBrowserRuntime("ctx-host", _agent("ctx-host"))
275
+
276
+ assert runtime._payload_for_call("open", "example.com")["profile_mode"] == "agent"
277
278
279
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
405
406
assert result == {"id": 1, "state": {"runtime": "host"}}
407
assert [payload["action"] for payload in emitted] == ["ensure", "open"]
408
+ assert [payload["profile_mode"] for payload in emitted] == ["existing", "existing"]
409
assert "__spaceBrowserPageContent__" in emitted[0]["content_helper"]["source"]
410
assert "capture" in emitted[0]["content_helper"]["required_apis"]
411
assert emitted[0]["content_helper"]["sha256"]