Skip to content

fix(cron): workdir redirects file-edit tools too (enables evolution-pipeline isolation)#585

Merged
Lexus2016 merged 1 commit into
mainfrom
evolution/fix-cron-workdir-file-tools
Jun 27, 2026
Merged

fix(cron): workdir redirects file-edit tools too (enables evolution-pipeline isolation)#585
Lexus2016 merged 1 commit into
mainfrom
evolution/fix-cron-workdir-file-tools

Conversation

@Lexus2016

Copy link
Copy Markdown
Owner

[REVIEW] enables re-pointing evolution pipeline cron jobs to a separate work dir

DO NOT MERGE without owner review. Marked [REVIEW]: this is the fix that lets the evolution pipeline's cron jobs run against a separate work dir instead of dirtying the runtime mirror.

The bug (split-brain)

A cron agent job's workdir only set TERMINAL_CWD. The terminal/git tools read TERMINAL_CWD live, so git honoured the workdir — but the file-edit tools did not.

tools/file_tools._resolve_base_dir resolves a relative path with this priority:

  1. the task's live terminal-env cwd
  2. a registered task cwd override
  3. absolute $TERMINAL_CWD
  4. os.getcwd()

In a long-lived gateway/daemon the shared "default" terminal env is still anchored at the runtime checkout, so its cwd (priority #1) shadowed TERMINAL_CWD (#3). Result: write_file/patch landed in the runtime mirror while git commit ran in the workdir — a split-brain that dirtied the runtime checkout the evolution pipeline mirrors.

The fix

Scoped entirely to cron/scheduler.py::_run_job_impl (workdir setup + finally), alongside the existing TERMINAL_CWD set/restore:

  • Live env present (the real-world case): point that env's .cwd at the workdir under _env_lock, capturing the prior cwd. The one env object backs both _resolve_base_dir (chore(actions)(deps): bump the actions-minor-patch group across 1 directory with 4 updates #1) and the shell-backed ShellFileOperations, so both file-tool paths follow the workdir. The prior cwd (incl. None) is restored under the lock in finally, mirroring the TERMINAL_CWD save/restore — no leak.
  • Cold start (no live env yet): the file tools already fall through to $TERMINAL_CWD (chore(actions)(deps): bump marocchino/sticky-pull-request-comment from 2.9.1 to 3.0.4 #3) = workdir; a terminal command run during the job lazily creates the shared env seeded from it. On cleanup that env's cwd is cleared non-destructively (no os.chdir, no teardown — the subprocess/container stays alive) and the file-ops cache dropped, so it can't leak the workdir into a later workdir-less job.

Not touched: the no_agent script path (it already does a real per-job os.chdir), workdir-less jobs (verified untouched), and the file/terminal tools themselves.

Tests (TDD)

New tests in tests/cron/test_cron_workdir.py, all failing pre-fix / green post-fix where they target the bug:

  • test_workdir_redirects_file_tools_and_terminal — reproduces the split (file base dir resolves to the runtime checkout while git follows the workdir). Fails pre-fix.
  • test_no_workdir_leaves_file_tool_resolution_untouched — workdir-less job unaffected.
  • test_sequential_workdir_then_no_workdir_no_leak — a workdir job doesn't leak into a following workdir-less job.
  • test_lazily_created_env_does_not_leak_workdir — cold-start env created mid-run is neutralised on cleanup. Fails pre-fix.

Suites green locally (pypy3.11): tests/cron/test_scheduler.py, test_cron_no_agent.py, test_run_one_job.py, test_cron_workdir.py, test_parallel_pool.py, tests/tools/test_file_tools_cwd_resolution.py, test_file_ops_cwd_tracking.py283 passed. (3 pre-existing failures in tests/tools/test_file_tools.py are unrelated macOS /tmp/private/tmp symlink mismatches, present without this change.) ruff check clean on edited files.

Cross-review

Reviewed by two non-Claude advisors (Gemini, Kimi). An initial FAIL verdict flagged: (a) restoring None cwd + holding the env lock — fixed; (b) a lazy-env leak — addressed with the cold-start neutralisation + a dedicated regression test. The remaining shared-"default"-env concurrency between a sequential workdir job and concurrent parallel workdir-less jobs is a pre-existing architectural property (process-global TERMINAL_CWD already has the same exposure); true isolation needs task-scoped env keys and is out of scope here.

Residual risk / merge-readiness

Safe to merge after CI is green. End-to-end proof still requires re-enabling the cron workdir on osoba and running one live evolution cycle after deploy to confirm the runtime mirror stays clean.

A cron AGENT job's `workdir` only set `TERMINAL_CWD`, which the terminal/git
tools read live — so `git` honoured the workdir, but the file-edit tools did
not. `tools/file_tools._resolve_base_dir` resolves relative paths against the
task's *live* terminal-env cwd (priority #1), which in a long-lived
gateway/daemon is the shared "default" env still anchored at the runtime
checkout. That #1 cwd shadowed `TERMINAL_CWD` (#3): `write_file`/`patch` landed
in the runtime mirror while git committed in the workdir — a split-brain that
dirtied the runtime checkout the evolution pipeline mirrors.

Fix (cron/scheduler.py::_run_job_impl workdir setup + finally only):
- When a live "default" terminal env exists at job start, point THAT env's
  `.cwd` at the workdir (under `_env_lock`), capturing the prior cwd. That one
  object backs both `_resolve_base_dir` (#1) and the shell-backed
  `ShellFileOperations`, so both file-tool paths follow the workdir. The prior
  cwd (incl. `None`) is restored under the lock in `finally`, mirroring the
  existing `TERMINAL_CWD` save/restore — no leak into a later job.
- Cold start (no live env yet): the file tools already fall through to
  `$TERMINAL_CWD` (#3) = workdir, and a terminal command run during the job
  lazily creates the shared env seeded from it. On cleanup that env's cwd is
  cleared (non-destructively — no os.chdir / teardown) and the file-ops cache
  dropped, so it cannot leak the workdir into a later workdir-less job (the
  same leak `TERMINAL_CWD` already had for that env's git).

NOT touched: the `no_agent` script path (real per-job `os.chdir`), workdir-less
jobs (verified untouched), and the file/terminal tools themselves. Workdir jobs
are serialized by `tick()`, so mutating the shared in-process env cwd has the
same (already-accepted) blast radius as the existing process-global
`TERMINAL_CWD` mutation. The remaining shared-"default"-env concurrency between
a sequential workdir job and concurrent parallel workdir-less jobs is a
pre-existing architectural property (true isolation needs task-scoped env keys)
and is out of scope here.

TDD: new tests reproduce the split (file base dir resolves to the runtime
checkout while git follows the workdir) — failing pre-fix, green post-fix —
plus regressions that a workdir-less job is unaffected, that a workdir job does
not leak into a following workdir-less job, and that a lazily-created env is
neutralised on cleanup. Cross-reviewed by two non-Claude advisors (Gemini,
Kimi).
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: evolution/fix-cron-workdir-file-tools vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11809 on HEAD, 11803 on base (🆕 +6)

🆕 New issues (3):

Rule Count
unresolved-attribute 2
invalid-assignment 1
First entries
tests/cron/test_cron_workdir.py:709: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["default"]` and value of type `object` on object of type `dict[str, dict[str, Any]]`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:3299: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`

✅ Fixed issues (1):

Rule Count
invalid-assignment 1
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

Unchanged: 6218 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@Lexus2016 Lexus2016 changed the title fix(cron): redirect file-edit tools to a job's workdir, not just git [REVIEW] fix(cron): workdir redirects file-edit tools too (enables evolution-pipeline isolation) Jun 27, 2026
@Lexus2016 Lexus2016 merged commit cdfe707 into main Jun 27, 2026
33 checks passed
@Lexus2016 Lexus2016 deleted the evolution/fix-cron-workdir-file-tools branch June 27, 2026 10:18
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