Send browser helper source to host connector
Make the Browser plugin the source of truth for browser-page-content.js by attaching its source and sha256 to host-browser operations when the CLI has no matching helper hash. Store the helper hash in connector metadata and cover the routing/ensure path in tests.
Alessandro committed
May 8, 2026 at 15:23 UTC
c020f1af2888d6bd6327ce8fd714aa06cc170d72
3 files changed
+46
-6
plugins/_a0_connector/helpers/ws_runtime.py
+4
@@ -69,6 +69,7 @@ class HostBrowserMetadata:
69
profile_label: str
70
profile_path: str
71
cdp_endpoint: str
72
+ content_helper_sha256: str
73
features: tuple[str, ...]
74
support_reason: str
75
updated_at: float
@@ -371,6 +372,7 @@ def store_sid_host_browser_metadata(sid: str, payload: dict[str, Any]) -> HostBr
372
profile_label=str(payload.get("profile_label", "") or "").strip(),
373
profile_path=str(payload.get("profile_path", "") or "").strip(),
374
cdp_endpoint=str(payload.get("cdp_endpoint", "") or "").strip(),
375
+ content_helper_sha256=str(payload.get("content_helper_sha256", "") or "").strip().lower(),
376
features=features,
377
support_reason=support_reason,
378
updated_at=time.time(),
@@ -418,6 +420,7 @@ def host_browser_metadata_for_sid(sid: str) -> dict[str, Any] | None:
420
"profile_label": metadata.profile_label,
421
"profile_path": metadata.profile_path,
422
"cdp_endpoint": metadata.cdp_endpoint,
423
+ "content_helper_sha256": metadata.content_helper_sha256,
424
"features": list(metadata.features),
425
"support_reason": metadata.support_reason,
426
"updated_at": metadata.updated_at,
@@ -484,6 +487,7 @@ def all_host_browser_metadata() -> list[dict[str, Any]]:
487
"browser_family": metadata.browser_family,
488
"profile_label": metadata.profile_label,
489
"profile_path": metadata.profile_path,
490
+ "content_helper_sha256": metadata.content_helper_sha256,
491
"features": list(metadata.features),
492
"support_reason": metadata.support_reason,
493
"updated_at": metadata.updated_at,
plugins/_browser/helpers/connector_runtime.py
+38
-6
@@ -2,7 +2,9 @@ from __future__ import annotations
2
3
import asyncio
4
import base64
5
+import hashlib
6
import uuid
7
+from functools import lru_cache
8
from pathlib import Path
9
from typing import Any
10
from urllib.parse import urlparse
@@ -40,6 +42,7 @@ from plugins._browser.helpers.config import (
42
BROWSER_OP_EVENT = "connector_browser_op"
43
BROWSER_OP_TIMEOUT = 120.0
44
HOST_BROWSER_SCREENSHOT_DIR = ("tmp", "browser", "host-screenshots")
45
+CONTENT_HELPER_PATH = Path(__file__).resolve().parents[1] / "assets" / "browser-page-content.js"
46
MAX_ARTIFACT_SIZE_BYTES = 25 * 1024 * 1024
47
BASE64_DECODE_CHARS_PER_CHUNK = 64 * 1024
48
_LOCAL_PROVIDERS = {"ollama", "lm_studio"}
@@ -167,15 +170,26 @@ class ConnectorBrowserRuntime:
170
if self._needs_prepare(sid, payload):
171
await self._send_browser_op(
172
sid,
170
- {
171
- "op_id": str(uuid.uuid4()),
172
- "context_id": self.context_id,
173
- "action": "ensure",
174
- },
173
+ self._with_content_helper(
174
+ sid,
175
+ {
176
+ "op_id": str(uuid.uuid4()),
177
+ "context_id": self.context_id,
178
+ "action": "ensure",
179
+ },
180
+ ),
181
)
182
sid = self._select_sid() or sid
183
178
- return await self._send_browser_op(sid, payload)
184
+ return await self._send_browser_op(sid, self._with_content_helper(sid, payload))
185
+
186
+ def _with_content_helper(self, sid: str, payload: dict[str, Any]) -> dict[str, Any]:
187
+ metadata = host_browser_metadata_for_sid(sid) or {}
188
+ if str(metadata.get("content_helper_sha256") or "").strip().lower() == _content_helper_sha256():
189
+ return payload
190
+ payload = dict(payload)
191
+ payload["content_helper"] = _content_helper_payload()
192
+ return payload
193
194
async def _send_browser_op(self, sid: str, payload: dict[str, Any]) -> Any:
195
op_id = str(payload["op_id"])
@@ -335,6 +349,24 @@ class ConnectorBrowserRuntime:
349
return "; ".join(parts)
350
351
352
+@lru_cache(maxsize=1)
353
+def _content_helper_payload() -> dict[str, str]:
354
+ try:
355
+ source = CONTENT_HELPER_PATH.read_text(encoding="utf-8")
356
+ except OSError as exc:
357
+ raise RuntimeError(
358
+ f"Host-browser content helper could not be read from {CONTENT_HELPER_PATH}: {exc}"
359
+ ) from exc
360
+ return {
361
+ "source": source,
362
+ "sha256": hashlib.sha256(source.encode("utf-8")).hexdigest(),
363
+ }
364
+
365
+
366
+def _content_helper_sha256() -> str:
367
+ return _content_helper_payload()["sha256"]
368
+
369
+
370
def _agent_uses_local_chat_model(agent: Any) -> bool:
371
try:
372
from plugins._model_config.helpers import model_config
tests/test_host_browser_connector.py
+4
@@ -36,6 +36,7 @@ def test_host_browser_metadata_selection_is_context_scoped():
36
"status": "ready",
37
"browser_family": "chrome",
38
"profile_label": "Default",
39
+ "content_helper_sha256": "abc123",
40
"features": ["open", "content"],
41
},
42
)
@@ -44,6 +45,7 @@ def test_host_browser_metadata_selection_is_context_scoped():
45
rows = ws_runtime.host_browser_metadata_for_context(context_id)
46
assert rows[0]["browser_family"] == "chrome"
47
assert rows[0]["enabled"] is True
48
+ assert rows[0]["content_helper_sha256"] == "abc123"
49
finally:
50
ws_runtime.unregister_sid(sid)
51
@@ -323,6 +325,8 @@ def test_connector_runtime_ensures_preparable_host_browser_before_action(monkeyp
325
326
assert result == {"id": 1, "state": {"runtime": "host"}}
327
assert [payload["action"] for payload in emitted] == ["ensure", "open"]
328
+ assert "__spaceBrowserPageContent__" in emitted[0]["content_helper"]["source"]
329
+ assert emitted[0]["content_helper"]["sha256"]
330
finally:
331
ws_runtime.unregister_sid(sid)
332