Files
workspace/.claude/CLAUDE.md
Roberto Musso bc2c76d2bb docs: update CLAUDE.md with Google OAuth architecture and gotchas
Document non-obvious details from Steps 1-5 implementation:
- adiuvAI: deep-link protocol workaround, requestSingleInstanceLock,
  backup-key.ts device-bound key, loginWithOAuth fetch() vs get()
- api: _pending_states in-memory limitation, nullable password_hash,
  OAUTH_REDIRECT_URI pointing to API not website, 409 unverified-email
  guard, OAuth testing patterns with AsyncMock + monkeypatch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 18:39:20 +02:00

225 lines
13 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Keeping This File Up to Date
Update this file whenever a lesson is learned during development. Specifically, update CLAUDE.md when:
- A non-obvious architectural decision is made or discovered
- A gotcha, footgun, or surprising behavior is encountered (and the fix/workaround)
- A new command, workflow, or tool is added to the project
- A convention is established that isn't obvious from reading the code
- An integration detail is clarified (e.g., how the IPC protocol actually behaves, edge cases in the agent tool call cycle)
Do **not** add things already derivable from reading the code, generic best practices, or ephemeral task notes — only durable, reusable knowledge.
## Repository Layout
This is a **monorepo with git submodules**. Each submodule is an independent repo with its own `.claude/CLAUDE.md` for detailed guidance.
| Directory | What | Submodule |
|-----------|------|-----------|
| **`adiuvAI/`** | Electron desktop app (TypeScript/React) | `git.muticolturano.com/adiuvAI/adiuvAI` |
| **`api/`** | FastAPI backend (Python) | `git.muticolturano.com/adiuvAI/api` |
| **`website/`** | Landing page (single `index.html`) | `git.muticolturano.com/adiuvAI/website` |
| **`docs/`** | Planning docs & working memory (not a submodule) | -- |
After cloning, run `git submodule update --init --recursive` to populate submodule contents.
---
## adiuvAI (Electron App)
> **Detailed docs**: `adiuvAI/.claude/CLAUDE.md` covers commands, architecture, AI subsystem, design context, and conventions in depth.
### Commands
```bash
cd adiuvAI
npm run start # Start dev server (Electron + Vite)
npm run lint # ESLint
npm run knip # Dead code analysis
npm run make # Build installers (Windows/Linux/macOS)
npm run package # Package without creating installers
npx drizzle-kit generate # Generate migration from schema changes
npx drizzle-kit push # Push schema directly (dev only)
```
No test suite currently.
### Architecture
```
Renderer (React 19 + TanStack Router)
↓ custom ipcLink (NOT electron-trpc — incompatible with tRPC v11)
Preload (contextBridge: window.electronTRPC + window.electronAI)
↓ IPC channels
Main Process (Node.js)
├── tRPC router (all CRUD + AI procedures)
├── SQLite (better-sqlite3 + Drizzle ORM, WAL mode)
├── LanceDB (vector embeddings, 1536-dim text-embedding-3-small)
└── LangGraph orchestrator (3 specialist agents, pluggable LLM providers)
```
**This is a local-first app.** All user data (tasks, notes, projects) lives in local SQLite. The AI system (LangGraph + LangChain) runs entirely in the Electron main process with pluggable providers (OpenAI, Anthropic, GitHub Copilot).
**IPC channels**:
- `'trpc'` — bidirectional tRPC request/response (all CRUD)
- `'ai:stream'` — one-way token streaming from main → renderer
- `'ai:action'` — AI side-effects (e.g., task auto-created by agent)
**Key source paths**:
- `src/main/ipc.ts` — Custom tRPC↔IPC bridge
- `src/main/router/index.ts` — All tRPC routers (~600 LOC)
- `src/main/ai/orchestrator.ts` — LangGraph intent routing + 3 agents (~991 LOC)
- `src/main/db/schema.ts` — 6 tables (clients, projects, tasks, checkpoints, notes, taskComments)
- `src/renderer/routes/` — File-based routing (TanStack Router auto-generates `routeTree.gen.ts`)
- `src/renderer/components/ui/` — shadcn/ui primitives (new-york theme, neutral colors)
- `src/main/auth/auth-manager.ts` — Login, register, logout, OAuth flow (singleton)
- `src/main/auth/backup-key.ts` — Device-specific AES-256 backup key (safeStorage, not password-derived)
- `src/main/ai/token.ts` — Two-tier token storage: safeStorage + electron-store fallback
**Non-obvious details**:
- `electron-trpc` is NOT used — custom IPC bridge in `ipc.ts` + `ipcLink.ts` because electron-trpc bundles tRPC v10 internals
- Vite configs use `.mts` extension to avoid ESM/CJS conflicts with electron-forge
- `forge.config.ts` has complex cross-compilation hooks (downloads platform-specific native binaries for better-sqlite3 and LanceDB)
- DB has no foreign key constraints — cascade deletes are implemented in tRPC procedures
- Timestamps are milliseconds (JavaScript `Date.getTime()`), not ISO strings
- Notes auto-embed to LanceDB on create/update (fire-and-forget, errors swallowed)
**Google OAuth (adiuvAI side)**:
- `adiuvai://` is NOT accepted by Google as a redirect URI — Google only accepts `http://localhost` or `https://`. The API backend exposes `GET /auth/oauth/google/web-callback` which receives the Google redirect and immediately bounces to `adiuvai://oauth/callback?...`. The redirect URI registered in Google Cloud Console points to the backend, not the Electron app.
- `app.requestSingleInstanceLock()` is required for the `second-instance` event to fire on Windows/Linux. If it returns `false`, call `app.quit()` immediately (another instance is already running).
- In dev (`process.defaultApp === true`), `setAsDefaultProtocolClient('adiuvai')` must include `[path.resolve(process.argv[1])]` as the third argument so the OS protocol registration includes the entry script.
- `loginWithOAuth` uses `fetch()` directly (not `this.get()`) because the authorize endpoint is public — `get()` throws when not authenticated.
- The backup key in `backup-key.ts` is stored in `encryptedTokens` under the key `backup_key`, reusing `getToken/setToken` from `token.ts`. It is device-bound and never password-derived, so social-login users can use backup features without issue.
---
## api (FastAPI Backend)
### Commands
```bash
cd api
# Development
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Production
gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 --timeout 120
# Database migrations
alembic upgrade head
# Testing
pytest # all tests
pytest -v # verbose
pytest tests/test_agents.py # single file
pytest tests/test_agents.py -k test_name # single test
# Linting/formatting
ruff check .
ruff format .
# Docker (full stack: app + postgres + minio + qdrant)
docker compose up --build
```
### Architecture
```
FastAPI app (app/main.py)
├── Middleware: TierRateLimiter → Sanitizer → CORS
├── HTTP Routes (app/api/routes/)
│ ├── auth.py — register, login, token refresh (bcrypt + HS256 JWT)
│ ├── chat.py — POST /chat, WS /chat/stream
│ ├── plans.py — execution plan playbooks
│ ├── storage.py — E2E-encrypted cloud storage (S3)
│ ├── backup.py — encrypted backup upload/download
│ ├── vectors.py — encrypted vector upsert/search (Pinecone/Qdrant)
│ ├── plugins.py — plugin marketplace (Power+ tier)
│ └── billing.py — Stripe subscriptions
├── Agent System (app/agents/)
│ ├── task_agent.py — 8 tools
│ ├── project_agent.py — 6 tools
│ ├── checkpoint_agent.py — 4 tools
│ └── note_agent.py — 5 tools
├── Orchestration (app/core/)
│ ├── orchestrator.py — intent classification + agent routing
│ ├── agent_registry.py — decorator-based agent registry
│ ├── execution_plan.py — server-side prompt templates + plan builder
│ ├── llm.py — LiteLLM factory (100+ providers)
│ └── memory_middleware.py
├── Billing (app/billing/)
│ ├── tier_manager.py — feature matrix (Free/Pro/Power/Team)
│ └── stripe_service.py — Stripe checkout + webhooks
├── Storage (app/storage/) — S3 blob store, vector store, encryption
└── Marketplace (app/marketplace/) — plugin catalog, review, revenue sharing
```
**LLM routing**: `gpt-4o-mini` classifies intent → routes to domain agent → agent uses `gpt-4o` with tools → tool calls describe client-side operations (JSON) → Electron executes locally and returns results.
**Zero-trust data model**: The backend never stores or decrypts user content. PostgreSQL holds only auth, billing, plugin metadata, and storage record pointers. All user data is E2E-encrypted before leaving the Electron client.
**Key config**: `app/config/settings.py` — all env vars via Pydantic Settings. Copy `.env.example` to `.env` for local dev. Stripe and S3 gracefully stub when keys aren't configured.
**Database**: PostgreSQL with async SQLAlchemy 2.0 + asyncpg. 9 ORM models in `app/models.py`. Alembic migrations in `alembic/versions/`.
**Testing**: pytest + pytest-asyncio. Fixtures in `tests/conftest.py` create in-memory SQLite + moto-mocked S3. Test users seeded per tier (free/pro/power/team).
### Non-obvious details
- **Tier from DB, not JWT**: `get_current_user` decodes JWT but fetches authoritative tier from `subscriptions` table — tier changes take effect immediately without re-login
- **Refresh tokens hashed**: Plaintext returned to client, stored as SHA-256 in DB — server can never retrieve the plaintext (intentional)
- **WebSocket auth via query param**: `?token=<jwt>` instead of Bearer header (WebSocket handshake limitation)
- **Prompt IP protection**: `PromptTemplateRegistry` keeps prompts server-side; clients receive opaque `template_id`. `SanitizerMiddleware` strips leaked fragments from responses
- **Agents don't execute operations**: Tools return JSON describing client-side ops — the Electron client executes against local SQLite
- **Alembic async/sync split**: App uses `postgresql+asyncpg`, Alembic CLI needs `postgresql+psycopg2``env.py` handles the URL conversion
- **Tool loop cap**: Agent `_tool_loop` stops after 5 iterations to prevent infinite loops
- **Route order matters**: `/backup/history` must be declared before `/backup/{backup_id}` to avoid path param shadowing
- **CORS includes `app://`**: Electron uses custom `app://` protocol, not http/https
- **Vector search on encrypted data is not semantic**: Backend derives deterministic 32-dim floats from blob SHA-256 for storage/search — a known trade-off
**Google OAuth (api side)**:
- OAuth routes live in `app/api/routes/auth.py`: `GET /auth/oauth/{provider}/authorize`, `POST /auth/oauth/{provider}/callback`, `GET /auth/oauth/{provider}/web-callback` (bounces to deep link, excluded from OpenAPI schema).
- Provider abstraction in `app/auth/oauth_providers.py``GoogleOAuthProvider` uses `httpx` directly (no `authlib`). PKCE S256 is implemented manually via `generate_pkce_pair()`.
- `_pending_states` dict in `routes/auth.py` is **in-memory** — works for single-process dev but does not survive restarts and does not scale to multiple workers. Replace with Redis in production.
- `users.password_hash` is **nullable** — social-only users have `password_hash=None`. `await db.flush()` is required before creating a linked `OAuthAccount` to populate `new_user.id` before commit.
- `OAUTH_REDIRECT_URI` must point to the **API backend** (e.g. `https://api.adiuvai.com/...`), not the website domain. `adiuvai.com` is a static site with no server-side routing.
- **Unverified email + existing account = 409**: if `email_verified=False` and the email is already registered, the callback returns 409. Without this guard, branch 3 would attempt to INSERT a duplicate email and crash with a DB constraint violation (500).
- **Testing OAuth routes**: mock `GoogleOAuthProvider.exchange_code` and `get_userinfo` with `patch.object(..., new=AsyncMock(...))` — works because FastAPI instantiates a new provider per request. Use `monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...)` to simulate configured credentials without restarting the app.
### Tier System
| Feature | Free | Pro | Power | Team |
|---------|------|-----|-------|------|
| Rate limit | 20/min | 60/min | 120/min | 200/min |
| Agents | 3 | unlimited | unlimited | unlimited |
| Cloud storage | 0 GB | 5 GB | 25 GB | unlimited |
| Plugin marketplace | no | no | yes | yes |
Enforced in `app/api/middleware/rate_limit.py` (sliding window) and `app/billing/tier_manager.py` (feature checks + quota enforcement).
---
## Cross-Project Integration
The Electron app and FastAPI backend communicate via **WebSocket** (`/chat/stream`):
1. Electron connects with `?token=<jwt>` query param
2. Client sends `ChatRequest` JSON frame
3. Server streams text chunks, then a final frame: `{"done": true, "response": "...", "actions": []}`
4. Server sends `tool_call` frames → Electron executes against local SQLite → returns `tool_result`
5. Server pings every 30 seconds to keep connection alive
The Electron app also has a **fully local AI path** (LangGraph orchestrator in main process) that doesn't require the backend — this is the primary path for desktop use.
---
## MCP Servers
- **Langfuse Docs** (`https://langfuse.com/api/mcp`) — configured at workspace level for prompt management documentation
- **shadcn** (`npx shadcn@latest mcp`) — configured in `adiuvAI/` for UI component generation