Please read this first
Describe the bug
WorkspaceEditor.apply_operation handles update_file with a move_to by writing the updated text to the destination and then removing the source:
https://github.com/openai/openai-agents-python/blob/main/src/agents/sandbox/apply_patch.py#L112-L116
moved_relative_path, moved_display_path = self._resolve_path(operation.move_to)
moved_destination = self._session.normalize_path(moved_relative_path)
await self._write_text(moved_destination, updated_text)
if moved_destination != destination:
await self._session.rm(destination, user=self._user)
The guard compares two paths. It cannot tell whether they name the same file. When the sandbox filesystem folds case, notes.txt and Notes.txt are one file, the comparison still reports them as different, and the rm deletes what the write just produced. The operation reports Updated notes.txt and Moved notes.txt to Notes.txt, and the file is gone with the edit inside it.
Two conditions have to hold together, and both are ordinary:
- The host running the SDK compares paths case-sensitively.
normalize_path returns a host-native Path, so this is every Linux and macOS host.
- The sandbox filesystem folds case. APFS on macOS is case-insensitive by default, as is NTFS, as is a Docker bind mount backed by either.
A macOS laptop running UnixLocalSandboxSession is both at once, and unix_local.py treats Darwin as a first-class platform.
Renaming a file to fix its capitalisation is a normal thing to ask an agent to do, which is what makes this worth reporting rather than a curiosity. There is no error, no warning, and nothing in the tool output that says the file was destroyed.
Debug information
- Agents SDK version:
main at 1d471a4, and v0.22.0
- Related library versions: not relevant, the path never reaches a provider
- Python version: 3.13.15
- Operating system: reproduced on Windows using the script below, which models the two conditions above. Not run on macOS. See the note under the repro steps.
- Model and model provider: none, the defect is below the model
- Does the issue reproduce with the latest Agents SDK release? Yes, the code is identical in v0.22.0.
- Does the issue occur consistently or intermittently? Consistently, whenever both conditions hold.
Traceback (most recent call last):
File "repro_case_rename.py", line 61, in main
assert session.files, "the edit and the file were both destroyed by the rename"
AssertionError: the edit and the file were both destroyed by the rename
Repro steps
Save this at the root of a repository checkout and run it. It needs no sandbox provider and no network.
"""apply_patch update_file with a case-only move_to deletes the file."""
import asyncio
import io
from pathlib import PurePosixPath
from agents.editor import ApplyPatchOperation
from tests.sandbox._apply_patch_test_session import ApplyPatchSession
class CaseFoldingSession(ApplyPatchSession):
"""A host that compares paths case-sensitively, over a filesystem that folds case."""
def normalize_path(self, path, **kwargs):
return PurePosixPath(str(super().normalize_path(path, **kwargs)).replace("\\", "/"))
def _key(self, path):
return str(self.normalize_path(path)).lower()
async def read(self, path, *, user=None):
key = self._key(path)
if key not in self.files:
raise FileNotFoundError(key)
return io.BytesIO(self.files[key])
async def write(self, path, data, *, user=None):
payload = data.read()
self.files[self._key(path)] = (
payload.encode("utf-8") if isinstance(payload, str) else bytes(payload)
)
async def rm(self, path, *, recursive=False, user=None):
self.files.pop(self._key(path), None)
async def mkdir(self, path, *, parents=False, user=None):
return None
async def main() -> None:
session = CaseFoldingSession()
session.files = {"/workspace/notes.txt": b"alpha\nbeta\n"}
await session.apply_patch(
ApplyPatchOperation(
type="update_file",
path="notes.txt",
diff="@@\n alpha\n-beta\n+gamma\n",
move_to="Notes.txt",
)
)
print("files after the rename:", session.files)
assert session.files, "the edit and the file were both destroyed by the rename"
asyncio.run(main())
The two overrides are the whole of the model. normalize_path returns a PurePosixPath because the machine I have is Windows, where Path comparison folds case and the guard holds by accident. The _key lowercasing is the case-insensitive filesystem. On macOS both come for free and the plain ApplyPatchSession would show it, but I cannot run that here and would rather say so than imply I did.
The same shape reaches the real sandbox through the apply_patch tool, from an operation of the form {"type": "update_file", "path": "notes.txt", "move_to": "Notes.txt", "diff": "..."}.
Expected behavior
The file survives the rename and holds the updated text.
Whichever way you want it fixed, the source removal needs to know it is not pointing at the file that was just written. Asking the session whether the two paths resolve to the same file is the honest version, since case folding is a property of the sandbox filesystem and not of the machine running the SDK. Comparing the case-folded strings is cheaper and would also refuse a legitimate case-only rename on a case-sensitive filesystem, so it trades one wrong answer for another.
There is a related question you may want to settle at the same time. A move_to that names an existing different file overwrites it with no check, on any filesystem. I have not filed that separately because the fix probably lives in the same few lines.
I am happy to open a pull request with the fix and a regression test that pins the case-folding session, if you would like it.
Reported by Claude Opus 5 running under my supervision. I read this before posting it. The reproduction was executed and its output is quoted above; the macOS behaviour is reasoned from the code and the platform, not observed.
Please read this first
Describe the bug
WorkspaceEditor.apply_operationhandlesupdate_filewith amove_toby writing the updated text to the destination and then removing the source:https://github.com/openai/openai-agents-python/blob/main/src/agents/sandbox/apply_patch.py#L112-L116
The guard compares two paths. It cannot tell whether they name the same file. When the sandbox filesystem folds case,
notes.txtandNotes.txtare one file, the comparison still reports them as different, and thermdeletes what thewritejust produced. The operation reportsUpdated notes.txtandMoved notes.txt to Notes.txt, and the file is gone with the edit inside it.Two conditions have to hold together, and both are ordinary:
normalize_pathreturns a host-nativePath, so this is every Linux and macOS host.A macOS laptop running
UnixLocalSandboxSessionis both at once, andunix_local.pytreats Darwin as a first-class platform.Renaming a file to fix its capitalisation is a normal thing to ask an agent to do, which is what makes this worth reporting rather than a curiosity. There is no error, no warning, and nothing in the tool output that says the file was destroyed.
Debug information
mainat1d471a4, and v0.22.0Repro steps
Save this at the root of a repository checkout and run it. It needs no sandbox provider and no network.
The two overrides are the whole of the model.
normalize_pathreturns aPurePosixPathbecause the machine I have is Windows, wherePathcomparison folds case and the guard holds by accident. The_keylowercasing is the case-insensitive filesystem. On macOS both come for free and the plainApplyPatchSessionwould show it, but I cannot run that here and would rather say so than imply I did.The same shape reaches the real sandbox through the
apply_patchtool, from an operation of the form{"type": "update_file", "path": "notes.txt", "move_to": "Notes.txt", "diff": "..."}.Expected behavior
The file survives the rename and holds the updated text.
Whichever way you want it fixed, the source removal needs to know it is not pointing at the file that was just written. Asking the session whether the two paths resolve to the same file is the honest version, since case folding is a property of the sandbox filesystem and not of the machine running the SDK. Comparing the case-folded strings is cheaper and would also refuse a legitimate case-only rename on a case-sensitive filesystem, so it trades one wrong answer for another.
There is a related question you may want to settle at the same time. A
move_tothat names an existing different file overwrites it with no check, on any filesystem. I have not filed that separately because the fix probably lives in the same few lines.I am happy to open a pull request with the fix and a regression test that pins the case-folding session, if you would like it.
Reported by Claude Opus 5 running under my supervision. I read this before posting it. The reproduction was executed and its output is quoted above; the macOS behaviour is reasoned from the code and the platform, not observed.