refactor(tools): browser_tool — data-driven tool table, guarded-action helper, compact re-export blocks, _pid_exists delegates to gateway.status
This commit is contained in:
+89
-195
@@ -102,9 +102,7 @@ from agent.browser_registry import ( # noqa: F401 (test-patchable surface)
|
||||
get_provider as _registry_get_browser_provider,
|
||||
)
|
||||
try:
|
||||
from agent.browser_registry import (
|
||||
registry_generation as _browser_registry_generation,
|
||||
)
|
||||
from agent.browser_registry import registry_generation as _browser_registry_generation
|
||||
except ImportError:
|
||||
# Isolated compat tests install a minimal ``agent.browser_registry`` stub
|
||||
# with only ``get_provider``; no mutable registry → constant generation.
|
||||
@@ -151,24 +149,12 @@ _SANE_PATH = os.pathsep.join(_SANE_PATH_DIRS)
|
||||
|
||||
|
||||
from tools.browser_tool_install import ( # noqa: F401 (re-exported; tests patch tools.browser_tool.<name>)
|
||||
_discover_homebrew_node_dirs,
|
||||
_browser_candidate_path_dirs,
|
||||
_merge_browser_path,
|
||||
_browser_install_hint,
|
||||
_is_npx_agent_browser_sentinel,
|
||||
_requires_real_termux_browser_install,
|
||||
_termux_browser_install_error,
|
||||
_agent_browser_candidate_present,
|
||||
_resolve_npx_bin,
|
||||
_agent_browser_candidates,
|
||||
_find_agent_browser,
|
||||
warm_agent_browser_npx_cache,
|
||||
_chromium_search_roots,
|
||||
_chromium_installed,
|
||||
_maybe_autoinstall_chromium,
|
||||
_running_in_docker,
|
||||
check_browser_requirements,
|
||||
check_browser_vision_requirements,
|
||||
_discover_homebrew_node_dirs, _browser_candidate_path_dirs, _merge_browser_path,
|
||||
_browser_install_hint, _is_npx_agent_browser_sentinel, _requires_real_termux_browser_install,
|
||||
_termux_browser_install_error, _agent_browser_candidate_present, _resolve_npx_bin,
|
||||
_agent_browser_candidates, _find_agent_browser, warm_agent_browser_npx_cache,
|
||||
_chromium_search_roots, _chromium_installed, _maybe_autoinstall_chromium, _running_in_docker,
|
||||
check_browser_requirements, check_browser_vision_requirements,
|
||||
)
|
||||
|
||||
# Throttle screenshot cleanup to avoid repeated full directory scans.
|
||||
@@ -269,27 +255,13 @@ def _get_open_command_timeout(*, first_open: bool = False) -> int:
|
||||
|
||||
|
||||
from tools.browser_tool_session import ( # noqa: F401 (re-exported; tests patch tools.browser_tool.<name>)
|
||||
_needs_chromium_sandbox_bypass,
|
||||
_apply_chromium_sandbox_args,
|
||||
_read_command_output_files,
|
||||
_unlink_command_output_files,
|
||||
_format_browser_timeout_error,
|
||||
_agent_browser_argv,
|
||||
_prepare_session_socket_dir,
|
||||
_agent_browser_command_env,
|
||||
_popen_agent_browser,
|
||||
_create_local_session,
|
||||
_create_lightpanda_session,
|
||||
_local_backend_process_dead,
|
||||
_create_cdp_session,
|
||||
_create_cloud_session_or_fallback,
|
||||
_create_session_for_key,
|
||||
_get_session_info,
|
||||
_discard_timed_out_browser_session,
|
||||
_read_browser_daemon_pid,
|
||||
_browser_daemon_responsive,
|
||||
_handle_browser_command_timeout,
|
||||
_interpret_browser_command_output,
|
||||
_needs_chromium_sandbox_bypass, _apply_chromium_sandbox_args, _read_command_output_files,
|
||||
_unlink_command_output_files, _format_browser_timeout_error, _agent_browser_argv,
|
||||
_prepare_session_socket_dir, _agent_browser_command_env, _popen_agent_browser,
|
||||
_create_local_session, _create_lightpanda_session, _local_backend_process_dead,
|
||||
_create_cdp_session, _create_cloud_session_or_fallback, _create_session_for_key,
|
||||
_get_session_info, _discard_timed_out_browser_session, _read_browser_daemon_pid,
|
||||
_browser_daemon_responsive, _handle_browser_command_timeout, _interpret_browser_command_output,
|
||||
_run_browser_command,
|
||||
)
|
||||
|
||||
@@ -300,12 +272,8 @@ def _get_vision_model() -> Optional[str]:
|
||||
|
||||
|
||||
from tools.browser_tool_cdp import ( # noqa: F401 (re-exported; tests patch tools.browser_tool.<name>)
|
||||
_resolve_cdp_override,
|
||||
_get_cdp_override_raw,
|
||||
_get_cdp_override,
|
||||
_get_dialog_policy_config,
|
||||
_ensure_cdp_supervisor,
|
||||
_stop_cdp_supervisor,
|
||||
_resolve_cdp_override, _get_cdp_override_raw, _get_cdp_override, _get_dialog_policy_config,
|
||||
_ensure_cdp_supervisor, _stop_cdp_supervisor,
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -324,9 +292,7 @@ _DEFAULT_PROVIDER_REGISTRY: Dict[str, type] = dict(_PROVIDER_REGISTRY)
|
||||
_cached_cloud_provider: Optional[CloudBrowserProvider] = None
|
||||
_cloud_provider_resolved = False
|
||||
_cached_cloud_provider_scope: Optional[str] = None
|
||||
_cached_cloud_providers: Dict[
|
||||
tuple[str, tuple[int, int]], Optional[CloudBrowserProvider]
|
||||
] = {}
|
||||
_cached_cloud_providers: Dict[tuple[str, tuple[int, int]], Optional[CloudBrowserProvider]] = {}
|
||||
_cloud_provider_cache_lock = threading.RLock()
|
||||
_allow_private_urls_resolved = False
|
||||
_cached_allow_private_urls: Optional[bool] = None
|
||||
@@ -338,21 +304,11 @@ _browser_engine_resolved = False
|
||||
|
||||
|
||||
from tools.browser_tool_cloud import ( # noqa: F401 (re-exported; tests patch tools.browser_tool.<name>)
|
||||
_is_legacy_provider_registry_overridden,
|
||||
_ensure_browser_plugins_loaded,
|
||||
_get_cloud_provider,
|
||||
_instantiate_explicit_cloud_provider,
|
||||
_autodetect_cloud_provider,
|
||||
_resolve_cloud_provider_uncached,
|
||||
_is_local_mode,
|
||||
_is_local_backend,
|
||||
_get_browser_engine,
|
||||
_is_headed_mode,
|
||||
_should_inject_engine,
|
||||
_auto_local_for_private_urls,
|
||||
_use_real_profile,
|
||||
_allow_private_urls,
|
||||
_resolve_allow_private_urls,
|
||||
_is_legacy_provider_registry_overridden, _ensure_browser_plugins_loaded, _get_cloud_provider,
|
||||
_instantiate_explicit_cloud_provider, _autodetect_cloud_provider,
|
||||
_resolve_cloud_provider_uncached, _is_local_mode, _is_local_backend, _get_browser_engine,
|
||||
_is_headed_mode, _should_inject_engine, _auto_local_for_private_urls, _use_real_profile,
|
||||
_allow_private_urls, _resolve_allow_private_urls,
|
||||
)
|
||||
|
||||
from hermes_constants import is_termux as _is_termux_environment # noqa: F401 (read via origin)
|
||||
@@ -374,14 +330,9 @@ _headed_mode_resolved = False
|
||||
|
||||
|
||||
from tools.browser_tool_lightpanda_fallback import ( # noqa: F401
|
||||
_using_lightpanda_engine,
|
||||
lightpanda_engine_status,
|
||||
_lightpanda_fallback_reason,
|
||||
_needs_lightpanda_fallback,
|
||||
_annotate_lightpanda_fallback,
|
||||
_copy_fallback_warning,
|
||||
_run_chrome_fallback_command,
|
||||
_chrome_fallback_screenshot,
|
||||
_using_lightpanda_engine, lightpanda_engine_status, _lightpanda_fallback_reason,
|
||||
_needs_lightpanda_fallback, _annotate_lightpanda_fallback, _copy_fallback_warning,
|
||||
_run_chrome_fallback_command, _chrome_fallback_screenshot,
|
||||
)
|
||||
|
||||
|
||||
@@ -394,17 +345,10 @@ _real_profile_chrome_procs: list = [] # Popen handles of directly-launched real
|
||||
|
||||
|
||||
from tools.browser_tool_real_profile import ( # noqa: F401
|
||||
_terminate_real_profile_chrome,
|
||||
_cdp_http_ready,
|
||||
_agent_browser_get_cdp,
|
||||
_cdp_on_data_dir,
|
||||
_agent_browser_close_session,
|
||||
_REAL_PROFILE_CHROME_FLAGS,
|
||||
_real_profile_unsupported_reason,
|
||||
_real_profile_snapshot_error,
|
||||
_launch_real_profile_chrome,
|
||||
_attach_agent_browser_to_real_profile,
|
||||
_real_profile_cdp,
|
||||
_terminate_real_profile_chrome, _cdp_http_ready, _agent_browser_get_cdp, _cdp_on_data_dir,
|
||||
_agent_browser_close_session, _REAL_PROFILE_CHROME_FLAGS, _real_profile_unsupported_reason,
|
||||
_real_profile_snapshot_error, _launch_real_profile_chrome,
|
||||
_attach_agent_browser_to_real_profile, _real_profile_cdp,
|
||||
)
|
||||
|
||||
|
||||
@@ -603,9 +547,7 @@ class _BrowserSessionBackend:
|
||||
reason = _suspect_browser_sessions.pop(self._session_key, None)
|
||||
if reason is None:
|
||||
return True
|
||||
logger.info(
|
||||
"Recycling suspect browser session %s before reuse (%s)", self._session_key, reason
|
||||
)
|
||||
logger.info("Recycling suspect browser session %s before reuse (%s)", self._session_key, reason)
|
||||
try:
|
||||
_cleanup_single_browser_session(self._session_key)
|
||||
except Exception:
|
||||
@@ -628,33 +570,14 @@ _cleanup_lock = threading.Lock()
|
||||
|
||||
|
||||
from tools.browser_tool_lifecycle import ( # noqa: F401 (re-exported; tests patch tools.browser_tool.<name>)
|
||||
_session_expiry_timestamp,
|
||||
_session_has_expired,
|
||||
_emergency_cleanup_all_sessions,
|
||||
_session_owner_scope,
|
||||
_cleanup_inactive_browser_sessions,
|
||||
_write_owner_pid,
|
||||
_verify_reapable_browser_daemon,
|
||||
_socket_dir_idle_seconds,
|
||||
_owner_pid_alive,
|
||||
_reap_socket_dir,
|
||||
_reap_orphaned_browser_sessions,
|
||||
_browser_cleanup_thread_worker,
|
||||
_start_browser_cleanup_thread,
|
||||
_stop_browser_cleanup_thread,
|
||||
_update_session_activity,
|
||||
_kill_process_tree,
|
||||
_legacy_kill_process_tree,
|
||||
_pid_exists,
|
||||
_cleanup_old_screenshots,
|
||||
_cleanup_old_recordings,
|
||||
_drop_last_active_binding,
|
||||
cleanup_browser,
|
||||
_kill_verified_daemon,
|
||||
_release_session_resources,
|
||||
_force_reap_browser_session,
|
||||
_cleanup_single_browser_session,
|
||||
cleanup_all_browsers,
|
||||
_session_expiry_timestamp, _session_has_expired, _emergency_cleanup_all_sessions,
|
||||
_session_owner_scope, _cleanup_inactive_browser_sessions, _write_owner_pid,
|
||||
_verify_reapable_browser_daemon, _socket_dir_idle_seconds, _owner_pid_alive, _reap_socket_dir,
|
||||
_reap_orphaned_browser_sessions, _browser_cleanup_thread_worker, _start_browser_cleanup_thread,
|
||||
_stop_browser_cleanup_thread, _update_session_activity, _kill_process_tree,
|
||||
_legacy_kill_process_tree, _pid_exists, _cleanup_old_screenshots, _cleanup_old_recordings,
|
||||
_drop_last_active_binding, cleanup_browser, _kill_verified_daemon, _release_session_resources,
|
||||
_force_reap_browser_session, _cleanup_single_browser_session, cleanup_all_browsers,
|
||||
)
|
||||
|
||||
# atexit only — NO SIGINT/SIGTERM handlers calling sys.exit(): a SystemExit raised
|
||||
@@ -812,9 +735,7 @@ BROWSER_TOOL_SCHEMAS = [
|
||||
|
||||
|
||||
from tools.browser_tool_snapshot import ( # noqa: F401
|
||||
_store_full_snapshot,
|
||||
_truncate_snapshot,
|
||||
_redact_browser_output,
|
||||
_store_full_snapshot, _truncate_snapshot, _redact_browser_output,
|
||||
_extract_screenshot_path_from_text,
|
||||
)
|
||||
|
||||
@@ -1097,39 +1018,37 @@ def _camofox(func_name: str, *args):
|
||||
return getattr(importlib.import_module("tools.browser_camofox"), func_name)(*args)
|
||||
|
||||
|
||||
def _guarded_action_session(task_id: Optional[str], action: str) -> tuple[str, Optional[str]]:
|
||||
"""``(session_key, blocked_payload)`` for an input action on the task's current page."""
|
||||
def _guarded_action(task_id: Optional[str], action: str, command: str, args: list, ok: Dict[str, Any], err: str) -> str:
|
||||
"""Input action on the task's current page, refused when the SSRF guard flags the page."""
|
||||
effective_task_id = _last_session_key(task_id or "default")
|
||||
return effective_task_id, _blocked_private_page_action(effective_task_id, action)
|
||||
blocked = _blocked_private_page_action(effective_task_id, action)
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
return _tool_response(_run_browser_command(effective_task_id, command, args), ok, err)
|
||||
|
||||
|
||||
def browser_click(ref: str, task_id: Optional[str] = None) -> str:
|
||||
"""Click the element ``ref`` (e.g. "@e5")."""
|
||||
if _is_camofox_mode():
|
||||
return _camofox("camofox_click", ref, task_id)
|
||||
effective_task_id, blocked = _guarded_action_session(task_id, "click")
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
if not ref.startswith("@"):
|
||||
ref = f"@{ref}"
|
||||
result = _run_browser_command(effective_task_id, "click", [ref])
|
||||
return _tool_response(result, {"clicked": ref}, f"Failed to click {ref}")
|
||||
return _guarded_action(task_id, "click", "click", [ref], {"clicked": ref}, f"Failed to click {ref}")
|
||||
|
||||
|
||||
def browser_type(ref: str, text: str, task_id: Optional[str] = None) -> str:
|
||||
"""Type ``text`` into the element ``ref`` (fill: clears, then types)."""
|
||||
if _is_camofox_mode():
|
||||
return _camofox("camofox_type", ref, text, task_id)
|
||||
effective_task_id, blocked = _guarded_action_session(task_id, "type")
|
||||
effective_task_id = _last_session_key(task_id or "default")
|
||||
blocked = _blocked_private_page_action(effective_task_id, "type")
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
if not ref.startswith("@"):
|
||||
ref = f"@{ref}"
|
||||
result = _run_browser_command(effective_task_id, "fill", [ref, text])
|
||||
|
||||
from agent.display import (
|
||||
redact_browser_typed_text_for_display, redact_tool_args_for_display
|
||||
)
|
||||
from agent.display import redact_browser_typed_text_for_display, redact_tool_args_for_display
|
||||
# Typed text goes through the secret-pattern redactor so API keys / tokens don't
|
||||
# leak into tool progress or chat history (the raw value already went to the browser).
|
||||
display_text = (redact_tool_args_for_display("browser_type", {"text": text}) or {})["text"]
|
||||
@@ -1177,11 +1096,7 @@ def browser_press(key: str, task_id: Optional[str] = None) -> str:
|
||||
"""Press a keyboard key (e.g. "Enter", "Tab")."""
|
||||
if _is_camofox_mode():
|
||||
return _camofox("camofox_press", key, task_id)
|
||||
effective_task_id, blocked = _guarded_action_session(task_id, "press")
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
result = _run_browser_command(effective_task_id, "press", [key])
|
||||
return _tool_response(result, {"pressed": key}, f"Failed to press {key}")
|
||||
return _guarded_action(task_id, "press", "press", [key], {"pressed": key}, f"Failed to press {key}")
|
||||
|
||||
|
||||
def _blocked_private_page_json(blocked_url: str, why: str) -> str:
|
||||
@@ -1245,9 +1160,7 @@ def browser_console(clear: bool = False, expression: Optional[str] = None, task_
|
||||
errors = []
|
||||
if errors_result.get("success"):
|
||||
for err in errors_result.get("data", {}).get("errors", []):
|
||||
errors.append({
|
||||
"message": _redact_browser_output(err.get("message", "")), "source": "exception"
|
||||
})
|
||||
errors.append({"message": _redact_browser_output(err.get("message", "")), "source": "exception"})
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
@@ -1262,21 +1175,11 @@ def browser_console(clear: bool = False, expression: Optional[str] = None, task_
|
||||
|
||||
|
||||
from tools.browser_tool_eval_policy import ( # noqa: F401
|
||||
_eval_ssrf_guard_active,
|
||||
_JS_URL_LITERAL_RE,
|
||||
_expression_targets_private_url,
|
||||
_current_page_private_url,
|
||||
_RISKY_BROWSER_EVAL_PATTERNS,
|
||||
_JS_STRING_LITERAL_RE,
|
||||
_SENSITIVE_BROWSER_EVAL_TOKENS,
|
||||
_allow_unsafe_browser_evaluate,
|
||||
_restrict_browser_evaluate,
|
||||
_decode_js_string_literal,
|
||||
_decoded_js_string_literals,
|
||||
_sensitive_browser_eval_token_reason,
|
||||
_risky_browser_eval_reason,
|
||||
_enforce_browser_eval_policy,
|
||||
_camofox_current_page_private_url,
|
||||
_eval_ssrf_guard_active, _JS_URL_LITERAL_RE, _expression_targets_private_url,
|
||||
_current_page_private_url, _RISKY_BROWSER_EVAL_PATTERNS, _JS_STRING_LITERAL_RE,
|
||||
_SENSITIVE_BROWSER_EVAL_TOKENS, _allow_unsafe_browser_evaluate, _restrict_browser_evaluate,
|
||||
_decode_js_string_literal, _decoded_js_string_literals, _sensitive_browser_eval_token_reason,
|
||||
_risky_browser_eval_reason, _enforce_browser_eval_policy, _camofox_current_page_private_url,
|
||||
)
|
||||
|
||||
|
||||
@@ -1323,9 +1226,7 @@ def _eval_supervisor_fast_path(effective_task_id: str, expression: str) -> Optio
|
||||
err = sup_result.get("error") or "evaluate_runtime failed"
|
||||
if "supervisor" not in err.lower():
|
||||
return _dumps(_err(err))
|
||||
logger.debug(
|
||||
"browser_eval: supervisor path unavailable (%s), falling back to subprocess", err
|
||||
)
|
||||
logger.debug("browser_eval: supervisor path unavailable (%s), falling back to subprocess", err)
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as exc: # pragma: no cover — defensive
|
||||
@@ -1499,9 +1400,7 @@ _LP_VISION_FALLBACK_REASON = (
|
||||
|
||||
|
||||
from tools.browser_tool_vision import ( # noqa: F401 (re-exported; tests patch tools.browser_tool.<name>)
|
||||
_vision_mode_label,
|
||||
_lightpanda_vision_preroute,
|
||||
_native_vision_result,
|
||||
_vision_mode_label, _lightpanda_vision_preroute, _native_vision_result,
|
||||
_analyze_screenshot_with_aux_llm,
|
||||
)
|
||||
|
||||
@@ -1609,9 +1508,7 @@ _chromium_autoinstall_attempted = False
|
||||
# Registry
|
||||
# ---------------------------------------------------------------------------
|
||||
from tools.registry import registry, tool_error
|
||||
from tools.browser_extension_router import (
|
||||
extension_controller_available, routed_browser_handler
|
||||
)
|
||||
from tools.browser_extension_router import extension_controller_available, routed_browser_handler
|
||||
|
||||
_BROWSER_SCHEMA_MAP = {s["name"]: s for s in BROWSER_TOOL_SCHEMAS}
|
||||
|
||||
@@ -1621,33 +1518,33 @@ def check_browser_routed_requirements(action: str = "browser_snapshot") -> bool:
|
||||
return check_browser_requirements() or extension_controller_available(action)
|
||||
|
||||
|
||||
# (tool name, emoji, availability gate, fallback call) — routed-through-extension
|
||||
# tools (gate None) use the per-action gate; get_images/console/vision keep the plain
|
||||
# requirement checks. ``_fallback`` receives (args, kw).
|
||||
def _fallback_call(fn_name: str, arg_defaults: Dict[str, Any], extra_kw: tuple = ()):
|
||||
"""Adapter from the registry's ``(args, kw)`` to ``<fn_name>(**schema_args, task_id=...)``;
|
||||
the function is looked up in module globals at call time so monkeypatching works."""
|
||||
def call(args, kw):
|
||||
params = {a: args.get(a, d) for a, d in arg_defaults.items()}
|
||||
params["task_id"] = kw.get("task_id")
|
||||
for k in extra_kw:
|
||||
params[k] = kw.get(k)
|
||||
return globals()[fn_name](**params)
|
||||
return call
|
||||
|
||||
|
||||
# (tool name, emoji, availability gate, schema-arg defaults[, extra kw names]) — the tool
|
||||
# function is the module global of the same name. —
|
||||
# routed-through-extension tools (gate None) use the per-action gate; get_images/console/vision
|
||||
# keep the plain requirement checks.
|
||||
_BROWSER_TOOL_TABLE = (
|
||||
("browser_navigate", "🌐", None,
|
||||
lambda args, kw: browser_navigate(url=args.get("url", ""), task_id=kw.get("task_id"))),
|
||||
("browser_snapshot", "📸", None,
|
||||
lambda args, kw: browser_snapshot(
|
||||
full=args.get("full", False), task_id=kw.get("task_id"), user_task=kw.get("user_task"))),
|
||||
("browser_click", "👆", None,
|
||||
lambda args, kw: browser_click(ref=args.get("ref", ""), task_id=kw.get("task_id"))),
|
||||
("browser_type", "⌨️", None,
|
||||
lambda args, kw: browser_type(ref=args.get("ref", ""), text=args.get("text", ""), task_id=kw.get("task_id"))),
|
||||
("browser_scroll", "📜", None,
|
||||
lambda args, kw: browser_scroll(direction=args.get("direction", "down"), task_id=kw.get("task_id"))),
|
||||
("browser_back", "◀️", None,
|
||||
lambda args, kw: browser_back(task_id=kw.get("task_id"))),
|
||||
("browser_press", "⌨️", None,
|
||||
lambda args, kw: browser_press(key=args.get("key", ""), task_id=kw.get("task_id"))),
|
||||
("browser_get_images", "🖼️", check_browser_requirements,
|
||||
lambda args, kw: browser_get_images(task_id=kw.get("task_id"))),
|
||||
("browser_vision", "👁️", check_browser_vision_requirements,
|
||||
lambda args, kw: browser_vision(
|
||||
question=args.get("question", ""), annotate=args.get("annotate", False), task_id=kw.get("task_id"))),
|
||||
("browser_console", "🖥️", check_browser_requirements,
|
||||
lambda args, kw: browser_console(
|
||||
clear=args.get("clear", False), expression=args.get("expression"), task_id=kw.get("task_id"))),
|
||||
("browser_navigate", "🌐", None, {"url": ""}),
|
||||
("browser_snapshot", "📸", None, {"full": False}, ("user_task",)),
|
||||
("browser_click", "👆", None, {"ref": ""}),
|
||||
("browser_type", "⌨️", None, {"ref": "", "text": ""}),
|
||||
("browser_scroll", "📜", None, {"direction": "down"}),
|
||||
("browser_back", "◀️", None, {}),
|
||||
("browser_press", "⌨️", None, {"key": ""}),
|
||||
("browser_get_images", "🖼️", check_browser_requirements, {}),
|
||||
("browser_vision", "👁️", check_browser_vision_requirements, {"question": "", "annotate": False}),
|
||||
("browser_console", "🖥️", check_browser_requirements, {"clear": False, "expression": None}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1670,14 +1567,11 @@ def _routed_handler(name: str, fallback):
|
||||
|
||||
# Legacy per-tool gate names (tests + external callers); also looked up by the
|
||||
# registration loop below via globals().
|
||||
for _name, _emoji, _check_fn, _fallback in _BROWSER_TOOL_TABLE:
|
||||
for _name, _emoji, _check_fn, _defaults, *_extra in _BROWSER_TOOL_TABLE:
|
||||
if _check_fn is None:
|
||||
_check_fn = globals()[f"check_{_name}_requirements"] = _routed_check_fn(_name)
|
||||
registry.register(
|
||||
name=_name,
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP[_name],
|
||||
handler=_routed_handler(_name, _fallback),
|
||||
check_fn=_check_fn,
|
||||
emoji=_emoji,
|
||||
name=_name, toolset="browser", schema=_BROWSER_SCHEMA_MAP[_name],
|
||||
handler=_routed_handler(_name, _fallback_call(_name, _defaults, *_extra)),
|
||||
check_fn=_check_fn, emoji=_emoji,
|
||||
)
|
||||
|
||||
@@ -69,8 +69,7 @@ def _emergency_cleanup_all_sessions():
|
||||
except Exception as e:
|
||||
_bt.logger.debug("Real-profile chrome cleanup on exit failed: %s", e)
|
||||
if _bt._active_sessions:
|
||||
_bt.logger.info("Emergency cleanup: closing %s active session(s)...",
|
||||
len(_bt._active_sessions))
|
||||
_bt.logger.info("Emergency cleanup: closing %s active session(s)...", len(_bt._active_sessions))
|
||||
try:
|
||||
_bt.cleanup_all_browsers()
|
||||
except Exception as e:
|
||||
@@ -112,9 +111,7 @@ def _session_owner_scope(task_id: str):
|
||||
yield
|
||||
return
|
||||
|
||||
from agent.secret_scope import (
|
||||
build_profile_secret_scope, reset_secret_scope, set_secret_scope
|
||||
)
|
||||
from agent.secret_scope import build_profile_secret_scope, reset_secret_scope, set_secret_scope
|
||||
from hermes_cli.env_loader import hydrate_profile_secret_sources
|
||||
|
||||
home_token = set_hermes_home_override(owner_home)
|
||||
@@ -189,8 +186,7 @@ def _write_owner_pid(socket_dir: str, session_name: str) -> None:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(str(os.getpid()))
|
||||
except OSError as exc:
|
||||
_bt.logger.debug("Could not write owner_pid file for %s: %s",
|
||||
session_name, exc)
|
||||
_bt.logger.debug("Could not write owner_pid file for %s: %s", session_name, exc)
|
||||
|
||||
|
||||
def _verify_reapable_browser_daemon(daemon_pid: int, socket_dir: str,
|
||||
@@ -234,14 +230,11 @@ def _verify_reapable_browser_daemon(daemon_pid: int, socket_dir: str,
|
||||
|
||||
socket_dir_l = socket_dir.lower()
|
||||
socket_base_l = os.path.basename(socket_dir).lower()
|
||||
bound = socket_dir_l in cmdline or (
|
||||
socket_base_l and socket_base_l in cmdline)
|
||||
bound = socket_dir_l in cmdline or (socket_base_l and socket_base_l in cmdline)
|
||||
if not bound:
|
||||
try:
|
||||
env_dir = (proc.environ() or {}).get(
|
||||
"AGENT_BROWSER_SOCKET_DIR", "")
|
||||
bound = bool(env_dir) and os.path.normpath(env_dir) == \
|
||||
os.path.normpath(socket_dir)
|
||||
env_dir = (proc.environ() or {}).get("AGENT_BROWSER_SOCKET_DIR", "")
|
||||
bound = bool(env_dir) and os.path.normpath(env_dir) == os.path.normpath(socket_dir)
|
||||
except (psutil.AccessDenied, psutil.NoSuchProcess, OSError):
|
||||
bound = False # environ() can be denied even same-user; cmdline already failed — fail closed
|
||||
|
||||
@@ -361,8 +354,7 @@ def _reap_socket_dir(socket_dir: str, session_name: str, tracked_names: set) ->
|
||||
"Refusing to reap browser daemon PID %d (session %s): "
|
||||
"no start-time fingerprint available", daemon_pid, session_name)
|
||||
return False
|
||||
_bt.logger.info("Reaped orphaned browser daemon PID %d (session %s)",
|
||||
daemon_pid, session_name)
|
||||
_bt.logger.info("Reaped orphaned browser daemon PID %d (session %s)", daemon_pid, session_name)
|
||||
reaped = True
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
pass
|
||||
@@ -519,25 +511,11 @@ def _legacy_kill_process_tree(proc: "subprocess.Popen") -> None:
|
||||
|
||||
|
||||
def _pid_exists(pid: int) -> bool:
|
||||
"""Best-effort 'is this PID alive' check (signal 0 / psutil on Windows)."""
|
||||
"""Best-effort 'is this PID alive' (cross-platform via gateway.status; zombies count as dead)."""
|
||||
if pid <= 0:
|
||||
return False
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import psutil
|
||||
|
||||
return psutil.pid_exists(pid)
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
os.kill(pid, 0) # windows-footgun: ok — psutil.pid_exists above handles Windows
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
from gateway.status import _pid_exists as _gateway_pid_exists
|
||||
return _gateway_pid_exists(pid)
|
||||
|
||||
|
||||
def _unlink_older_than(directory: Path, pattern: str, max_age_hours: float, label: str) -> None:
|
||||
@@ -713,15 +691,11 @@ def _cleanup_single_browser_session(task_id: str) -> None:
|
||||
except Exception as e:
|
||||
_bt.logger.warning("lightpanda stop failed for task %s: %s", task_id, e)
|
||||
elif _bt._session_has_expired(session_info):
|
||||
_bt.logger.debug(
|
||||
"Skipping agent-browser close for expired session %s", task_id
|
||||
)
|
||||
_bt.logger.debug("Skipping agent-browser close for expired session %s", task_id)
|
||||
else:
|
||||
try:
|
||||
_bt._run_browser_command(task_id, "close", [], timeout=10)
|
||||
_bt.logger.debug(
|
||||
"agent-browser close command completed for task %s", task_id
|
||||
)
|
||||
_bt.logger.debug("agent-browser close command completed for task %s", task_id)
|
||||
except Exception as e:
|
||||
_bt.logger.warning("agent-browser close failed for task %s: %s", task_id, e)
|
||||
|
||||
|
||||
@@ -220,8 +220,7 @@ def _create_local_session(task_id: str, allow_real_profile: bool = True) -> Dict
|
||||
return _bt._create_lightpanda_session(task_id)
|
||||
|
||||
info = _session_record("h", None, {"local": True})
|
||||
_bt.logger.info("Created local browser session %s for task %s",
|
||||
info["session_name"], task_id)
|
||||
_bt.logger.info("Created local browser session %s for task %s", info["session_name"], task_id)
|
||||
return info
|
||||
|
||||
|
||||
@@ -230,14 +229,10 @@ def _create_lightpanda_session(task_id: str) -> Dict[str, Any]:
|
||||
from tools.browser_lightpanda import launch_lightpanda
|
||||
|
||||
session_name = f"lp_{uuid.uuid4().hex[:10]}"
|
||||
server, err = launch_lightpanda(
|
||||
session_name, block_private_networks=not _bt._is_local_backend()
|
||||
)
|
||||
server, err = launch_lightpanda(session_name, block_private_networks=not _bt._is_local_backend())
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
_bt.logger.info(
|
||||
"Created Lightpanda session %s (port %s) for task %s", session_name, server.port, task_id
|
||||
)
|
||||
_bt.logger.info("Created Lightpanda session %s (port %s) for task %s", session_name, server.port, task_id)
|
||||
return {
|
||||
"session_name": session_name,
|
||||
"bb_session_id": None,
|
||||
@@ -361,9 +356,7 @@ def _get_session_info(task_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
):
|
||||
return existing_session
|
||||
else:
|
||||
_bt.logger.info(
|
||||
"Replacing expired or dead browser session for task %s", task_id
|
||||
)
|
||||
_bt.logger.info("Replacing expired or dead browser session for task %s", task_id)
|
||||
_bt._cleanup_single_browser_session(task_id)
|
||||
replacement = _replacement_after_teardown()
|
||||
if replacement is not None:
|
||||
@@ -491,9 +484,7 @@ def _handle_browser_command_timeout(
|
||||
_bt._discard_timed_out_browser_session(task_id, session_info, task_socket_dir)
|
||||
return
|
||||
|
||||
_bt._browser_session_backend(task_id).mark_suspect(
|
||||
"browser command timed out; session may be poisoned"
|
||||
)
|
||||
_bt._browser_session_backend(task_id).mark_suspect("browser command timed out; session may be poisoned")
|
||||
|
||||
session_name = str(session_info.get("session_name") or "")
|
||||
daemon_pid = _bt._read_browser_daemon_pid(task_socket_dir, session_name) if session_name else None
|
||||
@@ -640,9 +631,7 @@ def _spawn_and_collect(
|
||||
_bt._unlink_command_output_files(stdout_path, stderr_path)
|
||||
_bt._handle_browser_command_timeout(task_id, session_info, task_socket_dir)
|
||||
if stderr and stderr.strip():
|
||||
_bt.logger.warning(
|
||||
"browser '%s' stderr after timeout: %s", command, stderr.strip()[:500]
|
||||
)
|
||||
_bt.logger.warning("browser '%s' stderr after timeout: %s", command, stderr.strip()[:500])
|
||||
_bt.logger.warning("browser '%s' timed out after %ds (task=%s, socket_dir=%s)",
|
||||
command, timeout, task_id, task_socket_dir)
|
||||
return {
|
||||
|
||||
@@ -112,8 +112,7 @@ def _analyze_screenshot_with_aux_llm(screenshot_path: Path, question: str) -> st
|
||||
_screenshot_b64 = base64.b64encode(_screenshot_bytes).decode("ascii")
|
||||
data_url = f"data:image/png;base64,{_screenshot_b64}"
|
||||
vision_model = _bt._get_vision_model()
|
||||
_bt.logger.debug("browser_vision: analysing screenshot (%d bytes)",
|
||||
len(_screenshot_bytes))
|
||||
_bt.logger.debug("browser_vision: analysing screenshot (%d bytes)", len(_screenshot_bytes))
|
||||
|
||||
vision_timeout = 120.0
|
||||
vision_temperature = 0.1
|
||||
@@ -146,9 +145,7 @@ def _analyze_screenshot_with_aux_llm(screenshot_path: Path, question: str) -> st
|
||||
try:
|
||||
response = _bt.call_llm(**call_kwargs)
|
||||
except Exception as _api_err:
|
||||
from tools.vision_tools import (
|
||||
_is_image_size_error, _resize_image_for_vision, _RESIZE_TARGET_BYTES,
|
||||
)
|
||||
from tools.vision_tools import _is_image_size_error, _resize_image_for_vision, _RESIZE_TARGET_BYTES
|
||||
if not (_is_image_size_error(_api_err) and len(data_url) > _RESIZE_TARGET_BYTES):
|
||||
raise
|
||||
_bt.logger.info(
|
||||
|
||||
Reference in New Issue
Block a user