fix(secrets): route auxiliary-client provider key reads through the profile secret scope

OPENROUTER_API_KEY (client construction, unavailability description,
max-tokens param heuristic), OPENAI_API_KEY (custom runtime + explicit
custom endpoint), named-custom-provider key_env, and auxiliary task-config
key_env now resolve via a shared _scoped_key_env helper (get_secret with
UnscopedSecretError → os.environ fallback for unscoped CLI/startup probes).
This commit is contained in:
Teknium 2026-08-02 00:54:06 -07:00
parent 5c6cc38010
commit ca5ce1110b
1 changed files with 30 additions and 7 deletions

View File

@ -1133,6 +1133,29 @@ def _nous_min_key_ttl_seconds() -> int:
return 1800
def _scoped_key_env(name: str) -> str:
"""Read a provider API key env var through the profile secret scope.
Auxiliary-client resolution runs both inside agent turns (secret scope
installed its verdict is authoritative under multiplex, so a scoped
miss must NOT borrow another profile's process-env key) and on unscoped
startup/CLI probe paths, which keep the legacy ``os.environ`` read via
the ``UnscopedSecretError`` fallback (Slack pattern, #59739).
"""
if not name:
return ""
try:
from agent.secret_scope import UnscopedSecretError, get_secret
try:
return (get_secret(name) or "").strip()
except UnscopedSecretError:
pass
except Exception:
pass
return (os.getenv(name) or "").strip()
# ── Codex Responses → chat.completions adapter ─────────────────────────────
# All auxiliary consumers call client.chat.completions.create(**kwargs) and
# read response.choices[0].message.content. This adapter translates those
@ -2470,7 +2493,7 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op
# the OPENROUTER_API_KEY env-var path rather than failing outright.
logger.debug("Auxiliary client: OpenRouter pool exhausted, trying OPENROUTER_API_KEY")
or_key = explicit_api_key or os.getenv("OPENROUTER_API_KEY")
or_key = explicit_api_key or _scoped_key_env("OPENROUTER_API_KEY")
if not or_key:
_mark_provider_unhealthy("openrouter", ttl=60)
return None, None
@ -2487,7 +2510,7 @@ def _describe_openrouter_unavailable() -> str:
return "OpenRouter credential pool has no usable entries (credentials may be exhausted)"
if not _pool_runtime_api_key(entry):
return "OpenRouter credential pool entry is missing a runtime API key"
if not str(os.getenv("OPENROUTER_API_KEY") or "").strip():
if not _scoped_key_env("OPENROUTER_API_KEY"):
return "OPENROUTER_API_KEY not set"
return "no usable OpenRouter credentials found"
@ -3119,7 +3142,7 @@ def _resolve_custom_runtime() -> Tuple[Optional[str], Optional[str], Optional[st
if not isinstance(runtime, dict):
openai_base = os.getenv("OPENAI_BASE_URL", "").strip().rstrip("/")
openai_key = os.getenv("OPENAI_API_KEY", "").strip()
openai_key = _scoped_key_env("OPENAI_API_KEY")
if not openai_base:
return None, None, None
runtime = {
@ -5925,7 +5948,7 @@ def resolve_provider_client(
custom_base = _to_openai_base_url(explicit_base_url).strip()
custom_key = (
(explicit_api_key or "").strip()
or os.getenv("OPENAI_API_KEY", "").strip()
or _scoped_key_env("OPENAI_API_KEY")
or _read_main_api_key_if_same_host(custom_base)
or "no-key-required" # local servers don't need auth
)
@ -6021,7 +6044,7 @@ def resolve_provider_client(
custom_key = (custom_entry.get("api_key") or "").strip()
custom_key_env = (custom_entry.get("key_env") or custom_entry.get("api_key_env") or "").strip()
if not custom_key and custom_key_env:
custom_key = os.getenv(custom_key_env, "").strip()
custom_key = _scoped_key_env(custom_key_env)
custom_key = custom_key or "no-key-required"
if custom_key == "no-key-required":
logger.warning(
@ -6843,7 +6866,7 @@ def auxiliary_max_tokens_param(value: int, *, model: Optional[str] = None) -> di
misses the case where a custom base URL serves e.g. ``gpt-5.4``.
"""
custom_base = _current_custom_base_url()
or_key = os.getenv("OPENROUTER_API_KEY")
or_key = _scoped_key_env("OPENROUTER_API_KEY")
# Use max_completion_tokens for direct OpenAI-compatible providers that reject
# max_tokens on newer GPT-4o/o-series/GPT-5-style models.
_custom_host = base_url_hostname(custom_base) or ""
@ -7304,7 +7327,7 @@ def _resolve_task_provider_model(
task_config.get("key_env") or task_config.get("api_key_env") or ""
).strip()
if cfg_key_env:
cfg_api_key = os.getenv(cfg_key_env, "").strip() or None
cfg_api_key = _scoped_key_env(cfg_key_env) or None
cfg_api_mode = str(task_config.get("api_mode", "")).strip() or None
# 'auto' is a sentinel meaning "inherit from main runtime / auto-detect", not