sand-boxer/src/sandboxer/extensions/credential_delivery.py
tegwick 17d4160b6e
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
feat: deliver owner-bound credentials into bwrap commands
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
2026-09-06 00:25:07 +02:00

120 lines
4.8 KiB
Python

"""Owner-configured credential provider envelope; no secret retrieval in Glas."""
from __future__ import annotations
import json
import os
import selectors
import signal
import subprocess
import sys
import time
from contextlib import suppress
from pathlib import Path
def provider_argv(config: dict, refs: list[str], context: dict) -> list[str]:
if len(refs) != 1:
raise ValueError("credential execution requires exactly one configured route")
route = config.get("credential_routes", {}).get(refs[0])
if not isinstance(route, dict):
raise ValueError("credential route is not configured by the owner")
for field in ("profiles", "projects", "actors"):
choices = route.get(field)
if (
not isinstance(choices, list)
or not choices
or any(not isinstance(item, str) or not item for item in choices)
):
raise ValueError("credential route requires explicit consumer allowlists")
if (
context.get("profile_id") not in route.get("profiles", [])
or context.get("project") not in route.get("projects", [])
or context.get("actor") not in route.get("actors", [])
or not context.get("run_id")
):
raise ValueError("credential route consumer binding denied")
command = route.get("exec_argv")
if (
not isinstance(command, list)
or not command
or any(not isinstance(arg, str) or not arg or "\0" in arg for arg in command)
or not Path(command[0]).is_absolute()
or command[-1] != "--"
):
raise ValueError("credential provider requires an absolute executable and trailing --")
return command
def execute(config: dict, request: dict, socket_path: Path) -> dict:
prefix = provider_argv(config, request["credential_route_refs"], request["execution_context"])
argv = [
*prefix,
sys.executable,
str(Path(__file__).with_name("credential_exec.py")),
str(socket_path),
]
# Never let an interactive provider key stand in for owner acquisition.
env = os.environ.copy()
env.pop("ANTHROPIC_API_KEY", None)
env.pop("ANTHROPIC_AUTH_TOKEN", None)
proc = subprocess.Popen(
argv,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=env,
start_new_session=True,
)
assert proc.stdin is not None and proc.stdout is not None
budget = request["timeout_seconds"] + 30
deadline = time.monotonic() + budget
limit = request["max_output_bytes"] * 2 + 65536
output = bytearray()
pending = memoryview(json.dumps(request).encode())
try:
os.set_blocking(proc.stdin.fileno(), False)
os.set_blocking(proc.stdout.fileno(), False)
with selectors.DefaultSelector() as selector:
selector.register(proc.stdin, selectors.EVENT_WRITE)
selector.register(proc.stdout, selectors.EVENT_READ)
while selector.get_map():
remaining = deadline - time.monotonic()
if remaining <= 0:
raise RuntimeError("credential delivery deadline exceeded")
for key, _ in selector.select(min(remaining, 1)):
if key.fileobj is proc.stdin:
written = os.write(proc.stdin.fileno(), pending[:65536])
pending = pending[written:]
if not pending:
selector.unregister(proc.stdin)
proc.stdin.close()
else:
chunk = os.read(proc.stdout.fileno(), 65536)
if not chunk:
selector.unregister(proc.stdout)
output.extend(chunk)
if len(output) > limit:
raise RuntimeError("credential delivery output exceeded bound")
if proc.wait(timeout=max(0.1, deadline - time.monotonic())) != 0:
raise RuntimeError("credential provider refused or failed")
response = json.loads(output)
if not isinstance(response, dict) or "boundary_error" in response:
raise RuntimeError("credential broker refused or failed")
return response
except Exception:
# Provider stdout/stderr and parser exception excerpts must never escape.
raise RuntimeError("owner credential delivery failed") from None
finally:
if proc.poll() is None:
with suppress(ProcessLookupError):
os.killpg(proc.pid, signal.SIGTERM)
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
with suppress(ProcessLookupError):
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
proc.stdout.close()
if not proc.stdin.closed:
proc.stdin.close()