From a6ae6c4647e2d05fb686a83e73a5d2657a4504d7 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 26 May 2026 08:58:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=20DESIGN=20=C2=A77.5=20=E6=9C=AB?= =?UTF-8?q?=E5=B0=BE=E6=B2=89=E6=B7=80=20Stage=20C=20=E6=B2=99=E7=9B=92?= =?UTF-8?q?=E5=AE=9E=E6=96=BD=E7=A1=AC=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 §7.5 的原则具化成 6 条硬协议,Stage C 实施时按表对账而非凭记忆: ① 网络 blocklist 硬编码段(cloud metadata 169.254/16 / loopback / 内网三段 / CGNAT,PG IP 单独再 block 一遍 belt-and-suspenders); ② egress proxy 模型(容器 HTTP_PROXY env + iptables DROP except proxy 端口防 SDK 绕 env;宿主侧 proxy 做域名 allowlist + 字节计量 + network_audit 审计日志;allowlist 初始集列出 PyPI/GitHub/npm 等); ③ 进程组清理协议(docker exec 走 setsid + kill -- -PGID,防 nohup & / disown 跨 exec 持久化破"同 user 不内隔离"残留风险假设); ④ 磁盘配额硬化时点(外部开放前必须升 xfs/ext4 project quota 或 zfs dataset quota,否则扫描间隙打满共享 fs 拖死同节点其他 user); ⑤ Executor 接口走 backend driver + ZCBOT_SANDBOX_RUNTIME config 注入 (未来切 gVisor/Firecracker/e2b 应用层零改动,避免 Docker API 形状 泄漏到接口层); ⑥ 工具按信任域二分 dispatch — host in-process: read/write/edit/glob/ grep/load_skill/web_search/web_fetch(原本就在 host 持凭据 / 走 paths.py 校验,塞容器无收益付 200ms × N);container exec: shell/run_python(执行任意代码必隔离)。 同时写死 gVisor / Firecracker-e2b / 容器内 tool-runner 三档升级触发信号 + 不升级的反向兜底理由,防凭审美决策。 PROGRESS:状态表 §7 SaaS 行 + 下一步候选 #2 加锚指向新清单;时间估值 2-3 天 → 3-5 天;补 5 项 Stage C 完成 DoD 红队回归用例(metadata IP timeout / PG IP block / nohup 残留 docker top 不见 / 跨 user 容器互访 阻断 / 非 allowlist 域名 403)。 Co-Authored-By: Claude Opus 4.7 (1M context) --- DESIGN.md | 36 ++++++++++++++++++++++++++++++++++++ PROGRESS.md | 10 +++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ad47c89..5cce430 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -380,6 +380,42 @@ 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 锚定)** — 原则到代码的具化,实施时按此对账,避免靠记忆: + +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 看似已覆盖"实际能直连) + +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 镜像域名。后续按用户反馈逐步加,优于"全开后某天挖矿事件"。 + +3. **进程组清理协议**:`docker exec` 命令通过 `setsid` 包一层(`docker exec setsid ` 或在容器 entrypoint 内封装);timeout / cancel / 正常结束三种路径都走 `kill -- -PGID` 杀整个进程组。**目的**:防 `nohup &` / `disown` / 派生 daemon 在容器内跨 exec 持久化。同 user 不做内隔离 → 残留进程能看到后续 exec 的 in-memory 状态,这是接受的 #1 残留风险,但其前提是没有 stale 进程能跨 exec 存活——这条协议守不住,残留风险就放大成"跨对话持久后门"。 + +4. **磁盘配额硬化时点**:首版用应用层统计 + 周期扫描(对应 §7.5 软配额);**外部用户开放前必须升级到 xfs project quota 或 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`)导致后期重写。 + +6. **工具按信任域二分,Executor 内部 dispatch**: + - **Host in-process backend**:`read` / `write` / `edit` / `glob` / `grep` / `load_skill` / `web_search` / `web_fetch` — 这些工具原本就在 host 持有凭据(Bocha API key)或走 `paths.py::resolve_user_path` 校验(user-rooted 安全边界已存在,`/v1/files` API 复用同一份),塞进容器既无安全收益又付 ~200ms exec overhead × N 次。 + - **Container exec backend**:`shell` / `run_python` — 执行模型生成的任意代码,必须容器隔离。 + - Dispatcher 内部分流,使用方(`AgentLoop`)零感知。**接口形状按"未来若需全部进容器 + 内部 tool-runner"留好**:只换 host backend 实现成 unix socket RPC,接口不动。 + +**升级触发信号(写下来防遗忘,反向兜底:无信号不升级)**: + +| 升级方向 | 触发信号 | 不升级的理由 | +|---|---|---| +| 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 收益;**美学统一性 ≠ 升级理由** | + + ### 7.6 Core 代码改造(按依赖顺序) | # | 项 | 状态 | diff --git a/PROGRESS.md b/PROGRESS.md index 169c1f2..db888ff 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-05-25(dev SPA 前端 CDN 资源本地化) +最后更新:2026-05-26(§7.5 沙盒落地清单写入 DESIGN) --- @@ -15,12 +15,16 @@ | 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 | | 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 | | 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill | -| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工 ✅;D `/v1` JSON API ✅;D' 过渡 auth + dev SPA ✅;单活 run 锁 + cancel ✅;0004 schema 瘦身 ✅;入口归位 ✅;真 OIDC 待;**C(Executor+docker sandbox)待 —— 外部用户开放 hard prereq,完成前仅 dogfood + 信任同事白名单**。 | +| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工 ✅;D `/v1` JSON API ✅;D' 过渡 auth + dev SPA ✅;单活 run 锁 + cancel ✅;0004 schema 瘦身 ✅;入口归位 ✅;真 OIDC 待;**C(Executor+docker sandbox)待 —— 外部用户开放 hard prereq,完成前仅 dogfood + 信任同事白名单;DoD 详 DESIGN §7.5 落地清单 6 条**。 | --- ## 已完成关键能力 +### 2026-05-26 + +- **§7.5 沙盒落地清单 6 条写入 DESIGN(Stage C 实施硬协议)**:Stage C 动手前把"原则 → 具体协议"沉淀,防实施时漏。① 网络 blocklist 硬编码段(`169.254.0.0/16` cloud metadata / loopback / 内网三段 / `100.64.0.0/10` CGNAT,**PG IP 单独再 block 一遍**——Capital One 2019 同款攻击向量);② egress proxy 模型(容器 `HTTP_PROXY` env + iptables DROP except proxy 端口防 SDK 绕 env,宿主侧 proxy 做域名 allowlist + 字节计量 + `network_audit` 审计日志,allowlist 初始集列出 PyPI / GitHub / npm 等);③ 进程组清理协议(`docker exec` 走 `setsid` + `kill -- -PGID`,防 `nohup &` / `disown` 跨 exec 持久化破"同 user 不内隔离"残留风险假设);④ 磁盘配额硬化时点(开外部前必须升 xfs/ext4 project quota 或 zfs dataset quota,否则扫描间隙打满共享 fs 拖死同节点);⑤ Executor 接口走 backend driver + `ZCBOT_SANDBOX_RUNTIME` config 注入(未来切 gVisor/Firecracker/e2b 应用层零改动,避免 Docker API 形状泄漏到接口层);⑥ 工具按信任域二分 dispatch — **host in-process**:`read/write/edit/glob/grep/load_skill/web_search/web_fetch`(原本就在 host 持凭据 / 走 paths.py 校验,塞容器无收益付 200ms × N),**container exec**:`shell/run_python`(执行任意代码必隔离)。同时把 gVisor / Firecracker / 容器内 tool-runner 三档升级触发信号写死,反向兜底"无信号不升级"。否决:(a) 把落地清单同时写进 DESIGN 和 PROGRESS 双 source — 漂移源,PROGRESS 只指针 DESIGN;(b) 在落地清单里写"勾对"验收语气 — DESIGN 写为什么 + 协议形状,验收语气进 PROGRESS 下一步候选 DoD;(c) 立即开始实施 — 设计先沉淀,实施排进下一步候选 #2 单独节奏。`RUN.md` 不动(运行方式无变化,Stage C 还没实施)。 + ### 2026-05-25 - **dev SPA 前端 CDN 资源本地化 + 升级稳定版**:`web/static/dev.html` 顶部 markdown 渲染依赖从 jsDelivr CDN 改成本地 `web/static/vendor/markdown/` 文件,避免内网/跨境访问 CDN 抖动导致 Markdown / XSS sanitizer / 代码高亮不可用。版本按 npm/latest 核对后固定为 `marked@16.2.1`(`marked.umd.js`)、`dompurify@3.2.6`、`highlight.js@11.11.1`(含 `github.min.css`),并新增 `tests/test_static_vendor.py` 标准库 unittest 回归检查:HTML 不再出现 `cdn.jsdelivr.net`,且 4 个本地 vendor 文件存在非空。`DESIGN.md` 不动(无架构变化);`RUN.md` 不动(运行方式无变化)。 @@ -220,7 +224,7 @@ Python 合计 ~3400 行(+ dev.html 1700 静态 + vendor 1MB) ## 下一步候选(性价比排序) 1. **真 OIDC 接入 + CORS 收紧**(~1 天)—— `/v1/auth/login` 内部换 OIDC ID token 校验(路由层 Depends 不动);CORS 改 platform 域名 allowlist。**真发布给真实用户前必做**。 -2. **§7 C Executor + sandbox**(~2-3 天)—— `run_python`/`shell` → `Executor.run(...)`,本地保留 subprocess、SaaS 走 docker;`api_key_env` → `KeyProvider` 运行时注入。多用户在线跑代码前置。 +2. **§7 C Executor + sandbox**(~3-5 天,按 DESIGN §7.5 落地清单 6 条逐项实施)—— `run_python`/`shell` → `Executor.run(...)`,本地保留 subprocess、SaaS 走 docker;`api_key_env` → `KeyProvider` 运行时注入。多用户在线跑代码前置。**Stage C 完成 DoD** = 6 条落地清单全完成 + 红队回归用例通过:① 容器内 `curl http://169.254.169.254/...` → timeout / connection refused;② 容器内 `psql postgresql://...` → IP block(连接失败);③ 容器内 `nohup sleep 1000 &` exec 退出后 `docker top ` 看不到残留进程;④ 跨 user 容器互访(A 容器 `curl http://:*`)→ 网络隔离阻断;⑤ 出网走 proxy 时未在 allowlist 的域名 → 403。原 ~2-3 天估值未含 egress proxy 部署 / xfs project quota 升级 / 红队用例,补回真实工程量。 3. **Phase 6 context 三层压缩**(~1 天)—— 兜底,V4 长上下文一般用不到。 > §7 B + D + D' + 单活 run 锁 + cancel + 0004 schema 瘦身 + 入口归位 主体已完工。剩余:真 OIDC → C(Executor)→ F(deploy / billing)。§7 E CLI 双模式撤;Phase G Jinja2/HTMX 撤(详见 DESIGN §7.9)。