Commit Graph

28 Commits

Author SHA1 Message Date
caoqianming c55d0d11f0 fix(context): 发送期补齐悬空 tool_calls,断中断 run 留下的协议崩 + bump 0.20.2
run 在写入 assistant.tool_calls 之后、tool 结果写库之前被中断(上游流式断连 /
用户取消 / 崩溃),历史里留下一条 tool_calls 后面没有对应 tool 结果的消息;用户
随后继续发言,下一轮原样发给 DeepSeek/OpenAI 即被拒(must be followed by tool
messages),任务卡死在 run_status=error(监控页排查 task 5c5d6d25 实测)。

prepare_messages_with_stats 入口(早返回分支之前)新增 _repair_dangling_tool_calls:
对每条 assistant.tool_calls 扫描紧随其后的 tool 结果,为缺失的 tool_call_id 补占位
tool 消息。纯发送期不改库 → 覆盖所有中断路径 + 存量坏数据自愈,stats 计 repaired_tool_calls。
区别于 06-06/06-12 的 arguments 损坏修复(那治参数投毒,此为结构性悬空)。

新增 4 个单测,context 套件 14 项全过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:08:20 +08:00
caoqianming 81da2f6f55 fix(context): 不压 assistant tool_call 参数,断 run_python 投毒空转
旧 assistant tool_call.arguments(>800 字符)被压成 {"_compacted":...} marker 发给
LLM,模型在长 doc/ppt 任务里反复看到后仿写它当真实参数 → run_python 拿不到
code/script_path 报错空转(DB 实测最近 60 个 task 命中 83 次,其中 61 次是模型仿写
marker)。把原本只给 task_progress 的豁免升级成通用规则:删 _compact_assistant_tool_calls
/ _compact_tool_call_arguments,只压 tool 结果 + skill,assistant 参数一律原样保留。

附诊断脚本 scripts/diag_run_python_empty.py / diag_run_python_trace.py;全量 120 tests OK。
bump 0.10.0 -> 0.10.1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:41:54 +08:00
caoqianming 958678aa12 feat(skills): 用户私有 skill(.skills)+ 创作工具 + skill-creator + Web 查看页
每用户可在私有 .skills/ 下造/改 skill,只对自己生效。

- SkillRegistry 改多来源(SkillSource 列表:内置 + 用户 .skills),后扫同名
  覆盖先扫 → user wins;user_overrides 记覆盖关系、discovery 显式标注;
  Skill 加 source;from_dir 区分"非 skill 目录(静默)"与"格式错(SkillLoadError)",
  坏的用户 skill 收进 load_errors 注入 prompt,不崩整次扫描。容器路径改写下沉
  到 registry.container_dir(按 source 给 /sandbox/skills 或 /workspace/.skills),
  LoadSkillTool 去掉 container_skills_dir 参数。
- 新增 host-side 工具 save_skill / fork_skill(tools/skill_authoring.py):
  fs 的 base_dir 锚 cwd/容器 wd 够不到 user_root/.skills,故用 host-side typed
  tool(与 seedream/document_* 同范式)。save_skill 写时校验 frontmatter;
  fork_skill copytree 整目录(带脚本)+ 自动对齐 frontmatter name。
- 新增 skill-creator 引导 skill(重点教写好 description + fork 语义)。
- Web:左侧 rail 底部「技能」按钮 → modal 分平台/我的两组,点开看完整
  SKILL.md,我的可删;后端加 GET /v1/skills/{name}(正文)+ DELETE
  /v1/skills/{name}(只删 user 源 + 防穿越);/v1/skills 带 source/overrides/
  load_errors;新 web/static/js/skills.js。创建/改/fork 仍走对话。
- .skills 是 dotfile(文件面板隐藏,与 .memory 一致;validate_task_name 已禁
  . 起头 working_dir,天然不撞)。
- 测试:test_user_skills.py(20 例)+ 改写 test_load_skill.py;全 121 过。
- 文档:DESIGN §3.5 / PROGRESS / RUN(布局+端点)/ SKILL_LIST 同步。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:46:39 +08:00
caoqianming 8b6e66b006 feat(context): 压缩加"上下文压力门槛",短任务不压缩以护缓存+保信息
prepare_messages_with_stats 加 compact_threshold_chars:总 chars 未超阈值
则完全跳过压缩、原样发 —— 短任务 prompt 前缀逐轮字节一致、DeepSeek 前缀
缓存全程命中,且不白丢旧 tool 细节(context 预算还很空时无谓压缩=纯损失)。
超阈值才走原压缩逻辑。

- loop 按 caps.reliable_context × 0.5 × 2.5(char/token 粗折算)算阈值
  (flash ≈ 33 万 chars),_COMPACT_CONTEXT_RATIO/_CHARS_PER_TOKEN 可调
- compaction_skipped 进 stats / llm_start 事件可观测
- 默认 compact_threshold_chars=0 = 永远压缩(向后兼容)

背景:实测 task b27466a0 DeepSeek 缓存命中已 92-94%、滑动边界损失有限
(压缩函数确定性、旧消息压缩态稳定),故只补门槛、暂不改边界为阶梯式。
新增 2 测试(below/above 门槛),全量 105 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:42:18 +08:00
caoqianming 0df9e5fe3f feat(loop): 停机判据从"步数"解耦为"是否在推进"
max_iterations 降级为纯安全 backstop(flash 50→120 / pro 100→150),
不再当"轮预算"砍正经长任务;真正的空转防护改用进展信号:

- _RepeatGuard.record 多返 productive(净产出=非[Error]且非一字不差重复)
- _execute_tool_call 三个返回点都带 productive
- run loop 全局 _stall:整步全无净产出+1、任一净产出清零,
  连续 _STALL_LIMIT=8 步主动停([stopped: no progress]),
  比撞 backstop 早得多掐死循环,配逐指纹 HARD=4 双保险
- 撞 backstop / 空转停都 emit"回复继续可续跑"提示,不再静默停

诊断依据:task b27466a0"中途断了"实为撞 max_iterations=50 后干净停、
用户离开 25min 回来打"继续"续完(非崩溃);"步骤太长"=DeepSeek API
延迟 126-185s 而非工具(全<13s)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:22:02 +08:00
caoqianming b70b993257 feat(preview): pptx 在线预览 —— LibreOffice→PDF + 复用 PDF iframe(DESIGN §8.3 Stage 1)
文件区点 .pptx 不再只能下载。后端转 PDF,前端复用现成 PDF iframe。

- web/pptx_render.py: pptx_to_pdf() 调 soffice,独立临时 profile 绕单 profile
  锁、60s 超时 kill;缓存 .preview/<stem>.<hash>.pdf(hash=mtime+size,源改即
  失效,prune 旧 hash);soffice 缺失抛 SofficeNotFoundError
- web/app.py: GET /v1/files/preview_pdf —— _safe_join 防穿越 + 仅 .ppt(x) +
  per-path asyncio.Lock 防并发重转 + run_in_executor 不堵事件循环;缺失 501/失败 500
- preview.js: ppt 组 + main/mini 共用 _showPptAsPdf(spinner loading + 失败回退下载)
- dev.html: .preview-spinner(复用 @keyframes spin)
- 转换跑 web host 进程不进沙盒;部署 host 装 libreoffice-impress + fonts-noto-cjk
  (sandbox Dockerfile 不动)
- tests/test_pptx_render.py: 10 例(缓存命中跳 soffice/源变失效+prune/缺失降级/越界拒绝)
- 文档:RUN.md(host 装 + 故障兜底 2 行)、PROGRESS.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:04:02 +08:00
caoqianming 087980d027 test(prompt): 锁住 docker 下 system prompt 不漏宿主路径(药3 复核收口)
高轮数烧 token 诊断的最后一味:复核「不停 glob /home/ubuntu/zcbot」幽灵路径。
证据链确认是 2026-06-03「system prompt 焊死宿主路径」那个 bug 的历史残影——
失败 task(ab063233 06-02 / ff1686b7 06-03)首条 assistant tool_calls 即带宿主
路径+真 uid(只可能来自当时运行时拼接的 system prompt;messages 表无 system-role
行),两者均建于修复前后。fs 工具在 docker 容器跑、容器无此宿主路径 → base
path not found → 重试风暴(实测 51 次)。

复核当前代码:docker 模式即便传 tool_base=/home/ubuntu/zcbot + 真 uid,prompt
只含 /workspace/<wd>、不漏宿主路径/uid/tmp(agent_builder.py docker 分支)。
新增 tests/test_system_prompt_paths.py(2 例)钉死防回归。三味药全部收口;
药1 重复守卫此后兜底同类风暴。无功能代码改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:30:32 +08:00
caoqianming 8150bf0b83 feat(tools+loop): 批量抓取 + 重复调用守卫 —— 治高轮数烧 token
DB 实测高轮数 task 的浪费来自三股根因(空 {} 风暴 / 报错重试 /
检索不收敛)叠加,且 loop 对重复调用零防护。本轮两味药:

药2 检索/抓取类 host 工具批量化(从工具形态减往返):
- web_fetch: url -> urls(1-10 并发,总预算 16000 按条分摊,单条失败不连坐)
- document_search: query -> queries(1-8 并发,批内去重,批量自动缩量防爆 context)
- document_download: file_name+kb_name -> items(1-10 并发,单条失败标 [Error] 不毁整批)
按「开发期不写兼容层」直接换签名、不留单数别名;skills/documents/SKILL.md 同步。

药1 loop 病理性重复守卫(core/loop.py::_RepeatGuard):
- 按 (工具名, 精确参数) 指纹跟踪「无产出重复」
- 只惩罚无产出(结果为 [Error] 或与之前一字不差);结果每次不同=有产出、清零,
  绝不误伤正常迭代(改脚本重跑 / 修 bug 重跑构建)
- SOFT=2 注入软提示;HARD=4 拦截不执行,逼模型换路
- 顺带堵 _malformed_tool_calls 漏空 {} 的洞(空 {} 每次返同句缺参错 -> 走 dup 被拦)

测试:tests/test_loop_repeat_guard.py(7 例)+ test_secret_host_tools.py 改新形态
并加批量/去重/失败隔离 3 例;相邻 24 测试全过。
诊断脚本留 scripts/diag_*.py 供复跑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:47:56 +08:00
caoqianming 824f746571 fix(progress): 停压 task_progress 参数修进度还原 + 进度区移到对话区顶部
问题1(进度不对): 上下文压缩把旧 task_progress tool_call 参数换成
{"_compacted":true,"step_id":"sX"} 这种像合法调用的标记, 既毒化模型让它
照抄出残废 update_step(丢 step.status)入库, 又让前端 applyProgressAction
读不到 args.step → 步骤永停 pending。修复: task_progress 参数一律不压缩。

问题2(没像 codex 在顶部): 删掉每条消息卡内联进度块, 进度统一只在对话区
顶部单一 dock 实时显示(钉顶不滚); 全部完成时折叠成一行摘要。prompt/tool
描述改为跑完标 completed 而非 clear, 留住全绿收尾。

校验: unittest test_context_compaction/test_task_progress_tool 12 过;
node --test frontend_task_progress 2 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:52:44 +08:00
caoqianming c898ff863d Disable static asset caching 2026-06-08 09:16:31 +08:00
caoqianming 4ee09976ee Show task progress above composer 2026-06-08 09:04:43 +08:00
caoqianming 8616ba2b56 Add task progress tool 2026-06-08 08:44:16 +08:00
caoqianming cbb16b896f refactor(dev): 前端模块化 Step 2(起)— 抽出 layout.js
三栏布局(pane 折叠 rail + 拖拽 splitter + 手机单列视图)是 main.js 里
唯一对其他功能节零出边的干净段,用它打样增量剥离。

- layout.js(121 行):import $ + 4 个 LS_*_COLLAPSED/WIDTH,只导出
  mqPhone / setMobileView(后者供 selectTask 在手机宽下选中任务自动切
  对话面板,是唯一跨模块调用)。折叠/splitter/mobile-tab 顶层事件绑定
  原样保留(ES module 默认 defer,import 时 DOM 已就绪)。
- main.js:删 114 行 → 2606 行,加 layout import 并清掉随之不再用的
  4 个 LS_* import。逻辑零改动,纯剪切 + 连线;node --check 过,
  main 残留 layout 私有符号清零。

顺手修 Step 1 遗留测试失败:test_static_vendor 第二用例原只 grep
dev.html 找 formatContextStats / context_original_chars / cache_hit_tokens,
模块化后这些搬进 js/*.js → 改为扫 dev.html + js/*.js 合并源。2 测试全过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:27:51 +08:00
caoqianming af2ad3cef1 feat(usage): 记账给前缀缓存命中折价 + 前端体现缓存命中/真实成本
排查"rust→PPT"task(flash,34 轮)发现累计 tokens_in 69.9 万里 88.6% 是缓存
命中,但 _fallback_chat_cost_cny 把命中段也按 input 全价算,记账虚高 2-3x。

- capabilities: 加 cache_hit_cny_per_mtoken(deepseek flash 0.1 / pro 0.2;
  0=不区分按全价兜底,绝不少记)
- usage: 成本公式拆三段「命中×缓存价 + (input−命中)×input价 + output×output价」;
  loop 把 cache_hit_tokens + 缓存单价透传进 record_chat_usage
- web: 不加 DB 列。app.py 加 _usage_aggregates(单查询 GROUP BY usage_events,
  复用列表 msg_counts 批量范式,无 N+1)on-the-fly 算每 task 真实成本 + 缓存命中,
  _task_dict 带出;dev.html 列表行显 ¥、顶栏 formatTaskUsage 显「tok·缓存命中%·¥」
- scripts: backfill_chat_cost_cache_discount.py 按 units 已存 token 重算历史
  cost_cny(只改成本列,默认 dry-run,--apply 落库)

折价只对新 chat 事件即时生效;历史走 backfill 脚本(部署后跑)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:26:09 +08:00
caoqianming 5f8b157733 perf(context): 压缩旧 assistant tool_call arguments + keep_recent 20→12
旧 assistant `tool_calls[].function.arguments` 超 ~800 chars 时压成合法 JSON
标记(保留 path/script_path/name/original_chars),避免 `write(content=...)`
源码参数反复进 prompt;keep_recent 20→12 收窄原文窗口。role/tool_call_id/
name 等协议字段不变,tool_call 协议完整。stats 增 compacted_tool_call_arguments。
DESIGN §8.2 退出标准补一条:列表 N 条/N tok 是历史累计、不随发送前压缩下降。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:25:56 +08:00
caoqianming 1c30a9e54e Reduce chat context token usage 2026-06-04 16:41:14 +08:00
caoqianming 42755e246e fix(sandbox): shell 也注入 PYTHONPATH + HOME=/tmp(修只读 rootfs 两副作用)
容器 --read-only rootfs 下两个副作用:
- PYTHONPATH 原先只 run_python 注入,shell `python -c "from skills..."` 撞
  ModuleNotFoundError
- /home/zcbot 不可写,matplotlib/fontconfig 往 ~/.config / ~/.cache 写缓存刷
  "Read-only file system" / "No writable cache" 噪音

抽 _CONTAINER_ENV = {PYTHONPATH, HOME=/tmp},shell/run_python/fs 三路共用
(-e 确定性覆盖)。HOME=/tmp 一刀让缓存落 tmpfs(matplotlib→/tmp/.config、
fontconfig→/tmp/.cache),免逐个 MPLCONFIGDIR/XDG_CACHE_HOME。纯代码改,
重启 web 生效,免重建镜像。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:12:53 +08:00
caoqianming e62d4fce4a fix(tools): MP 工具改服务端限量查询,止血 IP 被封根因
mp_search_summary 之前不传分页参数,mp-api 默认 chunk_size=1000 且
list(docs) 自动翻完所有页,limit 只做客户端切片 —— 每次搜索都整库级下载,
被 MP 判 abusive traffic 封 host IP/ASN。改为 search(num_chunks=1,
chunk_size=limit, ...) 服务端单页限量。mp_get_entries 的 limit 同为"只裁剪
保存、不减流量"的假参数,但 get_entries_in_chemsys 天然全量(相图用途)无法
限量,只在 description 点明开销。测试加断言锁定 num_chunks/chunk_size 已传。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:11:16 +08:00
caoqianming 68ce996ad2 feat(tools): documents/pymatgen secret-bearing 能力改 host-side tools,key 不进 sandbox
新增 tools/documents.py(document_list_kb/search/download)和 tools/materials_project.py
(mp_search_summary/get_structure/get_entries),key 只在宿主读取,sandbox/run_python 拿不到。
agent_builder 仅在对应 env 存在时注册。删 skills/pymatgen/materials.py::mp_rester() 旧入口,
smoke 改走 host tool。同步 DESIGN §6.7 secret-bearing 规则 + RUN/SKILL_LIST/两个 SKILL.md。

实测:MP step D 真连 api.materialsproject.org 返 403(工具行为正确,干净透传 [Error]),
疑似 .env 里 legacy key 在新版 mp-api 失效,待换 next-gen key 再验。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:35:10 +08:00
caoqianming aab1da3296 fix(executor_docker): 删 setsid 修 docker exec 延迟 stdout 丢失
实证:`docker exec ... setsid python -c "sleep(2); print"` 等满 2s 输出空,
同条件去 setsid 输出 hello。setsid 调 setsid() syscall 后 docker exec/runc
的 stdio attach 出问题,延迟输出被截。上一条 _run_subprocess 重写修了独立的
poll-loop bug 但不是用户当下症状元凶。setsid 历史是给 §7.5 Step 3b PGID kill
协议铺路,该协议未实现的当下是空头载荷 + 副作用。改 _exec_shell:141 / _exec_python:177
各删 1 个 "setsid"。回归测试加 test_run_subprocess_delayed_output_not_lost
(真子进程 sleep+print)+ test_argv_does_not_contain_setsid(防回潮)。19/19 PASS。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:02:17 +08:00
caoqianming 91cc14278c fix(executor_docker): 重写 _run_subprocess 修 docker exec stdout 多 chunk 静默丢失
旧实现在 poll loop 里反复 communicate(timeout=0.5) 违反 subprocess API 假设,
配合 setsid bash -c block-buffered stdout 在多 chunk 输出时 chunk 静默丢失,
LLM 调 run_python / shell 拿到空 [exit 0] 8 字符,误判 paper_server 不可用。
改单次 communicate(timeout=full) + 侧线程 daemon poll cancel + 入口 inline
快路径。回归测试用真子进程 bash sleep 多次 echo 锁死,17/17 PASS。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 10:26:59 +08:00
caoqianming 4b7d7e6f77 fix(skill_tool): docker backend 下返回容器路径而非 host 绝对路径
实测部署 dogfood analyze skill 时,LLM load_skill 拿到 host 绝对路径
`/home/lighthouse/zcbot/skills/analyze`,照 SKILL.md 拼 references 路径调 read
→ 容器内 namespace 没这条路径(容器只有 /sandbox/skills:ro 这 mount),抓瞎。

修法:LoadSkillTool 加 container_skills_dir 构造参数;agent_builder 在装它时
判 ZCBOT_SANDBOX_BACKEND==docker → 传 "/sandbox/skills",其它 → 保持原 host
绝对路径(开发期 host backend 不破)。

结构性收益:proposal/ppt/research/coding/pymatgen/stats_ml/plot_pub 全部 skill
references 在 docker backend 下自动 work,不用一个个改 SKILL.md 教用容器路径。

tests/test_load_skill.py 4 case 锁:host 走 host 路径 / docker 走 /sandbox /
末尾斜杠拼接不双斜杠 / 未知 skill 走原路径。docker executor 15/15 回归 PASS。

部署后:git pull + 重启 agent 进程让新代码生效(SkillRegistry 每请求重建但
LoadSkillTool 实例化在 build_agent 里,需要新进程)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 13:37:19 +08:00
caoqianming 85415470d2 sandbox+infra: Python 3.10→3.12(host+Dockerfile)+ docker PYTHONPATH 加 /sandbox
为通过 3 个科学 skill 的 smoke 解决两件基础设施问题。

1. emmet-core 0.86.0rc1 用了 typing.NotRequired(3.11+),host .venv 是 3.10
   → mp_rester ImportError。选 3.12(当下 ML 生态默认稳定版,比 3.11 多一年
   优化,比 3.13 wheel 覆盖更全 Windows 不踩坑)。Dockerfile python:3.11
   → 3.12 同步升,部署机 rebuild image 时生效。

2. executor_docker.py:172 PYTHONPATH 由 /workspace 改 /sandbox:/workspace,
   修历史 bug —— skills/ bind mount 到容器 /sandbox/skills:ro,SKILL.md 教
   LLM `from skills.xxx import yyy`,docker backend 之前根本 import 不到
   (research/paper 同款受影响,只是 dogfood 一直跑 host backend 没暴露)。
   test_executor_docker.py:243 regression 测试改为 assertIn 含 /sandbox,
   15/15 PASS。

smoke 验证:pymatgen XRD / sklearn / statsmodels / plot_pub 全通,
mp_rester 联网遇 MP 服务侧 IP/ASN 403(LBNL 对国内 IP 临时封,非代码问题)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 11:35:02 +08:00
caoqianming eaf7f3ea1e Stage C 收尾包:资源 yaml + 磁盘配额 + 网络放开 + 容器内源持久化
dogfood + 信任同事白名单阶段 Step 4 完整 egress proxy 暂不做(沉淀为升级触发
信号:任一陌生用户注册 / 模型异常 outbound / 信任白名单出现非密切相识者 → 必上)。
本批 3 件:

(A) 容器资源 yaml 化(可调不重 build):
- agent.yaml 加 sandbox 段(memory/cpus/pids_limit)
- SandboxPool ctor 加三字段,优先级 env > yaml > 默(2g/1.0/256)
- setup_pool/init_pool 透传 sandbox_cfg
- sandbox check 输出加 [info] 4 行给运维一眼对账

(B) 应用层磁盘配额(§7.5 #4 软配额):
- migration 0008 user_disk_usage 单行 per user
- core/storage/disk_quota.py:parse_bytes("5gb"/int)+ scan_user_dir
  (os.scandir 跳顶层 .zcbot_tmp / .memory)+ upsert ON CONFLICT
  + check_disk_quota + scan_all_users 串行
- lifespan _disk_scanner 后台 task(启动跑一次 + 默 15min 周期)
- DockerExecutor write/edit 起手 gate 超额 [Error] 不调容器
- /v1/files/upload 同款 gate 超额 HTTP 413
- yaml `quotas.disk_bytes_per_user: 5gb` + `disk_scan_interval_seconds: 900`
- race 接受:扫描间隙写入轻微突破(image/video 配额同款 race-tolerant);
  外部用户开放前 OS 层 xfs prjquota 兜底
- 11 测试 covered parse_bytes / scan / 跳 dotfile

(C) 网络放开 + 容器内源持久化:
- network.py 去 --internal flag,容器走 docker bridge default 有 NAT outbound
- 已存在 internal network 不自动 rm 仅 warn,RUN.md 给迁移命令(避免破现有容器)
- iptables 红线段不动(169.254/127/10/172.16/192.168/100.64/PG_IP DROP),
  挡 cloud metadata + 内网扫描 + loopback,基线不依赖 proxy
- Dockerfile 加 /etc/pip.conf(global index-url + timeout 60) + /etc/npmrc
  (global registry),让运行时模型 `pip install foo` / `npm install bar`
  也走 mirror(此前 --build-arg 只 build 时生效)

unittest discover 46/46 PASS(原 35 + 新 11)。
DESIGN 不动(延后决策仍在 §7.7 Stage C 阶段语义内,触发信号沉淀进
PROGRESS / RUN);RUN.md 加 env 列表 + 网络迁移 + 配额 + 故障兜底 3 行。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:35:53 +08:00
caoqianming 23ff996d38 Stage C Step 3d: fs 工具进容器 + DESIGN §7.5 #6 重写(物理边界替代代码护栏)
Ubuntu dogfood 暴露 host 工具漏底:base_dir=Path.cwd() 无 user_root 校验,
模型 glob "*" 列出 host /home/lighthouse/zcbot/.git/.venv/... zcbot 源码自身。
DESIGN §7.5 #6 原写"host 工具走 paths.py::resolve_user_path 校验"是假命题
(代码里没那函数),绝对路径完全不挡。

修法:fs 工具(read/write/edit/glob/grep)也走 docker exec,物理边界替代
代码护栏(Phase B path validator 那条不做 ── 脆弱)。

- core/sandbox/tool_runner.py 新增:容器内 helper,stdin 接 JSON args,
  调 tools/fs.py 的 Tool 子类;base_dir=cwd,user_root=/workspace
- DockerExecutor 加 FS_TOOLS 信任域 + _exec_fs_tool:docker exec -i ...
  python /sandbox/tool_runner.py <name>,stdin 喂 JSON args(CJK / 引号
  透明传不被 shell metachar 切)
- _run_subprocess 加 stdin 参数 + is_fs_tool 分支返 stdout 直透(原 Tool
  返回串语义保持),exit≠0 stderr 当 ToolResult content
- SandboxPool 加 repo_root 字段 + <repo>/skills:/sandbox/skills:ro mount
  让容器内 read SKILL references 能解析
- Dockerfile COPY tools/ /sandbox/tools/ + tool_runner.py(build-time COPY
  而非 mount ── 容器内代码不应跟随 host repo 改动)
- web/app.py 透传 ROOT 给 init_pool
- 留 host 的工具:load_skill(SkillRegistry 内存查找)/ web_search /
  web_fetch / seedream / seedance(持 Bocha/ARK key 不入容器)
- DESIGN §7.5 #6 重写:"几乎所有工具进容器,host 只留持 key + 跨 user 的",
  原假命题溯源标注 2026-05-26 修正

代价:每 fs tool call +~200ms docker exec overhead,对话级 N≤15 总 1-3s,
LLM 推理 5-30s 下噪声。升级触发(§7.9 升级表)docker exec → unix socket RPC
仍按原信号(overhead/total > 30% 持续 / 长驻服务工作流)。

测试:test_executor_docker 加 4 fs 路径测试(argv 形态 / CJK stdin JSON /
exit≠0 stderr 透传 / timeout);改原 read 直通测试 → load_skill 直通
(read 现在进容器)。unittest discover 35/35 PASS。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:56:41 +08:00
caoqianming 1a950dedb5 Stage C Step 5: main.py sandbox check + lifespan fs quota WARN
- main.py sandbox check 子命令:5 项独立探测 + 汇总 exit code
  ① docker daemon 可达 ② zcbot-sandbox:latest 镜像存在
  ③ zcbot-sandbox-net network 存在(warn 不 err) ④ 镜像 zcbot uid 与 host
  uid 对齐 ⑤ workspace/users 所在 fs 类型可 quota
- core/sandbox/check.py:detect_fs_quota(path) -> (level, msg) 抽出来给
  lifespan 与 CLI 共用;识别 xfs+prjquota/ext4+project/zfs/btrfs/tmpfs/其他
- web/app.py lifespan docker backend 启用时调 detect_fs_quota 打 WARN
  到 stdout(不阻塞启动,应用层周期扫描仍生效)
- err vs warn 分界:err = docker backend fail-fast 根因(daemon/镜像/uid),
  warn = 不阻塞启动但外部开放前要清(network 缺/fs 不可 quota)
- run_sandbox_check 用 module-level getattr 而非固化 CHECKS 元组,让
  unittest patch core.sandbox.check.check_xxx 生效
- tests/test_sandbox_check.py 19 测试覆盖各分支 + exit code 汇总;
  unittest discover 31/31 PASS
- RUN.md 加"部署前置对账"小节 + "配额硬化"重写(fs 状态→处理映射表 +
  xfs 升级 4 步) + 故障兜底 3 行;DESIGN 不动

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:41:16 +08:00
caoqianming dfac0acfa6 Stage C Step 3: DockerExecutor 集成 AgentLoop + web lifespan reaper
- core/executor_docker.py 新增 DockerExecutor:组合 HostExecutor+SandboxPool,
  shell/run_python 走 docker exec(setsid + --user 1000:1000 + --workdir),
  其他工具直通 host(§7.5 #6 信任域二分)
- run_python tmp .py 落 <user_root>/.zcbot_tmp/<task_id>/(dotfile,/v1/files
  天然过滤),容器内对应 /workspace/.zcbot_tmp/...,跑完 unlink
- ZCBOT_SANDBOX_BACKEND=host|docker env 切 backend,默 host(Windows dogfood
  零变化);docker 路径 pool 未 init → fail-fast 不静默退化
- web/app.py lifespan:docker backend 启动时 init_pool + shutdown_all 清孤儿 +
  60s 后台 reaper(run_in_executor 调 sync reap_idle);关闭时 cancel + 兜底清
- pool.py 顺手清 Step 2 债:asyncio.Lock → threading.Lock,ensure 改同步
  (主使用方是 BG 线程 tool call,ephemeral loop 会让 asyncio.Lock 跨锁失效)
- Cancel limitation 接受:Popen.kill() 仅杀 docker CLI 客户端,容器内进程靠
  idle 5min reaper 兜底;升级到 PGID 协议(§7.5 #3)等用户反馈触发
- tests/test_executor_docker.py 11 测试覆盖关键路径(host 直通/argv 形态/
  tmp 清理/timeout/cancel/未知工具/enable_run_python=False)
- DESIGN.md 不动(纯按 §7.5 #5 #6 既有协议实施)
- RUN.md 加 ZCBOT_SANDBOX_BACKEND env 段 + 切 docker 的前置条件 + 集成验证路径
- unittest discover 12/12 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:13:16 +08:00
caoqianming ade7f3d1e1 Vendor markdown frontend assets 2026-05-25 09:31:36 +08:00