Execution cn
Jianmu 的执行沙箱层为工具调用和独立 job 运行提供了可切换的隔离环境:LocalSandbox 在进程内直接执行,追求零延迟的开发体验;DockerSandbox 通过容器实现网络隔离、文件系统只读挂载、权限裁剪等安全边界,适合生产环境或运行不受信任的代码。两种后端通过统一的 SandboxRunnerProtocol 协议暴露一致接口,由 ToolRunner 外观在运行时根据约束配置透明选择。
架构概览:三层协同的沙箱运行时¶
执行子系统由三层结构组成。顶层是 ToolRunner 外观,负责接收工具调用请求、发射生命周期事件(execution.started → execution.completed / execution.failed)并委托到底层沙箱。中间层是 SandboxRunnerProtocol 协议定义的两方法契约(run_tool、run_job),LocalSandbox 和 DockerSandbox 各自实现该契约。底层是 Docker 容器内的运行时入口点(tool_entry.py),它在被隔离的容器进程中重新加载工具模块并执行。
graph TB
subgraph "调用方"
TE[ToolExecutor 节点]
SK[SkillNode 节点]
GD[Guard 检查与审批]
end
subgraph "ToolRunner 外观层"
TR[ToolRunner]
EC[ExecutionEventContext]
EB[RuntimeEventBus]
end
subgraph "沙箱后端"
SP[SandboxRunnerProtocol]
LS[LocalSandbox]
DS[DockerSandbox]
end
subgraph "Docker 子层"
DTR[DockerToolRunner]
PDS[_PersistentDockerSession]
DTL[DockerToolLoader]
end
subgraph "容器内运行时"
TE2[tool_entry.py]
end
TE --> TR
SK --> TR
TR --> EB
TR --> SP
SP --> LS
SP --> DS
DS --> DTR
DTR --> PDS
DTR --> DTL
DTR -.->|docker run| TE2
GD -.->|拦截检查| TR
ToolRunner 的工厂方法 from_execution_constraints() 从 ExecutionConstraints 或全局配置 execution.tool_runner 解析目标模式("local" / "docker"),返回装配好的 runner 实例。当约束指定 mode="docker" 时,ToolRunner 内部持有 DockerSandbox,后者组合 DockerToolRunner。
共享数据模型:ExecutionConstraints 与 JobResult¶
两个核心数据类贯穿整个执行层。ExecutionConstraints 描述运行时限制与后端偏好,JobResult 封装沙箱化脚本执行的收集结果。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mode |
str \| None |
None |
执行后端:"local" 或 "docker";None 时回退全局配置 |
scope |
str \| None |
None |
约束作用范围标签,如 "tools"、"skill"、"all" |
docker_image |
str \| None |
None |
Docker 镜像覆盖,优先级高于全局 execution.docker.image |
network |
str \| None |
None |
Docker 网络模式,安全默认为 "none" 断网 |
memory |
str \| None |
None |
Docker 内存限制(如 "512m") |
cpus |
float \| None |
None |
Docker CPU 核数限制 |
timeout_s |
float \| None |
None |
执行超时秒数 |
tmpfs_noexec |
bool |
True |
Docker tmpfs 是否禁用执行权限 |
JobResult 则是纯粹的产出容器,包含 stdout(字符串)、stderr(字符串)、exit_code(整数)和 artifacts(dict[str, bytes],工作目录产生的输出文件映射)。
LocalSandbox:进程内直连执行¶
LocalSandbox 是默认后端(execution.tool_runner: local),它在当前 Python 进程中直接调用工具实例的 execute() 方法,完全不引入进程边界。
run_tool:零开销工具执行¶
工具调用直接在当前进程中完成——LocalSandbox.run_tool() 仅一行 await tool.execute(args or {})。这使内建工具(Python REPL、文件读写、HTTP 请求等)的调用无序列化开销,适合开发和调试场景。
run_job:子进程脚本隔离¶
run_job 创建临时目录,将脚本写入 main.py,把 input_files 字典物化为目录中的文件(路径经过 _safe_relative_path 安全检查,拒绝绝对路径和 .. 遍历逃逸),然后通过 asyncio.create_subprocess_exec 启动子进程运行。超时和取消信号均会触发 proc.kill() + proc.wait() 终止子进程。执行完成后遍历工作目录收集除 main.py 和输入文件以外的所有产物作为 artifacts。
DockerSandbox:容器级安全隔离¶
DockerSandbox 通过 docker run 或 docker exec 在容器内执行代码,提供以下安全边界:
--read-only:根文件系统只读挂载--cap-drop=ALL:移除所有 Linux capabilities--security-opt no-new-privileges:禁止 setuid 等权限提升--tmpfs /tmp:rw,nosuid,noexec,size=64m:临时文件系统禁用可执行权限--network none(默认):完全断开网络(工具配置默认值)
双模式容器生命周期¶
Docker 执行支持两种容器管理策略,由 ExecutionDockerConfig.reuse_container 控制:
一次性容器(默认 reuse_container: false):每次工具调用都启动一个新的 docker run --rm 容器。--cidfile 参数用于追踪容器 ID,确保在超时、取消或异常退出时能执行 docker kill + docker rm -f 清理。cidfile 及其父临时目录在每次调用后通过 _cleanup_cidfile_bundle 移除。
持久化会话(reuse_container: true):_PersistentDockerSession 管理一个长期运行的 docker run -d 辅助容器(通过 tail -f /dev/null 保活)。后续工具调用复用该容器,通过 docker exec 直接注入命令。会话绑定到特定技能/工作空间目录(以标签 jianmu-session-scope 区分),进程退出时通过 atexit.register(_cleanup_all_persistent_containers) 统一清理。
DockerToolRunner:工具发现与调用¶
DockerToolRunner 负责在容器内完成工具的发现和执行两个阶段。
发现阶段(list_specs()):设置环境变量 jianmu_TOOL_ACTION=list,运行 python -m jianmu.execution.docker_runtime.tool_entry。容器入口点加载挂载的工具模块,收集所有 Tool 实例并输出 JSON 格式的 spec() 列表。返回的规格被封装为 DockerToolProxy 对象——它们实现了 Tool 接口,但 run() 方法会将调用转发回 DockerToolRunner.run()。
执行阶段(run()):设置 jianmu_TOOL_ACTION=run 和 jianmu_TOOL_NAME=<name>。对于内建工具,直接通过名称路由;对于自定义工具,_resolve_tool_module() 通过 inspect.getsourcefile() 定位工具源码文件,将其路径转换为容器内的挂载路径(/skill/<rel> 或 /workspace/<rel> 或 /jianmu_tool_src/<file>)。
工具参数通过环境变量 jianmu_TOOL_ARGS 传递。当参数序列化后超过 tool_args_max(默认 60000 字符)时,参数会被写入临时 JSON 文件并通过 -v 挂载入容器,路径写入 jianmu_TOOL_ARGS_FILE 环境变量。
容器内运行时入口点¶
一个入口点模块在隔离的容器进程中运行,构成沙箱化执行的"内部回路"。
tool_entry.py 的 main() 根据 jianmu_TOOL_ACTION 分发到 list(返回所有工具的 JSON spec)或 run(解析参数、执行工具、输出 {"ok": True/False, "result": ...})。build_tool_map() 先注册内建工具(通过 ToolSet.resolve(builtin_names)),再用自定义工具覆盖同名键——自定义工具始终优先于内建工具。
ToolRunner:统一执行外观与事件体系¶
ToolRunner 是工具执行的最终外观,它将沙箱后端、超时控制、事件发射三者编织为一个统一的调用流。
sequenceDiagram
participant TE as ToolExecutor 节点
participant TR as ToolRunner
participant EB as RuntimeEventBus
participant SB as Sandbox (Local/Docker)
TE->>TR: run(tool, args, event_context)
TR->>EB: emit "execution.started"
alt 配置了 timeout_s
TR->>SB: asyncio.wait_for(run_tool(tool, args), timeout)
else 无超时
TR->>SB: run_tool(tool, args)
end
alt 成功
SB-->>TR: result
TR->>EB: emit "execution.completed"
TR-->>TE: result
else TimeoutError
TR->>EB: emit "execution.failed" (timeout)
TR-->>TE: TimeoutError
else 其他异常
TR->>EB: emit "execution.failed" (error)
TR-->>TE: re-raise
end
事件上下文 ExecutionEventContext 携带 run_id、node_name 和 tool_call_id,这些字段会被注入到每一个 execution.* 事件中,使遥测系统能够关联单次工具调用从启动到完成(或失败)的完整生命周期。
ToolRunner.from_execution_constraints() 是推荐的外部入口:它从 ExecutionConstraints 和全局 ExecutionConfig 中读取模式、超时、镜像等配置,自动装配对应的沙箱实例。调用方(如 ToolExecutor 节点)无需关心底层是 Local 还是 Docker。
配置体系:三层优先级与安全默认值¶
执行行为的配置遵循三层优先级:ExecutionConstraints(节点级)> 全局 jianmu.yaml(项目级)> 代码默认值。
| 配置路径 | 类型 | 默认值 | 优先级 |
|---|---|---|---|
execution.tool_runner |
str |
"local" |
全局默认,被 constraints.mode 覆盖 |
execution.timeout_s |
float \| None |
None |
全局默认,被 constraints.timeout_s 覆盖 |
execution.max_retries |
int |
3 |
工具执行失败重试次数 |
execution.retry_backoff |
float |
0.2 |
重试退避间隔秒数 |
execution.tool_args_max |
int \| None |
None (env: 60000) |
工具参数环境变量最大字节数 |
execution.docker.image |
str |
"jianmu-tools:latest" |
Docker 执行镜像 |
execution.docker.timeout |
float |
60.0 |
Docker 执行默认超时 |
execution.docker.network |
str |
"none" |
默认断网,安全第一 |
execution.docker.reuse_container |
bool |
false |
是否复用持久化容器 |
execution.docker.env_allowlist |
list[str] |
[] |
注入容器的环境变量白名单 |
execution.docker.env_allowprefix |
list[str] |
[] |
注入容器的环境变量前缀白名单 |
在 jianmu.yaml 中,执行配置块结构如下: |
execution:
max_retries: 3
retry_backoff: 0.2
tool_runner: local
docker:
image: jianmu-tools:latest
timeout: 60.0
network: none
Constraints 数据类(jianmu/config/constraints.py)作为节点级入口,其 execution 字段在构造时通过 _coerce_execution_constraints() 将字典自动转换为 ExecutionConstraints 实例,支持字符串到浮点的时间解析(如 "30s"、"2m" → 120.0)。
隔离能力对比与适用场景¶
| 维度 | LocalSandbox | DockerSandbox |
|---|---|---|
| 进程隔离 | 子进程(仅 run_job);工具在同进程 |
独立容器,完全进程隔离 |
| 文件系统 | 临时目录(tempfile.mkdtemp),执行后清理 |
--read-only 根文件系统 + tmpfs /tmp |
| 网络隔离 | 无限制 | --network none 断网(默认可配) |
| 权限控制 | 继承当前用户权限 | --cap-drop=ALL + no-new-privileges |
| 资源限制 | 仅超时控制 | 内存、CPU、超时三重限制 |
| 启动延迟 | ~0ms | 容器启动 ~0.5-2s(持久化会话可消除) |
| 环境依赖 | 依赖主机已安装的 Python 包 | 依赖 Docker 镜像内预装的包 |
| 适用场景 | 开发调试、可信内建工具、快速迭代 | 生产环境、不受信任代码、多租户隔离 |
推荐选择策略¶
- 开发与原型阶段:使用
LocalSandbox(默认),享受零延迟和即时报错反馈。 - 生产环境:切换到
docker模式,配合jianmu-tools:latest镜像获得真正的安全边界。 - 高频工具调用:启用
reuse_container: true以消除容器冷启动开销(仅docker exec延迟)。 - 安全敏感场景:保持
network: "none"和tmpfs_noexec: true,并通过env_allowlist精确控制泄漏到容器的环境变量。
与 Guard 体系的协同¶
执行沙箱与 Guard 体系 协同工作,形成"先检查后执行"的安全流水线。ToolExecutor 节点在调用 ToolRunner.run() 之前,先通过 GuardEnforcer 运行策略检查、频率限制、预算控制和确认检查。只有当 Guard 返回 ALLOW 或 CONFIRM(用户确认后),工具才会被实际提交到沙箱执行。沙箱本身提供的是"最后一道防线"——即使 Guard 放行了一个工具调用,DockerSandbox 的容器隔离仍确保恶意或异常的代码无法逃逸到主机。
阅读下一步¶
执行沙箱是工具系统的底层支撑,理解它的隔离机制后,建议继续深入以下相关主题:
- 工具抽象:Tool 基类、@tool 装饰器与 ToolSet 工具集装配:了解沙箱执行的"被调用方"——工具如何定义、注册和装配。
- Guard 体系:工具策略检查、预算控制、频率限制与确认拦截:了解沙箱执行前的安全审查流水线。
- Skill 定义与目录:SKILL.md 解析、行为树技能与 Prompt 技能:了解技能在执行沙箱之上的建模方式。
- 内建工具:计算器、Python REPL、文件读写、HTTP、搜索:了解可在两种沙箱中直接使用的内建工具清单。