Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

> **中文版文档见 [README_zh.md](README_zh.md).**

For unexplained Windows process exits, use the [independent process monitor](docs/external-process-monitor.md) to collect uv/Python exit codes, lifecycle, memory, health, and available Windows events without restarting the proxy.

## ✨ Core Features

- **Protocol conversion** - Supports three standard formats: OpenAI Chat Completions, Anthropic Messages API, and Responses
Expand Down
2 changes: 2 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

> **English version: see [README.md](README.md).**

Windows 代理无异常记录却自动退出时,使用[独立进程监控](docs/external-process-monitor.md),在不重启代理的情况下采集 uv/Python 生命周期、退出码、内存、探活和可读取的系统事件。

## ✨ 核心特性

- **协议转换** - 支持 OpenAI Chat Completions、Anthropic Messages API 和 Responses 三种标准格式
Expand Down
310 changes: 310 additions & 0 deletions docs/HANDOFF_WORKBUDDY2API_UNEXPECTED_PROCESS_EXIT_2026-10-23.md

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions docs/external-process-monitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Windows 独立进程取证监控

此工具用于回答「uv 和 Python 谁先退出、Windows 记录的退出码/退出时间是什么」。它不判断根因、不自动重启、不导入代理代码;兼容 main 2.0.6 和 PR #17 的 2.0.7。仅依赖 Python 3.10+ 标准库和 Windows PowerShell。

需要进一步捕获终止发起者时,参见[Silent Process Exit 取证和诊断启动命令](silent-process-exit.md)。本机已验证并配置专用诊断映像;旧 `uv run python ...` 不会触发这层 Windows 取证。

## 三种模式

| 模式 | 行为 |
| --- | --- |
| `--watch-proxy` | 每 5 秒后台发现以 `-m codebuddy_proxy` 运行的 uv/Python,附加并发现后代;消失后继续等待下一轮,不启动代理 |
| `--pid PID` | 立即附加指定活进程和后代;所有已捕获进程退出后结束 |
| `--launch EXE ARGS...` | 明确启动新进程并保留句柄;所有已捕获进程退出后结束,供以后手动切换 |

监控句柄只请求查询/同步权限,没有终止/重启目标的接口。`--duration SECONDS` 或 Ctrl+C 只结束监控,目标继续运行。等待模式匹配本机可访问的同名模块,不能按工作目录区分实例;console-script 启动、改名解释器等用 `--pid` 精确附加。

## 先保全,再观察

先确认实际进程和 `/health`,不能因业务日志没更新就推断进程死亡。已有活进程应立即附加,而不是等待扫描。

把 `.workbuddy2api` 下当前 `process-events.jsonl`、`console.log`、`crash.log`、`codebuddy-proxy.jsonl`、`proxy.log` 复制到时间戳目录,保存 SHA256,不改原件。证据可能包含凭证和请求,只留本机,不上传 PR。

## 独立启动监控器(不启动代理)

本次 worktree 的示例(合并后可将 `$monitorRepo` 改成主目录)。解释器借用原项目环境,不安装/修改依赖。

```powershell
$monitorRepo = 'D:\workspace\workbuddy2api-external-monitor'
$monitorPython = 'D:\workspace\workbuddy2api\.venv\Scripts\pythonw.exe'
$monitorArgs = '"' + $monitorRepo + '\scripts\watch_processes.py" --watch-proxy'
$monitor = Start-Process -FilePath $monitorPython -ArgumentList $monitorArgs `
-WorkingDirectory $monitorRepo -WindowStyle Hidden -PassThru
$monitor | Select-Object Id, StartTime
```

`pythonw.exe` 无交互式控制台,监控器和代理分别运行。不能把监控工作放进代理线程,也不要在要测试关闭的代理控制台里运行它。本工具不安装服务/计划任务、不设置开机自启。如果宿主用 Job Object 连带终止后代,独立控制台仍不保证逃离 Job;持久部署可由维护者在独立计划任务/服务中启动相同命令。尚未在 Wave 关窗场景验证此边界。

默认目录 `$HOME\.workbuddy2api\monitor\YYYYMMDD-HHMMSS-随机标识\`。先确认 `events.jsonl` 有 `monitor_start`,随后 `sample` / `health` 持续更新。等待时 `sample.processes` 可为空。无日志或出现 `monitor_error` 表示监控未生效。

```powershell
$latest = Get-ChildItem "$HOME\.workbuddy2api\monitor" -Directory |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
Get-Content (Join-Path $latest.FullName 'events.jsonl') -Tail 12
```

若代理已运行,以 `Get-CimInstance Win32_Process` 找到 uv 根 PID,把启动参数换成 `--pid 实际PID`,不要复用历史 PID。不存在/无权限会明确失败。避免重复监控相同范围。

## 将来的启动模式

**本次实施不执行下面的真实代理命令,也不停止现有代理。** 将来由维护者选择切换时,在主项目目录执行:

```powershell
Set-Location 'D:\workspace\workbuddy2api'
.\.venv\Scripts\python.exe ..\workbuddy2api-external-monitor\scripts\watch_processes.py `
--launch uv run python -m codebuddy_proxy --desensitize --host 0.0.0.0
```

`--launch` 之后全是目标参数;监控选项放在前面。目标继承工作目录、环境和原参数,以 `CREATE_NO_WINDOW` 运行,stdin=DEVNULL,原生 stdout/stderr 写入该次目录 `target-stdout.log` / `target-stderr.log`,覆盖绕过 Python Tee 的输出。控制台和交互输入行为已改变,先完成认证;已有服务占用端口时不要再启动一份。

OS 输出文件不轮转,避免目标持有句柄时截断证据,长时间启动模式需另行管理磁盘空间。监控 JSONL 轮转 8 MiB × 当前文件加 4 个备份;多个运行目录另行归档。取证事件 flush+fsync,普通 sample 仅 flush。磁盘满会使监控失败,不能保证断电或磁盘故障仍有完整证据。

## 记录与判读

默认每 250 ms 枚举后代/检查退出,每 2 秒记录内存/心跳和 localhost:8787/health(超时 1 秒),每 30 秒查询 Windows 事件(后台执行,总超时 15 秒)。UTC 时间、monotonic_ns、seq 用于关联;**seq 是发现顺序,退出顺序须看 exit_ticks**。

| 事件 | 内容 |
| --- | --- |
| `process_observed` | pid、ppid、image、created_ticks / created_at;句柄已保留,不代表此刻才启动 |
| `process_exit` | Windows 无符号 exit_code / exit_code_hex、GetProcessTimes 的 exit_ticks / exited_at、最后存活采样、cause=unknown |
| `sample` | PID+创建时间、working_set_bytes / private_bytes;权限不足写 memory_error |
| `health` | HTTP 状态/是否成功/耗时;不读取正文、不走环境代理、不跟随重定向 |
| `business_log_tail` | 文件长度/更新时间,末尾最多 64 KiB / 32 条中的 timestamp/event/request_id/status/run_id/pid 白名单;不复制 headers/body |
| `windows_event_query` / `windows_events` | 查询窗口、System/Application 警告/错误/严重事件,以及可读 Security 4688/4689、Sysmon 1/5,后两者按目标 PID 过滤 |
| `*_failed` / `process_open_failed` / `monitor_error` | 采集失败,不能解释成“没有发生事件” |
| `monitor_stop` | 停止原因、still_tracked、targets_stopped=false;心跳中断且无此记录,监控器本身也可能消失 |

先用 `WaitForSingleObject(handle, 0)` 判断终止再读 `GetExitCodeProcess`,可区分仍运行与真正退出码 259。原始 exit_ticks 保留 Windows 100ns 值,格式化时间精度为微秒。句柄和创建时间用于防止 PID 重用误关联。

事件查询重叠 60 秒,按 channel+record_id 去重,每频道最多 200 条,超出标 truncated。延迟超过窗口、日志被清空仍可能漏采。超时/无权限/频道不存在明确记录;不修改审计策略、不安装 Sysmon。频道可读但无匹配事件,不代表已开启进程审计;4688/4689 需管理员核实策略。4689 的 Subject 账户不是终止调用方进程 ID。

复盘步骤:

1. 按 `(pid, created_ticks)` 分组,把 uv、venv Python、解释器的 `process_exit` 按 `exit_ticks` 排序。相等时不能确定先后;只捕获一层时不能推断其他层。
2. 对齐最后成功/首次失败的探活,以及业务 request_id 完成事件。尾部摘要不完整,**不能证明没有在途请求或处于空闲**;完整原日志按 request_id 配对,并统一数字/字符串 timestamp 的时区。
3. 检查系统事件、truncated 和采集失败;不能把时间邻近的更新/SideBySide 错误直接视作原因。
4. 保留运行目录、轮转文件、原业务日志,再根据状态选择 WER/ProcDump/ETW/EDR。退出码只是状态证据,程序/外部调用者都可指定相同值;本工具不识别 TerminateProcess 调用方。

## 观测边界

不能恢复已死亡且未留句柄的历史进程退出码。等待模式有约 5 秒加 CIM 查询延迟的窗口,后代枚举有 250 ms 窗口,短命进程可能漏采;启动模式持有根进程,但不保证捕获所有短命后代。不能从新监控倒推 08:53 故障根因。

记录监控器自己的可处理 SIGINT/SIGTERM,不拦截/注入目标信号,不能证明目标收到 Ctrl+C/CTRL_CLOSE。监控器/系统一起退出、断电可能同时丢证据。终端关闭、Job Object、真实 native crash 和资源耗尽仍待现场验证。

## 测试和 Review

```powershell
$env:PYTHONUTF8 = '1'
$env:PYTHONPATH = 'src;.'
D:\workspace\workbuddy2api\.venv\Scripts\python.exe -m pytest test_external_process_monitor.py -q
D:\workspace\workbuddy2api\.venv\Scripts\python.exe -m pytest -q --tb=short `
-k 'not test_session_profiles_are_isolated_and_saved_with_metadata'
```

测试仅对独立创建的临时进程精确 Popen 对象/句柄强制终止,绝不按名称批量结束。覆盖正常/异常/绕过 Python 钩子的退出、259、高位状态、强制终止、uv/Python 分别退出和顺序、监控停止后目标存活、发现、轮转、脱敏、PID 身份及频道权限。高位状态测试调用 ExitProcess 指定值,不是真实访问冲突。原 Windows 0600 权限断言失败单独报告,未改原测试。

Review 检查最小权限、无终止/重启接口、句柄释放、PID 重用、259、非阻塞采样、事件过滤/上限/失败、采集延迟和隐私。真实 Wave 关窗、注销/断电、终止者归因尚未验收,不能以隔离测试代替生产复现。

参考:[进程句柄/ID](https://learn.microsoft.com/en-us/windows/win32/procthread/process-handles-and-identifiers)、[WaitForSingleObject](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject)、[4689](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4689)、[Sysmon](https://learn.microsoft.com/en-us/windows/security/operating-system-security/sysmon/sysmon-events)。

当前 Windows 实际解释器取证入口及回滚以 [silent-process-exit.md](silent-process-exit.md) 为准。2026-10-23 20:38/21:02 的再次退出证实 venv launcher 取证不足;新入口使用两个专用真实解释器映像,原 pythonw 观察器与额外独立 observer 保留。最新回归 267 passed、1 skipped、1 deselected。
65 changes: 65 additions & 0 deletions docs/silent-process-exit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Windows 实际解释器退出取证

2026-10-23 20:38 现场证实旧配置只覆盖 venv launcher,真正的 Anaconda python.exe 先退出,转储无法说明它的来源。21:02 再次出现启动监控解释器与代理解释器相近退出,顺序与此前不同。尚未确认根因。

## 当前方案

`scripts/prepare_native_runtime.py` 从 `sys.base_prefix` 复制真实 CPython console/windowed 映像及 Python、VC runtime、Anaconda zlib DLL,生成独立 runtime。不是复制 venv redirector。专用映像名为 `workbuddy2api-runtime.exe` 和 `workbuddy2api-observer.exe`,只对这两个名称配置 Silent Process Exit,不修改共享 python.exe/pythonw.exe 的设置,也不改原 venv 文件。

bootstrap 保留 DLL 搜索目录句柄,通过 site.addsitedir 使用原 venv 依赖及 editable 源码。sys.prefix 指向诊断 runtime,sys.base_prefix 保持原解释器;这是一项明确的环境差异。原 venv 及基础 Python 仍须保留。复制文件、基础映像/DLL 和原 pyvenv.cfg 的 SHA256 在启动前校验;升级 Python 后需重新验证。

`scripts/manage_native_runtime.ps1` 的管理员验证要求:

- OS 映像、sys.executable 和实际 workload PID 相符,拒绝再次落入 redirector 子进程;确认 FastAPI、SSL、SQLite 和原代理源码可导入。
- 临时真实解释器正常退出 23、自行 os._exit(29),分别取得对应 3000。
- 只终止本脚本创建且 readiness PID 匹配的临时 Process 对象,取得 3001、已知 PowerShell 发起者和目标/发起者两方 mini dump。
- 临时 probe 配置回滚后,为两个生产映像设置 0x200、ReportingMode=2、IgnoreSelfExits=0、DumpType=0、MaxNumberOfDumpFiles=8;再对两个生产映像执行临时 import/identity 检查,分别确认 3000。这些测试事件不能当作真实事故。
- 任何失败均尝试按每值 journal 回滚并保留错误。拒绝覆盖已有 SPE 设置或已启用 IFEO filter/debugger;不递归删除注册表,不停止真实代理。

日志/转储保留在仅当前用户、SYSTEM、Administrators 可访问的 state 目录;mini dump 仍可能包含敏感信息,不上传公开 PR。数量限制不等于总字节配额;启动监控的 stdout/stderr 也没有字节轮转。

## 已部署与下次启动

本机有效状态:`C:\Users\marcus\.workbuddy2api\silent-exit\20261023-native-runtime-04\result.json`,2026-10-23 21:11:10 UTC+8,status=native_runtime_enabled。01/02 为已回滚失败尝试(缺少 Anaconda zlib 导致进程初始化失败);03 验证成功后已回滚,由含源文件哈希及双生产映像验证的 04 取代。

原 pythonw 观察器保持运行。另有专用观察器 PID 36748 于 21:12:24 启动,只观察,不启动代理,日志在 `C:\Users\marcus\.workbuddy2api\monitor\20261023-211224-5ed62f15`。PID/存活状态应现场核对;没有开机自启或自动重启设置。

下次手动启动(命令保持一致):

```powershell
& 'D:\workspace\workbuddy2api-external-monitor\scripts\start_proxy_with_evidence.ps1'
```

只检查配置,不启动:

```powershell
& 'D:\workspace\workbuddy2api-external-monitor\scripts\start_proxy_with_evidence.ps1' -CheckOnly
```

入口只接受 native_runtime_enabled,拒绝旧 launcher-only 状态;检查哈希及两套 Windows 配置,拒绝已有代理。以无控制台 observer 映像启动独立监控,再启动真实 runtime 映像,不经过 uv 或 venv redirector。入口返回 observer PID、监控目录及启动日志;这只是进程创建确认,健康状态需查看日志。关闭启动终端不会主动执行清理代理,但 Windows Job/会话策略仍可能影响进程。

取证配置供未来启动生效。此次未由维护者启动/停止/重启真实代理,也没有把仍在运行的进程热切换到新解释器。

## 配置与回滚

在 64 位管理员 Windows PowerShell 中,新机器/重新验证使用全新 state 目录:

```powershell
& 'D:\workspace\workbuddy2api-external-monitor\scripts\manage_native_runtime.ps1' -Action ValidateAndEnable -StateDirectory 'C:\Users\marcus\.workbuddy2api\silent-exit\NEW-UNUSED-DIRECTORY'
```

已有本工具配置应先使用其 state 回滚,不覆盖。当前配置回滚:

```powershell
& 'D:\workspace\workbuddy2api-external-monitor\scripts\manage_native_runtime.ps1' -Action Rollback -StateDirectory 'C:\Users\marcus\.workbuddy2api\silent-exit\20261023-native-runtime-04'
```

回滚只恢复设置,保留文件及证据,不停止任何代理/观察器。旧 launcher-only 配置仍作为历史独立 state 保留;需要撤销时用旧 manage_silent_exit.ps1 配合原 state,不能拿新脚本混用旧 journal。

## 判断边界及验证

3000 表示受支持路径上的自行退出;3001 可提供跨进程终止发起者,结合目标创建时间、句柄时间线及双方转储分析。未捕获事件不能排除内核终止、部分 Job 清理、最后线程退出、断电等未覆盖路径。改变映像名称可能改变按名称清理行为,即使不再复现也不能单凭稳定性确认根因。

回归:267 passed、1 skipped、1 deselected;排除项仍是原 Windows POSIX 0600 权限断言。新增测试验证真实 OS 映像/PID、最小 PATH 下依赖解析、无控制台 observer/临时子进程的完整退出记录、参数转义、文件变更拒绝、HKCU 私有测试键配置/回滚与冲突保护。原生管理员事件/转储验收另行完成;未做真实代理启动验收。Review 重点为范围、进程所有权、可回滚性、依赖一致性及证据关联。

参考:[Microsoft Silent Process Exit](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/registry-entries-for-silent-process-exit/)、[Python DLL 搜索目录](https://docs.python.org/3.10/library/os.html#os.add_dll_directory)。
Loading
Loading