Conversation
|
Code Review PR #77592: fix(security): prevent multiplex dotenv credential leakage. LGTM. Clean security fix at the load_hermes_dotenv shared boundary. Covers all call sites. Tests prove Profile A and B keep distinct credentials. Approve. |
SummaryOne open PR addresses Issue #77562. Its diff moves the multiplex isolation guard to the shared load_hermes_dotenv() boundary, preventing routed profile .env values from entering process-global os.environ while retaining profile-private external secret hydration, with regression coverage for cross-profile isolation and provider refresh. Related pull requests
Suggested consolidationkeep open with a salvage path: retain #77592's shared-loader guard, profile-private hydration, and both regression tests as the concrete implementation path for #77562; no duplicate PRs are present. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I77562(["issue #77562 (open)"])
P77592["PR #77592 (open)"]
P77592 -->|fixes| I77562
class I77562 open
class P77592 open
class P77592 target
click I77562 "https://github.com/NousResearch/hermes-agent/issues/77562"
click P77592 "https://github.com/NousResearch/hermes-agent/pull/77592"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 6 kB of PR diffs, 15 kB of issue/PR text, <1 kB of discussion (1 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
|
Ran into this same clobber in production and filed #77969 / #77970 before triage pointed me here — that issue is now closed as a duplicate of #77562, and this PR's guard is the broader fix, so I'd rather help this one land than compete with it. One observation from the field, plus tests you're welcome to take. Field data. Five Discord bots in one multiplexed gateway. The bit I'd pin down. This PR returns early on Cheap insurance is a test asserting the startup path still populates the process env, so the ordering invariant fails loudly in CI instead of in someone's guild. Tests on offer. #77970 carries three, written against this code path and passing on my install:
Happy to open them as a PR against this branch, or just paste them here — whichever suits. I'll close #77970 once this lands. |
|
suggesting changes The shared loader guard currently fires whenever the process-wide multiplex flag is true, even when no routed profile scope is installed. That makes startup/shared environment loading depend on the current module-import order: with multiplexing active but no scope, current Security evidence:
Not checked:
Signed: GPT-5.6-sol-xhigh in Codex |
c803511 to
1dfd324
Compare
|
Addressed the startup-scope review concern in
Validation after the final rebase onto |
|
looks mergeable Reviewed the multiplex dotenv isolation change. The scoped loader branch keeps routed profile credentials out of process-global os.environ while preserving unscoped startup behavior. Current-main reproduction showed the pre-fix leak; focused PR-head isolation and secret-source tests passed. No residual source-backed bypass was found. Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
1dfd324 to
58a421f
Compare
…gle-profile control test Follow-up to the #77592 salvage: emit a once-per-home debug line where the multiplex guard skips the process-global dotenv load (requested on #77562), and port the single-profile control test from #77970 so the guard is pinned to the multiplex flag rather than the home override alone. Co-authored-by: DonShelly <25538402+DonShelly@users.noreply.github.com>
…gle-profile control test Follow-up to the #77592 salvage: emit a once-per-home debug line where the multiplex guard skips the process-global dotenv load (requested on #77562), and port the single-profile control test from #77970 so the guard is pinned to the multiplex flag rather than the home override alone. Co-authored-by: DonShelly <25538402+DonShelly@users.noreply.github.com>
…gle-profile control test Follow-up to the #77592 salvage: emit a once-per-home debug line where the multiplex guard skips the process-global dotenv load (requested on #77562), and port the single-profile control test from #77970 so the guard is pinned to the multiplex flag rather than the home override alone. Co-authored-by: DonShelly <25538402+DonShelly@users.noreply.github.com>
|
Thanks for this PR. Merged via #101244 (0fd9218) on current main — routed multiplex profiles stop leaking .env into os.environ or borrowing default creds. This PR was one of the vehicles for that merge: your commits were cherry-picked into #101244 with your git authorship preserved. Closing this one since the same change is now on main. If anything from your original change is still missing on main >= 0fd9218, please open a fresh PR/issue against main and tag it. Thanks again. |
…gle-profile control test Follow-up to the NousResearch#77592 salvage: emit a once-per-home debug line where the multiplex guard skips the process-global dotenv load (requested on NousResearch#77562), and port the single-profile control test from NousResearch#77970 so the guard is pinned to the multiplex flag rather than the home override alone. Co-authored-by: DonShelly <25538402+DonShelly@users.noreply.github.com>
What does this PR do?
Prevents turn-scoped
load_hermes_dotenv()calls from copying a routed profile's credentials into process-globalos.environwhile a multiplex profile scope is active.The root cause was that
load_hermes_dotenv()always reached_load_dotenv_with_fallback(..., override=True), even when the active Hermes home came from a routed multiplex profile. The gateway helper guarded one reload call site, but lazy imports, cron, and other callers could invoke the loader directly and bypass that guard.The guard now lives at the shared loader boundary and requires both:
This keeps unscoped gateway startup loading unchanged. Inside a routed profile scope, the loader refreshes external secret providers through
hydrate_profile_secret_sources(), which writes to the existing profile-private snapshot, and returns without mutating the shared process environment.Related Issue
Fixes #77562
Type of Change
Changes Made
hermes_cli/env_loader.py: skip process-global dotenv mutation only during an active routed multiplex profile scope, while retaining profile-private external-secret hydration.tests/gateway/test_multiplex_credential_isolation.py: prove two routed profiles retain distinct credentials and channel allowlists while the process-global allowlist remains unchanged.tests/test_env_loader_secret_sources.py: prove unscoped multiplex startup still loads.env, and routed profile loading still hydrates Bitwarden-backed credentials without exporting bootstrap/provider secrets globally.Review Follow-up
This revision addresses the startup-order concern raised by @DonShelly and @egilewski:
get_hermes_home_override() is not None;DISCORD_ALLOWED_CHANNELS;DISCORD_ALLOWED_CHANNELSvalues plus an unchanged process-global value.The routed-scope discriminator follows the boundary identified by @DonShelly in #77970, while this PR keeps its existing external secret-provider hydration behavior and coverage.
How to Test
scripts/run_tests.sh tests/test_env_loader_secret_sources.py -k multiplex_without_profile_scope_still_loads -qfailed because
DISCORD_ALLOWED_CHANNELSremained unset instead of loading123,456.scripts/run_tests.sh tests/gateway/test_multiplex_credential_isolation.py tests/test_env_loader_secret_sources.py -q→ 25 passed.
→ 91 passed across 9 files.
git diff --check, andscripts/check-windows-footguns.pyon the three changed files:→ all passed.
scripts/run_tests.sh -j 16→ 30,678 passed, 22 failed, 264 skipped across 2,816 files in 1,313.8s.
-j 4parameters on this branch and a cleanupstream/mainworktree at762610538:→ both produced 698 passed, 21 failed, 6 skipped, with the same failing tests and the same separate import-error file. The extra FIFO timing failure seen only in the 16-worker full run passed in both four-worker comparisons.
GitHub
mainadvanced after that full comparison. The final commit was rebased ontofe5e7799f; none of the 20 intervening commits touched this PR's three files, and the 91-test affected-surface suite passed again after the rebase. The full-suite checkbox remains unchecked because the repository-wide suite has existing macOS/environment failures.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/A (function behavior documented in its docstring)cli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
No screenshot is applicable because this is a process-level credential-isolation fix with no UI change.