Skip to content

Commit 70f0bbc

Browse files
committed
feat(sandbox): add RemoteOperator with SandboxNextProvider
- Add RemoteOperatorConfig to rock/config.py (endpoint, api_key, region, etc.) - Add RemoteProvider Protocol (lifecycle + optional Template API) - Implement SandboxNextProvider (httpx AsyncClient, 5xx retry, state mapping) - Implement RemoteOperator (delegates to provider, Redis merge, graceful fallback) - Extend OperatorContext and OperatorFactory for 'remote' type - Wire remote_config in admin/main.py - Add 43 unit tests (mock httpx MockTransport + AsyncMock provider) - Add design doc (docs/proposals/remote-operator.md) - Add SandboxNext OpenAPI spec (docs/proposals/sandbox-next.yaml) - Update CLAUDE.md module map resolves #1330
1 parent 4889118 commit 70f0bbc

15 files changed

Lines changed: 2655 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ uv run ruff format . # Format
3939
```
4040
rock/
4141
├── admin/ # Admin service: API routers, Ray service, scheduler, metrics
42-
├── sandbox/ # SandboxManager, Operators (Ray/K8s), SandboxActor
42+
├── sandbox/ # SandboxManager, Operators (Ray/K8s/OpenSandbox/Remote), SandboxActor
4343
├── deployments/ # AbstractDeployment → Docker/Ray/Local/Remote, configs, validator
4444
├── rocklet/ # Lightweight sandbox runtime server
4545
├── sdk/ # Client SDK: Sandbox client, agent integrations, EnvHub client, JobViewer
@@ -53,7 +53,7 @@ rock/
5353

5454
### Key Patterns
5555

56-
- **Operator pattern**: `AbstractOperator``RayOperator` / `K8sOperator` — decouples scheduling from execution
56+
- **Operator pattern**: `AbstractOperator``RayOperator` / `K8sOperator` / `OpenSandboxOperator` / `RemoteOperator` — decouples scheduling from execution. RemoteOperator delegates to a `RemoteProvider` Protocol (first impl: `SandboxNextProvider`) with Redis info merge and graceful template API fallback.
5757
- **Deployment hierarchy**: `AbstractDeployment``DockerDeployment``RayDeployment`, plus `LocalDeployment`, `RemoteDeployment`
5858
- **Actor pattern (Ray)**: `SandboxActor` (remote, detached) wraps a `DockerDeployment` instance
5959
- **Config flow**: `SandboxManager``DeploymentManager.init_config()` (normalize config) → `Operator.submit()` (orchestrate)
@@ -153,7 +153,7 @@ All defined in `rock/env_vars.py` with lazy evaluation via module `__getattr__`.
153153

154154
Loaded by `RockConfig.from_env()`. Files: `rock-local.yml`, `rock-dev.yml`, `rock-test.yml`.
155155

156-
Key sections: `ray`, `k8s`, `runtime` (operator_type, standard_spec, max_allowed_spec), `redis`, `proxy_service`, `scheduler`.
156+
Key sections: `ray`, `k8s`, `runtime` (operator_type, standard_spec, max_allowed_spec), `redis`, `proxy_service`, `scheduler`, `opensandbox`, `remote`.
157157

158158
## Git Workflow
159159

docs/proposals/remote-operator.md

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# Remote Operator 设计方案
2+
3+
## 1. 背景
4+
5+
ROCK Admin 通过 `OperatorFactory``runtime.operator_type` 创建对应的 Operator 实例,现有支持 `ray``k8s``opensandbox` 三种后端。这些 Operator 均与特定基础设施强绑定(Ray 集群、K8s CRD、OpenSandbox SDK),无法通用地接入任意远端 sandbox 平台。
6+
7+
本方案新增 **Remote Operator**,以 HTTP REST 形式接入远端 sandbox 平台,并通过 **Provider 抽象** 支持多平台适配。设计模式参照 K8s Operator 的 `K8sProvider` Protocol + `BatchSandboxProvider` 实现。
8+
9+
### 现有架构
10+
11+
```
12+
SandboxManager
13+
└── AbstractOperator (submit / restart / get_status / stop / delete)
14+
├── RayOperator → Ray Actor
15+
├── K8sOperator → K8sProvider Protocol → BatchSandboxProvider (K8s CRD)
16+
└── OpenSandboxOperator → OpenSandboxClient (SDK)
17+
18+
ProxyService
19+
├── SandboxProxyService → Rocklet RPC (Ray / K8s)
20+
└── OpenSandboxProxyService → OpenSandboxBackend (SDK)
21+
```
22+
23+
K8s Operator 的关键设计:`K8sProvider` 是一个 `Protocol`,定义 `submit`/`get_status`/`stop` 三个核心方法;`K8sOperator` 作为薄封装层,将生命周期调用委托给 provider,自身只负责 Redis 信息合并。Remote Operator 复用这一模式。
24+
25+
## 2. 目标与非目标
26+
27+
**目标:**
28+
29+
- 新增 `remote` operator 类型,通过 `runtime.operator_type: "remote"` 启用
30+
- 定义 `RemoteProvider` Protocol,支持不同远端平台适配
31+
- 实现首个 provider:`SandboxNextProvider`(SandboxNext Gateway REST API)
32+
- 不依赖 Ray,启动时跳过 Ray 初始化
33+
- 命令/文件执行复用现有 `SandboxProxyService`(Rocklet RPC),远端平台运行 Rocklet
34+
35+
**非目标(Phase 1):**
36+
37+
- 不实现 archive / restore
38+
- 不实现 restart(远端平台语义各异)
39+
- 不新增独立 ProxyService
40+
41+
## 3. 架构设计
42+
43+
### 3.1 整体架构
44+
45+
```
46+
SandboxManager
47+
└── AbstractOperator
48+
└── RemoteOperator # 薄封装,委托给 provider
49+
└── RemoteProvider (Protocol) # provider 抽象接口
50+
└── SandboxNextProvider # 首个实现:HTTP REST
51+
└── (future providers) # E2B, Modal, 自定义平台 ...
52+
53+
ProxyService (复用现有)
54+
└── SandboxProxyService → Rocklet RPC (与 Ray / K8s 一致)
55+
```
56+
57+
### 3.2 RemoteProvider Protocol
58+
59+
定义文件:`rock/sandbox/operator/remote/provider.py`
60+
61+
**生命周期方法(必须实现):**
62+
63+
| 方法 | 签名 | 说明 |
64+
|------|------|------|
65+
| `submit` | `(config: DockerDeploymentConfig, user_info: dict) → SandboxInfo` | 创建沙箱,返回含 `sandbox_id``state``extended_params`(含平台 ID)的 SandboxInfo |
66+
| `get_status` | `(remote_sandbox_id: str) → SandboxInfo \| None` | 查询实时状态,映射为 Rock State;404 返回 None |
67+
| `stop` | `(remote_sandbox_id: str) → bool` | 停止沙箱(暂停或终止,语义由 provider 定义) |
68+
| `delete` | `(remote_sandbox_id: str) → bool` | 永久删除沙箱,已不存在返回 True |
69+
70+
`remote_id` 由 RemoteOperator 从 Redis 缓存的 `extended_params` 中解析后传入,provider 不直接依赖 Redis。
71+
72+
**Template API(可选):**
73+
74+
| 方法 | 签名 | 说明 |
75+
|------|------|------|
76+
| `create_template` | `(spec: Any) → dict` | 创建模板,返回含 `template_id``status` 的 dict |
77+
| `get_template_status` | `(template_id: str) → dict \| None` | 查询模板状态,不存在返回 None |
78+
| `delete_template` | `(template_id: str) → bool` | 删除模板,不存在返回 True |
79+
80+
Template 方法默认 raise `NotImplementedError`,RemoteOperator 捕获后转为 `BadRequestRockError``scale_template` 不在 Protocol 中(SandboxNext 无 scale 端点),保持 AbstractOperator 默认行为。
81+
82+
### 3.3 RemoteOperator
83+
84+
定义文件:`rock/sandbox/operator/remote/operator.py`
85+
86+
继承 `AbstractOperator``supports_running_delete = True`。通过 `_create_provider()` 工厂方法根据 `RemoteOperatorConfig.provider` 选择 provider 实现(当前仅 `"sandbox_next"`)。
87+
88+
| 方法 | 行为 |
89+
|------|------|
90+
| `submit` | 直接委托 `provider.submit()` |
91+
| `get_status` | ① Redis 获取用户元数据 → ② 解析 `remote_sandbox_id` → ③ 委托 `provider.get_status()` → ④ 合并(provider 实时状态优先,深合并 `extended_params`|
92+
| `stop` | 从 Redis 解析 `remote_sandbox_id` → 委托 `provider.stop()` |
93+
| `delete` | 从 Redis 解析 `remote_sandbox_id` → 委托 `provider.delete()` |
94+
| `restart` | 不支持,raise `BadRequestRockError` |
95+
| `create_template` | 委托 provider,`NotImplementedError``BadRequestRockError` |
96+
| `get_template_status` | 同上 |
97+
| `delete_template` | 同上 |
98+
| `scale_template` | 不委托,保持 AbstractOperator 默认 `BadRequestRockError` |
99+
100+
### 3.4 SandboxNextProvider
101+
102+
定义文件:`rock/sandbox/operator/remote/providers/sandbox_next_provider.py`
103+
104+
使用 `httpx.AsyncClient` 与 SandboxNext Gateway 通信(完整 OpenAPI 规范见 `docs/proposals/sandbox-next.yaml`)。认证支持 `X-Api-Key` 头和 Bearer token,可同时配置。
105+
106+
#### API 端点摘要
107+
108+
| Method | Path | 说明 |
109+
|--------|------|------|
110+
| `POST` | `/v1/sandboxes` | 创建沙箱,返回 `Sandbox`(201 同步 / 202 异步) |
111+
| `GET` | `/v1/sandboxes/{id}` | 查询详情(200),404 表示不存在 |
112+
| `DELETE` | `/v1/sandboxes/{id}` | 删除沙箱(202 异步受理) |
113+
| `POST` | `/v1/sandboxes/{id}/pause` | 暂停(需 `pause_resume` capability) |
114+
| `POST` | `/v1/sandboxes/{id}/resume` | 恢复(需 `pause_resume` capability) |
115+
| `POST` | `/v1/templates` | 创建模板(202,需 `template_create` capability) |
116+
| `GET` | `/v1/templates/{id}` | 查询模板(200),404 表示不存在 |
117+
| `DELETE` | `/v1/templates/{id}` | 删除模板(202) |
118+
119+
#### 生命周期方法映射
120+
121+
| Provider 方法 | SandboxNext API | 关键映射 |
122+
|---------------|-----------------|----------|
123+
| `submit` | `POST /v1/sandboxes` | `request_id` = Rock `sandbox_id`(幂等键),`resources``DockerDeploymentConfig` 转换;响应中 `sandbox_id``extended_params.remote_sandbox_id``access.agent_token``auth_token``access.endpoint_template``host_ip` + `extended_params.endpoint_template` |
124+
| `get_status` | `GET /v1/sandboxes/{id}` | 404 → 返回 None;否则映射状态 |
125+
| `stop` | `POST /v1/sandboxes/{id}/pause` | 501(不支持)→ 降级为 `delete` |
126+
| `delete` | `DELETE /v1/sandboxes/{id}` | 404 → 返回 True |
127+
128+
#### Template 方法映射
129+
130+
| Provider 方法 | SandboxNext API | 关键映射 |
131+
|---------------|-----------------|----------|
132+
| `create_template` | `POST /v1/templates` | `TemplateSpec``NewTemplate`;409 → 幂等回退 GET;501 → `NotImplementedError` |
133+
| `get_template_status` | `GET /v1/templates/{id}` | 404 → 返回 None |
134+
| `delete_template` | `DELETE /v1/templates/{id}` | 404 → 返回 True;501 → `NotImplementedError` |
135+
136+
#### 状态映射
137+
138+
| SandboxNext 状态 | Rock State | 说明 |
139+
|------------------|-----------|------|
140+
| `creating` | `PENDING` | 创建中 |
141+
| `running` | `RUNNING` | 运行中 |
142+
| `pausing` | `STOPPED` | 暂停中(过渡态) |
143+
| `paused` | `STOPPED` | 已暂停 |
144+
| `resuming` | `PENDING` | 恢复中(过渡态) |
145+
| `failed` | `STOPPED` | 异常,不可用但未删除 |
146+
| GET 404 || Provider 返回 None |
147+
| 其他未知 | `PENDING` | 保守降级 |
148+
149+
可通过 `RemoteOperatorConfig.state_mapping` 覆盖默认映射表。
150+
151+
#### 数据面连通
152+
153+
Provider 在 `submit()` 返回的 `SandboxInfo` 中填充:
154+
155+
| SandboxInfo 字段 | 来源 | 说明 |
156+
|-------------------|------|------|
157+
| `host_ip` | `access.endpoint_template` | 原始字符串直接使用,不解析 |
158+
| `port_mapping` | 写死 | `{Port.PROXY: 8000, Port.SERVER: 8080, Port.SSH: 22}`,与 K8s 一致 |
159+
| `auth_token` | `access.agent_token` | Rocklet 认证 token |
160+
| `extended_params[remote_sandbox_id]` | 响应 `sandbox_id` | 平台分配的沙箱 ID |
161+
| `extended_params[endpoint_template]` | `access.endpoint_template` | 原始值,供后续使用 |
162+
| `extended_params[backend]` | 固定 `"sandbox_next"` | 后端标识 |
163+
164+
`SandboxProxyService` 通过 `host_ip` + `port_mapping` 构造 Rocklet RPC 连接,与 Ray / K8s 完全一致,无需改造。
165+
166+
#### 重试策略
167+
168+
对 5xx 错误进行指数退避重试(默认最多 3 次,退避基数 0.5s),4xx 错误直接返回。通过 `provider_options.retry_max``provider_options.retry_backoff_base` 可配置。
169+
170+
#### TemplateSpec 字段映射
171+
172+
| TemplateSpec 字段 | NewTemplate 字段 | 说明 |
173+
|-------------------|-----------------|------|
174+
| `from_image` | `from_image` | 直接映射 |
175+
| `cpu_count` | `resources.vcpu` | 转入 Resources 对象 |
176+
| `memory_mb` | `resources.memory_mb` | 直接映射 |
177+
| `disk_gb` | `resources.disk_mb` | GB → MB 转换 |
178+
| `num_gpus` / `accelerator_type` || SandboxNext 不支持 |
179+
| `os` ||`class` 决定 |
180+
181+
`NewTemplate` 还需 `request_id`(幂等键)、`region``class``name`,由 provider 从 `RemoteOperatorConfig``TemplateSpec` 生成。
182+
183+
> **注意**:SandboxNext Template 模型无 capacity/pool 概念,与 K8sOperator 的 Pool CRD 输出格式不同。
184+
185+
## 4. 配置设计
186+
187+
### 4.1 RemoteOperatorConfig
188+
189+
新增到 `rock/config.py`,当 `runtime.operator_type == "remote"` 时生效:
190+
191+
| 字段 | 类型 | 默认值 | 说明 |
192+
|------|------|--------|------|
193+
| `provider` | `str` | `"sandbox_next"` | provider 类型 |
194+
| `endpoint` | `str` | (必填) | Gateway API 域名 |
195+
| `api_key` | `str \| None` | `None` | `X-Api-Key` 头认证 |
196+
| `access_token` | `str \| None` | `None` | Bearer token 认证 |
197+
| `protocol` | `str` | `"https"` | 连接协议 |
198+
| `default_timeout` | `int` | `600` | HTTP 请求超时(秒) |
199+
| `region` | `str` | `"cn-hangzhou"` | SandboxNext region |
200+
| `sandbox_class` | `str` | `"headless-vm"` | 沙箱形态 |
201+
| `namespace` | `str` | `"rock"` | 命名空间 |
202+
| `state_mapping` | `dict \| None` | `None` | 覆盖状态映射表 |
203+
| `provider_options` | `dict` | `{}` | provider 特有的额外配置 |
204+
205+
`endpoint` 为空时抛 `ValueError`
206+
207+
### 4.2 RockConfig 集成
208+
209+
`RockConfig` 新增 `remote: RemoteOperatorConfig | None` 字段,`from_env()` 从 YAML `remote` 段解析。
210+
211+
### 4.3 YAML 配置示例
212+
213+
```yaml
214+
runtime:
215+
operator_type: "remote"
216+
217+
remote:
218+
provider: "sandbox_next"
219+
endpoint: "api.cn-hangzhou.sandbox.internal"
220+
api_key: "your-x-api-key"
221+
protocol: "https"
222+
default_timeout: 600
223+
region: "cn-hangzhou"
224+
sandbox_class: "headless-vm"
225+
namespace: "rock"
226+
```
227+
228+
## 5. 工厂集成
229+
230+
- **OperatorContext**:新增 `remote_config: RemoteOperatorConfig | None = None` 字段
231+
- **OperatorFactory**:`create_operator()` 新增 `"remote"` 分支,校验 `remote_config` 后创建 `RemoteOperator`,注入 `redis_provider` 和 `nacos_provider`
232+
- **辅助函数**:`operator_requires_ray("remote")` → `False`;`operator_supports_scheduler("remote")` → `False`(远端平台自行调度)
233+
- **Admin 启动**:`rock/admin/main.py` 构造 `OperatorContext` 时传入 `remote_config=rock_config.remote`
234+
235+
## 6. 文件结构
236+
237+
```
238+
rock/sandbox/operator/remote/
239+
├── __init__.py
240+
├── operator.py # RemoteOperator
241+
├── provider.py # RemoteProvider Protocol
242+
├── constants.py # EXT_REMOTE_ID, BACKEND_NAME 等常量
243+
└── providers/
244+
├── __init__.py
245+
└── sandbox_next_provider.py # SandboxNextProvider
246+
247+
tests/unit/sandbox/operator/remote/
248+
├── __init__.py
249+
├── test_operator.py # RemoteOperator 单元测试
250+
└── test_sandbox_next_provider.py # SandboxNextProvider 单元测试 (mock httpx)
251+
```
252+
253+
## 7. 测试策略
254+
255+
| 测试文件 | 覆盖范围 |
256+
|---------|---------|
257+
| `test_operator.py` | submit/get_status/stop/delete/restart、Redis 合并逻辑、provider 委托验证、`NotImplementedError` → `BadRequestRockError` 转换 |
258+
| `test_sandbox_next_provider.py` | HTTP 调用(`httpx.MockTransport`)、状态映射、错误处理、认证头、Template CRUD(含 409 幂等、404 处理、501 不支持) |
259+
260+
## 8. 设计决策
261+
262+
| 决策 | 结论 | 理由 |
263+
|------|------|------|
264+
| 多 Provider 支持 | 当前绑定 `SandboxNextProvider`,保留 Protocol + 工厂方法扩展点 | 暂无多平台需求,但抽象层不删 |
265+
| 探活机制 | 与 K8s/Ray 一致,复用现有逻辑 | 统一运维 |
266+
| 重试策略 | 5xx 指数退避(最多 3 次),4xx 直接返回 | 有限重试,避免无限等待 |
267+
| 数据面连通 | `endpoint_template` 直接作为 `host_ip`,`port_mapping` 写死 | 复用现有 proxy 链路,与 K8s 一致 |
268+
| stop 语义 | 不支持 `pause_resume`(501)时降级为 `delete` | 保证 stop 语义可达 |
269+
| 租约管理 | 不设 `timeout_seconds`,使用平台默认值;不实现 renew | Rock 自身的 `auto_archive_seconds` / `auto_delete_seconds` 控制生命周期 |
270+
| Region / Class | 全局配置,所有沙箱使用同一值 | 简化 Phase 1 |
271+
272+
## 9. 与现有 Operator 对比
273+
274+
| 维度 | RayOperator | K8sOperator | OpenSandboxOperator | RemoteOperator |
275+
|------|------------|------------|--------------------|---------------|
276+
| 后端 | Ray Actor | K8s CRD | OpenSandbox SDK | HTTP REST API |
277+
| Provider 抽象 | 无 | K8sProvider Protocol | 无 | RemoteProvider Protocol |
278+
| 需要 Ray | 是 | 否 | 否 | 否 |
279+
| 支持 Scheduler | 是 | 是 | 否 | 否 |
280+
| supports_running_delete | False | False | True | True |
281+
| restart | 支持 | 不支持 | 不支持 | 不支持 |
282+
| Template API | 不支持 | 支持 (Pool CRD) | 不支持 | 支持 (HTTP REST,可选) |
283+
| Proxy 层 | Rocklet RPC | Rocklet RPC | OpenSandboxBackend | Rocklet RPC (复用) |
284+
285+
## 10. 后续扩展
286+
287+
当前 `RemoteProvider` Protocol 和 `_create_provider()` 工厂方法已作为扩展点保留。后续如需接入其他平台:
288+
289+
1. 新增 Provider 实现 `RemoteProvider` Protocol
290+
2. 在 `_create_provider()` 中新增分支
291+
3. 可有独立的配置子结构

0 commit comments

Comments
 (0)