feat(sandbox): chromium 秒崩第二根因 bisect 探针(bump 0.58.4)

pids-limit 1024 部署后复测(task 9dcae061,green 实例全新容器):mmdc 仍
首调秒崩 "Failed to launch the browser process: Code: null" —— 排除 pids
打满,第二根因在 hardening flag × chromium 150(trixie)交互。

新 deploy/sandbox/probe_chromium_bisect.sh:线上 docker run 全款 hardening
逐 flag 开关(no-new-privileges / read-only / cap-drop / mem·cpu /
seccomp=unconfined),每 case 跑 mmdc + 裸 chromium 两条,--user zcbot +
HOME=/tmp 忠实复现 exec 路径,尾部带判读表。RUN.md 故障兜底条目同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-11 09:35:10 +08:00
parent 0d7725b010
commit 6c3122eb4b
4 changed files with 68 additions and 3 deletions

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-07-11(sandbox pids-limit 256→1024 修 mermaid 渲染必崩,bump 0.58.2) 最后更新:2026-07-11(chromium 秒崩第二根因 bisect 探针,bump 0.58.4)
--- ---
@ -23,6 +23,7 @@
### 2026-07 ### 2026-07
- **07-11 / 0.58.4**:**mermaid 崩塌第二根因排查工具 `deploy/sandbox/probe_chromium_bisect.sh`**:pids-limit 1024 部署后复测(同 task 9dcae061,green 实例全新容器)仍首调秒崩——排除 pids 打满,根因在 hardening flag × chromium 150 交互。探针把线上 docker run 全款 hardening 逐 flag 开关(no-new-privileges / read-only / cap-drop / mem·cpu / seccomp=unconfined),每 case 跑 mmdc + 裸 chromium 两条,忠实复现 exec 路径(--user zcbot + HOME=/tmp),尾部带判读表;RUN.md 故障兜底条目同步。
- **07-11 / 0.58.3****admin 用量表默认时间范围 all→近7天**`admin.js` 「按模型」「各用户用量」两表初始 range 从"全部"改"近7天"(下拉仍可切全部/30天API 端 `range` 默认值不动。 - **07-11 / 0.58.3****admin 用量表默认时间范围 all→近7天**`admin.js` 「按模型」「各用户用量」两表初始 range 从"全部"改"近7天"(下拉仍可切全部/30天API 端 `range` 默认值不动。
- **07-11 / 0.58.2**:**sandbox `pids_limit` 256→1024,修容器内 mermaid 渲染必崩**(task 9dcae061 排查):DB 全量扫描证实近 90 天 mmdc 渲图 0 成功(26 次 launch fail + 67 次 timeout),根因 `--pids-limit` 把线程也计数——chromium headless 一次启动 ~150-200 线程,叠加 shell 超时只杀 host 侧 docker CLI、容器内 mmdc+chromium 树残留(executor_docker 已知 Cancel limitation),256 打满 → `pthread_create EAGAIN` → chromium SIGTRAP(dbus 报错是干扰项)。改 agent.yaml + pool.py 默认值两处;RUN.md 故障兜底补条目。**待服务器验证**:重启 web + 容器回收后跑 `probe_mermaid.sh`,若 chromium 150(07-08 镜像重建带进)仍秒崩则另查宿主 docker/seccomp;彻底修法是 Step 3b PGID kill 协议(超时清残留),此次未动。附带 4 个 diag 脚本(scripts/diag_task_9dcae061.py 等)未入库。**unifyllm 文本模型再加 2 个进 pro 档**——`gpt55`(gpt-5.5)/`gpt54_mini`(gpt-5.4-mini),参数照 gpt56_sol 模板(temp 硬约束 1.0),mini 档 tool_calling/code_quality 记 good、max_iterations 100;网关 `GET /v1/models` 确认模型 ID + diag 冒烟(tool-calling)两模型均过 + `ModelCapabilities.load` 线上路径验证;`diag_unifyllm.py` 默认清单 5→7 个(RUN.md 同步)。 - **07-11 / 0.58.2**:**sandbox `pids_limit` 256→1024,修容器内 mermaid 渲染必崩**(task 9dcae061 排查):DB 全量扫描证实近 90 天 mmdc 渲图 0 成功(26 次 launch fail + 67 次 timeout),根因 `--pids-limit` 把线程也计数——chromium headless 一次启动 ~150-200 线程,叠加 shell 超时只杀 host 侧 docker CLI、容器内 mmdc+chromium 树残留(executor_docker 已知 Cancel limitation),256 打满 → `pthread_create EAGAIN` → chromium SIGTRAP(dbus 报错是干扰项)。改 agent.yaml + pool.py 默认值两处;RUN.md 故障兜底补条目。**待服务器验证**:重启 web + 容器回收后跑 `probe_mermaid.sh`,若 chromium 150(07-08 镜像重建带进)仍秒崩则另查宿主 docker/seccomp;彻底修法是 Step 3b PGID kill 协议(超时清残留),此次未动。附带 4 个 diag 脚本(scripts/diag_task_9dcae061.py 等)未入库。**unifyllm 文本模型再加 2 个进 pro 档**——`gpt55`(gpt-5.5)/`gpt54_mini`(gpt-5.4-mini),参数照 gpt56_sol 模板(temp 硬约束 1.0),mini 档 tool_calling/code_quality 记 good、max_iterations 100;网关 `GET /v1/models` 确认模型 ID + diag 冒烟(tool-calling)两模型均过 + `ModelCapabilities.load` 线上路径验证;`diag_unifyllm.py` 默认清单 5→7 个(RUN.md 同步)。
- **07-10 / 0.58.0**:**第二图像后端 gpt_image(unifyllm 网关,pro 档)**——媒体 provider 从豆包单家泛化为多家:新 `config/media/unifyllm.yaml`(image 段 `gpt_image` variant,model=gpt-5.6-sol 渠道)+ 新 tool `tools/gpt_image.py`(仅 prompt 参数——网关实测忽略 size/quality,尺寸上游自动定,不支持 i2i;b64 直落 `figures/`,banner 协议同 seedream,价目未知期 cost 记 0、usage tokens 进 units 留对账)。装配泛化:`ArkConfig.load` 支持通用键名 `api_key_env`/`base_url`(ark_* 旧键兜底);`agent_builder._choose_image_variant` 跨 provider 选 image variant(豆包在前 = 默认仍 seedream_5,本 run 只挂选中的一个图像工具),`_MEDIA_TOOLS_BLOCK` 拆分段按实挂工具拼装(`_media_tools_block`,gpt_image 段自带"不支持改图/尺寸→建议切回 Seedream"红线);web `_list_media_variants``config/media/*.yaml`(image/video 清单、admin 门控 catalog 同步泛化,variant key 跨文件唯一、撞名先读生效);`media.js` 三处白名单加 gpt_image(banner/产物 chip/摘要行);prompt 润色 hint 无 default_size 时不再谎报 2048。`gpt_image` 进 pro 档(与 seedream 共享 images_per_day 配额,kind=image 同口径)。手工验证 `scripts/test_gpt_image_manual.py`(variant 选择 4 例 + prompt 块 3 例 + 真调网关落图)全过,unittest 全量 201 过。 - **07-10 / 0.58.0**:**第二图像后端 gpt_image(unifyllm 网关,pro 档)**——媒体 provider 从豆包单家泛化为多家:新 `config/media/unifyllm.yaml`(image 段 `gpt_image` variant,model=gpt-5.6-sol 渠道)+ 新 tool `tools/gpt_image.py`(仅 prompt 参数——网关实测忽略 size/quality,尺寸上游自动定,不支持 i2i;b64 直落 `figures/`,banner 协议同 seedream,价目未知期 cost 记 0、usage tokens 进 units 留对账)。装配泛化:`ArkConfig.load` 支持通用键名 `api_key_env`/`base_url`(ark_* 旧键兜底);`agent_builder._choose_image_variant` 跨 provider 选 image variant(豆包在前 = 默认仍 seedream_5,本 run 只挂选中的一个图像工具),`_MEDIA_TOOLS_BLOCK` 拆分段按实挂工具拼装(`_media_tools_block`,gpt_image 段自带"不支持改图/尺寸→建议切回 Seedream"红线);web `_list_media_variants``config/media/*.yaml`(image/video 清单、admin 门控 catalog 同步泛化,variant key 跨文件唯一、撞名先读生效);`media.js` 三处白名单加 gpt_image(banner/产物 chip/摘要行);prompt 润色 hint 无 default_size 时不再谎报 2048。`gpt_image` 进 pro 档(与 seedream 共享 images_per_day 配额,kind=image 同口径)。手工验证 `scripts/test_gpt_image_manual.py`(variant 选择 4 例 + prompt 块 3 例 + 真调网关落图)全过,unittest 全量 201 过。

2
RUN.md
View File

@ -848,7 +848,7 @@ sudo xfs_quota -x -c "limit -p bhard=10g zcbot_<user_uuid>" /opt
| 容器内 shell 写工作目录报 `Permission denied`(but `sandbox check` ⑤ HOST_UID aligned ok) | DockerExecutor 写死了 `--user 1000:1000` 不会自动跟 build 的 HOST_UID 同步(改 `--user zcbot` 后已修)。仍报错检查镜像内 `docker run --rm --entrypoint id zcbot-sandbox:latest zcbot` 输出 uid 是否 = `id -u $(whoami)` | | 容器内 shell 写工作目录报 `Permission denied`(but `sandbox check` ⑤ HOST_UID aligned ok) | DockerExecutor 写死了 `--user 1000:1000` 不会自动跟 build 的 HOST_UID 同步(改 `--user zcbot` 后已修)。仍报错检查镜像内 `docker run --rm --entrypoint id zcbot-sandbox:latest zcbot` 输出 uid 是否 = `id -u $(whoami)` |
| 容器内 mmdc 渲 mermaid 报 `Failed to launch chromium` / `No usable sandbox` | chromium 在 `--cap-drop=ALL` 下自家 setuid sandbox 起不来,要 `--no-sandbox`。镜像已落 `/sandbox/puppeteer-config.json` + 给 mmdc 套了 wrapper 自动 `-p` 注入 ── **裸调 `mmdc -i x -o y` 就该成**。仍跪:`docker exec ... cat /usr/local/bin/mmdc` 看 wrapper 在不在(老镜像没 rebuild 则没有);或显式 `mmdc -p /sandbox/puppeteer-config.json -i x -o y` | | 容器内 mmdc 渲 mermaid 报 `Failed to launch chromium` / `No usable sandbox` | chromium 在 `--cap-drop=ALL` 下自家 setuid sandbox 起不来,要 `--no-sandbox`。镜像已落 `/sandbox/puppeteer-config.json` + 给 mmdc 套了 wrapper 自动 `-p` 注入 ── **裸调 `mmdc -i x -o y` 就该成**。仍跪:`docker exec ... cat /usr/local/bin/mmdc` 看 wrapper 在不在(老镜像没 rebuild 则没有);或显式 `mmdc -p /sandbox/puppeteer-config.json -i x -o y` |
| 容器内 mmdc 渲图卡到 **timeout** 而非报错 | chromium 默认用 `/dev/shm`,docker 不传 `--shm-size` 时只 64MB → 起不来一直挂。已在 `pool.py``docker run``--shm-size`(默 512m,env `ZCBOT_SANDBOX_SHM_SIZE` / yaml `sandbox.shm_size`)。**已 running 的旧容器不会自动生效**,重启 web + 等 idle 回收(或 `docker rm -f zcbot-sandbox-<uid>`)后新起的才带。实测脚本 `deploy/sandbox/probe_mermaid.sh` | | 容器内 mmdc 渲图卡到 **timeout** 而非报错 | chromium 默认用 `/dev/shm`,docker 不传 `--shm-size` 时只 64MB → 起不来一直挂。已在 `pool.py``docker run``--shm-size`(默 512m,env `ZCBOT_SANDBOX_SHM_SIZE` / yaml `sandbox.shm_size`)。**已 running 的旧容器不会自动生效**,重启 web + 等 idle 回收(或 `docker rm -f zcbot-sandbox-<uid>`)后新起的才带。实测脚本 `deploy/sandbox/probe_mermaid.sh` |
| 容器内 mmdc 报 `Failed to launch the browser process: Code: null`(伴满屏 dbus 报错),wrapper / `-p` 配置都给了照崩;或直接跑 chromium `Trace/breakpoint trap (core dumped)` | **`--pids-limit` 打满**(线程也计数):chromium headless 一次启动 ~150-200 线程,加上 shell 超时残留在容器里的 mmdc+chromium 树(超时只杀 host 侧 docker CLI),256 秒穿 → `pthread_create: Resource temporarily unavailable (11)` → chromium CHECK 崩(SIGTRAP)。2026-07-11 起默认已调 1024(yaml `sandbox.pids_limit` / env `ZCBOT_SANDBOX_PIDS_LIMIT`)。**已 running 的旧容器不生效**,重启 web + 等 idle 回收(或 `docker rm -f zcbot-sandbox-<uid>`)。dbus 报错是干扰项(成功时也打);验证跑 `deploy/sandbox/probe_mermaid.sh`,仍崩则查宿主 docker 版本 / seccomp | | 容器内 mmdc 报 `Failed to launch the browser process: Code: null`(伴满屏 dbus 报错),wrapper / `-p` 配置都给了照崩;或直接跑 chromium `Trace/breakpoint trap (core dumped)` | **`--pids-limit` 打满**(线程也计数):chromium headless 一次启动 ~150-200 线程,加上 shell 超时残留在容器里的 mmdc+chromium 树(超时只杀 host 侧 docker CLI),256 秒穿 → `pthread_create: Resource temporarily unavailable (11)` → chromium CHECK 崩(SIGTRAP)。2026-07-11 起默认已调 1024(yaml `sandbox.pids_limit` / env `ZCBOT_SANDBOX_PIDS_LIMIT`)。**已 running 的旧容器不生效**,重启 web + 等 idle 回收(或 `docker rm -f zcbot-sandbox-<uid>`)。dbus 报错是干扰项(成功时也打);验证跑 `deploy/sandbox/probe_mermaid.sh`。**2026-07-11 实测 1024 后全新容器仍首调秒崩** → 第二根因在 hardening × chromium 150 交互,跑 `deploy/sandbox/probe_chromium_bisect.sh` 逐 flag 定位(脚本尾部带判读表) |
| 模型不渲本地 mmdc、反复试 `mermaid.ink` 等在线渲图服务还失败 | 容器**有外网**(bridge+NAT),但 `mermaid.ink` 等**境外服务易被墙/不稳**,渲图不该依赖出站。docker backend 的 system prompt「运行环境」段(`agent_builder.py` 注入)已写明"渲图走本地 mmdc、别调在线服务";撞到多半是 prompt 没更新 / 跑在 host backend。渲 mermaid 一律 `mmdc -i x.md -o x.png` | | 模型不渲本地 mmdc、反复试 `mermaid.ink` 等在线渲图服务还失败 | 容器**有外网**(bridge+NAT),但 `mermaid.ink` 等**境外服务易被墙/不稳**,渲图不该依赖出站。docker backend 的 system prompt「运行环境」段(`agent_builder.py` 注入)已写明"渲图走本地 mmdc、别调在线服务";撞到多半是 prompt 没更新 / 跑在 host backend。渲 mermaid 一律 `mmdc -i x.md -o x.png` |
| Export 报 "无可导出内容" | task 没 messages(只 system 不算);先发条消息再 export | | Export 报 "无可导出内容" | task 没 messages(只 system 不算);先发条消息再 export |
| `NoSubtaskError: working_dir ... 前缀嵌套` | §7.4 no-subtask:同 user 不允许 working_dir 嵌套(child / parent)。**同项目多对话**用**完全相同**的 working_dir;否则改成 sibling(平级) | | `NoSubtaskError: working_dir ... 前缀嵌套` | §7.4 no-subtask:同 user 不允许 working_dir 嵌套(child / parent)。**同项目多对话**用**完全相同**的 working_dir;否则改成 sibling(平级) |

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.58.3" __version__ = "0.58.4"

View File

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# 二分定位:chromium 150(trixie)在 sandbox hardening 下首调即崩(SIGTRAP,rc=133)。
# 背景:2026-07-11 pids-limit 调 1024 后,全新容器里 mmdc 仍 "Failed to launch the
# browser process: Code: null"(task 9dcae061);dbus 报错是干扰项。本脚本把线上
# docker run 的 hardening flag 逐个关掉,找到翻转成功的那一个。
#
# 用法(服务器 repo 根,需 docker 权限):
# bash deploy/sandbox/probe_chromium_bisect.sh [镜像名] # 默 zcbot-sandbox:latest
# 每个 case 跑两条:mmdc 渲一张最小图(走 wrapper 注入的 puppeteer-config)+ 直接跑
# chromium(看裸 rc:133=SIGTRAP,139=SEGV)。全程忠实复现线上 exec 路径:
# --user zcbot + HOME=/tmp(executor_docker._CONTAINER_ENV 同款)。
set -u
IMG="${1:-zcbot-sandbox:latest}"
TEST_MMDC='printf "flowchart TB\n A-->B\n" > /tmp/d.mmd; timeout 60 mmdc -i /tmp/d.mmd -o /tmp/d.png >/tmp/mmdc.log 2>&1; rc=$?; if [ -s /tmp/d.png ]; then echo "[PASS] mmdc rendered png"; else echo "[FAIL] mmdc rc=$rc"; tail -4 /tmp/mmdc.log; fi'
TEST_CHROME='timeout 30 /usr/lib/chromium/chromium --no-sandbox --headless --disable-gpu --disable-dev-shm-usage --dump-dom about:blank >/tmp/c.log 2>&1; rc=$?; if [ $rc -eq 0 ]; then echo "[PASS] chromium rc=0"; else echo "[FAIL] chromium rc=$rc (133=SIGTRAP 124=timeout)"; grep -iv "dbus\|crashpad" /tmp/c.log | tail -4; fi'
echo "===== host env ====="
docker --version
uname -r
C=$(docker ps --filter "label=zcbot.product=sandbox" --format '{{.Names}}' | head -1)
if [ -n "$C" ]; then
docker inspect "$C" --format "live container $C: pids={{.HostConfig.PidsLimit}} shm={{.HostConfig.ShmSize}} readonly={{.HostConfig.ReadonlyRootfs}} nnp={{.HostConfig.SecurityOpt}}"
else
echo "(no live sandbox container)"
fi
run_case() {
local name="$1"; shift
echo
echo "===== $name ====="
docker run --rm --user zcbot -e HOME=/tmp "$@" --entrypoint bash "$IMG" -c "$TEST_MMDC"
docker run --rm --user zcbot -e HOME=/tmp "$@" --entrypoint bash "$IMG" -c "$TEST_CHROME"
}
# 线上 pool.py 的 hardening 全款(iptables init / network 与本问题无关,不带)
FULL=(--read-only --tmpfs /tmp:exec,size=512m,mode=1777 --shm-size=512m
--cap-drop=ALL --security-opt=no-new-privileges --pids-limit=1024
--memory=2g --cpus=1.0)
run_case "0 baseline 无 hardening(预期 PASS;FAIL=镜像/chromium 本身坏)"
run_case "1 full hardening 线上同款(预期复现 FAIL)" "${FULL[@]}"
run_case "2 full 去 no-new-privileges" \
--read-only --tmpfs /tmp:exec,size=512m,mode=1777 --shm-size=512m \
--cap-drop=ALL --pids-limit=1024 --memory=2g --cpus=1.0
run_case "3 full 去 read-only(tmpfs 保留)" \
--tmpfs /tmp:exec,size=512m,mode=1777 --shm-size=512m \
--cap-drop=ALL --security-opt=no-new-privileges --pids-limit=1024 --memory=2g --cpus=1.0
run_case "4 full 去 cap-drop=ALL" \
--read-only --tmpfs /tmp:exec,size=512m,mode=1777 --shm-size=512m \
--security-opt=no-new-privileges --pids-limit=1024 --memory=2g --cpus=1.0
run_case "5 full 去 memory/cpus 限制" \
--read-only --tmpfs /tmp:exec,size=512m,mode=1777 --shm-size=512m \
--cap-drop=ALL --security-opt=no-new-privileges --pids-limit=1024
run_case "6 full + seccomp=unconfined(只定根因,不作为修法)" "${FULL[@]}" \
--security-opt seccomp=unconfined
echo
echo "===== 判读 ====="
echo "0 FAIL => 镜像里的 chromium 150 本身跑不了,与 hardening 无关:pin 回旧版 chromium 或换 puppeteer 自带 chrome"
echo "1 PASS => docker run 层没问题,差异在 exec 路径/容器运行态:进活容器跑 probe_mermaid.sh 对比"
echo "1 FAIL, 某个 N PASS => 被去掉的那个 flag 就是根因,针对性放宽"
echo "只有 6 PASS => 宿主 docker 默认 seccomp profile 太老,不认 chromium 150 的新 syscall:升级 docker-ce 或给 sandbox 换新版 seccomp profile"
echo "全部 FAIL 含 6 => 再看内核版本(uname -r)与 chromium 150 的兼容性,考虑 pin 旧版"