Prhub

#2805 rollout: shared miles-side layer for agent-function legs (session URL, sandbox credentials)

原始 PR 作者 nblintao 合并时间 2026-09-02 11:17 文件变更 14 提交数 9 评论 1 代码增减 +442 / -343

执行摘要

将三个 agent 功能脚本中重复的 session URL 和沙箱凭证处理逻辑提取到共享包 miles/rollout/agentic/。

OpenEnv、NeMo Gym 和 Harbor 三个 agent 功能的启动器和 worker 侧代码中存在完全相同的 session URL 解析逻辑(resolve_session_url)和沙箱凭证处理合约(PROVIDER_CREDENTIALS, sandbox_key_supply 等)。这种重复代码带来了维护负担和潜在的漂移风险。PR 描述明确指出:'Moves the logic that the OpenEnv, NeMo Gym and Harbor agent-function legs had in identical copies into a shared package',旨在将这些共享逻辑集中到一个地方,并为其建立直接、可独立测试的单元测试。

这是一个值得精读的高质量重构 PR。尽管行为无变化,但其设计决策(安全合约、torch-free 约束、测试策略)和实施方法(结构化提交、机械性证明)都值得学习。对于关注 agentic rollout、沙箱集成或安全凭证管理的工程师,本 PR 提供了很好的参考。核心风险点在于移动的正确性和共享包对 torch-free 的维护,已通过测试得到缓解。

讨论亮点

Review 讨论极少,仅有 Claude bot 的自动审查和 Shi-Dong 的一句 'LGTM!'。这表明重构的意图清晰,实现机械且正确,无明显争议。

实现拆解

  1. 创建共享包 miles/rollout/agentic/:新增 session.py 和 credentials.py 两个核心模块。session.py 中的 resolve_session_url 函数负责追加 /v1 后缀并根据 MILES_ROUTER_EXTERNAL_HOST 环境变量重写 URL 主机。credentials.py 集中定义了沙箱供应商凭证规范表 PROVIDER_CREDENTIALS(包含 Daytona, E2B, Modal),并实现了四个关键函数:sandbox_key_supply(确保只转发密钥文件路径,不转发密钥值)、forward_address(验证地址型环境变量不包含凭证信息,通过拒绝含 '@' 的值)、preflight_sdk(在启动时预检 SDK 是否安装,避免运行时失败)、resolve_provider_api_key(worker 侧解析 API 密钥,优先使用环境变量,其次读取文件)。
  2. 重构现有代码:examples/experimental/openenv/openenv_launch_common.py 中的本地凭证处理逻辑(_PROVIDER_CREDENTIALS, _sandbox_key_supply, _forward_address, _preflight_sdk)被移除,改为从新共享包导入公共版本。examples/experimental/nemo-gym/nemogym_agent_function.py 和 examples/experimental/openenv/openenv_agent_function.py 中的本地 _resolve_session_url 函数被删除,改为从 miles.rollout.agentic.session 导入 resolve_session_url。examples/experimental/openenv/tb2_sandbox_recipe.py 中的 resolve_api_key 函数被重命名并移至 credentials.py,更名为 resolve_provider_api_key。
  3. 全面测试覆盖:新增 tests/fast/rollout/agentic/ 测试目录,包含 test_session.py 和 test_credentials.py。这些测试使用 tmp_path 和 monkeypatch 模拟环境变量和文件系统,验证了 resolve_session_url 的 URL 重写行为、sandbox_key_supply 的所有分支(文件存在/不存在/为空/不可读、环境变量供给)、forward_address 对包含 '@' 的 URL 的拒绝、preflight_sdk 的成功与失败路径、resolve_provider_api_key 的优先级逻辑。特别地,test_shared_package_is_torch_free 验证了新共享包在导入时不依赖 torch,确保其能在 CPU-only 主机上被 NeMo Gym 适配器安全导入。
  4. 测试职责调整:原有的 tests/fast/examples/experimental/openenv/test_openenv_launch_common.py 被大幅精简,仅保留启动器自身的职责测试(如 test_every_backend_has_a_credential_spec),而将凭证合约的具体行为测试迁移到了共享包的新测试文件中,实现了测试职责的清晰分离。
  5. 细节优化:共享模块中移除了日志消息里的 'openenv' 前缀,使输出更通用(如 f"{provider} credential supply: ...")。PR 的提交历史采用了结构化的 'mechanical_provable' 和 'non_mechanical_provable' 分类,并提供了可复现的证明脚本,展示了高度可追溯的重构过程。
文件 模块 状态 重要度
miles/rollout/agentic/credentials.py 凭证管理 added 8.97
miles/rollout/agentic/session.py 会话路由 added 7.25
tests/fast/rollout/agentic/test_credentials.py 凭证测试 added 7.96

关键符号

resolve_session_url sandbox_key_supply forward_address preflight_sdk resolve_provider_api_key

关键源码片段

miles/rollout/agentic/credentials.py core-logic

新建的共享凭证处理核心模块,定义了供应商凭证规范表和所有关键的密钥供应、地址转发、SDK 预检函数,是本次重构的核心逻辑所在。

# 该函数负责在启动器侧处理沙箱凭证的供应。
# 核心安全原则:只转发密钥文件的路径,绝不转发密钥值,
# 因为 worker 的环境变量会通过 ray 的 runtime_env 以明文形式记录在日志和元数据中。
def sandbox_key_supply(
    env: dict[str, str],
    *,
    provider: str,
    key_env_vars: tuple[str, ...],
    file_env_var: str,
    arg_path: str,
    default_path: str,
    provision_hint: str,
) -> None:
    """Key-supply contract, shared by the sandbox backends: rollout workers
    get the provider credential from their OWN environment (e.g.
    platform-injected) or from a file they can read (a dotfile, K8s Secret
    mount, or shared-FS path). The launcher forwards only the file PATH, never
    the value: worker env rides ray's runtime_env, which exec_command_cpu
    echoes into driver logs and ray persists in job metadata, all in
    plaintext."""
    key_file = Path(arg_path or default_path).expanduser()
    try:
        key_present = bool(key_file.read_text(encoding="utf-8").strip())
    except OSError:
        key_present = False
    # 两种供应方式(文件或环境变量)都可接受,但无法从启动器验证 worker 节点的情况。
    # 因此需要明确告知正在使用哪种方式。
    names = " + ".join(key_env_vars)
    if key_present:
        # 文件可读:只将文件路径放入环境变量,由 worker 自己读取。
        env[file_env_var] = str(key_file)
        print(
            f"{provider} credential supply: file {key_file} "
            "(readable here; forwarding the path, workers read it themselves)",
            flush=True,
        )
    elif arg_path:
        # 如果用户显式配置了路径但文件不存在或为空,这是配置错误,应立即失败。
        raise ValueError(f"{file_env_var}={arg_path} is missing or empty")
    elif all(os.environ.get(var, "").strip() for var in key_env_vars):
        # 所有必需的环境变量都已设置(如平台注入),则假定 worker 也有。
        print(
            f"{provider} credential supply: worker environment ({names} "
            "set here; workers are assumed to have them in their env — "
            "single-host inheritance or platform-injected pod env)",
            flush=True,
        )
    else:
        # 既无可用文件,环境变量也不完整,则抛出错误并提供配置指引。
        raise ValueError(
            f"the {provider} sandbox mode needs credentials: put them in a file "
            f"({key_file}; {file_env_var} overrides) or in the "
            f"environment as {names}. Provision the file with:\n"
            f"  {provision_hint}"
        )
miles/rollout/agentic/session.py core-logic

新建的共享 session URL 解析模块,实现了将内部 session 地址重写为 agent 可访问的外部地址的逻辑,是 agent 功能的关键网络入口点。

"""The session-scoped policy URL an agent function hands to its agent."""
​
import os
from urllib.parse import urlparse, urlunparse
​
​
def resolve_session_url(base_url: str) -> str:
    """Build the OpenAI-compatible policy URL, rewriting host for off-cluster agents."""
    # 1. 标准化:追加 /v1 后缀,使其符合 OpenAI 兼容 API 的路径规范。
    session_url = f"{base_url}/v1"
    # 2. 主机重写:如果设置了 MILES_ROUTER_EXTERNAL_HOST 环境变量,
    # 则将内部主机地址(如 10.0.0.1:30000)替换为外部可路由的地址(如 trainer.tailnet:30000)。
    # 这允许运行在集群外部的 agent 进程访问内部的 session server。
    external_host = os.getenv("MILES_ROUTER_EXTERNAL_HOST")
    if external_host:
        parsed = urlparse(session_url)
        # 保留端口号(如果存在),否则只替换主机名。
        netloc = f"{external_host}:{parsed.port}" if parsed.port else external_host
        session_url = urlunparse(parsed._replace(netloc=netloc))
    return session_url

评论区精华

PR 整体审查与批准 other

Reviewer 'Shi-Dong' 留下评论 'LGTM!' 并批准了 PR。Claude bot 的自动审查未发现高置信度问题。

结论:PR 获得批准,未在审查阶段发现技术问题。 · 已解决

风险与影响

  1. 重构正确性:本 PR 主要是代码移动和去重,行为上声明无变化。核心风险在于移动过程中是否遗漏或错误修改了逻辑。鉴于 PR 提供了可重现的机械性证明,并且测试覆盖了凭证合约和 session URL 的所有关键路径,风险较低。但需注意 tb2_sandbox_recipe.py 中 resolve_api_key 函数的重命名和移动(commit 2cb5357),以及 tb2_sandbox_daytona.py 和 tb2_sandbox_e2b.py 中导入语句的相应调整,这是最容易出错的环节。
  2. 安全边界:forward_address 函数通过拒绝包含 '@' 的值来防止 URL 中嵌入凭证,这是一个关键的安全控制。测试 test_a_forwarded_url_may_not_smuggle_a_credential 对此进行了验证。风险在于未来新增的地址型环境变量是否可能意外包含合法字符 '@',导致误拒。
  3. 依赖影响:新增的 miles/rollout/agentic/ 包被设计为 torch-free,测试 test_shared_package_is_torch_free 确保了这一点。这对其在 CPU-only 环境(如 NeMo Gym 测试)中的可用性至关重要。如果未来开发者不慎在此包中引入 torch 依赖,将破坏这一约束。
  4. 测试冗余:重构后,tests/fast/examples/experimental/openenv/test_openenv_launch_common.py 中的测试变得非常简单,仅检查规范的存在性。这可能是预期的,但需确保这些保留的测试依然有价值。
  1. 用户影响:无直接功能变化,对终端用户透明。
  2. 系统影响:为 agent 功能的基础设施提供了一个更整洁、更安全的共享层。降低了未来为 OpenEnv、NeMo Gym 或 Harbor 添加新沙箱后端时的开发成本和出错概率。凭证处理逻辑的集中化也提升了安全审计的便利性。
  3. 团队影响:为负责 agentic rollout 的开发者提供了清晰的扩展点。共享的凭证合约和 session URL 解析逻辑减少了跨示例代码的同步负担。详尽的测试为未来修改这些核心逻辑提供了安全网。
重构正确性验证 安全合约维护 (torch-free, 凭证转发 )

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论