A config.yaml with an explicit empty/null `providers:` key parses to None in
PyYAML. `cfg.get("providers", {})` only returns the {} default when the key is
*absent*, so an explicit null yields None instead of an empty mapping. master
already guards each read with `isinstance(providers_cfg, dict)`, but the value
fed to that guard is still a footgun: the natural chained form
`cfg.get("providers", {}).get(...)` (already fixed once in api/config.py)
crashes with AttributeError on None.
Harden every providers-key read in the three remaining files to
`cfg.get("providers") or {}` so an explicit null degrades to an empty dict at
the source, matching the existing api/config.py convention. Sites hardened:
api/onboarding.py:578 (_provider_api_key_present)
api/providers.py:1051 (_provider_has_shadowed_codex_oauth_value)
api/providers.py:1219 (_provider_has_key)
api/providers.py:1265 (_get_provider_api_key)
api/providers.py:2348 (get_providers catalog)
api/providers.py:2781 (_clean_provider_key_from_config)
api/routes.py:5045 (_context_length_config_api_key_for_provider)
api/routes.py:5110 (_context_length_lookup_inputs_for_model)
api/routes.py:15916 (_handle_live_models)
Adds tests/test_none_providers_config_guard.py: per-file source-form pins (fail
on master / on any single-file revert) plus behavioural checks driving the real
functions with `providers: None` to prove no crash and parity with the
empty-mapping config. Updates the existing test_issue3717 routes-string pin to
match the hardened form.
This is a focused salvage of grab-bag PR #3967 -- only the None-providers
hardening is taken; the PR's unrelated changes (account-usage worker-pool
refactor, function removals) are intentionally dropped.
Co-authored-by: lidi1011 <lidi1011@users.noreply.github.com>
Codex re-gate found the registry-derived scrub still missed credential env vars
the agent resolves via raw os.getenv() that aren't in PROVIDER_REGISTRY:
CUSTOM_API_KEY (hermes_cli/models.py), AWS_BEARER_TOKEN_BEDROCK
(hermes_cli/model_switch.py), AZURE_ANTHROPIC_KEY (runtime_provider.py), and the
boto3 AWS_* credential family (agent/bedrock_adapter.py). Same cross-profile leak
class on custom/AWS-backed provider quota + detached-worker paths.
- Add _NON_REGISTRY_AGENT_CREDENTIAL_ENV_NAMES fail-closed set, union into
_profile_secret_env_names so BOTH leak sites pick it up.
- _account_usage_subprocess_env now relies on the centralized scrub set (WebUI-
only fallback only if the api.profiles import fails), dropping the partial
local AWS pair.
- 2 more regressions (quota subprocess + detached worker) covering CUSTOM_API_KEY
+ AWS family, fail without the fix.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Codex gate caught that the new load_pool() path runs under the process-default
profile (sibling endpoints /api/providers + /api/models/live already wrap).
Without it a multi-profile client sees+seeds the default profile's pool
(#4247/#4067 profile-isolation class).
Custom/aliased providers with keys in the credential pool (added via
hermes auth add) were reported unconfigured in the WebUI because provider
detection only checked env vars. Add _has_explicit_pool_credentials() that
checks the pool while filtering the ambient gh_cli entry (so Copilot doesn't
falsely show configured on any machine with gh installed), cache-routed to
avoid the cold per-provider load_pool latency, and resolve provider aliases
before load_pool() at every retrieval site so detection and retrieval read
the same pool (glm->zai, github->copilot, etc.). /api/models/live wrapped in
profile scope; getattr-safe runtime_api_key for OAuth-only entries.
Co-authored-by: akrhin <akrhin@users.noreply.github.com>
Opus stage-338 review SHOULD-FIX: silent drop at api/providers.py:1049
was diagnostically opaque. logger.warning() now surfaces the bad
config entry so operators can spot misconfigurations.
Co-authored-by: Opus advisor <opus-advisor@hermes.local>