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)为:
- 运行 pre-stop hooks(
run_pre_stop_hooks()); - 调用
stop()持久化 snapshot 支撑的工作区状态; shutdown()关闭沙箱;- 通过
client.delete(session)删除 provider 资源(当 client 存在且会话类型为SandboxSession时); _aclose_dependencies()关闭会话级依赖。
关键语义:
- 清理必须幂等:
self._cleaned标志保证同一资源只清理一次,且全程由asyncio.Lock串行化,避免并发aclose()竞态; - 清理失败也必须释放并发守卫:
cleanup()中的finally块总会执行self._release_agents(),即使 pre-stop hook、stop 或 persistence 失败,也会把SandboxAgent的active_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实现):
- 注入的 live 会话:
run_config.sandbox.session直接复用; RunState携带的可恢复沙箱状态:_resume_state_payload_for_agent()从run_state._sandbox中按 resume key 取回序列化状态;- 显式
SandboxRunConfig.session_state:client.deserialize_session_state(explicit_state)后client.resume(...); - 新建会话:以
run_config.sandbox.manifest或agent.default_manifest为输入调用client.create(...)。
Manifest 与 snapshot 输入只用于播种全新会话,不会覆盖注入或恢复的工作区——这正是文档强调"Manifest 是 fresh-session 的工作区契约,而不是每个 live 沙箱的完整真相来源"的原因。
2.2 三类状态不可混用
| 状态载体 | 含义 | 用途 |
|---|---|---|
RunState的 sandbox payload | Runner 管理的序列化沙箱状态(含backend_id、current_agent_key、session_state、sessions_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 要求Shell,Filesystem提供apply_patch与view_image,详见 docs/sandbox/guide.md 的 capabilities 表格。
3.3 指令的固定拼接顺序
最终指令按文档化顺序构建,build_sandbox_instructions()(runtime_agent_preparation.py)的实现顺序与文档完全一致:
- SDK 默认沙箱 base prompt(
agents.sandbox.instructions.prompt.md),或base_instructions显式替换; instructions(作为 "Agent instructions" 小节追加);- 各 capability 的指令片段("Sandbox capability instructions");
- remote-mount 策略文本(
build_remote_mount_policy_instructions(manifest),对应 "Sandbox remote mount policy"); - 渲染后的文件系统树("# 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_command、view_image、apply_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:信任基目录 + 使用期校验
LocalFile与LocalDir是宿主侧输入。其约束为:
- 默认基于 SDK 进程工作目录解析
src,src必须留在该基目录内,除非被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_limits(SandboxArchiveLimits(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)对path与host_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定义可以无缝切换UnixLocalSandboxClient、DockerSandboxClient或托管 provider(docs/sandbox/clients.md 的决策表)。
5.3 部分启动失败也要清理
临时克隆、挂载、sink 与依赖资源不仅要在正常关闭时清理,在部分启动失败时也要清理。cleanup()的 finally 块保证资源映射清空、current-agent 复位、并发守卫释放;多个资源逐个清理时,首个异常被记录但其余资源仍会继续清理,最终统一抛出首个错误(见_SandboxSessionResources.cleanup与SandboxRuntimeSessionManager.cleanup的错误聚合逻辑,测试test_runner_owned_cleanup_redacts_client_delete_failure覆盖了 delete 失败路径)。
5.4 有界输出与私有运行时元数据
Capability 工具应上报有界输出,并保留 provider 的退出状态或结构化错误数据,但不向模型暴露私有运行时元数据(如宿主路径、凭据、内部连接信息)。
六、远程挂载的简洁性边界:默认单一生命周期
6.1 默认只支持一种窄生命周期
远程挂载默认收敛到一种窄生命周期:
- 在沙箱创建时声明挂载;
- 挂载内容保持在工作区持久化之外(snapshot / persist 流程会 detach 或跳过挂载路径);
- 关闭时卸载挂载。
当 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")FuseMountPattern(blobfuse2发现环境 Azure 权限)与S3FilesMountPattern(mount.s3files使用环境 IAM 权限)都需要 broad 确认。恢复含挂载的会话时,SDK 只在当前受信任 manifest 与持久化状态拥有完全相同的无凭据挂载拓扑时恢复凭据;缺失或不匹配会导致 resume 在沙箱启动前失败——序列化状态本身永远不授予权限。
七、回归审查清单:可执行的验证步骤
文档给出的 Review Checklist 本身就是一份可操作的安全与正确性清单,结合源码可映射为以下验证动作:
- 命名每个资源的 owner:每个 live 会话、provider client、挂载、进程、capability、临时资源都必须有明确 owner(注入会话 → 调用方;client 创建/恢复的会话 → Runner)。
- 分别测试五条会话路径:注入(injected)、恢复(resumed)、显式状态(explicit
session_state)、快照播种(snapshot-seeded)、全新会话(fresh)。SandboxRuntimeSessionManager._create_resources的分支结构(runtime_session_manager.py)就是这五条路径的直接映射。 - 验证会话映射在复杂场景下保持正确: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)。 - 在适用平台测试路径与归档边界:宿主路径、符号链接、穿越、归档限制、凭据脱敏。归档相关测试见 tests/sandbox/test_extract.py,路径安全测试见 tests/sandbox/test_entries.py、tests/sandbox/test_apply_patch.py 与 tests/sandbox/test_docker.py。
- 走公开
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)。 - 每条沙箱路径校验/规范化/比较/序列化都要测
PureWindowsPath输入:在每个宿主上测试,并确认原始反斜杠字符串保留其预期的校验行为。源码证据:coerce_posix_path、windows_absolute_path与normalize_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)。
八、延伸阅读
- 沙箱代理完整指南:
SandboxAgent、Manifest、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),仅供参考