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-Änderungennpm run buildinweb/. - mypy ist
strict; ruffline-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);
viewerist Pflichtparameter. Share-Zeilen wirken nur beivisibility=='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.pyscanntsrc/und verbietet die geschützten Modelle (Entry, auch alsmodels.Entry) INNERHALB der Query-Konstrukteselect(…),.get(…),.query(…),aliased(…)sowie Raw-SQL (text(…)) auf die geschützten Tabellen — alles außerhalb vonvisibility.py/models.py. Ein bloßermodels.Entry-Bezug (z. B. Typannotation) ist erlaubt; der verbotene Pfad ist die Query. Wer eine neue sichtbarkeitsrelevante Query braucht: Helper INvisibility.pyergä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 inblocked; kein externer Fallback, auch nicht bei Timeout. Das begleitende Audit hängt amaudit_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 zwarblockedzurück, das Event ist aber nicht garantiert persistiert. Wer das Audit verlässlich braucht (K-108 Queue), injiziert eine Session/Sink. - Hard Guard:
privat/familieankind=="external"⇒blocked— zweite Verteidigungslinie vor der Policy. - Allowlist:
EndpointConfigvalidiertkind=="local"gegenLOCAL_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 vorauthorize_access_tokenräumen —session.clear()erst nach dem Exchange (Fehlerpfade einzeln, als HTTPException durch die SessionMiddleware). Rollen aus demgroups-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ßerhalbvisibility.py; kein HTTP-Client außerhalbllm_router.py. - Kein LLM-Call am
router.runvorbei; kein externer Fallback fürprivat/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.