拆解 OpenHands(10)--- Runtime
0x00 摘要对于Agent Google 白皮书给出一个简洁而实用的定义Agent 模型 工具 编排层 部署运行时这里和目前大部分的 AI Agent 的定义LLM Tool Memory多了一层部署运行时。因此可见Runtime的重要性。在 OpenHands 里真正让“AI 想法”落地就是Runtime。它像一座可移动的实验室四面墙把主机世界隔开却给 Agent 留下齐全的操作台文件、终端、网络。Agent 只需递上一张写满指令的纸条Runtime 便接管全部脏活启动进程、捕捉回显、隔离风险、回收资源再把结果封装成“观察报告”送回前台。由于所有动作都在统一沙箱里完成外部系统既不用担心权限越界也不必为环境差异头疼。简言之Runtime 是决策与执行之间的安全转换器它是Agent与外部环境交互的桥梁也是整个系统稳定运转的压舱石保障了整个系统的高效协同运作。因为本系列借鉴的文章过多可能在参考文献中有遗漏的文章如果有还请大家指出。0x01 工作机制在当今数字化时代代码的安全执行已成为至关重要的议题。OpenHands 项目为应对这一挑战精心设计了一套基于 Docker 容器的沙盒系统旨在为代码执行提供一个既安全又隔离的环境。这一系统不仅保障了代码的安全性还确保了执行的一致性和可复现性同时实现了资源的有效控制和不同项目之间的隔离。1.1 Environment 和 SandboxEnvironment 指的是 Agent 可操作的容器相当于给了 Agent 一台可自行操作的计算机Agent 可以在其中端到端地完成任务这个赛道包括 Sandbox、Browser Infra、Agent 操作系统等不同的细分领域。其中Sandbox 是一种安全机制为执行中的程序提供隔离环境即为 Agent 提供了一个可以隔离运行的虚拟机环境开发者可以在这个环境中实现 Agent 的开发、部署、运行。但随着 Agent 的不断发展传统的虚拟机并不能很好地满足 Agent 需求原因在于Agent 对虚拟机的性能提出了更高的要求比如需要更高的隔离性、更快的启动速度、更强的稳定性Agent 的虚拟机还要求具备一定的 AI 性能例如需要具备代码解释器code interpretor的功能或需要整合开发者常用的 AI 架构如 Vercel AI SDK。1.2 安全执行代码安全执行的五大理由安全性在执行不受信任的代码时必须确保这些代码不会对主机系统造成损害。沙盒环境通过严格的访问控制防止恶意代码访问或修改主机系统的资源从而保护主机免受潜在威胁。一致性沙盒环境确保代码在不同机器和配置下的执行结果具有一致性。这种一致性消除了“在我的机器上可以运行”的常见问题使得代码在任何环境下都能稳定运行。资源控制通过沙盒化可以精确控制资源的分配和使用。这不仅防止了失控的进程对主机系统造成影响还确保了资源的合理分配提升了系统的整体性能。隔离性不同的项目或用户可以在各自的隔离环境中工作互不干扰。这种隔离性不仅保护了主机系统还确保了不同项目之间的独立性避免了资源竞争和潜在的冲突。可复现性沙盒环境的一致性和可控性使得复现错误和问题变得更为容易。这在调试和问题解决过程中尤为重要因为一致的环境可以确保问题的重现和解决。1.3 解决方案OpenHands 通过基于 Docker 容器的沙盒环境为代码执行构建了一道坚固的“安全防线”。每个项目在启动时系统会自动创建一个独立的 Docker 容器作为其专属沙盒。这个沙盒拥有隔离的文件系统、网络环境和资源配额确保代码只能访问容器内部的文件网络请求被限制在预设的安全域内CPU 和内存的使用也受到严格管控。这种隔离性带来了三重保障避免项目间干扰某一项目的代码错误如无限循环导致的内存溢出只会影响其所在的沙盒不会波及其他项目或主机系统。防范恶意代码风险即使 AI 生成的代码中包含潜在的危险操作如删除系统文件也会被沙盒环境拦截无法对主机造成实质损害。简化环境一致性管理沙盒的基础镜像可以预先配置好特定版本的编程语言、依赖库和工具链确保代码在开发、测试、生产环境中的执行结果一致避免了“在我电脑上能运行”的问题。尽管沙盒环境提供了高度的安全性和隔离性但它并非一个孤立的“孤岛”。OpenHands 框架提供了精细的“端口映射”和“目录挂载”机制允许开发者将沙盒内的特定端口暴露给主机便于调试或将主机的指定目录挂载到沙盒中用于输入输出文件传递。这种机制在确保安全性的同时也提供了足够的灵活性使得开发和调试过程更加高效。通过这种设计OpenHands 不仅确保了代码的安全执行还提升了开发效率和环境管理的便利性。这种平衡是现代软件开发中不可或缺的一部分为开发者提供了一个既安全又高效的开发环境。1.4 核心功能Runtime的核心功能可以概括为四个主要方面工作环境的构建与管理Runtime负责创建和管理代理的工作区域无论是隔离性更强的容器环境还是便捷的本地环境都能根据需求提供定制化的工作空间确保代理在执行任务时不会受到外部因素的干扰。动作的执行代理发出的指令如文件编辑、命令执行等都由Runtime解析并精确执行它充当了决策与实践之间的桥梁。环境变量的维护Runtime负责维护任务执行所需的环境变量为任务执行提供必要的配置支持。环境的生命周期管理Runtime全程管理环境的生命周期从初始化到断开连接形成完整的闭环。同时通过EventStream实时输出执行日志和观察结果为控制器、记忆系统、MCP等组件提供关键的状态反馈确保整个系统的协同运作。1.5 工作机制OpenHands 运行时系统采用基于 Docker 容器实现的客户端 - 服务器架构其工作机制概述如下用户输入用户提供一个自定义的基础 Docker 镜像。镜像构建OpenHands 以用户提供的镜像为基础构建一个新的 Docker 镜像即 “OH 运行时镜像”。该新镜像包含 OpenHands 专属代码核心为 “运行时客户端”。容器启动当 OpenHands 启动时会使用 OH 运行时镜像启动一个 Docker 容器。动作执行服务器初始化动作执行服务器在容器内部初始化一个ActionExecutor动作执行器配置必要组件如 Bash Shell并加载指定的插件。通信过程OpenHands 后端通过 RESTful API 与动作执行服务器通信发送动作指令并接收执行反馈数据。动作执行运行时客户端接收来自后端的动作指令在沙箱环境中执行这些指令并将执行反馈数据回传。反馈数据返回动作执行服务器将执行结果以反馈数据Observation的形式发送回 OpenHands 后端。客户端的核心作用作为 OpenHands 后端与沙箱环境之间的中间媒介实现双向数据传递。在容器内安全执行各类动作指令包括 Shell 命令、文件操作、Python 代码等。管理沙箱环境的状态涵盖当前工作目录和已加载插件等信息。对反馈数据进行格式化处理后返回给后端为结果处理提供统一的接口规范。工作机制参见下图。0x02 核心逻辑Runtime是在用户交互期间为用户的智能体应用程序提供动力的底层引擎。它是一个系统接收用户定义的智能体、工具和回调并协调它们对用户输入的执行管理信息流、状态变化以及与外部服务如 LLM 或存储的交互。可以将运行时视为你的智能体应用程序的引擎。用户定义部件智能体、工具而运行时处理它们如何连接并一起运行以满足用户请求。Runtime支持多种执行环境包括Docker容器、本地环境等使Agent能够安全地执行代码和命令。其派生类有DockerRuntime、RemoteRuntime、LocalRuntime、KubernetesRuntime、CLIRuntime。核心功能命令执行提供Bash shell访问能力。浏览器交互支持网页浏览和交互操作。文件系统操作文件读写编辑等操作。git 操作管理仓库克隆、分支管理、变更跟踪。环境变量管理运行时环境变量配置。插件系统管理支持VSCode、Jupyter等插件集成。2.1. base.pybase.py文件定义了Runtime类它作为代理与外部环境交互的主要接口。它处理各种操作包括沙盒执行浏览器交互文件系统操作环境变量管理插件管理Runtime类的关键特性使用配置和事件流进行初始化使用ainit方法异步初始化环境变量针对不同类型的操作执行方法运行、读取、写入、浏览等文件操作的抽象方法由子类实现2.2 ActionExecutionClientaction_execution_client.py文件包含ActionExecutionClient类它实现了运行时接口。它是一个抽象实现意味着仍需要通过具体实现来扩展才能使用。此客户端通过HTTP调用与action_execution_server交互以实际执行运行时操作。class ActionExecutionClient(Runtime): Base class for runtimes that interact with the action execution server. This class contains shared logic between DockerRuntime and RemoteRuntime for interacting with the HTTP server defined in action_execution_server.py. def __init__( self, config: OpenHandsConfig, event_stream: EventStream, llm_registry: LLMRegistry, sid: str default, plugins: list[PluginRequirement] | None None, env_vars: dict[str, str] | None None, status_callback: Any | None None, attach_to_existing: bool False, headless_mode: bool True, user_id: str | None None, git_provider_tokens: PROVIDER_TOKEN_TYPE | None None, ): self.session HttpSession() self.action_semaphore threading.Semaphore(1) # Ensure one action at a time self._runtime_closed: bool False self._vscode_token: str | None None # initial dummy value self._last_updated_mcp_stdio_servers: list[MCPStdioServerConfig] [] super().__init__( config, event_stream, llm_registry, sid, plugins, env_vars, status_callback, attach_to_existing, headless_mode, user_id, git_provider_tokens, )其余的所有Runtime均继承 ActionExecutionClientclass KubernetesRuntime(ActionExecutionClient): class LocalRuntime(ActionExecutionClient): class RemoteRuntime(ActionExecutionClient): class DockerRuntime(ActionExecutionClient):2.3 运行时类型2.3.1 Docker运行时环境为使用Docker容器设计的Docker运行时环境OpenHands做如下配置为每个会话创建和管理Docker容器在容器内执行动作支持直接文件系统访问和本地资源管理适合开发、测试和需要完全控制执行环境的场景Docker运行时环的关键特性为实时日志和调试能力直接访问本地文件系统由于本地资源执行速度更快容器隔离以提高安全性2.3.2 本地运行时环境本地运行时环境设计用于本地机器上的直接执行。目前仅支持以本地用户身份运行直接在主机上运行action_execution_server无Docker容器开销直接访问本地系统资源适合Docker不可用或不需要开发和测试关键特性需要的设置最少直接访问本地资源无容器开销最快执行速度重要此运行时不提供隔离因为它直接在主机上运行。所有动作以运行OpenHands的用户的相同权限执行。对于需要适当隔离的安全执行请改用Docker运行时。2.3.3 远程运行时环境远程运行时环境设计用于远程环境中的执行连接到运行ActionExecutor的远程服务器通过向远程客户端发送请求执行动作支持分布式执行和基于云的部署适合生产环境、可扩展性和本地资源限制是问题的场景关键特性可扩展性和资源灵活性减少本地资源使用支持基于云的部署通过隔离提高安全性的潜力目前这主要用于并行评估例如这个SWE-Bench示例。2.3.4 单例模式Runtime的子类不是单例模式。从函数_create_runtime 可以看出每次运行都会创建一个新的实例。每个Runtime都有自己的状态如sid会话ID。每个实例也可以调用close()来清理资源。async def _create_runtime( self, runtime_name: str, config: OpenHandsConfig, agent: Agent, git_provider_tokens: PROVIDER_TOKEN_TYPE | None None, custom_secrets: CUSTOM_SECRETS_TYPE | None None, selected_repository: str | None None, selected_branch: str | None None, ) - bool: Creates a runtime instance Parameters: - runtime_name: The name of the runtime associated with the session - config: - agent: Return True on successfully connected, False if could not connect. Raises if already created, possibly in other situations. self.runtime runtime_cls( configconfig, event_streamself.event_stream, llm_registryself.llm_registry, sidself.sid, pluginsagent.sandbox_plugins, status_callbackself._status_callback, headless_modeFalse, attach_to_existingFalse, env_varsenv_vars, git_provider_tokensgit_provider_tokens, ) return True2.4 工作流程Runtime作为EventStreamSubscriber.RUNTIME订阅者处理来自事件流的Action并生成Observation。Runtime的工作流程如下初始化Runtime使用配置和事件流进行初始化。设置环境变量。加载并初始化插件。动作处理Runtime通过事件流接收动作。验证并路由到适当的执行方法。动作执行执行不同类型的动作使用run方法执行bash命令使用run_ipython方法执行IPython单元使用read和write方法执行文件操作使用browse和browse_interactive方法浏览网页观察生成动作执行后生成相应的观察结果。观察结果被添加到事件流中。插件集成插件如Jupyter和AgentSkills被初始化并集成到运行时。沙盒环境ActionExecutor在Docker容器内设置沙盒环境。初始化用户环境和bash shell。从OpenHands后端接收的动作在此沙盒环境中执行。浏览器交互使用BrowserEnv类处理网络浏览动作。2.5. Runtime与其他组件关系Runtime与其他组件的主要关系如下运行时与openhands.events模块中定义的事件系统紧密交互。依赖于openhands.core.config中的配置类。日志通过openhands.core.logger处理。我们详细看看。2.5.1 EventStreamRuntime通过与EventStream与其他模块进行事件驱动的交互。class Runtime(FileEditRuntimeMixin): # Runtime订阅事件流 event_stream.subscribe( EventStreamSubscriber.RUNTIME, self.on_event, self.sid ) # Runtime处理传入的事件 def on_event(self, event: Event) - None: if isinstance(event, Action): asyncio.get_event_loop().run_until_complete(self._handle_action(event)) # Runtime返回观察结果给事件流 self.event_stream.add_event(observation, source)2.5.2 AgentControllerAgentController通过EventStream向Runtime发送操作命令Runtime执行后将结果返回。AgentController 发送Action → EventStreamRuntime 接到 Action 后执行Rutime 发送 Observation → EventStreamAgentController 接收到 Observation 并继续执行决策流程2.5.3 SessionWebSession 和 AgentSession 负责管理 Runtime 的生命周期# AgentSession runtime_cls get_runtime_cls(runtime_name) # 创建Runtime实例 self.runtime runtime_cls( configconfig, event_streamself.event_stream, llm_registryself.llm_registry, sidself.sid, pluginsagent.sandbox_plugins, status_callbackself._status_callback, headless_modeFalse, attach_to_existingFalse, git_provider_tokensoverrided_tokens, env_varsenv_vars, user_idself.user_id, ) # 连接到Runtime await self.runtime.connect() # 关闭Runtime EXECUTOR.submit(self.runtime.close)2.5.4 插件系统Runtime管理和执行各种插件功能。# 初始化加载插件 self.plugins ( copy.deepcopy(plugins) if plugins is not None and len(plugins) 0 else [] ) # add VSCode plugin if not in headless mode if not headless_mode: self.plugins.append(VSCodeRequirement()) # 执行插件相关操作 if any(isinstance(plugin, JupyterRequirement) for plugin in self.plugins): code import os\n for key, value in env_vars.items(): # Note: json.dumps gives us nice escaping for free code fos.environ[{key}] {json.dumps(value)}\n code \n self.run_ipython(IPythonRunCellAction(code)) ......2.5.5 文件系统和存储Runtime 提供文件操作能力。def read(self, action: FileReadAction) - Observation: def write(self, action: FileWriteAction) - Observation:2.5.6 git仓库Runtime 提供git仓库操作能力。async def clone_or_init_repo( self, git_provider_tokens: PROVIDER_TOKEN_TYPE | None, selected_repository: str | None, selected_branch: str | None, ) - str:2.5.7 Python MCPRuntime 提供运行python代码和call_tool_mcp操作能力。def run_ipython(self, action: IPythonRunCellAction) - Observation: async def call_tool_mcp(self, action: MCPAction) - Observation:0x03 代码3.1 定义初始化class Runtime(FileEditRuntimeMixin): 智能代理运行时环境的抽象基类。 这是OpenHands中的一个扩展点允许应用程序自定义代理与外部环境的交互方式。 该运行时提供一个沙箱环境包含以下功能 - Bash shell访问 - 浏览器交互 - 文件系统操作 - Git操作 - 环境变量管理 应用程序可通过以下方式替换为自定义实现 1. 创建一个继承自Runtime的类 2. 实现所有必需的方法 3. 在配置中设置运行时名称或使用get_runtime_cls()方法 该类通过get_runtime_cls()中的get_impl()方法实例化。 内置实现包括 - DockerRuntime基于Docker的容器化环境 - RemoteRuntime远程执行环境 - LocalRuntime用于开发的本地执行环境 - KubernetesRuntime基于Kubernetes的执行环境 - CLIRuntime命令行界面运行时 参数 sid唯一标识当前用户会话的会话ID sid: str # 会话ID唯一标识用户会话 config: OpenHandsConfig # OpenHands配置对象 initial_env_vars: dict[str, str] # 初始环境变量字典 attach_to_existing: bool # 是否连接到现有运行时环境 status_callback: Callable[[str, RuntimeStatus, str], None] | None # 状态回调函数接收会话ID、运行时状态和消息 runtime_status: RuntimeStatus | None # 当前运行时状态 _runtime_initialized: bool False # 运行时初始化状态标记 security_analyzer: SecurityAnalyzer | None None # 安全分析器实例用于检测潜在风险 def __init__( self, config: OpenHandsConfig, event_stream: EventStream, llm_registry: LLMRegistry, sid: str default, plugins: list[PluginRequirement] | None None, env_vars: dict[str, str] | None None, status_callback: Callable[[str, RuntimeStatus, str], None] | None None, attach_to_existing: bool False, headless_mode: bool False, user_id: str | None None, git_provider_tokens: PROVIDER_TOKEN_TYPE | None None, ): # 初始化Git处理器绑定shell执行和文件创建的回调方法 self.git_handler GitHandler( execute_shell_fnself._execute_shell_fn_git_handler, # Git操作所需的shell执行函数 create_file_fnself._create_file_fn_git_handler, # Git操作所需的文件创建函数 ) # 初始化会话ID self.sid sid # 绑定事件流组件间通信的核心 self.event_stream event_stream # 若事件流存在订阅运行时相关事件 if event_stream: event_stream.subscribe( EventStreamSubscriber.RUNTIME, # 订阅者类型运行时 self.on_event, # 事件处理回调函数 self.sid # 订阅者ID当前会话ID ) # 初始化插件列表深拷贝传入的插件为空时设为默认空列表 self.plugins ( copy.deepcopy(plugins) if plugins is not None and len(plugins) 0 else [] ) # 非无头模式下添加VSCode插件 if not headless_mode: self.plugins.append(VSCodeRequirement()) # 绑定状态回调函数 self.status_callback status_callback # 记录是否连接到现有运行时 self.attach_to_existing attach_to_existing # 深拷贝配置对象避免外部修改影响内部状态 self.config copy.deepcopy(config) # 注册程序退出时的关闭回调确保资源正确释放 atexit.register(self.close) # 初始化默认环境变量基于沙箱配置 self.initial_env_vars _default_env_vars(config.sandbox) # 合并用户传入的环境变量覆盖默认值 if env_vars is not None: self.initial_env_vars.update(env_vars) # 初始化Provider处理器管理Git等服务的访问令牌 self.provider_handler ProviderHandler( provider_tokensgit_provider_tokens or cast(PROVIDER_TOKEN_TYPE, MappingProxyType({})), # 令牌为空时使用空映射 external_auth_iduser_id, # 外部认证ID关联用户 external_token_managerTrue, # 启用外部令牌管理 ) # 同步调用异步方法获取Provider相关环境变量 raw_env_vars: dict[str, str] call_async_from_sync( self.provider_handler.get_env_vars, # 获取环境变量的异步方法 GENERAL_TIMEOUT, # 超时时间 True, # 允许重试 None, # 重试间隔使用默认 False # 不抛出异常 ) # 合并Provider返回的环境变量 self.initial_env_vars.update(raw_env_vars) # 检查是否启用VSCode插件通过判断插件列表中是否包含VSCodeRequirement实例 self._vscode_enabled any( isinstance(plugin, VSCodeRequirement) for plugin in self.plugins ) # 初始化文件编辑混合类提供文件编辑相关功能 FileEditRuntimeMixin.__init__( self, enable_llm_editorconfig.get_agent_config().enable_llm_editor, # 是否启用LLM辅助编辑 llm_registryllm_registry, # LLM注册中心用于获取语言模型实例 ) # 记录用户ID self.user_id user_id # 记录Git服务提供商令牌 self.git_provider_tokens git_provider_tokens # 初始化运行时状态默认为None self.runtime_status None # 初始化安全分析器若配置启用 self.security_analyzer None if self.config.security.security_analyzer: # 根据配置获取安全分析器类默认使用SecurityAnalyzer analyzer_cls options.SecurityAnalyzers.get( self.config.security.security_analyzer, SecurityAnalyzer ) # 实例化安全分析器 self.security_analyzer analyzer_cls() # 为安全分析器绑定事件流用于发布安全相关事件 self.security_analyzer.set_event_stream(self.event_stream)3.2 关键代码3.2.1 环境初始化setup_initial_env提供了环境初始化能力def setup_initial_env(self) - None: if self.attach_to_existing: return logger.debug(fAdding env vars: {self.initial_env_vars.keys()}) self.add_env_vars(self.initial_env_vars) if self.config.sandbox.runtime_startup_env_vars: self.add_env_vars(self.config.sandbox.runtime_startup_env_vars) # Configure git settings self._setup_git_config()3.2.2 事件处理on_event函数接受来自事件流的 Action_handle_action函数执行对应的操作生成Observation。def on_event(self, event: Event) - None: if isinstance(event, Action): asyncio.get_event_loop().run_until_complete(self._handle_action(event))3.2.3 微代理支持get_microagents_from_selected_repo 从仓库加载微代理配置get_microagents_from_org_or_user比如get_microagents_from_org_or_user是 OpenHands 系统中组织 / 用户级微智能体加载的核心逻辑负责从代码仓库的组织或用户级配置仓库中加载微智能体轻量级智能体组件。主要功能包括仓库路径解析从目标仓库路径中提取组织 / 用户名确定配置仓库位置。平台适配区分 GitLab 和其他平台如 GitHub使用不同的配置仓库名称GitLab 用openhands-config其他用.openhands。仓库克隆通过带认证的 URL 克隆配置仓库采用浅克隆--depth 1提高效率。微智能体加载从克隆仓库的microagents目录加载微智能体并在加载完成后清理临时文件。异常处理针对认证失败、克隆错误等场景进行日志记录确保流程稳健性。流程如下