Skip to content

fix(cron): resolve bash for .sh jobs via shared Windows-aware resolver (#46332) - #77532

Open
andrexibiza wants to merge 1 commit into
NousResearch:mainfrom
andrexibiza:fix/cron-bash-resolver
Open

fix(cron): resolve bash for .sh jobs via shared Windows-aware resolver (#46332)#77532
andrexibiza wants to merge 1 commit into
NousResearch:mainfrom
andrexibiza:fix/cron-bash-resolver

Conversation

@andrexibiza

@andrexibiza andrexibiza commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Related #23405 #23489 #44350 #46332 #46364 #47837 #52204 #60617 #72697 #77393

What does this PR do?

Fixes Layer 1 of issue #46332 (the interpreter-selection half): cron .sh/.bash jobs on native Windows must resolve bash through the repository's shared Windows-aware resolver, never a raw shutil.which("bash") that lands on the System32 WSL launcher.

cron/scheduler.py _run_job_script() still did:

_bash = shutil.which("bash") or ("/bin/bash" if os.path.isfile("/bin/bash") else None)

On Windows 10/11, shutil.which("bash") returns C:\Windows\System32\bash.exe (the WSL launcher — on PATH by default, ahead of Git for Windows). With no distro installed it exits 1; with a distro it cannot run Windows paths at all — either way, .sh cron watchdogs fail with exit 127 even though Git Bash is installed.

Now it routes through tools.environments.local._find_bash() — the shared resolver that already encodes the correct precedence (Hermes portable Git → Git for Windows install dirs → PATH lookup with a start probe), merged via #47837:

  • RuntimeError (Windows, no usable bash anywhere — the only "bash" is a WSL stub that cannot start) → the actionable error message, instead of spawning the stub.
  • Any other resolver failure (e.g. module unavailable in an embedded context) → degrades to the historical PATH lookup, unchanged behavior.

Layer 2 (Windows bash-path conversion) is already covered by the sibling PR #77393 (_bash_safe_path, the MSYS convention) — this PR deliberately does not re-touch that line, so the two halves merge without colliding.

Addressing existing review commentary

@GottZ (#46332 triage, 2026-07-28): "The reported cause requires both a Git-Bash-aware shared resolver that cannot stop at a WSL launcher and a Windows-only Bash-path conversion; #52204 is the closest consolidated implementation, but its PATH continuation still needs correction."

This PR implements exactly the first requirement by reusing the shared resolver instead of re-implementing precedence — sidestepping the PATH-continuation flaw the triage flagged in #52204. The path conversion half ships separately in #77393.

@teknium1 (review of #72697): that diff "bypasses the repository's shared configured/PortableGit/known-install resolver precedence and ... its POSIX-host test cannot exercise Windows serialization."

Agreed — this PR does the opposite: it calls the shared resolver directly (full PortableGit/known-install precedence inherited), and the regression tests force the Windows resolution path via monkeypatched _find_bash/shutil.which on any host.

Prior art and consolidation map

The #46332 umbrella has accumulated 13+ PRs. Current state on this class:

Half Canonical Status
Layer 1 (resolver) this PR open, suite green locally
Layer 2 (path form) #77393 open, all CI green, CLEAN
Stale/superseded #44350, #23405, #23489 (path-only, May base) reconciled → close
Stale/superseded #46364, #52204 (resolver, Jun/Jul base, now conflicting), #60617 (no CI), #72697 (partial per review) reconciled → close

Credit where due: #46364 (Tranquil-Flow) and #52204 (Frowtek) diagnosed the cron-side launcher selection and proposed Git-Bash-over-WSL resolution; #60617 (paulhopcraft-dot) proposed skipping the launcher stub. This PR builds on their diagnosis with the already-merged shared resolver, which covers their cases and the portable-Git install location theirs missed.

How to Test

  1. uv run --frozen pytest -q tests/cron/test_cron_no_agent.py tests/cron/test_scheduler.py
  2. uv run --frozen ruff check cron/scheduler.py tests/cron/test_cron_no_agent.py

Verified locally on this branch (Windows host, main venv): 71 passed (cron suite incl. 3 new resolver regression tests), ruff clean, git diff --check clean. The regression tests force the Windows resolution path via monkeypatches so they exercise the fix on any host; the shared resolver itself is covered by its own test file (tests/tools/test_windows_native_support.py).

Checklist

Fixes #46332

NousResearch#46332)

cron _run_job_script() resolved bash with a raw shutil.which("bash"); on
native Windows that returns the System32 WSL launcher before Git for
Windows, so .sh cron jobs ran under WSL bash (exit 127 / silent failure)
whenever Git Bash wasn't first on PATH.

Route through tools/environments.local._find_bash() — the shared resolver
(portable Git → Git for Windows dirs → PATH lookup with start probe) that
already encodes the Git-Bash-over-WSL preference. RuntimeError (no usable
bash on Windows) surfaces the actionable error; any other resolver failure
degrades to the historical PATH lookup.

Adds regression tests (resolver wins over raw which, RuntimeError message,
non-RuntimeError fallback). Prior art: NousResearch#46364 and NousResearch#52204 diagnosed the
cron-side launcher selection; main's resolver shipped via NousResearch#47837 — this
takes the shared path rather than re-implementing precedence (the flaw the
NousResearch#46332 triage flagged in NousResearch#52204).

Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
@monerostar

Copy link
Copy Markdown
Contributor

Native Win11 verification (monerostar)

Host: Windows 11 (build 26200) / MINGW64, Python 3.11.15. Checked out this branch against current upstream main.

Bug class is real on this machine

C:\Windows\System32\bash.exe exists (WSL launcher). When PATH is poisoned to System32-first:

Resolver Result
shutil.which("bash") (main cron path) C:\Windows\System32\bash.EXE
_find_bash() (this PR) C:\Users\Admin\AppData\Local\hermes\git\bin\bash.exe (Hermes portable Git Bash)

Live spawn of a HERMES_HOME/scripts/watchdog.sh that prints CRON_OK_FROM_WIN11:

Tests

pytest tests/cron/test_cron_no_agent.py -k "bash or resolver or Windows or find_bash or sh"
3 passed (TestRunJobScriptBashResolver.*)

Notes

(Approve blocked for non-maintainer; treating this as a verified +1 with live evidence.)

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cron Cron scheduler and job management platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Aug 3, 2026
@andrexibiza

Copy link
Copy Markdown
Contributor Author

Thank you for the native Win11 verification — the resolver-vs-which table is exactly the evidence the fix needed. The System32-first PATH case (WSL bash + mangled path → rc=127) is the reported #46332 failure mode, and your live spawn shows _find_bash() winning over shutil.which under the poisoned PATH. Tests confirmed: 3 passed on your box. Appreciate the MINGW64 caveat in your notes — if you spot any residual System32-shadowing case the resolver misses, this PR is the place to pin it.

@SayHell0W0rld

SayHell0W0rld commented Aug 19, 2026

Copy link
Copy Markdown

Real-environment verification (Windows native + git-bash, 2026-08-19)

We hit this exact bug class in production: cron jobs hermes-state-sync.sh and boss-daily-collect.sh both failed with exit 127 (/bin/bash: C:Userseli.ji...: No such file or directory), stderr carrying WSL localhost-proxy UTF-16 noise. Manual .sh runs worked fine (git-bash terminal prepends its own dirs to PATH); only cron (gateway process inheriting the clean system PATH) triggered the failure — which is exactly why this bug is invisible to manual testing.

Verified _find_bash() from this PR under a simulated cron-host environment (machine + user PATH, no git-bash injection):

Resolver Result
shutil.which("bash") (current main) C:\WINDOWS\system32\bash.EXE (WSL launcher) ❌
_find_bash() (this PR) C:\Program Files\Git\bin\bash.exe (Git Bash) ✅

End-to-end: executed a .sh with the _find_bash() result → exit=0, output OK.

Conclusion: this PR's fix is effective on our real Windows environment (Win11 + Git for Windows, system PATH with System32 first). Repro env: Windows 11, Python 3.11.15, MINGW64 git-bash, cron/scheduler.py:_run_job_script.

Copy link
Copy Markdown
Contributor Author

感谢你提供这份真实环境的闭环验证——这正是这个修复最需要的现场证据。

你们复现出的环境分界非常关键:在 Git Bash 终端里手动执行时,终端会注入自己的 PATH,因此脚本正常;而 gateway/cron 进程继承的是纯系统 PATH,shutil.which("bash") 就会命中 C:\Windows\System32\bash.exe,最终形成 WSL launcher + Windows 路径的组合并以 exit 127 失败。这也解释了为什么该问题很难通过常规手动测试发现。

更重要的是,你们在同一份模拟 cron 宿主环境的纯净 PATH 下验证了 _find_bash() 会解析到 C:\Program Files\Git\bin\bash.exe,并用该结果实际执行 .sh 得到 exit=0。这不仅验证了解析结果,也完成了执行层面的端到端闭环。

这份结果与 @monerostar 之前的 Win11 验证一致,同时补上了真实 Hermes cron 任务、gateway 继承环境以及“手动正常 / cron 失败”这一侧的关键证据。我会把它视为本 PR 在原生 Windows 环境中的一份完整验证记录。感谢你们把触发条件拆得这么清楚并跑完闭环。

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

Labels

comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage 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] Cron jobs with .sh scripts fail because WSL bash is picked over Git Bash + backslashes get eaten by MSYS"

4 participants