A local language model proposes a bench experiment. A validator the model cannot read decides whether that experiment may run.
Built for the electrochemistry bench at Rowow LLC, which develops open source ion exchange membrane electrolysis for precious metal and rare earth recovery.
Letting a language model operate lab instruments is already done. Several MCP instrument bridges exist, and small local models have been driven against real hardware in published work. Building another bridge is not the contribution.
What is missing from those is enforcement. LabLink's own documentation says its constraints are "advisory only... it does not and cannot enforce them."
ionsmith is built the other way round. The model cannot exceed the envelope, and cannot discover where the envelope is:
- Limits live in a file the model can neither read nor write.
- A rejection names the quantity that was out of range and never the threshold, never the direction, never how far.
- The model does not issue instrument calls at all. It writes a complete experiment document, that document is reviewed in full before anything is energised, and only then does a deterministic runner execute it.
An agent that cannot find the boundary cannot be walked up to it, whether by an unlucky sample, by text injected into a document it was asked to read, or by a retry loop that quietly turns into a search for the edge.
There is a worked demonstration of that last point in RESULTS.md.
Asked for an experiment at a voltage above the ceiling and told only
voltage_out_of_range, the model did not step the voltage down looking for the
limit. It redesigned the experiment into something different, which then failed
a different budget. It never converged on the threshold, because nothing it was
told pointed at one.
This is software. It is not an interlock and it is not a protection device.
It does not replace, and must not be treated as a substitute for, hardware current limiting, fusing, ventilation, an emergency stop, or a competent operator who is present and watching. A software validator running on a general purpose operating system can be bypassed, misconfigured, or simply wrong. Every hazard control that a bench needs without this project, it still needs with it.
The property this project demonstrates is narrow and specific:
The model cannot command the bench outside the configured limits, and cannot learn what those limits are.
That claim is testable and the test suite asserts it.
A separate claim it does not make is that an accepted profile is the experiment you asked for. In testing, a request for a 45 minute soak at 1.8 A produced an accepted profile that ran 0.1 A for 271 seconds: comfortably inside the envelope, correctly accepted, and not the requested experiment.
ionsmith/fidelity.py compares the objective text against the profile and
prints a warning when they disagree:
ACCEPTED, but it may not be the experiment you asked for:
amps: objective asks 1.8, profile has 0.1 (94% lower than asked)
seconds: objective asks 2700, profile has 270 (90% lower than asked)
The limits were not exceeded. Read the profile before arming.
That check is report only. It cannot accept or refuse anything, it never changes an exit code, and nothing it produces is sent back to the model. The validator enforces limits; it does not enforce fidelity to the objective, and the warning is there so a silent substitution is visible before the bench is armed rather than after. See RESULTS.md for the detail.
objective (text)
|
v
local model ------------------> JSON experiment profile
| |
| cannot read limits.toml v
| check_shape() structure only
| |
| v
| review() whole profile vs limits
| |
|<---- rejection codes only ----------+
|
accepted profile (YAML)
|
v
deterministic runner (separate, later)
The model writes a document rather than driving instruments, and that choice buys three things:
- Reproducibility. A profile can be diffed, versioned, published and re-run to get the identical sequence. An agent improvising tool calls produces a different run every time, which is not an experiment.
- Review before power. Validation happens once, on a finished artifact, before anything is energised. Walking every step of a complete document is a stronger position than intercepting a stream of requests while the cell is already running.
- It fits a small model. One document has to be right, rather than two hundred sequential decisions.
The schema is deliberately small. Every field the model can write is a field the validator knows how to check. There is no free text command, no raw SCPI passthrough and no escape hatch, because a field nobody validates is a hole.
Pure standard library. Python 3.11 or newer, for tomllib. No dependencies.
cp limits.example.toml limits.toml
# edit for your bench, then lock it down
sudo chown root:root limits.toml
sudo chmod 0400 limits.toml
python3 -m ionsmith models --host YOUR_SERVER
python3 -m ionsmith generate \
--host YOUR_SERVER --model Qwen3-8B-GGUF \
--objective "Measure the area resistance of a cation exchange membrane coupon in 0.5 M NaCl at room temperature" \
--out profile.yaml --audit audit.json
python3 -m ionsmith review profile.yaml # no model involved
generate exits 0 on an accepted profile, 3 when the profile was refused, and
1 when no profile could be produced at all. A refusal is not an error: it is
the system working, and the audit JSON records what was asked and what it was
told.
The check refuses to load a file that is world readable or owned by the account the agent runs as. Be aware of its current limitation, which is documented rather than hidden: mode 0440 owned by another user but in the agent's group passes the check while still being readable by the agent. Genuinely separating reader from validator needs the validator to run as its own user or process, which this codebase does not do yet.
--waive-limit-protection exists for a bench with nothing connected. Do not
make it a habit, and do not use it on a bench that can deliver current. When it
is used, the audit records that it was used.
python3 -m pytest tests -q # 57 tests, no network, no hardware
The suite covers the per-call validator, the whole-profile review, and the generation schema. Several tests exist specifically to assert the properties above rather than merely convenient behaviour, including that a rejection never contains a limit value and that the schema sent to the model carries no bound on any quantity the limits constrain.
Nothing in this codebase talks to an instrument. Execution is a later, separate piece that will only ever receive a profile the review has already accepted.
Measured behaviour of the generation loop against two local models, including success rates, a failure taxonomy, throughput, and captured refusals, is in RESULTS.md.
Apache-2.0. See LICENSE and NOTICE.
This is the software subtree of the wider project. The hardware is published separately under CERN-OHL-S-2.0. The two licences are deliberately distinct.