# P1-02 证据:Windows/Linux shell 与进程树终止 日期:2026-07-09(无人值守轮次) 状态:**完成(Windows 侧自动化全绿;Linux 侧逻辑已实现并单测覆盖参数/提示词,进程组用例在 Linux 上运行时生效)** ## 目标(摘自 REPAIR_BACKLOG.md) - Windows 明确通过 `cmd.exe` 执行;Linux 明确通过 `/bin/bash -lc` 执行,不依赖 `shell=True` 的平台默认值。 - 超时与主动中止都终止完整子进程树(Windows `taskkill /F /T`;Linux 独立 POSIX 进程组,SIGTERM→宽限→SIGKILL 整组)。 - 只保留一份通用 `SYSTEM_PROMPT.md`,运行时插入**短**平台 shell/path 段;两平台互不串段。 - 保留输出流、超时、截断、工具结果结构;不加命令审批/沙箱/路径限制。 ## 改动文件 | 文件 | 改动 | |---|---| | `core/platform_shell.py` | **新增**窄平台适配:`shell_command()`、`popen_flags()`、`kill_process_tree()`、`shell_prompt_section()`、`apply_platform_section()`、占位符 `{{SHELL_PLATFORM_SECTION}}` | | `core/agent/tools.py` | `tool_bash` 的 Popen 改 `shell_command(command) + popen_flags()`;`_kill_tree` 委托 `kill_process_tree`;移除 `ctx["shell"]` 隐式开关 | | `core/llm_engine.py` | `load_system_prompt()` 读文件后过 `apply_platform_section()`(每次请求仍重读,既有行为不变) | | `SYSTEM_PROMPT.md` | 通用正文化:第 1 节去 Windows 路径/conda 环境名;原 1.1「shell 真相」cmd 表整体移入运行时 Windows 段;工具表与 3.2 去掉 `cmd.exe`/`dir`/`findstr` 字样;占位符落在原 1.1 位置 | | `tests/test_cross_platform_shell.py` | **新增** 20 项断言(A 平台参数 / B 提示词 / C 进程树 / D 安全边界) | ## 关键设计决策 1. **Windows 用字符串命令行,不用 argv 列表。** `["cmd.exe","/d","/c",cmd]` 列表形态会被 CPython `list2cmdline` 把内部引号转义成 `\"`,cmd 不认, 带引号路径直接 `'...\python.exe"' is not recognized`(实测复现)。最终契约: `cmd.exe /d /s /c ""` 作为**字符串**交给 CreateProcessW,cmd 按 /s 规则解析 /c 参数 (外层引号剥离、内部引号保留)。实测矩阵:引号 Python 路径 `rc=0`;`echo a && echo b` 正确; `%USERPROFILE%` 展开;`;`/单引号行为与旧文档一致。 注:旧 `shell=True` 之所以能跑,是因为 CPython 对带引号程序名直接 CreateProcess(不经 cmd); 新契约统一显式过 cmd,行为更可预测且与提示词一致。 2. **POSIX 安全不变量**:`kill_process_tree` 仅当 `os.getpgid(pid) == pid`(确认 `start_new_session` 生效、子进程是组首)才 `killpg`,否则退化单进程 `kill`,绝不误杀调用方所在组。 流程:SIGTERM 整组 → 轮询至 `grace_s=3.0s` → SIGKILL 整组。 3. **提示词单一来源**:`SYSTEM_PROMPT.md` 唯一;`apply_platform_section` 只做占位符替换, 无占位符(兜底提示词)原样返回。`load_system_prompt()` 每请求重读 → 平台段永远对应当前平台。 ## 验证结果(本机 Windows 11 x64,CPython 3.10.21,全部显式超时) | 命令 | 结果 | |---|---| | `python tests/test_cross_platform_shell.py` | **20/20 PASS**(EXIT=0) | | `python tests/test_bash_stream.py` | **30/30 PASS**(EXIT=0) | | `python tests/test_tool_params.py` | **35/35 PASS**(EXIT=0) | | `python tests/run_tests.py`(agent core,含 test_agent_core) | **41/41**(EXIT=0) | | `python tests/smoke_mode.py`(完整 agent 回合,走 tool_bash) | ALL PASS(EXIT=0) | | P0-03 一致性检查(AGENTS.md 指针 / ARCHITECTURE 历史资料标记 / 无第三方任务书正文) | 3/3 PASS | ### 测试点明细(test_cross_platform_shell.py) - **A 平台参数**:Windows 命令 == `cmd.exe /d /s /c "echo hi"`;Linux argv == `["/bin/bash","-lc","echo hi"]`; Linux `popen_flags() == {"start_new_session": True}`,Windows 为空。 - **B 提示词**:通用正文含占位符且无平台泄漏(无 `cmd.exe`/`/bin/bash`);Windows 段含 `cmd.exe` 无 `/bin/bash`,Linux 段反之;替换后通用正文逐字节相同(B6);`load_system_prompt()` == 文件+当前平台段。 - **C 进程树(真实进程,父挂 30s + 孙每 0.2s 写心跳文件)**: - C1 超时 3s → 错误结果含「超时」,耗时 <15s,**父与孙都不存在**(心跳静默 >0.6s 且无完成标记); - C2 主动中止 1.5s → 错误结果含「中止」,**父与孙都不存在**; - C3 Linux 独立进程组(Windows 上 SKIP;`pgid==pid` 断言 + killpg 后子进程消失,Linux 运行即生效)。 - **D 安全边界**:已退出进程、`None` 输入均不抛异常。 ### 调试过程记录(铁律:所有调试命令显式超时) 1. 首跑 C1 失败:`'...\python.exe"' is not recognized` → 定位为 `list2cmdline` 引号转义; 读 CPython 3.10 `subprocess.py` 确认 `shell=True` 实为直接 CreateProcessW(带引号程序名不经 cmd)。 2. 尝试 `["cmd.exe","/d","/s","/c", '"'+cmd+'"']` 列表 → 仍失败(同样被转义)。 3. 改**字符串**命令行 + 实测矩阵(引号路径/&&/管道/%VAR%)→ 全过,定稿。 4. 首跑进程树用例 `hb_last=None`:父脚本模板漏传 `child.py` 脚本路径(把心跳路径当脚本)→ 手动 `subprocess.run` 复现(超时 6s/3s 探针)→ 修模板,20/20 通过。 ## Linux 侧待办(不阻塞本任务) - 在 Linux 环境跑一次 `python tests/test_cross_platform_shell.py`(C3 生效)+ `test_bash_stream.py` 即可闭环;代码路径与 Windows 共用同一套 `kill_process_tree`/`shell_command` 分派。