Skip to content

fix(update): avoid cryptography self-lock on Windows - #77517

Closed
duclucky wants to merge 1 commit into
NousResearch:mainfrom
duclucky:fix/windows-update-cryptography-self-lock
Closed

fix(update): avoid cryptography self-lock on Windows#77517
duclucky wants to merge 1 commit into
NousResearch:mainfrom
duclucky:fix/windows-update-cryptography-self-lock

Conversation

@duclucky

@duclucky duclucky commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Prevents hermes update from loading Bitwarden's native cryptography extension into the updater process before its uv child replaces dependencies.

On Windows, importing cryptography maps cryptography/hazmat/bindings/_rust.pyd. The parent updater then holds the exact native image that uv pip install -e . needs to rename or delete during a cryptography version change, so the update deterministically fails with os error 5 and can leave the venv partially uninstalled.

The update path still loads the selected profile's .env, managed environment, and terminal config. It skips only external secret-source fetching, which is not needed for Git/dependency maintenance. Because the full argparse construction also imports secrets_cli, Bitwarden's cryptographic cache primitives are now imported only when an encrypted cache is actually read or written.

Related Issue

Fixes #73381

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • Tests (adding or improving test coverage)

Changes Made

  • Add a load_external_secrets switch to load_hermes_dotenv(), defaulting to the existing behavior.
  • Disable external secret-source fetching only for the update subcommand after profile arguments have been resolved.
  • Lazy-load cryptography primitives in Bitwarden's encrypted-cache operations so parser construction cannot map _rust.pyd.
  • Add subprocess regressions for both module startup and complete main() dispatch, plus controls for normal external-source and dotenv behavior.

How to Test

$env:TZ='UTC'
$env:LANG='C.UTF-8'
$env:HERMES_TEST_WORKERS='4'
.\.venv\Scripts\python.exe scripts\run_tests_parallel.py --paths 'tests/hermes_cli/test_update_secret_import_lock.py:tests/hermes_cli/test_env_loader.py:tests/hermes_cli/test_startup_fast_guards.py:tests/hermes_cli/test_update_check.py:tests/hermes_cli/test_update_venv_health.py:tests/hermes_cli/test_bitwarden_status.py:tests/hermes_cli/test_secrets_bitwarden_non_tty.py:tests/secret_sources/test_secret_source_registry.py' -q
.\.venv\Scripts\python.exe scripts\run_tests_parallel.py --paths tests/test_bitwarden_secrets.py -q -k encrypted_cache_falls_back_on_network_error
.\.venv\Scripts\ruff.exe check agent/secret_sources/bitwarden.py hermes_cli/main.py hermes_cli/env_loader.py tests/hermes_cli/test_update_secret_import_lock.py
.\.venv\Scripts\python.exe scripts/check-windows-footguns.py agent/secret_sources/bitwarden.py hermes_cli/main.py hermes_cli/env_loader.py tests/hermes_cli/test_update_secret_import_lock.py

Results on Windows 11 / Python 3.12.13:

  • 69 targeted tests passed
  • encrypted-cache fallback test passed
  • Ruff passed
  • Windows footgun scan passed for all 4 changed files

The full tests/test_bitwarden_secrets.py file has two existing Windows-specific failures unrelated to this patch: its install fixture archives bws while the Windows implementation correctly searches for bws.exe, and its POSIX 0o600 mode assertion observes 0o666 on NTFS. Those are not reported as passing here.

Checklist

Code

  • I've read the Contributing Guide
  • My commit message follows Conventional Commits
  • I searched open and merged PRs/issues and found no PR for the updater's self-lock
  • My PR contains only changes related to this fix
  • I've run the entire pytest tests/ -q suite and all tests pass
  • I've added tests for the bug
  • I've tested on Windows 11

Documentation & Housekeeping

  • Documentation update: N/A; behavior and public parameter are documented in code
  • cli-config.yaml.example: N/A; no config key added
  • CONTRIBUTING.md / AGENTS.md: N/A; no workflow change
  • Cross-platform impact considered; default behavior is unchanged outside the update invocation
  • Tool descriptions/schemas: N/A

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard platform/windows Native Windows-specific behavior or breakage area/install-update Installer, updater, packaging, wheels, doctor sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 3, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

All 40 PRs address or reference the Windows update-lock complex, spanning shim quarantine, launcher ancestry, venv and gateway holders, installer recreation, Desktop handoff, false-positive filtering, ZIP preservation, and updater-native-module self-locks. For #73381, #77517 uniquely prevents the updater itself from loading Bitwarden's cryptography _rust.pyd, while the other PRs address adjacent lock sources or recovery paths.

Related pull requests

Duplicates

#23353/#23408, #29358/#31712/#31806/#31808, #46726/#47569/#47610/#47621/#52044, #57852/#58343, and #74419/#74618/#74707/#75380/#75881/#76057 are overlapping families; the canonical landed implementations are #26677, #35257, #52044, and #75881 respectively. #68821 and #77517 are complementary rather than duplicates: the former hard-stops a still-locked shim, while the latter prevents the updater from mapping cryptography _rust.pyd itself.

Suggested consolidation

Keep #77517 open with the concrete salvage path already present—update-only external-secret suppression, lazy Bitwarden cryptography imports, and full-dispatch regression coverage—because no other diff addresses the updater's own _rust.pyd mapping. Close overlapping open work as duplicate of the cited merged implementations where applicable, retain #57852, #61752, #65726, #68821, and #70725 only for their distinct salvage cuts, and require author rebase/splitting for #70477, #75380, and #76057.

Complex graph

flowchart 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
    I73381(["issue #73381 (open)"])
    P77517["PR #77517 (open)"]
    P77517 -->|best fix| I73381
    class I73381 open
    class P77517 open
    class P77517 best
    class P77517 target
    click I73381 "https://github.com/NousResearch/hermes-agent/issues/73381"
    click P77517 "https://github.com/NousResearch/hermes-agent/pull/77517"
Loading

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 40 pull requests and 34 issues in this complex. Each diff was read against this issue; Assessment working set: 555 kB of PR diffs, 250 kB of issue/PR text, 146 kB of discussion (144 comments), 212 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@tneemo tneemo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent verification — reproduced and confirmed on Windows 11

Verified the diff on a real Windows 11 host (the exact platform where the bug bites). Applied the head files + ran the new regression test.

Result: 5/5 tests pass in tests/hermes_cli/test_update_secret_import_lock.py:

  • test_update_startup_does_not_import_bitwarden_or_cryptography
  • test_complete_update_dispatch_does_not_import_cryptography
  • test_normal_startup_still_loads_enabled_external_secret_source
  • test_dotenv_loading_is_preserved_when_external_secrets_are_skipped[True]
  • test_dotenv_loading_is_preserved_when_external_secrets_are_skipped[False]

Before/after check on the actual mechanism:

State cryptography in sys.modules after startup
current main ✅ loaded → maps _rust.pyd into the process (the self-lock)
with this diff not loaded for the hermes update path; still loaded for normal startup

Confirmed the root-cause mechanism exactly as described: on current main, importing agent.secret_sources.bitwarden pulls cryptography.hazmat.bindings._rust into the interpreter; with the lazy imports + load_external_secrets=False for the update subcommand, the updater process no longer holds the native image that uv needs to replace.

Design observations:

  • The sys.argv[1:2] != ["update"] gate in main.py is correctly placed after profile-flag stripping, so hermes -p profile update still routes correctly.
  • Moving AESGCM/HKDF/hashes imports inside the cache read/write/derive functions keeps the lazy-load semantic tight — only encrypted-cache ops pay the import.
  • The new flag default (load_external_secrets=True) preserves normal startup behavior — no regression for users who rely on Bitwarden env injection.

This is a clean, surgical fix for a nasty Windows-only self-lock. MERGEABLE, CI-ready tests included. Approving the diff as-is.

@duclucky
duclucky force-pushed the fix/windows-update-cryptography-self-lock branch from 80dd64d to a5b5928 Compare August 3, 2026 14:14
@duclucky

duclucky commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Rebased this PR onto current main (4a99ba195); the new head is a5b5928d5. git range-diff shows the patch itself is unchanged, and the branch is now 1 commit ahead / 0 behind.

Windows validation through the repository-mandated scripts/run_tests.sh wrapper:

  • tests/hermes_cli/test_update_secret_import_lock.py: 5 passed
  • env-loader/external-secret coverage (test_env_loader.py, test_env_loader_secret_sources.py, test_env_loader_applied_homes.py, test_env_loader_op_bootstrap.py): 44 passed
  • secret-source registry + non-TTY Bitwarden CLI coverage: 33 passed
  • tests/test_bitwarden_secrets.py: 16 passed, 2 unrelated Windows portability assertions failed (bws fixture name vs bws.exe, and POSIX 0o600 mode on Windows reporting 0o666). That test file is unchanged by this PR, and neither failing behavior is in the patch.
  • Ruff on all four changed files: passed

I also attempted tests/hermes_cli/test_cmd_update.py separately; it did not complete after more than five minutes on this Windows host, so I terminated that isolated test process tree and am not claiming it as passed.

The behavior boundary remains the same after rebase: normal startup still loads configured external secrets, while only the update dispatch skips external-secret fetching; Bitwarden's cryptography imports remain lazy and are loaded when encrypted-cache operations actually run.

@aare

aare commented Aug 10, 2026

Copy link
Copy Markdown

Hitting this exact bug on a clean system -- no gateway running, no other Hermes processes, just the updater locking its own _rust.pyd. The self-repair loop on every launch is painful. tneemo already verified and approved -- would love a second review to get this merged. Thanks!

teknium1 pushed a commit that referenced this pull request Aug 15, 2026
… base interpreter is uv-managed

Two gaps left every Windows git-checkout install unable to recover from
the exact failure state #83569 reports:

1. Self-lock detection. _detect_venv_python_processes() always excludes
   the calling process by design — a CLI hermes update IS the venv python.
   An updater that had already imported a native venv extension (the
   canonical one being cryptography.hazmat.bindings._rust, mapped while
   hermes_cli.main resolved external secret sources) passed every
   preflight and then died mid-sync with os error 5 when uv tried to
   rewrite the mapped .pyd, stranding the venv half-updated. A new
   preflight now refuses the sync before touching the checkout, writes
   the update-incomplete marker so the next fresh launch completes the
   install, and exits 2. Verified on a live Windows 11 host: after
   importing hermes_cli.main, tasklist /m _rust.pyd shows the .pyd mapped
   in the caller, and a peer process cannot open it read-write
   (Permission denied) — while a rename succeeds, matching how uv/pip
   actually fail (truncate+write, not rename).

2. Early-recovery install path. _early_recovery._run_repair_install used
   sys.executable -m pip unconditionally. Windows git checkouts install
   on a uv-managed base interpreter (python-build-standalone), whose
   EXTERNALLY-MANAGED marker makes plain pip abort with
   externally-managed-environment — the repair no-oped and the venv
   stayed broken. The repair now detects the PEP 668 marker, prefers
   uv pip install with VIRTUAL_ENV pointed at the project venv, and
   falls back to pip --break-system-packages when no uv binary exists.

Both fixes ship with subprocess/unit regressions (sabotage-verified):
the new tests fail on pre-fix code and pass with it. Complements #77517,
which keeps the updater from importing cryptography in the first place;
this PR is the defence-in-depth when any future path loads it anyway.

Fixes #83569
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #87241 with your commit cherry-picked onto current main — your authorship is preserved in git history. Thanks for the fix and the thorough regression tests! This was the root-cause fix for the Windows update self-lock loop (#86735).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows Desktop update fails — venv missing cryptography + Windows file locking causes uv pip install exit code 2

6 participants