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:
me committed 2026-10-14 13:27:33 -06:00
commit be87900794
35 files changed
+5003

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
"""Git adapter: clone/fetch/push over HTTPS with askpass auth.
The token/password is supplied through a generated GIT_ASKPASS script
that cats 0600 runtime-tmpfs files -- it never appears on argv, never
inside the remote URL, and never in output (output is sanitized too).
Credentials embedded in URLs (https://user:pass@host) are rejected.
"""
from __future__ import annotations
import configparser
import os
import re
import shutil
import urllib.parse
from pathlib import Path
from ..errors import invalid_argument, origin_denied, unsafe_tool_integration
from ..policy import require_url_allowed
from ..items import item_secrets
from .common import (clean_env, run_subprocess, secret_dir, write_askpass,
write_secret_file)
_OPS = {"clone", "fetch", "push"}
_REFSPEC_OK = re.compile(r"^[\w\-./:+]+$")
def _validate_repo_url(item, policy, url):
if not url:
raise invalid_argument("missing repository url")
if url.startswith("git@") or url.startswith("ssh://"):
raise unsafe_tool_integration(
"git: ssh remotes not wired to vault credentials in this phase; use https")
p = urllib.parse.urlparse(url)
if p.scheme not in ("https", "http"):
raise invalid_argument(f"unsupported git remote scheme {p.scheme!r}")
if "@" in (p.netloc or ""):
raise invalid_argument("credentials must not be embedded in the remote URL")
require_url_allowed(policy, url)
def _check_workdir(config_roots, workdir):
wd = Path(workdir).resolve()
roots = [Path(r).expanduser().resolve() for r in config_roots] or [Path.home()]
if not any(str(wd).startswith(str(r) + os.sep) or wd == r for r in roots):
raise origin_denied("git", f"workdir {wd} outside allowed roots")
if not (wd / ".git").is_dir():
raise invalid_argument(f"{wd} is not a git worktree")
return wd
def _remote_url(workdir: Path, remote: str) -> str:
"""Read the remote's URL straight from .git/config -- the credential
will actually go there, so THAT is the URL we validate, not whatever
the agent claimed in `repository`."""
cfg = configparser.RawConfigParser()
cfg.read(workdir / ".git" / "config")
try:
url = cfg.get(f'remote "{remote}"', "url")
except configparser.Error:
raise invalid_argument(f"remote {remote!r} has no url configured")
if "://" in url and "@" in urllib.parse.urlparse(url).netloc:
raise invalid_argument("remote URL has embedded credentials")
return url
def validate(item, policy, params, config):
op = params.get("op") or params.get("operation")
if op not in _OPS:
raise invalid_argument(f"git op must be one of {sorted(_OPS)}")
_validate_repo_url(item, policy, params.get("repository") or "")
if op != "clone":
workdir = _check_workdir(config.git_workdir_roots, params.get("workdir") or ".")
remote = params.get("remote") or "origin"
remote_url = _remote_url(workdir, remote)
_validate_repo_url(item, policy, remote_url)
def run(item, policy, params, config):
op = params.get("op") or params.get("operation")
if op not in _OPS:
raise invalid_argument(f"git op must be one of {sorted(_OPS)}")
repo_url = params.get("repository") or ""
_validate_repo_url(item, policy, repo_url)
username = item.get("username")
password = item.get("password")
if not password:
raise unsafe_tool_integration("git: item has no token/password")
secrets = item_secrets(item)
git = shutil.which("git") or "/usr/bin/git"
with secret_dir() as d:
ufile = d / "user"
pfile = d / "pass"
write_secret_file(ufile, username or "oauth2")
write_secret_file(pfile, password)
askpass = write_askpass(d, ufile, pfile)
env = clean_env({
"GIT_ASKPASS": str(askpass),
"GIT_TERMINAL_PROMPT": "0",
})
if op == "clone":
dest = params.get("dest")
if not dest:
raise invalid_argument("clone requires dest")
argv = [git, "clone", "--", repo_url, str(dest)]
else:
workdir = _check_workdir(config.git_workdir_roots, params.get("workdir") or ".")
remote = params.get("remote") or "origin"
if not _REFSPEC_OK.match(remote):
raise invalid_argument("bad remote name")
remote_url = _remote_url(workdir, remote)
if remote_url.startswith("git@") or remote_url.startswith("ssh://"):
raise unsafe_tool_integration(
"git: ssh remotes not wired to vault credentials in this phase")
_validate_repo_url(item, policy, remote_url)
claimed = urllib.parse.urlparse(repo_url).netloc
actual = urllib.parse.urlparse(remote_url).netloc
if claimed.lower() != actual.lower():
raise origin_denied(policy.ref,
f"repository claim {claimed!r} != remote {actual!r}")
argv = [git, "-C", str(workdir)]
if op == "fetch":
argv += ["fetch", "--", remote]
else:
refspec = params.get("refspec") or params.get("branch") or "HEAD"
if not _REFSPEC_OK.match(refspec):
raise invalid_argument("bad refspec")
argv += ["push", "--", remote, refspec]
result = run_subprocess(argv, env=env, timeout=180, secrets=secrets)
result["op"] = op
result["repository"] = urllib.parse.urlparse(repo_url).netloc
return result