ENG-824: force UTF-8 in the scratchpad so non-UTF-8 host locales (GBK/CJK Windows) don't crash#253
ENG-824: force UTF-8 in the scratchpad so non-UTF-8 host locales (GBK/CJK Windows) don't crash#253alecantu7 wants to merge 5 commits into
Conversation
…e scratchpad (ENG-824) On a host whose default code page isn't UTF-8 (GBK/cp936 on Chinese Windows, and likely other CJK locales) LocalScratchpadRuntime crashed before any cell ran: `_BOOT_SCRIPT_PATH.read_text()` decoded the boot script (which contains `…`/`—`) with the locale default → `'gbk' codec can't decode byte 0xa6`. Fix, at every parent↔child byte boundary + interpreter-level: - Read/write the boot script as UTF-8 (read_text/encode). - Force UTF-8 mode in the subprocess env (PYTHONUTF8=1 / PYTHONIOENCODING=utf-8, via _utf8_env, setdefault so an explicit override wins) — so the child's file I/O and stdio are UTF-8 regardless of host locale. - Decode/encode the cell payload + stdout/install output as UTF-8 (errors="replace" on display output so odd bytes never crash the reader). Non-breaking: this content is already UTF-8 on the wire, so UTF-8-default hosts (macOS/Linux/English Windows) are unchanged; it only turns the hard crash into working on non-UTF-8-locale hosts. Tests: _utf8_env forces UTF-8 / respects overrides; the boot script must be read as UTF-8 (its bytes are not GBK-decodable). 96 existing scratchpad tests still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…view, ENG-824) Self-review: a bare PYTHONIOENCODING=utf-8 downgrades the child's stdio error handler from surrogateescape → strict (verified), which adds nothing over PYTHONUTF8=1 (already utf-8 for open()/filesystem/stdio) and re-introduces a strict-encode crash on exotic output. Keep only PYTHONUTF8=1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-review (adversarial)Major — found & fixed (0dc7261). I originally set both It added nothing (UTF-8 mode already makes Low — noted, not changed:
Electron/CLI paths unchanged; 96 existing scratchpad tests + 3 new pass. |
…elf-review, ENG-824) Skill review: the main scratchpad subprocess got _utf8_env but the dependency install subprocess didn't, so on a non-UTF-8 host locale pip/uv output could come back as mojibake. Pass env=_utf8_env(os.environ) here too for consistency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/code-review skill pass (high) — outcomeRan the review skill (complementing the earlier manual pass). No new correctness bugs; the substantive one (
Remaining findings are low/noted, not changing: forcing UTF-8 mode also flips the user's own |
ZoranPandovski
left a comment
There was a problem hiding this comment.
@alecantu7 _setup_parent_site_packages still writes _parent_venv.pth with a plain open(pth_path, "w"), so it's locale-encoded, while the child (now in UTF-8 mode) reads .pth files as UTF-8.
Worth adding encoding="utf-8" there in this PR since it's the same class of bug.
| assert env["PYTHONUTF8"] == "0" | ||
|
|
||
|
|
||
| def test_boot_script_must_be_read_as_utf8(): |
There was a problem hiding this comment.
The boot script test checks the file's bytes rather than how we read it. If someone reverts read_text(encoding="utf-8") back to read_text(), the test still passes on UTF-8 CI.
Could we make it pin the actual read, e.g. monkeypatch Path.read_text and assert it's called with utf-8?
…pt read (PR #253 review, ENG-824) Address Zoran's review on #253: - _setup_parent_site_packages wrote _parent_venv.pth with a plain open() (host- locale encoded) while the child reads .pth as UTF-8 under UTF-8 mode — same class of bug as the boot script. Write it as encoding="utf-8". - Extract _read_boot_script() and add a test that spies on Path.read_text to assert the boot-script read passes encoding="utf-8" — the previous bytes-only test would still pass on UTF-8 CI if the explicit encoding were dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @ZoranPandovski — both addressed in d9406ce:
All 4 tests green locally. Re-requesting review. |
…w, ENG-824) Self-review of the review-response commit: I pinned the boot-script *read* as UTF-8 but left the sibling .pth *write* fix untested — asymmetric with the exact concern Zoran raised. Add a regression test that spies on open() and asserts the _parent_venv.pth write passes encoding="utf-8" (bypasses the heavy __init__; verified it fails if the encoding is dropped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-review (adversarial) of the review-response commitsA — Asymmetric coverage → fixed (1fc202f). I'd pinned the boot-script read as UTF-8 but left the sibling B — Premise re-checked, holds. The write fix assumes the child reads C — Minor, accepted as-is. The read spy couples to the keyword-arg form ( Full file: 5/5 tests green locally. |
What
On a host whose default code page isn't UTF-8 — GBK/cp936 on Chinese Windows, and likely other CJK locales —
LocalScratchpadRuntimecrashes before any cell runs, so the scratchpad (and thus most real tasks) is 100% unusable for those users.Root cause (confirmed)
local.pycrossed the parent↔scratchpad byte boundary using the host locale encoding everywhere. First failure:_BOOT_SCRIPT_PATH.read_text()decodes the 32 KB boot script with the locale default; the script contains…/—(a…at byte ~6382), so on GBK it throws'gbk' codec can't decode byte 0xa6. Verified locally: the boot script's bytes decode fine as UTF-8 but raise under GBK — byte0xa6matches the reporter's error exactly.Fix (interpreter-level + explicit at every boundary)
read_text(encoding="utf-8")/encode("utf-8")).PYTHONUTF8=1/PYTHONIOENCODING=utf-8via_utf8_env(setdefault, so an explicit operator override still wins) — so the child's file I/O + stdio are UTF-8 regardless of host locale, covering user data (Chinese DB rows/CSVs) too.errors="replace"on display output so stray bytes never crash the reader).Why it can't break anything
All of this content is already UTF-8 on the wire (anton writes UTF-8). On macOS/Linux/English-Windows the default is already UTF-8 → behavior is identical. It only changes non-UTF-8-locale hosts — turning a hard crash into working. There's no working baseline to regress (it was a 100% crash there).
Testing
tests/test_scratchpad_utf8.py:_utf8_envforces UTF-8 / respects an explicit override; the boot script must be read as UTF-8 (its bytes are not GBK-decodable — the regression guard).test_scratchpad.py,test_scratchpad_observer_dispatch.py).Origin
Prod Cowork-desktop (Windows) user tried to test a PostgreSQL connection 3× today; every attempt died on this GBK codec error (Langfuse
tool:report_failure×3). See ENG-824.Coordination
This is the scratchpad half. A companion
coworkPR setsPYTHONUTF8=1on the cowork-server sidecar launch, covering the non-scratchpad anton reads (workspaceanton.md/.env, memory store) at the process level. The two are independent (either can merge first); together they close the whole locale surface. Both reach users on the next desktop build (cowork-server bumps its anton dep).Security check
Encoding-only change; no new surface, no secrets touched.
Closes ENG-824 (scratchpad half).