Repository navigation
[Bug]: content-ops image prepare decodes its UTF-8 plan with the host locale, failing non-ASCII plans on cp936 and the shipped UTF-8 guard on main #7063
Description
Problem
Impact: This is a current-main merge-blocking regression plus a public-capability failure. tests/test_loopx_text_io_utf8.py deterministically fails on latest main because loopx/capabilities/content_ops/media_cli.py:55 reads a UTF-8 plan with the host locale codec. The same omission makes the shipped content-ops image prepare command either fail before it creates or processes an image intent, or silently corrupt the prepared title/prompt, for non-ASCII text on a non-UTF-8 Windows host. It was introduced by #6876, merged on 2026-10-22, in the new single-image capability; the neighboring generation and media prepare paths in the same function already pin UTF-8, so the image path is the outlier.
- Issue origin: Observed and reproduced in a real environment by an AI coding agent on a zh-CN Windows host.
- LoopX version or commit:
f1f758bdf56118428f1f2f83220fe5020b03d8b0(latestmainwhen reproduced) - Host or runtime surface: Codex CLI / native Windows, Python 3.14.3, preferred encoding
cp936 - LoopX area: Capability or extension (providers, adapters, skills)
On this host, Path.read_text() without encoding= uses cp936/gbk. Image plans are JSON documents authored as UTF-8 and may legitimately contain Chinese, Japanese, Korean, or emoji text. Depending on the byte sequence, the read fails before ImageWorkspace.prepare() runs or succeeds with mojibake; either way the plan text is not reliably represented. The static guard that protects the whole shipped runtime fails the Python test job.
Minimal reproduction
- On a native zh-CN Windows host (Python reports
locale.getpreferredencoding(False) == "cp936"), check out the commit above. - Create a UTF-8 image plan whose
titleandpromptcontain non-ASCII text, a registry containing one active goal, and a source directory. - Run (paths abbreviated):
uv run --extra test loopx --registry <registry.json> --runtime-root <runtime> --format json content-ops image prepare --goal-id media-demo --plan-json <plan.json> --source-directory <project>- Independently run the shipped-runtime guard:
uv run --extra test python -m pytest -q tests/test_loopx_text_io_utf8.pyExpected behavior
The image plan is decoded as UTF-8, content-ops image prepare returns a successful prepared intent, and tests/test_loopx_text_io_utf8.py passes on main. Neighboring prepare paths already use read_text(encoding="utf-8").
Actual behavior
The real command fails before preparing the intent:
{"ok":false,"mode":"content-ops","error":"'gbk' codec can't decode byte 0xad in position 71: illegal multibyte sequence"}
EXIT=1
Some UTF-8 text is valid `cp936` byte pairs and therefore decodes without raising. A plan with `title` and `prompt` set to `你好` prepared successfully on the same host, but the response contained `浣犲ソ` for both fields. The defect therefore includes silent corruption as well as hard failure.
The guard fails on the same commit:
FAILED tests/test_loopx_text_io_utf8.py::test_shipped_runtime_pins_utf8_for_every_text_file_read_and_write
E AssertionError: assert ['loopx\\capa...ia_cli.py:55'] == []
E Left contains one more item: 'loopx\\capabilities\\content_ops\\media_cli.py:55'
1 failed, 1 passed in 30.64s
Sanitized diagnostics
Python: 3.14.3
locale.getpreferredencoding(False): cp936
Guard lane: python -m pytest ... in .github/workflows/python-tests.yml
Introduced by: 3a3cc7a2e3017102235231eaa53a40c8374bfbd8 (#6876)
Additional context
- feat(content-ops): add media providers and Goal artifact production #6876: feat(content-ops): add media providers and Goal artifact production #6876
- [Bug]: shipped-runtime UTF-8 guard fails on main - the stride schema asset is read with the host locale codec #6352 is a recent example of the same shipped-runtime guard class failing on
mainand was fixed promptly. - The guard's own history references earlier regressions fix(doctor): make Git diagnostics robust to non-UTF-8 host locales #4338, [Bug]: Windows non-UTF-8 locale — Path text I/O decodes structured state files with the locale encoding #4941, fix(control-plane): read structured state files as UTF-8 on non-UTF-8 hosts #4942, and [Bug]: Windows — write_private_json_atomic() raises PermissionError after the write already succeeded (os.open on a directory) #4997, showing this class is intentionally maintained as a merge-blocking invariant.
- The affected shape is general: non-ASCII image plans on a non-UTF-8 Windows host can be rejected or silently corrupted, not a single provider or fixture.
A one-line encoding="utf-8" pin at the image prepare read keeps the behavior aligned with the adjacent generation and media paths.
Activity