Files
claude-marketplace/nexus-dev/skills/nexus-stack/SKILL.md
T

9.4 KiB

name, description
name description
nexus-stack 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 (Stand: K-104 gemerged)

Präskriptive Muster gegen den realen Code. Bei Abweichung gilt der Code; melde Drift in diesem Skill.

Layout & Kommandos

  • 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.

FastAPI-App-Factory (src/nexus/main.py)

create_app() ist die EINE Stelle, die zusammensteckt — kein App-Aufbau in Routen/Tests:

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)

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.

Settings (src/nexus/config.py)

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.

Lese-Pfad: nexus/visibility.py ist der EINZIGE Query-Weg (SICHT-1/2/3)

Jede Lese-Query auf Entry/EntryVersion/EntryShare/EntrySource/Proposal/Source lebt in nexus/visibility.py — nirgends sonst. Pattern:

# 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 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.

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)

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äumensession.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:

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.