hermes-agent/tests/cron
entropy-0x abc349bd79 fix(cron): isolate per-job TERMINAL_CWD from concurrent cron jobs
A cron job with a per-job `workdir` overrides the process-global
`os.environ["TERMINAL_CWD"]` for the entire duration of its agent run and
restores it afterwards. The scheduler dispatches workdir jobs on a
single-thread sequential pool and workdir-less jobs on a separate parallel
pool, and the in-code comments claimed this made the override safe.

That only prevents two workdir jobs from overlapping each other. The two
pools run concurrently in the same process and share `os.environ`, so while
a workdir job has `TERMINAL_CWD` pointed at its project directory, any
workdir-less job firing in the same window reads that same global through the
terminal, file, and code-exec tools and runs its commands in the wrong
directory. The corruption window spans the whole workdir-job run, and a file
write or delete can land in another job's tree.

This serializes the override with a writer-preferring readers-writer lock.
Workdir jobs acquire it as writers (exclusive for their whole run); workdir-
less jobs acquire it as readers, so they still run in parallel with each
other but never alongside a workdir job's override. The guarantee is based on
run overlap rather than tick boundaries, so it also holds when a workdir job
spans ticks.

## What does this PR do?

Fixes a directory-isolation bug in the cron scheduler: a workdir cron job's
process-global `TERMINAL_CWD` override could be observed by a concurrently
running workdir-less cron job, causing that job's shell/file/code-exec
commands to execute in the wrong directory.

## Related Issue

N/A

## Type of Change

- [x] 🐛 Bug fix (non-breaking change that fixes an issue)
- [ ]  New feature (non-breaking change that adds functionality)
- [ ] 🔒 Security fix
- [ ] 📝 Documentation update
- [ ]  Tests (adding or improving test coverage)
- [ ] ♻️ Refactor (no behavior change)
- [ ] 🎯 New skill (bundled or hub)

## Changes Made

- `cron/scheduler.py`: add `_ReadWriteLock` (writer-preferring) and the
  module-global `_terminal_cwd_lock`.
- `cron/scheduler.py`: in `run_job`, acquire the lock as a writer for workdir
  jobs and as a reader for workdir-less jobs, spanning the `TERMINAL_CWD`
  override and its restore in the `finally` block.
- `cron/scheduler.py`: correct the stale comments in `run_job` and `tick` that
  claimed the sequential pool alone made the override safe.
- `tests/cron/test_terminal_cwd_lock.py`: new tests for reader concurrency,
  writer exclusion, and the no-cross-observation regression.

## How to Test

1. `python -m pytest tests/cron/test_terminal_cwd_lock.py -q` — the regression
   test `test_reader_never_observes_writer_override` fails without the lock and
   passes with it.
2. `python -m pytest tests/cron/test_cron_workdir.py tests/cron/test_parallel_pool.py -q`
   — confirms the existing `TERMINAL_CWD` set/restore and pool behaviour are
   unchanged.

## Checklist

### Code

- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits (`fix(scope):`, etc.)
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains only changes related to this fix
- [x] I've run the affected `tests/cron/` suites and all tests pass
- [x] I've added tests for my changes (required for bug fixes)
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)

### Documentation & Housekeeping

- [x] I've updated relevant documentation (docstrings/comments) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture — N/A
- [x] I've considered cross-platform impact (Windows, macOS) — uses stdlib `threading` only
- [x] I've updated tool descriptions/schemas if I changed tool behavior — N/A
2026-07-01 15:39:48 +05:30
..
__init__.py test: add unit tests for 8 modules (batch 2) 2026-02-26 13:54:20 +03:00
conftest.py fix(cron): resolve model.default + fail fast on missing model 2026-06-21 12:37:56 +05:30
test_blueprint_catalog.py docs: finish Automation Blueprints terminology rebrand (#44470) 2026-06-11 17:22:22 -04:00
test_claim_job_for_fire.py revert(cron): return cron job storage to per-profile (reverts #32117 + #50993) (#51116) 2026-06-22 17:53:50 -07:00
test_codex_execution_paths.py refactor(session-log): delete _save_session_log and all callers 2026-05-20 11:44:10 -07:00
test_compute_next_run_last_run_at.py fix(cron): use last_run_at as croniter base for cron jobs 2026-04-29 08:24:48 -07:00
test_cron_context_from.py chore: prune unused imports and duplicate import redefinitions 2026-05-28 22:26:25 -07:00
test_cron_inactivity_timeout.py chore: prune unused imports and duplicate import redefinitions 2026-05-28 22:26:25 -07:00
test_cron_no_agent.py chore: prune unused imports and duplicate import redefinitions 2026-05-28 22:26:25 -07:00
test_cron_profile_isolation.py fix(cron): make per-profile cron isolation intentional and tested (#4707) (#53570) 2026-06-27 03:55:01 -07:00
test_cron_prompt_injection_skill.py fix(cron): don't strict-scan script-injected output in no-skills jobs (#43223) 2026-06-10 08:27:24 +05:30
test_cron_provider_pin.py fix(cron): null-safe deliver in cron list + re-resolve BSM secrets per run 2026-07-01 01:05:33 -07:00
test_cron_script.py test(cron): make env-sanitize probe var deterministic 2026-06-20 00:22:55 +05:30
test_cron_workdir.py fix(cron): make sequential jobs non-blocking too + sweep MCP after jobs finish 2026-06-04 05:40:13 -07:00
test_cronjob_schema.py test(cron): guard schedule-required description text on CRONJOB_SCHEMA 2026-05-26 14:09:37 -07:00
test_file_permissions.py chore: prune unused imports and duplicate import redefinitions 2026-05-28 22:26:25 -07:00
test_jobs.py fix(cron): commit one-shot dispatch before side effect to stop crash re-fire loop (#56177) 2026-07-01 01:30:36 -07:00
test_jobs_changed_notify.py feat(cron): wire on_jobs_changed, cron.chronos config, docs + agent↔NAS contract 2026-06-18 15:11:32 +10:00
test_jobs_crossprocess_lock.py fix: complete cron jobs lock salvage 2026-06-15 06:29:00 -07:00
test_parallel_pool.py revert(cron): remove per-job profile support (PR #28124) (#43956) 2026-06-10 20:46:17 -07:00
test_rewrite_skill_refs.py fix(curator): rewrite cron job skill refs after consolidation (#18253) 2026-04-30 23:04:50 -07:00
test_run_one_job.py refactor(cron): extract run_one_job shared firing helper from tick 2026-06-18 14:26:29 +10:00
test_scheduler.py test(cron): apply run_job patches via ExitStack, not a positional list (#56192) 2026-07-01 01:49:44 -07:00
test_scheduler_mcp_init.py chore: prune unused imports and duplicate import redefinitions 2026-05-28 22:26:25 -07:00
test_scheduler_provider.py security(cron): fail closed in scheduler backstop when validator errors 2026-07-01 14:23:01 +05:30
test_suggestions.py test(cron): document consent-first self-learning suggestions 2026-06-20 23:23:47 -07:00
test_terminal_cwd_lock.py fix(cron): isolate per-job TERMINAL_CWD from concurrent cron jobs 2026-07-01 15:39:48 +05:30