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