fix(cron): workdir redirects file-edit tools too (enables evolution-pipeline isolation)#585
Merged
Merged
Conversation
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).
Contributor
🔎 Lint report:
|
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[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
workdironly setTERMINAL_CWD. The terminal/git tools readTERMINAL_CWDlive, so git honoured the workdir — but the file-edit tools did not.tools/file_tools._resolve_base_dirresolves a relative path with this priority:$TERMINAL_CWDos.getcwd()In a long-lived gateway/daemon the shared
"default"terminal env is still anchored at the runtime checkout, so its cwd (priority #1) shadowedTERMINAL_CWD(#3). Result:write_file/patchlanded in the runtime mirror whilegit commitran 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 existingTERMINAL_CWDset/restore:.cwdat 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-backedShellFileOperations, so both file-tool paths follow the workdir. The prior cwd (incl.None) is restored under the lock infinally, mirroring theTERMINAL_CWDsave/restore — no leak.$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 (noos.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_agentscript path (it already does a real per-jobos.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.py— 283 passed. (3 pre-existing failures intests/tools/test_file_tools.pyare unrelated macOS/tmp→/private/tmpsymlink mismatches, present without this change.)ruff checkclean on edited files.Cross-review
Reviewed by two non-Claude advisors (Gemini, Kimi). An initial FAIL verdict flagged: (a) restoring
Nonecwd + 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-globalTERMINAL_CWDalready 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
workdiron osoba and running one live evolution cycle after deploy to confirm the runtime mirror stays clean.