Files
model-platform/README.md
T
tao.chenandClaude Fable 5 dfe3f0b118 fix(security): P0-1 — port exposure + service-token auth on /internal/* + jupyter RPC
The fix lands in three concentric layers, all backed by a single
INTERNAL_SERVICE_TOKEN shared secret so we have one mechanism
instead of three:

1. docker-compose: drop the backend.ports: 8891:8000 and
   runtime.ports: 8892:8000 mappings. Nginx is the only host
   ingress again (architecture §2.2).
2. /internal/v1/*: the storage control plane had six endpoints, five
   of which were dead code (frontend already migrated to
   /api/v1/data-resources/* with JWT; schedule only ever called
   POST /internal/v1/objects). Delete the dead routes, mount the
   one survivor with Depends(require_internal_service) that
   compares the X-Internal-Service-Token header against
   settings.internal_service_token with secrets.compare_digest.
3. POST /api/v1/jupyter on the runtime container: previously open
   inside the Docker network. Same token mechanism — backend's
   runtime_http_client now carries the header, runtime's
   handle_jupyter_action requires the same header. /api/v1/health
   stays open for the Nginx and compose healthchecks.

The schedule worker was already configured to call
POST /internal/v1/objects; build_storage_http_client now
sets the token header so its existing call site keeps working
without changes.

Files touched:
  backend/src/backend/storage_api.py   # 5 dead routes deleted + token guard
  backend/src/backend/main.py          # runtime_http_client header
  runtime/src/runtime/main.py          # require_internal_service Depends
  common/src/common/config.py          # internal_service_token setting
  schedule/src/schedule/service.py     # httpx client header
  docker-compose.yml                   # ports dropped, INTERNAL_SERVICE_TOKEN env
  .env.example                         # INTERNAL_SERVICE_TOKEN placeholder
  API.md / README.md / DEVELOP.md      # §9 trimmed to 1 endpoint

Verified:
  compileall -> 0 errors
  pytest backend/tests -> 37 passed
  in-process ASGI smoke:
    POST /internal/v1/objects no/wrong/correct token -> 401/401/200
    POST /api/v1/jupyter   no/wrong/correct token -> 401/401/200
    5 deleted internal routes -> 404
  docker compose config (with env) -> OK

P0-1 still has one open sub-item (rclone RC --rc-no-auth) that
the user has explicitly deferred; not touched here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 16:48:46 +08:00

9.8 KiB
Raw Blame History

Model Platform

自托管的 Jupyter 模型开发平台:交互式 workspace、DAG 调度、对象存储工件、 按 workspace 隔离的运行时 — 全部经一个 Nginx 网关对外。

技术栈:React Router SPA · FastAPI · APScheduler · MySQL · S3 兼容存储 (或本地文件系统,STORAGE_BACKEND=local)· 共享 Jupyter · rclone FUSE 挂载(仅 s3 模式) 单一入口(Nginx :80);其他服务只在 Docker 内网互通。

它做什么

能力 位置
workspace 内 notebook 编辑,行级锁 backend/jupyter.py + scripts.is_locked
Jupyter 鉴权路由(浏览器永远拿不到 runtime token nginx/default.conf + auth_request + backend/jupyter.py
notebook / script / version / run_log 的对象存储(s3 / local 二选一) common/storage/ + backend/scripts.py
DAG 调度:节点、边、cron、手动触发、重试、快照 backend/schedules.py + backend/schedule_runs.py + schedule/5 个模块)
DAG 执行走 MySQL Outbox(无 Redis,无进程内队列) schedule/orchestrator.py + schedule/worker.py
每个 workspace 一个 Jupyter 子进程池,配 asyncio 锁 runtime/process.py
runtime 内 rclone FUSE 把 workspace 桶挂上来(s3 模式) runtime/mount.py
仅 MySQL 持久化(26 张表,软删除,无外键) common/db/models/

架构一览

                        ┌────────────────────┐
                        │  Browser (SPA)     │
                        └─────────┬──────────┘
                                  │ HTTPS / WS
                        ┌─────────▼──────────┐
                        │  Nginx (only :80) │  ← templates/default.conf
                        │   /api/  /jupyter/ /storage/
                        └────┬───────┬──────┘
                             │       │
              ┌──────────────┘       └─────────────┐
              ▼                                     ▼
   ┌──────────────────┐               ┌──────────────────────┐
   │ FastAPI Backend  │               │ Runtime (Jupyter)     │
   │ + /internal/v1   │   control     │ - rclone FUSE mount  │ (P0-1)
   │   /objects       │   token-Auth  │                       │
   │ (storage)        ├──────────────►│ - subprocess pool    │
   │ - DAG CRUD       │               │   (per workspace)    │
   │ - script CRUD    │               └──────────┬───────────┘
   │ - auth_request   │                          │ FUSE / shared vol
   │ - /api/v1/...    │                          ▼
   └────┬──────┬──────┘               ┌──────────────────────┐
        │      │                       │ Object storage      │
        │      └──────── HTTP ───────►│ (s3: S3 service /    │
        ▼                              │  local: shared vol) │
   ┌────────────┐                      │ 4 buckets per usage │
   │  MySQL     │◄───────── poll ─────│                      │
   │ - 26 tbls  │                      └──────────────────────┘
   │ - outbox   │
   │ - jobstore │
   └────┬───────┘
        ▲
        │ outbox poll
   ┌────┴──────────────────────────┐
   │ Schedule Executor             │
   │ - CronScheduler (APScheduler) │
   │ - DispatchOrchestrator        │
   │ - NodeExecutor (worker)       │
   │ - SchedulerService (facade)   │
   └───────────────────────────────┘

对象存储通过 STORAGE_BACKENDs3 | local)二选一。s3 模式下 4 个 purpose 命名桶 workspace / version / run-log / trash)是独立的 S3 bucketlocal 模式下 是 LOCAL_STORAGE_BASE_DIR 的子目录,通过 Docker volume local-storage 共享。 详见 DEVELOP.md §存储。

详细设计见 ARCHITECTURE.md。实现的偏离和近期重构记录在 HANDOVER.md

目录结构

frontend/                    React Router SPA
backend/                     FastAPI:公开 API + 内部存储 API
runtime/                     Jupyter 子进程管理 + rclone FUSE
schedule/                    DAG 调度器(5 模块:context/scheduler/
                             orchestrator/worker/service
common/                      配置、SQLAlchemy 模型、存储 SDK、
                             outbox 事件、jobstore
migrations/                  Alembic 基线 + 各特性 migration
nginx/                       (仅概念 — 见下方「容器」一节)
scripts/                     nginx-entrypoint.sh(模板渲染)
docker-compose.yml           4 服务 — web / backend / runtime / schedule
default.conf                 Nginx 模板(挂载,启动时渲染)
.env.example                 common.config.Settings 消费的所有环境变量

容器

服务 镜像 暴露 用途
web nginx:alpine 宿主机 :8888:80 SPA、/api/ 反向代理、/jupyter/{ws}/ auth_request 代理、/storage/ S3 直通(仅 s3 模式)
backend Dockerfile 仅内网 DAG CRUD、script CRUD、schedule 触发、/api/v1/auth/jupyter/internal/v1/objects 服务间 RPC(共享 INTERNAL_SERVICE_TOKEN 鉴权,P0-1)
runtime Dockerfile 仅内网 每个 workspace 一个 Jupyter 子进程池、rclone FUSE 挂载 workspace 桶(s3 模式)
schedule Dockerfile 仅内网 cron tick + DAG 执行(轮询 MySQL Outbox

架构故意只暴露一个宿主机端口(网关);其他服务都在 Docker 内网。 这一点在 docker-compose.yml 里强制执行 — backend / runtime / schedule 都没有 ports:。在 P0-1 之前,backend 与 runtime 曾短暂地把 8891 / 8892 映射到宿主机;此映射已被删除,改用 INTERNAL_SERVICE_TOKEN 头对 /internal/v1/* 做服务间鉴权,见 API.md §9

快速启动

cp .env.example .env
# 编辑 .env — 至少改 MYSQL 密码,以及(s3 模式下)S3 凭据。

# 静态检查
uv sync --all-packages
uv run --frozen --package backend python -m compileall -q backend/src common/src
uv run --frozen --package schedule python -m compileall -q schedule/src
uv run --frozen --package runtime python -m compileall -q runtime/src

# 应用 schema
uv run --frozen --package backend alembic upgrade head

# 启动整套服务
docker compose config          # 校验
docker compose up -d --build
docker compose ps

访问 http://localhost:8888

日志

docker compose logs -f backend
docker compose logs -f schedule
docker compose logs -f runtime

停服(保留 MySQL + S3 / local-storage 数据卷)

docker compose down

抹数据

docker compose down -v

配置

所有环境变量在 common/src/common/config.py 里用 pydantic-settings 的 Settings 类一次性声明,外面套一层 @lru_cache 单例。新增环境变量:

  1. common/src/common/config.pySettings 里加字段(带合理 default,使 dev 启动不需要设)
  2. .env.example 加一行带注释
  3. 调用点用 settings.<name>,永远不要用 os.environ["..."]

完整环境变量列表和含义见 DEVELOP.md

存储布局

4 个 purpose 命名桶。从 StorageObjects.usage_type 到桶的映射由 单一入口backend/storage_api.py:resolve_bucket)决定:

usage_type 桶(环境变量) 默认名
working_copypublic_scriptdata_resourcesnapshot S3_WORKSPACE_BUCKET workspace
version_artifact S3_VERSION_BUCKET version
run_logrun_result S3_RUN_LOG_BUCKET run-log
(软删除目标) S3_TRASH_BUCKET trash

STORAGE_BACKEND=s3 模式下是 4 个独立 S3 桶。STORAGE_BACKEND=local 模式下 是 LOCAL_STORAGE_BASE_DIR(默认 /data)下的 4 个子目录:

/data/
├── workspace/   # S3_WORKSPACE_BUCKET
├── version/     # S3_VERSION_BUCKET
├── run_log/     # S3_RUN_LOG_BUCKET
└── trash/       # S3_TRASH_BUCKET

某个 workspace 的 artifact_bucket 列(非 NULL 时)覆盖该 workspace 的默认桶, 无视 usage_type — 适合把付费客户隔离到专属桶。

对象 key 是两层扁平路径 — workspace_id 加服务端签发的 ulid

<workspace_bucket>/<workspace_id>/<ulid>{.<ext>}

文件名、扩展名、MIME、逻辑路径都放在 StorageObjectsScripts 行里,不进 object key — 重新组织存储不需要重写数据库。

Backend 代码从不写容器本地文件系统(STORAGE_BACKEND=local 模式除外,那里共享 local-storage volume 就是规范存储)。Schedule Executor 在 tempfile.TemporaryDirectory() 里暂存节点工件(自动清理)。只有 runtime 容器保留宿主 volume — s3 模式下 rclone FUSE 挂载需要;local 模式下是 no-op 透传。

文档

  • README.md(本文)— 快速导读
  • ARCHITECTURE.md — 设计图 + 简化历史
  • HANDOVER.md — 实现偏离、近期重构、待办事项
  • DEVELOP.md — 开发指南(环境变量、代码规约、常用操作)
  • CLAUDE.md — agent 面向的本仓库规约

许可

内部。