Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion cron/lifecycle_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,20 @@ def _read_script_for_scanning(script_path: str) -> str:
``UnicodeDecodeError`` on such files, and swallowing that error would let
an attacker hide the command in binary noise. Returns an empty string
only when the file cannot be read at all.

``_resolve_script_path``'s ``Path.expanduser()`` raises ``RuntimeError``
(not ``OSError``) for a ``~user``-shaped path with no matching system
user — e.g. an LLM-authored ``script`` value that happens to start with
``~someuser/...``. Catch it alongside ``OSError`` so a malformed script
path degrades to "can't scan it" instead of crashing job creation with an
unhandled exception (same bug class as agent/subdirectory_hints.py's
tilde-expansion crash).
"""
try:
return _resolve_script_path(script_path).read_bytes().decode(
"utf-8", errors="replace"
)
except OSError:
except (OSError, RuntimeError):
return ""


Expand Down
17 changes: 17 additions & 0 deletions tests/hermes_cli/test_gateway_restart_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,23 @@ def test_missing_script_does_not_raise(self, tmp_path):
from cron.lifecycle_guard import check_gateway_lifecycle
check_gateway_lifecycle("clean prompt", str(tmp_path / "nonexistent.sh"))

def test_tilde_unknown_user_script_does_not_crash(self):
"""A ``~user``-shaped script path with no matching system user must
degrade to "can't scan it" instead of raising RuntimeError.

``Path.expanduser()`` raises ``RuntimeError`` (not ``OSError``) for
an unresolvable ``~user`` prefix — same bug class as
agent/subdirectory_hints.py's tilde-expansion crash (an LLM-authored
script value like ``~nonexistent_user_xyzzy_12345/restart.sh`` must
not crash cron job creation).
"""
from cron.lifecycle_guard import check_gateway_lifecycle
# Must not raise RuntimeError — the guard treats an unscannable
# script as absent and only judges the (clean) prompt.
check_gateway_lifecycle(
"clean prompt", "~nonexistent_user_xyzzy_12345/restart.sh",
)

def test_relative_script_resolved_under_scripts_dir(self, tmp_path, monkeypatch):
"""A bare/relative script name resolves under HERMES_HOME/scripts (the
same place the scheduler runs it from) — otherwise the guard would read
Expand Down
Loading