fix(secrets): warn on legacy config shapes that silently load no secrets - #78575
Open
0xr00tf3rr3t wants to merge 1 commit into
Open
fix(secrets): warn on legacy config shapes that silently load no secrets#785750xr00tf3rr3t wants to merge 1 commit into
0xr00tf3rr3t wants to merge 1 commit into
Conversation
Two `secrets:` shapes are read by nothing on current main and emit no
diagnostic whatsoever — the user gets zero credentials and no explanation.
1. `secrets.provider` — the single-backend selector that predates source
composition. Nothing reads it.
2. `secrets.<source>` as a scalar rather than a mapping — coerced to {} in
_ordered_enabled_sources (registry.py:286), so is_enabled() returns
False and the source never runs.
Both are exactly what a config written against the earlier iteration looks
like, so this lands on the users who adopted secret sources earliest. It is
especially hard to spot when systemd also injects the same env file via
EnvironmentFile: everything keeps working while the source contributes
nothing, so the config looks alive and is dead until you start Hermes from
a plain shell or move to another host.
Adds a warning for each, following the existing 'secrets.sources names
unknown source(s)' precedent in the same function. Diagnostics only — no
resolution behavior changes, no config keys added, no new surface.
Tests exercise the real orchestrator against a real temp HERMES_HOME with
real helper subprocesses. The 5 warning assertions fail without this patch
and pass with it; 9 further tests pin that resolution is unchanged (legacy
stays inert, current schema still loads, half-migrated configs still work,
disabled stays disabled) and pass either way. One test asserts the warning
never echoes the configured value, which can be a command string.
0xr00tf3rr3t
force-pushed
the
fix/secrets-legacy-config-silent
branch
from
August 4, 2026 15:54
2e3fde4 to
ad91633
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Two
secrets:config shapes are read by nothing on currentmainand emit no diagnostic whatsoever. The user gets zero credentials and no explanation for why.secrets.provider— the single-backend selector that predates source composition. Nothing reads this key.secrets.<source>as a scalar instead of a mapping — coerced to{}atagent/secret_sources/registry.py:286, sois_enabled()returnsFalseand the source never runs.Both are exactly what a config written against the earlier iteration of the secrets work looks like, so this lands on the people who adopted secret sources earliest.
This adds a warning for each. Diagnostics only — no resolution behavior changes, no config keys added, no new surface.
Reproducing on current main
Silence. The helper is never run,
MY_API_KEYnever loads, and nothing says why.The failure mode that makes this worth fixing: it is especially hard to spot when the same env file is also injected by a systemd
EnvironmentFile=. Everything keeps working while the Hermes source contributes nothing — the config looks alive and is dead. You discover it the day you runhermes chatfrom a plain shell, or move the profile to a host without that unit. I hit exactly this on my own deployment; the block had been inert for weeks while systemd quietly did the work.Related Issue
No existing issue — found while migrating a config from the pre-composition schema.
Context: the
secrets.providerselector was intentionally dropped during the salvage of #44509 into #69266, because mutually-exclusive selection would have regressed multi-source composition. That was the right call. This PR is only about the migration edge it left behind: a config written against the old shape fails closed and silent.Type of Change
Changes Made
agent/secret_sources/registry.py(+21, one function, purely additive)_ordered_enabled_sources()warns once whensecrets.provideris present, naming the working replacement (secrets.<source>.enabled: true).Both follow the existing
secrets.sources names unknown source(s)precedent in the same function — same logger, same phrasing style, same place.tests/secret_sources/test_legacy_secrets_config.py(new, 14 tests)How to Test
Tests run against the real orchestrator with a real temp
HERMES_HOMEand real helper subprocesses (chmod +xshell scripts) — no mocks, per the rubric's E2E guidance.Verification that the tests actually test the fix:
The 9 that pass either way are the point: they pin that resolution is unchanged — legacy shapes stay inert (they do not start working), the current schema still loads, a half-migrated config (
provideralongside a validcommandmapping) still loads its valid section, and a disabled source stays disabled.One test asserts the warning never echoes the configured value, since a
commandsection's value is a shell string that can name sensitive paths.Regression surface:
Pre-existing failures unrelated to this change, present identically on unmodified
main:tests/tools/test_browser_secret_exfil.pyandtests/tools/test_image_source.py(verified by stashing the patch and re-running), plustests/gateway/relay/collection errors from a missingpytest_asyncioin my environment.Checklist
Code
fix(secrets):)Documentation & Housekeeping
cli-config.yaml.exampleNotes for reviewers
Two judgment calls worth flagging:
Warn rather than raise. A hard failure would be more visible, but the secret-source design is deliberately fail-open —
apply_allnever blocks startup, and every source degrades to "no value." Raising here would break that contract for a config that is merely stale. The warning is emitted once per startup via the same logger as the neighbouringsecrets.sourcescheck.Nothing is "fixed" into working. A legacy config keeps loading nothing; it just says so now. Silently starting to honor
secrets.providerwould resurrect the mutually-exclusive selector that was intentionally removed, and would change behavior for anyone whose stale key names a source they no longer want enabled.