5.7 KiB
5.7 KiB
Repository Guide
Current architecture
frontend: React Router SPA. Production files are built innginx/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_runsandoutbox_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_contextand the in-file_*_adminshelpers. Self-protection (cannot disable/demote/delete self) and the last-admin guard forUsers.platform_role_idmirror the workspace pattern._count_active_system_admins(session, exclude_user_id=...)lives inbackend/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 whenis_current_system_adminhappens to be False. The last-admin check must run first. - Delete on already-soft-deleted users returns 404, not 409.
delete_platform_employeecollapsesuser is None or user.is_deleted != 0into a single 404 "用户不存在" raise. This intentionally differs fromDELETE /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"](notOptional). To clear the platform role, add a separate endpoint or a different field — do not loosen the Literal. - DELETE cascade covers
WorkspaceMembersonly. It writesis_deleted=1, deleted_at=nowonWorkspaceMembersrows whereuser_id = :uid AND is_deleted=0. It does not touchWorkspaces. Document that boundary explicitly. - Codex MCP on this machine may fail with
InvalidParametereven when prompts include the requiredmodel: "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 textwill miss the table reference; use case-insensitive matching (text.lower()) when building a mock session'sscalardispatcher, or test for"roles.role_id"/"roles.role_code"instead. - Mocking
Depends-style helpers requires async callables._load_role_by_codeand_count_active_system_adminsare awaited; substituting them with a synclambdaraisesTypeError: object int can't be used in 'await' expression. Wrap mocks inasync deffactories. - Mock response ordering matters for re-reads.
update_platform_employeequeriescurrent_role(before write) and thenresponse_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 byuser.platform_role_idpost-write. - Mock users must declare every attribute the handler writes.
Users.deleted_atis not in the column defaults;SimpleNamespace(user_id=..., ...)will raiseAttributeErrorontarget.deleted_at = now. Setuser.deleted_at = Noneexplicitly 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 sharedrolestable exists. The reviewer or a future agent will catch these inconsistencies. - Frontend coupling is intentionally conservative.
frontend/app/components/admin/UserManagementPage.tsxandapi.tsstill call/api/v1/admin/employees. Do not migrate them in the same change as a platform endpoint addition — the contract surface is intentionally duplicated.