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.
116 lines
4.2 KiB
Python
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
|