# MVP v3.0 详细设计方案（基于 v2.5）

## 0. 结论摘要（v3.0 要交付什么）

v3.0 = v2.5 + **WebUI** + **用户数据上传/下载（SFTPGo）**，形成第一个可对外发布的版本：
- 用户可以通过 **SFTP** 上传数据/模型/代码（至少数据），落到 GPFS（容器内 `/private`）并对 Ray worker 可见。
- 用户可以通过 API/WebUI 提交训练任务，任务读取自己上传的数据。
- 用户可以下载训练产物（checkpoints/logs 等），最小闭环跑通。

## 1. 范围与原则

### 1.1 继承 v2.5 的前提（不回退）
- **Stateless Ray Node Pool**：head 写 `head.json`，worker watchdog 自动 join/自愈。
- **User Management**：token 鉴权、任务可见性隔离（跨用户 404 不泄漏）。
- **作业产物隔离**：Ray job 目录落到 `/private/users/<user_id>/jobs/<ray_submission_id>/...`。
- **API server 短期运行方式**：代码在宿主机，挂载到 head 容器，在 head 容器内启动（保持现状）。

### 1.2 v3.0 新增目标
1) **Data Management（SFTPGo）**
   - 提供用户上传/下载入口（SFTP 为主）。
   - 数据落到 GPFS（dev 环境 NFS/GPFS，生产环境 GPFS），训练 job 在 worker 容器内可直接读取。
2) **WebUI**
   - 用户可视化创建任务、查看队列/状态/日志、查看“数据路径约定”和自己的 SFTP 信息。
   - 目标是 “可用而非豪华”，支持核心工作流。
3) **权限闭环**
   - 用户只能使用自己目录下的数据（`/private/users/<user_id>/...`）或公共目录（`/private/common/...`）。
   - 防止用户提交任务读取其他用户的文件路径。

### 1.3 v3.0 明确不做（留给 v3.5）
- 不做 “自定义 reward function / 自定义 verl 代码 / 多版本 verl 共存”（路线图 v3.5）。
- 不做复杂 Serving/训推一体（路线图 v3.5）。
- 不做 IB 网络/拓扑优化（路线图 v3.5）。
- 不做系统级可观测性平台（路线图 v4.0）。

## 2. 架构概览

参考 `roadmap_v3.0.png`，v3.0 的控制面与数据面：

### 2.1 控制面（Control Plane）
- **API Server（FastAPI）**
  - v2.5 的任务队列/调度/重试 + 用户管理能力继续复用
  - 新增：数据管理能力（与 SFTPGo 对接） + WebUI
- **WebUI**
  - 通过 API 使用 token 登录
  - 提供任务/日志/数据入口（不直接运行训练）
- **Ray Head（状态节点）**
  - 仍在 head 容器内（或单独节点）
  - job server/dashbaord 提供 job submit/status/logs 能力

### 2.2 数据面（Data Plane）
- **GPFS（容器内挂载 `/private`）**
  - 存放 common 与 users 两大根目录
- **Ray Worker Node（无状态）**
  - 自动连接 head，执行训练
  - 读取 `/private/users/<user>/...` 的数据

### 2.3 新增组件：SFTPGo（Data Management）
- 作为独立服务运行（容器化优先），后端存储使用 **filesystem**（GPFS 挂载路径）。
- 用户的 home directory 指向 `/private/users/<user_id>`（或其子目录）。

## 3. 存储与目录规范（v3.0 统一约定）

### 3.1 目录层级
统一以容器内 `/private` 作为根路径（dev/prod 对齐）：
- `/private/common/`：公共资源
  - `hf/`：HF cache
  - `datasets/`：公共数据集（可选）
  - `code/`：公共代码（例如公共 verl repo snapshot）
  - `db/`：SQLite（队列、用户、token）
  - `logs/`：API/supervisor/watchdog 日志
- `/private/users/<user_id>/`：用户空间（v3.0 重点）
  - `datasets/`：用户上传的数据集（推荐）
  - `models/`：用户保存/上传的本地模型（允许；也用于“把 job 产物移动到长期保存目录”）
  - `code/`：用户上传的代码（v3.0 **不支持执行**；仅存放/下载）
  - `jobs/`：训练任务产物（已在 v2.5 落地）
  - `tmp/`：临时文件（可选）

### 3.2 Jobs Retention（两段式：3 天移入回收站，7 天后永久删除）
v3.0 引入 **jobs 目录两段式保留策略**：
- 第 1 阶段（soft-delete）：job 结束后 **3 天**，将该 job 目录从 `jobs/` **移动到用户回收目录**；
- 第 2 阶段（hard-delete）：进入回收目录后再过 **7 天**，从回收目录 **永久删除**。

目录约定（建议）：
- jobs 根目录：`/private/users/<user_id>/jobs/<ray_submission_id>/...`
- 回收目录：`/private/users/<user_id>/trash/jobs/<ray_submission_id>/...`

计时规则：
- 以 job 进入 terminal 状态（SUCCEEDED/FAILED/CANCELED）的结束时间为起点；
- “3 天”用于从 `jobs/` 移入 `trash/jobs/`；
- “7 天”用于从 `trash/jobs/` 永久删除（即总共最多 10 天窗口）。

用户保留关键产物的方式（无需 keep 标记）：
- 在 “3 天窗口”内把需要长期保存的文件从 `jobs/<submission_id>/...` **移动/复制**到 `models/`（例如权重）或 `datasets/`（例如评估输出数据）；
- 即便已被移动到回收目录，用户仍可在 “7 天窗口”内从 `trash/jobs/<submission_id>/...` 把需要的文件移到 `models/` / `datasets/`；
- janitor 只管理 `jobs/` 与 `trash/jobs/`，不会触碰 `models/` 与 `datasets/`。

这里的“清理程序”我们称为 **janitor**：
- 定义：一个后台清理执行器，按固定周期扫描“已结束且已过期”的 job 目录并删除
- v3.0 目标：实现“3 天移入回收站 + 7 天后删除”这一条产品规则（不提供 keep/延长保留标记）

实现建议（按你的偏好）：
- **janitor 作为 API server 内置后台线程**运行：
  - 优点：天然可访问 SQLite（任务状态、结束时间、user_id、ray_submission_id），并能把清理结果写回 events 表用于审计
  - 部署更简单：不额外引入 cronjob/独立服务
- 删除/移动动作建议 **直接在 GPFS/NFS 文件系统上操作**（API server 运行在 head 容器，已挂载 `/private`）：
  - 第 1 阶段：`os.rename`（同文件系统原子移动）把 `jobs/<sid>` 移到 `trash/jobs/<sid>`；
    - 若跨文件系统（理论上不应发生），则降级为 copy+delete；
    - 移动前做严格路径前缀校验（必须在 `.../users/<u>/jobs/` 下）。
  - 第 2 阶段：对 `trash/jobs/<sid>` 执行递归删除（例如 `shutil.rmtree`），同样做路径前缀校验（必须在 `.../users/<u>/trash/jobs/` 下）。
  - 为什么不依赖 SFTPGo API：SFTPGo 只是用户访问协议层（SFTP/Web），目录物理就在同一份文件系统；文件系统直连更简单、也不依赖 SFTPGo 在线。
- 如果你强烈希望“通过 SFTPGo API 删除”：
  - 可以作为可选实现/补充（例如用于统一审计或未来接入配额/策略），但不建议作为唯一手段（SFTPGo 停机不应阻塞清理）。

### 3.3 用户在 SFTPGo 内移动/整理文件（确认点）
支持用户在 SFTPGo 中进行“移动/重命名/整理”（例如把权重从 `jobs/` 移动到 `models/`）：
- 前提：SFTPGo 用户权限允许对其 home 目录进行 `rename/mkdir/remove` 等操作（v3.0 默认可写）。
- 行为：用户可以把 `jobs/` 下某些文件移动到 `models/` 或 `datasets/`，用于长期保存权重/评估产物等。
- 与 retention 的关系：只要文件被移动出 `jobs/`，就不会被 jobs 清理逻辑删除。

### 3.4 路径权限规则（API 侧校验）
v2.5 约束是 “只允许 `/private/common/...`”。  
v3.0 需要升级为：
- 允许：
  - `/private/common/...`
  - `/private/users/<current_user_id>/...`
- 禁止：
  - 任何其他绝对路径（例如 `/private/users/other/...`、`/etc/...`）

并把该规则应用到 TaskSpec 的相关字段（至少）：
- `train_file` / `val_file`
- `code_path`：仍仅允许 `/private/common/...`（v3.0 不支持执行用户 code）
- 本地模型路径字段：允许 `/private/users/<me>/models/...`（确认：v3.0 允许）

## 4. SFTPGo 方案设计（Data Management）

### 4.1 运行形态
推荐用容器运行 SFTPGo（与 Ray/API 解耦），挂载同一份 `/private`：
- `sftpgo` 容器挂载 `../../shared:/private`
- 对外暴露：
  - SFTP 端口（建议 2022）
  - WebAdmin/API 端口（建议 8081，仅内网或管理员访问）

#### 4.1.1 镜像来源（现成 Docker 镜像）
SFTPGo 有现成可用的 Docker 镜像（无需自建）：
- v3.0 推荐优先使用官方/上游发布的 `sftpgo` 镜像作为运行基座
- 我们在 v3.0 里不需要定制 SFTPGo 代码，只需要：
  - 正确挂载 GPFS/NFS（容器内 `/private`）
  - 配置管理员账号（用于 API server 联动创建/禁用用户、重置密码）
  - 配置每用户 home/chroot

> 注意：具体镜像名/tag 在不同环境可能有差异（官方/镜像仓库策略会变动）。落地时建议在 `argus@h1` 上 `docker search sftpgo` 或由你们内部镜像仓库提供固定版本；v3.0 设计只要求“使用现成镜像”，不强依赖某个 tag。

#### 4.1.2 docker-compose 服务草案（示意）
下面给出一个**示意**（最终以实际镜像名/tag 与你们端口规划为准）：

```yaml
services:
  sftpgo:
    image: sftpgo/sftpgo:latest   # 示例：使用现成镜像
    container_name: argus-sftpgo
    ports:
      - "2022:2022"   # SFTP
      - "8081:8080"   # WebAdmin/API（建议仅内网/管理员）
    volumes:
      - ../../shared:/private
      - ../../shared/common/sftpgo:/var/lib/sftpgo   # 持久化 SFTPGo 元数据（可选/建议）
    environment:
      # 管理员账号/密码（示意，具体变量名以镜像文档为准）
      SFTPGO_ADMIN_USERNAME: "admin"
      SFTPGO_ADMIN_PASSWORD: "${SFTPGO_ADMIN_PASSWORD}"
```

与 v3.0 的配合点：
- API server 使用 `data.sftpgo.admin_api_base` + admin 凭据联动创建用户
- 用户 home/chroot 统一指向 `/private/users/<user_id>`

### 4.2 用户隔离
每个用户在 SFTPGo 中的 home dir 绑定到：
- `/private/users/<user_id>`（chroot），用户只能读写自己的目录。

### 4.3 用户创建与凭据管理（两种实现，建议先做 A）

**方案 A（v3.0 推荐）：API Server 负责“联动创建 SFTPGo 用户”**
- 在 v2.5 的 `POST /api/v2/users` 成功后：
  - API server 调用 SFTPGo 管理 API 创建同名用户
  - 设置 home dir = `/private/users/<user_id>`
  - 设置权限（默认可写；是否只读可配置）
- 认证方式：
  - v3.0 最小可用：用户名+密码（确认：v3.0 先 password；API 生成一次性密码，用户首次登录后要求改密）
  - 或：SSH public key（WebUI 允许上传 public key，API 写入 SFTPGo）

**方案 B（更强但复杂）：SFTPGo 外部认证**
- SFTPGo 把认证委托给 API server（token/SSO），SFTP 也走内部 token。
- 复杂度高，建议 v3.0 不做，放到 v3.5 或更后。

### 4.4 用户上传/下载体验
用户通过 SFTP 上传：
- `datasets/...`（训练数据）
- `models/...`（本地模型，可选）
下载：
- `jobs/<ray_submission_id>/...`（checkpoints/logs）

WebUI/文档提供 “路径如何写进 TaskSpec” 的指引。

## 5. WebUI 方案设计（最小可用）

### 5.1 目标页面
v3.0 WebUI 采用“**多子页面 + 侧边导航栏**”而不是把所有功能挤到单页：
- 原因：信息密度更可控，后续可扩展（v3.5+）且不会把一个页面做成“巨型表单/巨型列表”。
- 实现仍保持轻量：服务端渲染（或静态 HTML + 少量 JS），不引入复杂前端工程。

信息架构（IA）建议如下：
1) **登录页**（`/ui/login`）
   - 用户粘贴 token（管理员发放），浏览器保存（localStorage/sessionStorage）
   - 提供“退出登录/清空 token”
2) **任务列表页**（`/ui/tasks`）
   - 默认列表：最近 N 条任务（按 created_at 倒序）
   - 支持过滤：workload、state（QUEUED/RUNNING/SUCCEEDED/FAILED/CANCELED）、时间范围
   - 支持快捷操作：进入详情、取消任务
3) **新建任务页**（`/ui/tasks/new`）
   - 两种模式（二选一，均可实现）：
     - **YAML 直接提交**：上传/粘贴 TaskSpec YAML（最省开发）
     - **表单生成 YAML**：选择 workload，填写核心字段（train/val/model/nnodes/gpus），生成 YAML 预览后提交
   - 提交后跳转到任务详情页
4) **任务详情页**（`/ui/tasks/{task_id}`）
   - 顶部：task_id、workload、state、created_at、updated_at、error_summary
   - Attempt 卡片：latest attempt_no、ray_submission_id、ray_status、start/end
   - 操作区：取消任务（若非 terminal）、刷新状态、复制路径/ID
   - 链接到日志页与产物提示（SFTP 路径）
5) **任务日志页**（`/ui/tasks/{task_id}/logs`）
   - 默认 tail=2000，可选 200/1000/5000
   - 提供“自动刷新（每 3~5 秒）”开关（简单轮询即可）
6) **数据页**（`/ui/data`）
   - 显示 SFTP 连接信息（host/port/username）
   - 显示用户目录约定：
     - home：`/private/users/<user_id>`
     - datasets：`/private/users/<user_id>/datasets`
     - models：`/private/users/<user_id>/models`
     - jobs：`/private/users/<user_id>/jobs`
     - trash/jobs：`/private/users/<user_id>/trash/jobs`
   - 明确 retention：jobs 结束后 3 天移入回收站，回收站 7 天后删除；重要文件请移到 `models/` 或 `datasets/`
7) **（仅管理员可见）用户管理页**（`/ui/admin/users`，可选但很有价值）
   - 创建用户、禁用用户、签发 token、重置 SFTP 密码（方案 A）

### 5.2 页面组织与导航（建议）
侧边栏导航（普通用户）：
- Tasks（列表）
- New Task（新建）
- Data（SFTP/目录说明）

管理员侧边栏额外增加：
- Admin / Users

### 5.3 大致示意图（wireframe）

下面是一个粗略示意（非最终 UI，仅表达信息结构与布局）：

```
┌──────────────────────────────────────────────────────────────────────┐
│ Argus MVP v3.0                                      [user: alice]    │
├───────────────┬──────────────────────────────────────────────────────┤
│  Side Nav     │  /ui/tasks                                            │
│               │                                                      │
│  • Tasks      │  [Filter] workload=all  state=all  [Search task_id]  │
│  • New Task   │                                                      │
│  • Data       │  Task List                                           │
│  • Admin(*)   │  ┌────────────────────────────────────────────────┐  │
│               │  │ task_id                 workload  state   ...   │  │
│               │  │ mvp2-alice-ppo-...      ppo       RUNNING ...   │  │
│               │  │ mvp2-alice-sft-...      sft       SUCCEEDED...  │  │
│               │  └────────────────────────────────────────────────┘  │
│               │  [View] [Cancel]                                     │
└───────────────┴──────────────────────────────────────────────────────┘
```

任务详情页（示意）：
```
┌──────────────────────────────────────────────────────────────────────┐
│ /ui/tasks/{task_id}                                                   │
├──────────────────────────────────────────────────────────────────────┤
│ task_id: mvp2-alice-ppo-...   state: RUNNING   workload: ppo          │
│ created_at: ...               updated_at: ...                         │
│ error_summary: (empty)                                                │
│                                                                        │
│ latest_attempt: a01  ray_submission_id: ...--a01  ray_status: RUNNING  │
│ [Open Logs] [Cancel Task] [Refresh]                                   │
│                                                                        │
│ Artifacts (SFTP paths):                                                │
│   jobs/:   /private/users/alice/jobs/<ray_submission_id>/              │
│   trash/:  /private/users/alice/trash/jobs/<ray_submission_id>/        │
│   tip: move important files to /private/users/alice/models/            │
└──────────────────────────────────────────────────────────────────────┘
```

### 5.2 技术取舍（建议：不引入 Node 构建）
为了降低部署复杂度，建议 v3.0 WebUI 以 “服务端渲染 + 少量 JS/HTMX” 或 “纯静态 HTML+fetch” 实现：
- 由 API server 提供静态资源（FastAPI StaticFiles）
- 页面调用同源 API，避免跨域与复杂前端构建链

## 6. API 扩展设计（概览）

v3.0 可以保持 `/api/v2/...` 不变，增量加：
- SFTPGo 集成管理端点（管理员）：
  - 创建/禁用用户时联动 SFTPGo
  - 重置 SFTP 密码 / 更新 SSH key
- 用户数据端点（可选，最小化）：
  - `/api/v2/me`：返回 user_id、SFTP 信息（host/port/home）
  - `/api/v2/files`：仅用于浏览/下载（上传仍走 SFTP）

详细见 `specs/mvp/v3.0/v3.0_api.md`。

## 7. 配置与部署（v3.0 新增项）

在 `configs/dev.yaml` 基础上扩展一组 `data` 配置（示意）：
```yaml
data:
  shared_root: "/private"              # 通常与 ray.shared_root 一致
  user_root: "/private/users"          # 用户空间根目录
  allow_common_prefix: "/private/common/"
  allow_user_prefix_template: "/private/users/{user_id}/"

  sftpgo:
    enabled: true
    host: "127.0.0.1"
    sftp_port: 2022
    admin_api_base: "http://127.0.0.1:8081/api/v2"
    admin_user: "admin"
    admin_password_env: "SFTPGO_ADMIN_PASSWORD"   # 仅 head 容器内可读

  retention:
    jobs_trash_after_days: 3
    jobs_purge_after_days: 7
    trash_root_template: "/private/users/{user_id}/trash/jobs"
    janitor_interval_s: 3600     # 每小时扫一次（可配置）
```

## 8. 风险点与对策

1) **路径逃逸/越权读取**
   - 必须在 API 提交任务时校验路径前缀
   - SFTPGo 必须 chroot 到用户 home
2) **大文件上传稳定性**
   - 优先用 SFTP（断点续传/可靠性更好）
3) **用户 token 与 SFTP 凭据的生命周期**
   - token 走 v2.5 SQLite
   - SFTP 凭据建议独立（密码/SSH key），并提供 reset 流程
4) **GPFS/NFS 权限**
   - 确保 `/private/users/<user>` 目录权限可被 SFTPGo 写入且 worker 可读

## 9. 已确认结论（来自你的反馈）
1) 允许用户上传并在训练时使用自定义数据集：允许（`/private/users/<u>/datasets/...`）。
2) 允许用户上传并在训练时使用本地模型路径：允许（`/private/users/<u>/models/...`）。
3) v3.0 不允许执行用户自定义代码（不注入 `PYTHONPATH` 作为可执行 code path）。
4) SFTPGo 认证方式：v3.0 先 password。
5) WebUI：按“简单最小必要功能”做（token 粘贴登录优先）。

## 10. 待确认问题（需要你给结论）
（已确认）jobs 清理执行主体：v3.0 采用 **API server 内置 janitor 后台线程**。
