跳转至

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 的容器隔离仍确保恶意或异常的代码无法逃逸到主机。

阅读下一步

执行沙箱是工具系统的底层支撑,理解它的隔离机制后,建议继续深入以下相关主题: