上一篇我们看了插件桥——一个跑在独立进程里、通过 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)」并收回结果。

它由两个文件构成清晰的两层:

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 上优雅降级。

二、连接 Box:又一次「双形态」

2.1 传输策略与插件桥如出一辙

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

这个「是否共享文件系统」的区别,不是无关紧要的实现细节——它后面会直接决定技能挂载和附件传输走哪条路。

2.2 心跳与断线退避

连上后启动 20 秒一次的心跳(_HEARTBEAT_INTERVAL_SEC = 20,connector.py:30),靠 CommonAction.PING 探活(connector.py:162)。一旦连接掉了,连接回调触发 runtime_disconnect_callback,最终落到 BoxService 的指数退避重连: