使用沙盒代理迁移遗留代码库

本教程介绍如何利用沙盒代理安全地迁移遗留代码库。通过将编排层置于沙盒之外,代理在隔离环境中执行命令和文件编辑,避免大规模迁移PR的风险。教程涵盖任务分片、沙盒清单构建、代理定义、运行迁移、产物验证及更换沙盒提供商,并提供生产环境注意事项和追踪配置。

技术背景

代码现代化是持续过程,遗留依赖、安全风险、合规压力不断累积。一次性大规模迁移PR难以审查且合并风险高。沙盒代理模式将代理执行环境隔离,每次处理一个任务,降低风险。适用于需要逐步迁移大型代码库、确保安全性和可审计性的场景。限制包括沙盒资源限制、网络隔离可能影响依赖下载等。

核心原理

核心原理是将代理框架(编排、工具、凭据)置于可信主机,而shell命令和文件编辑在隔离沙盒中执行。沙盒作为工具被harness调用,仅接收任务所需文件和命令。数据流:应用接收迁移请求→拆分为任务→harness创建沙盒并暂存仓库和简报→代理使用沙盒工具检查、编辑、测试→主机接收报告和补丁,写入审计日志,删除沙盒。关键设计决策:沙盒边界由Manifest定义,仅暂存必要文件;代理能力限定为Shell和ApplyPatch;输出为类型化结果,便于验证。

架构分析

架构分为主机侧和沙盒侧。主机侧包含Agents SDK harness、工具、MCP服务器、凭据、策略和审计。沙盒侧仅包含任务工作区、命令执行和文件编辑能力。流程:1) 应用拆分任务;2) harness创建沙盒并暂存;3) 代理在沙盒内工作;4) 主机接收结果并清理。组件关系:SandboxAgent定义代理,Manifest定义沙盒内容,SandboxClient创建会话,Runner运行代理。设计思想:信任边界分离,最小权限原则,可替换沙盒提供商。

完整实战教程

代码现代化永无止境。过时的依赖、安全风险、合规压力以及遗留模式不断在大型代码库中累积,而一次大规模的迁移PR难以审查且合并风险高。代码迁移代理应在受控环境中工作,一次只处理一个范围明确的任务:检查相关仓库、编辑文件、运行检查并返回补丁。

本教程使用Agents SDK,将编排层置于沙盒*之外*:编排保留在可信的主机进程中,而shell命令和文件编辑在隔离的执行环境中运行。这种分离使主机框架可以使用密钥、工具和外部服务,同时仅向沙盒提供任务所需的文件和命令。

完成本教程后,您将能够:

示例为双服务代码迁移。每个服务在各自沙盒中运行并返回自己的补丁包,其格式与您用于分别打开PR进行审查和CI的格式相同。在每个沙盒中,代理将OpenAI客户端包装器从Chat Completions迁移到Responses API。在此过程中,它运行测试、修补应用和测试、运行编译检查、重新运行测试,并返回带有补丁的类型化迁移报告。

我们将在本地使用Docker运行沙盒。提供商特定代码隔离在沙盒创建中,因此相同的框架和代理可以指向托管沙盒提供商(如E2B或Cloudflare),而无需更改SandboxAgent、工具、清单或提示。

架构:沙箱作为工具

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

另一种模式是启动一个编码代理,其 harness、代理循环、工具和文件系统都位于沙箱内。这种方法可行,但它将编排和工具集成推到了与运行生成代码相同的环境中。

在这种模式中,沙箱是 harness 在代理需要文件系统、终端命令、测试运行或补丁时调用的东西。更广泛的代理栈保留在 harness 侧。

流程:

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

环境要求

将 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. 定义迁移任务

本手册在 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.com/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.com/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 用于托管沙箱,Cloudflare 用于托管 worker 支持的沙箱。模式相同:更换沙箱客户端,而不是代理。

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,
    ),
)

从命令行运行 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"),
    ),
)

从命令行运行 Cloudflare:

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

从命令行运行 Docker:

python src/run_migration_agent.py --backend docker

生产注意事项

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

| 边界 | 生产模式 |

| --- | --- |

| 控制框架 | 将编排、工具、凭据、策略和审计保留在主机上。 |

| 沙箱 | 仅暂存任务工作区。在其中运行命令和编辑。任务完成后将其拆除。 |

| 数据访问 | 通过主机路由客户存储和网络访问,而不是直接通过沙箱。 |

| 输出 | 在主机中验证沙箱输出,然后再向用户显示或应用更改。 |

追踪与 ZDR

此示例通过 RunConfig.tracing_disabled=True 在每次运行时禁用托管追踪。若要在运行本教程的 CLI 时选择启用,请传递 --enable-hosted-tracing。当您希望在整个进程中禁用追踪时,Agents SDK 还支持全局环境变量 OPENAI_AGENTS_DISABLE_TRACING=1。

后续步骤

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

当任务跨越多个包时,例如 Jest 到 Vitest 的迁移,主机框架可以使用仓库元数据或管理器代理来规划分片。每个分片应产生与此指南相同的结果:来自隔离沙箱的经过验证的补丁、报告和审计跟踪。

常见问题

常见错误:1) 沙盒内无法访问网络导致依赖安装失败,解决:在沙盒镜像中预装依赖或配置代理。2) 清单路径错误导致文件未挂载,检查Manifest的root和entries。3) 代理工具调用失败,确保capabilities包含所需工具。4) 输出类型不匹配,检查output_type定义。排查方法:查看审计日志(migration_audit.jsonl),检查沙盒会话日志,使用本地Docker调试。生产优化:使用托管沙盒(E2B/Cloudflare)提高可扩展性,启用追踪(--enable-hosted-tracing),定期清理沙盒资源。

企业应用扩展

可扩展方向:1) 将任务分片策略自动化,使用仓库元数据或管理器代理规划分片。2) 集成更多沙盒提供商,如AWS Lambda。3) 增加验证步骤,如静态分析、安全扫描。4) 实现补丁自动应用和回滚机制。5) 结合CI/CD流水线,实现迁移任务的自动触发和审批。

总结

本教程介绍如何利用沙盒代理安全地迁移遗留代码库。通过将编排层置于沙盒之外,代理在隔离环境中执行命令和文件编辑,避免大规模迁移PR的风险。教程涵盖任务分片、沙盒清单构建、代理定义、运行迁移、产物验证及更换沙盒提供商,并提供生产环境注意事项和追踪配置。

内容来源与验证

继续系统学习

站内还有更多完整实战教程

加入会员,解锁全部教程、完整代码与持续更新内容。

加入会员
查看全部AI 智能体教程