Skip to content

fix(cron): stop lifecycle guard false-positives and crashes on .py/binary scripts - #77332

Merged
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/cron-lifecycle-guard-77201
Aug 3, 2026
Merged

fix(cron): stop lifecycle guard false-positives and crashes on .py/binary scripts#77332
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/cron-lifecycle-guard-77201

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Summary

Python cron scripts using pathlib division (e.g. Path.home() / ".hermes") are no longer false-positive blocked by the lifecycle guard, and binary scripts invoked by absolute path no longer crash it.

Root cause: _iter_referenced_shell_scripts treated pathlib's bare / operator as an executable path, resolved it to the filesystem root, and failed closed on every .py script containing Path(...) / "...". Separately, binary executables with NUL bytes crashed Path.resolve() with ValueError: embedded null byte (#76762).

Changes

  • cron/lifecycle_guard.py: skip the shell-script reference walk for .py scripts (they run via the Python interpreter, never through a shell); preserve direct regex scanning for lifecycle commands in their text. Pure-separator / tokens are filtered in _iter_referenced_shell_scripts so shell-script chains are still caught. Binary files with NUL bytes return (None, False) instead of crashing, and Path.resolve() catches ValueError alongside OSError.
  • tests/hermes_cli/test_gateway_restart_loop.py: 4 new tests — pathlib division not blocked, literal lifecycle command in .py still blocked, binary path doesn't crash, shell-script reference walk still catches nested lifecycle commands.

Validation

Before After
.py with Path.home() / ".hermes" Blocked (false positive) Allowed
.py with hermes gateway restart Blocked Blocked (regex still catches)
/usr/bin/python3 -c "print(1)" in prompt Crashes (ValueError) Passes (result=False)
.sh chain → hermes gateway restart Blocked Blocked (walk preserved)
  • 82/82 tests in test_gateway_restart_loop.py pass
  • E2E: 6/6 real-import scenarios pass (pathlib, literal command, binary, shell chain, prompt-only blocked, innocent text not blocked)
  • ruff clean

Credit

Cherry-picked from #77201 by @criptogus. Closes #77131. Also fixes #76762.
Competing PRs #77137 and #77230 addressed the same issue but with less coverage — #77137 removes "/" in executable entirely (weakens shell-chain detection), #77230 has no tests and includes unrelated changes.

…nary scripts

The gateway lifecycle guard (cron/lifecycle_guard.py) applied shell-style
tokenization and script-reference resolution to non-shell content, with two
regressions:

NousResearch#77131 - every .py cron script using pathlib division was hard-blocked:
  Path.home() / ".hermes" / ".env" tokenizes the bare "/" operator as an
  executable path, which resolves to the filesystem root; the regular-file
  check then fails closed as unsafe. Since Python runs under the
  interpreter, never through a POSIX shell, the shell-script reference walk
  is a false-positive generator on Python sources. check_gateway_lifecycle
  now skips the walk for *.py scripts (the direct command regex still scans
  the full text), and _iter_referenced_shell_scripts skips pure-separator
  tokens.

NousResearch#76762 - terminal commands invoking a binary by absolute path (e.g.
  /usr/bin/python3) crashed the guard with ValueError: embedded null byte:
  the walk read the binary's bytes, decoded them as text, and re-tokenized
  machine code; the recursion then hit Path.resolve() on a NUL-bearing
  path while only OSError was caught. _read_referenced_script now skips
  NUL-containing files (binaries are not referenced shell scripts) and
  resolve() tolerates ValueError.

Shell scripts (.sh/.bash/.zsh) keep the full deep scan; literal lifecycle
commands in .py scripts are still blocked by the direct regex. New tests
cover all four behaviors.
On Linux, /usr/bin/python3 is >1MB, so the size check fired before
the NUL check could run — the binary was returned as unsafe=True
(blocked) instead of (None, False) (skip). Reorder: read the bounded
chunk first, check for NUL bytes (binary → skip), then check size
(oversized text → fail closed).
@kshitijk4poor
kshitijk4poor force-pushed the salvage/cron-lifecycle-guard-77201 branch from 640224d to 39dac43 Compare August 3, 2026 04:37
@kshitijk4poor
kshitijk4poor merged commit 037825c into NousResearch:main Aug 3, 2026
35 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/cron-lifecycle-guard-77201 branch August 5, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants