mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
docs(review): update Box architecture review documents
Replace old review docs with 5 focused documents: - box-architecture.md: deep architecture analysis (LangBot + SDK) - box-issues.md: 22 issues rated P0/P1/P2 - box-test-coverage.md: test coverage analysis - box-tob-analysis.md: toB commercialization analysis - box-vs-plugin-runtime.md: Box vs Plugin runtime comparison
This commit is contained in:
383
docs/review/box-architecture.md
Normal file
383
docs/review/box-architecture.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# Box 系统架构深度分析
|
||||
|
||||
> 更新日期: 2026-04-16
|
||||
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||
> 相关文档: [问题清单](./box-issues.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) │
|
||||
└──────────────┼─────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 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 双向中继) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 容器/沙箱 (Podman/Docker container 或 nsjail sandbox) │
|
||||
│ - 隔离文件系统、网络、PID 命名空间 │
|
||||
│ - 资源限制 (CPU, 内存, PID 数) │
|
||||
│ - exec: 用户命令在此执行 │
|
||||
│ - managed process: MCP Server 等长驻进程在此运行 │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**核心设计原则**: Box Runtime 作为独立进程运行,通过 Action RPC 与 LangBot 主进程通信。两者复用 SDK 的 IO 层(Handler → Connection → Controller)。
|
||||
|
||||
---
|
||||
|
||||
## 2. LangBot 侧模块
|
||||
|
||||
### 2.1 BoxService (`pkg/box/service.py`, 514 行)
|
||||
|
||||
应用层门面,协调 Profile、安全校验、配额、连接:
|
||||
|
||||
```
|
||||
BoxService
|
||||
├─ initialize()
|
||||
│ ├─ _ensure_default_host_workspace() 创建默认工作目录
|
||||
│ └─ connector.initialize() 连接 Box Runtime
|
||||
│
|
||||
├─ 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
|
||||
│
|
||||
├─ 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() 可观测性
|
||||
```
|
||||
|
||||
**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 行)
|
||||
|
||||
管理与 Box Runtime 的通信连接:
|
||||
|
||||
- **本地 stdio**: 无 `runtime_url` 时,fork `python -m langbot_plugin.box.server --port {port}` 子进程
|
||||
- **远程 WebSocket**: 有 `runtime_url` 时,连接 `ws://{host}:{port+1}`(+1 偏移,5410 是 relay,5411 是 RPC)
|
||||
- **同步等待**: 使用 `asyncio.Event` + `wait_for(timeout=30s)` 模式确认连接
|
||||
|
||||
### 2.3 BoxWorkspaceSession (`pkg/box/workspace.py`, 404 行)
|
||||
|
||||
在 BoxService 上的高级抽象,用于 Skill 和 MCP 场景:
|
||||
|
||||
- **路径重写**: `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 行) — 死代码
|
||||
|
||||
三层安全策略设计(SandboxPolicy/ToolPolicy/ElevatedPolicy),但**全项目无任何调用**。详见 [问题清单 #1](./box-issues.md)。
|
||||
|
||||
---
|
||||
|
||||
## 3. SDK 侧模块
|
||||
|
||||
### 3.1 BoxRuntime (`box/runtime.py`, 388 行)
|
||||
|
||||
核心编排器,管理 session 生命周期:
|
||||
|
||||
```
|
||||
Session 生命周期:
|
||||
|
||||
Client EXEC/CREATE_SESSION
|
||||
│
|
||||
▼
|
||||
_get_or_create_session(spec)
|
||||
├─ _reap_expired_sessions_locked() 清理 TTL 过期 session
|
||||
├─ 已存在? → _assert_session_compatible() → 复用
|
||||
└─ 新建? → backend.start_session(spec) → 创建容器
|
||||
│
|
||||
▼
|
||||
execute(spec)
|
||||
├─ 获取 session lock (每 session 独立)
|
||||
├─ backend.exec(session, spec) 在容器中执行命令
|
||||
├─ 更新 last_used_at
|
||||
└─ 超时? → 销毁 session
|
||||
│
|
||||
▼
|
||||
Session 保持存活直到:
|
||||
├─ TTL 过期 (默认 300s,下次操作时清理)
|
||||
├─ 执行超时 (自动销毁)
|
||||
├─ 客户端 DELETE_SESSION
|
||||
└─ SHUTDOWN
|
||||
```
|
||||
|
||||
**关键设计**:
|
||||
- 每 session 有独立 `asyncio.Lock`,同一 session 内的命令串行执行
|
||||
- 全局 `_lock` 保护 `_sessions` dict 的读写
|
||||
- 兼容性检查:比较 10 个字段(network/image/host_path/mount_path/cpus/memory_mb/pids_limit/read_only_rootfs/host_path_mode/workspace_quota_mb)
|
||||
|
||||
### 3.2 Backend 系统
|
||||
|
||||
#### CLISandboxBackend (`box/backend.py`, 389 行)
|
||||
|
||||
Podman/Docker 的公共基类:
|
||||
|
||||
```
|
||||
start_session(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' 保活进程
|
||||
3. 返回 BoxSessionInfo
|
||||
|
||||
exec(session, spec):
|
||||
docker/podman exec -e KEY=VAL <container>
|
||||
sh -lc 'mkdir -p <workdir> && cd <workdir> && <cmd>'
|
||||
|
||||
start_managed_process(session, spec):
|
||||
docker/podman exec -i <container>
|
||||
sh -lc 'mkdir -p <cwd> && cd <cwd> && exec <command> <args>'
|
||||
返回 asyncio.subprocess.Process (stdin/stdout PIPE)
|
||||
```
|
||||
|
||||
容器以 idle 进程启动(`while true; do sleep 3600; done`),实际命令通过 `docker exec` 执行。`--rm` 确保容器退出时自动清理。
|
||||
|
||||
**孤儿清理**: 启动时枚举 `langbot.box=true` 标签的容器,instance_id 不匹配的强制删除。
|
||||
|
||||
#### NsjailBackend (`box/nsjail_backend.py`, 510 行)
|
||||
|
||||
轻量级 Linux 沙箱(无容器引擎依赖):
|
||||
|
||||
- 使用 namespace 隔离(user/mount/pid/ipc/uts/cgroup/net)
|
||||
- 挂载宿主 `/usr`/`/lib`/`/bin`/`/sbin` 只读 + 选定 `/etc` 条目
|
||||
- 每 session 创建独立目录(workspace/tmp/home)
|
||||
- 资源限制: cgroup v2 优先,fallback 到 rlimit
|
||||
- **无自定义镜像**: 使用宿主 OS,`image` 字段固定为 `'host'`
|
||||
|
||||
**后端选择优先级**: Podman → Docker → nsjail(启动时逐个探测,首个可用的胜出,不做运行时 failover)
|
||||
|
||||
### 3.3 Server (`box/server.py`, 268 行)
|
||||
|
||||
两个服务共存:
|
||||
|
||||
1. **Action RPC**: `BoxServerHandler` 处理 11 种 action(HEALTH/STATUS/EXEC/CREATE_SESSION/...),通过 stdio 或 WS 传输
|
||||
2. **WS Relay** (aiohttp, port 5410): `GET /v1/sessions/{id}/managed-process/ws`,双向桥接 WebSocket ↔ managed process stdin/stdout
|
||||
|
||||
端口分配:
|
||||
- Port N (默认 5410): WS relay(managed process I/O)
|
||||
- Port N+1 (5411): Action RPC WebSocket(仅远程模式使用)
|
||||
|
||||
### 3.4 Client (`box/client.py`, 177 行)
|
||||
|
||||
`ActionRPCBoxClient` 封装 `Handler.call_action()` 调用:
|
||||
|
||||
- 每个方法对应一个 RPC action
|
||||
- 错误还原: `_translate_action_error()` 通过字符串前缀匹配还原 SDK 侧异常类型
|
||||
- `execute()` timeout = 300s,其他默认 15s
|
||||
|
||||
### 3.5 Models (`box/models.py`, 302 行)
|
||||
|
||||
核心数据模型:
|
||||
|
||||
| 模型 | 用途 |
|
||||
|------|------|
|
||||
| `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) |
|
||||
| `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` 下
|
||||
|
||||
### 3.6 Security (`box/security.py`, 54 行)
|
||||
|
||||
`validate_sandbox_security()`: 黑名单校验 host_path,阻止挂载 `/etc`/`/proc`/`/sys`/`/dev`/`/root`/`/boot` 及 Docker/Podman socket。
|
||||
|
||||
**已知缺陷**: 根路径 `/` 未拦截,用户 home 目录未拦截,是 denylist 而非 allowlist 策略。详见 [问题清单 #5](./box-issues.md)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 工具系统集成
|
||||
|
||||
### 4.1 ToolManager 编排 (`toolmgr.py`)
|
||||
|
||||
```
|
||||
ToolManager.initialize()
|
||||
├─ NativeToolLoader (exec/read/write/edit)
|
||||
├─ PluginToolLoader (插件工具)
|
||||
├─ MCPLoader (MCP Server 工具)
|
||||
└─ SkillAuthoringToolLoader (Skill CRUD)
|
||||
|
||||
工具调用优先级: native → plugin → mcp → skill_authoring
|
||||
```
|
||||
|
||||
### 4.2 Native Tools (`native.py`)
|
||||
|
||||
| 工具 | 是否在 Box 中执行 | 是否访问宿主文件系统 |
|
||||
|------|:---:|:---:|
|
||||
| `exec` | 是 | 否 |
|
||||
| `read` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||
| `write` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||
| `edit` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||
|
||||
**沙箱边界不对称**: 这是刻意的设计权衡 — `read`/`write`/`edit` 绕过沙箱以获得性能(避免容器 I/O 开销),但意味着 LLM 可以直接读写 allowed_roots 下的任何文件。
|
||||
|
||||
exec 对 Skill 的特殊处理: 如果 `workdir` 引用 `/workspace/.skills/<name>`,会创建 `BoxWorkspaceSession`,将 Skill 的 `package_root` 作为 `host_path`,并可选做 Python 环境自举。
|
||||
|
||||
### 4.3 MCP-in-Box (`mcp_stdio.py`)
|
||||
|
||||
`BoxStdioSessionRuntime` 让 MCP stdio 服务器在 Box 容器中运行:
|
||||
|
||||
```
|
||||
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 协议握手
|
||||
```
|
||||
|
||||
配置 (`MCPServerBoxConfig`): `network='on'`(MCP 服务器通常需要网络),`host_path_mode='ro'`(默认只读),`startup_timeout_sec=120`(留时间给 pip install)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 启动与生命周期
|
||||
|
||||
### 5.1 启动顺序 (`build_app.py`)
|
||||
|
||||
```
|
||||
BuildAppStage.run(ap)
|
||||
├─ ... (persistence, models, sessions) ...
|
||||
│
|
||||
├─ BoxService(ap) line 134
|
||||
├─ box_service.initialize() line 135
|
||||
│ └─ connector.initialize()
|
||||
│ ├─ [local] fork box.server subprocess
|
||||
│ └─ [remote] connect WS
|
||||
├─ ap.box_service = box_service line 136
|
||||
│
|
||||
├─ ToolManager(ap) line 138
|
||||
├─ tool_mgr.initialize() line 139
|
||||
│ ├─ NativeToolLoader (检查 box_service.available)
|
||||
│ ├─ PluginToolLoader
|
||||
│ ├─ MCPLoader (Box 可用时,stdio MCP 走沙箱)
|
||||
│ └─ SkillAuthoringToolLoader
|
||||
├─ ap.tool_mgr = tool_mgr line 140
|
||||
│
|
||||
├─ ... (platform, pipeline) ...
|
||||
├─ SkillManager.initialize() line 160
|
||||
└─ ... (RAG, HTTP, plugins) ...
|
||||
```
|
||||
|
||||
BoxService 在 ToolManager **之前**初始化。ToolManager 创建 loader 时检查 `box_service.available`。
|
||||
|
||||
### 5.2 初始化失败处理
|
||||
|
||||
```python
|
||||
# service.py:68-81
|
||||
try:
|
||||
await self._runtime_connector.initialize()
|
||||
self._available = True
|
||||
except Exception as e:
|
||||
self._available = False
|
||||
logger.warning(f"Box runtime unavailable: {e}")
|
||||
```
|
||||
|
||||
**静默降级**: Box 初始化失败不会阻止应用启动,仅导致 4 个 native tool 不暴露给 LLM。与 Plugin 的行为不同(Plugin 失败会抛异常)。
|
||||
|
||||
### 5.3 销毁流程
|
||||
|
||||
```
|
||||
app.dispose()
|
||||
└─ box_service.dispose()
|
||||
├─ connector.dispose()
|
||||
│ ├─ cancel _handler_task
|
||||
│ ├─ cancel _ctrl_task
|
||||
│ └─ terminate subprocess (SIGTERM)
|
||||
└─ loop.create_task(client.shutdown())
|
||||
└─ RPC SHUTDOWN → Box Runtime 清理所有容器
|
||||
```
|
||||
|
||||
Box 额外做了 RPC SHUTDOWN 通知 Runtime 主动清理容器,比 Plugin 的直接杀进程更安全。
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置
|
||||
|
||||
### 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'
|
||||
```
|
||||
|
||||
### docker-compose.yaml
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./data/box:/workspaces # 工作区挂载
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Docker backend
|
||||
```
|
||||
|
||||
### REST API (3 个端点)
|
||||
|
||||
| 端点 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/box/status` | GET | 可用性、Profile、后端信息 |
|
||||
| `/api/v1/box/sessions` | GET | 活跃 session 列表 |
|
||||
| `/api/v1/box/errors` | GET | 最近 50 条错误 |
|
||||
|
||||
**注意**: 前端目前未接入这 3 个 API。
|
||||
152
docs/review/box-issues.md
Normal file
152
docs/review/box-issues.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Box 系统架构问题清单
|
||||
|
||||
> 更新日期: 2026-04-16
|
||||
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||
|
||||
---
|
||||
|
||||
## P0 — 合并前建议修复
|
||||
|
||||
### 1. policy.py 是死代码
|
||||
|
||||
- **位置**: `pkg/box/policy.py` (98 行)
|
||||
- **现状**: `SandboxPolicy`、`ToolPolicy`、`ElevatedPolicy` 三个类已定义,但全项目无任何导入或调用
|
||||
- **影响**: 三层安全策略(sandbox 模式/工具白名单/权限提升)完全未生效,当前的实际策略是 "Box 可用就暴露全部 4 个 native tool,不可用就全部隐藏"
|
||||
- **建议**: 要么删除死代码,要么接入 NativeToolLoader 调用链
|
||||
|
||||
### 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 连接时验证)
|
||||
|
||||
### 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 根路径未拦截
|
||||
|
||||
- **位置**: SDK `box/security.py` `BLOCKED_HOST_PATHS_POSIX`
|
||||
- **现状**: 黑名单中没有 `/`,`host_path="/"` 可通过校验并挂载整个主机文件系统
|
||||
- **建议**: 将 `/` 加入黑名单,或改用白名单策略
|
||||
|
||||
---
|
||||
|
||||
## P1 — 合并后优先跟进
|
||||
|
||||
### 6. Session 数量无上限
|
||||
|
||||
- **位置**: SDK `box/runtime.py` `_get_or_create_session()`
|
||||
- **现状**: `_sessions` dict 无容量限制,恶意或异常调用可创建无限 session
|
||||
- **建议**: 加 `max_sessions` 配置项,达到上限时拒绝新建或清理最老 session
|
||||
|
||||
### 7. Quota 检查存在 TOCTOU
|
||||
|
||||
- **位置**: `pkg/box/service.py` `_enforce_workspace_quota()`
|
||||
- **现状**: 应用层先读磁盘大小再执行命令,两步之间有竞态窗口
|
||||
- **建议**: 短期用 Docker `--storage-opt size=` 做内核级限制;长期用 Redis 原子计数器做预留式配额
|
||||
|
||||
### 8. 全局锁持有期间执行慢操作
|
||||
|
||||
- **位置**: SDK `box/runtime.py` `_get_or_create_session()` — `self._lock` 下调用 `backend.start_session()` (即 `docker run`)
|
||||
- **影响**: `docker run` 可能耗时数秒(含镜像拉取),期间阻塞所有并发请求
|
||||
- **建议**: 在 `_lock` 下仅做状态检查和 session 注册,容器创建在锁外执行
|
||||
|
||||
### 9. Session 清理是机会性的
|
||||
|
||||
- **位置**: SDK `box/runtime.py` `_reap_expired_sessions_locked()` — 仅在 `_get_or_create_session()` 时调用
|
||||
- **影响**: 如果长时间无新 session 请求,过期 session(含容器)不会被清理
|
||||
- **建议**: 加一个独立的 `asyncio.create_task` 定时清理(如每 60s 一次)
|
||||
|
||||
### 10. 缺少 Windows 兼容处理
|
||||
|
||||
- **位置**: `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`
|
||||
- **影响**: 绕过锁和封装,在并发场景下可能读到不一致状态
|
||||
- **建议**: 在 BoxRuntime 上增加公共方法(如 `get_session_managed_process(session_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 检查阻塞事件循环
|
||||
|
||||
- **位置**: `pkg/box/service.py` `_get_workspace_size_bytes()` — 使用同步 `os.scandir` 递归遍历
|
||||
- **影响**: 大工作区可能阻塞 asyncio event loop
|
||||
- **建议**: 用 `asyncio.to_thread()` 包装,或用 `aiofiles` 异步扫描
|
||||
|
||||
### 20. client.py 反序列化不一致
|
||||
|
||||
- **位置**: SDK `box/client.py:118-126` — `execute()` 手动逐字段构建 `BoxExecutionResult`
|
||||
- **对比**: `start_managed_process()` 使用 `model_validate(data)` 自动反序列化
|
||||
- **建议**: 统一使用 `model_validate`
|
||||
|
||||
### 21. 错误类型还原基于字符串前缀匹配
|
||||
|
||||
- **位置**: SDK `box/client.py:59-82` `_translate_action_error()`
|
||||
- **影响**: 如果 server 端错误消息格式变化,client 会回退到通用 `BoxError`
|
||||
- **建议**: 在 ActionResponse 中增加结构化的错误类型字段
|
||||
|
||||
### 22. 前端无 Box 相关 UI
|
||||
|
||||
- **位置**: `web/src/` — 无任何 Box 组件、类型定义或 API 调用
|
||||
- **现状**: 后端有 3 个 REST API(`/api/v1/box/{status,sessions,errors}`)但前端未接入
|
||||
- **建议**: 后续迭代加 Box 状态面板(至少展示可用性、活跃 session、最近错误)
|
||||
103
docs/review/box-test-coverage.md
Normal file
103
docs/review/box-test-coverage.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Box 系统测试覆盖分析
|
||||
|
||||
> 更新日期: 2026-04-16
|
||||
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||
|
||||
---
|
||||
|
||||
## 1. 测试文件清单
|
||||
|
||||
### 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 清理 |
|
||||
|
||||
### SDK 仓库
|
||||
|
||||
| 文件 | 行数 | 测试数 | CI 运行 | 覆盖范围 |
|
||||
|------|------|--------|---------|---------|
|
||||
| `tests/box/test_nsjail_backend.py` | 384 | 18 | 是 | nsjail 可用性、session、arg 构建、资源限制 |
|
||||
|
||||
**总计**: 10 个测试文件, ~4400 行, ~144 个测试; 其中 12 个集成测试在 CI 中不运行。
|
||||
|
||||
---
|
||||
|
||||
## 2. 覆盖良好的区域
|
||||
|
||||
| 区域 | 质量 | 说明 |
|
||||
|------|------|------|
|
||||
| BoxRuntime session 管理 | 优秀 | session 复用、冲突检测、TTL 配置 |
|
||||
| BoxService Profile 系统 | 优秀 | 4 个内置 Profile、locked/unlocked 字段、timeout clamp |
|
||||
| BoxService host mount 安全 | 优秀 | allowed_roots、disallowed_roots、shared_host_root |
|
||||
| BoxService workspace quota | 优秀 | 前置/后置配额检查、超额清理 |
|
||||
| BoxService 输出截断 | 优秀 | 短/精确边界/长输出、独立 stderr |
|
||||
| BoxService 可观测性 | 优秀 | 状态报告、error ring buffer、buffer 上限 |
|
||||
| RPC client/server 协议 | 优秀 | execute/get_sessions/delete/create/conflict error |
|
||||
| BoxRuntimeConnector | 良好 | local/remote 模式、Docker 平台、relay URL、dispose |
|
||||
| BoxWorkspaceSession | 良好 | payload 构建、managed process 路径重写 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 覆盖缺失的区域
|
||||
|
||||
### 3.1 零测试 (Critical)
|
||||
|
||||
| 区域 | 源文件 | 影响 |
|
||||
|------|--------|------|
|
||||
| **`security.py`** | SDK `box/security.py` | `validate_sandbox_security()` 无任何测试。阻止 `/etc`/`/proc`/Docker socket 等危险挂载的安全函数从未被验证 |
|
||||
| **`policy.py`** | `pkg/box/policy.py` | 三层安全策略(SandboxPolicy/ToolPolicy/ElevatedPolicy)无测试(也是死代码) |
|
||||
|
||||
### 3.2 未测试的关键路径
|
||||
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| **Session TTL 过期** | 测试配置了 `session_ttl_sec` 但从未推进时间验证过期清理 |
|
||||
| **并发 session 访问** | 无并发 exec / 并发创建 / race condition 测试 |
|
||||
| **Container backend (Podman/Docker)** | 仅通过集成测试覆盖(CI 不运行),单元测试全用 FakeBackend |
|
||||
| **BoxRuntime shutdown()** | 在 test cleanup 中调用但未验证行为 |
|
||||
| **BoxServerHandler 错误路径** | 畸形请求、未知 action 类型 |
|
||||
| **WS relay** | 仅在集成测试中覆盖(CI 不运行) |
|
||||
| **NsjailBackend _run_nsjail** | 总是被 mock,实际 subprocess 调用未验证 |
|
||||
| **NsjailBackend managed process** | 完全未测试 |
|
||||
| **MCP stdio 完整生命周期** | 依赖安装 → 进程启动 → 健康检查 → 重试 |
|
||||
| **BoxService start_managed_process** | 仅集成测试覆盖 |
|
||||
|
||||
### 3.3 边缘情况缺失
|
||||
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| BoxSpec 校验 | 无效 session_id 格式、超长命令、env 特殊字符 |
|
||||
| BoxExecutionResult | 仅 COMPLETED 和 TIMED_OUT,无 ERROR 状态测试 |
|
||||
| 多后端 fallback | 仅单后端配置,无 Podman 不可用 → fallback Docker 测试 |
|
||||
| Profile YAML 加载 | 测试用硬编码字符串,未从真实 config.yaml 加载 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 集成测试 vs CI 的差距
|
||||
|
||||
CI 仅运行 `tests/unit_tests/`,以下场景**从未在自动化中验证**:
|
||||
|
||||
- 真实容器的创建/执行/销毁
|
||||
- 容器网络隔离(`--network none`)
|
||||
- 容器资源限制生效
|
||||
- Managed process 的 WS 双向 I/O
|
||||
- 孤儿容器清理
|
||||
- Session 删除清理容器
|
||||
- 进程退出检测
|
||||
|
||||
**建议**: 在 CI 中加一个可选的 Docker-in-Docker 集成测试 stage,至少覆盖核心执行路径。
|
||||
162
docs/review/box-tob-analysis.md
Normal file
162
docs/review/box-tob-analysis.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Box 系统 toB 商业化分析
|
||||
|
||||
> 更新日期: 2026-04-16
|
||||
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||
|
||||
---
|
||||
|
||||
## 1. 现有优势
|
||||
|
||||
| 能力 | toB 价值 | 代码位置 |
|
||||
|------|---------|---------|
|
||||
| **沙箱隔离执行** | 企业安全运行不受信代码的基础能力 | SDK `box/backend.py` |
|
||||
| **多后端支持** | 适配不同企业容器基础设施 (Podman/Docker/nsjail) | SDK `box/runtime.py` `_select_backend()` |
|
||||
| **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` |
|
||||
| **静默降级** | Box 不可用不影响其他功能,降低部署门槛 | `pkg/box/service.py:78` `_available=False` |
|
||||
| **孤儿容器清理** | 防止泄漏的容器持续占用资源 | SDK `backend.py` `cleanup_orphaned_containers` |
|
||||
| **网络隔离** | `--network none` 防止数据外泄 | SDK `backend.py` start_session |
|
||||
| **只读根文件系统** | `--read-only` 防止容器被持久篡改 | SDK `backend.py` start_session |
|
||||
| **Host path 白名单** | `allowed_host_mount_roots` 限制可挂载目录 | `pkg/box/service.py` `_validate_host_mount` |
|
||||
|
||||
---
|
||||
|
||||
## 2. toB 差距分析
|
||||
|
||||
### 2.1 安全与合规
|
||||
|
||||
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||
|------|------|---------|--------|
|
||||
| **WS relay 认证** | 无认证,任何人可 attach | 至少 token 认证 | **P0** |
|
||||
| **安全策略** | policy.py 是死代码,实际无细粒度控制 | 工具级 allow/deny、沙箱模式控制 | **P0** |
|
||||
| **审计日志** | 仅内存中 50 条 `_recent_errors` | 持久化审计:谁何时执行了什么、结果如何 | **P0** |
|
||||
| **Host path 校验** | 黑名单策略,`/` 未拦截 | 白名单策略,默认拒绝 | **P1** |
|
||||
| **数据驻留** | 无控制 | GDPR / 等保要求的数据隔离 | **P2** |
|
||||
|
||||
### 2.2 多租户
|
||||
|
||||
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||
|------|------|---------|--------|
|
||||
| **租户隔离** | 无租户概念 | BoxSpec/Profile 绑定 tenant_id | **P0** |
|
||||
| **RBAC** | 仅 token 认证 | admin/operator/viewer 角色权限 | **P0** |
|
||||
| **资源配额** | 单一 workspace quota | 每租户 CPU 时间/内存/并发/执行次数配额 | **P1** |
|
||||
| **Session 隔离** | 所有 session 共享 dict | 按租户分区,互不可见 | **P1** |
|
||||
|
||||
### 2.3 可靠性
|
||||
|
||||
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||
|------|------|---------|--------|
|
||||
| **连接恢复** | 无重连、无心跳 | 自动重连 + 健康检查 | **P0** |
|
||||
| **Session 清理** | 机会性(仅新建时触发) | 定时清理 + 独立 reaper | **P1** |
|
||||
| **水平扩展** | 单 Box Runtime 实例 | 多实例负载均衡 | **P1** |
|
||||
| **优雅降级** | 已有(_available=False) | 已满足基本要求 | 已有 |
|
||||
|
||||
### 2.4 可观测性
|
||||
|
||||
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||
|------|------|---------|--------|
|
||||
| **监控指标** | 无 Prometheus metrics | session 数/执行延迟/资源用量/错误率 | **P1** |
|
||||
| **结构化日志** | Python logging, 无结构化 | JSON 格式日志,含 trace_id/tenant_id | **P1** |
|
||||
| **前端面板** | 无 Box UI | 状态面板:可用性/活跃 session/错误/资源用量 | **P2** |
|
||||
|
||||
---
|
||||
|
||||
## 3. SaaS 部署架构建议
|
||||
|
||||
### 3.1 方案 A: 共享 Box Runtime Pool (快速上线)
|
||||
|
||||
```
|
||||
LangBot Instance ──> Box Runtime (共享)
|
||||
├─ tenant_id 标签隔离
|
||||
├─ Redis 配额计数器
|
||||
└─ Container labels: langbot.tenant_id=xxx
|
||||
```
|
||||
|
||||
- **优点**: 改动最小,加 tenant_id 到 BoxSpec/labels 即可
|
||||
- **缺点**: 容器引擎共享,安全隔离弱
|
||||
|
||||
### 3.2 方案 B: 每租户 K8s Namespace + gVisor (推荐中期)
|
||||
|
||||
```
|
||||
LangBot ──> K8s API
|
||||
├─ namespace: tenant-xxx
|
||||
│ ├─ RuntimeClass: gVisor (runsc)
|
||||
│ ├─ ResourceQuota
|
||||
│ └─ NetworkPolicy
|
||||
└─ namespace: tenant-yyy
|
||||
└─ ...
|
||||
```
|
||||
|
||||
- **优点**: 强隔离(namespace + gVisor),原生 K8s 配额
|
||||
- **缺点**: 需要重写 backend 为 K8s Job,部署复杂度高
|
||||
|
||||
### 3.3 方案 C: K8s Job 直接编排 (长期)
|
||||
|
||||
```
|
||||
LangBot ──> K8s Job per execution
|
||||
├─ 每次执行创建 Job
|
||||
├─ Pod Security Standards
|
||||
├─ 自动调度和资源分配
|
||||
└─ Job TTL Controller 自动清理
|
||||
```
|
||||
|
||||
- **优点**: 最强隔离,天然水平扩展
|
||||
- **缺点**: 冷启动延迟,架构重写
|
||||
|
||||
**推荐演进路径**: A → B → C
|
||||
|
||||
---
|
||||
|
||||
## 4. 配额体系建议
|
||||
|
||||
### 三层配额
|
||||
|
||||
| 层 | 实现 | 作用 |
|
||||
|----|------|------|
|
||||
| **内核层** | Docker `--cpus`/`--memory`/`--storage-opt` | 硬性资源上限,不可绕过 |
|
||||
| **应用层** | Redis 原子计数器 | 并发 session 数/执行次数/CPU 时间预算 |
|
||||
| **计费层** | 月度聚合 | 按租户计费(session-hours/execution-count) |
|
||||
|
||||
### Profile 与套餐映射
|
||||
|
||||
| 套餐 | Profile | locked 字段 | 配额 |
|
||||
|------|---------|------------|------|
|
||||
| Free | `offline_readonly` | network, host_path_mode, rootfs | 10 exec/天, 0.5 CPU, 256MB |
|
||||
| Pro | `default` | (无) | 100 exec/天, 1 CPU, 512MB |
|
||||
| Enterprise | `network_extended` | (按需) | 无限, 2 CPU, 1GB, 自定义镜像 |
|
||||
|
||||
### TOCTOU 配额修复
|
||||
|
||||
当前 `_enforce_workspace_quota` 的 TOCTOU 问题可通过两种方式解决:
|
||||
|
||||
1. **预留式配额** (应用层): Redis `INCRBY` 预扣额度 → 执行 → 成功则扣减,失败则回滚
|
||||
2. **内核级限制** (Docker): `--storage-opt size=500m` 直接限制容器可写层大小
|
||||
|
||||
---
|
||||
|
||||
## 5. 优先实施路线
|
||||
|
||||
### Phase 1 (2-4 周): 安全基线
|
||||
|
||||
- [ ] WS relay 加 token 认证
|
||||
- [ ] 接入或删除 policy.py
|
||||
- [ ] Box 加重连和心跳
|
||||
- [ ] 审计日志持久化(至少写文件/数据库)
|
||||
- [ ] `security.py` 加 `/` 拦截,考虑白名单
|
||||
|
||||
### Phase 2 (4-8 周): 多租户基础
|
||||
|
||||
- [ ] BoxSpec 加 `tenant_id` 字段
|
||||
- [ ] 容器 labels 加 tenant 标识
|
||||
- [ ] Redis 配额计数器(并发/执行次数/时间)
|
||||
- [ ] RBAC 基础框架
|
||||
- [ ] 定时 session reaper
|
||||
|
||||
### Phase 3 (8-16 周): 生产就绪
|
||||
|
||||
- [ ] Prometheus metrics exporter
|
||||
- [ ] 前端 Box 状态面板
|
||||
- [ ] K8s backend 支持 (方案 B)
|
||||
- [ ] 结构化日志 (JSON, trace_id)
|
||||
- [ ] 水平扩展支持
|
||||
222
docs/review/box-vs-plugin-runtime.md
Normal file
222
docs/review/box-vs-plugin-runtime.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Box Runtime vs Plugin Runtime: 连接架构对比
|
||||
|
||||
> 更新日期: 2026-04-16
|
||||
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体差异
|
||||
|
||||
| 维度 | 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) |
|
||||
| **Client 抽象** | Handler 即 API | 独立 `ActionRPCBoxClient` 封装 Handler |
|
||||
| **启用/禁用** | `is_enable_plugin` 开关 | 无开关(可用/不可用由初始化结果决定) |
|
||||
| **初始化失败** | 异常上抛 | 静默降级 `_available=False` |
|
||||
| **Shutdown** | 直接杀进程 | RPC SHUTDOWN → 清理容器 → 再杀进程 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 传输决策
|
||||
|
||||
### Plugin: 3-路决策
|
||||
|
||||
```python
|
||||
# pkg/plugin/connector.py:106-165
|
||||
if get_platform() == 'docker' or use_websocket_to_connect_plugin_runtime():
|
||||
# Docker/WS → ws://langbot_plugin_runtime:5400/control/ws
|
||||
elif get_platform() == 'win32':
|
||||
# Windows → 起子进程(无 pipe) + ws://localhost:5400/control/ws
|
||||
else:
|
||||
# Unix/Mac → StdioClientController(python -m langbot_plugin.cli rt -s)
|
||||
```
|
||||
|
||||
### Box: 2-路决策
|
||||
|
||||
```python
|
||||
# pkg/box/connector.py:56-60
|
||||
if self.manages_local_runtime: # = not configured_runtime_url
|
||||
await self._start_local_stdio() # StdioClientController
|
||||
else:
|
||||
await self._connect_remote_ws() # ws://{host}:{port+1}
|
||||
```
|
||||
|
||||
### 决策矩阵
|
||||
|
||||
| 环境 | Plugin | Box |
|
||||
|------|--------|-----|
|
||||
| Docker | WS → `:5400` | WS → `:{port+1}` (5411) |
|
||||
| Windows 非 Docker | subprocess + WS (`:5400`) | **stdio (可能失败!)** |
|
||||
| Unix/Mac 非 Docker | stdio | stdio |
|
||||
| 手动配置 URL | 通过配置项 | WS → 用户配置的 URL |
|
||||
|
||||
**Box 的 Windows 问题**: 无 Win32 分支,asyncio ProactorEventLoop 不支持 subprocess stdio pipe。Plugin 为此专门做了处理。
|
||||
|
||||
---
|
||||
|
||||
## 3. 连接建立
|
||||
|
||||
### 同步模式差异
|
||||
|
||||
**Plugin**: `new_connection_callback` 内直接 ping + await handler_task,`initialize()` 通过 `create_task()` 异步启动,不阻塞等待连接。
|
||||
|
||||
**Box**: 使用 `asyncio.Event` + `wait_for(timeout=30s)` 模式,`initialize()` 同步等待连接成功或超时。
|
||||
|
||||
### Box stdio 路径
|
||||
|
||||
```
|
||||
connector._start_local_stdio()
|
||||
├─ connected = asyncio.Event()
|
||||
├─ ctrl = StdioClientController(python, ['-m', 'langbot_plugin.box.server', '--port', N])
|
||||
├─ _ctrl_task = create_task(ctrl.run(callback))
|
||||
│ callback:
|
||||
│ handler = Handler(connection) ← 基础 Handler, 无 disconnect_callback
|
||||
│ client.set_handler(handler)
|
||||
│ _handler_task = create_task(handler.run())
|
||||
│ call_action(PING, {}) ← 握手, timeout=15s
|
||||
│ connected.set() ← 通知外层
|
||||
│ await _handler_task ← 阻塞直到断开
|
||||
└─ await wait_for(connected.wait(), 30s) ← 同步等待
|
||||
```
|
||||
|
||||
### Plugin stdio 路径
|
||||
|
||||
```
|
||||
connector.initialize()
|
||||
├─ ctrl = StdioClientController(python, ['-m', 'langbot_plugin.cli', 'rt', '-s'])
|
||||
├─ task = ctrl.run(callback)
|
||||
│ callback:
|
||||
│ disconnect_callback:
|
||||
│ [WS] → runtime_disconnect_callback → 重连
|
||||
│ [stdio] → 仅日志, 不重连
|
||||
│ handler = RuntimeConnectionHandler(conn, disconnect_cb, ap)
|
||||
│ create_task(handler.run())
|
||||
│ handler.ping() ← 握手, timeout=10s
|
||||
│ await handler_task ← 阻塞直到断开
|
||||
├─ create_task(heartbeat_loop()) ← 20s ping loop
|
||||
└─ create_task(task) ← 不等待连接
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 心跳与重连
|
||||
|
||||
### 心跳
|
||||
|
||||
| 维度 | Plugin | Box |
|
||||
|------|--------|-----|
|
||||
| 有心跳? | 是 (`connector.py:69-76`) | **否** |
|
||||
| 间隔 | 20s | N/A |
|
||||
| 失败处理 | 仅 DEBUG 日志,不触发重连 | N/A |
|
||||
| 生命周期 | 整个应用生命周期,跨越重连 | N/A |
|
||||
|
||||
### 重连
|
||||
|
||||
| 维度 | Plugin | Box |
|
||||
|------|--------|-----|
|
||||
| Docker/WS 断开 | `runtime_disconnect_callback` → sleep 3s → re-initialize | Handler loop 退出,**永久不可用** |
|
||||
| WS 连接失败 | 同上 | 存储错误 → `initialize()` 抛异常 → `_available=False` |
|
||||
| stdio 断开 | 仅日志,不重连 | Handler loop 退出,永久不可用 |
|
||||
| 重连退避 | 固定 3s,无 backoff | N/A |
|
||||
|
||||
**Box 断开后的效果链**:
|
||||
1. `handler.run()` 捕获 `ConnectionClosedError`
|
||||
2. `_disconnect_callback is None` → break
|
||||
3. `_handler_task` 完成 → `_make_connection_callback` 返回
|
||||
4. 后续 `client._call()` → `BoxRuntimeUnavailableError`
|
||||
5. Box 功能永久不可用
|
||||
|
||||
---
|
||||
|
||||
## 5. 共享 IO 层
|
||||
|
||||
两者复用同一套 SDK IO 基础设施:
|
||||
|
||||
```
|
||||
Handler ← ABC (runtime/io/handler.py)
|
||||
├── RuntimeConnectionHandler (Plugin 用, LangBot 侧)
|
||||
├── ControlConnectionHandler (Plugin 用, SDK 侧)
|
||||
├── BoxServerHandler (Box 用, SDK 侧)
|
||||
└── 匿名 Handler 实例 (Box 用, LangBot 侧)
|
||||
|
||||
Connection ← ABC
|
||||
├── StdioConnection (stdio: 16KB chunks, 应用层分帧协议)
|
||||
└── WebSocketConnection (WS: 64KB chunks, 原生 WS 分帧)
|
||||
|
||||
Controller ← ABC
|
||||
├── StdioClientController (fork 子进程, pipe stdin/stdout)
|
||||
├── StdioServerController (接管当前进程 stdin/stdout)
|
||||
├── WebSocketClientController (连接 WS 服务端)
|
||||
└── WebSocketServerController (监听 WS 端口)
|
||||
```
|
||||
|
||||
共享的核心机制:
|
||||
- `call_action()` / `call_action_generator()` — RPC 调用/流式调用
|
||||
- `ActionRequest` / `ActionResponse` — 请求/响应协议
|
||||
- `seq_id` 关联 — 并发请求复用单连接
|
||||
- `CommonAction.PING` — 两者都用于初始握手
|
||||
- 文件传输 (`send_file`) — Plugin 用,Box 不用
|
||||
|
||||
---
|
||||
|
||||
## 6. 端口方案
|
||||
|
||||
| 服务 | Plugin | Box |
|
||||
|------|--------|-----|
|
||||
| Action RPC (stdio) | stdin/stdout | stdin/stdout |
|
||||
| Action RPC (WS) | `:5400` | `:{port+1}` (默认 5411) |
|
||||
| 辅助服务 | debug WS `:5401` | managed process WS relay `:5410` |
|
||||
|
||||
**Box 特点**: 即使在 stdio 模式,也额外在 `:5410` 启动 aiohttp WS 服务用于 managed process attach。Plugin 在 stdio 模式不开额外端口。
|
||||
|
||||
---
|
||||
|
||||
## 7. 销毁对比
|
||||
|
||||
### Plugin
|
||||
|
||||
```python
|
||||
dispose():
|
||||
if stdio: ctrl.process.terminate()
|
||||
_dispose_subprocess() # Windows 子进程
|
||||
heartbeat_task.cancel()
|
||||
```
|
||||
|
||||
### Box
|
||||
|
||||
```python
|
||||
connector.dispose():
|
||||
_handler_task.cancel()
|
||||
_ctrl_task.cancel()
|
||||
_subprocess.terminate()
|
||||
|
||||
service.dispose():
|
||||
connector.dispose()
|
||||
loop.create_task(client.shutdown()) # RPC SHUTDOWN → 清理所有容器
|
||||
```
|
||||
|
||||
Box 的 RPC SHUTDOWN 确保容器被正确停止,不会成为孤儿。Plugin 直接杀进程。
|
||||
|
||||
---
|
||||
|
||||
## 8. 改进建议
|
||||
|
||||
### P0
|
||||
|
||||
1. **Box 加重连**: 在 `_make_connection_callback` 中设置 `disconnect_callback`,WS 模式 sleep 3s → re-initialize
|
||||
2. **Box 加心跳**: 30s 间隔 ping loop,参考 `PluginRuntimeConnector.heartbeat_loop()`
|
||||
|
||||
### P1
|
||||
|
||||
3. **Box 加 Windows 支持**: 像 Plugin 一样加 Win32 分支 (subprocess + WS)
|
||||
4. **考虑 Box 继承 ManagedRuntimeConnector**: 复用 `_start_runtime_subprocess`/`_wait_until_ready`/`_dispose_subprocess`
|
||||
5. **两者都加 WS 认证**: 至少 token 认证
|
||||
|
||||
### P2
|
||||
|
||||
6. **Plugin 重连加退避**: 固定 3s 无 backoff 可能造成日志洪水,建议指数退避
|
||||
7. **统一连接管理模式**: Event-based (Box) vs direct-await (Plugin),考虑收敛为一种
|
||||
Reference in New Issue
Block a user