fix(cron): pull Bitwarden Secrets Manager secrets in run_job#33479
fix(cron): pull Bitwarden Secrets Manager secrets in run_job#33479briandevans wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR fixes a regression where the cron scheduler did not load Bitwarden Secrets Manager (BSM) secrets, causing cron jobs that depend on BSM-managed credentials to fail with HTTP 401. The fix invokes _apply_external_secret_sources during _run_job_impl so secrets land in os.environ before the agent is constructed.
Changes:
- Added BSM secret resolution step in
_run_job_implafter the dotenv load, with errors logged and swallowed. - Added a regression test verifying BSM secrets reach
os.environbeforeAIAgentconstruction. - Added a test verifying that BSM apply failures do not block job execution.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| cron/scheduler.py | Calls _apply_external_secret_sources during job execution to populate BSM secrets into env. |
| tests/cron/test_scheduler.py | Adds TestRunJobBSMSecretResolution covering successful BSM apply and failure tolerance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from hermes_cli.env_loader import _apply_external_secret_sources | ||
| _apply_external_secret_sources(_get_hermes_home()) | ||
| except Exception: | ||
| logger.debug("BSM secret apply failed", exc_info=True) |
| def test_run_job_applies_bitwarden_secrets(self, tmp_path, monkeypatch): | ||
| """BSM apply must run during run_job so the secret lands in os.environ | ||
| before AIAgent is constructed.""" | ||
| from agent.secret_sources.bitwarden import FetchResult | ||
| from hermes_cli import env_loader | ||
|
|
||
| # Clean per-process dedup state so the apply path actually runs. | ||
| env_loader._SECRET_SOURCES.clear() | ||
| env_loader.reset_secret_source_cache() | ||
|
|
| env_loader._SECRET_SOURCES.clear() | ||
| env_loader.reset_secret_source_cache() |
| env_loader._SECRET_SOURCES.clear() | ||
| env_loader.reset_secret_source_cache() |
|
|
||
| with patch("cron.scheduler._hermes_home", tmp_path), \ | ||
| patch("cron.scheduler._resolve_origin", return_value=None), \ | ||
| patch("dotenv.load_dotenv"), \ |
| import agent.secret_sources.bitwarden as bw_module | ||
| monkeypatch.setattr(bw_module, "apply_bitwarden_secrets", _fake_apply) |
|
@copilot All findings addressed in commit bdcdd1f73:
|
|
CI audit — both test failures are pre-existing baselines on clean
Reproduced both on |
bdcdd1f to
4dcfb54
Compare
4dcfb54 to
47fe484
Compare
47fe484 to
559f6a7
Compare
559f6a7 to
3cb49b3
Compare
3cb49b3 to
6aa1cdd
Compare
6aa1cdd to
e2e8b9a
Compare
e2e8b9a to
a43af2d
Compare
The cron scheduler called bare `dotenv.load_dotenv()` and never invoked the BSM apply path. Any cron job that needed a BSM-managed credential (Discord token, provider API key, etc.) saw the .env placeholder string instead of the real value and failed with HTTP 401, while the gateway and the CLI happily resolved the same secrets via `load_hermes_dotenv()`. Add a follow-up call to `_apply_external_secret_sources(_get_hermes_home())` after the dotenv reload so cron picks up the same secrets the gateway does. The `_APPLIED_HOMES` process-level dedup added in NousResearch#32271 (de76f4d) makes calling this on every tick effectively free after the first pull. A failing BSM backend must never block job execution, so the call is wrapped in a debug-logged try/except. This is the minimal additive change rather than the full substitution of `load_hermes_dotenv()`: existing tests patch `dotenv.load_dotenv` directly via 19 sites, and replacing the dotenv call would change path-loading semantics (project_env loading) the scheduler has never exercised. The bug is specifically missing BSM resolution; this fix covers exactly that. Fixes NousResearch#33465
Three follow-ups on PR NousResearch#33479: 1. Promote `_apply_external_secret_sources` to a public `apply_external_secret_sources` in `hermes_cli.env_loader`, with a backward-compatible alias for existing tests that imported the underscore name. Cron now imports the public symbol instead of reaching into a name-mangled private attribute. 2. Replace the inline cleanup at the end of the two new BSM tests with an autouse setup+teardown fixture so `_SECRET_SOURCES` / the `_APPLIED_HOMES` dedup set are reset around every test even when an assertion fails partway through. `test_scheduler.py` is large and shares enough state that a leak would manifest in sibling tests. 3. Minor docstring fix in `reset_secret_source_cache` to track the new public name. Patching of `dotenv.load_dotenv` and `agent.secret_sources.bitwarden.apply_bitwarden_secrets` remains correct because both imports inside the production code are function-local (deferred to call time), so the monkeypatch on the source module is honoured. Confirmed by passing tests under the original PR as well as this follow-up.
a43af2d to
b42d2a5
Compare
What does this PR do?
The cron scheduler called bare
dotenv.load_dotenv()and never invoked the Bitwarden Secrets Manager (BSM) apply path. Any cron job that needed a BSM-managed credential (Discord token, provider API key, etc.) saw the.envplaceholder string instead of the real value and failed with HTTP 401, while the gateway and the CLI happily resolved the same secrets viaload_hermes_dotenv().This PR adds a follow-up call to
_apply_external_secret_sources(_get_hermes_home())immediately after the existing dotenv reload in_run_job_impl, so cron picks up the same secrets the gateway does. The_APPLIED_HOMESprocess-level dedup added in #32271 (de76f4d) makes calling this on every tick effectively free after the first successful pull. A failing BSM backend must never block job execution, so the call is wrapped in adebug-loggedtry/except.Why additive instead of switching to
load_hermes_dotenv()?The issue's suggested fix is to replace
from dotenv import load_dotenv; load_dotenv(...)withload_hermes_dotenv(...). I chose the minimal additive change instead for two reasons:dotenv.load_dotenvdirectly (seetests/cron/test_scheduler.py,tests/cron/test_cron_profile.py,tests/cron/test_cron_workdir.py).load_hermes_dotenvbindsload_dotenvat module-import time insidehermes_cli/env_loader.py, so those patches would no longer intercept and the test suite would need a coordinated rewrite.load_hermes_dotenvalso loads aproject_env(<repo>/.env) when passed one. The scheduler has never loaded a project env, so switching to it would silently broaden the load surface beyond what the bug requires.The bug is specifically the missing BSM apply step. Calling
_apply_external_secret_sourcesdirectly delivers exactly that, leaves the existing dotenv flow (and its test contract) untouched, and is the same stepload_hermes_dotenvitself invokes after its dotenv calls._apply_external_secret_sourcesis already a stable, directly-tested API (seetests/test_env_loader_secret_sources.pyandtests/test_bitwarden_secrets.py).Related Issue
Fixes #33465
Type of Change
Changes Made
cron/scheduler.py— after the dotenv reload in_run_job_impl, lazily import_apply_external_secret_sourcesand call it with the cron job's HERMES_HOME so BSM secrets land inos.environbeforeAIAgentis constructed; swallow any exception with adebuglog so a failing backend can't block job execution.tests/cron/test_scheduler.py— addTestRunJobBSMSecretResolutionwith two cases: (1) BSM-enabled config makes the apply path putANTHROPIC_API_KEYintoos.environbeforeAIAgentinit; (2) a raising BSM backend does not blockrun_job. An autouse fixture importsrun_agenteagerly so its module-levelload_hermes_dotenvruns once against the ambient HERMES_HOME and doesn't mask whether the scheduler itself is invoking the apply path.How to Test
origin/main(before this PR): configure~/.hermes/config.yamlwithsecrets.bitwarden.enabled: trueand a valid BSM project, set the provider API key value only in BSM (not in.env), and observe that any cron-scheduled job receives HTTP 401 on first tick while the gateway resolves the same key cleanly.uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/cron/test_scheduler.py::TestRunJobBSMSecretResolution -v— both new cases pass.uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/cron/ tests/test_env_loader_secret_sources.py tests/test_bitwarden_secrets.py -v— 215 existing + new tests all pass; no regressions.Checklist
Code
fix(scope):,feat(scope):, etc.)Documentation & Housekeeping
docs/, docstrings) — N/A (inline comments cover the why)cli-config.yaml.exampleif I added/changed config keys — N/A (no new config keys)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
N/A.