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.
This commit is contained in:
commit
be87900794
35 files changed
+5003
No files matched your search
@@ -0,0 +1,115 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user