mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-09-28 15:01:00 +08:00
refactor(cron): simplify the interpreter salvage after review
- Refuse pythonw: it discards captured output, and the user-interpreter path skips the Windows helper that used to swap it for python.exe, so an agent job would go silently quiet. - Drop the one-off key pop on clear: like workdir/monitor_script, a cleared interpreter is stored as null and _script_argv already treats it as unset. - Drop an unused import in the no-agent interpreter test. - Docstring says what the check is (name-based), parametrize ids.
This commit is contained in:
@@ -2085,8 +2085,6 @@ def update_job(job_id: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]
|
||||
_normalize_job_updates(job, updates)
|
||||
_apply_pin_update(job, updates)
|
||||
updated = _apply_skill_fields({**job, **updates})
|
||||
if updated.get("interpreter") is None:
|
||||
updated.pop("interpreter", None) # cleared: absent key = Hermes' own Python
|
||||
_reject_terminal_activation(job, updated, job_id)
|
||||
# Re-check on the MERGED record; scoped to changed fields so legacy records keep loading.
|
||||
if {"monitor_script", "monitor_url", "no_agent", "script"}.intersection(updates):
|
||||
|
||||
@@ -369,9 +369,10 @@ def _resolve_script_path(script_path: str) -> tuple[Optional[Path], Optional[str
|
||||
def _resolve_cron_interpreter(interpreter: str) -> tuple[Optional[str], Optional[str]]:
|
||||
"""``(python_exe, error)`` for a job's ``interpreter`` field. Checked at run time, not create
|
||||
time: a user venv can be rebuilt or moved while the job lives. Bare names are refused — they
|
||||
silently change meaning with PATH. Only a Python image is accepted (link target included):
|
||||
the lifecycle guard classifies ``.py`` scripts as Python and skips its shell reference walk,
|
||||
so ``interpreter=/bin/bash`` would run an unscanned ``.py`` body as shell."""
|
||||
silently change meaning with PATH. The name (and the symlink target's name) must look like a
|
||||
Python: the lifecycle guard classifies ``.py`` scripts as Python and skips its shell reference
|
||||
walk, so ``interpreter=/bin/bash`` would run an unscanned ``.py`` body as shell. ``pythonw``
|
||||
is refused because it discards captured output (an agent job would go silently quiet)."""
|
||||
from cron.lifecycle_guard import _INTERPRETER_IMAGE_RE
|
||||
|
||||
raw = interpreter.strip()
|
||||
@@ -384,13 +385,14 @@ def _resolve_cron_interpreter(interpreter: str) -> tuple[Optional[str], Optional
|
||||
names = {resolved.name.lower(), resolved.resolve().name.lower()}
|
||||
except FileNotFoundError:
|
||||
return None, f"Interpreter not found: {raw}"
|
||||
except (RuntimeError, OSError) as exc: # unknown ~user, broken symlink, unreadable parent
|
||||
except (RuntimeError, OSError) as exc: # unknown ~user, unreadable parent, symlink loop
|
||||
return None, f"Unable to resolve interpreter path {raw!r}: {exc}"
|
||||
if not stat.S_ISREG(mode):
|
||||
return None, f"Interpreter path is not a file: {resolved}"
|
||||
if sys.platform != "win32" and not mode & 0o111:
|
||||
return None, f"Interpreter is not executable: {resolved}"
|
||||
if not all(_INTERPRETER_IMAGE_RE.match(name) for name in names):
|
||||
if not all(_INTERPRETER_IMAGE_RE.match(name) and not name.startswith("pythonw")
|
||||
for name in names):
|
||||
return None, f"Interpreter must be a Python executable (python, python3, python3.12, ...): {resolved}"
|
||||
return str(resolved), None
|
||||
|
||||
|
||||
@@ -283,7 +283,6 @@ def test_run_job_no_agent_uses_configured_interpreter(hermes_env):
|
||||
Proves the override survives the no_agent branch of ``run_job`` →
|
||||
``_run_job_script_with_claim_heartbeat`` → ``_run_job_script``.
|
||||
"""
|
||||
import os
|
||||
import stat as _stat
|
||||
import sys
|
||||
|
||||
|
||||
@@ -106,7 +106,10 @@ class TestRunJobScript:
|
||||
(lambda d: "/bin/bash", "must be a Python executable"),
|
||||
(lambda d: (d / "python").symlink_to("/bin/bash") or str(d / "python"),
|
||||
"must be a Python executable"),
|
||||
])
|
||||
(lambda d: (d / "pythonw").symlink_to(sys.executable) or str(d / "pythonw"),
|
||||
"must be a Python executable"),
|
||||
], ids=["bare-name", "missing", "directory", "not-executable", "bash",
|
||||
"python-symlink-to-bash", "pythonw"])
|
||||
def test_configured_interpreter_is_refused_unless_a_python_path(
|
||||
self, cron_env, tmp_path, make_interpreter, expected
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user