Files
model-platform/CLAUDE.md
T
2026-08-06 19:26:57 +08:00

5.7 KiB

Repository Guide

Current architecture

  • frontend: React Router SPA. Production files are built in nginx/Dockerfile.
  • backend: public FastAPI API and internal S3 storage API in one process.
  • runtime: shared Jupyter lifecycle, MySQL edit leases and short-lived in-memory access tickets.
  • schedule: APScheduler, MySQL JobStore, MySQL Outbox polling and DAG execution.
  • common: SQLAlchemy models, database/session helpers, IDs and object-store helpers.
  • migrations: Alembic schema and seed migrations.
  • nginx: static frontend, /api/ proxy and authenticated /jupyter/ proxy.

Redis and the former separate Storage API container are intentionally removed.

Commands

From the repository root:

# Local Python workspace
uv sync --all-packages

# Static Python check
python -m compileall common/src backend/src runtime/src schedule/src

# Database migration
uv run --package backend alembic upgrade head

# Full Docker stack
cp .env.example .env
docker compose config
docker compose up -d --build

Frontend development:

cd frontend
pnpm install
pnpm dev
pnpm typecheck
pnpm build

Service rules

  • Browser traffic enters through Gateway only.
  • Frontend API calls use same-origin /api/v1/... paths.
  • Backend writes schedule_runs and outbox_events, then performs best-effort HTTP dispatch to Schedule Executor.
  • Schedule Executor always polls pending MySQL Outbox rows, so HTTP dispatch failure does not lose a task.
  • Cron jobs are persisted by APScheduler in MySQL table apscheduler_jobs.
  • Runtime must stay single-replica while file leases and Jupyter tickets use the simplified implementation.
  • Never expose the internal Jupyter token to the browser.
  • Never delete Docker volumes when preserving MySQL or storage data is required.

Main entrypoints

frontend/app/routes/platform.tsx
backend/src/backend/main.py
runtime/src/runtime/main.py
schedule/src/schedule/main.py
nginx/default.conf.template

Engineering notes from recent platform-employee work

These are hard-won lessons from the GET/POST/PATCH/DELETE /api/v1/platform/employees rollout. Read before touching platform auth, soft-delete, or Users.platform_role_id flows.

  • Reuse system_admin_context and the in-file _*_admins helpers. Self-protection (cannot disable/demote/delete self) and the last-admin guard for Users.platform_role_id mirror the workspace pattern. _count_active_system_admins(session, exclude_user_id=...) lives in backend/src/backend/platform.py; do not reinvent the count in the handler.
  • PATCH guard order is load-bearing. Always check self-protection, then leaves_admin_pool, then the count. Putting the self-demotion check before the last-admin check looks equivalent but lets the test mock bypass the count helper when is_current_system_admin happens to be False. The last-admin check must run first.
  • Delete on already-soft-deleted users returns 404, not 409. delete_platform_employee collapses user is None or user.is_deleted != 0 into a single 404 "用户不存在" raise. This intentionally differs from DELETE /workspaces/{id} which returns 409 for already-disabled. Document both to avoid reviewer pushback.
  • PATCH cannot null-out platform_role_id. PlatformEmployeeUpdate.role_code: Literal["admin","developer"] (not Optional). To clear the platform role, add a separate endpoint or a different field — do not loosen the Literal.
  • DELETE cascade covers WorkspaceMembers only. It writes is_deleted=1, deleted_at=now on WorkspaceMembers rows where user_id = :uid AND is_deleted=0. It does not touch Workspaces. Document that boundary explicitly.
  • Codex MCP on this machine may fail with InvalidParameter even when prompts include the required model: "kimi-k2.7-code", sandbox: "danger-full-access", approval-policy: "on-request". The upstream proxy rejects the request before our wrapper can recover. Fall back to local implementation rather than retrying — three consecutive failures indicate a transport issue, not a prompt issue.
  • SQLAlchemy 2.0 compile(literal_binds=True) uppercases keywords. "from roles" in text will miss the table reference; use case-insensitive matching (text.lower()) when building a mock session's scalar dispatcher, or test for "roles.role_id" / "roles.role_code" instead.
  • Mocking Depends-style helpers requires async callables. _load_role_by_code and _count_active_system_admins are awaited; substituting them with a sync lambda raises TypeError: object int can't be used in 'await' expression. Wrap mocks in async def factories.
  • Mock response ordering matters for re-reads. update_platform_employee queries current_role (before write) and then response_role (after write). A scalar mock that returns a single fixed value will make the response use the pre-write role. Track call order or look up by user.platform_role_id post-write.
  • Mock users must declare every attribute the handler writes. Users.deleted_at is not in the column defaults; SimpleNamespace(user_id=..., ...) will raise AttributeError on target.deleted_at = now. Set user.deleted_at = None explicitly when building mocks for delete tests.
  • Documentation review is part of the task. API.md descriptions must distinguish "clearing" from "demoting" platform_role_id, separate workspace 409 from user 404 semantics, and avoid language like "platform developer role" when only one shared roles table exists. The reviewer or a future agent will catch these inconsistencies.
  • Frontend coupling is intentionally conservative. frontend/app/components/admin/UserManagementPage.tsx and api.ts still call /api/v1/admin/employees. Do not migrate them in the same change as a platform endpoint addition — the contract surface is intentionally duplicated.