"""Filtered environment for spawning LLM CLI subprocesses. Only keys needed for binary resolution, locale, proxies, TLS, and adapter-specific prefixes are forwarded from ``os.environ``. Adapter implementations merge their own overrides (for example explicit API keys for that CLI only). """ from __future__ import annotations import os _SAFE_SUBPROCESS_ENV_KEYS = frozenset( { "HOME", # macOS Keychain item lookup (where `claude login` stores OAuth on darwin) # requires USER. LOGNAME is the POSIX/Linux equivalent kept for parity. "USER", "LOGNAME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "PATH", "PATHEXT", "SYSTEMROOT", "WINDIR", "COMSPEC", "SHELL", "TMP", "TEMP", "TMPDIR", "LANG", "TERM", "TZ", "NO_PROXY", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "NO_COLOR", "FORCE_COLOR", "COLORTERM", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", } ) _SAFE_SUBPROCESS_ENV_PREFIXES = ( "LC_", "CODEX_", "CURSOR_", "CLAUDE_", "GEMINI_", "GOOGLE_", # ``ANTIGRAVITY_*`` currently only carries non-secret config # (``ANTIGRAVITY_CLI_BIN`` path, ``ANTIGRAVITY_CLI_TIMEOUT_SECONDS`` # numeric). If Google ever ships a secret-bearing env (e.g. # ``ANTIGRAVITY_API_KEY``), revisit this — a blanket prefix would # leak the secret into every other CLI subprocess. Mirror the # COPILOT_ pattern below if that happens. "ANTIGRAVITY_", "OPENCODE_", "KIMI_", # Pi CLI (pi.dev) non-secret config vars (e.g. ``PI_AGENT_DIR`` / # ``PI_CONFIG_DIR``, ``PI_BIN``, ``PI_MODEL``). Pi's per-provider API keys # are NOT ``PI_``-prefixed (they are ``ANTHROPIC_API_KEY`` etc.) and are # forwarded only via the Pi adapter's ``CLIInvocation.env`` — see # ``PI_PROVIDER_ENV_KEYS`` in ``env_overrides.py`` — so this prefix carries # no secrets. "PI_", # NOTE: deliberately NO ``COPILOT_`` entry. ``COPILOT_GITHUB_TOKEN`` is a # GitHub PAT; if we forwarded it via this prefix allowlist it would leak # into every other CLI subprocess (Codex, Kimi, Claude Code, etc.). All # Copilot envs (config + credentials) flow through ``CLIInvocation.env`` # built by ``CopilotAdapter.build`` so they only reach the Copilot # subprocess. See ``env_overrides.py`` for the COPILOT_*_ENV_KEYS tuples. ) def build_cli_subprocess_env(overrides: dict[str, str] | None) -> dict[str, str]: """Return a subprocess ``env`` dict: safe inherited keys plus optional overrides.""" env: dict[str, str] = {} for key, value in os.environ.items(): if key in _SAFE_SUBPROCESS_ENV_KEYS or any( key.startswith(prefix) for prefix in _SAFE_SUBPROCESS_ENV_PREFIXES ): env[key] = value if overrides: env.update(overrides) return env