From 63517308915a560acef646ef4747f47a16ca0b60 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Tue, 19 May 2026 13:31:26 +0800 Subject: [PATCH] docs(review): refresh box architecture review for feat/sandbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync the docs/review/ suite to the current state of the feat/sandbox branch (both LangBot and langbot-plugin-sdk), ~30 commits ahead of the prior review. - box-architecture.md: rewrite for the new box.{backend,runtime,local,e2b} config schema, add E2B backend, 6 native tools (incl. glob/grep), Skill Tool Call activation, shared multi-process MCP container, SkillManager, BoxSkillStore (SDK), 25 actions, 9 error types, heartbeat/reconnect - box-issues.md: move resolved items (reconnect, heartbeat, Windows, nsjail image conflict, frontend monitoring card) into a Resolved section; add new P0 (INIT/backend ordering), P1 (extra_mounts immutability after container creation), P2 (skill_store test gap, integration tests not in CI) - box-session-scope.md: add §0 Implementation Status — Phase 1 shipped, MCP unification landed earlier than originally scoped - box-test-coverage.md: realign file inventory (4,400 -> 6,500 LOC), add 7 new test files including SDK backend_selection/e2b/skill_store - box-tob-analysis.md: connection recovery now满足基本要求; add E2B and backend self-heal to capabilities; tick off Phase 1 reconnect/heartbeat - box-vs-plugin-runtime.md: heartbeat/reconnect/Windows support now aligned with Plugin Runtime; revise remaining gaps (WS auth, shared base class) --- docs/review/box-architecture.md | 493 ++++++++++++++++++--------- docs/review/box-issues.md | 201 +++++------ docs/review/box-session-scope.md | 29 +- docs/review/box-test-coverage.md | 88 +++-- docs/review/box-tob-analysis.md | 16 +- docs/review/box-vs-plugin-runtime.md | 51 ++- 6 files changed, 552 insertions(+), 326 deletions(-) diff --git a/docs/review/box-architecture.md b/docs/review/box-architecture.md index 25c63857..b8406984 100644 --- a/docs/review/box-architecture.md +++ b/docs/review/box-architecture.md @@ -1,141 +1,214 @@ # Box 系统架构深度分析 -> 更新日期: 2026-04-16 +> 更新日期: 2026-05-19 > 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk) -> 相关文档: [问题清单](./box-issues.md) | [Runtime 对比](./box-vs-plugin-runtime.md) | [测试覆盖](./box-test-coverage.md) | [toB 分析](./box-tob-analysis.md) +> 相关文档: [问题清单](./box-issues.md) | [Session 作用域](./box-session-scope.md) | [Runtime 对比](./box-vs-plugin-runtime.md) | [测试覆盖](./box-test-coverage.md) | [toB 分析](./box-tob-analysis.md) --- ## 1. 全局架构 ``` -┌──────────────────────────────────────────────────────────────┐ -│ LangBot 主进程 │ -│ │ -│ LocalAgentRunner ──> ToolManager ──> NativeToolLoader │ -│ │ │ │ │ -│ │ │ exec/read/write/edit │ -│ │ │ │ │ -│ │ ├──> MCPLoader ──> BoxStdioSession │ -│ │ │ │ -│ │ └──> PluginToolLoader │ -│ │ │ -│ BoxService (门面) │ -│ ├─ Profile 管理 (locked 字段) │ -│ ├─ Host mount 校验 (allowed_roots) │ -│ ├─ Workspace quota 检查 │ -│ ├─ 输出截断 (head+tail) │ -│ └─ BoxRuntimeConnector │ -│ └─ ActionRPCBoxClient │ -│ │ Action RPC (stdio 或 WebSocket) │ -└──────────────┼─────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────┐ +│ LangBot 主进程 │ +│ │ +│ LocalAgentRunner ──> ToolManager ──> NativeToolLoader │ +│ │ │ │ │ +│ │ │ exec / read / write / edit │ +│ │ │ glob / grep │ +│ │ │ │ +│ │ ├──> MCPLoader ──> BoxStdioSession │ +│ │ │ (shared 容器, 多 process) │ +│ │ │ │ +│ │ ├──> SkillToolLoader (activate 工具) │ +│ │ │ │ +│ │ ├──> SkillAuthoringToolLoader │ +│ │ │ │ +│ │ └──> PluginToolLoader │ +│ │ │ +│ BoxService (门面) │ +│ ├─ Profile 管理 (locked 字段) │ +│ ├─ Host mount 校验 (allowed_mount_roots) │ +│ ├─ Workspace quota 检查 │ +│ ├─ 输出截断 (head+tail) │ +│ ├─ Session ID 模板解析 (resolve_box_session_id) │ +│ ├─ 技能挂载组装 (build_skill_extra_mounts) │ +│ ├─ 重连循环 (_reconnect_loop, 指数退避) │ +│ └─ BoxRuntimeConnector │ +│ ├─ 心跳 loop (20s ping) │ +│ └─ ActionRPCBoxClient │ +│ │ Action RPC (stdio 或 WebSocket) │ +│ │ +│ SkillManager (skill_mgr) │ +│ └─ 从 Box runtime 拉取 skills, 不可用时回落 data/skills │ +└──────────────────────────────────────────────────────────────────┘ │ ▼ -┌──────────────────────────────────────────────────────────────┐ -│ Box Runtime 进程 (SDK 侧) │ -│ │ -│ BoxServerHandler (Action RPC 处理) │ -│ │ │ -│ BoxRuntime (session 管理/进程生命周期) │ -│ │ │ -│ Backend (启动时选择一个): │ -│ PodmanBackend ─┐ │ -│ DockerBackend ─┤── CLISandboxBackend │ -│ NsjailBackend ─┘ │ -│ │ -│ aiohttp WS Relay (:5410) │ -│ /v1/sessions/{id}/managed-process/ws │ -│ (managed process stdin/stdout 双向中继) │ -└──────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────┐ +│ Box Runtime 进程 (SDK 侧) │ +│ │ +│ BoxServerHandler (Action RPC 处理, INIT 配置注入) │ +│ │ │ +│ BoxRuntime (session 管理 / 进程生命周期 / TTL reaper) │ +│ │ └─ session.managed_processes: dict[pid, _ManagedProcess] +│ │ │ +│ Backend (启动时根据 box.backend 配置选择): │ +│ DockerBackend ──┐ │ +│ PodmanBackend ──┤── CLISandboxBackend │ +│ NsjailBackend ──┘ (本地 CLI 或 fallback 到容器内 CLI) │ +│ E2BBackend (云沙箱, 需要 E2B_API_KEY) │ +│ │ +│ BoxSkillStore │ +│ ├─ list / get / create / update / delete │ +│ ├─ scan_skill_directory / read_skill_file / write_skill_file │ +│ └─ preview_skill_zip / install_skill_zip (zip 或 GitHub) │ +│ │ +│ aiohttp 单端口服务 (默认 :5410): │ +│ /rpc/ws — Action RPC │ +│ /v1/sessions/{id}/managed-process/ws — 默认 process │ +│ /v1/sessions/{id}/managed-process/{pid}/ws — 指定 process │ +└──────────────────────────────────────────────────────────────────┘ │ ▼ -┌──────────────────────────────────────────────────────────────┐ -│ 容器/沙箱 (Podman/Docker container 或 nsjail sandbox) │ -│ - 隔离文件系统、网络、PID 命名空间 │ -│ - 资源限制 (CPU, 内存, PID 数) │ -│ - exec: 用户命令在此执行 │ -│ - managed process: MCP Server 等长驻进程在此运行 │ -└──────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────┐ +│ 容器 / 沙箱 (Docker/Podman 容器, nsjail sandbox, 或 E2B 远程沙箱) │ +│ - 隔离文件系统 / 网络 / PID 命名空间 │ +│ - 资源限制 (CPU, 内存, PID 数, 可选 workspace 配额) │ +│ - 主挂载 (host_path → mount_path) + 任意条 extra_mounts │ +│ └─ Skills 通过 extra_mounts 挂在 /workspace/.skills/ │ +│ - exec: 用户命令在此执行 │ +│ - managed process: 多个长驻进程并存 (MCP Server / 自定义服务) │ +└──────────────────────────────────────────────────────────────────┘ ``` -**核心设计原则**: Box Runtime 作为独立进程运行,通过 Action RPC 与 LangBot 主进程通信。两者复用 SDK 的 IO 层(Handler → Connection → Controller)。 +**核心设计原则**: +- Box Runtime 作为独立进程运行,通过 Action RPC 与 LangBot 主进程通信,两者复用 SDK 的 IO 层(Handler → Connection → Controller) +- 一个 session_id 对应一个容器/沙箱实例。同一 session 内可并存多条 mount 与多个 managed process +- Skill / 默认 exec / MCP Server 共享同一个 session 容器(详见 [box-session-scope.md](./box-session-scope.md)) --- ## 2. LangBot 侧模块 -### 2.1 BoxService (`pkg/box/service.py`, 514 行) +### 2.1 BoxService (`pkg/box/service.py`, 722 行) -应用层门面,协调 Profile、安全校验、配额、连接: +应用层门面,协调 Profile、安全校验、配额、连接、Skill 挂载与 Session 模板: + +主要公开方法(按定义顺序): ``` BoxService - ├─ initialize() - │ ├─ _ensure_default_host_workspace() 创建默认工作目录 - │ └─ connector.initialize() 连接 Box Runtime + ├─ initialize() 连接 Box Runtime + 默认 workspace 准备 + ├─ _on_runtime_disconnect(connector) 触发重连 + ├─ _reconnect_loop(connector) 指数退避重连 + ├─ available (property) 连接状态 │ - ├─ execute_tool(parameters, query) Agent 调用 exec 时的入口 - │ ├─ _build_spec(parameters, query) 合并 Profile + 参数 + locked 覆盖 - │ ├─ _validate_host_mount(spec) 校验 host_path 在 allowed_roots 内 - │ ├─ _enforce_workspace_quota(spec) 前置磁盘配额检查 - │ ├─ client.execute(spec) RPC 调用 - │ ├─ _enforce_workspace_quota(spec) 后置检查(超额则销毁 session) - │ └─ _truncate_output(result) 截断 stdout/stderr + ├─ resolve_box_session_id(query) 从 pipeline 模板解析 session_id + ├─ build_skill_extra_mounts(query) 组装 pipeline-bound skill 的挂载列表 │ - ├─ execute_spec_payload(payload) 内部调用(BoxWorkspaceSession 用) - ├─ create_session(payload) 显式创建 session - ├─ start_managed_process(session_id, payload) 启动长驻进程 - ├─ get_managed_process(session_id) 查询进程状态 - ├─ get_managed_process_websocket_url(sid) 获取 WS attach URL - ├─ get_system_guidance() 返回 LLM 系统提示词 - └─ get_status() / get_recent_errors() 可观测性 + ├─ execute_tool(parameters, query) Agent 调用 exec 时的入口 + │ ├─ _apply_profile / build_spec + │ ├─ _validate_host_mount + │ ├─ _enforce_workspace_quota (phase=pre) + │ ├─ client.execute(spec) + │ ├─ _enforce_workspace_quota (phase=post) + │ └─ _truncate (stdout/stderr) + │ + ├─ execute_spec_payload(spec_payload, ...) 内部入口(其他 loader 调用) + ├─ create_session(spec_payload, ...) 显式创建 session + ├─ start_managed_process(session_id, ...) 启动 managed process + ├─ get_managed_process(session_id, pid) 查询进程状态(pid 默认 'default') + ├─ stop_managed_process(session_id, pid) 单独停止某个 managed process + ├─ get_managed_process_websocket_url(...) 返回 WS attach URL + │ + ├─ list_skills() / get_skill(name) Skill 元数据 + ├─ create_skill / update_skill / delete_skill Skill CRUD + ├─ scan_skill_directory(path) 扫描目录 + ├─ list_skill_files / read_skill_file / write_skill_file + ├─ preview_skill_zip / install_skill_zip zip / GitHub 安装 + │ + ├─ shutdown() / dispose() 清理:RPC SHUTDOWN + 进程终止 + ├─ get_status() / get_sessions() / get_recent_errors() + └─ get_system_guidance() LLM 系统提示 ``` -**Profile 系统**: 4 个内置 Profile(`default`/`offline_readonly`/`network_basic`/`network_extended`),`locked` frozenset 字段不可被 LLM 覆盖。参数合并顺序:Profile defaults → LLM 请求参数 → locked 强制值。 +**Profile 系统**: 4 个内置 Profile(`default` / `offline_readonly` / `network_basic` / `network_extended`),`locked` frozenset 字段不可被 LLM 覆盖。参数合并顺序:Profile defaults → LLM 请求参数 → locked 强制值。 **输出截断**: 默认 4000 字符上限,保留前 60% + 后 40%,中间插入 `[...truncated...]`。 -### 2.2 BoxRuntimeConnector (`pkg/box/connector.py`, 160 行) +**Skill 挂载合并**: `execute_tool()` 调用时,`build_skill_extra_mounts(query)` 会把当前 pipeline-bound 的所有 skill 的 `package_root` 作为 `extra_mounts` 加入 BoxSpec,挂在 `/workspace/.skills/`。LLM 通过 `activate` 工具显式激活某个 skill 后,工具调用才允许引用这个 skill 的虚拟路径。 + +### 2.2 BoxRuntimeConnector (`pkg/box/connector.py`, 357 行) 管理与 Box Runtime 的通信连接: -- **本地 stdio**: Unix/macOS 无特殊配置时,fork `python -m langbot_plugin.box.server --port {port}` 子进程 -- **远程 WebSocket**: Docker / `--standalone-box` / 显式 `runtime_url` 时,连接 `ws://{host}:{port}/rpc/ws`(同一端口,路径区分) -- **Windows**: subprocess + WebSocket(Windows 不支持 async stdio pipe) -- **同步等待**: 使用 `asyncio.Event` + `wait_for(timeout=30s)` 模式确认连接 +- **本地 stdio**: Unix/macOS 默认路径,fork `python -m langbot_plugin.box --port {port}` 子进程 +- **本地 subprocess + WS**: Windows 本地(asyncio ProactorEventLoop 不支持 stdio pipe) +- **远程 WebSocket**: Docker 部署 / `box.runtime.endpoint` 显式配置时,连接 `ws://{host}:{port}/rpc/ws` +- **同步等待**: `asyncio.Event` + `wait_for(timeout=30s)` 模式确认连接 +- **心跳**: `_heartbeat_loop()` 每 20s 调用 `ping()`,失败仅 DEBUG 日志(断开检测靠 connection close) +- **重连**: `runtime_disconnect_callback` 由 BoxService 提供,触发 `_reconnect_loop` +- **INIT 注入**: 连接建立后立即下发当前 `box.*` 配置子树(剔除 `runtime` 私有字段),Runtime 据此初始化 backend -### 2.3 BoxWorkspaceSession (`pkg/box/workspace.py`, 404 行) +> **历史改进**: 2026-04-16 版本本文档曾列 P0 「Box 无心跳 / 无重连」,已修复(commit `2dfd9d5d`、`c6882cf`、`5029d9c` 等)。 -在 BoxService 上的高级抽象,用于 Skill 和 MCP 场景: +### 2.3 BoxWorkspaceSession 工具 (`pkg/box/workspace.py`, 413 行) -- **路径重写**: `host_path → /workspace` 映射 -- **Venv 重写**: `.venv/bin/python → python` -- **Python 环境自举**: 检测 `requirements.txt`/`pyproject.toml`,生成 shell 脚本自动创建 venv 并安装依赖 -- **Session 作用域**: 每个 Skill/MCP Server 有独立 session_id +此文件目前提供两类能力: -### 2.4 policy.py (`pkg/box/policy.py`, 98 行) — 死代码 +1. **路径与命令重写工具函数** — `normalize_host_path` / `rewrite_mounted_path` / `unwrap_venv_path` / `rewrite_venv_command` / `infer_workspace_host_path`,被 MCP loader 与 Skill 路径解析共用。 +2. **`BoxWorkspaceSession`** — 围绕 BoxService 的轻量包装,专供 MCP-in-Box 场景使用(管理一个共享 session 的 session_id、构建挂载 payload、stage host 文件到共享 workspace)。 -三层安全策略设计(SandboxPolicy/ToolPolicy/ElevatedPolicy),但**全项目无任何调用**。详见 [问题清单 #1](./box-issues.md)。 +**变化点**: 早期 Skill exec 会为每个 skill 创建独立 BoxWorkspaceSession(独占 session);当前实现已转为 `extra_mounts` 模式,Skill 不再独占容器,只追加挂载。这部分 wrapping 逻辑已从 native loader 移除。 + +### 2.4 policy.py (`pkg/box/policy.py`, 98 行) — 仍是死代码 + +三层安全策略设计(`SandboxPolicy` / `ToolPolicy` / `ElevatedPolicy`),全项目无任何导入或调用。详见 [问题清单 #1](./box-issues.md)。 + +### 2.5 SkillManager (`pkg/skill/manager.py`, 186 行) + +``` +SkillManager + ├─ initialize() 调用 reload_skills() + ├─ reload_skills() 先从 Box runtime list_skills(), + │ 不可用则回落 data/skills/ 扫描 + ├─ refresh_skill_from_disk() 单 skill 重新加载 + ├─ get_skill_by_name(name) + └─ get_managed_skills_root() 返回 Box 视角的 skills_root 路径 +``` + +skill 元数据通过 `parse_frontmatter` 解析 `SKILL.md` 头部(`name` / `description` / `instructions`),不再做整体扫描的代价(典型 < 50 个)。 + +### 2.6 Skill activation (`pkg/skill/activation.py`, 33 行) + Skill loader 辅助 + +历史上 skill 通过 LLM 在文本中输出 `[ACTIVATE_SKILL:name]` 标记激活;当前已改为 **Tool Call 机制**: + +- `SkillToolLoader` (`pkg/provider/tools/loaders/skill.py`, 157 行) 暴露 `activate` 工具,参数为 skill 名 +- 工具实现调用 `register_activated_skill(query, skill_data)`,将激活态写入 `query.variables['_activated_skills']` +- 这种 KV-cache-friendly 模式对齐 Claude Code 设计;详见 [box-session-scope.md §4.3](./box-session-scope.md) 的 Tool Call 描述 + +`activation.py` 现仅保留对外辅助函数(pipeline 层调用 loader 的 `register_activated_skill`)。 --- ## 3. SDK 侧模块 -### 3.1 BoxRuntime (`box/runtime.py`, 388 行) +### 3.1 BoxRuntime (`box/runtime.py`, 599 行) -核心编排器,管理 session 生命周期: +核心编排器,管理 session 生命周期与 backend 调度: ``` Session 生命周期: - Client EXEC/CREATE_SESSION + Client EXEC / CREATE_SESSION │ ▼ _get_or_create_session(spec) ├─ _reap_expired_sessions_locked() 清理 TTL 过期 session ├─ 已存在? → _assert_session_compatible() → 复用 + ├─ Backend session 失踪? → 重建 (commit c6882cf) └─ 新建? → backend.start_session(spec) → 创建容器 - │ + │ └─ 应用 spec.extra_mounts (多挂载) ▼ execute(spec) ├─ 获取 session lock (每 session 独立) @@ -153,24 +226,31 @@ Session 生命周期: **关键设计**: - 每 session 有独立 `asyncio.Lock`,同一 session 内的命令串行执行 +- 每 session 维护 `managed_processes: dict[process_id, _ManagedProcess]`,支持多个长驻进程并存(MCP / 自定义) - 全局 `_lock` 保护 `_sessions` dict 的读写 -- 兼容性检查:比较 10 个字段(network/image/host_path/mount_path/cpus/memory_mb/pids_limit/read_only_rootfs/host_path_mode/workspace_quota_mb) +- 兼容性检查:比较核心 spec 字段,`image` 字段对不支持自定义镜像的 backend(nsjail/E2B)会跳过 + +**Backend 选择 (`_select_backend`)**: 优先级 +1. 显式 `box.backend` 配置(`docker` / `nsjail` / `e2b`) +2. `local` (默认) → Docker / Podman / nsjail CLI 顺序探测 +3. `get_status` 调用时若当前 backend 不可用,会尝试重新选择 (commit `e5617c7`) ### 3.2 Backend 系统 -#### CLISandboxBackend (`box/backend.py`, 389 行) +#### CLISandboxBackend (`box/backend.py`, 411 行) -Podman/Docker 的公共基类: +Docker / Podman 公共基类: ``` start_session(spec): - 1. validate_sandbox_security(spec) 安全校验 + 1. validate_sandbox_security(spec) 2. docker/podman run -d --rm --name --network none (可选) - --cpus/--memory/--pids-limit 资源限制 - --read-only + --tmpfs /tmp 只读根文件系统 - -v :: 工作区挂载 - sh -lc 'while true; do sleep 3600; done' 保活进程 + --cpus/--memory/--pids-limit + --read-only + --tmpfs /tmp + -v :: 主挂载 + -v ::.. 额外挂载 (extra_mounts) + sh -lc 'while true; do sleep 3600; done' 3. 返回 BoxSessionInfo exec(session, spec): @@ -183,11 +263,13 @@ start_managed_process(session, spec): 返回 asyncio.subprocess.Process (stdin/stdout PIPE) ``` -容器以 idle 进程启动(`while true; do sleep 3600; done`),实际命令通过 `docker exec` 执行。`--rm` 确保容器退出时自动清理。 +容器以 idle 进程启动,实际命令通过 `docker exec` 执行。`--rm` 确保容器退出时自动清理。 + +**Windows 支持**: backend 内对 Windows 路径处理与 subprocess 调用做了适配(commit `120817a`)。 **孤儿清理**: 启动时枚举 `langbot.box=true` 标签的容器,instance_id 不匹配的强制删除。 -#### NsjailBackend (`box/nsjail_backend.py`, 510 行) +#### NsjailBackend (`box/nsjail_backend.py`, 552 行) 轻量级 Linux 沙箱(无容器引擎依赖): @@ -195,52 +277,108 @@ start_managed_process(session, spec): - 挂载宿主 `/usr`/`/lib`/`/bin`/`/sbin` 只读 + 选定 `/etc` 条目 - 每 session 创建独立目录(workspace/tmp/home) - 资源限制: cgroup v2 优先,fallback 到 rlimit -- **无自定义镜像**: 使用宿主 OS,`image` 字段固定为 `'host'` +- **CLI 兼容**: 通过 `shutil.which(self._nsjail_bin)` 检测系统安装版 nsjail;不存在时再尝试容器内 nsjail(commit `686fcc0`、`feed530`) +- **无自定义镜像**: 使用宿主 OS,`image` 字段固定为 `'host'`,兼容性检查跳过 image -**后端选择优先级**: Podman → Docker → nsjail(启动时逐个探测,首个可用的胜出,不做运行时 failover) +#### E2BBackend (`box/e2b_backend.py`, 429 行) -### 3.3 Server (`box/server.py`, 268 行) +云沙箱后端(commit `75b547f` 引入): -单端口 aiohttp 服务(默认 5410),通过路径区分: +- 通过 `e2b` SDK 与 E2B 平台通信 +- 配置:`box.e2b.api_key` / `api_url` / `template` +- 支持 `extra_mounts`(commit `0fea9b1` 同步上传文件) +- 无本地容器引擎依赖,适合无 Docker 的部署或 SaaS 多租户场景 +- 不支持自定义 image 字段,由 template 控制 -1. **Action RPC** (`/rpc/ws`): `BoxServerHandler` 处理 11 种 action(HEALTH/STATUS/EXEC/CREATE_SESSION/...),通过 stdio 或 WS 传输。WS 模式使用 `AiohttpWSConnection` 适配层。 -2. **WS Relay** (`/v1/sessions/{id}/managed-process/ws`): 双向桥接 WebSocket ↔ managed process stdin/stdout +### 3.3 Server (`box/server.py`, 508 行) -端口分配: -- Port N (默认 5410): 所有 WebSocket 端点(Action RPC + managed process relay) +单端口 aiohttp 服务(默认 5410),通过路径区分(commit `8c71ec5` 合并端口): -### 3.4 Client (`box/client.py`, 177 行) +1. **Action RPC** (`/rpc/ws`): `BoxServerHandler` 处理所有 action,包括 `INIT` 配置注入、skill store 操作等 +2. **WS Relay** (`/v1/sessions/{id}/managed-process/ws` 与 `/v1/sessions/{id}/managed-process/{pid}/ws`): 双向桥接 WebSocket ↔ 指定 managed process stdin/stdout + +stdio 模式同样会在 5410 启动 aiohttp,专门承担 managed process attach;Action RPC 走 stdin/stdout。 + +### 3.4 Client (`box/client.py`, 377 行) `ActionRPCBoxClient` 封装 `Handler.call_action()` 调用: -- 每个方法对应一个 RPC action +- 25+ 方法对应 25+ 个 RPC action(exec / session / managed-process / skill / status / shutdown) - 错误还原: `_translate_action_error()` 通过字符串前缀匹配还原 SDK 侧异常类型 - `execute()` timeout = 300s,其他默认 15s +- `BoxRuntimeClient` 是 ABC,供后续可能的非 RPC 实现复用 -### 3.5 Models (`box/models.py`, 302 行) +包级别 `__init__.py` 显式导出:`BoxRuntimeClient`、`ActionRPCBoxClient`(commit `df9c722`)。 + +### 3.5 Actions (`box/actions.py`, 34 行) + +`LangBotToBoxAction` 枚举共定义 **25 个** action: + +| 类别 | Actions | +|------|---------| +| 控制 | `INIT`、`HEALTH`、`STATUS`、`GET_BACKEND_INFO`、`SHUTDOWN` | +| 执行 | `EXEC` | +| Session | `CREATE_SESSION` / `GET_SESSION` / `GET_SESSIONS` / `DELETE_SESSION` | +| Managed Process | `START_MANAGED_PROCESS` / `GET_MANAGED_PROCESS` / `STOP_MANAGED_PROCESS` | +| Skill | `LIST_SKILLS` / `GET_SKILL` / `CREATE_SKILL` / `UPDATE_SKILL` / `DELETE_SKILL` / `SCAN_SKILL_DIRECTORY` / `LIST_SKILL_FILES` / `READ_SKILL_FILE` / `WRITE_SKILL_FILE` / `PREVIEW_SKILL_ZIP` / `INSTALL_SKILL_ZIP` | + +### 3.6 Models (`box/models.py`, 331 行) 核心数据模型: | 模型 | 用途 | |------|------| -| `BoxSpec` | 执行请求(cmd/workdir/timeout/network/session_id/image/host_path/资源限制) | -| `BoxProfile` | 预设配置 + `locked` frozenset | -| `BoxSessionInfo` | Session 状态(含 backend_name/backend_session_id/created_at/last_used_at) | -| `BoxManagedProcessSpec` | 长驻进程启动参数(command/args/env/cwd) | +| `BoxNetworkMode` | `OFF` / `ON` | +| `BoxExecutionStatus` | `COMPLETED` / `TIMED_OUT` | +| `BoxHostMountMode` | `NONE` / `READ_ONLY` / `READ_WRITE` | +| `BoxManagedProcessStatus` | `RUNNING` / `EXITED` | +| `BoxMountSpec` | 单条挂载(host_path/mount_path/mode)— **新增** | +| `BoxSpec` | 执行请求;新增 `extra_mounts: list[BoxMountSpec]`、`persistent`、`workspace_quota_mb` | +| `BoxProfile` | 4 个内置 Profile + `locked` frozenset | +| `BoxSessionInfo` | Session 状态(含 backend_name/created_at/last_used_at) | +| `BoxManagedProcessSpec` | 长驻进程参数(process_id/command/args/env/cwd) | | `BoxManagedProcessInfo` | 进程状态(status/exit_code/stderr_preview/attached) | | `BoxExecutionResult` | 执行结果(status/exit_code/stdout/stderr/duration_ms) | -`BoxSpec` 校验器: -- `workdir` 默认继承 `mount_path` -- `host_path` 支持 POSIX 和 Windows 路径 -- 设置了 `host_path` 时,`workdir` 必须在 `mount_path` 下 +`BoxSpec` 校验器: `workdir` 默认继承 `mount_path`;`host_path` 支持 POSIX 和 Windows 路径;设置 `host_path` 时 `workdir` 必须在 `mount_path` 下。 -### 3.6 Security (`box/security.py`, 54 行) +### 3.7 BoxSkillStore (`box/skill_store.py`, 647 行) + +新增模块(commit `4ab3502`),把 skill 持久化收归 Box runtime: + +``` +BoxSkillStore + ├─ list_skills() / get_skill(name) + ├─ create_skill(data) / update_skill(name, data) / delete_skill(name) + ├─ scan_skill_directory(path) 扫描目录返回候选 skill 包列表 + ├─ list_skill_files(name, path) 浏览 skill 内文件树 + ├─ read_skill_file(name, path) / write_skill_file(name, path, content) + ├─ preview_skill_zip(zip_bytes, ...) 不落盘预览 zip 内容 + └─ install_skill_zip(zip_bytes, ...) 解压、校验、复制到 skills_root + └─ 支持 source_subdir / target_suffix(commit 1aa043f) +``` + +GitHub 安装路径:HTTP 层(`api/http/service/skill.py`)先 `git clone` 拉取,再走 `install_skill_zip` 或 directory 路径。Skill 文件存放于 `box.local.skills_root`(默认 `skills`,相对 `host_root`),容器内对应 `/workspace/.skills/`。 + +### 3.8 Security (`box/security.py`, 52 行) `validate_sandbox_security()`: 黑名单校验 host_path,阻止挂载 `/etc`/`/proc`/`/sys`/`/dev`/`/root`/`/boot` 及 Docker/Podman socket。 **已知缺陷**: 根路径 `/` 未拦截,用户 home 目录未拦截,是 denylist 而非 allowlist 策略。详见 [问题清单 #5](./box-issues.md)。 +### 3.9 Errors (`box/errors.py`, 33 行) + +| 异常类型 | 含义 | +|----------|------| +| `BoxError` | 基类 | +| `BoxValidationError` | spec/参数校验失败 | +| `BoxBackendUnavailableError` | 无可用 backend | +| `BoxRuntimeUnavailableError` | Runtime 服务不可用 | +| `BoxSessionConflictError` | session 已存在但 spec 不兼容 | +| `BoxSessionNotFoundError` | session 不存在 | +| `BoxManagedProcessConflictError` | session 已有同名 process | +| `BoxManagedProcessNotFoundError` | process 不存在 | + --- ## 4. 工具系统集成 @@ -249,42 +387,52 @@ start_managed_process(session, spec): ``` ToolManager.initialize() - ├─ NativeToolLoader (exec/read/write/edit) + ├─ NativeToolLoader (exec / read / write / edit / glob / grep) ├─ PluginToolLoader (插件工具) ├─ MCPLoader (MCP Server 工具) + ├─ SkillToolLoader (activate 工具 — Tool Call 激活) └─ SkillAuthoringToolLoader (Skill CRUD) -工具调用优先级: native → plugin → mcp → skill_authoring +工具调用优先级: native → plugin → mcp → skill → skill_authoring ``` -### 4.2 Native Tools (`native.py`) +### 4.2 Native Tools (`native.py`, 846 行) | 工具 | 是否在 Box 中执行 | 是否访问宿主文件系统 | |------|:---:|:---:| -| `exec` | 是 | 否 | -| `read` | **否** | **是** — 直接 `open()` 宿主文件 | +| `exec` | 是 | 否 | +| `read` | **否** | **是** — 直接 `open()` 宿主文件 | | `write` | **否** | **是** — 直接 `open()` 宿主文件 | -| `edit` | **否** | **是** — 直接 `open()` 宿主文件 | +| `edit` | **否** | **是** — 直接 `open()` 宿主文件 | +| `glob` | **否** | **是** — 直接遍历宿主目录 | +| `grep` | **否** | **是** — 直接读宿主文件 | -**沙箱边界不对称**: 这是刻意的设计权衡 — `read`/`write`/`edit` 绕过沙箱以获得性能(避免容器 I/O 开销),但意味着 LLM 可以直接读写 allowed_roots 下的任何文件。 +**沙箱边界不对称**: 这是刻意的设计权衡 — `read`/`write`/`edit`/`glob`/`grep` 绕过沙箱以获得性能(避免容器 I/O 开销与跨进程拷贝),但意味着 LLM 可以直接读写 `allowed_mount_roots` 下任何文件。Skill 路径经 `_resolve_host_path()` 重写,禁止穿越 `package_root`。 -exec 对 Skill 的特殊处理: 如果 `workdir` 引用 `/workspace/.skills/`,会创建 `BoxWorkspaceSession`,将 Skill 的 `package_root` 作为 `host_path`,并可选做 Python 环境自举。 +**exec 的 Skill 分支**: 命令中引用 `/workspace/.skills/` 的 skill 时: +1. 验证 skill 已激活 +2. 单次 exec 只能引用一个 skill 包 +3. 若 skill 是 Python 项目(有 `requirements.txt` 或 `pyproject.toml`),命令会被 venv bootstrap 包裹(在 skill 挂载点内创建 `.venv`) +4. 调用 `box_service.execute_tool()` → 走默认 session_id 与已组装好的 `extra_mounts`,**不再为每 skill 起独立 session** -### 4.3 MCP-in-Box (`mcp_stdio.py`) +### 4.3 MCP-in-Box (`mcp_stdio.py`, 354 行) -`BoxStdioSessionRuntime` 让 MCP stdio 服务器在 Box 容器中运行: +`BoxStdioSessionRuntime` 让 MCP stdio 服务器在 Box 容器中运行,**共享 session、多 process**模式(commit `529088e`): ``` initialize() - 1. 创建 BoxWorkspaceSession - 2. workspace.create_session() 创建容器 - 3. workspace.execute_raw(install_cmd) 安装依赖 (可选) - 4. workspace.start_managed_process(...) 启动 MCP Server - 5. websocket_client(ws_url) 通过 WS relay 连接 - 6. ClientSession.initialize() MCP 协议握手 + 1. 复用/创建共享 session (session_id = _build_box_session_id()) + - persistent=True,长期保持 + 2. workspace.execute_raw(install_cmd) 安装依赖 (可选) + 3. 将每个 MCP server 文件 stage 到 /workspace/.mcp// + 4. workspace.start_managed_process(process_id=) + 5. websocket_client(ws_url) 通过 WS relay 连接 + 6. ClientSession.initialize() MCP 协议握手 ``` -配置 (`MCPServerBoxConfig`): `network='on'`(MCP 服务器通常需要网络),`host_path_mode='ro'`(默认只读),`startup_timeout_sec=120`(留时间给 pip install)。 +配置 (`MCPServerBoxConfig`): `network='on'` (MCP 服务器通常需要网络),`host_path_mode='ro'` (默认只读),`startup_timeout_sec=120` (留时间给 pip install)。 + +每条 MCP server 是同一 session 中的一个 managed process,独立的 `process_id`、独立 attach URL,互不阻塞。 --- @@ -296,23 +444,25 @@ initialize() BuildAppStage.run(ap) ├─ ... (persistence, models, sessions) ... │ - ├─ BoxService(ap) line 134 - ├─ box_service.initialize() line 135 + ├─ BoxService(ap) + ├─ box_service.initialize() │ └─ connector.initialize() - │ ├─ [local] fork box.server subprocess - │ └─ [remote] connect WS - ├─ ap.box_service = box_service line 136 + │ ├─ [stdio] fork box subprocess + │ ├─ [subprocess+WS] Windows 本地 + │ └─ [remote WS] connect URL + │ └─ 启动心跳 _heartbeat_task + ├─ ap.box_service = box_service │ - ├─ ToolManager(ap) line 138 - ├─ tool_mgr.initialize() line 139 + ├─ ToolManager(ap) + ├─ tool_mgr.initialize() │ ├─ NativeToolLoader (检查 box_service.available) │ ├─ PluginToolLoader │ ├─ MCPLoader (Box 可用时,stdio MCP 走沙箱) │ └─ SkillAuthoringToolLoader - ├─ ap.tool_mgr = tool_mgr line 140 + ├─ ap.tool_mgr = tool_mgr │ ├─ ... (platform, pipeline) ... - ├─ SkillManager.initialize() line 160 + ├─ SkillManager.initialize() (从 Box runtime 加载 skill 列表) └─ ... (RAG, HTTP, plugins) ... ``` @@ -321,7 +471,6 @@ BoxService 在 ToolManager **之前**初始化。ToolManager 创建 loader 时 ### 5.2 初始化失败处理 ```python -# service.py:68-81 try: await self._runtime_connector.initialize() self._available = True @@ -330,7 +479,7 @@ except Exception as e: logger.warning(f"Box runtime unavailable: {e}") ``` -**静默降级**: Box 初始化失败不会阻止应用启动,仅导致 4 个 native tool 不暴露给 LLM。与 Plugin 的行为不同(Plugin 失败会抛异常)。 +**静默降级**: Box 初始化失败不会阻止应用启动,仅导致 6 个 native tool、所有 Skill 工具和 MCP-in-Box 工具不暴露给 LLM。与 Plugin 的行为不同(Plugin 失败会抛异常)。 ### 5.3 销毁流程 @@ -338,8 +487,8 @@ except Exception as e: app.dispose() └─ box_service.dispose() ├─ connector.dispose() - │ ├─ cancel _handler_task - │ ├─ cancel _ctrl_task + │ ├─ cancel _heartbeat_task + │ ├─ cancel _handler_task / _ctrl_task │ └─ terminate subprocess (SIGTERM) └─ loop.create_task(client.shutdown()) └─ RPC SHUTDOWN → Box Runtime 清理所有容器 @@ -351,33 +500,59 @@ Box 额外做了 RPC SHUTDOWN 通知 Runtime 主动清理容器,比 Plugin 的 ## 6. 配置 -### config.yaml +### config.yaml (重构后) ```yaml box: - profile: 'default' # 内置 Profile 名 - runtime_url: '' # 空 = 自动 fork 子进程 - shared_host_root: './data/box' # Docker 部署时用 '/workspaces' - default_host_workspace: '' # 默认为 /default - allowed_host_mount_roots: # 安全白名单 - - './data/box' - - '/tmp' + backend: 'local' # 'local' (探测) / 'docker' / 'nsjail' / 'e2b' + # BOX_BACKEND 环境变量优先级更高 + runtime: + endpoint: '' # 外部 Runtime 的 WS 基地址 'ws://host:5410' + # 留空 = 本地自管 Runtime + local: + profile: 'default' + image: '' # 覆盖 profile 默认 image + host_root: './data/box' # 工作区挂载根,Docker 部署需绝对路径 + default_workspace: '' # 默认 '/default' + skills_root: 'skills' # Box 管理的 skill 包目录(相对 host_root) + allowed_mount_roots: # 默认 [''] + - './data/box' + - '/tmp' + workspace_quota_mb: null # 配额覆盖,null = 走 profile + e2b: + api_key: '' # 也可走 E2B_API_KEY 环境变量 + api_url: '' # 自托管 E2B 时填写 + template: '' # 默认 template ID ``` +> **重大变更**: 较 2026-04-16 文档,配置结构完全重组(commit `eefdea4`)。原字段 `box.profile` / `box.runtime_url` / `box.shared_host_root` / `box.allowed_host_mount_roots` 全部迁入 `box.local.*` 子表,新增 `box.backend` 与 `box.e2b.*` 配置组。 + ### docker-compose.yaml ```yaml volumes: - ./data/box:/workspaces # 工作区挂载 - - /var/run/docker.sock:/var/run/docker.sock # Docker backend + - /var/run/docker.sock:/var/run/docker.sock # Docker backend 复用宿主 docker ``` -### REST API (3 个端点) +### Pipeline 配置 (templates/metadata/pipeline/ai.yaml) -| 端点 | 方法 | 说明 | -|------|------|------| -| `/api/v1/box/status` | GET | 可用性、Profile、后端信息 | -| `/api/v1/box/sessions` | GET | 活跃 session 列表 | -| `/api/v1/box/errors` | GET | 最近 50 条错误 | +`local-agent.config.box-session-id-template` 控制 session 作用域,预设: -**注意**: 前端目前未接入这 3 个 API。 +- `{launcher_type}_{launcher_id}` — 每个会话 (推荐,默认) +- `{launcher_type}_{launcher_id}_{sender_id}` — 群聊每个用户 +- `{launcher_type}_{launcher_id}_{conversation_id}` — 每个对话上下文 +- `{query_id}` — 每条消息(完全隔离) + +详见 [box-session-scope.md](./box-session-scope.md)。 + +### REST API + +| 端点 | 方法 | 说明 | 前端 | +|------|------|------|:---:| +| `/api/v1/box/status` | GET | 可用性、Profile、后端信息 | ✅ 监控页 | +| `/api/v1/box/sessions` | GET | 活跃 session 列表 | ❌ | +| `/api/v1/box/errors` | GET | 最近 50 条错误 | ❌ | +| `/api/v1/skills` 等 | GET/POST/PUT/DELETE | Skill CRUD、文件浏览、zip/GitHub 安装、preview | ✅ Skill 管理页 | + +前端 `web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx` 已接入 `/api/v1/box/status`,展示 backend 名称、profile 与活跃 session 数。Sessions 与 errors API 仍未接入。 diff --git a/docs/review/box-issues.md b/docs/review/box-issues.md index 9510862b..76a29e16 100644 --- a/docs/review/box-issues.md +++ b/docs/review/box-issues.md @@ -1,152 +1,157 @@ # Box 系统架构问题清单 -> 更新日期: 2026-04-16 +> 更新日期: 2026-05-19 > 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk) --- +## 已解决(自上一轮 review) + +下列原 P0/P1 项在最新分支已被修复,仅作记录: + +| 原编号 | 问题 | 处理 commit / 说明 | +|--------|------|---------------------| +| #3 | Box 无重连机制 | `_make_connection_callback` 已接入 `runtime_disconnect_callback`;`BoxService._reconnect_loop()` 实现指数退避重连 (`2dfd9d5d`、`c6882cf`) | +| #4 | Box 无心跳 | `BoxRuntimeConnector._heartbeat_loop()`,间隔 20s(沿用 Plugin 模式) | +| #10 | Windows 兼容 | connector 增加 Windows 分支 (subprocess + WS),backend 适配 Windows Docker (`120817a`、`fafb7a4`) | +| #12 | nsjail image 字段冲突 | `_assert_session_compatible()` 在不支持自定义镜像的 backend 跳过 image 字段 | +| #22 | 前端无 Box UI | 监控页 `SystemStatusCards.tsx` 已接入 `/api/v1/box/status`;Skill 管理页接入了全部 skill API(sessions/errors API 仍未接入) | + +--- + ## P0 — 合并前建议修复 ### 1. policy.py 是死代码 - **位置**: `pkg/box/policy.py` (98 行) - **现状**: `SandboxPolicy`、`ToolPolicy`、`ElevatedPolicy` 三个类已定义,但全项目无任何导入或调用 -- **影响**: 三层安全策略(sandbox 模式/工具白名单/权限提升)完全未生效,当前的实际策略是 "Box 可用就暴露全部 4 个 native tool,不可用就全部隐藏" -- **建议**: 要么删除死代码,要么接入 NativeToolLoader 调用链 +- **影响**: 三层安全策略(沙箱模式 / 工具白名单 / 权限提升)完全未生效。当前实际策略仍是"Box 可用就暴露全部 6 个 native tool,不可用就全部隐藏" +- **建议**: 要么删除死代码,要么接入 NativeToolLoader 的工具暴露 / exec 调用链。如果短期不会接入,至少在 `pkg/box/__init__.py` 显式标注其状态 ### 2. WebSocket relay 无认证 -- **位置**: SDK `box/server.py` `create_ws_relay_app()` + `handle_managed_process_ws()` -- **现状**: 任何能访问 5410 端口的客户端都可以 attach managed process 的 stdin/stdout -- **影响**: 网络内的攻击者可直接向 MCP Server 发送任意指令 -- **建议**: 至少加 token 认证(从 RPC 通道获取临时 token,WS 连接时验证) +- **位置**: SDK `box/server.py` — Action RPC 路径 `/rpc/ws` 与 managed-process relay `/v1/sessions/{id}/managed-process/{pid}/ws` +- **现状**: 任何能访问 5410 端口的客户端都可以连接,attach 任意 session 的 managed process stdin/stdout,或直接发起 EXEC +- **影响**: 容器化 / Docker compose 部署中,若 Box runtime 端口外暴露,网络内的攻击者可直接控制沙箱 +- **建议**: 至少加 token 认证(INIT 时下发,WS 连接 query string 或 header 校验);多 process 后 attach 面更大,更不能裸奔 -### 3. Box 无重连机制 - -- **位置**: `pkg/box/connector.py` `_make_connection_callback()` — Handler 创建时未设置 `disconnect_callback` -- **现状**: 连接断开后 Handler loop 直接退出,Box 功能永久不可用直到应用重启 -- **对比**: Plugin 在 WS 模式下有 `sleep(3) -> re-initialize` 自动重连 -- **建议**: 参考 Plugin 的 `runtime_disconnect_callback`,至少 WS 模式加重连 - -### 4. Box 无心跳 - -- **位置**: `pkg/box/connector.py` — 无 `heartbeat_loop()` 方法 -- **现状**: 初始握手后无定期探活,连接断开只能在下次 RPC 调用时被动发现 -- **对比**: Plugin 有 20s 间隔的 ping loop -- **建议**: 加 30s 间隔心跳,失败时触发重连 - -### 5. security.py 根路径未拦截 +### 3. security.py 根路径未拦截 - **位置**: SDK `box/security.py` `BLOCKED_HOST_PATHS_POSIX` -- **现状**: 黑名单中没有 `/`,`host_path="/"` 可通过校验并挂载整个主机文件系统 -- **建议**: 将 `/` 加入黑名单,或改用白名单策略 +- **现状**: 黑名单中没有 `/`,`host_path="/"` 可通过校验并挂载整个主机文件系统;用户 home 目录、`/var` 等也未拦截 +- **建议**: 将 `/` 加入黑名单,或改用白名单策略与 LangBot 侧 `allowed_mount_roots` 二次拦截 + +### 4. INIT 与 backend 初始化的竞态 + +- **位置**: SDK `box/runtime.py` `init()` 在握手后才下发实际配置;`backend` 在 INIT 之前可能已经按默认值实例化 +- **现状**: commit `5029d9c` 修复了 "init config before backend reuse" 的部分场景,但 backend 重新实例化时若有正在执行的 session,可能命中旧 backend +- **建议**: 整理 init/handshake 顺序——要么 INIT 完成前不接受任何业务 action,要么允许 backend 配置变更时显式清理现有 session --- ## P1 — 合并后优先跟进 -### 6. Session 数量无上限 +### 5. Session 数量无上限 - **位置**: SDK `box/runtime.py` `_get_or_create_session()` - **现状**: `_sessions` dict 无容量限制,恶意或异常调用可创建无限 session -- **建议**: 加 `max_sessions` 配置项,达到上限时拒绝新建或清理最老 session +- **建议**: 加 `max_sessions` 配置项,达到上限时拒绝新建或按 LRU 清理 -### 7. Quota 检查存在 TOCTOU +### 6. Quota 检查存在 TOCTOU - **位置**: `pkg/box/service.py` `_enforce_workspace_quota()` - **现状**: 应用层先读磁盘大小再执行命令,两步之间有竞态窗口 - **建议**: 短期用 Docker `--storage-opt size=` 做内核级限制;长期用 Redis 原子计数器做预留式配额 -### 8. 全局锁持有期间执行慢操作 +### 7. 全局锁持有期间执行慢操作 -- **位置**: SDK `box/runtime.py` `_get_or_create_session()` — `self._lock` 下调用 `backend.start_session()` (即 `docker run`) -- **影响**: `docker run` 可能耗时数秒(含镜像拉取),期间阻塞所有并发请求 +- **位置**: SDK `box/runtime.py` `_get_or_create_session()` — `self._lock` 下调用 `backend.start_session()` (即 `docker run` / `nsjail` 进程启动 / E2B `Sandbox.create`) +- **影响**: `docker run` 可能耗时数秒(含镜像拉取)、E2B 冷启动通常 > 1s,期间阻塞所有并发请求 - **建议**: 在 `_lock` 下仅做状态检查和 session 注册,容器创建在锁外执行 -### 9. Session 清理是机会性的 +### 8. Session 清理是机会性的 - **位置**: SDK `box/runtime.py` `_reap_expired_sessions_locked()` — 仅在 `_get_or_create_session()` 时调用 - **影响**: 如果长时间无新 session 请求,过期 session(含容器)不会被清理 - **建议**: 加一个独立的 `asyncio.create_task` 定时清理(如每 60s 一次) -### 10. 缺少 Windows 兼容处理 +### 9. server.py 直接访问 runtime 私有字段 -- **位置**: `pkg/box/connector.py` — 无 `win32` 分支 -- **现状**: Windows 的 asyncio ProactorEventLoop 不支持 subprocess stdio pipe -- **对比**: Plugin 专门加了 Win32 分支(subprocess + WS 通信) -- **建议**: 加 Windows 分支,或在文档/代码中明确声明不支持 - -### 11. server.py 直接访问 runtime 私有字段 - -- **位置**: SDK `box/server.py:139` — `handle_managed_process_ws` 直接读 `runtime._sessions` +- **位置**: SDK `box/server.py` — managed-process WS handler 直接读 `runtime._sessions` - **影响**: 绕过锁和封装,在并发场景下可能读到不一致状态 -- **建议**: 在 BoxRuntime 上增加公共方法(如 `get_session_managed_process(session_id)`) +- **建议**: 在 BoxRuntime 上增加公共方法(如 `get_session_managed_process(session_id, process_id)`) -### 12. nsjail image 字段与兼容性检查冲突 - -- **位置**: SDK `box/nsjail_backend.py:148` 设 `image='host'`;`runtime.py:284` 检查 `image` 字段一致性 -- **影响**: 用 nsjail 后端时,如果调用方 BoxSpec 指定了 `image='python:3.11-slim'`(默认值),存储的 `image='host'` 与后续请求的默认值不匹配,永远冲突 -- **建议**: nsjail 后端的兼容性检查应跳过 `image` 字段,或统一忽略 image 当 backend 不支持自定义镜像时 - ---- - -## P2 — 后续迭代 - -### 13. 重复的 `_is_path_under` 函数 - -- **位置**: `pkg/box/service.py` 行 30 和行 36 — 同名函数定义两次 -- **建议**: 删除重复定义 - -### 14. Skill 激活协议无递归保护 - -- **位置**: `pkg/skill/activation.py` -- **影响**: LLM 在第二次调用中可再次输出 `[ACTIVATE_SKILL:]` 标记,触发无限循环 -- **建议**: 加 `max_activation_depth` 检查 - -### 15. localagent.py 工具循环无迭代上限 - -- **位置**: `pkg/provider/runners/localagent.py` `while pending_tool_calls` 循环 -- **影响**: 恶意或混乱的 LLM 可无限产生 tool call,消耗资源 -- **建议**: 加 `max_tool_iterations` 配置项(如默认 50 次) - -### 16. localagent.py 中的死代码 - -- **位置**: `pkg/provider/runners/localagent.py:29-35` — `SANDBOX_EXEC_TOOL_NAME` 和 `SANDBOX_EXEC_SYSTEM_GUIDANCE` -- **现状**: 旧命名方案的遗留常量,从未被引用(实际使用 `EXEC_TOOL_NAME` from native.py) -- **建议**: 删除 - -### 17. @loader_class 装饰器未使用 - -- **位置**: `pkg/provider/tools/loader.py` — `preregistered_loaders` 列表和 `@loader_class` 装饰器 -- **现状**: MCPLoader 和 PluginToolLoader 的 `@loader_class` 被注释掉,ToolManager 手动实例化所有 loader -- **建议**: 要么启用装饰器自动注册,要么删除未用的机制 - -### 18. 工具名冲突风险 - -- **位置**: `pkg/provider/tools/toolmgr.py` `execute_func_call()` — 按优先级 native -> plugin -> mcp -> skill_authoring 分发 -- **影响**: 如果 plugin 或 MCP 有名为 `exec`/`read`/`write`/`edit` 的工具,会被 native loader 静默遮蔽 -- **建议**: 加命名空间前缀或冲突检测告警 - -### 19. workspace quota 检查阻塞事件循环 +### 10. workspace quota 检查阻塞事件循环 - **位置**: `pkg/box/service.py` `_get_workspace_size_bytes()` — 使用同步 `os.scandir` 递归遍历 - **影响**: 大工作区可能阻塞 asyncio event loop - **建议**: 用 `asyncio.to_thread()` 包装,或用 `aiofiles` 异步扫描 -### 20. client.py 反序列化不一致 +### 11. extra_mounts 一旦容器创建即固定 -- **位置**: SDK `box/client.py:118-126` — `execute()` 手动逐字段构建 `BoxExecutionResult` -- **对比**: `start_managed_process()` 使用 `model_validate(data)` 自动反序列化 +- **位置**: SDK `box/runtime.py` 的兼容性检查;`pkg/box/service.py:build_skill_extra_mounts()` +- **现状**: Skill 挂载在容器创建时一次性写入;同一 session 后续 pipeline 切换 skill 列表时,新挂载不会生效(除非销毁重建) +- **影响**: 用户长时间共享 session 的场景下,新激活的 skill 可能挂不上 +- **建议**: 要么在创建时把 pipeline 绑定的所有 skill 都挂上(实际现状)+ 写入文档;要么变更挂载时强制销毁 session 重建(已被 commit `5029d9c` 部分覆盖,需校验) + +--- + +## P2 — 后续迭代 + +### 12. 重复的 `_is_path_under` 函数 + +- **位置**: `pkg/box/service.py` 行 30 附近 — 同名函数定义两次 +- **建议**: 删除重复定义 + +### 13. localagent.py 工具循环无迭代上限 + +- **位置**: `pkg/provider/runners/localagent.py` `while pending_tool_calls` 循环 +- **影响**: 恶意或混乱的 LLM 可无限产生 tool call,消耗资源 +- **建议**: 加 `max_tool_iterations` 配置项(如默认 50 次) + +### 14. localagent.py 中的死代码 + +- **位置**: `pkg/provider/runners/localagent.py:29-35` 附近 — 旧命名 `SANDBOX_EXEC_TOOL_NAME` 和 `SANDBOX_EXEC_SYSTEM_GUIDANCE` +- **现状**: 旧命名方案的遗留常量,从未被引用(实际使用 `EXEC_TOOL_NAME` from native.py) +- **建议**: 删除 + +### 15. @loader_class 装饰器未使用 + +- **位置**: `pkg/provider/tools/loader.py` — `preregistered_loaders` 列表和 `@loader_class` 装饰器 +- **现状**: 各 loader 的 `@loader_class` 多数被注释掉,ToolManager 手动实例化所有 loader +- **建议**: 要么启用装饰器自动注册,要么删除未用的机制 + +### 16. 工具名冲突风险 + +- **位置**: `pkg/provider/tools/toolmgr.py` `execute_func_call()` — 按优先级 native → plugin → mcp → skill → skill_authoring 分发 +- **影响**: 如果 plugin 或 MCP 有名为 `exec`/`read`/`write`/`edit`/`glob`/`grep`/`activate` 的工具,会被前序 loader 静默遮蔽 +- **建议**: 加命名空间前缀或冲突检测告警 + +### 17. client.py 反序列化不一致 + +- **位置**: SDK `box/client.py` — `execute()` 与其他方法对返回值的反序列化方式不统一(部分手动构造 model,部分用 `model_validate`) - **建议**: 统一使用 `model_validate` -### 21. 错误类型还原基于字符串前缀匹配 +### 18. 错误类型还原基于字符串前缀匹配 -- **位置**: SDK `box/client.py:59-82` `_translate_action_error()` -- **影响**: 如果 server 端错误消息格式变化,client 会回退到通用 `BoxError` -- **建议**: 在 ActionResponse 中增加结构化的错误类型字段 +- **位置**: SDK `box/client.py` `_translate_action_error()` +- **影响**: 如果 server 端错误消息格式变化,client 会回退到通用 `BoxError`,丢失类型信息 +- **建议**: 在 ActionResponse 中增加结构化的错误类型字段(如 `error_code` 枚举) -### 22. 前端无 Box 相关 UI +### 19. 前端只用到了 status -- **位置**: `web/src/` — 无任何 Box 组件、类型定义或 API 调用 -- **现状**: 后端有 3 个 REST API(`/api/v1/box/{status,sessions,errors}`)但前端未接入 -- **建议**: 后续迭代加 Box 状态面板(至少展示可用性、活跃 session、最近错误) +- **位置**: `web/src/app/home/monitoring/...` 已接入 `/api/v1/box/status` +- **现状**: `/api/v1/box/sessions` 与 `/api/v1/box/errors` 后端可用、前端未消费 +- **建议**: 在监控页或独立 Box 详情页展示活跃 session 列表与最近错误,提升运维体感 + +### 20. skill_store 测试覆盖偏薄 + +- **位置**: SDK `tests/box/test_skill_store.py` 仅 88 行 +- **现状**: 相对 `skill_store.py` 的 647 行实现,单测覆盖度不够;GitHub 安装路径、`source_subdir` / `target_suffix` 组合、损坏 zip 的错误处理等场景未覆盖 +- **建议**: 至少补到核心 path 覆盖(preview/install/list/file CRUD 各 2~3 个 case) + +### 21. 集成测试未进 CI + +- **位置**: LangBot `tests/integration_tests/box/test_box_integration.py`、`test_box_mcp_integration.py`,SDK 端的 E2B 真机测试 +- **现状**: 容器实际执行、E2B 真实 sandbox、Managed process WS attach 均仅本地能跑 +- **建议**: 加一个可选的 Docker-in-Docker CI stage,或在合并前手动跑 checklist diff --git a/docs/review/box-session-scope.md b/docs/review/box-session-scope.md index ea0074cf..8255a6a5 100644 --- a/docs/review/box-session-scope.md +++ b/docs/review/box-session-scope.md @@ -1,11 +1,38 @@ # Box Session Scope Design -> Date: 2026-04-18 +> Date: 2026-04-18 (last reviewed 2026-05-19) > Branch: `feat/sandbox` (LangBot + langbot-plugin-sdk) > Related: [Box Architecture](./box-architecture.md) | [Box vs Plugin Runtime](./box-vs-plugin-runtime.md) --- +## 0. Implementation Status (2026-05-19) + +This document was authored as a design proposal. The current `feat/sandbox` branch +has shipped the design largely as written: + +| Item | Status | Notes | +|------|--------|-------| +| `BoxMountSpec` + `BoxSpec.extra_mounts` | ✅ Shipped | SDK `box/models.py` | +| Docker / nsjail / E2B backends apply extra mounts | ✅ Shipped | Last gap closed by SDK commit `0fea9b1` (E2B) | +| `box-session-id-template` in `local-agent` pipeline config | ✅ Shipped | `templates/metadata/pipeline/ai.yaml`, default `{launcher_type}_{launcher_id}` | +| `BoxService.resolve_box_session_id(query)` | ✅ Shipped | `pkg/box/service.py:166` | +| `BoxService.build_skill_extra_mounts(query)` | ✅ Shipped | `pkg/box/service.py:189` | +| Skill exec uses unified container + extra mounts | ✅ Shipped | `pkg/provider/tools/loaders/native.py` skill branch | +| MCP-in-Box uses shared persistent session, multi-process | ✅ Shipped (earlier than originally scoped) | SDK commit `529088e`, LangBot `mcp_stdio.py:_build_box_session_id` | +| `BoxManagedProcessSpec.process_id` + multi-process per session | ✅ Shipped | `BoxRuntime` keeps `managed_processes: dict[pid, _ManagedProcess]` | +| Per-tenant / quota integration with templates | ❌ Not started | See [box-tob-analysis.md](./box-tob-analysis.md) | + +The "Phase 2 deferred" note in §10 is **out of date** — MCP unification went in on +the same line. Pipeline-scoped (not user-scoped) MCP container is the realized +behavior: each pipeline's MCP servers share one `mcp-` session, and +user exec sessions use the template-derived id. + +The remaining open work is multi-tenant overlays (tenant_id in session_id, +quota counters keyed by tenant), tracked in the toB analysis doc rather than here. + +--- + ## 1. Problems ### 1.1 Default exec: per-message containers diff --git a/docs/review/box-test-coverage.md b/docs/review/box-test-coverage.md index 18b8f835..3fe5b52d 100644 --- a/docs/review/box-test-coverage.md +++ b/docs/review/box-test-coverage.md @@ -1,6 +1,6 @@ # Box 系统测试覆盖分析 -> 更新日期: 2026-04-16 +> 更新日期: 2026-05-19 > 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk) --- @@ -9,25 +9,34 @@ ### LangBot 仓库 -| 文件 | 行数 | 测试数 | CI 运行 | 覆盖范围 | -|------|------|--------|---------|---------| -| `tests/unit_tests/box/test_box_connector.py` | 79 | 6 | 是 | Connector 传输决策、WS relay URL、dispose | -| `tests/unit_tests/box/test_box_service.py` | 1168 | 40+ | 是 | Service 核心逻辑(最全面) | -| `tests/unit_tests/box/test_workspace.py` | 144 | 7 | 是 | WorkspaceSession 路径重写、payload 构建 | -| `tests/unit_tests/provider/test_mcp_box_integration.py` | 642 | 22 | 是 | MCP Box 配置、路径重写、payload、runtime info | -| `tests/unit_tests/provider/test_localagent_sandbox_exec.py` | 444 | 5 | 是 | LocalAgent exec 流程、流式、Skill 激活 | -| `tests/unit_tests/provider/test_tool_manager_native.py` | 249 | 14 | 是 | ToolManager 路由、native tool CRUD、路径穿越 | -| `tests/unit_tests/provider/test_skill_tools.py` | 569 | 20 | 是 | Skill 管理、激活、路径、authoring CRUD | -| `tests/integration_tests/box/test_box_integration.py` | 324 | 6 | **否** | 真实容器执行、超时、网络隔离 | -| `tests/integration_tests/box/test_box_mcp_integration.py` | 361 | 6 | **否** | Managed process、WS attach、session 清理 | +| 文件 | 行数 | CI 运行 | 覆盖范围 | +|------|------|---------|---------| +| `tests/unit_tests/box/test_box_connector.py` | 106 | 是 | Connector 传输决策、WS relay URL、dispose、心跳/重连 | +| `tests/unit_tests/box/test_box_service.py` | 1224 | 是 | Service 核心逻辑(最全面) | +| `tests/unit_tests/box/test_workspace.py` | 147 | 是 | WorkspaceSession 路径重写、payload 构建 | +| `tests/unit_tests/provider/test_mcp_box_integration.py` | 707 | 是 | MCP Box 配置、路径重写、payload、shared-session/multi-process、runtime info | +| `tests/unit_tests/provider/test_localagent_sandbox_exec.py` | 444 | 是 | LocalAgent exec 流程、流式、Skill 激活 (Tool Call) | +| `tests/unit_tests/provider/test_tool_manager_native.py` | 249 | 是 | ToolManager 路由、native tool CRUD、路径穿越、6 工具暴露 | +| `tests/unit_tests/provider/test_skill_tools.py` | 582 | 是 | Skill 管理、Tool Call 激活、路径、authoring CRUD | +| `tests/unit_tests/test_skill_service.py` | 396 | 是 | HTTP service:skill CRUD、zip/GitHub install、文件浏览 | +| `tests/unit_tests/test_paths.py` | 23 | 是 | paths 工具 | +| `tests/unit_tests/test_preproc.py` | 134 | 是 | PreProcessor 注入 session 变量、bound skill 解析 | +| `tests/unit_tests/pipeline/test_chat_handler_logging.py` | 78 | 是 | Chat handler 日志相关回归 | +| `tests/integration_tests/box/test_box_integration.py` | 329 | **否** | 真实容器执行、超时、网络隔离 | +| `tests/integration_tests/box/test_box_mcp_integration.py` | 368 | **否** | Managed process、WS attach、shared-session 清理 | ### SDK 仓库 -| 文件 | 行数 | 测试数 | CI 运行 | 覆盖范围 | -|------|------|--------|---------|---------| -| `tests/box/test_nsjail_backend.py` | 384 | 18 | 是 | nsjail 可用性、session、arg 构建、资源限制 | +| 文件 | 行数 | CI 运行 | 覆盖范围 | +|------|------|---------|---------| +| `tests/box/test_backend_selection.py` | 255 | 是 | 显式 backend / local 模式探测顺序 / 配置变更触发 reselect | +| `tests/box/test_nsjail_backend.py` | 452 | 是 | nsjail 可用性、安装版 CLI vs 容器内 CLI、session、arg 构建、资源限制 | +| `tests/box/test_e2b_backend.py` | 482 | 是 | E2B SDK mock、session 生命周期、extra_mounts 同步 | +| `tests/box/test_skill_store.py` | 88 | 是 | zip preview/install、基础 file CRUD | -**总计**: 10 个测试文件, ~4400 行, ~144 个测试; 其中 12 个集成测试在 CI 中不运行。 +**总计**: 17 个测试文件, ~6,500 行测试代码; 其中 2 个集成测试(约 700 行)在 CI 中不运行。 + +> 较 2026-04-16 版增加:`test_skill_service.py`、`test_paths.py`、`test_preproc.py`、`test_chat_handler_logging.py` (LangBot),`test_backend_selection.py`、`test_e2b_backend.py`、`test_skill_store.py` (SDK)。`test_nsjail_backend.py` 增加 CLI 兼容性 case (commit `feed530`)。 --- @@ -35,32 +44,36 @@ | 区域 | 质量 | 说明 | |------|------|------| -| BoxRuntime session 管理 | 优秀 | session 复用、冲突检测、TTL 配置 | +| BoxRuntime session 管理 | 优秀 | session 复用、冲突检测、TTL 配置、消失 session 重建 | | BoxService Profile 系统 | 优秀 | 4 个内置 Profile、locked/unlocked 字段、timeout clamp | -| BoxService host mount 安全 | 优秀 | allowed_roots、disallowed_roots、shared_host_root | +| BoxService host mount 安全 | 优秀 | allowed_mount_roots、disallowed_roots、shared host root | | BoxService workspace quota | 优秀 | 前置/后置配额检查、超额清理 | | BoxService 输出截断 | 优秀 | 短/精确边界/长输出、独立 stderr | | BoxService 可观测性 | 优秀 | 状态报告、error ring buffer、buffer 上限 | +| BoxService session 模板 | 良好 | `resolve_box_session_id` + `build_skill_extra_mounts` 在 service / native / mcp 三处都有覆盖 | | RPC client/server 协议 | 优秀 | execute/get_sessions/delete/create/conflict error | -| BoxRuntimeConnector | 良好 | local/remote 模式、Docker 平台、relay URL、dispose | -| BoxWorkspaceSession | 良好 | payload 构建、managed process 路径重写 | +| BoxRuntimeConnector | 良好 | local/remote 模式、Docker 平台、relay URL、心跳与重连回调 | +| BoxWorkspaceSession | 良好 | payload 构建、managed process 路径重写、stage host file | | BoxHostMountMode.NONE | 良好 | 枚举校验、workdir 约束 | -| NsjailBackend | 良好 | 可用性、session 生命周期、arg 构建、资源限制 | -| MCP Box 集成 | 良好 | config model、路径重写 (6 case)、payload | -| Native tool loader | 良好 | 文件 CRUD、目录列表、路径穿越拦截 | -| LocalAgent exec 流程 | 良好 | 完整 tool call 循环、流式、system prompt 注入 | -| Skill 系统 | 良好 | 加载、激活、marker、路径解析、authoring CRUD | +| NsjailBackend | 良好 | 可用性、安装版 vs 容器内、session 生命周期、arg 构建、资源限制 | +| E2BBackend | 良好 | mock SDK、session/extra_mounts 同步 | +| Backend selection | 良好 | 显式 backend 优先级、local 探测顺序、配置变更触发 reselect | +| MCP Box 集成 | 良好 | config model、路径重写、payload、shared-session 多 process | +| Native tool loader | 良好 | 6 工具(exec/read/write/edit/glob/grep)、路径穿越拦截 | +| LocalAgent exec 流程 | 良好 | 完整 tool call 循环、流式、system prompt 注入、Tool Call 激活 | +| Skill 系统 | 良好 | 加载、Tool Call 激活、marker、路径解析、authoring CRUD、HTTP service | --- ## 3. 覆盖缺失的区域 -### 3.1 零测试 (Critical) +### 3.1 零测试 / 严重不足 | 区域 | 源文件 | 影响 | |------|--------|------| -| **`security.py`** | SDK `box/security.py` | `validate_sandbox_security()` 无任何测试。阻止 `/etc`/`/proc`/Docker socket 等危险挂载的安全函数从未被验证 | -| **`policy.py`** | `pkg/box/policy.py` | 三层安全策略(SandboxPolicy/ToolPolicy/ElevatedPolicy)无测试(也是死代码) | +| **`security.py`** | SDK `box/security.py` (52 行) | `validate_sandbox_security()` 无任何测试。阻止 `/etc`/`/proc`/Docker socket 等危险挂载的安全函数从未被验证 | +| **`policy.py`** | `pkg/box/policy.py` (98 行) | 三层安全策略无测试(也是死代码) | +| **`skill_store.py` 边缘场景** | SDK `box/skill_store.py` (647 行) vs 测试 88 行 | GitHub 安装路径、`source_subdir` / `target_suffix` 组合、损坏 zip、文件冲突等场景未覆盖 | ### 3.2 未测试的关键路径 @@ -68,23 +81,26 @@ |------|------| | **Session TTL 过期** | 测试配置了 `session_ttl_sec` 但从未推进时间验证过期清理 | | **并发 session 访问** | 无并发 exec / 并发创建 / race condition 测试 | -| **Container backend (Podman/Docker)** | 仅通过集成测试覆盖(CI 不运行),单元测试全用 FakeBackend | +| **Container backend (Docker)** | 仅通过集成测试覆盖(CI 不运行),单元测试全用 FakeBackend | +| **E2B 真实 sandbox** | 单测全是 mock,未对接真实 E2B API | | **BoxRuntime shutdown()** | 在 test cleanup 中调用但未验证行为 | | **BoxServerHandler 错误路径** | 畸形请求、未知 action 类型 | | **WS relay** | 仅在集成测试中覆盖(CI 不运行) | -| **NsjailBackend _run_nsjail** | 总是被 mock,实际 subprocess 调用未验证 | | **NsjailBackend managed process** | 完全未测试 | -| **MCP stdio 完整生命周期** | 依赖安装 → 进程启动 → 健康检查 → 重试 | -| **BoxService start_managed_process** | 仅集成测试覆盖 | +| **MCP stdio 完整生命周期** | 依赖安装 → 进程启动 → 健康检查 → 多 process 并发 → 重试 | +| **BoxService start/stop_managed_process** | 单 process 流转有单测,多 process 互不阻塞主要靠集成测试 | +| **重连指数退避** | connector 单测覆盖回调接线,未实际跑完整重连周期 | ### 3.3 边缘情况缺失 | 区域 | 说明 | |------|------| | BoxSpec 校验 | 无效 session_id 格式、超长命令、env 特殊字符 | +| BoxSpec.extra_mounts | 重复 mount_path、与 host_path 冲突、绝对 vs 相对路径 | | BoxExecutionResult | 仅 COMPLETED 和 TIMED_OUT,无 ERROR 状态测试 | -| 多后端 fallback | 仅单后端配置,无 Podman 不可用 → fallback Docker 测试 | +| 多后端 fallback | local 模式探测顺序仅靠 mock,无真实 Docker 不可用 → nsjail 真机 fallback 测试 | | Profile YAML 加载 | 测试用硬编码字符串,未从真实 config.yaml 加载 | +| INIT 配置变更触发 backend 重建 | 单测仅在初始化场景验证 | --- @@ -94,10 +110,12 @@ CI 仅运行 `tests/unit_tests/`,以下场景**从未在自动化中验证**: - 真实容器的创建/执行/销毁 - 容器网络隔离(`--network none`) -- 容器资源限制生效 +- 容器资源限制生效(cpus/memory/pids_limit) - Managed process 的 WS 双向 I/O +- 多 process 同 session 并发 I/O - 孤儿容器清理 - Session 删除清理容器 - 进程退出检测 +- E2B 真实 sandbox 行为 -**建议**: 在 CI 中加一个可选的 Docker-in-Docker 集成测试 stage,至少覆盖核心执行路径。 +**建议**: 在 CI 中加一个可选的 Docker-in-Docker 集成测试 stage,至少覆盖核心执行路径(exec / MCP attach / session 销毁)。 diff --git a/docs/review/box-tob-analysis.md b/docs/review/box-tob-analysis.md index 81890470..c41f45ae 100644 --- a/docs/review/box-tob-analysis.md +++ b/docs/review/box-tob-analysis.md @@ -1,6 +1,6 @@ # Box 系统 toB 商业化分析 -> 更新日期: 2026-04-16 +> 更新日期: 2026-05-19 > 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk) --- @@ -10,7 +10,9 @@ | 能力 | toB 价值 | 代码位置 | |------|---------|---------| | **沙箱隔离执行** | 企业安全运行不受信代码的基础能力 | SDK `box/backend.py` | -| **多后端支持** | 适配不同企业容器基础设施 (Podman/Docker/nsjail) | SDK `box/runtime.py` `_select_backend()` | +| **多后端支持** | 适配不同企业容器基础设施 (Podman/Docker/nsjail/E2B) | SDK `box/runtime.py` `_select_backend()` | +| **E2B 云沙箱** | SaaS / 无 Docker 部署的兜底执行环境 | SDK `box/e2b_backend.py` | +| **连接自愈** | 心跳 + 自动重连,单点 Box runtime 故障可恢复 | `pkg/box/connector.py` `_heartbeat_loop`, `pkg/box/service.py` `_reconnect_loop` | | **Profile + locked 字段** | 运维锁定安全边界,LLM/用户无法绕过 | `pkg/box/service.py`, SDK `box/models.py` | | **资源限制** | CPU/内存/PID 数限制防止资源滥用 | SDK `backend.py` `--cpus/--memory/--pids-limit` | | **Workspace quota** | 磁盘用量控制 | `pkg/box/service.py` `_enforce_workspace_quota` | @@ -47,10 +49,11 @@ | 维度 | 现状 | toB 要求 | 优先级 | |------|------|---------|--------| -| **连接恢复** | 无重连、无心跳 | 自动重连 + 健康检查 | **P0** | +| **连接恢复** | 已实现:20s 心跳 + `_reconnect_loop` 指数退避 | 已满足基本要求 | 已有 | | **Session 清理** | 机会性(仅新建时触发) | 定时清理 + 独立 reaper | **P1** | -| **水平扩展** | 单 Box Runtime 实例 | 多实例负载均衡 | **P1** | +| **水平扩展** | 单 Box Runtime 实例 | 多实例负载均衡(按 tenant 路由) | **P1** | | **优雅降级** | 已有(_available=False) | 已满足基本要求 | 已有 | +| **Backend 自愈** | 已实现:`get_status` 时若 backend 不可用会重新选择 | 已满足基本要求 | 已有 | ### 2.4 可观测性 @@ -58,7 +61,7 @@ |------|------|---------|--------| | **监控指标** | 无 Prometheus metrics | session 数/执行延迟/资源用量/错误率 | **P1** | | **结构化日志** | Python logging, 无结构化 | JSON 格式日志,含 trace_id/tenant_id | **P1** | -| **前端面板** | 无 Box UI | 状态面板:可用性/活跃 session/错误/资源用量 | **P2** | +| **前端面板** | 监控页接入 `/api/v1/box/status`(backend 名 + 活跃 session 数);`sessions` / `errors` 仍未接入 | 完整状态面板 + 历史错误/审计列表 | **P2** | --- @@ -141,9 +144,10 @@ LangBot ──> K8s Job per execution - [ ] WS relay 加 token 认证 - [ ] 接入或删除 policy.py -- [ ] Box 加重连和心跳 +- [x] ~~Box 加重连和心跳~~(已完成,见 [box-issues.md 已解决](./box-issues.md)) - [ ] 审计日志持久化(至少写文件/数据库) - [ ] `security.py` 加 `/` 拦截,考虑白名单 +- [ ] INIT 与 backend 初始化顺序整理(避免 backend 在配置到达前实例化) ### Phase 2 (4-8 周): 多租户基础 diff --git a/docs/review/box-vs-plugin-runtime.md b/docs/review/box-vs-plugin-runtime.md index 4bdda1e6..66e644ea 100644 --- a/docs/review/box-vs-plugin-runtime.md +++ b/docs/review/box-vs-plugin-runtime.md @@ -1,6 +1,6 @@ # Box Runtime vs Plugin Runtime: 连接架构对比 -> 更新日期: 2026-04-16 +> 更新日期: 2026-05-19 > 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk) --- @@ -10,10 +10,10 @@ | 维度 | Plugin Runtime | Box Runtime | |------|---------------|-------------| | **继承关系** | `PluginRuntimeConnector(ManagedRuntimeConnector)` | `BoxRuntimeConnector`(独立类) | -| **传输分支** | 3 条 (Docker/WS, Win32/subprocess+WS, Unix/stdio) | 2 条 (本地 stdio, 远程 WS) | -| **心跳** | 20s ping loop | **无** | -| **重连** | WS 模式: sleep 3s → re-initialize | **无** | -| **Handler 类型** | `RuntimeConnectionHandler` (1132 行, 25+ action) | 基础 `Handler` (311 行, 0 自定义 action) | +| **传输分支** | 3 条 (Docker/WS, Win32/subprocess+WS, Unix/stdio) | 3 条 (本地 stdio, Win32/subprocess+WS, 远程 WS) | +| **心跳** | 20s ping loop | 20s ping loop(`_heartbeat_loop`) | +| **重连** | WS 模式: sleep 3s → re-initialize | 由 BoxService `_reconnect_loop` 处理,指数退避 | +| **Handler 类型** | `RuntimeConnectionHandler` (1132 行, 25+ action) | 基础 `Handler` + `BoxServerHandler`(SDK 端 25 action) | | **Client 抽象** | Handler 即 API | 独立 `ActionRPCBoxClient` 封装 Handler | | **启用/禁用** | `is_enable_plugin` 开关 | 无开关(可用/不可用由初始化结果决定) | | **初始化失败** | 异常上抛 | 静默降级 `_available=False` | @@ -48,6 +48,8 @@ else: await self._start_local_stdio() # StdioClientController ``` +> 历史:2026-04-16 版本本文档曾把 Box 描述为 2 路决策(缺 Windows 分支)。现已对齐 Plugin 的 3 路设计。 + ### 决策矩阵 | 环境 | Plugin | Box | @@ -111,26 +113,21 @@ connector.initialize() | 维度 | Plugin | Box | |------|--------|-----| -| 有心跳? | 是 (`connector.py:69-76`) | **否** | -| 间隔 | 20s | N/A | -| 失败处理 | 仅 DEBUG 日志,不触发重连 | N/A | -| 生命周期 | 整个应用生命周期,跨越重连 | N/A | +| 有心跳? | 是 | 是(`connector.py` `_heartbeat_loop`) | +| 间隔 | 20s | 20s | +| 失败处理 | 仅 DEBUG 日志,不触发重连 | 仅 DEBUG 日志,依赖 connection close 触发重连 | +| 生命周期 | 整个应用生命周期 | 连接建立后启动;`dispose()` 时 cancel | ### 重连 | 维度 | Plugin | Box | |------|--------|-----| -| Docker/WS 断开 | `runtime_disconnect_callback` → sleep 3s → re-initialize | Handler loop 退出,**永久不可用** | -| WS 连接失败 | 同上 | 存储错误 → `initialize()` 抛异常 → `_available=False` | -| stdio 断开 | 仅日志,不重连 | Handler loop 退出,永久不可用 | -| 重连退避 | 固定 3s,无 backoff | N/A | +| Docker/WS 断开 | `runtime_disconnect_callback` → sleep 3s → re-initialize | `runtime_disconnect_callback` → `BoxService._reconnect_loop()`(指数退避) | +| WS 连接失败 | 同上 | 同上;初次失败时 `_available=False`,重连成功后恢复 | +| stdio 断开 | 仅日志,不重连 | 接同样回调;stdio 重连需重新 fork 子进程 | +| 重连退避 | 固定 3s,无 backoff | 指数退避 | -**Box 断开后的效果链**: -1. `handler.run()` 捕获 `ConnectionClosedError` -2. `_disconnect_callback is None` → break -3. `_handler_task` 完成 → `_make_connection_callback` 返回 -4. 后续 `client._call()` → `BoxRuntimeUnavailableError` -5. Box 功能永久不可用 +> 历史:2026-04-16 版本本文档曾把心跳与重连标记为 Box 缺失。这两项已在 commit `2dfd9d5d` / `c6882cf` / `5029d9c` 等修复(详见 [box-issues.md 已解决](./box-issues.md))。 --- @@ -209,16 +206,16 @@ Box 的 RPC SHUTDOWN 确保容器被正确停止,不会成为孤儿。Plugin ### P0 -1. **Box 加重连**: 在 `_make_connection_callback` 中设置 `disconnect_callback`,WS 模式 sleep 3s → re-initialize -2. **Box 加心跳**: 30s 间隔 ping loop,参考 `PluginRuntimeConnector.heartbeat_loop()` +1. **两者都加 WS 认证**: 至少 token 认证(INIT 时下发,连接时校验) ### P1 -3. **Box 加 Windows 支持**: 像 Plugin 一样加 Win32 分支 (subprocess + WS) -4. **考虑 Box 继承 ManagedRuntimeConnector**: 复用 `_start_runtime_subprocess`/`_wait_until_ready`/`_dispose_subprocess` -5. **两者都加 WS 认证**: 至少 token 认证 +2. **考虑 Box 继承 ManagedRuntimeConnector**: 复用 `_start_runtime_subprocess` / `_wait_until_ready` / `_dispose_subprocess`,减少重复代码 +3. **Plugin 重连加退避**: 固定 3s 无 backoff 可能造成日志洪水,建议向 Box 的指数退避看齐 +4. **统一连接管理模式**: Event-based (Box) vs direct-await (Plugin),考虑收敛为一种 -### P2 +### 已完成(自上一轮) -6. **Plugin 重连加退避**: 固定 3s 无 backoff 可能造成日志洪水,建议指数退避 -7. **统一连接管理模式**: Event-based (Box) vs direct-await (Plugin),考虑收敛为一种 +- ~~Box 加重连~~(commit `2dfd9d5d`) +- ~~Box 加心跳~~(20s loop 与 Plugin 一致) +- ~~Box 加 Windows 支持~~(commit `120817a` / `fafb7a4`)