Skip to content

Releases: benchwire/labwire

Labwire 0.5.0

Choose a tag to compare

@Silousr Silousr released this 29 Jul 13:42

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-mcp now requires mcp>=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, -32602 for 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 returns input_required with an
    elicitation showing the exact command and parameters; the host
    surfaces it to a human, and approval injects the standing
    confirmation (from LABWIRE_MCP_CONFIRMATION, an environment
    variable, never a CLI flag). S3 elicitations carry the refusal's
    request id and the labwire grant approve command; 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.py runs the round trip with zero
    hardware.
  • Tasks (experimental). For 2026-era clients declaring the
    io.modelcontextprotocol/tasks extension, commands with
    estimated_duration_s of 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
    failed is reserved for JSON-RPC faults, so instrument failures are
    completed with isError inside the result; a cancelled task
    carries no result, so the settlement outcome rides statusMessage
    and the signed bundle stays on the instrument host. tasks/cancel
    is cooperative, which maps cancel_semantics exactly.
  • 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

Choose a tag to compare

@Silousr Silousr released this 29 Jul 12:39

Fixed

  • Dependency upper bounds everywhere. labwire-mcp declared
    mcp>=1.2 with no ceiling, so the moment the MCP Python SDK ships its
    v2 (breaking, announced alongside the 2026-07-28 MCP specification),
    every fresh pip install labwire-mcp would 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 the labwire-* 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

Choose a tag to compare

@Silousr Silousr released this 29 Jul 07:56

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

  • interruptible is 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 while canceling settles succeeded with a
    ran_to_completion block, where 0.3 reported canceled after a
    completed action.

Added

  • Settlement (SPEC 8.3): acknowledgment is not settlement. Every run
    that ends by or during cancellation carries a signed cancellation
    block: never_started, halted (backend-confirmed only),
    halted_at_boundary (only from a boundary checkpoint itself),
    ran_to_completion, or unconfirmed, 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, and labwire verify rejects offline what the
    spec forbids: blockless canceled records, halted claims 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", and transfer, now sequenced by the
    bridge, stops between steps with the boundary named in the record.
    Ophyd declares "abort" only for EpicsMotor-family devices
    (TODO-VERIFY on real EPICS) with a settlement window on the status
    object; ophyd.sim axes are "none" because their stop() is
    literally pass. 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 prebuilt link=. New: an endpoint file
    format (load_endpoints, strict, unknown keys are errors), a
    labwire probe command 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
    import module:factory targets relative to the working directory, which
    broke the READMEs' own quickstart commands from a fresh clone. Targets
    now resolve with the current directory on sys.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

Choose a tag to compare

@Silousr Silousr released this 27 Jul 16:42

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 scoped unit keyword so state is
    as unit-mandatory as commands. Revisions are derived from the canonical
    read result; resource/changed rides the event channel under a reserved
    name. The liquid handler's deck is labwire: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_ref schema keyword, with
    kind matched against a registry (SPEC Appendix A) and enumerated_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's ResourceRef(...) builds annotated
    parameter types, so a bridge writes source: Container with 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 | revoke is the operator tool; the refusal (-32011) says
    mintable_by_agent: false in a typed field. A server declaring S3
    commands with no store refuses to start.
  • Optimistic concurrency (SPEC §10.5): if_revision on submit, refused
    with -32012 before any confirmation or grant is spent; terminal status
    carries resource_revisions, so a single agent never re-reads between
    steps.
  • Gripper moves in labwire-pylabrobot: move_plate, move_lid,
    move_resource at 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 enum uri, 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.resources is REQUIRED of servers ([] allowed).
  • A confirmation no longer satisfies S3. Deployments that raised a
    command to S3 stop working until grants are provisioned; the failure is
    loud (-32011, reason absent), never silent.
  • Submission precedence moves interlock and 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 -32003 where v0.2 returned -32009.
  • The error data requirement extends to -32012 (SPEC §12.2).
  • Manifests are "0.3": command.params records the normalized
    parameters (v0.2 recorded the raw submission, so a command with defaulted
    optionals signed a manifest describing something other than what ran),
    plus params_digest, an authorization block with REQUIRED
    identity_verified: false, and resource_revisions. Verifiers accept
    0.2 and 0.3 bundles both; labwire verify refuses a 0.3 bundle claiming
    identity was verified.
  • The unit and resource_ref schema keywords are claimed: unit
    REQUIRED on numeric nodes in content_schema and forbidden in command
    schemas; resource_ref permitted only in params_schema, never beside a
    pattern.
  • labwire-pylabrobot: describe_deck is deleted (the deck is a
    resource); the "plate/A1" address grammar is deleted (references are
    labwire:deck/... URIs); the annotation file keys resources: by URI
    and loses its per-resource safety_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 supplies resources: [].
  • 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 with labwire grant.
  • Clients: read resources from the descriptor; follow enumerated_by
    from any resource_ref you cannot fill; treat -32010/-32011/-32012
    per their details, which carry the recovery paths.

Fixed

  • Gripper move results in the PyLabRobot bridge reported the doubled origin
    labwire:deck/deck for 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 in tool_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