"""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()