Skip to content

Commit fa51258

Browse files
authored
Update base.py
1 parent 4117291 commit fa51258

1 file changed

Lines changed: 0 additions & 84 deletions

File tree

‎python_agent_harness/tools/base.py‎

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -14,90 +14,6 @@
1414
from ..core.models import ToolSpec
1515

1616

17-
def atomic_write_text(path: str, content: str) -> None:
18-
"""Replace PATH's contents with CONTENT atomically.
19-
20-
THE single write path for every tool that rewrites a file (``Edit``,
21-
``Insert``, ``Write``, and the pure-Python diff applier). It lives
22-
here, in the module every tool already imports, so the four callers
23-
cannot drift apart -- and because ``base`` imports nothing from
24-
``tools``, putting it here is also the only placement that avoids a
25-
circular import: ``filesystem.py`` re-imports ``edit``/``write``/
26-
``insert`` at its bottom, so a helper defined *there* would make
27-
``edit`` import a half-initialised ``filesystem``.
28-
29-
A plain ``open(path, "w")`` TRUNCATES the file before writing, so a
30-
write that fails partway through -- ENOSPC, a quota, an I/O error,
31-
the process being killed -- left the file empty and the user's
32-
content unrecoverable while the tool returned a tidy "Error: ..."
33-
string. Instead the new content goes to a sibling temp file which is
34-
then renamed over the target, mirroring the tmp-write + ``os.replace``
35-
discipline in ``SessionPersistence.save``. ``os.replace`` is atomic:
36-
a reader sees either the complete old file or the complete new one,
37-
and ANY failure before it leaves PATH untouched. The temp file is
38-
removed on the error path.
39-
40-
Deliberate details:
41-
42-
- ``errors="surrogateescape"`` and ``newline=""`` match the tools'
43-
read side, so invalid UTF-8 bytes (Latin-1, GBK, ...) round-trip
44-
and the content is written byte-for-byte instead of having "\\n"
45-
translated to ``os.linesep`` on Windows.
46-
- The temp name carries a random suffix rather than a fixed ".tmp":
47-
concurrent sub-agents can edit the same path, and a shared name
48-
would let one writer rename the other's half-written file into
49-
place.
50-
- An existing file's permission bits are copied onto the
51-
replacement, so editing an executable script does not drop its
52-
``+x``. A file that does not exist yet keeps the umask-derived
53-
permissions a plain ``open(path, "w")`` would have produced. Only
54-
the "no such file" case is tolerated: any other stat/chmod failure
55-
propagates rather than silently shipping the wrong mode.
56-
57-
There is deliberately no ``fsync``: the failures this guards against
58-
are process-level (a failed write, a kill, Ctrl-C), and in all of
59-
them the rename never happens. Surviving a power loss in the window
60-
between write and rename would need an fsync here and on the parent
61-
directory, which ``SessionPersistence.save`` does not do either.
62-
63-
Consequences of replacing the file rather than rewriting it in place,
64-
all inherent to the atomic-replace approach:
65-
66-
- PATH must already be symlink-resolved. ``os.replace`` onto a
67-
symlink overwrites the LINK with a regular file, where an in-place
68-
write would have followed it. Every caller resolves the real path
69-
first (``Edit``/``Insert``/``Write`` via ``os.path.realpath`` on
70-
entry, ``_apply_section`` on its resolved target).
71-
- Hard links are broken: the new content lands on a new inode, so
72-
other links to the old inode keep the old content.
73-
- Owner/group, ACLs, SELinux labels and chattr flags are NOT carried
74-
over (only the mode bits are). This matters mainly for an agent
75-
running as root over files owned by someone else.
76-
- On a crash between the write and the rename the temp file survives
77-
as ``<name>.<hex>.tmp``; the original is still intact, but the
78-
stray file is visible to Glob/Grep and to ``git status``.
79-
80-
Raises OSError on failure. Note this needs write permission on the
81-
DIRECTORY, not just on the file -- the one behavioural difference
82-
from a truncating in-place write (GNU ``patch``, used by ``Edit``'s
83-
diff mode, has always had the same requirement).
84-
"""
85-
try:
86-
mode: int | None = stat.S_IMODE(os.stat(path).st_mode)
87-
except FileNotFoundError:
88-
mode = None # brand-new file: keep the umask default from write_text
89-
tmp = f"{path}.{uuid.uuid4().hex[:8]}.tmp"
90-
try:
91-
Path(tmp).write_text(content, encoding="utf-8", errors="surrogateescape", newline="")
92-
if mode is not None:
93-
os.chmod(tmp, mode)
94-
os.replace(tmp, path)
95-
except OSError:
96-
with contextlib.suppress(OSError):
97-
os.unlink(tmp)
98-
raise
99-
100-
10117
class PendingToolResult:
10218
"""Handle for an asynchronous tool result (mirrors ``:async t``).
10319

0 commit comments

Comments
 (0)