> ## Documentation Index
> Fetch the complete documentation index at: https://docs.profy.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox runtime contract

# 沙箱调度：Core 编排 + agent-runtime 纯基建 契约

## 架构概述

```
User → Core agent-proxy (invoke / resume) → DB + Redis
                                                ↓ (Redis bridge)
User → Core agent-proxy → agent-runtime (connect_to / wait_and_connect) → E2B Provider
```

* **Core** 是沙箱生命周期唯一编排层：create、provision、resume、pause、terminate、healthCheck。
* **agent-runtime** 是纯基建：connect、disconnect、文件操作、命令执行、MinIO 同步。
* **Core agent-proxy** 是入口层：invoke 和 resume 路由均先调 Core sandbox service 获取 sandbox 信息，再转发 Python。

架构决策记录：[`.cursor/knowledge/decisions/sandbox-core-ownership.md`](../../../../.cursor/knowledge/decisions/sandbox-core-ownership.md)

## Core API

### `POST /api/marketplace/expert/prepare-invoke`

统一的沙箱编排入口，`expertIdentifier` 可选：

| 请求字段               | 类型      | 说明                              |
| ------------------ | ------- | ------------------------------- |
| `expertIdentifier` | string? | 可选，有则做 access check + config 加载 |
| `chatSessionId`    | string  | 必须，用于查找/创建 sandbox\_session     |
| `projectId`        | string? | 可选，项目协作上下文                      |

响应 `data.sandboxSession`：

| 字段                  | 类型      | 说明                                              |
| ------------------- | ------- | ----------------------------------------------- |
| `id`                | string  | sandbox\_session.id（Core 真相源）                   |
| `status`            | string  | `warm` \| `running` \| `paused` \| `terminated` |
| `providerSandboxId` | string? | running 时有值，warm 时 null                         |
| `provider`          | string  | `e2b` \| `noop`                                 |
| `containerUrl`      | string? | 预览 URL                                          |
| `isNewProvision`    | boolean | 是否为旧对话的新沙箱（需恢复 MinIO 文件）                        |

### `GET /api/sandbox/by-chat/:chatSessionId`

Core agent-proxy resume 路由在转发 Python 前调用，获取已有 sandbox 信息：

| 响应字段                | 类型      | 说明                  |
| ------------------- | ------- | ------------------- |
| `sessionId`         | string  | sandbox\_session.id |
| `providerSandboxId` | string? | 提供商原生 ID            |
| `provider`          | string  | 提供商名称               |
| `status`            | string  | 当前状态                |
| `containerUrl`      | string? | 预览 URL              |

### 生命周期 API

| 路径                                  | 方法   | 说明   |
| ----------------------------------- | ---- | ---- |
| `/api/sandbox/:sessionId/pause`     | POST | 暂停沙箱 |
| `/api/sandbox/:sessionId/resume`    | POST | 恢复沙箱 |
| `/api/sandbox/:sessionId/terminate` | POST | 终止沙箱 |
| `/api/sandbox/:sessionId/status`    | GET  | 查询状态 |

## Core agent-proxy → agent-runtime 请求体

### `POST /api/agent/invoke`

| 字段                    | 类型      | 说明                                                      |
| --------------------- | ------- | ------------------------------------------------------- |
| `sandbox_session_id`  | string? | Core sandbox\_session.id，cloud 必传                       |
| `provider_sandbox_id` | string? | running 时有值（eager connect），warm 时 null（lazy Redis poll） |
| `sandbox_provider`    | string? | Provider 名称，当前仅支持 `e2b`（抽象契约保留，便于未来扩展）                  |
| `sandbox_url`         | string? | 预览 URL                                                  |
| `sandbox_mode`        | string  | `cloud` / `local` / `none`                              |

### `POST /api/agent/resume`

| 字段                    | 类型      | 说明                                    |
| --------------------- | ------- | ------------------------------------- |
| `sandbox_session_id`  | string? | Core sandbox\_session.id              |
| `provider_sandbox_id` | string? | Core agent-proxy 从 sandbox service 获取 |
| `sandbox_provider`    | string? | 提供商名称                                 |

## Redis 桥接（Core→Python）

Core provision 完成后写 Redis `sandbox:{sessionId}`（TTL 24h）：

```json theme={null}
{
  "providerSandboxId": "e2b-abc123",
  "provider": "e2b",
  "containerUrl": "https://e2b-abc123.e2b.dev",
  "isNewProvision": true
}
```

Python `wait_and_connect` 轮询此 key（最多 5s / 10 次），读到后 `connect_to` + 按需从 MinIO 恢复文件。

## agent-runtime SandboxManager 职责

### 保留方法（纯 Infra）

| 方法                                                      | 职责                                      |
| ------------------------------------------------------- | --------------------------------------- |
| `connect_to(providerSandboxId, providerType)`           | 连接到 Core 指定的沙箱，失败直接 raise               |
| `wait_and_connect(sandboxSessionId)`                    | 从 Redis 读取 Core 的 provision 结果后 connect |
| `disconnect()`                                          | 释放 provider 引用（不 kill/pause）            |
| `write_file` / `read_file` / `edit_file` / `list_files` | 沙箱文件操作                                  |
| `run_command`                                           | 命令执行                                    |
| `upload_user_files`                                     | 用户附件上传                                  |
| `_sync_to_cloud` / `_restore_files_from_cloud`          | MinIO 文件同步与恢复                           |
| `bootstrap_website` / `restart_dev_server`              | Web 环境初始化                               |

### 已删除方法（业务逻辑 → Core）

| 方法                                               | 原职责                           | 处置                                    |
| ------------------------------------------------ | ----------------------------- | ------------------------------------- |
| `_get_or_create_unlocked` 决策树                    | 内存→hint→Redis→Supabase→create | 替换为 `connect_to` / `wait_and_connect` |
| `_supabase_*`                                    | Web HTTP 回写 sandbox\_id       | 删除                                    |
| `_persist_sandbox_id` 双写                         | Redis + Supabase              | 删除                                    |
| `_get_sandbox_id` 恢复链                            | Redis→Supabase→None           | 删除                                    |
| `set_platform_sandbox_hint`                      | 编排逻辑                          | 删除，用 `connect_to` 替代                  |
| `_try_platform_connect_unlocked`                 | hint 连接分支                     | 删除                                    |
| `_reset/cancel_auto_sleep_timer` / `_auto_sleep` | 产品级休眠策略                       | 删除，归 Core lifecycle                   |
| `_detach_unlocked`                               | session 切换 sleep              | 替换为 `_disconnect_unlocked`（仅释放引用）     |

## 存储层

### Supabase 已完全移除

`agent-runtime` 不再包含任何 Supabase 代码路径：

* `SupabaseStorageManager` 类已删除
* `is_supabase_configured()` / `get_supabase_url()` / `get_supabase_key()` 已删除
* `CloudDriveBackend._init_supabase()` / `_ensure_bucket_exists()` 已删除
* 存储后端统一为 S3/MinIO（`S3_ENDPOINT` 必须设置）

### MinIO Bucket 规范（3 个语义 bucket）

| Bucket      | 访问策略            | 用途                              | 使用方          |
| ----------- | --------------- | ------------------------------- | ------------ |
| `profy`     | Private         | 平台级内容（Core 文件上传、市场资源）           | Core         |
| `workspace` | Private         | 用户工作区（聊天附件、sandbox 文件同步、skills） | Web + Python |
| `avatars`   | Public download | 用户头像                            | Web          |

Private bucket 文件展示统一使用 **Signed URL**（1h 有效期）。

### Sandbox 对用户完全透明

* 用户 UI 中无 sandbox 状态指示器或生命周期控件
* Readiness loading 页面已移除，ChatInterface 直接渲染
* Sandbox provision / resume / pause / terminate 全由平台自动处理
* 工具执行结果（文件、终端、预览）正常展示——这些是用户产物，不是实现细节

架构决策记录：[`.cursor/knowledge/decisions/bucket-consolidation.md`](../../../../.cursor/knowledge/decisions/bucket-consolidation.md)

## 相关代码

* Core service: `services/core/src/db/service/sandbox-session.ts`
* Core routes: `services/core/src/routes/sandbox/index.ts`, `services/core/src/routes/marketplace-expert/index.ts`
* Core invoke: `services/core/src/routes/agent-proxy/invoke.ts`
* Core resume: `services/core/src/routes/agent-proxy/resume.ts`
* Runtime: `services/agent-runtime/src/services/sandbox.py`
* Runtime models: `services/agent-runtime/src/repository/models/agent.py`
