From 4a3c14d4af7b78a7e56ce7711fb72180817a204b Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Tue, 28 Jul 2026 19:01:38 -0400 Subject: [PATCH] docs: add Claude AI assistant rules and configuration 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. --- .claude/rules/auth-surfaces.md | 69 ++++ .claude/rules/call-safety.md | 45 ++ .claude/rules/concurrency-threads.md | 55 +++ .claude/rules/config-startup.md | 63 +++ .claude/rules/lifespan-wiring.md | 56 +++ .claude/rules/mcp-tools.md | 56 +++ .env.compose.example | 34 ++ CLAUDE.md | 240 +++++++++++ README.md | 57 ++- api/auth.py | 191 +++++++++ api/deps.py | 36 +- api/tokens.py | 107 +++++ api/websocket.py | 30 +- auth.py | 385 ++++++++++++++++++ config.py | 31 +- dashboard/package-lock.json | 11 + dashboard/package.json | 1 + dashboard/src/app.css | 3 + dashboard/src/hooks.client.ts | 5 + dashboard/src/lib/api.ts | 53 ++- dashboard/src/lib/auth.svelte.ts | 146 +++++++ .../src/lib/components/DeniedScreen.svelte | 17 + .../src/lib/components/LoginScreen.svelte | 17 + .../src/lib/components/TokensModal.svelte | 162 ++++++++ dashboard/src/lib/types.ts | 20 + dashboard/src/routes/+layout.svelte | 104 +++-- db/database.py | 43 ++ .../versions/a1b2c3d4e5f6_users_and_pats.py | 55 +++ docker-compose.yaml | 84 ++++ docs/configuration.md | 19 + docs/mcp-server.md | 11 +- main.py | 275 ++++++++++++- mcp_server/server.py | 14 +- pyproject.toml | 8 +- tests/test_api_security.py | 96 +++-- tests/test_auth.py | 239 +++++++++++ tests/test_mcp.py | 79 +++- tests/test_oauth_metadata.py | 77 ++++ tests/test_structure.py | 5 +- tests/test_websocket.py | 54 ++- 40 files changed, 2851 insertions(+), 202 deletions(-) create mode 100644 .claude/rules/auth-surfaces.md create mode 100644 .claude/rules/call-safety.md create mode 100644 .claude/rules/concurrency-threads.md create mode 100644 .claude/rules/config-startup.md create mode 100644 .claude/rules/lifespan-wiring.md create mode 100644 .claude/rules/mcp-tools.md create mode 100644 .env.compose.example create mode 100644 CLAUDE.md create mode 100644 api/auth.py create mode 100644 api/tokens.py create mode 100644 auth.py create mode 100644 dashboard/src/hooks.client.ts create mode 100644 dashboard/src/lib/auth.svelte.ts create mode 100644 dashboard/src/lib/components/DeniedScreen.svelte create mode 100644 dashboard/src/lib/components/LoginScreen.svelte create mode 100644 dashboard/src/lib/components/TokensModal.svelte create mode 100644 db/migrations/versions/a1b2c3d4e5f6_users_and_pats.py create mode 100644 docker-compose.yaml create mode 100644 tests/test_auth.py create mode 100644 tests/test_oauth_metadata.py diff --git a/.claude/rules/auth-surfaces.md b/.claude/rules/auth-surfaces.md new file mode 100644 index 0000000..2a303d9 --- /dev/null +++ b/.claude/rules/auth-surfaces.md @@ -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 `