docs: add Claude AI assistant rules and configuration
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 45s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s

Add comprehensive rule documentation for AI-assisted development covering
authentication surfaces, outbound-call safety invariants, and other project
conventions to guide Claude's understanding of critical system behaviors.
This commit is contained in:
2026-07-28 19:01:38 -04:00
parent 016d8be71d
commit 4a3c14d4af
40 changed files with 2851 additions and 202 deletions

View File

@@ -0,0 +1,69 @@
---
description: Casdoor SSO for the browser + owner-minted PATs for MCP/CLI; owner-only on every surface; one resolver; ?token= fallback; dev-owner on loopback
paths:
- "auth.py"
- "api/auth.py"
- "api/tokens.py"
- "api/deps.py"
- "api/websocket.py"
- "mcp_server/server.py"
- "main.py"
---
# Authentication across the four surfaces
Auth is **Casdoor SSO for the browser + owner-minted Personal Access Tokens for
MCP/CLI**, and the gateway is **owner-only**: exactly one operator (the Casdoor
user whose name matches `OWNER_NAME`) may use any surface; every other identity
gets 403. There is one resolver behind all of it — don't add a second auth
mechanism, a per-surface token, or a bypass.
- **`resolve_bearer(session, raw_token)` in [auth.py](../../auth.py) is the single
resolver.** It turns a bearer string into a `User` (or `None`), classifying it
as a PAT (`hs_pat_` prefix → hash lookup) or a Casdoor JWT (RS256, validated
against the endpoint's JWKS). `resolve_from_header_or_query` wraps it to accept
the token from the `Authorization` header *or* a `?token=` query param. Every
surface funnels through these — REST, WebSocket, MCP.
- **Owner gating is `is_owner(user)` + `get_current_owner`.** `is_owner` matches
`user.name == OWNER_NAME` (SSO) or the dev-owner sub (dev mode). REST routers
carry `dependencies=[Depends(get_current_owner)]` (aliased `_auth` in
[main.py](../../main.py)) → 401 if unauthenticated, 403 if not owner. New
protected routers get the same dependency. `/auth/me` is the one exception: it
resolves the user *without* the owner gate so a signed-in non-owner sees
`is_owner:false` (the dashboard's "not authorized" screen) instead of a bare
401.
- **The `?token=` query-param fallback is intentional and narrow.** Browsers
can't set headers on a WebSocket connect or on an `<audio src>`/`<a href>`
recording download, so the current token (Casdoor JWT, or a PAT) rides as
`?token=`. It's validated by the same resolver as the header. Don't widen it or
remove it without accounting for those two consumers.
- **WebSocket checks ownership itself** ([api/websocket.py](../../api/websocket.py)
`_authorize`) rather than via a router dependency, because WS handshakes don't
run FastAPI dependencies the same way. It opens a `session_scope`, resolves the
bearer (header or `?token=`), and closes with **4401** unless the caller is the
owner. Keep the check **before** `websocket.accept()`.
- **MCP is gated by the ASGI `_owner_only_mcp` wrapper in main.py**, not by
FastMCP auth. `create_mcp_server` builds `FastMCP(auth=None)`; the wrapper reads
the ASGI scope's `Authorization` header, resolves it via the same
`resolve_from_header_or_query` + `is_owner`, and short-circuits non-owner
requests with 401/403 (plus an RFC 9728 `WWW-Authenticate` header pointing at
`/.well-known/oauth-protected-resource/mcp`). This is why PATs *and* JWTs both
work on `/mcp` with one code path. The nested `mcp_http_app.lifespan` still runs
— the wrapper is pure middleware around the inner app.
- **Dev mode is the loopback bypass, not a token.** `CASDOOR_ENABLED=false` makes
every request resolve to the dev owner — permitted **only** on a loopback bind.
`_check_startup_config` in [main.py](../../main.py) exits if SSO is disabled and
`HOST` is off-loopback (the network would see a dev-owner-open gateway), and
exits if SSO is enabled but `CASDOOR_ENDPOINT`/`CLIENT_ID`/`CLIENT_SECRET`/
`OWNER_NAME` is missing. **Never weaken these to "warn and continue" — a
startup misconfiguration must stop the service.**
- **Never log a token.** `client_secret` is a `SecretStr`; read it via
`.get_secret_value()` only where you hand it to the Casdoor SDK. PAT plaintext
is shown once at creation and only its SHA-256 hash is stored — never log the
plaintext, a JWT, or a hash.