From 17e6ad0346f8ce1b53789e9e2435e7513b65f354 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 13 Jun 2026 13:37:31 +0200 Subject: [PATCH] feat(nexus-dev): rewrite nexus-stack skill against real merged code (K-101/103/104/106) Replaces the K-101 placeholder with prescriptive patterns verified against nexus-hub main: create_app factory + configure_logging order, Settings/secret-repr, the visibility.py single-query-path with the architecture test that bans direct selects, router.run as the only LLM interface (hard guard + LOCAL_ENDPOINT_ALLOWLIST), session auth via get_current_user/require_admin (404 for member), audit.record whitelist, PRIV-5 redaction CONTENT_KEYS, alembic autogenerate flow (name constraints, hand-add triggers/extensions), and the disposable pgvector/pg16 CI container strategy. Plugin 0.2.0 -> 0.3.0. --- nexus-dev/.claude-plugin/plugin.json | 10 +- nexus-dev/skills/nexus-stack/SKILL.md | 127 ++++++++++++++++++++------ 2 files changed, 106 insertions(+), 31 deletions(-) diff --git a/nexus-dev/.claude-plugin/plugin.json b/nexus-dev/.claude-plugin/plugin.json index 9a285ff..46b8a6b 100644 --- a/nexus-dev/.claude-plugin/plugin.json +++ b/nexus-dev/.claude-plugin/plugin.json @@ -1,11 +1,17 @@ { "name": "nexus-dev", - "version": "0.2.0", + "version": "0.3.0", "description": "Projekt-Plugin für nexus (Family Knowledge Hub): SDD-Karten-Workflow, Security/Privacy-Invarianten (SICHT/PRIV/fail-closed), Stack-Konventionen, Karten-Command. Für Claude Code Sessions im Repo l.kirchner/nexus-hub.", "author": { "name": "l.kirchner", "url": "https://gitea.luki-net.org/l.kirchner" }, "homepage": "https://gitea.luki-net.org/l.kirchner/nexus-hub", - "keywords": ["nexus", "sdd", "fastapi", "privacy", "acl"] + "keywords": [ + "nexus", + "sdd", + "fastapi", + "privacy", + "acl" + ] } diff --git a/nexus-dev/skills/nexus-stack/SKILL.md b/nexus-dev/skills/nexus-stack/SKILL.md index 3c568df..d143009 100644 --- a/nexus-dev/skills/nexus-stack/SKILL.md +++ b/nexus-dev/skills/nexus-stack/SKILL.md @@ -1,45 +1,114 @@ --- name: nexus-stack -description: Stack-Konventionen fuer nexus (Family Knowledge Hub) seit K-101. Verwenden bei jeder Implementierungs-Karte im Repo l.kirchner/nexus-hub - deckt ab uv-Projektlayout, FastAPI-Patterns, Settings, structlog-Redaction-Pflicht, Alembic, Web-Build, Deploy-Artefakt und Projekt-Kommandos. +description: Reale Stack-Konventionen von nexus (Family Knowledge Hub) gegen den gemergten Code-Stand (K-101/103/104/106). Verwenden bei jeder Implementierungs-Karte im Repo l.kirchner/nexus-hub — deckt ab App-Factory, Settings/structlog-Redaction, den visibility-Lese-Pfad mit Architektur-Test, router.run als einzige LLM-Schnittstelle, Session-Auth, Alembic-Fluss, Audit und die CI-Test-DB. Quelle ist der Code, nicht Vermutung. --- -# nexus Stack-Konventionen (ADR-0002, eingeführt mit K-101) +# nexus Stack-Konventionen (Stand: K-104 gemerged) -## Layout & Werkzeuge +Präskriptive Muster gegen den realen Code. Bei Abweichung gilt der Code; melde Drift in diesem Skill. -- **Backend:** uv-Projekt, Python 3.12, src-Layout `src/nexus/`. Console-Scripts: `nexus-api`, `nexus-worker`. -- **Web:** `web/` — Vite + React + TypeScript + Tailwind v4 (`@tailwindcss/vite`). Build-Artefakt `web/dist` wird von FastAPI unter `/` ausgeliefert (API-Routen gewinnen; SPA via `StaticFiles(html=True)`). -- **Kommandos** (verbindliche Liste: `docs/05_AGENT_RULES.md` → Projekt-Kommandos): `uv sync`, `uv run pytest`, `uv run ruff check`, `uv run mypy`, `uv run alembic upgrade head`, `npm run build` in `web/`. -- Vor jedem Merge lokal grün: ruff + mypy (strict) + pytest; CI (`.gitea/workflows/ci.yml`) führt dieselben Gates aus. +## Layout & Kommandos -## Settings & Konfiguration +- uv-Projekt, Python 3.12, src-Layout `src/nexus/`. Console-Scripts: `nexus-api`, `nexus-worker`, `nexus-seed`. Web: `web/` (Vite/React/TS/Tailwind), Build von FastAPI unter `/` ausgeliefert. +- Gates (verbindlich vor Merge, identisch in CI): `uv run ruff check && uv run mypy && uv run pytest`; bei Web-Änderungen `npm run build` in `web/`. +- mypy ist `strict`; ruff `line-length=100`. Neue Module fügen sich ein, nicht umgekehrt. -- Eine `Settings`-Klasse (`src/nexus/config.py`, pydantic-settings, Prefix `NEXUS_`), Zugriff via `get_settings()` (lru_cache). Tests leeren den Cache (siehe `tests/conftest.py`-Fixture). -- Produktion liest `/etc/nexus/env` (systemd `EnvironmentFile`). Neue Settings dort dokumentieren (proxmox-scripts `install/nexus-install.sh` legt die Datei an). -- `Settings.__repr__` redacted Secrets (`database_url=***`) — bei neuen Secret-Feldern beibehalten. -- Version = Git-SHA: `NEXUS_GIT_SHA` env → `GIT_SHA`-Datei im Artefakt (schreibt deploy.yml) → `"dev"`. +## FastAPI-App-Factory (`src/nexus/main.py`) -## Logging (PRIV-5 — nicht verhandelbar) +`create_app()` ist die EINE Stelle, die zusammensteckt — kein App-Aufbau in Routen/Tests: -- IMMER `nexus.logging_setup.configure_logging()` — nie eigenes logging-Setup, nie `print`. -- Content-Keys (`body`, `content`, `text`, `ocr_text`, `title`, `filename`, …) werden vom Redaction-Processor auf `***` gesetzt. Neue content-tragende Felder in `CONTENT_KEYS` ergänzen — niemals umbenennen, um Redaction zu umgehen. -- Log-Zeilen tragen IDs + Metadaten, nie Inhalte. Secrets (DB-Passwörter, Tokens) zusätzlich als `secrets=[...]` an `configure_logging` geben. -- Jede Karte mit neuen Log-Pfaden ergänzt einen Redaction-Test nach dem Muster `tests/test_logging_redaction.py`. +```python +def create_app() -> FastAPI: + settings = get_settings() + configure_logging(settings.log_level) # IMMER zuerst (PRIV-5) + app = FastAPI(docs_url="/api/docs", openapi_url="/api/openapi.json") + app.state.settings = settings + app.state.oauth = auth_routes.build_oauth(settings) # None ⇒ Login 503 + app.add_middleware(SessionMiddleware, secret_key=..., same_site="lax", + https_only=settings.public_url.startswith("https://")) + app.include_router(health.router); app.include_router(auth_routes.router) + app.include_router(routes.router) + # StaticFiles-SPA zuletzt unter "/" (API-Routen gewinnen) +``` -## Datenbank & Migrationen +Regeln: `configure_logging` vor allem anderen. Neue Router über `include_router` VOR dem SPA-Mount. uvicorn nie per CLI starten — Entrypoint `nexus-api` (`run()`) mit `log_config=None`, `access_log=False`, bindet an `settings.host`. -- SQLAlchemy 2 + Alembic; DSN ausschließlich über `NEXUS_DATABASE_URL` (sqlalchemy-Format `postgresql+psycopg://…`), nie in Dateien. -- Migrationen: `alembic/versions/`, Template ist typisiert (`script.py.mako`). Erste Fach-Migration kommt mit K-103 (setzt auch `target_metadata`). -- `/healthz` meldet `db: unconfigured | ok | unreachable` — Karten, die die DB anschließen, halten dieses Feld korrekt. +## Settings (`src/nexus/config.py`) -## Deploy (deploy.yml, seit K-101 scharf) +Eine `Settings`-Klasse (pydantic-settings, Prefix `NEXUS_`), Zugriff via `get_settings()` (lru_cache). Tests leeren den Cache. `Settings.__repr__` redacted Secrets — bei neuen Secret-Feldern (`*_secret`, `*_password`, DSN) beibehalten. Werte kommen prod aus `/etc/nexus/env`. Reale Felder u.a.: `host` (Default `127.0.0.1`), `public_url`, `database_url`, `oidc_*`, `session_secret`, `llm_policy_path`, `anthropic_api_key`. **Fail-closed by default:** fehlende Konfiguration ⇒ Feature aus (None), nie ein unsicherer Default. -- Push auf `main` → Test-Gate (`needs:`) → Artefakt (`src/`, `pyproject.toml`, `uv.lock`, `alembic*`, `start*.sh`, `GIT_SHA`, `web/dist`) → rsync nach `/opt/nexus/current` → `uv sync --frozen --no-dev` → Service-Restart → Healthcheck. -- Rollback: voriger Stand liegt in `/opt/nexus/previous` (Prozedur im deploy.yml-Header). -- systemd: `nexus.service` → `start.sh` (uvicorn), `nexus-worker.service` → `start-worker.sh`. Runtime-Provisionierung des LXC: proxmox-scripts `install/nexus-runtime.sh` (idempotent). +## Lese-Pfad: `nexus/visibility.py` ist der EINZIGE Query-Weg (SICHT-1/2/3) -## Rote Linien +Jede Lese-Query auf `Entry/EntryVersion/EntryShare/EntrySource/Proposal/Source` lebt in `nexus/visibility.py` — nirgends sonst. Pattern: -- Kein `requirements.txt`, kein pip — uv ist die einzige Quelle der Wahrheit (`uv.lock` committed). -- Keine neuen Top-Level-Prozesse außer API + Worker ohne ADR. -- API-first: jeder User-Workflow hat API-Tests, bevor/während die UI ihn bekommt. +```python +# in der Route: +viewer: User = Depends(get_current_user) +return list(session.scalars(visibility.visible_entries(viewer))) # nie select(Entry) direkt +entry = visibility.get_entry(session, viewer, entry_id) # Einzelzugriff +if entry is None: raise HTTPException(404, "not found") # nicht-sichtbar ≡ nicht-existent +``` + +- Sichtbarkeit wird IN der Query durchgesetzt (kein Python-Nachfilter); `viewer` ist Pflichtparameter. Share-Zeilen wirken nur bei `visibility=='geteilt'`. +- SICHT-2: nicht-sichtbar liefert `None` ⇒ überall dieselbe 404 (gleicher Text), nie ein abweichender Code/Zählwert. +- Kein Admin-Bypass auf Inhalte. +- **Erzwungen:** `tests/test_architecture.py` scannt `src/` und verbietet `select(Entry…)`, `.get(Entry…)`, `aliased(…)`, `models.X` und Raw-SQL auf die geschützten Tabellen außerhalb von `visibility.py`/`models.py`. Wer eine neue sichtbarkeitsrelevante Query braucht: Helper IN `visibility.py` ergänzen, nicht den Test aufweichen. Jede neue Lese-Route kommt zusätzlich in die **SICHT-4-Suite** (`tests/test_sicht4.py`). + +## LLM: `router.run(...)` ist die EINZIGE Modell-Schnittstelle (PRIV-1…6) + +Fach-Code importiert nie `httpx`/Modell-Clients — nur `LLMRouter`. Ein AST-Architektur-Test (`tests/test_llm_router_architecture.py`) verbietet HTTP-Client-Importe außerhalb von `llm_router.py`. + +```python +with LLMRouter() as r: + result = r.run(task_profile, privacy_class, payload) # status: completed | blocked +``` + +Hartverdrahtete Garantien (nicht umbauen ohne ADR): +- **Reihenfolge in `run`:** PRIV-6-Sensitivitäts-Pre-Check → Policy-Decision → forbidden? → Endpoint-Auflösung → **Hard Guard** → Health-Check (lokal) → Call. Jeder Fehlerpfad endet in `blocked` + Audit; **kein** externer Fallback, auch nicht bei Timeout. +- **Hard Guard:** `privat`/`familie` an `kind=="external"` ⇒ `blocked` — zweite Verteidigungslinie vor der Policy. +- **Allowlist:** `EndpointConfig` validiert `kind=="local"` gegen `LOCAL_ENDPOINT_ALLOWLIST` (gepinnte luki-ai host:port) — eine als „local" getarnte externe URL wird beim Policy-Load abgewiesen. Endpoints sind Konfiguration (`config/llm_policy.default.json`), nie Code. +- Policy-Änderung nur über `write_policy_as_admin` (admin-only, auditiert). + +## Auth: Session-Identität über `get_current_user` (K-104) + +```python +from nexus.auth import get_current_user, require_admin +viewer: User = Depends(get_current_user) # 401 ohne gültige Session +admin: User = Depends(require_admin) # 404 für member (AUTH-3, nicht 403) +``` + +- Identität kommt AUSSCHLIESSLICH aus der signierten Session (`SESSION_USER_KEY`), gesetzt vom OIDC-Callback. Es gibt keinen Header-/Dev-Mechanismus mehr (in K-104 entfernt) — nicht wieder einführen. +- OIDC-Flow in `api/auth_routes.py` (authlib, Code+PKCE). **Session-State (state/nonce/PKCE) NIE vor `authorize_access_token` räumen** — `session.clear()` erst nach dem Exchange (Fehlerpfade einzeln, als HTTPException durch die SessionMiddleware). Rollen aus dem `groups`-Claim (`nexus-admin`/`nexus-member`), Sync bei jedem Login. +- Tests authentifizieren über echte Session-Cookies (Helper in `tests/conftest.py`), nicht über einen Parallel-Mechanismus. + +## Audit: `nexus/audit.record(...)` (AUDIT-1…3) + +Einziger Schreibpfad. `meta`-Keys sind whitelisted (`ALLOWED_META_KEYS`) und Werte typvalidiert — Inhalte (Bodies/Titel/Claims) können strukturell nicht ins Audit. `audit_events` ist append-only (DB-Trigger gegen UPDATE/DELETE/TRUNCATE). Neue Event-Typen: nur Metadaten/IDs/Codes loggen. + +## Logging (PRIV-5, `nexus/logging_setup.py`) + +Immer `configure_logging()` — nie eigenes logging, nie `print`. Der Redaction-Processor setzt content-tragende Keys (`CONTENT_KEYS`: body/content/ocr_text/title/filename/…) rekursiv auf `***`, scrubbt URI-Credentials und literale Secrets. Neue content-tragende Felder in `CONTENT_KEYS` ergänzen, nicht umbenennen, um Redaction zu umgehen. + +## Datenmodell & Alembic + +SQLAlchemy 2 (`models.py`, `Base`). `alembic/env.py` zieht `target_metadata = Base.metadata`, DSN aus `NEXUS_DATABASE_URL`. Fluss: Modell ändern → `uv run alembic revision --autogenerate -m "K-1xx: …"` → generierte Migration PRÜFEN (Constraints benennen, Trigger/Extensions wie `CREATE EXTENSION vector` von Hand ergänzen, `--autogenerate` erfasst die nicht) → `uv run alembic upgrade head`. `/healthz` meldet `db: connected` + Alembic-Revision. + +## CI-Test-DB: Wegwerf-pgvector-Container je Lauf + +CI startet pro Lauf einen frischen `pgvector/pgvector:pg16`-Container (ci.yml Port 5433, deploy-Gate 5434; braucht Docker via LXC-Nesting auf dem Runner), migriert von leer (`alembic upgrade head`) und fährt die SICHT-4-Suite als eigenen benannten, Merge-blockierenden Schritt. Lokal identisch: + +```bash +docker run -d --rm --name nexus-test-pg -e POSTGRES_PASSWORD=test \ + -e POSTGRES_USER=nexus -e POSTGRES_DB=nexus -p 127.0.0.1:5433:5432 \ + pgvector/pgvector:pg16 +``` + +Tests gegen die Test-DB nutzen transaktional isolierte Sessions (Rollback je Test); abweichende URL via `NEXUS_TEST_DATABASE_URL`. + +## Rote Linien (Verstoß = Review-Stopp) + +- Kein direkter `select()` auf sichtbarkeitsrelevante Tabellen außerhalb `visibility.py`; kein HTTP-Client außerhalb `llm_router.py`. +- Kein LLM-Call am `router.run` vorbei; kein externer Fallback für `privat`/`familie`. +- Keine zweite Auth-Identität neben der Session; OIDC-State nicht vor dem Token-Exchange räumen. +- Inhalte nie in Logs/Audit; Secrets nie in Repr/Repo/Wiki/Chat. +- Keine Karte ohne Tests für ihre Akzeptanzkriterien; jede neue Lese-Route in die SICHT-4-Suite.