Repository navigation
fix(runtime): pin the delegation preview worker's stdio to UTF-8 - #6558
JasonBuildAI wants to merge 1 commit into
Conversation
`loopx.entrypoint.pin_utf8_stdio` pins a LoopX-started process's own streams to UTF-8. loopx-project#6470 found four stdio hosts that answer the same UTF-8 JSON transport but still read `sys.stdin` and wrote `sys.stdout` with the host locale codec - `cp936` on a zh-CN Windows host - and loopx-project#6472 pinned them. The delegation preview worker was another process of that family and was not covered: `DelegationPreviewTransport` starts it as `[python, -P, -m]` through `delegation_preview_bridge.ts`, which relays each framed request to its stdin as UTF-8. On a cp936 host `gbk` decodes the frame into different text, so a request whose `--project`/`--scan-root` names a workspace under a non-ASCII path no longer matches the worker's own cwd and its bound-work check rejects the still-valid read-only Turn with `preview cannot execute or retarget bound work`; unpaired bytes raise `UnicodeDecodeError` instead. Worker diagnostics are not relayed, so the caller only saw `delegation preview returned no structured result` and delegation inspection was unavailable for those workspaces - the default layout for a Chinese Windows account, whose home directory already contains non-ASCII text. Pin the worker before it reads its first frame and guard it in the existing `tests/test_runtime_stdio_host_utf8.py` family check. The new test fails on the previous source and passes with the pin. Closes loopx-project#6557. Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
5bac3ae to
55f1083
Compare
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; model=gpt-6.1-sol; provider=OpenAI; declaration_source=runtime_reported; reasoning_effort=max.
Exact head: 55f1083871930ba4c94b6e483967906b1e85b4b0;实际 merge base 3bc74f9317479e36286414d899a77172a115e330。本评审覆盖完整提案。
动机
在非 UTF-8 宿主上检查委派工作的用户,尤其是工作区路径包含中文时。
旧进程把桥接器发送的 UTF-8 请求按 GBK 解码,真实预览失败;当前头在读取前固定编码,相同 File/SQLite 预览正常返回。
恢复现有只读委派检查,不需要用户改路径或切换整个宿主的编码设置。
本 PR 不改变执行授权、进程清理、前端或 Lark 控件,也不代表所有 Windows 行为都已验收。
改动思路
编码约定继续由已有 pin_utf8_stdio 统一拥有,worker 只在首次读取前调用;TS 仍管进程清理,原 CLI/类型化 owner 仍管读取和权限,没有第二套状态机。
仅修复委派预览 worker 的字节输入适配并补齐现有 stdio 家族回归;不扩大为其他宿主、清理机制或全平台交付。
不再因中文工作区迫使用户迁移目录或重复启动检查;现有 CLI 的只读拒绝、当前状态重读及同一进程后续请求继续成立。 最强的“不发此补丁”方案是保留现状或只设置调用方环境;前者留下已证实故障,后者无法覆盖实际拥有问题的入口。保留现状会继续让正常检查不可用;只在一个调用方导出编码环境会漏掉直接模块入口,并复制已有规则。公共helper本身已符合约定,调用一次即可,无需新命令、设置或TS决策。
具体改动
规范来源:#6557;本次读取正文版本 body-sha256:079ab6295ab5441d6909d62d96e0cede42c49c34b5f0cbd87993959a31c7b550。以下标签对应原文没有编号的Expected behavior条款,不增加新的规范要求。另核对既有 docs/reference/source-cli-entrypoint.md 与实际launcher/helper。
UTF8-stdio:implemented。Expected behavior: worker pins its own streams with the existing helper. 由serve -> pin_utf8_stdio承担,验证见下。binding-text:implemented。Expected behavior: inspect the same non-ASCII workspace the caller named. 由serve bound-work checks and real Delegations.inspect承担,验证见下。strict-input:implemented。Existing source entry contract: malformed input remains rejected; UTF8 streams are untouched. 由loopx/entrypoint.py::pin_utf8_stdio承担,验证见下。
完整diff是2个路径 +71/-2:生产代码只增加既有helper的import和serve首条调用;测试保留原cwd默认并新增显式cwd,覆盖两种非ASCII字节形状。没有为了通过检查改宽绑定条件。基线不是“坏JSON被放过”:相同合法UTF8请求在locale解码时失败,当前进入原CLI并返回缺少fixture Goal的结构化错误;真实注册的MCP路径再证明不仅是假回执。两臂实际Python3.11.17和依赖版本已匹配,最初3.14基线留作非决定性历史。
关键代码讲解
serve:首帧读取前调用既有UTF8 helper;Goal、Agent、Todo、project、scan-root必须仍与绑定相同,execute/resume继续被拒绝。_run_argv:保留现有默认cwd,同时允许测试显式传入中文工作区;输入和捕获输出均使用字节,避免父进程编码替子进程掩盖问题。test_delegation_preview_worker_reads_the_binding_request:用两种非ASCII字节形状启动真实-P -m worker并读取run_once结构化响应,没有mock提供待证明的CLI结果。
对主干的风险
当前 Python 3.11.17:UTF-8 stdio 家族、helper owner、真实 preview reuse 共60项通过;强制GBK和中文临时目录的真实MCP File/SQLite两项通过,同解释器基线两项失败。独立worker两种名字×GBK/UTF8/改绑/execute/非法字节共10类,每个合法请求连续两帧,当前合法0退出、负例1退出且无文件/权威副作用;基线GBK合法输入失败、UTF8输入保持成功。Ruff通过;native premerge 5 direct+10 selected全部通过,无失败/skip/hold。
审查宿主为 macOS;通过子进程强制 GBK 复现编码边界,未独立执行原生 Windows 整机、已安装 App 或长期 provider 收益。作者另报的九个 Windows cleanup 失败未由本次 Mac 运行重验;不将其当当前已通过。两臂同有 Pydantic forward-reference warning,未改相关源码。
基线拒绝合法GBK场景,当前头正常接受;改绑、execute和非法字节仍被拒绝,临时registry逐字节不变、runtime未产生文件。 本次检查了原始base到head的全部改动、DCO和diff hygiene;已获取的main a99cb07addfc163fda9435c6455cc9462b657855 对这两个原文件的base blob未改变,merge-tree无冲突,但这不是实际组合后运行或安装采用的证明。没有远程CI查询、等待或派发。编码与launcher都只负责IO,原权限、claim/lease及typed effect owner不变;通过premerge也没有得到合并权限。
我的整体评价
我批准这个有明确反例、复用既有owner且完整可逆的修复。long_horizon=improved,user_experience=improved:中文目录下可持续使用原检查,不增加设置或用户准备轮次;本判断不声称长期净收益已实测。未来变更友好性检查已应用:在既有helper接入遗漏worker,没有理由再为两行生产改动抽出新框架。
编码约定继续由已有 pin_utf8_stdio 统一拥有,worker 只在首次读取前调用;TS 仍管进程清理,原 CLI/类型化 owner 仍管读取和权限,没有第二套状态机。 仅修复委派预览 worker 的字节输入适配并补齐现有 stdio 家族回归;不扩大为其他宿主、清理机制或全平台交付。 没有新增共享vocabulary或平行Python决策源;本地/advisory与完整semantic检查均与该边界一致。完整本地premerge通过,没有人工hold;仍需单独执行精确head的closeout/readiness和维护者合并决定。我未合并、升级或修复作者源码。
English verdict: APPROVE - 55f1083; the existing UTF-8 helper repairs the demonstrated GBK/CJK delegation-preview failure while preserving strict input and bound read-only checks; same-Python real File/SQLite baseline/head and local risk checks passed.
Goal And Delivered Outcome
mainat81ff72b64(native Windows 11,locale.getpreferredencoding(False)=cp936,sys.stdin.encoding=gbk).loopx.entrypoint.pin_utf8_stdioalready pins a LoopX-started process's own streams to UTF-8, and [Bug]: four shipped stdio hosts still decode their UTF-8 transport with the host locale codec, so non-ASCII paths and diffs crash or are silently rewritten on a cp936 host #6470 / fix(runtime): pin UTF-8 stdio in four shipped stdio hosts #6472 applied it to the four remaining Python stdio hosts of that transport family.loopx/control_plane/collaboration/delegation_preview_worker.pywas a fifth host the sweep did not cover:DelegationPreviewTransportstarts it as[python, -P, -m]throughdelegation_preview_bridge.ts, which relays each framed request to its stdin as UTF-8, while the worker readfor line in sys.stdinthrough the host locale codec.--project/--scan-rootare decoded into different text, so a read-only Turn bound to a workspace under a non-ASCII path no longer matches the worker's own cwd; the worker refuses it withpreview cannot execute or retarget bound work(or dies withUnicodeDecodeErrorwhen the bytes do not pair) and the caller only seesdelegation preview returned no structured result. With the pin, the same request is answered as a structured preview and the real transport journey passes with a non-ASCII--basetemp. Thereal_entrypointandregression_parityrows below carry the before/after output.mainatec327628f.Author Declaration
Implemented against
docs/reference/source-cli-entrypoint.mddocuments the pin contract. Repository revisionec327628f.stdin/stdout/stderrto UTF-8pin_utf8_stdio()as the first statement ofserve()inloopx/control_plane/collaboration/delegation_preview_worker.pytests/test_runtime_stdio_host_utf8.py::test_delegation_preview_worker_reads_the_binding_requestjson.loadsunchanged, so the bound-work check compares the caller's workspacetests/test_delegation_preview_reuse.py::test_mcp_registration_explicitly_selects_reusable_servicewith an ASCII and a non-ASCII--basetemppin_utf8_stdiokeepsstdinstrict and skips already-UTF-8 streamstests/test_cli_stdio_utf8.py(owner test of the shared helper)loopx/entrypoint.py::pin_utf8_stdioowner instead of a new helper; read the worker, its transport, its TS bridge and_python_module_commandto confirm the pin precedes the first read ofsys.stdinand that the caller already writes UTF-8. The refusal path is a read-only availability bug, not an authority bypass: the corrupt decode happens before any authority read or write, and the worker's bound-work check stays intact. Deliberately left out: no transport or bridge change, no retargeting of the bound-work check, no new guard family. A repository scan finds no other LoopX-started module that still reads a UTF-8 stdio transport through the locale codec (the remainingsys.stdinconsumers run inside the already-pinned CLI process or readsys.stdin.bufferdirectly, or are templates/tests). The advisory semantic-inventory probe over the diff reports no new vocabulary carriers. No new dependency and no JSON option change. Leftruff formatalone: the worker file is already unformatted onmain(verified against theec327628fblob) and reformatting it would balloon the diff.Scope And Continuation
Validation
55f108387(content-identical rebase of5bac3ae49, the head the heavier rows below ran against; rebased ontomainat3bc74f931)real_entrypointpassedtests/test_runtime_stdio_host_utf8.pystarts the worker exactly as its real caller does ([python, -P, -m], UTF-8 frame on stdin,PYTHONIOENCODING=gbk+PYTHONUTF8=0, cwd = the non-ASCII workspace) in both non-ASCII byte shapes. Whole family file: 12 passed in 7.41s at the tested revision.real_entrypointpassedValueError: preview cannot execute or retarget bound workand empty stdout; the pinned worker answers{"kind": "preview", "id": 1, "returncode": 1, "value": {..., "mode": "run_once", "error": "Goal 'goal' is not registered", ...}}for the same frame (the fixture goal is intentionally not registered, so the CLI reports its own structured error).regression_paritypassed81ff72b64,ec327628f).integrationpassedDelegations.inspect):test_mcp_registration_explicitly_selects_reusable_serviceis 2 passed at the tested revision with both an ASCII--basetempand one whose directory name contains CJK characters. Before the fix the same non-ASCII run was 2 failed withdelegation preview returned no structured result.integrationblockedtests/test_delegation_preview_reuse.pyfull file on this Windows host: 24 passed, 12 POSIX-skipped, 9 failed. All 9 failures are identical atec327628fwith this diff absent (transport cleanup_process is Noneassertions; the bridge close does not confirm on this host), so they are pre-existing Windows behavior unrelated to this change. Spot-checked at baseline by name for all 9.staticpassedruff checkover the CI scope (tests loopx/canary loopx/control_plane loopx/domain_packs loopx/presentation): All checks passed.ruff formatis not run by the workflow; baseline format note above.staticpassedscripts/generate_semantic_inventory.py --changed-from HEAD~1: no supported new vocabulary carriers. Public boundary scan of the 2 changed paths:public boundary scan clean: 2 files.staticblockedpython -m mypyon this Windows host reports 6 pre-existing errors in files this change does not touch (loopx/extensions/process_runtime.py,loopx/control_plane/effect_runtime.py; POSIX-only attributes such asSIGKILL,killpg,WNOHANG). The configured type-check file list does not include this change; CI runs it on Linux.Frontend / Visual Evidence
Type of Change
LoopX Area
Technical Direction
Shared-authority RFC fixture impact
Boundary Checklist
.loopx/,.codex/goals/, and liveACTIVE_GOAL_STATE.md).none.Signed-off-bytrailer (git commit -s).AI disclosure: this change was drafted with AI assistance at my request, under my direction and review. The constraints for it were set by the human requester (single defect, minimal diff, no unrelated refactor, no new dependency), and a human reviewed the result before submission.