拆解 OpenHands(10)--- Runtime 沙盒与 Docker 隔离机制
1. 为什么 Agent 需要一个 Docker 沙盒 Runtime如果你刚开始接触 OpenHands可能会觉得它和普通的代码助手差不多给个需求它写代码你复制粘贴。但真正跑过一次完整任务后你会发现它和“聊天式写代码”最大的区别在于——它真的会去执行。执行就意味着风险删错文件、跑飞进程、把主机环境搞乱。OpenHands 的 Runtime 就是来解决这个问题的它把 Agent 的“手”关进一个 Docker 容器里让它在里面随便折腾外面主机毫发无伤。我先把结论放前面OpenHands Runtime 是一套基于 Docker 容器的客户端-服务器执行架构。容器里跑着一个action_execution_server负责接收后端发来的 Action比如执行 bash、读写文件、跑 Python执行完把 Observation 回传。后端和容器之间通过 HTTP 通信容器和主机之间通过目录挂载和端口映射打通。理解这条链路你就能明白 Agent 的“隔离边界”到底画在哪里出问题时也知道该去哪个环节排查。这篇文章面向三类人一是刚把 OpenHands 跑起来、想搞清楚它背后怎么隔离执行环境的开发者二是遇到容器启动失败、挂载目录不生效、命令执行超时这类问题需要一套可跟做排查流程的人三是想把 OpenHands Runtime 的设计思路借鉴到自己 Agent 项目里的工程师。全文会给出可复制的 Docker 配置片段、Runtime 启动参数并完整演示一次 Agent 任务在沙盒内的执行与验证流程。在展开之前先明确一个概念区分这个区分后面会反复用到。Environment指的是 Agent 可操作的整台“计算机”包括文件系统、终端、网络、浏览器等Sandbox是其中负责隔离的安全机制OpenHands 选择用 Docker 容器来实现它。Runtime 则是把这两者串起来的调度层它既管容器的生命周期也管 Action 的分发和 Observation 的回收。你可以把 Runtime 理解成“实验室管理员”——墙是 Docker 砌的但开门、递工具、收实验报告这些活都是 Runtime 干的。2. TaoToken 前置给 Runtime 里的 Agent 接上模型能力Runtime 负责“执行”但“决策”得靠模型。OpenHands 的 Agent 在沙盒里跑命令之前需要先调用 LLM 来规划下一步动作。所以在你把 Docker 沙盒跑通之后下一步就是给 Agent 配一个稳定的模型入口。这里我用 TaoToken 来做演示它的接口兼容 OpenAI 风格配置起来比较直接。先拿 Key。打开https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite登录后创建一个 API Key复制出来备用。注意这个 Key 只在创建时完整显示一次丢了就得重建。拿到 Key 之后你需要确认两件事Base URL 和 Model ID。Base URL 用https://taotoken.net/api注意这个地址后面不加 UTM 参数直接填就行。Model ID 根据你实际要用的模型来填比如claude-sonnet-4-20250514这类标识具体以控制台里列出的为准。如果你不确定该选哪个模型可以先去https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite看一眼可用列表。这里有个容易踩的坑OpenHands 的配置文件里LLM 的base_url字段有时候需要带/v1后缀有时候不需要取决于你用的 SDK 版本。TaoToken 的兼容接口在https://taotoken.net/api下已经处理了路径所以你在 OpenHands 的config.toml里直接填https://taotoken.net/api即可不要自己再加/v1否则会出现 404。这个细节我在后面第 5 节的报错排查里会再展开。另外如果你打算长期跑 Agent 任务尤其是那种需要多轮规划、反复调用模型的场景建议了解一下 Coding Plan。它的计费方式对高频调用更友好具体可以看https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。对于只是偶尔跑一次验证的情况按量计费的 API Key 就够了。把 Key 和 Base URL 准备好之后先别急着往 OpenHands 里塞。我建议你用一个最简单的 curl 请求验证一下这个 Key 能不能通避免后面把模型问题和 Docker 问题混在一起排查。验证命令如下curl -s https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: reply with ok}], max_tokens: 16 }如果返回里能看到choices字段和正常内容说明模型入口是通的。这一步过了再往下配 Runtime 才有意义。如果这里就报 401那问题在 Key 上跟 Docker 无关别往下折腾。3. 可复制配置Docker 沙盒与 Runtime 启动参数这一节是全文最核心的部分我会给出可以直接复制使用的配置片段。OpenHands 的 Runtime 配置分散在两个地方一个是 Docker 容器本身的启动参数另一个是 OpenHands 的config.toml。两者配合才能把沙盒跑起来。先看 Docker 侧。OpenHands 默认会基于你指定的基础镜像构建一个“OH 运行时镜像”里面包含action_execution_server。但如果你想手动控制容器的挂载和端口可以用下面这个docker run片段作为参考。注意实际使用时 OpenHands 会自己管理容器这里给出来是为了让你理解挂载和端口是怎么映射的docker run -d \ --name openhands-runtime-demo \ --mount typebind,source/home/yourname/workspace,target/workspace \ -p 30000:30000 \ -e SANDBOX_USER_ID1000 \ -e WORKSPACE_MOUNT_PATH/home/yourname/workspace \ -e RUNTIME_HOST0.0.0.0 \ -e RUNTIME_PORT30000 \ --memory4g \ --cpus2 \ openhands/runtime:latest这里几个参数值得说明。--mount把主机的/home/yourname/workspace挂到容器的/workspaceAgent 在容器里读写/workspace就等于操作你主机上的这个目录这是输入输出文件传递的关键通道。-p 30000:30000把容器内的执行服务器端口暴露出来后端通过这个端口发 Action。--memory和--cpus是资源配额防止 Agent 跑飞把主机拖垮。SANDBOX_USER_ID决定容器内以哪个用户身份执行命令这个值要和主机上挂载目录的属主 UID 对上否则会出现权限拒绝。再看 OpenHands 的config.toml。这个文件通常放在项目根目录或~/.openhands/下Runtime 相关配置集中在[sandbox]和[core]段[core] runtime docker workspace_base /home/yourname/workspace [sandbox] base_container_image openhands/runtime:latest use_host_network false runtime_startup_env_vars { SANDBOX_USER_ID 1000 } timeout 120 enable_auto_lint true [llm] model claude-sonnet-4-20250514 base_url https://taotoken.net/api api_key sk-你的TaoTokenKeyruntime docker明确告诉 OpenHands 用 DockerRuntime。workspace_base是主机上的工作目录会被挂载进容器。timeout 120是单个 Action 的执行超时单位秒跑长命令时可以调大。use_host_network false表示容器用独立网络需要端口映射才能访问这是隔离性的一部分。如果你用的是 Cline 或 Claude Code 这类工具配合 OpenHands配置里同样要写全三件套Base URL、API Key、Model ID。以 Cline 的 MCP 配置为例片段长这样{ mcpServers: { openhands: { command: openhands, args: [--runtime, docker], env: { LLM_BASE_URL: https://taotoken.net/api, LLM_API_KEY: sk-你的TaoTokenKey, LLM_MODEL: claude-sonnet-4-20250514 } } } }这三个字段缺一不可。我见过有人只填了 Key 没填 Base URL结果请求打到了默认的 OpenAI 地址报 401也有人 Model ID 写错报model not found。所以配置写完先自查一遍这三项。配置就绪后启动 OpenHands。它会自动拉取基础镜像、构建运行时镜像、启动容器然后在容器内初始化ActionExecutor和 bash shell。你可以在启动日志里看到类似Runtime started with session id xxx的输出说明沙盒已经就绪。4. 验证请求一次 Agent 任务在沙盒内的完整执行配置写完不算完得实际跑一次任务看 Action 有没有真的进容器、Observation 有没有正常回来。这一节我带你走一遍完整流程从发任务到验证结果。先启动 OpenHands 后端。假设你已经装好了openhands-ai包运行openhands serve --config ./config.toml启动后打开 Web 界面或者直接用 API 发一个任务。这里我用 API 方式演示方便你复现。构造一个简单任务让 Agent 在/workspace下创建一个文件写入内容然后读出来确认。curl -s -X POST http://localhost:3000/api/conversations \ -H Content-Type: application/json \ -d { initial_user_msg: 在 /workspace 下创建 hello_runtime.txt写入 openhands sandbox ok然后 cat 出来确认, repository: null }后端收到任务后AgentController 会开始规划。它会先调用 LLM 生成一个CmdRunAction内容是echo openhands sandbox ok /workspace/hello_runtime.txt。这个 Action 被投递到 EventStreamRuntime 作为订阅者收到后通过 HTTP 把 Action 发给容器内的action_execution_server。容器内的执行服务器解析 Action在沙盒里执行命令。执行完它把 stdout、stderr、exit code 打包成CmdOutputObservation回传给后端。后端再把 Observation 放进 EventStreamAgentController 收到后继续下一步——这次是cat /workspace/hello_runtime.txt。整个链路走完后你去主机上看cat /home/yourname/workspace/hello_runtime.txt应该能看到openhands sandbox ok。这一步验证了两件事一是命令确实在容器里执行了二是挂载目录生效了容器内的/workspace和主机的workspace_base是同一个目录。如果你想更直观地确认隔离边界可以在任务里加一条命令让 Agent 尝试访问容器外的主机路径比如ls /home/yourname。正常情况下容器里看不到主机的/home/yourname除非你挂载了会返回No such file or directory。这就说明隔离生效了。再验证一下资源限制。发一个任务让 Agent 跑stress --vm 1 --vm-bytes 8G因为容器内存限制是 4g这个命令会被 OOM killer 干掉容器本身不会崩主机也不受影响。你会在 Observation 里看到进程被杀的返回码。这就是资源配额在起作用。最后看日志。Runtime 的执行日志会实时输出到后端控制台格式类似[Runtime] Action CmdRunAction: echo openhands sandbox ok /workspace/hello_runtime.txt [Runtime] Observation: exit_code0, stdout, stderr看到exit_code0就说明命令执行成功。如果 exit_code 非零或者 Observation 迟迟不回来那就进入下一节的排查流程。5. 本篇常见错排查401、local proxy failed、reading choices、OAuthRuntime 跑不起来报错往往集中在几个固定位置。我把最常见的四类整理出来对照着查基本能定位。第一类401 Unauthorized。这个几乎都出在模型配置上跟 Docker 无关。典型报错是Error code: 401 - {error: {message: Invalid API key}}。排查顺序先确认config.toml里api_key填的是 TaoToken 的 Key不是别的平台的再确认base_url是https://taotoken.net/api没有多加/v1最后用第 2 节的 curl 命令单独验证 Key 是否有效。如果 curl 通但 OpenHands 报 401那多半是配置文件没被加载检查--config路径对不对。第二类local proxy failed。报错长这样Failed to connect to local proxy at 127.0.0.1:30000。这是后端连不上容器内的执行服务器。原因通常有三个容器没启动成功、端口映射没生效、或者RUNTIME_HOST配错了。先docker ps看容器在不在再docker logs container_id看执行服务器有没有起来。如果容器在但端口不通检查-p 30000:30000有没有写以及use_host_network是不是设成了 true 导致端口映射失效。第三类reading choices。完整报错是Error reading choices from response或KeyError: choices。这说明模型返回的 JSON 结构不对通常是 Base URL 指错了地方请求打到了一个不兼容 OpenAI 格式的端点。确认base_url是https://taotoken.net/api并且 Model ID 是控制台里列出的有效值。如果 Model ID 写了个不存在的名字有些网关会返回错误页而不是标准 JSON也会触发这个报错。第四类OAuth 相关。如果你在配置里用了需要 OAuth 的 Git 提供商可能会看到OAuth token expired或Failed to refresh token。这类问题跟 Runtime 沙盒本身无关是 Git 凭证的事。检查git_provider_tokens配置或者临时把selected_repository设为 null先跑通不涉及 Git 的任务排除干扰。排查时有个通用技巧把日志级别调到 DEBUG。在config.toml里加[core] debug true这样 Runtime 会把每个 Action 的发送和每个 Observation 的接收都打出来你能清楚看到卡在哪一步。如果 Action 发出去了但 Observation 没回来问题在容器内如果 Action 根本没发出去问题在后端或模型调用。还有一个隐蔽的坑挂载目录权限。如果主机目录属主是 UID 1001而SANDBOX_USER_ID设的是 1000容器内写文件会报Permission denied。解决办法是chown -R 1000:1000 /home/yourname/workspace或者把SANDBOX_USER_ID改成和主机目录属主一致的值。这个报错不会让容器崩但会让 Agent 的文件操作全部失败表现得很像“Agent 不干活”容易误判。6. 把 Runtime 用顺手的几个实操建议跑通一次之后我想分享几个让 Runtime 更稳定的做法。这些都是实际用下来觉得有用的不是理论。第一给每个任务单独的工作目录。不要所有任务都挂同一个workspace_base否则前一个任务留下的文件会干扰后一个。可以在启动前用脚本生成带时间戳的目录然后动态改config.toml里的workspace_base。这样任务之间天然隔离清理也方便。第二超时值按任务类型调。默认 120 秒对大多数命令够用但如果你让 Agent 跑npm install或编译大项目很容易超时。可以在任务描述里明确告诉 Agent 用timeout命令包一层或者直接把[sandbox] timeout调到 600。超时后 Observation 会返回超时错误Agent 通常会重试但重试也可能再次超时所以提前调大更省事。第三定期清理停止的容器。OpenHands 每次会话创建一个容器任务结束后如果没正常 close容器会残留。时间长了docker ps -a里一堆 exited 容器占磁盘也占端口。可以加个定时任务清理超过一天的停止容器docker container prune --filter until24h -f第四模型调用和沙盒执行分开验证。出问题时先用 curl 确认模型通再用一个最简单的echo任务确认沙盒通。两个都通了再跑复杂任务。这样能把问题范围缩小到一半比一上来就跑完整任务然后对着报错猜要高效得多。第五如果你要长期跑 Agent 任务考虑用 Coding Plan 来管理模型调用成本。高频调用下按量计费容易失控套餐制更可控。入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite具体额度以页面为准。最后说一个理解上的点。Runtime 的隔离边界不是“越严越好”而是在安全和可用之间找平衡。挂载目录是必要的否则 Agent 的产出拿不出来端口映射是必要的否则后端指挥不动容器。真正要守住的是容器内的进程不能越权访问主机其他路径资源使用不能失控任务之间不能互相污染。把这三条守住Runtime 就算用明白了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →