docs(review): refresh box architecture review for feat/sandbox

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)
This commit is contained in:
Junyan Qin
2026-05-19 13:31:26 +08:00
parent d80972417e
commit 6351730891
6 changed files with 552 additions and 326 deletions

View File

@@ -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/<name>
│ - 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/<name>`。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 + WebSocketWindows 不支持 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` 字段对不支持自定义镜像的 backendnsjail/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 <name>
--network none (可选)
--cpus/--memory/--pids-limit 资源限制
--read-only + --tmpfs /tmp 只读根文件系统
-v <host>:<mount>:<mode> 工作区挂载
<image> sh -lc 'while true; do sleep 3600; done' 保活进程
--cpus/--memory/--pids-limit
--read-only + --tmpfs /tmp
-v <host>:<mount>:<mode> 挂载
-v <extra.host>:<extra.mount>:.. 额外挂载 (extra_mounts)
<image> 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不存在时再尝试容器内 nsjailcommit `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 种 actionHEALTH/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 attachAction RPC 走 stdin/stdout。
### 3.4 Client (`box/client.py`, 377 行)
`ActionRPCBoxClient` 封装 `Handler.call_action()` 调用:
- 每个方法对应一个 RPC action
- 25+ 方法对应 25+ 个 RPC actionexec / 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_suffixcommit 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/<name>`,会创建 `BoxWorkspaceSession`,将 Skill 的 `package_root` 作为 `host_path`,并可选做 Python 环境自举。
**exec Skill 分支**: 命令中引用 `/workspace/.skills/<name>` 的 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/<process_id>/
4. workspace.start_managed_process(process_id=<server>)
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: '' # 默认为 <shared_host_root>/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: '' # 默认 '<host_root>/default'
skills_root: 'skills' # Box 管理的 skill 包目录(相对 host_root
allowed_mount_roots: # 默认 ['<host_root>']
- './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 仍未接入。

View File

@@ -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 APIsessions/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 通道获取临时 tokenWS 连接时验证)
- **位置**: 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

View File

@@ -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-<pipeline>` 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

View File

@@ -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 serviceskill 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 销毁)

View File

@@ -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 周): 多租户基础

View File

@@ -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`