Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
129e968fff | ||
|
|
17e6ad0346 |
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 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.
|
||||
- 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 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.
|
||||
|
||||
Reference in New Issue
Block a user