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
l.kirchner d86a801cdc Merge branch 'feat/nexus-stack-skill' 2026-06-11 14:49:50 +02:00
l.kirchner a1756d8839 feat(nexus-dev): add nexus-stack skill (stack conventions since K-101)
Covers uv layout, settings pattern, PRIV-5 logging duties, alembic,
web build, deploy artifact and project commands. Plugin 0.1.0 -> 0.2.0.
2026-06-11 14:49:50 +02:00
2 changed files with 123 additions and 3 deletions
+9 -3
View File
@@ -1,11 +1,17 @@
{
"name": "nexus-dev",
"version": "0.1.0",
"description": "Projekt-Plugin für nexus (Family Knowledge Hub): SDD-Karten-Workflow, Security/Privacy-Invarianten (SICHT/PRIV/fail-closed), Karten-Command. Für Claude Code Sessions im Repo l.kirchner/nexus-hub.",
"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"
]
}
+114
View File
@@ -0,0 +1,114 @@
---
name: nexus-stack
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 (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:
```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)
```
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:
```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.