128 lines
9.6 KiB
Markdown
128 lines
9.6 KiB
Markdown
# Sandbox für Tests, Linter und Builds – Konzept
|
||
|
||
Stand: 23.10.2026. Noch nicht umgesetzt. Entscheidungen zu Ort und Isolation stehen aus (siehe [Offene Fragen](#offene-fragen-an-das-openshift-plattform-team)).
|
||
|
||
## Ziel
|
||
|
||
Die KI führt auf dem Stand des Chat-Branches Tests, Linter oder einen Build aus, sieht das Ergebnis und bessert nach. Sie kommt dabei nicht an Netz, Zugangsdaten oder andere Systeme. Ergebnisse sind für Menschen sichtbar (Ansicht *Änderungen*, Beschreibung des Pull Requests).
|
||
|
||
Nicht Ziel: beliebige Programme mit Zugriff auf Produktivsysteme ausführen, Deployments, Pushes aus der Sandbox.
|
||
|
||
## Grundsätze
|
||
|
||
1. **Code aus dem Repository ist fremder Code.** Jeder mit Schreibrecht auf das Repository kann Code einschleusen, der in der Sandbox läuft; Testcode ist Code. Die Sandbox muss das aushalten.
|
||
2. **Kein Netz, keine Geheimnisse.** Kein Git-Token, keine DB-Zugänge, keine Umgebungsvariablen der Plattform. Netz höchstens zum internen Paket-Mirror.
|
||
3. **Nie auf dem Rechner bzw. im Pod des Workers.** Jede Ausführung bekommt einen frischen, kurzlebigen Container.
|
||
4. **Nur Ergebnisse kommen zurück.** Exit-Code, gekürzte Ausgabe, optional JUnit-XML. Ändert ein Formatter Dateien, kommt das als Diff zurück und wird wie jeder andere Commit über `commit_change` angelegt. Aus der Sandbox wird nie gepusht.
|
||
5. **Ausgaben sind Fremdinhalt.** Testausgaben gehen eingefasst an die KI (`app.security.untrusted`), denn auch sie können Prompt Injection enthalten.
|
||
|
||
## Isolation
|
||
|
||
| Variante | Isolation | Einschätzung |
|
||
|---|---|---|
|
||
| Docker/Podman rootless | Teilt den Kernel mit dem Host | Für die lokale Entwicklung |
|
||
| Pod mit `restricted-v2` (OpenShift-Standard) | Teilt den Kernel mit dem Node; zufällige UID, keine Capabilities, Seccomp | Gleiches Niveau wie eure CI auf OpenShift; Start auf OpenShift |
|
||
| gVisor (`runsc`) | Eigener Kernel im Userspace | Gut, braucht Installation durch Cluster-Admins |
|
||
| Kata Containers / „OpenShift sandboxed containers“ | Jede Ausführung als kleine VM | Am stärksten; Nutzung nur über `runtimeClassName: kata` |
|
||
|
||
## Betrieb auf OpenShift (Zielumgebung)
|
||
|
||
Bifröst wird auf OpenShift betrieben, ohne Cluster-Admin-Rechte und ohne privilegierte Container. Das reicht: Die Plattform startet selbst keine Container (kein Docker/Podman im Pod), sondern lässt über die OpenShift-API für jeden Lauf einen kurzlebigen **Job** anlegen, wie es CI-Systeme tun.
|
||
|
||
### Aufbau
|
||
|
||
- **Eigener Namespace**, z. B. `bifrost-sandbox`, getrennt vom Namespace der Plattform. Secrets der Plattform sind von dort nicht erreichbar.
|
||
- **ServiceAccount der Plattform** mit Rechten nur in diesem Namespace (Role/RoleBinding, kein Cluster-Admin):
|
||
- `jobs`, `pods`: create, get, list, watch, delete
|
||
- `pods/log`: get
|
||
- **NetworkPolicy:** Egress standardmäßig verboten; erlaubt sind nur der Paket-Mirror und der Endpunkt der Plattform zum Abholen des Codes (siehe unten).
|
||
- **ResourceQuota und LimitRange:** begrenzen CPU, RAM, Speicher und die Zahl gleichzeitiger Sandboxes im Namespace.
|
||
- **Je Pod:**
|
||
- `activeDeadlineSeconds` (z. B. 600), `backoffLimit: 0`, `ttlSecondsAfterFinished`
|
||
- `automountServiceAccountToken: false`
|
||
- `securityContext`: `runAsNonRoot`, `readOnlyRootFilesystem`, `allowPrivilegeEscalation: false`, alle Capabilities entfernt, `seccompProfile: RuntimeDefault`
|
||
- `emptyDir` als einziges beschreibbares Arbeitsverzeichnis (mit `sizeLimit`)
|
||
- optional `runtimeClassName: kata`, wenn verfügbar
|
||
- **Images:** fertige Images aus der eigenen Registry (Python, Node, Java/Maven …), gebaut von der CI. Im Cluster wird nichts gebaut.
|
||
|
||
### Code hinein, Ergebnis heraus
|
||
|
||
- **Empfohlen:** Ein Init-Container lädt das Archiv des Branch-Stands (`git archive` des Commits) mit einem **Einmal-Token** von einem internen Endpunkt der Plattform. Das Token gilt nur für diesen Lauf und Commit und läuft nach Minuten ab. Das ist neben dem Mirror die einzige erlaubte Verbindung.
|
||
- **Alternative:** Die Plattform lädt das Archiv per `exec` in den Pod. Das braucht das Recht `pods/exec`, das oft nicht vergeben wird.
|
||
- **Ergebnis:** Exit-Code aus dem Job-Status, Ausgabe über `pods/log` (gekürzt), JUnit-XML als markierter Abschnitt der Ausgabe.
|
||
|
||
### Grenze
|
||
|
||
Normale Pods teilen sich den Kernel mit dem Node. Die Isolation besteht aus SCC, Seccomp, fehlendem Netz, fehlenden Geheimnissen und Limits, nicht aus einem eigenen Kernel. Das entspricht dem Niveau von OpenShift Pipelines (Tekton), Jenkins-Agents und GitLab-Runnern auf OpenShift, denen Code aus den eigenen Repositories schon heute anvertraut wird. Mehr Isolation bringt Kata („OpenShift sandboxed containers“): einmalige Installation durch die Cluster-Admins, danach nur `runtimeClassName: kata` im Pod.
|
||
|
||
### Variante mit OpenShift Pipelines (Tekton)
|
||
|
||
Ist OpenShift Pipelines verfügbar, kann die Plattform statt eigener Jobs einen **PipelineRun** starten und Status und Logs über die API lesen. Vorteil: bekannte Bedienung, Logs und Wiederverwendung vorhandener Tasks. Nachteil: Abhängigkeit vom Operator und etwas mehr Latenz.
|
||
|
||
## Befehle: fest statt freier Shell
|
||
|
||
Stufe 1 kennt nur **feste Befehle je Quelle**, z. B.:
|
||
|
||
| Name | Beispiel |
|
||
|---|---|
|
||
| `test` | `uv run pytest -q` |
|
||
| `lint` | `uv run ruff check .` |
|
||
| `build` | `mvn -q -DskipTests package` |
|
||
|
||
Festgelegt von Workspace-Admins an der Quelle (Image, Setup-Befehl, Befehle) oder in einer Datei im Repository (z. B. `bifrost.yml`), wobei Image und Limits immer aus der Plattform kommen. Die KI ruft ein Tool wie `run_check(source, name)` auf. Freie Befehle wären eine eigene, höhere Risikoklasse mit Freigabe und kommen frühestens in Stufe 3.
|
||
|
||
**Einstufung:** `run_check` wirkt nur in der Sandbox, verändert nichts außerhalb und wird wie `write_internal` behandelt (Vorschlag: erlaubt). Freie Befehle: Freigabe nötig.
|
||
|
||
## Abhängigkeiten
|
||
|
||
Der aufwendigste Teil im Alltag:
|
||
|
||
- **Im Image:** Für den Start enthalten die Images die Abhängigkeiten oder installieren sie im Setup-Befehl aus dem Mirror.
|
||
- **Mirror:** Paketquellen (PyPI, npm, Maven) über den internen Mirror (Nexus/Artifactory), aus dem Sandbox-Namespace ohne Zugangsdaten erreichbar oder mit einem reinen Lese-Token nur für den Mirror.
|
||
- **Cache:** später je Quelle zwischenspeichern, gültig bis sich die Lockfiles ändern (z. B. PersistentVolume oder Cache-Image).
|
||
|
||
## Einbindung in die Plattform
|
||
|
||
- **Schnittstelle mit zwei Backends:** *lokal* (Podman/Docker, für Entwicklung und Tests) und *OpenShift* (Jobs über die Kubernetes-API). Die API lässt sich mit dem vorhandenen `httpx` ansprechen; eine Client-Bibliothek wäre bequemer, kommt aber nur nach Rückfrage hinzu.
|
||
- **Runs:** Ein Testlauf dauert Minuten; der Run läuft mit Heartbeat weiter wie bisher. Abbruch des Runs löscht den Job.
|
||
- **Anzeige:** Tool-Schritt mit Live-Status; in *Änderungen* ein Badge „Tests grün/rot“ am Commit; Ergebnis auch in der Beschreibung des Pull Requests.
|
||
- **Steuerung:** Limits je Team (gleichzeitige Läufe, Minuten pro Tag), Audit-Log (`sandbox.run` mit Befehl, Commit, Dauer, Exit-Code), Metriken (`aiw_sandbox_runs_total`, Dauer).
|
||
- **Konfiguration (Vorschlag):** `AIW_SANDBOX_BACKEND` (`none` | `local` | `openshift`), `AIW_SANDBOX_NAMESPACE`, `AIW_SANDBOX_RUNTIME_CLASS`, `AIW_SANDBOX_TIMEOUT_SECONDS`, `AIW_SANDBOX_CPU`, `AIW_SANDBOX_MEMORY`, `AIW_SANDBOX_ALLOWED_IMAGES`.
|
||
|
||
## Stufen und Aufwand (grob)
|
||
|
||
| Stufe | Inhalt | Grobe Schätzung |
|
||
|---|---|---|
|
||
| 1 | Feste Befehle, Backend lokal und OpenShift-Jobs, kein Netz, Abhängigkeiten im Image, Ergebnis im Chat | ca. 1–2 Wochen |
|
||
| 2 | Paket-Mirror, Cache, JUnit-Auswertung, Badge in *Änderungen* und im Pull Request, Limits je Team | weitere 1–2 Wochen |
|
||
| 3 | Freie Befehle mit Freigabe, länger lebende Sandbox je Chat, Kata bzw. Tekton | abhängig von der Infrastruktur |
|
||
|
||
Das OpenShift-Backend lässt sich vollständig erst im eigenen Cluster testen.
|
||
|
||
## Alternative ohne eigene Sandbox: vorhandene CI
|
||
|
||
Die Plattform pusht den `ai/*`-Branch, die CI (Bitbucket Pipelines, Jenkins, Tekton) baut ihn, die Plattform liest den Build-Status über die Bitbucket-API und gibt ihn der KI.
|
||
|
||
- **Vorteil:** Isolation, Images und Mirror sind dort schon vorhanden.
|
||
- **Nachteile:** langsameres Feedback; widerspricht dem Grundsatz „nur Menschen pushen“ und bräuchte eine Ausnahme (z. B. automatischer Push nur auf `ai/*`, abgesichert durch die Branch Permission).
|
||
|
||
Geeignet als Übergang, falls die Sandbox auf sich warten lässt.
|
||
|
||
## Offene Fragen an das OpenShift-Plattform-Team
|
||
|
||
1. Bekommen wir einen eigenen Namespace für Sandboxes, mit Role/RoleBinding für einen ServiceAccount (Jobs/Pods anlegen und löschen, `pods/log` lesen), NetworkPolicies und ResourceQuota?
|
||
2. Ist der Operator **OpenShift sandboxed containers** (Kata) verfügbar bzw. geplant (`runtimeClassName`)?
|
||
3. Gibt es einen Paket-Mirror (PyPI, npm, Maven), der aus dem Cluster ohne Zugangsdaten erreichbar ist?
|
||
4. Ist **OpenShift Pipelines** (Tekton) verfügbar?
|
||
5. Aus welcher Registry dürfen Images gezogen werden, und wer baut und scannt die Sandbox-Images?
|
||
|
||
## Risiken und Gegenmaßnahmen
|
||
|
||
| Risiko | Gegenmaßnahme |
|
||
|---|---|
|
||
| Eingeschleuster Code im Repository | Kurzlebiger Container, `restricted-v2`, Seccomp, kein root, optional Kata |
|
||
| Datenabfluss | Egress verboten außer Mirror und Code-Endpunkt; keine Geheimnisse im Container |
|
||
| Missbrauch von Ressourcen | Quota, LimitRange, `activeDeadlineSeconds`, Limits je Team |
|
||
| Ausbruch aus dem Container | Aktuelle Nodes, Seccomp; für höheres Schutzniveau Kata |
|
||
| Prompt Injection über Testausgaben | Ausgabe als Fremdinhalt eingefasst; schreibende Schritte bleiben an die Regeln der Plattform gebunden |
|
||
| Geheimnisse in der Historie des Repositorys | Nur der Stand des Commits (`git archive`), keine Historie; ohne Netz kein Abfluss |
|