Files
agent-credential-broker/agent_credential_broker/adapters/browser.py
T
me be87900794 agent-credential-broker: fork of vaultwarden-agent-broker + OpenBao backend
Mechanical rename (package agent_credential_broker; entry points
agent-broker{,-mcp,-plugin}; env AGENT_BROKER_*; paths agent-broker;
agent-browser credential provider 'broker'; MCP server name agent-broker).

New backend 'openbao': stdlib-only HTTP client for OpenBao/Vault KV v2
(approle login or token file/env; metadata-only ref index; lazy re-login
on 401/403; token renewal). bitwarden-cli backend kept as transition.
2026-10-14 13:27:33 -06:00

116 lines
4.2 KiB
Python

"""Browser adapter.
Preferred path (native provider): the agent runs
agent-browser auth login <name> --credential-provider broker \\
--item <agent.ref>
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/<pid>/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