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:
kshitijk4poor
2026-09-28 02:32:05 +05:30
committed by kshitij
parent 4450cc9d90
commit 6e69a8933a
4 changed files with 11 additions and 9 deletions
-2
View File
@@ -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):
+7 -5
View File
@@ -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
-1
View File
@@ -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
+4 -1
View File
@@ -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
):