Compare commits

..
Author SHA1 Message Date
claude-bot 129e968fff nexus-stack: tighten 3 review findings (audit-on-block caveat, arch-test pattern scope, fail-closed only for nullable settings) 2026-06-13 13:40:43 +02:00
claude-bot 17e6ad0346 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.
2026-06-13 13:37:31 +02:00
l.kirchner 5e1e11dce8 Merge pull request 'feat(nexus-dev): nexus-stack skill (Stack-Konventionen seit K-101)' (#1) from feat/nexus-stack-skill into main
Reviewed-on: #1
2026-06-11 15:15:51 +02:00
2 changed files with 106 additions and 31 deletions
+8 -2
View File
@@ -1,11 +1,17 @@
{ {
"name": "nexus-dev", "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.", "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": { "author": {
"name": "l.kirchner", "name": "l.kirchner",
"url": "https://gitea.luki-net.org/l.kirchner" "url": "https://gitea.luki-net.org/l.kirchner"
}, },
"homepage": "https://gitea.luki-net.org/l.kirchner/nexus-hub", "homepage": "https://gitea.luki-net.org/l.kirchner/nexus-hub",
"keywords": ["nexus", "sdd", "fastapi", "privacy", "acl"] "keywords": [
"nexus",
"sdd",
"fastapi",
"privacy",
"acl"
]
} }
+98 -29
View File
@@ -1,45 +1,114 @@
--- ---
name: nexus-stack 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`. ## Layout & Kommandos
- **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.
## 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). ## FastAPI-App-Factory (`src/nexus/main.py`)
- 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"`.
## 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`. ```python
- 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. def create_app() -> FastAPI:
- Log-Zeilen tragen IDs + Metadaten, nie Inhalte. Secrets (DB-Passwörter, Tokens) zusätzlich als `secrets=[...]` an `configure_logging` geben. settings = get_settings()
- Jede Karte mit neuen Log-Pfaden ergänzt einen Redaction-Test nach dem Muster `tests/test_logging_redaction.py`. 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. ## Settings (`src/nexus/config.py`)
- 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.
## 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 gilt für die nullable Secrets/DSN:** ohne `database_url`/`oidc_discovery_url`/`oidc_client_secret`/`session_secret`/`anthropic_api_key` ist das jeweilige Feature aus. Achtung: NICHT alle Felder sind so — `oidc_client_id` hat den Default `nexus-web`, `public_url` einen http-localhost-Default, und ohne `session_secret` läuft die SessionMiddleware mit einem EPHEMEREN Secret. Den OIDC-Login schaltet erst `build_oauth` fail-closed ab (None ⇒ `/auth/login` 503), wenn Discovery/Secret/Session-Secret fehlen oder in Prod `public_url` nicht https ist.
- 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. ## Lese-Pfad: `nexus/visibility.py` ist der EINZIGE Query-Weg (SICHT-1/2/3)
- 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).
## 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). ```python
- Keine neuen Top-Level-Prozesse außer API + Worker ohne ADR. # in der Route:
- API-first: jeder User-Workflow hat API-Tests, bevor/während die UI ihn bekommt. 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 die geschützten Modelle (`Entry`, auch als `models.Entry`) INNERHALB der Query-Konstrukte `select(…)`, `.get(…)`, `.query(…)`, `aliased(…)` sowie Raw-SQL (`text(…)`) auf die geschützten Tabellen — alles außerhalb von `visibility.py`/`models.py`. Ein bloßer `models.Entry`-Bezug (z. B. Typannotation) ist erlaubt; der verbotene Pfad ist die Query. 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`; **kein** externer Fallback, auch nicht bei Timeout. Das begleitende Audit hängt am `audit_sink`: per Default schreibt der Router via Session, und ohne erreichbare/ konfigurierte DB (`audit_session`/`NEXUS_DATABASE_URL`) kann der Audit-Write fehlschlagen — der Router gibt dann zwar `blocked` zurück, das Event ist aber nicht garantiert persistiert. Wer das Audit verlässlich braucht (K-108 Queue), injiziert eine Session/Sink.
- **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.