# Sicherheit

Der Hub führt Skripte aus, die ein Modell geschrieben hat, mit Ihren Rechten, in Ihren Anwendungen. Die Seite sagt, was der Hub begrenzt, welche Risiken bleiben und was Ihren Rechner verlässt, und nennt die dafür wiederholten Messungen. Sie richtet sich an alle, die entscheiden, ob sie ein Modell mit ihren Anwendungen verbinden.

Die Messungen liefen unter Debian 13 mit dem Hub 0.1.0, gebaut aus seinen Quellen; die an einem laufenden Hub nutzten ein Wegwerf-Home-Verzeichnis mit eigenem Token.

> **Grenze**
>
> Der Hub ist keine Sandbox. Ein Skript läuft mit den Rechten Ihres Benutzers, in einer Anwendung, deren Python alles tun kann, was Sie können. Die Skriptsperre hält ein ungeschicktes Skript auf, kein entschlossenes. Der UNO-Port von LibreOffice hat keine Authentifizierung, und ein anderer Prozess Ihres Benutzers kann die Token-Datei lesen. Ein Skript, das in einer Schleife läuft, hält seine Anwendung beschäftigt, bis Sie sie schließen, außer im Modus `cli`.


## Was den Rechner verlässt

Alles, was der Hub zurückgibt, geht an das Sprachmodell und damit an alle, die es ausführen.

| Was | Verlässt den Rechner |
|---|---|
| Das Skript, das das Modell geschrieben hat | Ja: Das Modell ist sein Autor |
| `stdout`, `stderr` und `result` eines Skripts | Ja: Sie können den Inhalt eines Dokuments enthalten |
| Die Fehlerspur | Ja: Sie enthält Pfade des Rechners |
| `state` | Ja: Er enthält Inhalte aus den geöffneten Dokumenten |
| Der Pfad einer Aufnahme | Ja: er gehört zur Antwort |
| Das Bild einer Aufnahme | Nur wenn das Modell die Datei liest |
| Das Audit-Journal, das Token und die Konfiguration | Nie |

Wer in einer verbundenen Anwendung ein vertrauliches Dokument öffnet, weiß, dass das Modell dessen Inhalt lesen kann. Bei der Wiederholung gab `get_state` die Bezeichnung `Notes` einer Ebene zurück, die ein Skript angelegt hatte, mit `content_origin` auf `document`; diese Bezeichnung wird so gelesen wie der Inhalt eines Dokuments.

Der Hub verbindet sich nur mit Brücken auf `127.0.0.1`. Unter `strace -f -e trace=connect,bind,listen` durch eine Sitzung hindurch, die ein Skript enthielt, das seine Frist überschritt, machte der Hub insgesamt ein `connect` über IP: zu `127.0.0.1`, auf dem Port der Blender-Brücke. `cargo test --locked --test invariants` sucht im Quelltext nach jeder weiteren.

## Wer eine Brücke erreichen kann

Eine Brücke lauscht nur auf `127.0.0.1`, nimmt jeweils eine Verbindung an und antwortet einem Client, der mit dem Token beginnt. `ss -ltn` zeigte einen lauschenden Socket für die Blender-Brücke, auf dem für den Lauf gesetzten Port: `127.0.0.1:9313`. Vier Proben wurden an sie gesendet, bevor sich der Hub verband:

- Ein `hello` mit falschem Token: 0 Bytes zurück, und die Verbindung wurde sofort geschlossen.
- Eine HTTP-Anfrage: 0 Bytes zurück, und die Verbindung wurde sofort geschlossen.
- Nichts gesendet: 0 Bytes zurück, und die Verbindung wurde nach 5,0 s geschlossen.
- Ein `hello` mit dem richtigen Token: 99 Bytes zurück, die `hello`-Antwort, die die Anwendung und die Versionen nennt und das Token nicht enthält. Die Verbindung blieb offen.

Der eigene UNO-Port von LibreOffice hat keine Authentifizierung. Mit `soffice`, das auf `127.0.0.1:2102` lauschte, und ohne vorgelegtes Token las ein Python-Client mit `python3-uno` die LibreOffice-Version `25.2.3.2`, öffnete ein Calc-Dokument und schrieb eine Zelle hinein. Dieser Weg umgeht das Token, die Skriptsperre und das Audit-Journal.

## Was der Hub begrenzt

Die Grenzen sind Schlüssel des Abschnitts `[hub]` in `config.toml`, in der Datei, die `init` schreibt; die Werte unten sind die Standardwerte, die sie aufführt.

- **Skriptgröße**, `max_script_bytes`, 262 144 Bytes: Ein Skript von 300 009 Bytes wurde mit `script is 300009 bytes; the limit is 262144 bytes` abgelehnt.
- **Ausgabe pro Strom**, `max_output_bytes`, 1 048 576 Bytes: Ein Skript, das 2 000 000 Zeichen ausgab, lieferte ein `stdout` von 1 048 576 Bytes und `"truncated": true`.
- **Zeit**, `default_timeout_s`, 30 s: Im Modus `cli` kam `timeout` nach 30,0 s, kein Kindprozess blieb zurück, und der nächste Aufruf war nach 31 ms `ok`. Im Modus `tcp` kam `timeout` nach 31,2 s (32,0 s im zweiten Lauf), dann `busy` für den nächsten Aufruf.
- **Ein Skript zur Zeit pro Anwendung**: das `busy` oben. Im Modus `tcp` wurde die Anwendung nicht angehalten; sie führte ihr Skript von 40 Sekunden bis zum Ende aus, und der Hub antwortete 40,3 s nach dem Start wieder mit `ok` (40,1 s im zweiten Lauf).

Nur der Modus `cli` beendet ein Skript, das seine Zeit überschreitet. In den Modi `tcp` und `uno` kann der Hub ein Skript in der Anwendung nicht unterbrechen: Er antwortet `timeout`, lehnt neue Skripte mit `busy` ab und wartet.

## Die Skriptsperre

Die Sperre liest den Text eines Skripts, bevor es gesendet wird. Ein Skript, das `os.remove` aufrief, wurde abgelehnt, bevor es Inkscape erreichte, mit Kategorie, Konstrukt und Zeile: `G-1 (file deletion) at line 3: os.remove`. Lässt sich ein Pfad aus dem Text nicht prüfen, lässt die Sperre das Skript durch und fügt `stderr` eine Warnung hinzu, zum Beispiel `path not statically verifiable (open())`.

Ein Skript, das eine zweite Datei schrieb und mit `runpy` ausführte, löschte eine Datei, die es selbst angelegt hatte, und der Hub antwortete `ok`. Das ist die Grenze, die die README des Repositorys nennt: Die Sperre sieht `runpy`, `pickle`, `http.server`, die Speicherfunktionen der Anwendungen und einen Pfad, der einer Funktion als Parameter übergeben wird, nicht. Der Bediener hebt die Sperre mit `allow_dangerous_scripts = true` im Abschnitt `[security]` auf; die beiden Werkzeuge kennen keinen solchen Parameter, und das Journal verzeichnet dann `guard: bypassed`.

Zu den Fehlalarmen der Sperre gab `cargo test --locked --test guard realistic -- --nocapture` aus: `false positives: 0 of 21 realistic scripts (0.0%)`.

## Dateien, Modi und verweigerter Start

Nach `init` hatten `config.toml` und `token` beide den Modus 600. Eine Aufnahme, die `get_state` angefordert hatte, war eine Datei im Modus 600 in einem Verzeichnis im Modus 700 unter `XDG_RUNTIME_DIR`; sie war nach dem Ende des Hubs mit Exit-Code 0 noch vorhanden.

Der Hub verweigerte den Start, mit Exit-Code 1 und einer erklärenden Zeile, in fünf Fällen:

| Fall | Meldung, ohne den Pfad |
|---|---|
| die Token-Datei im Modus 0640 | `has mode 0640; expected 0600 (chmod 600)` |
| keine Token-Datei | `does not exist` |
| `config.toml` im Modus 0644 | `configuration file refused: … has mode 0644; expected 0600 (chmod 600)` |
| das Laufzeitverzeichnis des Hubs im Modus 0755 | `has mode 0755; expected 0700 (chmod 700)` |
| `XDG_RUNTIME_DIR` nicht gesetzt | `XDG_RUNTIME_DIR is not set; refusing to write volatile files elsewhere` |

Der Wert des Tokens erschien in keiner dieser Meldungen. `libremodular-launch libreoffice` lehnte eine Brückendatei im Modus 0664 ab, bevor es irgendetwas startete, mit Exit-Code 2: `has mode 0664 and can be changed by the group or by others (chmod go-w)`.

## Das Audit-Journal

Jeder Aufruf schreibt eine Zeile in `audit.jsonl` im Zustandsverzeichnis, im Modus 600, auch `get_state`. Vier Aufrufe (ein Skript, das lief, eines, das die Sperre ablehnte, eines, das einen Fehler auslöste, und eines für eine deaktivierte Anwendung) schrieben vier Zeilen. Eine Zeile enthält das Datum, das Werkzeug, das Ziel, den Status, die Dauer, die Größe und den SHA-256 des Skripts, das Ergebnis der Sperre und die Größe der Ausgabe:

```json
{
  "ts": "2026-10-21T08:56:27.564Z",
  "tool": "execute_script",
  "target_app": "inkscape",
  "status": "refused",
  "duration_ms": 0,
  "script_bytes": 60,
  "script_sha256": "3696b8a4f24f23da34ba239bbb401c56cd78fb53c2bef48ab2f7d668fd1fdbf8",
  "guard": [
    "G-1"
  ],
  "output_bytes": 524
}
```

Die Datei hält jeden Eintrag in einer Zeile; hier ist er umbrochen, damit er sich lesen lässt.

Eine Markierungszeichenfolge, die in die Skripte gesetzt wurde, fand sich nicht im Journal, ebenso wenig das Token. Der Skripttext wird nur geschrieben, wenn der Bediener `audit_script_text = true` setzt.

## Die Tests des Repositorys

Diese Befehle sind die der CI des Repositorys. Sie liefen außerhalb des Repositorys (`CARGO_TARGET_DIR`).

| Befehl | Ergebnis |
|---|---|
| `cargo fmt --check` | Exit-Code 0 |
| `cargo clippy --all-targets --locked -- -D warnings` | Exit-Code 0 |
| `cargo test --locked` | 328 bestanden, 0 fehlgeschlagen, 3 ignoriert: zwei brauchen ein echtes LibreOffice, einer einen Benutzer, der nicht root ist |
| `cargo test --locked --test invariants` | 36 bestanden |
| `python3 scripts/build_bridges.py --check` | Exit-Code 0 |
| `python3 -m unittest discover -s tests/bridges` | 189 Tests, OK, 6 übersprungen: zwei brauchen root, vier gelten nur unter Windows |

Eine Schwachstelle melden Sie wie unter [Über das Projekt](https://libremodular.davalan.fr/de/about/index.md#eine-schwachstelle-melden) beschrieben.

