# 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: ```bash # 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: ```bash 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 ```text 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.