Conversation
The spec for the three things v0.2 could not express, from the approved design. Sections 10 through 17 renumbered to 11 through 18 to make room for a real section 10, with every cross-reference in the repository bumped. Resources (new section 10, declaration in 7.6): URI-identified, typed, readable instrument state, declared in the descriptor beside commands so discovery is not a step an agent can skip. One read method. Child URIs compose by one protocol-defined rule from the read result's index, which is what keeps addressing out of per-instrument convention. Revisions are opaque and change with content; notification reuses the event channel under a reserved name. Content schemas carry a scoped unit keyword so a units-mandatory protocol does not ship a units-optional state format. Typed references (7.2): a resource_ref keyword on the schema node itself, with kind and enumerated_by both required, pattern forbidden on the same node, closure checked before a descriptor is ever served, and resolution at submission against a fresh read. The error names the pointer, the expected kind, the longest prefix that did resolve, and hands back a ready-to-send read request. Operator grants (8.6): S3 now takes something an agent structurally cannot produce. Grants live in a server-side store the protocol has no method to write, bound to a command name and the RFC 8785 digest of the normalized parameters (a binding adopted from LAP with credit), expiring and use-limited, consumed atomically. A refusal records a pending request so the operator's approval tool reads the parameters from the server's own store, never from a digest relayed through the agent that wants the approval. The manifest records an authorization block with a required identity_verified: false, turning the honesty caveat into a machine-checkable wire fact. Also per the approved decisions: if_revision optimistic concurrency with stale_revision (-32012), checked before authorization so a stale plan never costs an operator approval and never part of the digest so a re-read cannot invalidate a grant; normalized parameters as the single object that is validated, executed, digested, and recorded, closing the latent v0.2 bug where a manifest described something other than what ran; submission precedence reordered so everything knowable without an operator is checked first; and Appendix A, the kind registry, labelled honestly as seeded from one domain. Message models and error classes for every new shape land with the spec because the examples are machine-validated in CI and models are the machine-checkable form of the spec. Server behavior is V2; PROTOCOL_VERSION stays "0.2" until the enforcement that would make "0.3" true exists, and the capability flags advertise resources and grants as false for exactly that reason. Co-Authored-By: Claude <noreply@anthropic.com>
The v0.3 core. Protocol version is now "0.3", and the capability flags advertise what is genuinely true: resources and grants are implemented, so a server may say so. Resources are declared at class scope like channels: resource() takes a pydantic content model whose serialization schema becomes content_schema, validated at declaration with the same import-time discipline as @command, including the rule that numeric content carries unit keywords. A reader returns index and content in one snapshot; the revision is derived from the canonicalized read result, so a driver cannot forget to bump it, and touch() emits the reserved resource/changed event only when something actually moved. resource/read serves it; an unknown URI is refused naming what exists. Typed references resolve at submission against a fresh read, walking the schema and the instance together so the refusal carries an RFC 6901 pointer, the expected kind, the longest resolving prefix, did_you_mean candidates filtered by kind, and a ready-to-send read request. Closure is a descriptor-level validator, so a server cannot serve a dangling reference and a client refuses one on receipt. The submit chain now runs unsupported, validation, unknown_reference, stale_revision, interlock, busy, then confirmation or authorization: everything knowable without an operator first, so an agent is never asked to confirm, and a grant is never spent, on a call that could not run. One normalized parameter object is validated, executed, digested, and recorded, closing the v0.2 latent bug where a manifest described something other than what ran. S3 takes an operator grant. The store is three files with separated writers: grants.json only the operator tool writes, pending.jsonl and uses.json only the server writes, so use counts persist across restarts without the server ever touching the operator's file. Verification is synchronous with no awaits between check and consume, and a test proves eight concurrent submits spend a single-use grant exactly once. A refusal records a pending request, so labwire grant list shows the operator the real parameters from the server's own record, never a digest relayed through the agent. labwire grant approve mints the grant; revoke revokes it. A server declaring an S3 command with no store configured refuses to start. Manifests are 0.3: params_digest on every run, an authorization block whose identity_verified is required false, resource revision windows, and a verifier that accepts 0.2 bundles unchanged and refuses a 0.3 bundle claiming identity was verified, because that claim would be a lie in this version. if_revision ships per the approved decision: checked before authorization so a stale plan never costs an approval, never part of the digest so a re-read cannot invalidate a grant, and the terminal status returns the new revisions so a single agent never re-reads between steps. Co-Authored-By: Claude <noreply@anthropic.com>
…erences The V3 migration, all of it intended. The PyLabRobot bridge's invented address grammar is gone. addressing.py is now a bijection between PyLabRobot's tree and the protocol's URI space: labwire:deck/<labware>/<item>, composed by the one protocol- defined rule from the read result's index. Its discipline survived the rewrite: one spelling per thing, derived internal names refused with the canonical URI, and every failure naming what would have worked. describe_deck is deleted, not deprecated. The deck is the labwire:deck resource, declared at class scope with DeckState as its content model, read with resource/read, its revision derived so it cannot go stale silently, touched after every operation so resource/changed fires only when something moved. The command surface is nine operations; the deck is not one of them. Parameters that name things are typed references now. Container and TipSite are ResourceRef aliases, so the resource_ref keyword and its generated read-this-resource sentence ride inside params_schema with no pattern anywhere, and the server validates each value against a fresh read before a handler runs. The tests that asserted the old pattern now assert its absence. The annotation file rekeys resources by URI and loses its per-resource safety_class, which was documented in three places as reported-but-not- enforced; keeping a field that still cannot raise a call's class would be keeping a lie. locked stays and is enforced; hazard stays and now surfaces in the deck resource content where an agent actually reads it. The syringe pump gains labwire:syringe, a consumable resource on an instrument with no references at all, deliberately: the primitive is not deck-shaped, and this exercises content typing, derived revisions, and change notification without a single resource_ref. The balance and PSU declare nothing and lose nothing. The ophyd bridge declares resources: [] and its README says why in one paragraph: a signal-shaped instrument has no tree with nowhere to live, and inventing a resource would be surface for its own sake. Both dilution demos run on v0.3 end to end: URIs on the wire, the deck read as a resource with its revision printed, signed bundles verifying. Co-Authored-By: Claude <noreply@anthropic.com>
…ants The condition LIMITATIONS documented is met: real S3 exists, so the gripper ships. move_plate, move_lid, and move_resource are exposed at S3, non-interruptible because a plate held mid-traverse has no safe interruption, with resource-typed parameters (a plate is labware, a destination is a site, and passing a well where a site is wanted is a kind mismatch the reference walk catches before authorization). Verified against the chatterbox backend: the plate's parent really becomes the staging site. The rig gains a five-site staging carrier, indexed as kind site under PyLabRobot's own holder names. The demo shows the ceremony beat by beat, and CI asserts the order: the standing S2 confirmation that moved 800 uL of liquid this session is refused for one plate move; the refusal records a pending request; the operator (played by the harness on the same machine, with the one-user-one-machine caveat printed) lists and approves it; the granted move runs; and then the same valid, unexpired grant is refused on different parameters, the one beat that proves the binding is to parameters rather than an S3-shaped password. The manifest records mode grant, use 1/1, identity_verified false, and CI asserts no bundle anywhere contains a grant id. In the live agent path the harness plays operator only after the agent itself reports the refusal. The agent prompt is now hint-free and CI enforces it: no labwire:, no deck, no describe, no resource, no read, no URI-spelled labware name. Discovery has to come from the descriptor, and hygiene tests keep the preconditions true: no reference parameter carries a pattern, every reference points at a declared resource, and the descriptor leaks no user-styled labware name and spells no path. Writing those tests caught two real leaks: a docstring example carrying the old address grammar into returns_schema, and the interim prompt hint from V3. The MCP adapter maps resources to MCP resources under a namespaced URI used only where MCP requires global uniqueness, keeps wire spellings everywhere the model acts, synthesizes a read tool whose uri parameter is an enum, branches S2 confirmation from S3 authorization and never emits both, marks the upper classes with ToolAnnotations while leaving readOnlyHint unset because Labwire cannot yet tell a read from a state edit, and serializes error details instead of flattening them, because request_id, did_you_mean, and the ready-to-send read are the recovery paths the protocol designed. Co-Authored-By: Claude <noreply@anthropic.com>
SPEC-FINDINGS marks F1, F2, and F4 resolved in place, each with what was actually built, what was rejected on the way (the sidecar map F1 itself recommended lost to the in-schema keyword; ed25519 minting lost to a plain store), and its residual stated plainly: the kind registry is governed by nobody yet, there is no pagination and no reservation, a grant id is a bearer value and identity is still not proven. Findings are history, so nothing was deleted. CHANGELOG 0.3.0.dev0 with the full breaking list and migration notes per audience. ROADMAP's modeling-things section becomes shipped-with- residuals and keeps F3, F6, F7 explicitly out. PRIOR_ART credits what v0.3 borrows: WoT Thing Description's semantics-in-the-schema and the unit term, JSON-LD's intuition without its machinery, MCP's resource primitive reduced, and LAP's parameter-digest binding, with LAP's JWS tokens still named as the more complete design. Package versions to 0.3.0.dev0. The bridge README teaches URIs and the grant ceremony, and its LIMITATIONS draws the line that matters: command-level S3 is enforced now, resource-level hazard still is not, because per-argument classes are finding F3 and out of scope. Co-Authored-By: Claude <noreply@anthropic.com>
Silousr
marked this pull request as ready for review
July 27, 2026 12:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft while milestones V1 to V5 land; merges only when complete and green.
Design: approved plan in the maintainer's plan file; findings F1, F2, F4
in SPEC-FINDINGS.md are the drivers.
🤖 Generated with Claude Code