6efab28726
Custom providers could only authenticate from a static credential (inline api_key or a key_env env var). Enterprise gateways -- SSO/OIDC brokers, cloud IAM, internal auth proxies -- issue short-lived bearers instead, so a value copied into .env is stale within the hour: long sessions start returning 401s and the user has to restart or run an external cron that rewrites .env. The existing `secrets.command` source does not cover this: it runs once per process at startup (subsequent calls are no-ops by design), so it cannot re-mint a credential mid-session. Add providers.<name>.key_cmd: a command that prints a token, wrapped at resolution in a zero-argument callable. Both wire clients already accept a callable api_key and invoke it per request (the Entra ID path established this), so chat_completions, codex_responses and anthropic_messages all work unchanged and always send a fresh credential. The callable also routes the Anthropic client through its per-request Authorization hook, which is what OAuth-gated gateway routes require -- so no per-vendor auth wiring is needed anywhere in core. - cached until shortly before the advertised expiry (60s leeway), so the helper runs about once per token lifetime rather than once per request - expiry is read from the OAuth 2.0 relative `expires_in` when present, and otherwise from an absolute ISO 8601 deadline (`expiry`, `expiresOn`), which is what CLI token helpers commonly print. Reading only `expires_in` treated those helpers as advertising no TTL at all, cached their token for the life of the process, and returned 401 on every request once the real deadline passed. ISO parsing reuses hermes_cli.auth._parse_iso_timestamp rather than adding another datetime parser. - no synthetic expiry: when no TTL is advertised, or the advertised one is unparseable or already past, the token is used and refreshed on 401 instead of re-minted on an invented schedule - stdout contract matches OAuth 2.0 token endpoints and existing agent helpers (bare token or JSON access_token/expires_in); multi-line output is rejected rather than guessed at, so a misconfigured helper surfaces as a clear error instead of a corrupt-credential 401 - precedence: explicit --api-key still wins; otherwise key_cmd beats a static api_key/key_env on the same entry - failures never include the helper's output (may hold a partial token) or the command string (may embed a client secret) Resolution happens on two paths. agent/auxiliary_client.py resolves named custom providers itself rather than calling _resolve_named_custom_runtime, so key_cmd is honoured in both: wiring only the runtime resolver leaves the main agent turn working while every auxiliary call (title generation, compression, vision, embedding) falls back to the no-key-required placeholder and 401s. Precedence is identical on both paths, so one config entry cannot yield two different credentials depending on which resolver the caller reached. Closes #84162 Signed-off-by: LordMelkor <kray@block.xyz>