进阶 omnigent.ai 2026-10-14 06:47:13 · 19 阅读

第12章 Omnigent 社区沙箱 Provider 指南

# 社区沙箱 Provider 在不修改核心包的前提下,为 Omnigent host 增加新的运行环境。第三方 Python 包通过 omnigent.sandbox_providers entry-point 组注册 provider。在服务器上安装并配置后,即可通过 Web UI 或 API 提供托管会话。 沙箱 provider 决定 runner 在哪里执行,这与 Omnibox 是分开的——后者限制 agent 命令在该环境内能访问什么。如果想直接使用现成的 provider 而不是自己构建,请从 Cloud Sandbox Host 开始。 ## 选择 launcher 模型 每个注册的 launcher 必须直接或通过 ExecModelHostLauncher 继承 SandboxHostLauncher,两者都位于 omnigent.onboarding.sandboxes.base。
基类适用场景需要实现的方法
ExecModelHostLauncher 你的平台负责创建沙箱,并在其中暴露 shell 执行能力 prepare()、provision()、run(),以及 terminate() 负责清理。start_host() 由核心提供
SandboxHostLauncher 你的容器以 host 作为 entrypoint 启动,或由控制平面代为启动 prepare()、provision()、start_host(),以及 terminate() 负责清理。无需 exec 传输通道
先只支持托管会话。CLI bootstrap 是独立且可选的功能,需要文件传输、前台执行和额外的生命周期方法。下面的示例特意将其禁用。 ## 1. 创建包 使用一个唯一的 provider id,例如 acme。实现模块放在 omnigent.community.sandbox. 下:
omnigent-acme/
├── pyproject.toml
└── src/
    └── omnigent/
        └── community/
            └── sandbox/
                └── acme/
                    ├── __init__.py
                    ├── config.py
                    ├── launcher.py
                    └── plugin.py
acme/__init__.py 保持为空。不要提供核心 omnigent/__init__.py、omnigent/community/__init__.py 或 omnigent/community/sandbox/__init__.py 的替代版本。共享命名空间由核心提供,你的发行包只需包含自己的子树。 以下是基于 setuptools 的包定义:
# pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
 
[project]
name = "omnigent-acme"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["omnigent", "pydantic>=2", "click>=8"]
 
[project.entry-points."omnigent.sandbox_providers"]
acme = "omnigent.community.sandbox.acme.plugin:get_contribution"
 
[tool.setuptools.packages.find]
where = ["src"]
include = ["omnigent.community.sandbox.acme*"]
namespaces = true
把你的 provider SDK 加入依赖,发布前将 Omnigent 依赖限制在你测试过的版本。这些示例遵循当前源码的接口约定;请确认你的目标版本暴露相同的 API。 ## 2. 定义 provider 配置 config_model 用于校验服务器配置中的 sandbox.acme 块,并以 config= 的形式传给 launcher。在 provider 自己的命名空间下使用 Pydantic 模型:
# src/omnigent/community/sandbox/acme/config.py
from pydantic import BaseModel, ConfigDict, Field
 
 
class AcmeConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")
 
    namespace: str = Field(default="sandboxes", min_length=1)
extra="forbid" 会拒绝拼写错误的键,而不是默默忽略。校验在服务器解析配置时执行一次,在 registry 构造 launcher 时再执行一次。如果配置块缺失,模型会以无参数方式构造,因此可选配置项要提供默认值。 这个模型是可选的。不提供时,registry 会以无参数方式调用 launcher,也不会传入 provider 专属配置块。不要指望任意 YAML 键自动变成构造函数参数。 provider 的控制凭据应放在启动进程的环境变量或凭据存储中,而不是本示例的 YAML 里。在 prepare() 中解析它们,且绝不要把服务器的整个环境复制进沙箱。 ## 3. 实现 launcher 下面只是接口骨架,不是可用的 Acme 集成。在尝试启动前,请把四个 NotImplementedError 替换为对你平台 SDK 或 API 的调用。
# src/omnigent/community/sandbox/acme/launcher.py
from omnigent.onboarding.sandboxes.base import (
    ExecModelHostLauncher,
    RemoteCommandResult,
)
from omnigent.onboarding.sandboxes.types import SandboxCapabilities
 
from .config import AcmeConfig
 
 
class AcmeSandboxLauncher(ExecModelHostLauncher):
    provider = "acme"
 
    def __init__(self, *, config: AcmeConfig) -> None:
        self.config = config
 
    @property
    def capabilities(self) -> SandboxCapabilities:
        return SandboxCapabilities(
            managed_launch=True,
            programmatic_terminate=True,
        )
 
    def prepare(self) -> None:
        """Load the SDK and verify provider credentials; safe to repeat."""
        raise NotImplementedError("Implement provider preflight")
 
    def provision(self, name: str) -> str:
        """Create a sandbox in self.config.namespace and return its id."""
        raise NotImplementedError("Implement sandbox creation")
 
    def run(
        self, sandbox_id: str, command: str, *, check: bool = True
    ) -> RemoteCommandResult:
        """Run a shell command; raise on nonzero exit only when check=True."""
        raise NotImplementedError("Implement shell execution")
 
    def terminate(self, sandbox_id: str) -> None:
        """Delete the sandbox; an already-absent sandbox is success."""
        raise NotImplementedError("Implement idempotent deletion")
构造函数中不要做网络调用或创建资源:服务器也会构造 launcher 来读取其 capabilities 以供 /v1/info 使用。保持 plugin.py 和 config.py 的导入轻量;可选 SDK 留到 prepare() 或实际使用它们的方法中再加载。 ### 方法契约
  • prepare() 校验工具和凭据,但不预配计算资源。必须可重复调用。可预期的运维故障应通过 click.ClickException 抛出,消息中带上 provider 名称和有用的、不含机密的信息。
  • provision(name) -> str 返回一个不透明的沙箱 id,而不是 SandboxInfo 或 SDK 对象。如果在返回 id 之前创建失败,请清理已分配的部分资源;调用方无法清理一个从未收到的 id。
  • run(..., check=True) -> RemoteCommandResult 返回 returncode、stdout 和 stderr。check=True 时,非零退出码要抛出 click.ClickException;check=False 时,返回失败结果。如果 SDK 接收 argv 列表,用 ["sh", "-c", command] 调用 shell,不要按空格拆分命令。不要记录原始命令字符串:host 启动命令中包含凭据。
  • terminate(sandbox_id) 释放计算资源及其他 provider 侧资源。"已删除"应视为成功,但不要掩盖授权错误或临时故障。清理可以重试,且可能使用一个从未预配该沙箱的全新 launcher;要从 id 和已配置的凭据解析资源,而不是仅依赖内存中的句柄。不要要求清理实例先调用过 prepare()。
### exec-model 基类提供了什么 继承的 start_host() 会用你的 run() 完成以下操作:
  • 解析 $HOME 并创建 $HOME/workspace。
  • 通过 materialize_workspace() 克隆所需的仓库。
  • 在启动 host 前应用部署提供的 host_config。
  • 启动 omnigent host --server ...,并在其环境中注入 host 身份和启动 token。
  • 返回工作区的绝对路径:单仓库时是克隆目录,否则是父工作区。
请预装一个已包含 Omnigent、你支持的 harness 以及此流程所需 shell 工具的镜像。官方 ghcr.io/omnigent-ai/omnigent-host:latest 镜像可以作为起点;为保证部署可复现,请固定到某个已发布的镜像 tag。托管启动不会替你构建或安装 Omnigent wheel。 默认的 run_background() 使用 setsid、nohup 和一个 shell supervisor,把 host 输出写入 /tmp/omnigent-host.log。如果你的平台会在 exec 请求结束时杀掉子进程,请用 provider 支持的长驻进程机制重写 run_background()。不要为那种 exec 会话一关闭就立即退出的 host 报告成功。 如果需要用其他方式获取代码检出,可以重写 materialize_workspace(),不必替换整个 start_host()。 ### 不支持远程 exec 的 provider 直接继承 SandboxHostLauncher,并实现以下关键字参数契约:
from collections.abc import Callable, Sequence
 
from omnigent.onboarding.sandboxes.types import RepoWorkspace
 
 
def start_host(
    self,
    sandbox_id: str,
    *,
    token: str,
    host_id: str,
    host_name: str,
    server_url: str,
    repos: Sequence[RepoWorkspace] = (),
    host_config: dict[str, object] | None = None,
    on_stage: Callable[[str], None] | None = None,
) -> str:
    # Launch through your container entrypoint or control-plane API.
    raise NotImplementedError("Return the absolute in-sandbox workspace path")
你的实现必须让 omnigent host --server 运行时,OMNIGENT_HOST_TOKEN、OMNIGENT_HOST_ID 和 OMNIGENT_HOST_NAME 被设置为传入的值。host_config 要在 host 启动前安装好;如果能执行初始化命令,共享的 render_host_config_write_command() 辅助函数实现了核心的注入与替换语义。 RepoWorkspace 提供 url、branch 和 repo_name。物化所需仓库,并按与 exec 模型相同的工作区路径约定返回。如果传入了 on_stage,克隆前调用 on_stage("cloning"),host 启动前调用 on_stage("starting")。 对于创建计算资源时立即启动 host 的平台,provision() 可以只预留一个 id,把实际创建推迟到 start_host()。这样 Omnigent 能在新 host 连接前先注册启动 token。不要让 start_host() 阻塞 host 的整个生命周期;它返回后,服务器会等待 host 上线注册。 入口点方式和控制平面方式可分别参考内置的 Kubernetes launcher 和 Gensee launcher。 ## 4. 注册贡献
# src/omnigent/community/sandbox/acme/plugin.py
from omnigent.onboarding.sandboxes.registry import (
    SandboxProviderContribution,
    SandboxProviderMetadata,
)
 
from .config import AcmeConfig
 
 
def get_contribution() -> SandboxProviderContribution:
    return SandboxProviderContribution(
        name="omnigent-acme",
        providers={
            "acme": SandboxProviderMetadata(
                name="acme",
                launcher_class=(
                    "omnigent.community.sandbox.acme.launcher:AcmeSandboxLauncher"
                ),
                config_model=AcmeConfig,
            ),
        },
    )
contribution 名称用于在诊断信息中标识包。provider 键、metadata 名称和 launcher 的 provider 属性必须一致。一个 contribution 可以注册多个 provider,但不能覆盖已注册的名称。 registry 要求 launcher 的导入路径和 config 模型所在模块都位于 omnigent.community.sandbox.* 之下。launcher 以导入字符串的方式引用,因此在 entry-point 发现阶段无需加载。 SandboxProviderMetadata 还接受 managed_token_ttl_s,即托管 host 启动 token 的有效期(秒)。缺省时目前为 25 小时。请根据沙箱的生命周期选择合适的正值:这是认证有效期,不是 provider 侧的超时,也不是删除计算资源的指令。 ## 5. 安装与配置 把插件安装到与 Omnigent 服务器相同的 Python 环境中。在插件检出目录下执行:
uv pip install -e .
对于已发布的插件,在该环境中安装其发行包即可。如果 Omnigent 是以 uv tool 方式安装的,用 uv tool inject omnigent omnigent-acme 将插件加入该工具的环境。安装或升级后请重启服务器:registry 的发现结果是按进程缓存的。 配置服务器:
sandbox:
  provider: acme
  server_url: https://your-server.example.com
  acme:
    namespace: sandboxes
sandbox.server_url 必须能从沙箱内访问,并支持 host 回连 Omnigent。provider SDK 在服务器侧认证;host 收到的是服务器另行签发的启动 token。不要把 provider 控制凭据放进沙箱客户机,host token 应通过安全的环境或密钥机制传递,不要记录日志或放入进程参数。 launcher 实现完成后,在 Web UI 中选择 New Sandbox。下图展示了正常的托管生命周期,以及失败清理、空闲恢复、删除和过期沙箱回收。 托管沙箱 provider 生命周期:从预配、启动、恢复到终止 Omnigent 负责 host 注册、token 签发和会话绑定。你的 provider 负责计算资源、工作区准备、进程启动和清理。唤醒失败会保留已停止的沙箱及其工作区,供再次尝试。删除和回收会调用 provider 幂等的 terminate();临时性的清理失败会保持 pending,等待之后重试。 runner 的配置参见 host configuration,可选的过期沙箱回收参见 cloud-host 指南。 ## 可选能力 返回一个明确的 SandboxCapabilities 对象。其字段默认为 False;只有在对应行为已实现并经过测试时才设置标志。
能力契约
managed_launch实现服务器托管的 prepare / provision / start_host 流程
programmatic_terminate实现幂等的 terminate()。可靠的自动清理依赖它
cli_bootstrap支持下文描述的 omnigent sandbox create / connect 独立流程
file_copy实现 put(sandbox_id, local_path, remote_path)
streaming_exec实现 stream_exec(..., pty=False),返回 RemoteProcess
foreground_exec实现 exec_foreground(sandbox_id, command) -> int,包括 Ctrl-C 清理
local_port_forward实现 forward_local_port() 上下文管理器,用于本地到沙箱的回调转发
resume_stopped实现 resume(sandbox_id),在相同 id 下恢复计算资源和持久存储。host 的重启由核心单独处理
snapshot_restoreresume 恢复的是挂起时的快照而非冷启动;需与 resume_stopped 配合才有意义
multi_repo处理多个 RepoWorkspace 条目并返回其父工作区。需要显式开启,exec-model 子类也不例外
classifies_runner_by_agent在 start_host() 中接受额外的 agent_name 关键字参数,并把解析出的内置 agent 写入 provider metadata
服务器目前将已配置的社区 provider 视为托管启动选项。managed_launch=False 并不能把已配置的插件从选择器中隐藏;只配置真正实现了托管启动的 provider。 如果你的平台需要续期,可实现一个开销小且幂等的 keep_alive();服务器会对存活的托管 runner 定期调用它。 状态未知时,is_running() 可以返回 True、False 或 None。 回收器可以在每个工作区和 provider 上复用同一个 launcher。如果后台终止需要工作区级别的凭据,可以用上下文管理器重写 reaper_identity(workspace_id)。沙箱已不存在时,终止操作仍必须成功。 ## 增加 CLI bootstrap CLI 支持需要 ExecModelHostLauncher 的子类并设置 cli_bootstrap=True。除基本的 exec-model 方法外,还要实现 attach()、keep_alive()、put()、wheel_install_command() 和 exec_foreground()。要支持沙箱内的 App OAuth 流程,还需实现 stream_exec() 和 forward_local_port()。RemoteProcess.lines 在多次访问时必须返回同一个迭代器;其 close() 必须幂等。 CLI 用 config 模型的默认值构造社区 provider,而不是服务器的 sandbox.acme 块。请提供可用的默认值,或在 launcher 中解析 CLI 专属配置。create 会从 Omnigent 源码检出构建并分发 wheel;仅安装插件无法提供该源码检出。 实现 CLI 支持后,使用:
omnigent sandbox create --provider acme \
  --server https://your-server.example.com \
  --repo-root /path/to/omnigent
 
omnigent sandbox connect --provider acme \
  --sandbox-id YOUR_SANDBOX_ID \
  --server https://your-server.example.com
没有本地端口转发时,create 会跳过沙箱内登录流程,但不会绕过目标服务器要求的认证。仅托管模式的插件(包括上面的骨架)应改用 Web UI/API 流程。 ## 验证与排障 先在不分配计算资源的情况下检查发现和配置构造。用安装了插件的那个环境中的 Python 解释器运行:
from omnigent.onboarding.sandboxes.registry import instantiate, plugin_state
 
state = plugin_state()
print("Registered providers:", state.names())
print("Plugin load errors:", state.load_errors)
assert "acme" in state, state.load_errors
 
launcher = instantiate("acme", config={"namespace": "test-sandboxes"})
assert launcher.provider == "acme"
assert launcher.config.namespace == "test-sandboxes"
assert launcher.capabilities.managed_launch
assert not launcher.capabilities.cli_bootstrap
然后用 SDK 假对象和一个隔离的 provider 账号测试实现:
  • 在分配计算资源之前,拒绝未知的配置键和非法值。
  • 验证 run() 能捕获输出,并且对 check 的两种取值行为都正确。
  • 分别用空工作区、以及带仓库和分支的工作区启动托管会话,验证返回的工作区以及 host 是否上线。
  • 验证 host 能在预配/exec 连接关闭后继续存活。
  • 演练启动失败和删除;确认资源被释放。调用 terminate() 两次,包括从新构造的 launcher 调用。
  • 仅在声明支持时,才测试 resume、多仓库工作区和 CLI bootstrap。
  • 检查日志和 provider 操作记录,确认没有意外泄露 token。
registry 和生命周期的测试示例参见 test_registry.py、test_base.py 和 test_managed_hosts.py。
症状检查项
state.names() 中没有该 Provider确认包已安装到服务器环境、entry-point 组名精确为 omnigent.sandbox_providers,且发现流程已重启。检查 state.load_errors
命名空间或重名错误launcher 和 config 模型都要放在你自己的 omnigent.community.sandbox.* 子树下;选择唯一的 provider id
不是 SandboxHostLauncher 的子类注册 SandboxHostLauncher 或 ExecModelHostLauncher 的子类,而不是只有传输能力的类
sandbox.acme 配置非法与模型的字段和类型保持一致。配置块缺失时仍需满足模型的必填字段
host 一直不上线检查服务器可达性、镜像中的 Omnigent 安装、host 身份/token 的传递,以及 host 进程能否在 exec 结束后存活
损坏的 entry point 会被记录到 load_errors 中,且不会中断 registry 的发现过程。但配置一个缺失或被拒绝的 provider 仍会导致服务器配置校验失败,因为其名称未知。launcher 的导入和构造发生在之后,因此仅凭注册名称并不能证明其 SDK 依赖或运行时可用。 插件会在 Omnigent 服务器进程中执行 Python 代码。命名空间规则只是打包约束,不是安全沙箱。只安装你信任的 provider 包。

评论 (0)