Releases: benchwire/labwire
Release list
Labwire 0.5.0
Protocol version stays "0.4"; nothing on the Labwire wire changed.
This release is the MCP boundary: the adapter moves to the MCP
2026-07-28 revision and the Python SDK v2, published one day earlier.
Breaking
labwire-mcpnow requiresmcp>=2,<3(was>=1.2,<2). Hosts pinned
to the v1 SDK should stay on labwire-mcp 0.4.1.
Added
- Dual-era MCP service. One adapter process serves 2026-07-28
clients (per-request metadata,server/discover, required cache
hints,-32602for unknown tools) and handshake-era clients
(initialize) with no configuration. Every adapter test runs in both
eras against the same server object. - Human-in-the-loop confirmation. On the 2026 era, an S2 command
called without a confirmation returnsinput_requiredwith an
elicitation showing the exact command and parameters; the host
surfaces it to a human, and approval injects the standing
confirmation (fromLABWIRE_MCP_CONFIRMATION, an environment
variable, never a CLI flag). S3 elicitations carry the refusal's
request id and thelabwire grant approvecommand; the human types
the minted grant id. Neither path identifies WHO approved
(identity_verified: false, as before); the docs say so plainly.
examples/mcp_confirmation.pyruns the round trip with zero
hardware. - Tasks (experimental). For 2026-era clients declaring the
io.modelcontextprotocol/tasksextension, commands with
estimated_duration_sof 10 s or more return pollable tasks. The
extension is hand-implemented from its spec text (the SDK excludes
it). Status mapping is honest about the two impedance points: task
failedis reserved for JSON-RPC faults, so instrument failures are
completedwithisErrorinside the result; acancelledtask
carries no result, so the settlement outcome ridesstatusMessage
and the signed bundle stays on the instrument host.tasks/cancel
is cooperative, which mapscancel_semanticsexactly. - SPEC-FINDINGS F11: settlement is structured inside Labwire and
prose at a foreign protocol boundary; the cooperative cancel design
of MCP tasks independently validates F10's resolution.
Changed
- All packages released as 0.5.0 in lockstep;
labwire-*
cross-dependency bounds move to>=0.5,<0.6.
Full changelog: https://github.com/benchwire/labwire/blob/main/CHANGELOG.md
Labwire 0.4.1
Fixed
- Dependency upper bounds everywhere.
labwire-mcpdeclared
mcp>=1.2with no ceiling, so the moment the MCP Python SDK ships its
v2 (breaking, announced alongside the 2026-07-28 MCP specification),
every freshpip install labwire-mcpwould pull it and die. Now
mcp>=1.2,<2. The audit applied the same discipline across every
published package: majors bounded for pydantic, websockets, pynacl,
pyyaml, typer, jsonschema, the serial extras, and the bridge extras,
and thelabwire-*cross-dependencies are bounded to the current
minor (>=0.4.1,<0.5) so a future breaking labwire-core can never be
pulled under an older adapter. No code changes.
Full changelog: https://github.com/benchwire/labwire/blob/main/CHANGELOG.md
Labwire 0.4.0
Protocol version "0.4": cancellation made honest. Driven by field
reports from an Opentrons Flex owner (vcjdeboer on the PyLabRobot forum)
and the PyLabRobot maintainer: a stop request returning does not mean
motion stopped, and a Hamilton STAR command is on the wire before any
cancel can matter. SPEC-FINDINGS F10 documents both reports and the
resolution; F8 is superseded by it.
Breaking
interruptibleis removed from command capabilities; its
cancellable-by-default abandon-the-handler semantics are the behavior
the field reports indicted.cancel_semantics("abort",
"between_steps","none") replaces it, and undeclared commands
default to"none": not cancellable mid-run.- Cancelling a running
"none"command is refused with-32007, never
accepted-and-ignored. Completion now wins the race: a run that
finishes whilecancelingsettlessucceededwith a
ran_to_completionblock, where 0.3 reportedcanceledafter a
completed action.
Added
- Settlement (SPEC 8.3): acknowledgment is not settlement. Every run
that ends by or during cancellation carries a signedcancellation
block:never_started,halted(backend-confirmed only),
halted_at_boundary(only from a boundary checkpoint itself),
ran_to_completion, orunconfirmed, the honest first-class case.
Handler API:ctx.boundary()for between-steps commands,
ctx.confirm_halted()for aborts. 0.4 manifests record each command's
declared semantics, andlabwire verifyrejects offline what the
spec forbids: blocklesscanceledrecords,haltedclaims from
non-abort commands, boundary claims from commands with no boundaries.
A 24-agent adversarial review closed nine settlement holes (blockless
shutdown records, pre-start cancels claiming halts, boundary claims
from mid-step abandonment, and others) before release. - Bridge truth: PyLabRobot's abandon-the-await cancel is gone; every
atomic call declares"none", andtransfer, now sequenced by the
bridge, stops between steps with the boundary named in the record.
Ophyd declares"abort"only forEpicsMotor-family devices
(TODO-VERIFY on real EPICS) with a settlement window on the status
object;ophyd.simaxes are"none"because theirstop()is
literallypass. The syringe-pump driver earns"abort": STP then a
confirmed IDLE before any halt is claimed. MCP tool descriptions state
each tool's cancel semantics; the demos show a refused cancel, an
earned halt, and a boundary settlement.
Added in the adoption-engineering cycle
-
labwire-conformance(SPEC §15.3): an executable conformance suite
that points at ANY server over WebSocket and checks the spec's normative
requirements: handshake and version negotiation, descriptor validity and
mandatory units, S2/S3 refusal semantics, resource reads and reference
validation, error taxonomy, and signed-bundle verification including
tamper detection. Binary pass/fail per check with spec references; the
verdict is the §15.1 level actually earned. Command-executing checks are
opt-in; everything else stops at refusals servers must issue before
running anything. CI runs it against the reference server. -
Property-based wire fuzzing (hypothesis, deterministic in CI):
arbitrary JSON-RPC envelopes and submit params never kill a session and
always draw a taxonomy-tagged answer; pathologically deep payloads
survive; any content-changing byte flip or truncation of a signed bundle
fails verification (formatting-only flips legitimately still verify,
since signatures bind RFC 8785 canonical content, not raw bytes), and
unparseable or non-UTF-8 manifests get a verdict, not a traceback.
Honest result: the fuzz found no new breaks; the one wire gap of the
day (below) was found by the conformance suite first. -
Hardware-ready transports in
labwire-drivers: the line-protocol
link is now pluggable, TCP (as before) or USB-serial via the
labwire-drivers[serial]extra (pyserial-asyncio-fast, BSD-3-Clause,
the maintained successor of pyserial-asyncio; optional, never
vendored). Drivers accept a prebuiltlink=. New: an endpoint file
format (load_endpoints, strict, unknown keys are errors), a
labwire probecommand that asks a SCPI endpoint*IDN?and drafts
its annotation file with TODOs for everything a probe cannot know, and
docs/HARDWARE.md, the walkthrough for the day real equipment arrives.
Status stated everywhere it matters: real transports, tested against
simulators (TCP against the sims, serial against a PTY responder),
awaiting hardware; no vendor compatibility is claimed.
Fixed
- The WebSocket transport silently dropped unparseable frames instead of
answering-32700(and non-object JSON instead of-32600) as SPEC §12
requires. Found by running the new conformance suite against the
reference server on its first day. - Both bridge CLIs (
labwire-pylabrobot check,labwire-ophyd) failed to
importmodule:factorytargets relative to the working directory, which
broke the READMEs' own quickstart commands from a fresh clone. Targets
now resolve with the current directory onsys.path,python -m
style. Caught by the stranger test; covered by a regression test.
Full changelog: https://github.com/benchwire/labwire/blob/main/CHANGELOG.md
Labwire 0.3.0
Protocol version "0.3": things, not only quantities. Driven by findings
F1, F2, and F4 in SPEC-FINDINGS.md, each now resolved
there with its residual stated.
Added
- Resources (SPEC §7.6, §10): URI-identified, typed, readable instrument
state, declared in the descriptor beside commands and read with
resource/read. Content schemas carry a scopedunitkeyword so state is
as unit-mandatory as commands. Revisions are derived from the canonical
read result;resource/changedrides the event channel under a reserved
name. The liquid handler's deck islabwire:deck; the syringe pump gains
labwire:syringe, a consumable resource on an instrument with no
references at all, because the primitive is not deck-shaped. - Typed references (SPEC §7.2): the
resource_refschema keyword, with
kindmatched against a registry (SPEC Appendix A) andenumerated_by
naming the resource whose index lists valid values. Closure is checked
before a descriptor is served; values resolve against a fresh read at
submission; the refusal (-32010) carries an RFC 6901 pointer, the
expected kind, the longest resolving prefix,did_you_mean, and a
ready-to-send read request. The SDK'sResourceRef(...)builds annotated
parameter types, so a bridge writessource: Containerwith no regex. - Operator grants for S3 (SPEC §8.6): provisioned out of band in a store
the protocol has no method to write, bound to a command name and the RFC
8785 digest of its normalized parameters (a binding adopted from LAP with
credit), expiring and use-limited, consumed atomically. A refused S3
submission records a pending request;labwire grant list | approve | revokeis the operator tool; the refusal (-32011) says
mintable_by_agent: falsein a typed field. A server declaring S3
commands with no store refuses to start. - Optimistic concurrency (SPEC §10.5):
if_revisionon submit, refused
with-32012before any confirmation or grant is spent; terminal status
carriesresource_revisions, so a single agent never re-reads between
steps. - Gripper moves in
labwire-pylabrobot:move_plate,move_lid,
move_resourceat S3, non-interruptible, with resource-typed parameters.
The demos show the ceremony beat by beat, ending with a valid grant
refused on different parameters. Exercised against the chatterbox backend
only, never against physical hardware. - The MCP adapter maps resources onto MCP resources, synthesizes a
model-callable read tool with an enumuri, distinguishes S2 confirmation
from S3 authorization in schemas and descriptions, and serializes error
details instead of flattening them.
Breaking
- Protocol version is
"0.3"; a v0.2 client and a v0.3 server do not
interoperate. InstrumentDescriptor.resourcesis REQUIRED of servers ([]allowed).- A
confirmationno longer satisfiesS3. Deployments that raised a
command to S3 stop working until grants are provisioned; the failure is
loud (-32011, reasonabsent), never silent. - Submission precedence moves
interlockand capacity ahead of
confirmation and authorization: everything knowable without an operator
is checked first (SPEC §12.1). A submit against a tripped interlock now
returns-32003where v0.2 returned-32009. - The error
datarequirement extends to-32012(SPEC §12.2). - Manifests are
"0.3":command.paramsrecords the normalized
parameters (v0.2 recorded the raw submission, so a command with defaulted
optionals signed a manifest describing something other than what ran),
plusparams_digest, anauthorizationblock with REQUIRED
identity_verified: false, andresource_revisions. Verifiers accept
0.2 and 0.3 bundles both;labwire verifyrefuses a 0.3 bundle claiming
identity was verified. - The
unitandresource_refschema keywords are claimed:unit
REQUIRED on numeric nodes incontent_schemaand forbidden in command
schemas;resource_refpermitted only inparams_schema, never beside a
pattern. labwire-pylabrobot:describe_deckis deleted (the deck is a
resource); the"plate/A1"address grammar is deleted (references are
labwire:deck/...URIs); the annotation file keysresources:by URI
and loses its per-resourcesafety_class, which was documented three
times as reported-but-not-enforced.
Migration
- Instruments with no tree-shaped state: rebuild against 0.3 and change
nothing; the SDK suppliesresources: []. - Instruments that exposed state through a command result: declare a
resource(...)with a content model, move the command's body into its
@reader, and delete the command. - Deployments using S3: provision a grant store (
grant_store=or
LABWIRE_GRANT_STORE) and approve requests withlabwire grant. - Clients: read
resourcesfrom the descriptor; followenumerated_by
from anyresource_refyou cannot fill; treat-32010/-32011/-32012
per theirdetails, which carry the recovery paths.
Fixed
- Gripper move results in the PyLabRobot bridge reported the doubled origin
labwire:deck/deckfor labware standing directly on the deck, a URI that
does not resolve. The origin is now the deck resource itself. Caught on
the first live end-to-end run of the agent demo, which also fixed the
demo's operator-approval harness: the S3 refusal arrives in a turn that
still ends intool_use, so the pending request id has to be remembered
across turns or the operator never gets asked.
Full changelog: https://github.com/benchwire/labwire/blob/main/CHANGELOG.md