fix(secrets): fall back to stale disk cache when bws live fetch fails - #41938
fix(secrets): fall back to stale disk cache when bws live fetch fails#41938jackjin1997 wants to merge 2 commits into
Conversation
|
Positive verification — reviewed the full diff and 5 tests. The fallback logic is sound: catch Edge cases covered by tests: no disk cache → re-raise, One note: the warning string includes the raw No issues found. Looks good to merge. |
|
Thanks for the careful review @liuhao1024. Quick note on the stderr concern: the |
|
Gentle nudge for a maintainer look — this one's been sitting two weeks. It fixes #41925 (a BWS DNS/transient failure silently wiping the secret set fleet-wide), is still CLEAN/mergeable on current main, and @liuhao1024 already did a positive diff+test verification above. Scope is a single fallback path in |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused availability fix. The stale-cache outage premise remains real on current main (agent/secret_sources/bitwarden.py:394-410), but this needs rework before it can be salvaged.
Problems
- The patch uses removed private helpers (
_read_disk_cache/_write_disk_cache). Current main moved them to the sharedDiskCacheAPI indb495b0fbaaa63ebd7f6404413730f98f0fdf76b; Bitwarden now uses_DISK_CACHE.readatagent/secret_sources/bitwarden.py:394. - The fallback at PR line 506 ignores
cache_ttl_seconds: 0. That conflicts with the current contract inagent/secret_sources/_cache.py:107-109: non-positive TTL disables cache reads and writes. - Catching every
RuntimeErroralso serves stale secrets after invalid credentials or malformed BWS output (agent/secret_sources/bitwarden.py:450-469). The source contract identifies stale fallback as appropriate for NETWORK/TIMEOUT, not AUTH_FAILED (agent/secret_sources/base.py:65-69).
Suggested changes
- Port the fallback and its fixtures to
_DISK_CACHE.read/.write. - Gate fallback on both caching being enabled and a positive TTL, with a zero-TTL regression test.
- Restrict fallback to classified network/timeout failures and test auth/parse failures still raise.
Automated hermes-sweeper review.
| secrets, warnings = _run_bws_list(bws, access_token, project_id, server_url) | ||
| try: | ||
| secrets, warnings = _run_bws_list(bws, access_token, project_id, server_url) | ||
| except RuntimeError as exc: |
There was a problem hiding this comment.
This catches auth failures and malformed BWS responses too. Current ErrorKind explicitly reserves stale fallback for NETWORK/TIMEOUT rather than AUTH_FAILED (agent/secret_sources/base.py:65-69); classify the error and only reuse stale secrets for those transient cases.
| # running without any secrets. Without this fallback a fleet of bots | ||
| # sharing one BWS project all stop working on a single network blip. | ||
| # `ttl_seconds=inf` bypasses the freshness check in _read_disk_cache. | ||
| if use_cache: |
There was a problem hiding this comment.
Please also require cache_ttl_seconds > 0 here. Current shared-cache semantics define TTL <= 0 as disabling cache reads and writes (agent/secret_sources/_cache.py:107-109), but this inf read would revive an existing disk entry when caching is explicitly disabled.
Without this, a single DNS hiccup or BWS outage at gateway startup leaves the whole fleet running with an empty credential pool — every model call fails until someone restarts after the network recovers. When a previous successful fetch already populated the disk cache, return those secrets with an explicit warning instead of raising RuntimeError. `use_cache=False` (explicit opt-out) still raises so manual flows like the setup wizard surface the original error. The disk cache is not re-written on the fallback path so a process restart still triggers a proper TTL re-check. Fixes NousResearch#41925
…te by error kind The stale-fallback branch called _read_disk_cache(), a helper removed in db495b0 when disk-cache logic moved to the shared DiskCache class — every fallback attempt raised NameError instead of serving cached secrets, silently defeating the PR's whole purpose. Port to _DISK_CACHE.read(). Also tighten the fallback per DiskCache's TTL contract and the secret-source error taxonomy: - Gate on cache_ttl_seconds > 0 so a caller that opted out of caching entirely (ttl=0) never gets a secret value that didn't come from a live fetch, even on the failure path. - Gate on _classify_bws_error(str(exc)) being NETWORK or TIMEOUT, reusing the existing classifier — an AUTH_FAILED or malformed-output failure must still raise, since serving stale secrets there would mask a real credential/config problem instead of a transient outage. Ported the test helpers off the removed _write_disk_cache to a direct JSON write (matching this file's existing disk-cache test convention) and added tests for the auth-failure, malformed-output, and zero-TTL gates. Reverting the fix and re-running confirms 7 of 8 stale-fallback tests fail with the original NameError.
0a88eb2 to
21965dc
Compare
|
Good catch on all three — verified each independently:
Reverted the fix and reran: 7 of 8 stale-fallback tests fail with the original |
|
Merged via #69051 — your two commits landed as-is (cherry-picked, authorship preserved), ported onto the current DiskCache API with the fallback gated by the shared ErrorKind taxonomy so AUTH_FAILED never serves stale secrets. The 7-test suite you wrote (opt-outs, timestamp preservation, auth/malformed still raising) was the deciding factor in picking this PR over the competing implementation. Thanks @jackjin1997! Fixes #41925. |
What does this PR do?
When
_run_bws_list()raises (DNS down, transient BWS outage, network blip),fetch_bitwarden_secrets()previously propagated theRuntimeErrorstraight through. Theenv_loadercaller catches it and the gateway just runs without any injected secrets — every model call then fails withProvider 'X' is set in config.yaml but no API key was found.. A fleet of bots sharing one BWS project all stop working on a single network glitch.This PR wraps the live fetch in a try/except and, when the disk cache already holds secrets from a previous successful fetch, returns those secrets (regardless of TTL) with an explicit warning instead of raising. The behaviour matches the existing fresh-cache path closely: same return shape, same in-process promotion, just an extra warning carrying the cache age + the original error so operators see what happened.
Related Issue
Fixes #41925
Type of Change
Changes Made
agent/secret_sources/bitwarden.py: when_run_bws_list()raisesRuntimeError, attempt_read_disk_cache(cache_key, ttl_seconds=float('inf'), home_path)to bypass freshness. On hit: promote into_CACHE, return(secrets, [warning]). On miss: re-raise the original exception.use_cache=Falsestill raises (explicit opt-out — used by the setup wizard so it can surface the real error). Disk cache is not re-written, so a process restart still does a proper TTL re-check.tests/test_bitwarden_secrets.py: 5 regression tests + a_seed_stale_disk_cachehelper:test_stale_disk_cache_returned_when_bws_fails— happy path: stale entry exists → secrets returned + warningtest_stale_fallback_warning_includes_cache_age— warning contains the cache age so operators can decide whether to acttest_no_stale_fallback_when_disk_cache_missing— no cache → re-raise (no silent empty secrets)test_stale_fallback_skipped_when_use_cache_false— explicit opt-out is honouredtest_stale_fallback_does_not_overwrite_disk_cache— diskfetched_atpreserved so the next process restart triggers a proper re-checkHow to Test
Checklist
Code
Documentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACode Intelligence
fetch_bitwarden_secrets(callers:bsm_pull_into_envenv loader path L642 + 2 setup-wizard call sites inhermes_cli/secrets_cli.py)_read_disk_cacheline 111-134 (re-used as-is viattl_seconds=float('inf')— no signature change),_CachedFetch.is_fresh(correctly returns True forinfso cache key lookup is unaffected)AI Disclosure
This bug was identified and fixed with AI assistance.