Skip to content

fix(cron): honor workdir for no_agent job scripts - #99

Open
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56005
Open

fix(cron): honor workdir for no_agent job scripts#99
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56005

Conversation

@hashbender

Copy link
Copy Markdown
Owner

What does this PR do?

Fixes a bug where a no_agent cron job's configured workdir was silently ignored — the job's script always ran from HERMES_HOME/scripts/ instead of the configured working directory.

The no_agent path in run_job() called os.chdir(workdir) to set the script's working directory (its own comment: "For no_agent jobs this is just the subprocess cwd"). But _run_job_script() launches the subprocess with an explicit cwd=str(path.parent) (the scripts dir), and an explicit cwd= argument to subprocess.run() sets the child's working directory absolutely — it overrides the parent process's os.chdir(). So the chdir was a no-op for the subprocess and the workdir never took effect.

Impact: a no_agent watchdog/data-collection script that uses relative paths (./state.json, python ./sub.py, reading ./config) resolved them against the scripts directory rather than the project directory the user configured — silent wrong reads/writes or "file not found".

The fix threads the workdir into _run_job_script() as an optional cwd argument and passes it straight to subprocess.run(). It also removes the os.chdir() dance, which was process-global and unsafe while other jobs run in parallel.

Related Issue

No existing issue — found via a code audit of the cron execution path.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • cron/scheduler.py
    • _run_job_script() gains an optional cwd parameter. The subprocess now runs with cwd when it is an existing directory; otherwise it falls back to the historical default (the script's own dir under HERMES_HOME/scripts/). Backward compatible — the two callers that don't pass cwd are unchanged.
    • run_job() no_agent path: pass the job's validated workdir via _run_job_script(script_path, cwd=_job_workdir) and drop the ineffective (and process-global) os.chdir()/restore block. When the workdir no longer exists, log and fall back to running without it — mirroring the agent path's existing behavior.
  • tests/cron/test_cron_workdir.py
    • TestRunJobScriptCwdcwd honored when provided, default scripts-dir when omitted, and fallback when cwd doesn't exist.
    • TestNoAgentScriptWorkdir — end-to-end: a no_agent job's script runs in the configured workdir, defaults to the scripts dir without a workdir, and safely falls back when the workdir vanished after job creation.

How to Test

Reproduction (before the fix):

  1. Create a no_agent cron job with a workdir pointing at some project dir, and a script under HERMES_HOME/scripts/ that prints its own cwd (import os; print(os.getcwd())).
  2. Run the job. The delivered output is HERMES_HOME/scriptsnot the configured workdir. Any relative path in the script resolves against the wrong directory.

After the fix: the same job's script prints (and runs from) the configured workdir.

Automated regression tests (added in this PR):

$ pytest tests/cron/test_cron_workdir.py::TestRunJobScriptCwd \
         tests/cron/test_cron_workdir.py::TestNoAgentScriptWorkdir -v

TestRunJobScriptCwd::test_honors_cwd_when_provided                   PASSED
TestRunJobScriptCwd::test_defaults_to_scripts_dir_without_cwd        PASSED
TestRunJobScriptCwd::test_falls_back_when_cwd_missing                PASSED
TestNoAgentScriptWorkdir::test_no_agent_script_runs_in_workdir       PASSED
TestNoAgentScriptWorkdir::test_no_agent_without_workdir_uses_scripts_dir  PASSED
TestNoAgentScriptWorkdir::test_no_agent_vanished_workdir_falls_back  PASSED

6 passed

Before/after proof — with the scheduler.py fix reverted (tests kept), the end-to-end test fails, demonstrating the bug:

>   assert Path(response.strip()).resolve() == workdir.resolve()
E   AssertionError: assert <tmp>/home/scripts == <tmp>/project
FAILED ...::test_no_agent_script_runs_in_workdir

With the fix applied, all 6 pass.

Full cron suite: pytest tests/cron/571 passed (plus the 6 new). ruff check on the changed files passes clean; the Windows-footgun checker reports no findings on the diff.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(cron):)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run the affected test suite (tests/cron/) and it passes; added 6 regression tests that all pass
  • I've added tests for my changes
  • I've tested on my platform: local dev environment

Documentation & Housekeeping

  • I've updated relevant documentation (docstrings) — _run_job_script's docstring documents the new cwd argument
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact — uses subprocess.run(cwd=...) + Path.is_dir(), both cross-platform; removes a process-global os.chdir()
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Mirror-of: NousResearch#56005
NousResearch#56005

@tenki-reviewer

tenki-reviewer Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Complete

Files Reviewed: 2
Findings: 1

By Severity:

  • 🟡 Medium: 1

PR adds workdir support to no_agent cron jobs but introduces two medium-severity bugs: pre-check scripts for LLM (agent) cron jobs ignore the configured workdir, and a TOCTOU race between workdir existence check and subprocess execution produces misleading error messages.

Files Reviewed (2 files)
cron/scheduler.py
tests/cron/test_cron_workdir.py

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: 🟡 Medium (42/100) — 1 medium finding · 154 LOC across 2 files


Findings

2 medium-severity bugs found in , both related to the new workdir parameter handling for cron job scripts.

Finding 1: Pre-check script ignores workdir in LLM path

The no_agent code path (lines 2021–2030) correctly extracts, validates, and passes to . However, the LLM (agent) pre-check script invocation at line 2113 calls without passing the parameter. This causes pre-check scripts for agent cron jobs to always execute from regardless of the job's configured .

Finding 2: TOCTOU race on workdir existence check

At line 1642, is checked non-atomically before at line 1643. If the workdir is removed between these two calls (plausible on NFS mounts, tmpfs, or during ephemeral environment teardown), raises caught by the generic handler at line 1677. The operator sees with no indication the root cause was a vanished directory rather than a script bug.

Comment thread cron/scheduler.py
from tools.environments.local import _sanitize_subprocess_env

popen_kwargs = {"creationflags": windows_hide_flags()} if sys.platform == "win32" else {}
run_cwd = cwd if (cwd and Path(cwd).is_dir()) else str(path.parent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 TOCTOU race between Path.is_dir() and subprocess.run cwd in _run_job_script (bug)

The cwd existence check at cron/scheduler.py:1642 is not atomic with subprocess.run at line 1643-1651. If the workdir is removed between the check and the subprocess execution, subprocess.run raises FileNotFoundError, caught generically at line 1677 and returned as 'Script execution failed: [error]'. This gives the operator no indication that the root cause was a vanished workdir rather than a script bug. The race is plausible on NFS mounts, tmpfs-based workdirs, or during ephemeral-environment teardown.

💡 Suggestion: Wrap the subprocess.run call to catch FileNotFoundError / NotADirectoryError specifically when cwd was provided, and return a clear error message like 'Workdir no longer exists: ' so operators can distinguish a vanished directory from a genuine script failure.

📋 Prompt for AI Agents

In cron/scheduler.py around lines 1643-1678, add a specific except clause before the generic except Exception to catch FileNotFoundError and NotADirectoryError when the cwd parameter was provided. Return (False, f'Workdir no longer exists: {cwd}') instead of the generic message. This gives operators an actionable signal when the configured workdir disappears between the existence check and subprocess execution.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant