主导航
2026年4月7日

使用 Sandbox Agents 迁移旧代码库

代码现代化永远不会真正结束。过时的依赖项、安全风险、合规压力和陈旧的模式在大型代码库中不断堆积,而一个巨大的迁移 PR 既难以审查又充满合并风险。代码迁移代理应该在一个受控环境中工作,每次只执行一个任务:检查相关的代码库、编辑文件、运行检查并返回补丁。

本指南使用带有“工具控制(Harness)”的 Agents SDK,其中控制层位于沙箱外部:编排逻辑保留在受信任的主机进程中,而 Shell 命令和文件编辑则在隔离的执行环境中运行。这种分离使主机控制层能够使用密钥、工具和外部服务,同时仅向沙箱提供执行任务所需的文件和命令。

阅读完本指南后,您将能够:

  • 将代理控制层保持在执行 Shell 命令和文件编辑的沙箱环境之外
  • 将现代化工作拆分为任务级别的代码库片段
  • 使用测试、检查、构建产物和审计日志验证每个片段
  • 在不重写代理的情况下切换沙箱提供商

示例演示的是双服务代码迁移。每个服务在自己的沙箱中运行并返回自己的补丁包,其格式与用于创建单独审查和 CI 的 Pull Request 相同。在每个沙箱中,代理都会将 OpenAI 客户端封装从 Chat Completions 迁移到 Responses API。在此过程中,它会运行测试、修补应用和测试、运行编译检查、重新运行测试,并返回一份带有补丁的类型化迁移报告。

我们将通过本地 Docker 运行沙箱。提供商特定的代码被隔离在沙箱创建过程中,因此相同的控制层和代理可以指向托管的沙箱提供商(如 E2B 或 Cloudflare),而无需更改 SandboxAgent、工具、清单或提示词。

架构:作为工具的沙箱

Agents SDK harness running outside a swappable sandbox

受信任的主机进程拥有 Agents SDK 控制层、工具、MCP 服务器、凭据、策略和审计。执行环境仅接收当前任务的作用域工作区以及运行命令和编辑文件所需的沙箱功能。

另一种模式是启动一个编码代理,其控制层、代理循环、工具和文件系统都驻留在沙箱内。这确实可行,但它会将编排和工具集成推入运行生成代码的相同环境中。

在这种模式下,当代理需要文件系统、终端命令、测试运行或补丁时,控制层会调用沙箱。更广泛的代理栈保留在控制层侧。

工作流程

  1. 您的应用接收迁移请求,并将其拆分为任务级别的代码库片段。
  2. 对于每个任务,主机侧的 Agents SDK 控制层会启动一个代理并创建一个全新的沙箱。
  3. 控制层将该任务的代码库和迁移摘要暂存到沙箱中。
  4. 代理使用沙箱工具检查文件、编辑代码并运行测试。
  5. 主机接收任务的报告和补丁,编写审计日志,删除沙箱,并进入下一个任务。

要求

  • Python 3.10+
  • Docker,在本地运行以支持 Docker 沙箱示例
  • 一个 OpenAI API 密钥,导出为 OPENAI_API_KEY
  • 带有沙箱支持的 OpenAI Agents SDK
  • 可选:托管沙箱提供商凭据,例如用于 E2B 的 E2B_API_KEY,或用于 Cloudflare 的 Cloudflare Worker URL 和 API 密钥

请将 API 密钥保留在主机环境中。不要将它们添加到挂载的代码库或沙箱清单中。

安装依赖项

克隆 Cookbook 并进入此示例目录

git clone https://github.com/openai/openai-cookbook.git
cd openai-cookbook/examples/agents_sdk/sandboxed-code-migration

从该目录打开 sandboxed_code_migration_agent.ipynb 并安装以下依赖项。在运行完整的代理演示之前,请先启动 Docker。

%pip install -r requirements.txt --quiet

导入主机侧控制层

导入此示例使用的小型主机侧运行程序。它创建沙箱会话、启动代理循环、写入返回的构建产物、记录审计日志,并将提供商凭据保留在挂载的代码库之外。完整文件包含在 src/run_migration_agent.py 中;笔记本将重要部分提取到下面的演练中。

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

from agents import ModelSettings
from agents.mcp import MCPServer
from agents.sandbox import Manifest, SandboxAgent
from agents.sandbox.capabilities import ApplyPatch, Shell
from agents.sandbox.entries import LocalDir, LocalFile

EXAMPLE_ROOT = Path.cwd()
sys.path.insert(0, str(EXAMPLE_ROOT))

from src.run_migration_agent import (
    AGENT_INSTRUCTIONS,
    DEFAULT_MIGRATION_TASKS,
    DEVELOPER_INSTRUCTIONS,
    MigrationResult,
    MigrationTask,
    OPENAI_RESPONSES_MIGRATION_DOC_URL,
    run_migration_campaign,
)

migration_tasks = list(DEFAULT_MIGRATION_TASKS)
task = migration_tasks[0]

print("Migration campaign:")
for migration_task in migration_tasks:
    print(f"- {migration_task.name}: {migration_task.repo_path}")
print(f"\nInspecting first task: {task.name}")

1. 定义迁移任务

本 Cookbook 在 repo_fixtures/ 中包含了两个小的基准代码库。如果您按原样运行笔记本,主机控制层会将每个基准代码库挂载到一个全新的沙箱中(路径为 /workspace/repo),并要求代理遵循该代码库的 MIGRATION.md

任务列表将每个迁移片段指向一个本地代码库

@dataclass(frozen=True)
class MigrationTask:
    name: str
    repo_path: Path

    @property
    def migration_brief_path(self) -> Path:
        return self.repo_path / "MIGRATION.md"

DEFAULT_MIGRATION_TASKS = (
    MigrationTask(
        name="support_reply_service",
        repo_path=EXAMPLE_ROOT / "repo_fixtures" / "support_reply_service",
    ),
    MigrationTask(
        name="case_summary_service",
        repo_path=EXAMPLE_ROOT / "repo_fixtures" / "case_summary_service",
    ),
)

要将其适配到您自己的代码库,请替换任务的 repo_path 并编辑其 MIGRATION.md。通用的运行提示词无需更改,因为它会指示代理遵循挂载代码库的摘要。

在代理接触任务之前检查它:迁移摘要、OpenAI 客户端封装以及应用程序调用位置。

print(task.migration_brief_path.read_text())

现在检查两个主要代码目标:OpenAI 客户端封装和使用它的应用程序调用位置。

print((task.repo_path / "customer_support_bot" / "client.py").read_text())
print((task.repo_path / "customer_support_bot" / "replies.py").read_text())

2. 在代理进行任何编辑之前验证基准

在代理编辑代码库之前,请亲自运行相同的测试。代理在更改任何文件之前,也会在该任务的沙箱内运行此基准测试命令。

baseline = subprocess.run(
    [sys.executable, "-m", "unittest", "discover", "-s", "tests", "-t", "."],
    cwd=task.repo_path,
    text=True,
    capture_output=True,
    check=False,
)
print(baseline.stdout)
print(baseline.stderr)
assert baseline.returncode == 0
check = subprocess.run(
    [sys.executable, "-m", "compileall", "-q", "customer_support_bot", "tests"],
    cwd=task.repo_path,
    text=True,
    capture_output=True,
    check=False,
)
print(check.stdout)
print(check.stderr)
assert check.returncode == 0

3. 暂存沙箱工作区

清单是沙箱的边界。它告诉沙箱客户端要暂存哪些主机文件,以及它们应该出现在执行环境中的什么位置。在这里,我们将代理指令和一个任务代码库复制到 /workspace

对于实际的迁移,仅暂存目标签出代码、任务指令和运行所需的文件。将凭据、客户存储和内存留在主机控制层中。出于这个原因,下面的辅助程序保持精简:它仅暂存共享的代理指令和当前迁移任务的代码库。

def build_manifest(task: MigrationTask | None = None) -> Manifest:
    task = task or DEFAULT_MIGRATION_TASKS[0]
    return Manifest(
        root="/workspace",
        entries={
            "migration_agent/AGENTS.md": LocalFile(
                src=EXAMPLE_ROOT / "migration_agent" / "AGENTS.md"
            ),
            "repo": LocalDir(src=task.repo_path),
        },
    )

manifest = build_manifest(task)
print(f"manifest root: {manifest.root}")
for workspace_path in manifest.entries:
    print(workspace_path)

4. 定义沙箱代理

代理获得两个面向沙箱的功能:用于终端工作的 Shell() 和用于文件编辑的 ApplyPatch()。定义中的所有其他内容都留在主机控制层:指令、模型设置、MCP 服务器和类型化输出契约。

def build_agent(
    *,
    model: str,
    manifest: Manifest,
    mcp_servers: list[MCPServer] | None = None,
) -> SandboxAgent:
    return SandboxAgent(
        name="Code Migration Agent",
        model=model,
        instructions=AGENT_INSTRUCTIONS,
        developer_instructions=DEVELOPER_INSTRUCTIONS,
        default_manifest=manifest,
        capabilities=[Shell(), ApplyPatch()],
        mcp_servers=list(mcp_servers or []),
        model_settings=ModelSettings(tool_choice="required"),
        output_type=MigrationResult,
    )

agent = build_agent(model="gpt-5.4", manifest=manifest)
print(agent.name)
print([capability.type for capability in agent.capabilities])
print(agent.output_type)

可选:连接主机侧的 MCP 服务器

由于控制层在沙箱外部运行,它可以连接来自受信任主机进程的 MCP 服务器。沙箱不需要 MCP 凭据或广泛的网络访问权限。

此运行程序可以选择连接来自主机控制层的公共 OpenAI 文档 MCP。代理可以使用该文档上下文,而 Shell 命令和补丁仍然在沙箱中运行。

对于迁移,请保持此过程确定性。获取已批准的迁移指南,而不是要求代理在每次运行时搜索文档。

OPTIONAL_OPENAI_DOCS_MCP_URL = "https://developers.openai.ac.cn/mcp"

print("Optional host-side MCP:")
print(f"  server: {OPTIONAL_OPENAI_DOCS_MCP_URL}")
print(f"  pinned doc: {OPENAI_RESPONSES_MIGRATION_DOC_URL}")
print("To opt in for the full run, set this before the agent cell:")
print(f'  os.environ["OPENAI_DOCS_MCP_URL"] = "{OPTIONAL_OPENAI_DOCS_MCP_URL}"')

5. 运行迁移活动

完整运行是一个主机侧的迁移任务循环。对于每个任务,控制层会构建清单和代理,创建全新的沙箱会话,并将该会话传递给 Runner.run_streamed。任务完成后,主机将返回的补丁包写入 outputs/<task_name>/ 下,并在开始下一个任务之前删除沙箱。

manifest = build_manifest(task)
agent = build_agent(model=model, manifest=manifest, mcp_servers=mcp_servers)
client, session = await create_sandbox(backend, manifest, docker_image=docker_image)

try:
    async with session:
        result = Runner.run_streamed(
            agent,
            [{"role": "user", "content": f"Task name: {task.name}\n\n{prompt}"}],
            max_turns=30,
            run_config=RunConfig(
                sandbox=SandboxRunConfig(session=session),
                workflow_name=f"Sandboxed code migration: {task.name} ({backend})",
                tracing_disabled=not enable_hosted_tracing,
            ),
        )
        async for event in result.stream_events():
            if event.type == "run_item_stream_event" and event.name in {"tool_called", "tool_output"}:
                append_audit_event(audit_log_path, {"event": event.name})
finally:
    await client.delete(session)

上面的代码片段是 run_migration_task 的核心;下面的可运行单元格调用完整的辅助程序。运行过程受到保护,因此笔记本可以在不调用模型的情况下执行。当您想要端到端运行基于 Docker 的迁移时,请将 RUN_FULL_AGENT_DEMO 更改为 True

RUN_FULL_AGENT_DEMO = False
enable_hosted_tracing = False

# Optional: uncomment to let the host harness fetch the pinned Responses migration guide.
# os.environ["OPENAI_DOCS_MCP_URL"] = "https://developers.openai.ac.cn/mcp"

if RUN_FULL_AGENT_DEMO:
    campaign = await run_migration_campaign(
        tasks=migration_tasks,
        backend=os.getenv("SANDBOX_BACKEND", "docker"),
        model=os.getenv("OPENAI_MODEL", "gpt-5.4"),
        prompt=(
            "Migrate the mounted repo from Chat Completions to the Responses API. "
            "Follow migration_agent/AGENTS.md and repo/MIGRATION.md. "
            "Run the required baseline tests, patch the app and tests, "
            "run the check command, run the final tests, produce a diff, "
            "and return the structured migration result."
        ),
        docker_image=os.getenv("SANDBOX_DOCKER_IMAGE", "python:3.14-slim"),
        output_root=EXAMPLE_ROOT / "outputs",
        enable_hosted_tracing=enable_hosted_tracing,
    )
    for summary in campaign.task_summaries:
        print(f"{summary.task_name}: {summary.patch_path}")
else:
    print("Skipped. Set RUN_FULL_AGENT_DEMO=True to run the Docker-backed campaign.")

6. 检查返回的构建产物

主机运行程序将每个任务的类型化结果写入磁盘。沙箱可以在运行结束后消失;每个任务的报告、补丁、JSON 结果和审计日志仍保留在 outputs/<task_name>/ 中,活动摘要位于 outputs/batch_summary.json

artifact_names = [
    "migration_report.md",
    "migration.patch",
    "migration_result.json",
    "migration_audit.jsonl",
]

for migration_task in migration_tasks:
    print(f"\n=== {migration_task.name} ===")
    task_output_dir = EXAMPLE_ROOT / "outputs" / migration_task.name
    for artifact_name in artifact_names:
        path = task_output_dir / artifact_name
        if path.exists():
            print(f"\n--- {path.name} ---")
            print(path.read_text()[:3000])
        else:
            print(f"not generated yet: {path}")

可选:验证生成的构建产物

在向用户展示补丁或将其应用于真实代码库之前,主机可以检查每个返回的补丁、类型化结果和审计日志。此评估是确定性的:它读取活动输出,如果任何任务未产生预期的契约,则会失败。

if (EXAMPLE_ROOT / "outputs" / "batch_summary.json").exists():
    subprocess.run([sys.executable, "evals.py"], cwd=EXAMPLE_ROOT, check=True)
else:
    print("Skipped. Run the full agent demo before running artifact evals.")

7. 可选:切换沙箱提供商

本节展示了三种沙箱后端:用于本地运行的 Docker、用于托管沙箱的 E2B,以及用于基于托管 Worker 沙箱的 Cloudflare。模式相同:更改沙箱客户端,而不是代理。

Docker

client = DockerSandboxClient(docker.from_env())
session = await client.create(
    manifest=manifest,
    options=DockerSandboxClientOptions(image="python:3.14-slim"),
)

E2B

client = E2BSandboxClient()
session = await client.create(
    manifest=manifest,
    options=E2BSandboxClientOptions(
        sandbox_type=E2BSandboxType.E2B,
    ),
)

从 CLI 运行 E2B

export E2B_API_KEY="..."
python src/run_migration_agent.py --backend e2b

Cloudflare

client = CloudflareSandboxClient()
session = await client.create(
    manifest=manifest,
    options=CloudflareSandboxClientOptions(
        worker_url=os.environ["CLOUDFLARE_SANDBOX_WORKER_URL"],
        api_key=os.environ.get("CLOUDFLARE_SANDBOX_API_KEY"),
    ),
)

从 CLI 运行 Cloudflare

export CLOUDFLARE_SANDBOX_WORKER_URL="https://..."
export CLOUDFLARE_SANDBOX_API_KEY="..."
python src/run_migration_agent.py --backend cloudflare

从 CLI 运行 Docker

python src/run_migration_agent.py --backend docker

生产注意事项

生产代码应将编排、执行、数据访问和返回的输出保留在单独的信任边界之后。将每个迁移任务视为一个独立的审查单元。

边界生产模式
控制层 (Harness)将编排、工具、凭据、策略和审计保留在主机上。
沙箱仅暂存任务工作区。在其中运行命令和编辑。任务完成后将其销毁。
数据访问通过主机而不是直接通过沙箱路由客户存储和网络访问。
输出在向用户展示或应用更改之前,在主机中验证沙箱输出。

追踪 (Tracing) 和 ZDR

此示例通过 RunConfig.tracing_disabled=True 禁用了每次运行的托管追踪。要在使用此 Cookbook 的 CLI 时启用它,请传递 --enable-hosted-tracing。当您希望在全进程范围内禁用追踪时,Agents SDK 还支持全局 OPENAI_AGENTS_DISABLE_TRACING=1 环境变量。

后续步骤

要适配此模式,请用您自己的代码库、包或服务替换 migration_tasks。给每个任务一个签出代码和一份 MIGRATION.md。保持验证命令明确,并返回补丁以供审查,而不是自动应用主机侧的更改。添加确定性的评估,检查迁移契约,而不仅仅是测试套件是否通过。

当工作跨越许多包(例如 Jest 到 Vitest 的迁移)时,主机控制层可以使用代码库元数据或管理器代理来规划片段。每个片段都应产生此 Cookbook 所产生的结果:来自隔离沙箱的经过验证的补丁、报告和审计跟踪。

© . This website operates independently and is not affiliated with or endorsed by OpenAI, Inc. All brand names, logos, and trademarks are the property of their respective owners.