DeerFlow Lark CLI Broker(Pattern B):用 Sidecar 镜像把飞书凭据从 Agent 沙箱中隔离出去
2026/9/5 18:24:11 网站建设 项目流程

DeerFlow Lark CLI Broker(Pattern B):用 Sidecar 镜像把飞书凭据从 Agent 沙箱中隔离出去

【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow

在 DeerFlow 的 K8s 沙箱部署中,Agent 需要在沙箱里调用lark-cli完成飞书集成,而每个用户的appSecret与 OAuth token 又绝不能出现在沙箱文件系统里——否则一个被提示注入的 Agent 就能直接cat走凭据。本文以 docker/lark-cli-broker/README.md 为主线,完整拆解 Pattern B(Broker)方案的镜像构建、双模入口、shim 转发机制、provisioner 接线、loopback HTTP 契约与子命令拒绝清单,并结合 lark_broker.py 与 provisioner/app.py 的源码实现说明其底层原理与生产加固要点。

1. 为什么需要 Broker:Pattern A 的沙箱凭据暴露面

背景来自 issue #4338 的修复方案。DeerFlow 此前采用Pattern A:把lark-cli二进制预置进沙箱运行时目录,但每用户的凭据目录仍然以卷挂载方式进入沙箱容器——config目录里有长期有效的appSecretdata目录里有 OAuth token。由于 Agent 在沙箱内拥有bash工具,任何拿到 shell 的进程(包括被恶意网页内容提示注入的 Agent)都能直接读取这些明文文件。

Pattern B 的核心思路是:凭据留在外面,命令面留在里面。一个长驻的 sidecar 容器持有真实的lark-cli二进制和凭据目录,只通过 loopback HTTP 提供“命令执行”这一最小接口;沙箱里放一个极小的lark-clishim(垫片),把 argv/stdin 原样转发给 sidecar。结果是:

  • 原始的appSecret/ OAuth token 文件在沙箱文件系统中根本不存在,被攻陷的沙箱无法cat/外泄它们;
  • 但任何合法的lark-cli子命令仍然可以正常执行——对用户和 Agent 来说命令面完全无感。

这正是 lark_broker.py 模块开头文档化的设计目标:broker 侧半部分是一个“拥有lark-cli+ 凭据、只暴露命令面的长驻进程”,且整个模块仅用 Python 3 标准库实现,这样同一个模块既能跑在最小化的 broker sidecar 镜像里,也无需安装任何第三方依赖。

2. 一个镜像、两种角色:install-shimserve

Broker 镜像通过第一个 CLI 参数分发两种模式(见 entrypoint.sh 中的case分发逻辑):

模式角色行为
install-shim <dest>init 容器把 launcher + Python shim + 运行时标记文件.deerflow-lark-cli-runtime.json(内容为{"version": ..., "kind": "shim"})写入共享emptyDir<dest>,默认/mnt/integrations/lark-cli/runtime),然后以退出码 0 结束
serve(默认CMDsidecar127.0.0.1:8788上运行 broker HTTP 服务,加载真实lark-cli,凭据环境变量指向 sidecar 独占的/var/lark/{config,data}挂载

两种模式最终都收敛到同一个标准库模块:entrypoint 只是exec python3 /opt/broker/lark_broker.py [install-shim ...],镜像里没有其它运行时代码。

2.1install-shim:产出与 Pattern A 相同的布局

install-shim的实现是 lark_broker.py 中的install_shim(),它向目标目录写入三个东西:

  1. bin/lark-cli—— 位于PATH上的可执行文件,实际是一个/bin/shlauncher;
  2. bin/lark-cli-shim.py—— 真正干活的 Python shim 本体,与 launcher 同目录;
  3. .deerflow-lark-cli-runtime.json—— 运行时标记文件,kind字段为"shim",让运行时校验器知道linux-*真实二进制有意缺席(真实二进制在 sidecar 里),避免误判为安装失败。

这个布局与 Pattern A 的 init 镜像产物完全同构,因此沙箱里lark-cli出现在lark_cli_env_overlay(sandbox_paths=True)所指向PATH的同一位置,上层代码无需为两种模式分叉。

2.2 Launcher 与 Shim 分离:为什么bin/lark-cli不是 Python 脚本

README 强调了一个工程细节:PATH上的可执行文件bin/lark-cli是一个/bin/shlauncher,它负责解析出 Python 3 解释器后exec同目录下的 shim 本体bin/lark-cli-shim.py。分离的动机在 lark_broker.py 的LARK_CLI_BROKER_LAUNCHER_TEMPLATE常量中写得很清楚:

  • shim 本体需要发 HTTP 请求,必须是 Python;但 Broker 模式是opt-in的,如果直接把 shim 做成#!/usr/bin/env python3脚本,任何PATH上没有python3的沙箱镜像都会让每次lark-cli调用变成晦涩的 ENOEXEC/exit 127,而且这种故障可能在 CI 中漏网、只在运维侧暴露;
  • launcher 只用 shell 内建命令(command -v逐个探测python3python),即使沙箱PATH为空、仅靠DEERFLOW_LARK_BROKER_PYTHON环境变量钉住解释器路径也能工作;
  • 找不到解释器时,launcher大声失败:打印可操作的错误信息(提示设置DEERFLOW_LARK_BROKER_PYTHON)并以127退出,而不是留一个不透明的 ENOEXEC;
  • launcher 里 shim 本体的路径是安装时烘焙进去的绝对路径render_launcher_script()把占位符@@LARK_CLI_BROKER_SHIM_PATH@@替换为实际路径)。因为从PATH上直接运行lark-cli$0只是裸命令名、没有目录信息,靠$0做同目录查找会失败;而安装目录是 init 容器与沙箱共享的稳定挂载,绝对路径在两侧都有效。

标准all-in-one-sandbox镜像自带 Python 3,所以默认路径无需任何额外配置。两个脚本模板都以进程内常量LARK_CLI_BROKER_LAUNCHER_TEMPLATE/LARK_CLI_BROKER_SHIM_SCRIPT)作为唯一事实来源,由 broker 镜像构建时的install-shim生成——镜像里的副本永远不可能与 Gateway 进程内的副本漂移

2.3 Shim 本体的转发语义

shim 脚本本身(LARK_CLI_BROKER_SHIM_SCRIPT,lark_broker.py)逻辑非常克制:

  1. 读取DEERFLOW_LARK_BROKER_URL(默认http://127.0.0.1:8788),把sys.argv[1:]与 base64 编码的 stdin 打包成 JSON,POST /v1/exec
  2. 把 broker 返回的stdout_b64/stderr_b64原样写回 stdout/stderr,并以 broker 返回的exit_code退出;
  3. 任何传输层故障(broker 不可达、HTTP 错误、JSON 解析失败)都以非零码大声失败,保证 broker 宕机永远不会看起来像一次“成功”的lark-cli执行——HTTP 错误还会把 broker 返回的error字段透传出来(broker rejected request (HTTP 500: ...))。

3. 构建 Broker 镜像

构建上下文是仓库根目录(broker 模块位于backend/之下),构建命令见 README:

docker build -t deer-flow/lark-cli-broker:v1.0.65 \ --build-arg LARK_CLI_VERSION=v1.0.65 \ -f docker/lark-cli-broker/Dockerfile .

镜像 tag 应当编码 lark-cli 版本,使其可以独立于上游all-in-one-sandbox镜像单独升级。Dockerfile 的关键事实:

  • builder 阶段debian:bookworm-slim):复用 Pattern A 的共享脚本 build-runtime.sh 下载官方larksuite/cli的 Linux 发布二进制,并做SHA-256 校验后放入/opt/lark-cli——sidecar 拿到的是与 Pattern A 完全同一下载+校验路径产出的真实二进制;支持APT_MIRROR构建参数用于镜像源替换;
  • 最终阶段python:3.12-slim):拷贝构建好的二进制、单文件模块lark_broker.py与 entrypoint,并预置环境变量:
环境变量默认值作用
DEERFLOW_LARK_BROKER_CLI/opt/lark-cli/bin/lark-clibroker 实际调用的二进制路径
LARKSUITE_CLI_CONFIG_DIR/var/lark/configsidecar 侧凭据 config 目录
LARKSUITE_CLI_DATA_DIR/var/lark/datasidecar 侧 OAuth token 等 data 目录
DEERFLOW_LARK_BROKER_HOST/_PORT127.0.0.1/8788loopback 绑定地址
LARK_CLI_RUNTIME_DEST/mnt/integrations/lark-cli/runtimeinstall-shim缺省写入位置

CI 会以多架构(linux/amd64,linux/arm64)发布到ghcr.io/<owner>/deer-flow-lark-cli-broker:<lark-cli-version>,由lark-cli-images.yamlworkflow 触发(带lark_cli_version输入运行,或推送lark-cli-v*tag)。这条流水线与 DeerFlow 主v*发布解耦,因为该镜像跟踪的是上游larksuite/cli的版本节奏。

4. 接入 Provisioner:Opt-in、容器拓扑与就绪信号

Broker 模式是可选开启(opt-in)的,默认关闭。启用方式是发布镜像并在 provisioner 上配置LARK_CLI_BROKER_IMAGE。源码中 app.py 的默认值是空字符串,_lark_cli_broker_enabled()要求镜像配置与请求标志同时成立

LARK_CLI_BROKER_IMAGE = os.environ.get("LARK_CLI_BROKER_IMAGE", "") def _lark_cli_broker_enabled(provision_lark_cli_broker: bool) -> bool: return bool(LARK_CLI_BROKER_IMAGE) and provision_lark_cli_broker

因此镜像未发布或未配置时就是 no-op——走 Pattern A / 遗留路径,行为零变化。当配置生效且 Gateway 在建沙箱请求中携带provision_lark_cli_broker时,provisioner 会追加以下资源:

资源说明
lark-cli-runtimeemptyDirinit 容器与沙箱共享;沙箱侧为只读挂载
lark-cli-shim-initinit 容器运行 broker 镜像的install-shim模式,把 shim 预置进共享卷
lark-cli-brokersidecar运行serve模式,per-user 的config(只读)/data(可写)凭据挂载只进 sidecar;另有嵌套的locks挂载保持可写
沙箱容器只拿到 runtime 卷的只读挂载 +DEERFLOW_LARK_BROKER_URL环境变量,没有任何config/data挂载

几个值得注意的实现细节(均可在 app.py 中核对):

  • _build_lark_cli_init_containers()(L839-L883)中,broker 启用时返回的是lark-cli-shim-init容器(args=["install-shim", <runtime 路径>]privileged=False),取代Pattern A 的lark-cli-init容器——即“两者同时配置时 Broker 模式优先(supersedes)”;
  • _build_lark_cli_broker_sidecars()(L886-L948)把config/locks/data三个挂载只挂到 sidecar 的/var/lark/*路径上(PVC 模式下带 per-mountsub_path隔离),并注入LARKSUITE_CLI_CONFIG_DIR/LARKSUITE_CLI_DATA_DIR指向 sidecar 内路径;如果 provisioner 侧配置了DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS,还会把它转发为 sidecar 环境变量;
  • 沙箱侧的环境覆盖由 lark_cli.py 的lark_cli_env_overlay(user_id, broker=True)生成:只包含PATH(把 runtime 的bin/排到最前)和DEERFLOW_LARK_BROKER_URL绝不携带LARKSUITE_CLI_CONFIG_DIR/DATA_DIR——这是“明文凭据路径不出现在沙箱环境里”的代码级保证。

4.1 就绪信号:/api/capabilities与 Lark 集成状态

provisioner 通过GET /api/capabilities上报自身能力(app.py):

{"lark_cli_init_image": true, "lark_cli_broker_image": true}

Gateway 据此把 Lark 集成的沙箱运行时就绪状态暴露为/api/integrations/lark/status中的sandbox_runtime_mode: "broker"信号。其目的在源码注释里写得很直白:让前端 UI 变绿的同时,不能掩盖聊天时的command not found——即 UI 状态与运行时真实能力对齐。

5. Broker HTTP 契约(仅 loopback)

Broker 服务只用标准库ThreadingHTTPServer实现,绑定 loopback。在 K8s 中沙箱与 sidecar 共享 Pod 网络命名空间,所以127.0.0.1能打到 sidecar,而Pod 外的任何容器都不可达——这本身是一道网络层隔离。

5.1POST /v1/exec

  • 请求体{"args": [...], "stdin_b64": "..."},其中args必须是字符串列表,broker 侧会做类型校验,非法请求返回400 {"error": "invalid request"}
  • 响应体{"exit_code", "stdout_b64", "stderr_b64", "truncated"}
  • 执行语义在 lark_broker.py 的run_lark_cli()中:argsargv 列表 +shell=False交给subprocess.run,所以沙箱侧提供的任何参数不可能被 shell 解释注入第二条命令
  • 凭据环境变量由 broker 通过BrokerConfig.credential_env()单方面注入(含LARKSUITE_CLI_CONFIG_DIRLARKSUITE_CLI_DATA_DIR以及LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1/NO_SKILLS_NOTIFIER=1),客户端无法覆盖——沙箱进程不能把lark-cli指向别的 profile;
  • 超时返回exit_code 124+lark-cli: broker timed out;二进制缺失返回127;其它意外异常(OSError等)统一收敛为500 {"error": "broker exec failed"},保证 shim 侧总能拿到结构化响应而不是不透明的传输失败。

5.2 防御纵深:资源限制一览

这些常量在 lark_broker.py 中集中定义,针对的是“被攻陷沙箱恶意消耗自己 broker”的场景:

常量防御目标
LARK_BROKER_MAX_REQUEST_BYTES1 MiB超大请求体直接413
LARK_BROKER_MAX_OUTPUT_BYTES4 MiBstdout/stderr 截断并置truncated: true
LARK_BROKER_DEFAULT_TIMEOUT_SECONDS120单次lark-cli执行超时(可用DEERFLOW_LARK_BROKER_TIMEOUT覆盖)
LARK_BROKER_MAX_CONCURRENCY8BoundedSemaphore限制并发子进程数,打满即503 {"error": "broker busy"}
LARK_BROKER_SOCKET_TIMEOUT_SECONDS30防止客户端声明大Content-Length却不发 body、永久挂住 per-connection 线程

5.3GET /v1/health

返回{"ok": true},供 sidecar 就绪探测使用;其余路径返回404

6. 使用边界:只有命令面,没有文件系统桥

这是启用 Broker 模式前必须理解的语义收缩:broker 在sidecar 自己的工作目录里运行lark-cli,看不到沙箱文件系统,因此沙箱的 cwd 有意不被转发(shim 模块 docstring 亦有说明)。后果是:

  • 依赖“相对沙箱 cwd 的路径”读/写文件的子命令(例如上传某个本地文件)在 Broker 模式下不受支持
  • 绝对路径仍然有效,但指向的是sidecar 的文件系统,不是沙箱的;
  • 定位上,这是一座纯命令面桥(command-surface-only bridge),不是文件系统桥。

7. 可选子命令拒绝清单(DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS

Broker 移除了凭据文件,但完整lark-cli命令面仍然可达——任何能打印/导出 token 的子命令(如config showauth token)依旧能把凭据“读进 stdout”再外泄。因此 Broker 提供一个硬化开关:在 sidecar 上设置

DEERFLOW_LARK_BROKER_DENY_SUBCOMMANDS="config show, auth token"

实现要点(lark_broker.py):

  • parse_deny_subcommands()把逗号分隔串解析为命令前缀元组,"config show, auth token"(("config", "show"), ("auth", "token")),空白项被丢弃;
  • 匹配针对leading non-flag tokens:先过滤掉所有以-开头的选项及其值,因此config --json show同样会被config show规则命中;
  • 命中后不会 spawn 二进制,直接返回退出码126subcommand 'config show' is disabled in broker mode消息;
  • 默认值为空(行为零变化)。测试覆盖见 test_lark_broker.py(含解析、匹配与拒绝路径)。

README 给出的生产建议:在启用前,先确认所部署lark-cli版本的子命令面中不存在其它“唾手可得”的密钥导出命令;provisioner 侧配置了该变量后会自动注入到 sidecar 容器(app.py),未配置则不注入。

8. 小结与延伸阅读

Pattern B 用“sidecar 持有凭据 + loopback 命令面 + 沙箱 shim”的组合,把凭据暴露面从“沙箱文件系统”收缩到“Pod 内 loopback + 显式命令白名单”,同时通过单模块单标准库、双模式分发、进程内模板常量三个设计保证了镜像与 Gateway 永不漂移、失败永远可诊断。关键路径速查:

关注点路径
方案说明(本文主体)docker/lark-cli-broker/README.md
镜像定义docker/lark-cli-broker/Dockerfile / docker/lark-cli-broker/entrypoint.sh
Broker 核心实现(标准库单模块)backend/packages/harness/deerflow/integrations/lark_broker.py
沙箱环境覆盖(broker 模式)backend/packages/harness/deerflow/integrations/lark_cli.py
K8s 接线(init/sidecar/挂载/能力上报)docker/provisioner/app.py 与 docker/provisioner/README.md
行为验证backend/tests/test_lark_broker.py、backend/tests/test_provisioner_pvc_volumes.py

适用前提:该方案面向 K8s 多容器 Pod 拓扑(依赖沙箱与 sidecar 共享网络命名空间实现 loopback 互通),且需要运营方自行构建/发布 broker 镜像并配置LARK_CLI_BROKER_IMAGE;未配置时系统自动回退 Pattern A,不会改变既有行为。

【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询