Refine BYOB host browser endpoint selection

Reduce Browser Settings host-browser choices to automatic and advertised debug endpoints, with a validated Custom endpoint input and faster inventory refresh.

Update Browser docs and regressions to document the custom endpoint workflow and CLI websocket selection.
This commit is contained in:
Alessandro 2026-07-05 19:59:30 +02:00
parent c719134537
commit afa5231a5a
6 changed files with 133 additions and 10 deletions

View file

@ -162,8 +162,9 @@ control starts when Agent Zero actually needs to use the browser.
> control.
The **Host browser** list in Browser settings comes from the connected local A0
CLI, not from the Agent Zero Web UI server. If a newly authorized browser does
not appear, restart or reconnect A0 CLI.
CLI, not from the Agent Zero Web UI server. It shows Automatic, currently
advertised debug endpoints, and **Custom endpoint**. If a newly authorized
browser does not appear, restart or reconnect A0 CLI.
If the inspect checkbox is not enough for your browser build, launch it with an
explicit remote debugging port and a separate profile:
@ -172,7 +173,8 @@ explicit remote debugging port and a separate profile:
opera --remote-debugging-port=9222 --user-data-dir="$HOME/.config/a0-opera-debug"
```
Then pass the full DevTools websocket endpoint to A0 CLI:
Then choose **Custom endpoint** in Browser settings, run `/browser ws://...` in
A0 CLI, or pass the full DevTools websocket endpoint to A0 CLI:
```bash
export A0_HOST_BROWSER_REMOTE_DEBUGGING_ENDPOINTS="ws://127.0.0.1:9222/devtools/browser/..."

View file

@ -156,7 +156,8 @@ Remote debugging pages:
| Chrome, Edge, Brave, Vivaldi, Chromium | `chrome://inspect/#remote-debugging` |
| Opera | `opera://inspect/#remote-debugging` |
If a browser does not appear in the **Host browser** list after enabling remote
The **Host browser** list shows Automatic, currently advertised debug endpoints,
and **Custom endpoint**. If a browser does not appear after enabling remote
debugging, restart or reconnect the local A0 CLI. Restarting only the Agent Zero
Web UI server does not refresh the browser inventory; the list comes from the
connected CLI.
@ -168,7 +169,8 @@ directory:
opera --remote-debugging-port=9222 --user-data-dir="$HOME/.config/a0-opera-debug"
```
Then pass the full DevTools websocket endpoint to the CLI:
Then choose **Custom endpoint** in Browser settings, or pass the full DevTools
websocket endpoint to the CLI:
```bash
export A0_HOST_BROWSER_REMOTE_DEBUGGING_ENDPOINTS="ws://127.0.0.1:9222/devtools/browser/..."

View file

@ -26,6 +26,7 @@
- Keep narrow WebUI Browser controls usable by grouping navigation with Annotate/settings above a full-width address bar.
- For Bring Your Own Browser with an existing host profile, `host_browser_selection` may target automatic CLI selection, a browser family/id, or an explicit CDP endpoint and must be forwarded to the connector runtime as `browser_selection`.
- Browser Settings must refresh connected A0 CLI host-browser inventory while the settings view is open so newly authorized endpoints appear without saving or reopening.
- Browser Settings keeps the Host browser dropdown focused on automatic selection, advertised debug endpoints, and a validated Custom endpoint field instead of listing every installed local profile.
- Browser URL-intent handling must only claim web URL schemes and leave custom Agent Zero schemes to their owning surfaces.
- Prefer DOM/CDP browser actions with refs, selectors, frame-chain refs, and screenshots over viewport coordinate input. Coordinates remain a visual fallback.
- Do not hardcode user-specific browser paths or secrets.

View file

@ -10,7 +10,8 @@ const HOST_PROFILE_MODES = new Set(["existing", "agent"]);
const DEFAULT_MAX_OPEN_TABS = 32;
const MIN_MAX_OPEN_TABS = 1;
const HARD_MAX_OPEN_TABS = 50;
const HOST_BROWSER_STATUS_REFRESH_MS = 3000;
const HOST_BROWSER_STATUS_REFRESH_MS = 1000;
const CUSTOM_HOST_BROWSER_SELECTION = "__custom_endpoint__";
function normalizePathList(value) {
const source = Array.isArray(value)
@ -72,6 +73,39 @@ function normalizeHostBrowserSelection(value) {
return String(value || "").trim().toLowerCase().replace(/\s+/g, "_").slice(0, 200);
}
function normalizeCustomHostBrowserEndpoint(value) {
const raw = String(value || "").trim();
if (!raw) return "";
const candidate = raw.includes("://") ? raw : `ws://${raw}`;
try {
const url = new URL(candidate);
if (!["ws:", "wss:"].includes(url.protocol) || !url.host || !url.pathname.startsWith("/devtools/browser/")) {
return "";
}
return normalizeHostBrowserSelection(`${url.protocol}//${url.host}${url.pathname}${url.search || ""}`);
} catch (_error) {
return "";
}
}
function isCustomHostBrowserEndpoint(value) {
return Boolean(normalizeCustomHostBrowserEndpoint(value));
}
function debugPortVersionUrl(value) {
const raw = String(value || "").trim();
if (!raw) return "";
const candidate = raw.includes("://") ? raw : `ws://${raw}`;
try {
const url = new URL(candidate);
if (!["ws:", "wss:"].includes(url.protocol) || !url.host) return "";
if (url.pathname && url.pathname !== "/") return "";
return `http://${url.host}/json/version`;
} catch (_error) {
return "";
}
}
function normalizeBoolean(value, fallback = true) {
if (value === undefined || value === null || value === "") return fallback;
if (typeof value === "boolean") return value;
@ -121,6 +155,8 @@ export const store = createStore("browserConfig", {
hostBrowserStatus: null,
hostBrowserStatusLoading: false,
hostBrowserStatusRefreshTimer: null,
hostBrowserCustomEndpoint: "",
hostBrowserCustomMode: false,
async init(config) {
this.bindConfig(config);
@ -137,6 +173,8 @@ export const store = createStore("browserConfig", {
this.extensionDeleteLoadingPath = "";
this.hostBrowserStatus = null;
this.hostBrowserStatusLoading = false;
this.hostBrowserCustomEndpoint = "";
this.hostBrowserCustomMode = false;
},
startHostBrowserStatusRefresh() {
@ -158,6 +196,9 @@ export const store = createStore("browserConfig", {
if (!safeConfig) return;
if (this.config === safeConfig) return;
this.config = safeConfig;
if (isCustomHostBrowserEndpoint(safeConfig.host_browser_selection)) {
this.hostBrowserCustomEndpoint = safeConfig.host_browser_selection;
}
},
setAutofocusActivePage(enabled) {
@ -215,29 +256,82 @@ export const store = createStore("browserConfig", {
? connector.available_browsers
: [];
for (const browser of advertised) {
const value = normalizeHostBrowserSelection(browser?.id || browser?.family || browser?.cdp_endpoint);
const value = normalizeCustomHostBrowserEndpoint(browser?.cdp_endpoint || browser?.id);
if (!value || seen.has(value)) continue;
seen.add(value);
const label = browser?.label || hostBrowserFamilyLabel(browser?.family || value);
const status = browser?.status ? ` - ${hostBrowserStatusLabel(browser.status)}` : "";
options.push({ value, label: `${label}${status}` });
}
const fallbackValue = normalizeHostBrowserSelection(connector?.browser_id || connector?.browser_family);
const fallbackValue = normalizeCustomHostBrowserEndpoint(connector?.cdp_endpoint || connector?.browser_id);
if (fallbackValue && !seen.has(fallbackValue)) {
seen.add(fallbackValue);
const label = connector?.browser_label || hostBrowserFamilyLabel(connector?.browser_family || fallbackValue);
options.push({ value: fallbackValue, label });
}
}
const selected = normalizeHostBrowserSelection(this.config?.host_browser_selection);
if (selected && !seen.has(selected) && !isCustomHostBrowserEndpoint(selected)) {
seen.add(selected);
options.push({ value: selected, label: `Saved: ${selected}` });
}
options.push({ value: CUSTOM_HOST_BROWSER_SELECTION, label: "Custom endpoint" });
return options;
},
hostBrowserSelectValue() {
if (this.hostBrowserCustomMode) return CUSTOM_HOST_BROWSER_SELECTION;
const selected = normalizeHostBrowserSelection(this.config?.host_browser_selection);
if (!selected) return "";
if (this.hostBrowserOptions().some((option) => option.value === selected)) return selected;
if (isCustomHostBrowserEndpoint(selected)) return CUSTOM_HOST_BROWSER_SELECTION;
return selected;
},
setHostBrowserSelection(value) {
const safeConfig = ensureConfig(this.config);
if (!safeConfig) return;
if (value === CUSTOM_HOST_BROWSER_SELECTION) {
this.hostBrowserCustomMode = true;
if (isCustomHostBrowserEndpoint(safeConfig.host_browser_selection)) {
this.hostBrowserCustomEndpoint = safeConfig.host_browser_selection;
} else {
safeConfig.host_browser_selection = "";
}
return;
}
this.hostBrowserCustomMode = false;
safeConfig.host_browser_selection = normalizeHostBrowserSelection(value);
},
showCustomHostBrowserEndpoint() {
return this.hostBrowserSelectValue() === CUSTOM_HOST_BROWSER_SELECTION;
},
setCustomHostBrowserEndpoint(value) {
this.hostBrowserCustomMode = true;
this.hostBrowserCustomEndpoint = String(value || "").trim();
const safeConfig = ensureConfig(this.config);
if (!safeConfig) return;
const endpoint = normalizeCustomHostBrowserEndpoint(this.hostBrowserCustomEndpoint);
if (endpoint || !this.hostBrowserCustomEndpoint) {
safeConfig.host_browser_selection = endpoint;
}
},
customHostBrowserEndpointDiagnostic() {
if (!this.hostBrowserCustomEndpoint) {
return "Paste a ws://.../devtools/browser/... endpoint from the browser inspect page.";
}
const endpoint = normalizeCustomHostBrowserEndpoint(this.hostBrowserCustomEndpoint);
if (endpoint) return `Using ${endpoint}`;
const versionUrl = debugPortVersionUrl(this.hostBrowserCustomEndpoint);
if (versionUrl) {
return `This looks like a debug port. Open ${versionUrl} and copy webSocketDebuggerUrl.`;
}
return "Endpoint must be a ws:// or wss:// URL ending in /devtools/browser/...";
},
hostBrowserProfileModeLabel() {
const value = this.config?.host_browser_profile_mode || "existing";
if (value === "agent") return "Clean Agent Profile";

View file

@ -72,14 +72,32 @@
>
<span class="browser-config-field-label">Host browser</span>
<select
:value="$store.browserConfig.config.host_browser_selection || ''"
:value="$store.browserConfig.hostBrowserSelectValue()"
@focus="$store.browserConfig.loadHostBrowserStatus()"
@click="$store.browserConfig.loadHostBrowserStatus()"
@change="$store.browserConfig.setHostBrowserSelection($event.target.value)"
>
<template x-for="option in $store.browserConfig.hostBrowserOptions()" :key="option.value">
<option :value="option.value" x-text="option.label"></option>
</template>
</select>
<span class="browser-config-field-help">Choose which installed browser/debug endpoint A0 CLI should use when multiple are available.</span>
<span class="browser-config-field-help">Choose an allowed debug endpoint, or leave Automatic so A0 CLI picks.</span>
</label>
<label
class="browser-config-field"
x-show="$store.browserConfig.config.runtime_backend === 'host_required' && $store.browserConfig.config.host_browser_profile_mode !== 'agent' && $store.browserConfig.showCustomHostBrowserEndpoint()"
>
<span class="browser-config-field-label">Custom endpoint</span>
<input
type="text"
:value="$store.browserConfig.hostBrowserCustomEndpoint"
@focus="$store.browserConfig.loadHostBrowserStatus()"
@input="$store.browserConfig.setCustomHostBrowserEndpoint($event.target.value)"
placeholder="ws://127.0.0.1:9222/devtools/browser/..."
autocomplete="off"
/>
<span class="browser-config-field-help" x-text="$store.browserConfig.customHostBrowserEndpointDiagnostic()"></span>
</label>
<div

View file

@ -981,6 +981,9 @@ def test_browser_tool_does_not_auto_open_canvas_policy_is_documented():
config_html = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "config.html").read_text(
encoding="utf-8"
)
config_store_js = (
PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-config-store.js"
).read_text(encoding="utf-8")
assert "must not open a Browser surface automatically" in prompt
assert "Use the tool headlessly unless the user opens the Browser surface" in prompt
@ -1002,6 +1005,9 @@ def test_browser_tool_does_not_auto_open_canvas_policy_is_documented():
assert "chrome://inspect/#remote-debugging" in config_html
assert "opera://inspect/#remote-debugging" in config_html
assert "A0_HOST_BROWSER_REMOTE_DEBUGGING_ENDPOINTS" in config_html
assert "Custom endpoint" in config_html
assert "customHostBrowserEndpointDiagnostic" in config_store_js
assert "HOST_BROWSER_STATUS_REFRESH_MS = 1000" in config_store_js
def test_browser_skills_are_plugin_owned_and_progressively_linked():