上一篇我们看了插件桥——一个跑在独立进程里、通过 RPC 与主进程通信的扩展子系统。本篇的主角 Box 与它是「孪生兄弟」:同样是独立进程、同样是 action RPC、同样有 stdio/WebSocket 双形态。区别在于 Box 解决的是一个更尖锐的问题——怎么让 LLM Agent 真的执行任意代码,又不把宿主机交出去。读懂这一篇,你就理解了 LangBot 的 Agent 能力为什么敢「真跑 shell」。
第五篇里我们见过 ToolManager 聚合的四类工具源,其中 Native 工具 里就有 exec/read/write/edit 这类「在沙箱里操作文件系统、跑命令」的能力。问题是:LLM 是不可信的——它可能被 prompt 注入、可能产出 rm -rf /、可能想读取宿主机密钥。直接在 LangBot 进程里 subprocess.run 等于把整台机器交给模型。
Box 子系统(src/langbot/pkg/box/)就是这道安全边界。它把「执行代码」这件事隔离到一个独立的沙箱运行时里,由后端(Docker / nsjail / E2B)提供真正的隔离,LangBot 只通过 RPC 下发一份「执行规格(spec)」并收回结果。
它由两个文件构成清晰的两层:
connector.py — BoxRuntimeConnector:底层传输。负责连上 Box 运行时(选传输、握手、心跳、断线),不关心业务。service.py — BoxService:高层 API。负责构建执行规格、配额校验、会话隔离、附件编排、技能挂载——所有「LangBot 这一侧的策略」都在这里。flowchart LR
subgraph LB["LangBot 进程"]
TOOL["Native exec 工具<br>(第五篇 ToolLoader)"]
SVC["BoxService<br>策略层"]
CONN["BoxRuntimeConnector<br>传输层"]
end
subgraph BOXRT["Box 运行时(独立进程)"]
CLIENT["ActionRPCBoxClient"]
BACK["后端: Docker / nsjail / E2B"]
end
TOOL -->|execute_tool| SVC -->|build_spec + execute| CONN
CONN <-->|"action RPC<br>stdio / WebSocket"| CLIENT --> BACK
一个贯穿全篇的「主开关」:
box.enabled。它是顶层配置(不是box.local.*),默认True,关闭是显式 opt-in(src/langbot/pkg/box/service.py:1111)。一旦关掉,initialize()直接短路——不连远程、不 fork 子进程,并且如注释所言,沙箱类工具、skill 增改、stdio 形态的 MCP 全部一并禁用(service.py:86)。所有消费方都必须 gate 在available上优雅降级。
BoxRuntimeConnector 的类注释把传输决策写得很直白(connector.py:89)——这和第六篇插件桥的逻辑是镜像的:
# src/langbot/pkg/box/connector.py:123
def uses_websocket(self) -> bool:
"""Whether the connector should use WebSocket to reach the Box runtime."""
return bool(
self.configured_runtime_endpoint # 显式 runtime.endpoint
or platform.get_platform() == 'docker' # 容器内:Box 是独立容器
or platform.use_websocket_to_connect_box_runtime()
)
据此 initialize() 三路分发(connector.py:147):
flowchart TD
A["initialize()"] --> B{uses_websocket?}
B -->|"否<br>Unix/macOS 个人部署"| C["_start_local_stdio<br>fork 子进程, stdio 管道<br>共享文件系统"]
B -->|是| D{Windows 且无显式 endpoint?}
D -->|是| E["_start_subprocess_then_ws<br>(Windows 无异步 stdio)"]
D -->|"否<br>Docker / --standalone-box / 远程"| F["_connect_remote_ws<br>连独立/远程 Box 进程"]
C --> G["启动 20s 心跳"]
E --> G
F --> G
_start_local_stdio 把自己当父进程 fork 一个 lbp box -s 子进程,走 stdio 管道(connector.py:179)。关键副作用:父子进程共享同一个文件系统视图。--standalone-box、或显式 box.runtime.endpoint,都走 WebSocket 连一个独立的 Box 运行时(connector.py:239),Box 拥有自己的文件系统。这个「是否共享文件系统」的区别,不是无关紧要的实现细节——它后面会直接决定技能挂载和附件传输走哪条路。
连上后启动 20 秒一次的心跳(_HEARTBEAT_INTERVAL_SEC = 20,connector.py:30),靠 CommonAction.PING 探活(connector.py:162)。一旦连接掉了,连接回调触发 runtime_disconnect_callback,最终落到 BoxService 的指数退避重连: