Skip to content

Protocol v0.3: resources, typed references, operator grants - #5

Merged
Silousr merged 5 commits into
mainfrom
v0.3
Jul 27, 2026
Merged

Protocol v0.3: resources, typed references, operator grants#5
Silousr merged 5 commits into
mainfrom
v0.3

Conversation

@Silousr

@Silousr Silousr commented Jul 27, 2026

Copy link
Copy Markdown
Member

Draft while milestones V1 to V5 land; merges only when complete and green.

  • V1: spec + message models (this PR's first commit)
  • V2: core SDKs (resource serving, reference validation, S3 grants)
  • V3: migration (deck becomes a resource, addresses become typed references)
  • V4: gripper as S3, demos, MCP adapter
  • V5: docs, stranger tests, merge

Design: approved plan in the maintainer's plan file; findings F1, F2, F4
in SPEC-FINDINGS.md are the drivers.

🤖 Generated with Claude Code

Silousr and others added 5 commits July 27, 2026 04:42
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
Silousr marked this pull request as ready for review July 27, 2026 12:42
@Silousr
Silousr merged commit 6cc8d21 into main Jul 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant