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,117 @@
|
||||
"""Shared helpers for adapters: bounded subprocesses and askpass scripts.
|
||||
|
||||
Secret-bearing helper files are created only under
|
||||
$XDG_RUNTIME_DIR/agent-broker/tmp (tmpfs, 0700 dir, 0600
|
||||
files) and deleted immediately after use. Secrets never appear on argv.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import secrets as _secrets
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from ..config import tmp_dir
|
||||
from ..sanitize import sanitize_text, sanitize_bytes
|
||||
|
||||
|
||||
def make_secret_dir() -> Path:
|
||||
d = tmp_dir() / _secrets.token_hex(12)
|
||||
d.mkdir(mode=0o700)
|
||||
return d
|
||||
|
||||
|
||||
def write_secret_file(path: Path, value: str):
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write(value)
|
||||
|
||||
|
||||
def write_askpass(dirpath: Path, user_file: Path | None, pass_file: Path) -> Path:
|
||||
"""askpass script reads values from sibling files -- the secret is
|
||||
never inside the script itself or on any argv."""
|
||||
script = dirpath / "askpass.sh"
|
||||
lines = [
|
||||
"#!/bin/sh",
|
||||
"case \"$1\" in",
|
||||
]
|
||||
if user_file is not None:
|
||||
lines += [
|
||||
" *sername*|*ogin*) cat %s ;;",
|
||||
f" *) cat {pass_file} ;;",
|
||||
]
|
||||
else:
|
||||
lines.append(f" *) cat {pass_file} ;;")
|
||||
lines.append("esac")
|
||||
if user_file is not None:
|
||||
lines[3] = lines[3] % user_file
|
||||
script.write_text("\n".join(lines) + "\n")
|
||||
os.chmod(script, 0o700)
|
||||
return script
|
||||
|
||||
|
||||
def run_subprocess(argv: list[str], *, env: dict | None = None, input_text: str | None = None,
|
||||
timeout: int = 60, secrets: list[str] = ()) -> dict:
|
||||
"""Run argv without a shell. Returns sanitized {exit_code, stdout,
|
||||
stderr}. Never raises the child's output raw."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
argv,
|
||||
env=env,
|
||||
input=input_text,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
rc, raw_out, raw_err = proc.returncode, proc.stdout, proc.stderr
|
||||
timed_out = False
|
||||
except subprocess.TimeoutExpired as e:
|
||||
rc, timed_out = 124, True
|
||||
raw_out = (e.stdout or b"").decode("utf-8", "replace") if isinstance(e.stdout, bytes) else (e.stdout or "")
|
||||
raw_err = ((e.stderr or b"").decode("utf-8", "replace") if isinstance(e.stderr, bytes) else (e.stderr or "")) + \
|
||||
f"\n[timeout after {timeout}s]"
|
||||
out = sanitize_bytes(raw_out.encode(), [s.encode() for s in secrets if s])
|
||||
err = sanitize_bytes(raw_err.encode(), [s.encode() for s in secrets if s])
|
||||
result = {
|
||||
"exit_code": rc,
|
||||
"stdout": sanitize_text(out.decode("utf-8", "replace"), secrets),
|
||||
"stderr": sanitize_text(err.decode("utf-8", "replace"), secrets),
|
||||
}
|
||||
if timed_out:
|
||||
result["timed_out"] = True
|
||||
return result
|
||||
|
||||
|
||||
@contextmanager
|
||||
def secret_dir():
|
||||
d = make_secret_dir()
|
||||
try:
|
||||
yield d
|
||||
finally:
|
||||
for child in d.iterdir():
|
||||
try:
|
||||
child.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
d.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def clean_env(extra: dict | None = None, prepend_paths: list[str] = ()) -> dict:
|
||||
"""Minimal environment for secret-bearing children: no inherited
|
||||
credential variables are added beyond what the adapter explicitly
|
||||
injects. prepend_paths lists directories that must precede the base
|
||||
PATH (e.g. the dir of a resolved executable whose shebang needs a
|
||||
specific interpreter)."""
|
||||
env = {
|
||||
"PATH": ":".join([*prepend_paths, "/usr/local/bin", "/usr/bin", "/bin"]),
|
||||
"HOME": os.environ.get("HOME", "/"),
|
||||
"LANG": "C.UTF-8",
|
||||
}
|
||||
if extra:
|
||||
env.update(extra)
|
||||
return env
|
||||
Reference in new issue
Block a user