Journal
On this page
This is the product log of the Hub and its bridges: 12 entries dated 3 and 4 October 2026, in the order they were written. Most name the command that shows what they report, and what had not been tried yet. It is for engineers who want to see how the Hub was built.
2026-10-03 — Clean start in Rust
The hub is rewritten in Rust so that it ships as a single binary with no interpreter to install. The first Python implementation of the security foundation was removed; its behaviour was ported to Rust and compared on 119 scripts (see the next entry). The bridges that run inside the applications stay in Python.
2026-10-03 — Security foundation of the Hub, in Rust
The security foundation that existed in Python is ported to Rust: process hardening, the bridge token,
private control files, path confinement, size bounds, the script guard, the single output point, the
audit journal, strict configuration and the init command. There is no MCP server, no tool, no bridge
and no application module yet.
Done, with the command that shows it:
- Hardening, token, control files, paths and bounds:
cargo test --locked --test token_and_files. - Script guard, with import aliases resolved on the syntax tree:
cargo test --locked --test guard. The false positive rate on the realistic scripts is 0 of 21 (cargo test --locked --test guard realistic -- --nocapture). - Guard results pinned against the earlier Python implementation on 98 scripts, 95 identical and 3 that
differ on purpose (
rmdiris only refused on an object recognised as a path):cargo test --locked --test guard_golden. - Configuration read as a control file, strict by default:
cargo test --locked --test config. - Single output point and audit journal:
cargo test --locked --test output --test audit. initand the two binaries:cargo test --locked --test init.- Mechanical invariants, replayed in fresh processes where an ordering or a permission is observable:
cargo test --locked --test invariants. - Whole suite, formatting, lints and release build:
cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --locked && cargo build --release --locked.
Decisions taken while porting:
- The configuration file is now a control file: it is refused when it is a link, belongs to someone else, carries a group or other permission bit, or exceeds 1 MiB. The Python version read it directly.
- A configuration error reports a kind and a position, never text from the file, because the parser’s own message quotes the surrounding lines.
- The guard runs on its own thread. The stack is 16 MiB plus 512 bytes per byte of script, measured worst case being about 128 bytes per byte, and a script that would need more than 1 GiB (a little under 2 MiB of text) is refused instead of attempted. A panic of the parser is caught and refuses the script.
- Bracket nesting deeper than 200 is refused: CPython itself rejects it, so no valid script is lost.
- Name resolution has a work budget of two million steps, and a name met on its own is resolved once. A script that exhausts the budget is refused as too complex to analyse.
rmdirandunlinkare refused only on an object recognised as a path. The Python version refused any.rmdir, which also caught unknown objects.- The token type prints
Token([redacted])and has noDisplay. The token is not wiped from memory after use: that needs a crate outside the approved list or unsafe code. - Output objects serialise with sorted keys; the journal writes its fixed keys in a fixed order.
Not covered yet:
- The guard cannot see
runpy,pickle,http.server, the save functions of the applications, or a path passed as a function parameter. It is a safety net against accidents, not a sandbox. - A script that ruff’s parser rejects but CPython would accept is not analysed, because an unparsable script is passed on to the application for its own syntax error.
- Nothing has been run against a real application.
2026-10-03 — The Hub serves MCP against a reference bridge
The Hub now serves MCP on stdio with exactly two tools, execute_script and get_state. It reaches
bridges through a line-delimited JSON client on 127.0.0.1, routes only to enabled applications, and
is tested end to end against a reference bridge written for the tests. No application module and no
bridge exist yet: GIMP, Krita and Blender use the generic tcp module; Inkscape and LibreOffice,
when enabled, answer unavailable with “module not available in this build”.
Done, with the command that shows it:
- Bridge client: loopback only, hello with the token, bounded lines, one request in flight, busy after
a timeout until the late answer arrives:
cargo test --locked --test protocol. - Every status (
ok,error,timeout,busy,refused,unavailable), the guard and its bypass, the journal checked before sending, truncation, the token kept out of answers and journal, captures and their purge:cargo test --locked --test server. - The binary on stdio prints only protocol lines and refuses to start (exit code 1, no token shown)
without a token, with a token readable by others, with a volatile directory open to others, or
without
XDG_RUNTIME_DIR:cargo test --locked --test stdio. - Single output point, network confined to
protocol.rsand 127.0.0.1, modules isolated from the security and output modules, nothing butcli.rson stdout:cargo test --locked --test invariants. - Whole suite, formatting, lints and release build:
cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --locked && cargo build --release --locked.
Protocol details fixed while writing the client:
- The
helloanswer must carry"protocol": 1besidesok,app,app_versionandbridge_version; any other answer, a malformed line or a closed connection fails the handshake. - Request ids are
r1,r2, … per connection. An answer with another id breaks the connection. - A bridge that cannot take a request answers
{"id": ..., "ok": false, "busy": true}; the Hub reportsbusy. - A
pingrequest answers{"id": ..., "ok": true}; it is the only thinghealth()sends.
Decisions:
- rmcp 3.5.0 with
default-features = falseand the featuresserver,macrosandtransport-io: the tool macros and the stdio transport, nothing for HTTP, clients or authentication. tokio 1.53.2 withrtonly; the tests addio-utilto speak JSON-RPC lines in memory. Discarded: rmcp’sclientfeature for the tests, raw JSON-RPC lines are enough. - The bridge client uses blocking
std::netsockets, called fromspawn_blocking. It keeps its own line buffer, so it can bound a line, look for a late answer without blocking, and close the socket from another thread. Discarded:tokio::net, which adds thenetfeature for no gain here. - The token is not wiped from memory after use: that needs unsafe code or a crate outside the
approved list. The
Tokentype hides its value fromDebug, has noDisplay, and the process never writes a core dump. - Answers keep their fixed keys.
reasonalso comes withbusy,timeoutandunavailable, which the Hub produces itself. Guard warnings are appended tostderr, prefixed withlibremodular-mcp guard warning:, since the answer has no warnings key. - With
allow_dangerous_scripts = true, everyexecute_scriptis journalled withguard: bypassed, whether or not the guard found something. get_state("hub")lists the enabled applications with the same keys as anyget_state, itscontent_originbeinghub, and the list understate.modules.- A capture reported outside the captures directory is refused but never deleted: a bridge must not be able to make the Hub delete a file elsewhere. A refused capture inside the directory is deleted; a link is removed without touching its target. An accepted capture is reduced to mode 600.
- A client that closes stdin before
initializeends the process with exit code 1 (session failed).
Not covered yet:
- No bridge and no application module: the Python bridges and the GIMP, Krita, Blender, Inkscape and LibreOffice modules come next. Nothing has been run against a real application.
- On SIGTERM or SIGINT the process ends without the shutdown purge; captures older than an hour are purged at the next start.
- The hello does not check that the bridge reports the application the port was configured for.
2026-10-03 — Bridge core and GIMP bridge
The Python side exists: a common core for every bridge of the tcp and uno modes, a GIMP
adapter, and a build script that joins them into the single file GIMP loads,
bridges/generated/gimp_bridge.py. The Hub’s tcp module drives that file end to end. Nothing
has been run inside a real GIMP: the adapter is written from the public GIMP 3.0 API.
Done, with the command that shows it:
- The Hub refuses a bridge that announces another application than the one configured on its
port, without repeating the announced name:
cargo test --locked --test protocol --test server. - On SIGTERM and SIGINT the Hub purges the captures, releases its connections and exits with
code 0, after
libremodular-mcp: stopped on SIGTERM(orSIGINT) on the error stream:cargo test --locked --test stdio. - Bridge core: loopback only, one connection at a time, five seconds for the hello, token
compared in constant time, silent refusal of a bad hello, lines bounded to 8 MiB without
reading a longer one, token file refused when it is a link, another user’s, open to others
or not a regular file, scripts compiled as
<libremodular>in a fresh namespace with output captured and the creation mask at 077, no effect on import:python3 -m unittest discover -s tests/bridges(69 tests, also run on Python 3.9.23). - GIMP adapter with a fake
gipackage and withoutgi; generated file current, compilable for Python 3.9, unchanged by regeneration, without token: same command. - Both, from cargo, which fails when Python is missing, and the generated GIMP bridge driven by
the Hub’s
TcpModule:cargo test --locked --test bridges. - Token comparison and listening sockets only in
bridges/_core.py, generated files made of the core and the adapter only, no address but 127.0.0.1 inbridges/, no outgoing connection or process launch in the core:cargo test --locked --test invariants. - Whole suite, formatting, lints, release build, bridges:
cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --locked && cargo build --release --locked && python3 -m compileall -q bridges && python3 scripts/build_bridges.py --check.
Decisions:
- tokio gains the
signalfeature, which bringsmioandsignal-hook-registryas transitive dependencies. The wait on the session and on the signals is written withstd::future::poll_fn, so tokio’smacrosfeature is not needed. BridgeClient::connectandBridgeConnector::newtake the expected application name; the registry passes the name from its table of known applications.- The bridge reads the application version once, when it starts, in the thread that starts it: the hello never calls the application from a connection thread.
- The core reports an application whose API cannot be imported by a warning in
state; it recognises it by the versionunknown, which every adapter returns in that case. - A script’s exceptions,
SystemExitincluded, are caught inside the call handed to the main thread, and the traceback starts at the first frame of the script. - Each captured stream keeps at most 512 Ki characters, and an answer that would exceed the line limit is reduced (result dropped, texts cut) rather than sent: the Hub would otherwise drop the connection.
- Run as a program, a generated bridge serves until stopped; run from a console with
exec, it returns at once. It tells the two apart by comparing its compiled file name withsys.argv[0]. - The default token path follows
XDG_CONFIG_HOME, then~/.config. The bridge does not readLIBREMODULAR_CONFIG_DIR:LIBREMODULAR_TOKEN_PATHis its explicit override. - The capture exports a flattened copy of the active image, so that the user’s image keeps the
export file name GIMP remembers. The active image is the first one
Gimp.get_images()lists.
Not covered yet:
- Nothing has run inside GIMP: the console gesture,
GLib.idle_addas the way to GIMP’s main thread,Gimp.get_images,Gimp.Selection.bounds,Gimp.file_savewithNoneoptions and the enumeration names are unconfirmed. - A signal that arrives while the Hub is still starting, before the signal handlers are in place, ends the process without the shutdown purge.
- The bridge does not lower
RLIMIT_CORE: it runs inside the application’s process. - A reconnection made right after the previous connection closed can be refused for a few milliseconds, until the bridge notices the closure and frees its single place.
2026-10-04 — The GIMP bridge tried in GIMP 3.0.4, and a hang outside GIMP fixed
The bridge ran in GIMP 3.0.4 (Debian 13 package) started without its interface
(gimp-3.0 --new-instance --no-interface --batch-interpreter python-fu-eval, a Python main
loop kept running after the bridge started), driven by the Hub over MCP on stdio:
get_state("hub")listsgimpas connected, version3.0.4, bridge version1.- A script that creates an image and a layer returns
status: okwith the layer name inresult; the nextget_state("gimp")lists the image (color_mode: rgb), the layer (blend_mode: normal,opacity: 100.0,visible: true),selection: null, and a PNG capture of 200 by 100 pixels in mode 600. - A script that fails returns
NameErrorwith the traceback line 2 of<libremodular>.
Fixed: on a machine where gi and the GIMP typelib are installed, a bridge run as a plain
Python program (as the tests do) queued its calls with GLib.idle_add and waited for a main
loop that never ran, so execute and state never answered. Outside a process GIMP started,
Gimp.version() is None: the adapter now calls directly, and answers state: {} with the
usual warning, without asking GIMP anything. Tests: test_run_on_main_is_direct_when_gi_loads_but_the_process_is_not_gimp,
test_outside_gimp_nothing_asks_gimp_even_when_gi_loads (the first fails on the earlier adapter).
The end-to-end Rust test now says which step failed.
Still unconfirmed: starting the bridge from the interactive Python console (menu path, the environment variables reaching it, its lifetime when the console closes), several open images (which one is listed first), a non-empty selection.
2026-10-04 — Krita and Blender bridges, the Inkscape module, a reconnection delay
Three more applications: Krita and Blender in the tcp mode, each with an adapter and a
generated bridge, and Inkscape in the cli mode, a Rust module that starts one Python process
per script. None of them has been tried in the real application.
Done, with the command that shows it:
- The
tcpmodule waits 100 ms before its second attempt to connect, so a bridge that frees its single place late is not reported unavailable:cargo test --locked --test protocol(health_reaches_a_bridge_that_frees_its_place_late, and its counter-proof without the wait). - Blender adapter: one persistent timer, registered once from the thread that runs the bridge
file, drains a queue in Blender’s main thread; outside Blender’s interface (background mode,
the stand-alone
bpypackage) calls are direct. Krita adapter: a Qt object living in the interface thread receives calls through a queued signal; without a Krita instance and a Qt application, calls are direct. Both:python3 -m unittest discover -s tests/bridges(90 tests, also on Python 3.9.23), including a double whose mechanism never runs anything, and its counter-proof that always queues and waits forever. - The three generated bridges driven end to end by the Hub’s
TcpModule, passing whether or not the application’s module is installed:cargo test --locked --test bridges. - Inkscape module: argument list without shell, new process group, environment cleared to
PATH,HOME,LANG,LC_ALL,PYTHONPATH,LMM_DOCUMENT,LMM_WORK_DIR, script in a private file, limits applied by a fixed bootstrap, whole group killed on timeout, output bounded without blocking, entities refused when reading the state, a linked document refused:cargo test --locked --test inkscape(24 tests, with the test’s Python and a fakeinkscape). - Private work files created once or replaced without following a link:
cargo test --locked --test token_and_files. - No process started in
src/outside the Inkscape module, which answers only through theModuletrait:cargo test --locked --test invariants. - Whole suite, formatting, lints, release build, bridges:
cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --locked && cargo build --release --locked && python3 -m compileall -q bridges && python3 scripts/build_bridges.py --check.
Decisions:
- Blender: a single persistent timer registered from the main thread, rather than one
registration per call from the connection thread. Blender’s own documentation shows the
queue-and-timer pattern for work coming from another thread; whether registering a timer
from another thread is safe is unconfirmed.
app_versionis the first word ofbpy.app.version_string, so4.2.3 LTSreads4.2.3. - Krita: a queued signal to an object of the interface thread, rather than
QTimer.singleShot, which, called from a thread without a Qt event loop, would never fire. The capture savesDocument.projection, a copy; the document is never saved or exported. - Inkscape: the module never opens a file. The registry gives it the Hub’s file operations
(
ensure_private_file,replace_private_fileinsecurity.rs) through aWorkFilestrait, since modules may not import the security module. The process environment values are read byconfig.rs, the only reader of the environment. - Inkscape: the work directory is the one the Hub passes to
execute; the module remembers it forstate, which has no work directory in its signature. Before any script,stateis{}with a warning. The script file is removed once its process ends. - Inkscape: the bootstrap is given the script file and the limits as arguments (CPU seconds,
address space, open files), since the environment is fixed. It drops the current directory
from
sys.pathbefore importing anything, so a file a script left in the work directory cannot shadow a module the bootstrap needs.SystemExitis reported as an error, as in the bridges. The address space limit is 2 GiB, open files 256. - Inkscape: the process group is killed after every run, not only on timeout; the leader is
left unreaped (
waitidwithWNOWAIT) until then, so its group id cannot be reused. A process that leaves its group on purpose is not followed. - Inkscape: the capture runs
inkscapewith the reduced environment and its own process group, without the resource limits of scripts. - The test of applications without a module now uses LibreOffice, since Inkscape has one.
Not covered yet:
- Nothing has run in a real Krita, Blender or Inkscape: the Scripter gesture and the queued
signal,
blender --pythonand the timer, whetherblender --background --pythonkeeps running long enough for requests,Document.projection,inkexonPYTHONPATH, and theinkscapecommand line are unconfirmed. executein thetcpmodule still reconnects at once after a connection lost before the request was sent; onlyhealthandstatewait.
2026-10-04 — Blender, Krita and Inkscape tried, and an Inkscape hang fixed
The three modules ran against the real applications on a Debian 13 machine with 32 cores, driven by the Hub over MCP on stdio, in a throwaway home directory with its own token (Inkscape 1.4, Blender 4.3.2, Krita 5.2.9; the interface ones on a virtual X display):
- Inkscape: the example of
docs/modules/inkscape.mdreturned{"layers": ["Layer 1", "Notes"]};get_statelisted the layers and a PNG capture; a second script found the document as the first had left it; an error returned the right traceback line. - Blender: both paths ran, the direct call without the interface (1 ms) and the persistent timer with it (about 70 ms); the state listed the default scene and the new object.
- Krita: the queued signal to the interface thread ran (1 to 7 ms); the state listed the document and its layers, and the capture was a 300 by 200 PNG in mode 600. The bridge was started by a startup extension of the throwaway profile, not from Scripter.
Fixed: import inkex imports numpy, and its BLAS library starts one thread per core. Under
the module’s 2 GiB address-space limit, thirty-two threads reserving their stacks never
finished starting: the first script that imported inkex ended in timeout after 30 s
(reproduced outside the Hub with RLIMIT_AS alone; OPENBLAS_NUM_THREADS=1 removes it).
The cloud environment has four cores and could not show it. The module now sets
OPENBLAS_NUM_THREADS=1 and OMP_NUM_THREADS=1. Test:
the_process_sees_only_the_allowed_variables_and_never_the_token now also requires both
variables to be 1, and fails without the fix.
Still unconfirmed: starting the Blender and Krita bridges from the consoles of an interface
the operator opened; bpy.ops from the bridge; several documents open in Krita.
2026-10-04 — LibreOffice: three targets, one bridge, a launcher, and two corrections of the Hub
Writer, Calc and Impress are now served by one stand-alone bridge that connects to LibreOffice’s
UNO port, behind three Hub targets that share one connection. libremodular-launch libreoffice
starts LibreOffice and the bridge, or stops the bridge. Two defects found on the operator’s
machine were fixed first. The whole of it ran against a real LibreOffice 24.2.7.2 (not 25.2.3.2).
Done, with the command that shows it:
Module::statereceives the work directory of the application, prepared by the server like the oneexecutegets. An Inkscape module that has run no script describes thecurrent.svga previous Hub left; without one the state stays{}and the document is not created:cargo test --locked --test inkscape --test server(a_new_module_describes_the_document_a_previous_hub_leftand its counter-proof, which models the previous behaviour;get_state_prepares_the_private_work_directory_and_hands_over_the_one_execute_gets).- The second attempt of
TcpModule::execute, after a connection found gone before anything was written, waits the reconnect delay:cargo test --locked --test protocol(execute_waits_before_its_second_attempt_to_connect, which fails with “the bridge closed the connection during the handshake” once the wait is removed, and its counter-proof). - The bridge core hands the family of a request to an adapter that declares
families, refuses an unnamed or unserved family withUnknownFamilybefore any line of the adapter runs, and leaves the other adapters untouched:python3 -m unittest discover -s tests/bridges(FamilyTests; 145 tests in all, run with-W error::ResourceWarning). - The UNO adapter and its generated bridge, with
unoandcom.sun.star.beansimported when a request needs them: the same tests pass withpython3 -S(nounoanywhere) and withPYTHONPATH=/usr/lib/python3/dist-packages LD_LIBRARY_PATH=/usr/lib/libreoffice/program(the realunoimportable), since they install a fake.python3 -m unittest discover -s tests/bridges(test_uno_adapter.py). - The Hub module: one connection for the three targets, the family in every request, a shared
busy state,
get_state("hub")listing the three in modeuno, a harmlessclose():cargo test --locked --test libreoffice. The generated bridge, started by a Python that cannot importuno, answers for the three targets (LibreOfficeUnavailable, state{}with a warning, versionunknown):cargo test --locked --test bridges. - The launcher: argument lists without a shell,
127.0.0.1only, the token in no argument and no variable (only the path of its file), the bridge’s environment exactly the listed one, detached processes in groups of their own, a refusal beside a runningsoffice.bin(read from a fake/proc), a private record,--stopthat spares a process number another program has taken:cargo test --locked --test launch(38 tests, 12 runs in a row without a failure). The helpers it needed insecurity.rs:cargo test --locked --test token_and_files. Only the Inkscape module and the launcher start processes:cargo test --locked --test invariants. - A real LibreOffice, without interface:
cargo test --locked --test libreoffice_real -- --ignored --nocapture(ignored by default,needs LibreOffice and its python3-uno). With a window, on a virtual X display, the reallibremodular-launch, then the reallibremodular-mcpover MCP on stdio: the three targets connected with version24.2.7.2, a script refused withNoActiveDocument, a Calc script run, the state and a PNG capture returned,WrongDocumentTypefor Writer,--stopending the bridge and leaving LibreOffice alone. - Whole suite, formatting, lints, release build, bridges:
cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --locked && cargo build --release --locked && python3 -m compileall -q bridges && python3 scripts/build_bridges.py --check.
Decisions:
- Sharing the connection:
TcpModulegains an optionalfamilyand a connection slot other modules can hold (SharedConnection); its reconnection logic stays in one place and its behaviour without them is unchanged.LibreOfficeModuleis a thin wrapper over it. The registry makes one slot per bridge port. - The core gives an adapter no way to add a warning to a state answer, and changing that was outside this
work.
active: nulltherefore reaches the Hub without a reason, andLibreOfficeModuleadds the warning itself, naming both causes (no current document, or one of another family). - The bridge file is checked by a new
security::check_trusted_file(absolute, regular, not a link, ours, not writable by the group or others; readable by everyone, it is code). Files under/procare read bysecurity::read_system_file, one bounded read without following links. Both are listed as definitions that may exist only insecurity.rs. - LibreOffice receives the environment of the launcher except the
LIBREMODULAR_variables: a window needs the display and the session bus, and no application needs this program’s own variables. The environment is passed in as a parameter (config::environment_snapshotin production), like the programs and/proc. - The record is
soffice <pid>andbridge <pid> <bridge file>. The bridge is recognised when one of the arguments of its command line is exactly the bridge file. Exit code2is a usage error or a refusal the operator can fix,1a failure. - Two refusals beyond the ones asked for: equal
uno_portandbridge_port, and a bridge of an earlier launch that still runs (a second launch would leave it unrecorded and impossible to stop). - The test reference bridge gained a single-place mode, as a real bridge refuses a second connection at
once, and can announce
libreoffice. Fake programs of the launcher tests end when their directory goes. NOT_IN_BUILDis no longer reachable from a configuration, since every known application has a module; it stays for an application that would not have one.- In the build environment (not in the repository)
libreoffice-writer,libreoffice-calc,libreoffice-impressandxvfbwere installed with the package manager to run the real tests.
Findings, not fixed:
- With no document open, every script is refused with
NoActiveDocument: the Hub cannot create the first document, andsoffice --headless --calcopened none. The real test opens it with a separate UNO snippet. - The core reads the application’s version once, when the bridge starts. A bridge started before LibreOffice
listens (the launcher starts them one right after the other, and the bridge waits five seconds) reports
unknownfor good, and every state then carries the warning that the API cannot be imported. LibreOffice listened 1.3 s after its start in one run, and after 8 s but by 14 s on its first start. - The errors the adapter raises itself (
NoActiveDocument,WrongDocumentType,LibreOfficeUnavailable) come back with the frames of the bridge and the path of the bridge file in their trace. - The directory that holds the bridge file is not checked: only the file is.
- The process the launcher records is LibreOffice’s start process, not the
soffice.binthe system lists.
Not covered yet:
- LibreOffice 25.2.3.2, the interface on a real desktop session, Python 3.13.5 of Debian 13, what the PNG capture of a document with several sheets or pages shows, which document is current when the focus is on another window.
- The UNO port has no authentication: any process that can reach it drives LibreOffice without the token, the guard or the journal. Whether LibreOffice filters by user is unconfirmed.
2026-10-04 — LibreOffice tried in LibreOffice 25.2.3.2
cargo test --locked --test libreoffice_real -- --ignored --nocapturepasses against LibreOffice 25.2.3.2 (Debian 13 packages,/usr/bin/python33.13.5), 1 test in 3.1 s, no process left.- The real
libremodular-launch libreofficeand the reallibremodular-mcp serveon a virtual X display, in a throwaway home:get_state("hub")lists the three targets connected in modeunowithapp_version25.2.3.2andbridge_version1; a Calc script is refused withNoActiveDocumentwhile no document is open; after a Calc document is opened, a script writesA1andB2and returns them,get_state("libreoffice-calc")describes the sheet and returns a PNG path, a Writer script is refused withWrongDocumentType;--stopends the bridge and leaves LibreOffice running. The sheet is namedFeuille1there: names follow the locale. - A bridge file checked out under a
umaskof002has mode664and the launcher refuses it (chmod go-w); the guide now says so.
2026-10-04 — Two corrections of the LibreOffice bridge, the public files, the invariants, the cold recipe
Two defects found on the operator’s machine are fixed, each in its own commit. The repository gets the files a public repository needs, the mechanical invariants are reviewed and completed, and a smoke test runs the compiled binary. Nothing is published: no tag, no release.
Done, with the command that shows it:
- The core reads the application version again at each valid hello and at each state while it is
unknown, and keeps the first real answer:python3 -m unittest discover -s tests/bridges(VersionTestsoftest_core.py; counter-proofcore_reading_the_version_once()ofbridge_support.py, the core as it was). The adapters of GIMP, Krita and Blender and their tests are unchanged. The question goes throughrun_on_main, and a hello that does not carry the token never reaches the adapter. - A bridge started before LibreOffice, then LibreOffice, then the state and a new connection:
cargo test --locked --test libreoffice_real -- --ignored --nocapture a_bridge_started_before, against LibreOffice 24.2.7.2 (Ubuntu 24.04 packages installed in the build environment, without interface). Run against the bridge file of before the change it fails with the warning of the cold start still there. - With no document open a LibreOffice script runs with
docset toNoneand can create the first document withdesktop.loadComponentFromURL; the next request receives it; a current document of another family still givesWrongDocumentTypebefore any line runs;NoActiveDocumentis gone from the adapter, its tests and the guide (history entries above keep the name):test_uno_adapter.py(ScriptTests, with the adapter that refuses as counter-proof) andcargo test --locked --test libreoffice_real -- --ignored --nocapture the_hub_drives, which now creates the Calc, Impress and Writer documents through scripts of the Hub. - The example of the guide is the script that creates the document and writes two cells; its answer
comes from the real
libremodular-mcpdriven over MCP on stdio, with LibreOffice 24.2.7.2 in a window on a virtual X display, started by the reallibremodular-launch. README.md,docs/GUIDE_LLM.md,SECURITY.md,LICENSE(MIT),.github/workflows/ci.yml, andscripts/generate_changelog.pywith its test (tests/bridges/test_changelog.py: exact output for a fixed log, a script that misfiles a title and a check that never fails are detected).- Invariants:
cargo test --locked --test invariants(36 tests). New: exact approved dependencies pinned as=x.y.z; no path other than the/home/aliceexamples and no token, journal, capture or operator configuration in the tree;server.rsdeclares exactly two tools and reaches no application module; the README never says AI, Windows or macOS; the README and the guides agree on the versions and the example. Each has its counter-proof. The check that the single place opening a file does not follow links had none: it has one now. tests/smoke.rs: the compiled binary throughinit, then a session of a script that succeeds, one that raises, one refused by the guard and one for an unknown target; four journal lines in order and no script text, only the protocol on the standard output, the token only in its own file. Two counter-proofs: a journal asked to keep the script text, a token copied into another file.- Whole suite, formatting, lints, release build, bridges:
cargo fmt --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --locked && cargo build --release --locked && python3 -m compileall -q bridges && python3 scripts/build_bridges.py --check(322 Rust tests and 3 ignored, 174 Python tests; the Python ones also on 3.9.23 and withpython3 -S). - From a fresh clone made under a
umaskof002, with the commands of the README:cargo build --release --locked,target/release/libremodular-mcp init,cargo test --locked,python3 -m unittest discover -s tests/bridges,python3 scripts/build_bridges.py --check.
Decisions:
- The version is asked through
run_on_main, as the earlier entry on the bridge core decided for the hello: the application is not called from a connection thread. Instateit is asked after the adapter’s own call, which may have waited for the application, and the warning goes first. - The UNO adapter makes one attempt to read the version instead of waiting up to five seconds. With the re-read, a wait there would have been paid at every hello while LibreOffice is down, and the Hub gives up on a hello after five seconds: measured, 5.0 s. The bridge no longer waits at its start either.
- The launcher said “a script only runs on the active document”: one line of its output now says what is true. No other behaviour of the Hub changed.
server.rsdoes import fromsecurity: it chains the checks. The invariant pins the exact list of items it takes, and that it defines none, instead of forbidding the import.- The README states the launcher check as the code makes it: a bridge file the group or others can write is refused, mode 644 is accepted.
- The changelog lists titles under a single
Unreleasedsection, sorted by their first word.
Findings, not fixed:
- The Hub keeps the version of the hello of its current connection: after the bridge has found the
version,
get_statestill saysunknownuntil the Hub connects again (shown by the real cold start test, which asserts it). - The errors the LibreOffice adapter raises itself still come back with the frames of the bridge file in their trace, and the directory that holds the bridge file is still not checked.
CHANGELOG.mdis built from the history: any commit after it makespython3 scripts/generate_changelog.py --checkfail until the file is generated again, which is the point of that check before a publication.
Not covered yet:
- LibreOffice 25.2.3.2 for the two corrections, and a launcher start where LibreOffice is slow: it answered on UNO about one second after its start here.
- The CI workflow has never run on the forge. Private vulnerability reporting, which
SECURITY.mdnames, must be enabled in the repository settings before publication; it cannot be checked from here.
2026-10-04 — The two corrections tried in LibreOffice 25.2.3.2, and a race in the launcher tests
cargo test --locked --test libreoffice_real -- --ignored --nocapturepasses on LibreOffice 25.2.3.2 (2 tests, 6.41 s, no process left): a script with no open document receivesdoc = Noneand creates the Calc document, and a bridge started before LibreOffice finds the version once it listens.- A test of the launcher failed once in about six runs with “Text file busy” (os error 26), on
mainas on the branch of the previous entry. Cause, measured: a test writes the fake program it will run while another test of the same process starts a process; the child holds a copy of the write descriptor until itsexec, so running the file fails.tests/launch.rsnow takes one lock while a fake program is written and while any process is started. After the change, 40 runs ofcargo test --locked --test launchout of 40 pass. No source of the Hub changed. - Still not covered: a launcher start where LibreOffice is slow (14 s measured at the first start of an empty profile), and a real desktop session.
2026-10-04 — The presentation line opens the README
The README now opens, right under the title, with the line that says what the project is: “Universal MCP
server written in Rust, with a Hub that exposes only two tools”. The same line is to open the site and the
manual page. cargo test --locked --test invariants (36 tests) and the bridge tests still pass.