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
+334 -159
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` 字段对不支持自定义镜像的 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 <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 仍未接入。