openai-agents-python 沙箱运行时边界:所有权、会话来源、信任边界与清理语义全解析
2026/9/11 10:22:50 网站建设 项目流程

openai-agents-python 沙箱运行时边界:所有权、会话来源、信任边界与清理语义全解析

【免费下载链接】openai-agents-pythonA lightweight, powerful framework for multi-agent workflows项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python

导读

本文以 openai-agents-python 仓库中沙箱(Sandbox)运行时边界设计文档为骨架,系统拆解SandboxAgent与沙箱会话之间的职责划分:谁拥有沙箱会话的生命周期、会话从何处来、代理如何被准备、路径与凭据的信任边界如何划定、错误与挂载如何收敛。读完本文,你将掌握沙箱运行时的内部契约(对应 .agents/references/sandbox-runtime-boundary.md),能够安全地在自己的应用中注入、恢复、快照或并发使用沙箱会话,并能基于 tests/sandbox/test_runtime.py 等测试用例验证行为是否符合预期。


一、运行时所有权:外层 Runner 与沙箱会话的分层契约

沙箱运行时边界的第一条原则是分层所有权:不要把某一层的生命周期挪到另一层,除非你同时为两层都定义好 resume(恢复)与 cleanup(清理)行为。

从源码看,这个分层被实现在两个核心类中:

  • 外层Runner负责 agent 回合(turns)、审批(approvals)、handoff、tracing、会话历史与RunState
  • 沙箱会话(BaseSandboxSession)负责执行环境、工作区、进程、挂载与 provider 相关的连接状态。

对应实现位于 src/agents/sandbox/runtime.py 与 src/agents/sandbox/runtime_session_manager.py:

# runtime.py(节选) class SandboxRuntime(Generic[TContext]): def __init__(self, *, starting_agent, run_config, rollout_id=None, run_state=None) -> None: self._session_manager = SandboxRuntimeSessionManager( starting_agent=starting_agent, sandbox_config=self._sandbox_config, run_state=run_state, )

SandboxRuntime只做代理准备与内存结果入队,而会话的创建、恢复、清理全部委托给SandboxRuntimeSessionManager。这正是文档所言"沙箱会话拥有命令、文件变化与环境隔离;外层运行时拥有审批、追踪与恢复所需状态"这一核心模型的落地。

1.1 注入会话是调用方拥有的

当调用方通过SandboxRunConfig(session=...)注入一个已经创建的 live 会话时,Runner 可以配置并使用它,但不得删除或完全拆除它。在_create_resources中可以看到注入路径的显式处理:

# runtime_session_manager.py(节选) if sandbox_config.session is not None: self._configure_session(sandbox_config.session, ...) ... return _SandboxSessionResources( session=sandbox_config.session, client=None, owns_session=False, # 调用方拥有,Runner 不清理 )

owns_session=False意味着_SandboxSessionResources.cleanup()会直接短路返回(if not self._owns_session: return),因此async with sandbox:退出时由调用方自己执行aclose()完成全量清理。

1.2 Runner 创建的会话由 Runner 拥有

当会话由SandboxRunConfig.client创建或恢复时,它是 Runner 拥有的。_SandboxSessionResources.cleanup()的完整顺序(见 runtime_session_manager.py)为:

  1. 运行 pre-stop hooks(run_pre_stop_hooks());
  2. 调用stop()持久化 snapshot 支撑的工作区状态;
  3. shutdown()关闭沙箱;
  4. 通过client.delete(session)删除 provider 资源(当 client 存在且会话类型为SandboxSession时);
  5. _aclose_dependencies()关闭会话级依赖。

关键语义:

  • 清理必须幂等self._cleaned标志保证同一资源只清理一次,且全程由asyncio.Lock串行化,避免并发aclose()竞态;
  • 清理失败也必须释放并发守卫cleanup()中的finally块总会执行self._release_agents(),即使 pre-stop hook、stop 或 persistence 失败,也会把SandboxAgentactive_runs计数复位(guard.active_runs = max(0, guard.active_runs - 1)),见 tests/sandbox/test_runtime.py 中的test_runner_owned_cleanup_redacts_pre_stop_hook_failure(L592)等用例。

1.3SandboxAgent不能跨 run 并发复用

一个SandboxAgent实例在同一时刻只能绑定一个 live run,因为准备好的 capability 工具与会话状态都与该 run 绑定。acquire_agent()用实例上的_sandbox_concurrency_guard实现互斥:

# runtime_session_manager.py(节选) guard = getattr(agent, "_sandbox_concurrency_guard", None) if guard is None: guard = _SandboxConcurrencyGuard() agent._sandbox_concurrency_guard = guard with guard.lock: if guard.active_runs > 0: raise RuntimeError( f"SandboxAgent {agent.name!r} cannot be reused concurrently across runs" ) guard.active_runs += 1

因此,并发工作应通过agent.clone()或直接构造新的SandboxAgent实例来完成。


二、会话来源与保存状态:解析顺序与语义区分

2.1 四步解析顺序

会话来源按固定优先级解析(对应 docs/sandbox/guide.md 中的SandboxRunConfig说明与_create_resources实现):

  1. 注入的 live 会话run_config.sandbox.session直接复用;
  2. RunState携带的可恢复沙箱状态_resume_state_payload_for_agent()run_state._sandbox中按 resume key 取回序列化状态;
  3. 显式SandboxRunConfig.session_stateclient.deserialize_session_state(explicit_state)client.resume(...)
  4. 新建会话:以run_config.sandbox.manifestagent.default_manifest为输入调用client.create(...)

Manifest 与 snapshot 输入只用于播种全新会话,不会覆盖注入或恢复的工作区——这正是文档强调"Manifest 是 fresh-session 的工作区契约,而不是每个 live 沙箱的完整真相来源"的原因。

2.2 三类状态不可混用

状态载体含义用途
RunState的 sandbox payloadRunner 管理的序列化沙箱状态(含backend_idcurrent_agent_keysession_statesessions_by_agent跨 run 自动续接 Runner 管理的流程
SandboxRunConfig.session_state显式序列化的 provider 连接/会话状态RunState之外自己持久化状态时直接恢复
snapshot/SnapshotSpec保存的工作区内容播种全新沙箱会话的文件与工件

snapshot 代表"保存的工作区内容",与 provider 的会话状态不可互换serialize_resume_state()(runtime_session_manager.py)只在 stop-time 持久化完成之后才序列化 Runner 拥有的会话,这样后续 resume 时:后端存活就直接重连(reattach),后端不存活则从保存的 snapshot 重建工作区。

2.3 重复 agent 名称下的稳定 resume 身份

Handoff 图允许出现重名 agent,而对象身份(id())是进程局部的,序列化状态需要稳定 key 与显式的 current-agent 选择。SandboxRuntimeSessionManager_stable_resume_keys_by_agent_id+_allocate_unique_agent_identity为每个 agent 分配不冲突的 resume key,并用current_agent_key记录当前活动 agent。

相关测试覆盖了这些边界:

  • test_runner_serializes_unique_sandbox_resume_keys_for_duplicate_agent_names(test_runtime.py)
  • test_runner_restores_duplicate_name_sandbox_sessions_after_json_roundtrip(test_runtime.py)
  • test_session_manager_reserves_current_duplicate_resume_key_for_current_agent(test_runtime.py)

三、Agent 准备:从克隆到绑定的五步流程

3.1 每次 run 克隆 capability 实例

Capability 对象是可变的(持有self.session、采样设置等),跨 run 复用会泄漏工具、采样设置或会话引用。因此每次准备都执行clone_capabilities()(runtime_agent_preparation.py):

def clone_capabilities(capabilities: Sequence[Capability]) -> list[Capability]: return [capability.clone() for capability in capabilities]

随后在 runtime.py 中把克隆绑定到 live 会话:

for capability in prepared_capabilities: capability.bind(session) capability.bind_workspace_scope(self._workspace_scope) _bind_capability_run_as(prepared_capabilities, run_as)

绑定发生在上下文处理(context processing)之前,这样 capability 在转换输入时可以安全地检查self.session

3.2 先验证依赖,再暴露工具

prepare_sandbox_agent()(runtime_agent_preparation.py)在构造工具列表前先校验 capability 依赖:

available_capability_types = {capability.type for capability in capabilities} for capability in capabilities: required_capability_types = capability.required_capability_types() missing_capability_types = required_capability_types - available_capability_types if missing_capability_types: raise UserError(f"{type(capability).__name__} requires missing capabilities: {missing}")

这样确保 capability 工具构造、指令片段、输入处理与采样调整使用的是同一套有效 capability 集。例如内置Memorycapability 要求ShellFilesystem提供apply_patchview_image,详见 docs/sandbox/guide.md 的 capabilities 表格。

3.3 指令的固定拼接顺序

最终指令按文档化顺序构建,build_sandbox_instructions()(runtime_agent_preparation.py)的实现顺序与文档完全一致:

  1. SDK 默认沙箱 base prompt(agents.sandbox.instructions.prompt.md),或base_instructions显式替换;
  2. instructions(作为 "Agent instructions" 小节追加);
  3. 各 capability 的指令片段("Sandbox capability instructions");
  4. remote-mount 策略文本(build_remote_mount_policy_instructions(manifest),对应 "Sandbox remote mount policy");
  5. 渲染后的文件系统树("# Filesystem" 小节,render_manifest_description,深度为 3)。

注意resolve_instructions同时支持字符串与 callable(异步/同步均可),且动态指令与 hooks 通过get_public_agent(current_agent)观察公开的SandboxAgent,而不是内部克隆——prepare_sandbox_agent末尾的set_public_agent(prepared_agent, agent)建立了这条从克隆回指公开 agent 的链接。

3.4 Handoff 与嵌套 run 的边界

  • Handoff 停留在外层 run loop:切换的是"下一个回合由哪个 agent 执行",并为该沙箱 agent 选择另一个 agent 绑定的沙箱会话,不会产生嵌套 run;
  • Agent.as_tool()嵌套 run:拥有自己的嵌套 runner 与沙箱生命周期、自己的max_turns与审批流,从外层视角只算一次工具调用。

四、文件系统信任边界:POSIX 路径、宿主转换与归档防护

4.1 沙箱内一律按 POSIX 路径对待

无论宿主操作系统是 Windows 还是 macOS/Linux,沙箱内可见的每个路径都必须视为 POSIX 路径。严禁str(Path(...))str(PurePath(...))来生成、校验、比较或序列化沙箱路径——这些调用在 Windows 上会输出反斜杠。应使用:

  • PurePath.as_posix()
  • 或 workspace_paths.py 中的规范辅助函数,如coerce_posix_path()
def coerce_posix_path(path: str | PurePath) -> PurePosixPath: """Return a POSIX-flavored path for sandbox filesystem paths.""" if isinstance(path, PurePath): path = path.as_posix() else: path = path.replace("\\", "/") return PurePosixPath(path)

4.2 类型化路径对象与原始字符串的信任区分

信任边界必须区分两类输入:

  • 类型化的Path/PurePath(包括原生的 WindowsPath/PureWindowsPath):可转换为 POSIX 沙箱表示(windows_absolute_path()用于识别 Windows 绝对路径语法,workspace_paths.py);
  • 含反斜杠的原始字符串:当公共契约要求显式 POSIX 语法时,可能仍需要被拒绝。

不要用"静默规范化所有字符串"来掩盖输入校验失败。例如normalize_sandbox_cwd()对字符串中的\直接抛错("sandbox.cwd must use POSIX path separators"),而SandboxWorkspaceScope承载的是模型可见的相对路径基准(cwd为空表示工作区根)。从 docs/sandbox/guide.md 可知,SandboxRunConfig.cwd只改变exec_commandview_imageapply_patch等内置工具的相对路径解析,不改变Manifest.root或会话底层工作区边界。

4.3 宿主文件系统转换只在显式边界进行

解析 manifest、挂载目标、归档排除、snapshot、grant 或 provider 路径的代码,不得让宿主的Path实现改变沙箱路径的身份WorkspacePathPolicy(workspace_paths.py 起)集中了这一职责:

  • absolute_workspace_path()/normalize_path()/normalize_sandbox_path():校验路径落在工作区根或 extra grant 之下,越界抛InvalidManifestPathError
  • _raise_if_read_only_grant():对只读 grant 的写操作抛WorkspaceArchiveWriteError
  • resolve_symlinks仅在沙箱工作区是真实本地宿主目录(如UnixLocalSandboxSession)时启用,Docker/远程会话则走 POSIX 校验路径。

4.4LocalFile/LocalDir:信任基目录 + 使用期校验

LocalFileLocalDir是宿主侧输入。其约束为:

  • 默认基于 SDK 进程工作目录解析srcsrc必须留在该基目录内,除非被extra_path_grants覆盖;
  • 基目录之外的访问要求应用显式控制的extra_path_grants
  • 拒绝试图授权自身宿主访问的不可信 manifest。

并且必须在使用期(materialization 时)而非仅解析 manifest 时校验,防御:符号链接源、父目录被替换、平台路径别名、以及"校验与解包之间成员含义发生变化"的归档。对应实现位于 src/agents/sandbox/materialization.py,测试见 tests/sandbox/test_entries.py(如test_local_file_rejects_symlinked_source_ancestors,L388)与 tests/sandbox/test_docker.py(如test_docker_workspace_file_ops_reject_symlink_escape,L1646)。

4.5 归档解包防护

归档解包(src/agents/sandbox/session/archive_extraction.py)在写入前必须拒绝:

  • 路径穿越(traversal,如..逃逸,测试test_apply_patch_rejects_escape_root_path);
  • 不安全的链接(symlink escape);
  • 不支持的成员类型;

并强制成员数、字节数与解包后大小限制,且不物化无界成员列表。资源阈值由SandboxRunConfig.archive_limitsSandboxArchiveLimits(max_input_bytes=..., max_extracted_bytes=..., max_members=...))控制,见 src/agents/sandbox/runtime_session_manager.py 与 docs/sandbox/guide.md 的 "Materialization controls" 一节。

4.6 路径授权与凭据不落入持久化

  • extra_path_grants运行时访问,不是持久化工作区内容:snapshot 与persist_workspace()只包含工作区根,不包含任意授予路径;
  • 挂载或 provider 的凭据必须留在所属 adapter 内,不得出现在生成的 shell 命令、模型可见的错误、日志或序列化沙箱状态中。

SandboxPathGrant(workspace_paths.py)对pathhost_path都有严格校验:拒绝文件系统根(_raise_if_filesystem_root)、拒绝 UNC/设备路径、拒绝含..的 host_path、host_path配置时path必须为 POSIX 绝对路径。Docker 支持host_path把宿主路径映射为容器内不同的 POSIX 路径,而UnixLocalSandboxClient只支持同路径 grant,详见 docs/sandbox/clients.md。


五、Provider 与错误边界:归一化、可重试性与部分启动失败清理

5.1 错误归一化但保留诊断细节

后端失败应归一化为沙箱错误,但不得丢弃诊断所需的 provider 细节;可重试性(retryability)应在错误产生时显式保留,而不是事后靠匹配错误消息字符串推断。redact_mount_error_data装饰器(runtime_session_manager.py)在向上抛出前对挂载错误数据做脱敏,同时保留错误结构。

5.2 可移植路径与宿主路径分离

可移植的沙箱路径、宿主文件系统路径、provider 标识符三者必须分离。转换只属于后端或 materialization 边界,不进入面向 agent 的工具WorkspacePathPolicy的这一设计使同一套SandboxAgent定义可以无缝切换UnixLocalSandboxClientDockerSandboxClient或托管 provider(docs/sandbox/clients.md 的决策表)。

5.3 部分启动失败也要清理

临时克隆、挂载、sink 与依赖资源不仅要在正常关闭时清理,在部分启动失败时也要清理。cleanup()的 finally 块保证资源映射清空、current-agent 复位、并发守卫释放;多个资源逐个清理时,首个异常被记录但其余资源仍会继续清理,最终统一抛出首个错误(见_SandboxSessionResources.cleanupSandboxRuntimeSessionManager.cleanup的错误聚合逻辑,测试test_runner_owned_cleanup_redacts_client_delete_failure覆盖了 delete 失败路径)。

5.4 有界输出与私有运行时元数据

Capability 工具应上报有界输出,并保留 provider 的退出状态或结构化错误数据,但不向模型暴露私有运行时元数据(如宿主路径、凭据、内部连接信息)。


六、远程挂载的简洁性边界:默认单一生命周期

6.1 默认只支持一种窄生命周期

远程挂载默认收敛到一种窄生命周期:

  1. 在沙箱创建时声明挂载;
  2. 挂载内容保持在工作区持久化之外(snapshot / persist 流程会 detach 或跳过挂载路径);
  3. 关闭时卸载挂载。

当 tar 持久化或 hydration 需要 detach 挂载时,操作完成后必须立即恢复。挂载凭据必须始终是受信任的 live 配置,不得从序列化会话状态重建

6.2 动态挂载变更属于 opt-in provider 能力

动态挂载变更、native-snapshot 支撑的挂载、可恢复挂载都应是 opt-in 的 provider 能力,而非默认需求。如果特权挂载转换变得模糊不清,正确做法是停止沙箱,而不是引入协调/恢复状态机。除非 provider 暴露了可信原语、且变更得到聚焦的 provider 证据支持,否则不要添加:

  • 凭据解析器;
  • 刷新循环(refresh loops);
  • 持久化挂载注册表;
  • 动态挂载 API。

6.3 Vercel S3 adapter 的边界声明

Provider adapter 可以有意识地支持更窄的生命周期,但应在实施该策略的 adapter 状态旁明确记录边界,避免后续维护者把有意的排除误认为未完成功能。

Vercel S3 adapter(src/agents/extensions/sandbox/vercel/mounts.py 与 src/agents/extensions/sandbox/vercel/sandbox.py)遵循 create-time-only 形式:

  • 受信任的挂载配置仅存在于 live 会话中;
  • 含挂载的会话不能恢复(resume);
  • 挂载拓扑创建后不能改变

这在 docs/sandbox/clients.md 的托管平台挂载表中有明确对应:VercelSandboxClient仅支持 create-time-only 的 S3/S3-compatible 挂载,内联凭据需要allow_s3_credential_exposure=True

6.4 凭据暴露的显式确认机制

对于需要在模型可控的沙箱容器内运行挂载助手的场景,SDK 要求受信任的应用代码显式确认凭据暴露,且确认是运行时专用、不序列化的:

# 挂载级值,如内联访问密钥 manifest = manifest.with_in_container_mount_credential_exposure_acknowledged("data") # 更宽泛的权限,如托管/工作负载身份与外部凭据文件 manifest = manifest.with_in_container_mount_broad_credential_exposure_acknowledged("data")

FuseMountPatternblobfuse2发现环境 Azure 权限)与S3FilesMountPatternmount.s3files使用环境 IAM 权限)都需要 broad 确认。恢复含挂载的会话时,SDK 只在当前受信任 manifest 与持久化状态拥有完全相同的无凭据挂载拓扑时恢复凭据;缺失或不匹配会导致 resume 在沙箱启动前失败——序列化状态本身永远不授予权限。


七、回归审查清单:可执行的验证步骤

文档给出的 Review Checklist 本身就是一份可操作的安全与正确性清单,结合源码可映射为以下验证动作:

  1. 命名每个资源的 owner:每个 live 会话、provider client、挂载、进程、capability、临时资源都必须有明确 owner(注入会话 → 调用方;client 创建/恢复的会话 → Runner)。
  2. 分别测试五条会话路径:注入(injected)、恢复(resumed)、显式状态(explicitsession_state)、快照播种(snapshot-seeded)、全新会话(fresh)。SandboxRuntimeSessionManager._create_resources的分支结构(runtime_session_manager.py)就是这五条路径的直接映射。
  3. 验证会话映射在复杂场景下保持正确:handoff、重复 agent 名称、中断恢复、清理失败。对应测试如test_runner_resumed_handoff_materializes_manifest_for_new_sandbox_agent(test_runtime.py)、test_runner_restores_duplicate_name_sandbox_sessions_after_json_roundtrip(test_runtime.py)。
  4. 在适用平台测试路径与归档边界:宿主路径、符号链接、穿越、归档限制、凭据脱敏。归档相关测试见 tests/sandbox/test_extract.py,路径安全测试见 tests/sandbox/test_entries.py、tests/sandbox/test_apply_patch.py 与 tests/sandbox/test_docker.py。
  5. 走公开Runner路径做端到端验证:让 agent 准备、capability 绑定、持久化与清理一起执行,而不是只测内部组件。对应测试如test_runner_persists_workspace_and_tool_choice_state_across_sandbox_resume(test_runtime.py)、test_unix_local_runner_cleanup_preserves_resumed_caller_owned_workspace_root(test_runtime.py)。
  6. 每条沙箱路径校验/规范化/比较/序列化都要测PureWindowsPath输入:在每个宿主上测试,并确认原始反斜杠字符串保留其预期的校验行为。源码证据:coerce_posix_pathwindows_absolute_pathnormalize_sandbox_cwd\的拒绝逻辑(workspace_paths.py);测试证据:test_exec_command_tool_normalizes_raw_backslashes_before_workspace_scope(tests/sandbox/capabilities/test_shell_capability.py)、test_apply_patch_normalizes_backslashes_in_string_path(tests/sandbox/test_apply_patch.py)。

八、延伸阅读

  • 沙箱代理完整指南:SandboxAgentManifest、capabilities、生命周期与常见模式;
  • 沙箱客户端选择:本地、Docker、托管平台与挂载策略;
  • 沙箱运行时实现:代理准备、capability 绑定、清理编排;
  • 会话管理器实现:所有权、resume key、并发守卫、清理语义;
  • 路径策略实现:POSIX 转换、grant 校验、工作区边界;
  • 运行时测试 与 会话状态往返测试:所有权、重复名称、恢复与清理的回归验证。

【免费下载链接】openai-agents-pythonA lightweight, powerful framework for multi-agent workflows项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python

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

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

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

立即咨询