tests/test_lazy_secrets_dispatch.py::TestUpdatePathE2E ran the real
`hermes update --check` bare, so the child's `git fetch origin main`
hit github.com on every CI run. During the 2026-08-17 GitHub incident
the fetch stalled past the 30s subprocess timeout and both update tests
went red on main for hours with zero code change (slices seen on runs
32003200300 and 32011520139).
These tests assert the lazy-crypto / no-self-lock dispatch invariants,
not update connectivity. Rewrite all git remote URLs in the child to an
unreachable file:// path via GIT_CONFIG_* env overrides: the update path
still exercises its full parser/dispatch/fetch code, but the fetch now
fails in milliseconds, deterministically, offline. Exit code 1 was
already accepted by the assertions.
Before/after under a blackhole proxy simulating the outage:
OLD: TimeoutExpired after 20s (reproduces the CI failure)
NEW: completes in 0.2s, exit=1
Address blocking review on #86782 (trevorgordon981, 2026-08-15): the
previous lazy closures in main.py only deferred the module-level
"import secrets_cli" statement, but were themselves invoked at parse
time — so the chain main -> secrets_cli -> bitwarden -> cryptography
still ran eagerly on every command, including `hermes update --check`.
The closure indirection was dead laziness.
Move the laziness to where the crypto payload actually lives:
1. secrets_cli.py: drop the module-top "from agent.secret_sources
import bitwarden as bw" import. Each cmd_* handler now resolves the
backend via a local _load_bw() helper, which imports
agent.secret_sources.bitwarden on first use. register_cli() no
longer touches crypto at all — it only wires argparse structure.
2. _BWS_VERSION is duplicated in secrets_cli as a plain string so the
"install" subparser help text renders without importing the
backend. agent.secret_sources.bitwarden._BWS_VERSION stays the
source of truth; bump both together when pinning a new bws release.
3. Module-level PEP 562 __getattr__ resolves "secrets_cli.bw" lazily.
Existing upstream tests (test_secrets_bitwarden_non_tty.py) that
monkeypatch "hermes_cli.secrets_cli.bw.find_bws" keep working —
monkeypatch resolves the string one level deep, triggering
__getattr__, which imports the real bitwarden module and lets the
patch land on the same cached module object the handlers import.
4. main.py: revert the closure indirection back to a direct parse-time
_secrets_cli.register_cli() call — safe now that register_cli is
crypto-free by construction. The argparse wiring is again visible
at the call site (matching checkpoints.py / curator.py convention),
which addresses the original parse-time-vs-post-parse contract
concern from the previous review round.
Adds a decisive main()-level regression test requested by review:
test_main_update_check_crypto_absent_in_sys_modules spawns main() in a
subprocess with argv=['hermes', 'update', '--check'], patches
hermes_cli.main._cmd_update_check to short-circuit before any network,
and asserts cryptography.hazmat.bindings._rust stays out of
sys.modules both at dispatch time and after main() returns. This is
the exact invariant the Windows self-lock depends on; the previous
import-only tests could not observe the failure because parser
construction runs inside main().
Verification:
- scripts/run_tests.sh tests/test_lazy_secrets_import.py
tests/test_lazy_secrets_dispatch.py
tests/hermes_cli/test_secrets_bitwarden_non_tty.py
-> 13/13 passed (includes the new decisive test + the 2 upstream
tests that broke under the earlier _LazyBitwarden proxy).
- Sabotage run: same suite against the pre-fix main.py + secrets_cli.py
fails the new decisive test with "cryptography._rust loaded by main()
before update dispatch" — confirming the test guards the bug.
- Manual trace: at _cmd_update_check dispatch time, sys.modules
contains hermes_cli.secrets_cli (parse-time structure only) but NOT
agent.secret_sources.bitwarden and NOT cryptography._rust.
Refs: #86781
Refs: #83569
Address review feedback from trevorgordon981 on PR #86782:
1. **Pre-register parsers at parse-time, lazy-import backends only**
- secrets_cli.register_cli() and onepassword_secrets_cli.register_cli()
now run eager at parser-build time (no deferral past parse_args)
- Only the agent.secret_sources.bitwarden/onepassword imports are lazy
(inside each cmd_* handler via _load_bitwarden()/_load_onepassword())
- This eliminates the 'invalid choice' and infinite-recursion risks
2. **Known-source-names gate for env_loader registry**
- Only keys in {bitwarden, onepassword, op, 1password, bw} trigger the
registry import; a generic dict entry no longer forces crypto load
- Prevents unrelated config dicts from paying crypto cost
3. **End-to-end tests for the real dispatch paths**
- test_bitwarden_setup_help: runs real CLI subprocess with --help
- test_bitwarden_status/disable/onepassword_status: run real handlers
- test_update_check_clean/no_self_lock: run real update --check
- test_update_check_no_cryptography: sys.modules inspection (backup)
4. **Fix flaky test_turn_lease.py** (unrelated pre-existing failure)
The architecture guarantees:
- parse-time: zero cryptography load (all backends lazy)
- dispatch-time: crypto loads exactly once per secrets command
- update path: completely clean of cryptography._rust mapping
Refs #86781, #86782