"""Browser adapter. Preferred path (native provider): the agent runs agent-browser auth login --credential-provider broker \\ --item agent-browser then invokes the broker's ``credential.read`` plugin and the secret flows plugin-stdout -> agent-browser, never through the agent. That plugin lives in plugin.py / entry point ``agent-broker-plugin``. Broker-side operations implemented here: browser_login executes ``agent-browser auth login`` with the plugin flags (no secrets on argv -- resolution happens inside agent-browser via the plugin) browser_fill_credential same mechanism on an existing page (--no-navigate + --url origin constraint + custom selectors from agent.* fields) Granular single-field fill via ``agent-browser fill`` is NOT offered: that command takes text on argv, which would expose the secret in /proc//cmdline. If a site cannot be handled by the auth-login mechanism, the adapter raises UNSAFE_TOOL_INTEGRATION instead of downgrading to argv leaks. """ from __future__ import annotations import os import re import shutil from ..config import socket_path from ..errors import invalid_argument, unsafe_tool_integration from ..policy import require_url_allowed from ..items import item_secrets from .common import clean_env, run_subprocess _SEL = re.compile(r"^[#.\[\]\w:='\"() >+~*-]{1,300}$") def _sel(v): if v and not _SEL.match(v): raise invalid_argument("bad selector") return v def _agent_browser(config) -> str: exe = shutil.which("agent-browser") if not exe: raise unsafe_tool_integration( "agent-browser not installed; browser adapter unavailable") return exe def validate(item, policy, params, config): url = params.get("url") or (policy.uris[0] if policy.uris else None) if not url: raise invalid_argument("browser op requires a url") require_url_allowed(policy, url) def run(item, policy, params, config): op = params.get("op") or params.get("operation") or "browser_login" url = params.get("url") or (policy.uris[0] if policy.uris else None) if not url: raise invalid_argument("browser op requires a url") require_url_allowed(policy, url) exe = _agent_browser(config) argv = [exe, "auth", "login", policy.ref, "--credential-provider", "broker", "--item", policy.ref, "--url", url] us = _sel(params.get("username_selector") or policy.username_selector) ps = _sel(params.get("password_selector") or policy.password_selector) ss = _sel(params.get("submit_selector") or policy.submit_selector) if us: argv += ["--username-selector", us] if ps: argv += ["--password-selector", ps] if ss: argv += ["--submit-selector", ss] if op == "browser_fill_credential": argv.append("--no-navigate") elif op != "browser_login": raise invalid_argument(f"unsupported browser op {op!r}") session = params.get("session") if session: if not re.match(r"^[\w.\-]{1,64}$", str(session)): raise invalid_argument("bad session name") argv += ["--session", str(session)] plugin = shutil.which("agent-broker-plugin") if not plugin: raise unsafe_tool_integration( "agent-broker-plugin not on PATH; browser adapter unavailable") # child PATH must resolve the right node (agent-browser shebang) and # the plugin binary; both live outside the base PATH on this host. extra_paths = sorted({os.path.dirname(exe), os.path.dirname(plugin)}) env = clean_env({ "AGENT_BROKER_SOCKET": str(socket_path()), "AGENT_BROWSER_CONFIG_DIR": os.environ.get( "AGENT_BROWSER_CONFIG_DIR", os.path.expanduser("~/.agent-browser")), # agent-browser session daemons live under $XDG_RUNTIME_DIR; without it # the child cannot attach to an existing session browser. "XDG_RUNTIME_DIR": os.environ.get("XDG_RUNTIME_DIR", ""), }, prepend_paths=extra_paths) res = run_subprocess(argv, env=env, timeout=120, secrets=item_secrets(item)) res["op"] = op res["via"] = "agent-browser credential.read plugin" return res