Security
On this page
The Hub runs scripts that a model wrote, with your rights, inside your applications. This page says what the Hub bounds, which risks remain and what leaves your machine, and gives the measurements replayed for it. It is for anyone deciding whether to connect a model to their applications.
The measurements ran on Debian 13 with the Hub 0.1.0 built from its sources; those on a running Hub used a throwaway home directory with its own token.
What leaves the machine
Everything the Hub returns goes to the language model, and so to whoever runs it.
| What | Leaves the machine |
|---|---|
| The script the model wrote | Yes: the model is its author |
stdout, stderr and result of a script | Yes: they can hold the content of a document |
| The error trace | Yes: it holds paths of the machine |
state | Yes: it holds content taken from the open documents |
| The path of a capture | Yes: it is part of the answer |
| The image of a capture | Only if the model reads the file |
| The audit journal, the token and the configuration | Never |
A user who opens a confidential document in a connected application knows that its content can be read by the model. In the replay, get_state returned the label Notes of a layer that a script had created, with content_origin set to document; that label is read the way the content of a document is.
The Hub connects only to bridges on 127.0.0.1. Run under strace -f -e trace=connect,bind,listen through a session that included a script that outlasted its delay, the Hub made one connect over IP in total: to 127.0.0.1, on the port of the Blender bridge. cargo test --locked --test invariants reads the source for any other.
Who can reach a bridge
A bridge listens on 127.0.0.1 only, accepts one connection at a time and answers a client that opens with the token. ss -ltn showed one listening socket for the Blender bridge, on the port set for the run: 127.0.0.1:9313. Four probes were sent to it before the Hub connected:
- A
hellowith a wrong token: 0 bytes back, and the connection closed at once. - An HTTP request: 0 bytes back, and the connection closed at once.
- Nothing sent: 0 bytes back, and the connection closed after 5.0 s.
- A
hellowith the right token: 99 bytes back, thehelloanswer, which names the application and the versions and does not hold the token. The connection stayed open.
LibreOffice’s own UNO port has no authentication. With soffice listening on 127.0.0.1:2102 and no token presented, a Python client using python3-uno read the LibreOffice version, 25.2.3.2, opened a Calc document and wrote a cell into it. That path bypasses the token, the script guard and the audit journal.
What the Hub bounds
The limits are keys of the [hub] section of config.toml, in the file init writes; the values below are the defaults it lists.
- Script size,
max_script_bytes, 262,144 bytes: a script of 300,009 bytes was refused withscript is 300009 bytes; the limit is 262144 bytes. - Output per stream,
max_output_bytes, 1,048,576 bytes: a script printing 2,000,000 characters returned astdoutof 1,048,576 bytes and"truncated": true. - Time,
default_timeout_s, 30 s: in theclimode,timeoutafter 30.0 s, no child process left, and the next callokin 31 ms. In thetcpmode,timeoutafter 31.2 s (32.0 s in the second run), thenbusyfor the next call. - One script at a time per application: the
busyabove. In thetcpmode the application was not stopped; it ran its 40-second script to the end, and the Hub answeredokagain 40.3 s after the start (40.1 s in the second run).
Only the cli mode kills a script that runs past its time. In the tcp and uno modes the Hub cannot interrupt a script inside the application: it answers timeout, refuses new scripts with busy, and waits.
The script guard
The guard reads the text of a script before it is sent. A script that called os.remove was refused before reaching Inkscape, with the category, the construct and the line: G-1 (file deletion) at line 3: os.remove. When a path cannot be checked from the text, the guard lets the script through and adds a warning to stderr, for example path not statically verifiable (open()).
A script that wrote a second file and ran it with runpy deleted a file it had created, and the Hub answered ok. That is the limit the repository’s README states: the guard does not see runpy, pickle, http.server, the save functions of the applications, or a path passed to a function as a parameter. The operator lifts the guard with allow_dangerous_scripts = true in the [security] section; the two tools take no such parameter, and the journal then records guard: bypassed.
On the guard’s false positives, cargo test --locked --test guard realistic -- --nocapture printed false positives: 0 of 21 realistic scripts (0.0%).
Files, modes and refusals to start
After init, config.toml and token were both in mode 600. A capture asked for by get_state was a file in mode 600 in a directory in mode 700, under XDG_RUNTIME_DIR; it was still there after the Hub exited with code 0.
The Hub refused to start, with exit code 1 and one line of explanation, in five cases:
| Case | Message, without the path |
|---|---|
| the token file in mode 0640 | has mode 0640; expected 0600 (chmod 600) |
| no token file | does not exist |
config.toml in mode 0644 | configuration file refused: … has mode 0644; expected 0600 (chmod 600) |
| the Hub’s runtime directory in mode 0755 | has mode 0755; expected 0700 (chmod 700) |
XDG_RUNTIME_DIR unset | XDG_RUNTIME_DIR is not set; refusing to write volatile files elsewhere |
The token value appeared in none of these messages. libremodular-launch libreoffice refused a bridge file in mode 0664 before starting anything, with exit code 2: has mode 0664 and can be changed by the group or by others (chmod go-w).
The audit journal
Every call writes one line to audit.jsonl in the state directory, in mode 600, get_state included. Four calls (a script that ran, one the guard refused, one that raised an error, one for a disabled application) wrote four lines. A line holds the date, the tool, the target, the status, the duration, the size and the SHA-256 of the script, the guard’s result and the size of the output:
{
"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
}
The file holds each line on one line; it is broken here to be read.
A marker string placed in the scripts was not found in the journal, nor was the token. The script text is written only if the operator sets audit_script_text = true.
The tests of the repository
These commands are those of the repository’s CI. They ran outside the repository (CARGO_TARGET_DIR).
| Command | Result |
|---|---|
cargo fmt --check | exit code 0 |
cargo clippy --all-targets --locked -- -D warnings | exit code 0 |
cargo test --locked | 328 passed, 0 failed, 3 ignored: two need a real LibreOffice, one needs a user that is not root |
cargo test --locked --test invariants | 36 passed |
python3 scripts/build_bridges.py --check | exit code 0 |
python3 -m unittest discover -s tests/bridges | 189 tests, OK, 6 skipped: two need root, four are for Windows only |
To report a vulnerability, see About.