Skip to content

feat: an operation names the tool it is offered as - #79

Merged
antoinetoussaint-byte merged 2 commits into
mainfrom
feat/an-operation-names-its-tool
Oct 28, 2026
Merged

antoinetoussaint-byte merged 2 commits into
mainfrom
feat/an-operation-names-its-tool

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Wiki shipped the first marked solution artifact yesterday (obin-ai/core-solutions#457) and published this as a tool name:

Wiki_Get_V1_Wiki_Upload-configuration

That is the operationId, which this runtime builds as OperationPrefix_Verb_Path. It identifies the route and describes nothing — and since an Operation could not name itself, every solution's tools would have read that way.

The change

Operation.Tool is the model-facing name:

Operation{Path: "/v1/wiki/upload-configuration", Summary: "Read the installed upload destination",
    Tool: "read_upload_configuration", Policy: reads, Handler: answer}

It is the one part of the marker the owner says rather than core derives, because it is the one part a model reads. Everything else in x-codefly-operation stays core's own derivation from the declared policy.

Empty keeps today's behaviour exactly — the name falls back to the operationId — so no committed artifact changes until its owner names something.

Validated, not sanitised

Operation.validate refuses a name that is not snake_case within 64 characters. That is deliberate: core's toolName is a sanitiser for method names and admits capitals and hyphens by design (runnable/derive.go:257 keeps - and case), so Read-Board or ReadBoard would reach a model as typed. This is the last place that can still say no, and an owner naming its own tool is told what a name is rather than having one silently rewritten.

Refused: Read_Board, read-board, 9read, read board, read_board_, read__board, anything over 64 characters. The rule is ^[a-z][a-z0-9]*(_[a-z0-9]+)*$.

The operationId does not move

A nicer operationId would have fixed the tool name and the derived binding key in one go, and I deliberately did not do it: generated clients are named from the operationId, so improving one tool's name would rename every caller's method. The identity stays; only the model-facing word is the owner's to choose. TestAnOperationNamesTheToolItIsOfferedAs pins that naming a tool leaves Releases_Get_V1_Releases_Recorded untouched.

Validation

  • go build ./..., go vet ./... — clean. gofmt clean.
  • go test ./... -count=1 — ok, 16.7s, the whole suite.
  • Two new tests: one operation in the marker fixture names its tool and the other does not, so both the name and the fallback are asserted against the same rendered artifact, along with the operationId staying put; and the spelling rule is driven through Operation.validate over six rejected forms plus the empty one.

Note for reviewers: the test decodes the marker's tool as a plain document rather than through runnablev0.ToolExposure, because the proto enum does not round-trip encoding/json — the first version of the test failed on exactly that, which is worth knowing before writing another assertion over a marker.

Follow-up, not in this PR

Wiki's two reads still carry the fallback names until core-solutions sets Tool on them; that is a one-line change per operation there, once this is released.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 28, 2026 09:49
A marked read was offered to a model under its operationId, which this runtime
builds as OperationPrefix_Verb_Path. Wiki's first marked artifact
(obin-ai/core-solutions#457) therefore published
`Wiki_Get_V1_Wiki_Upload-configuration` as a tool name: it identifies the route
and describes nothing, and every solution's tools would have read that way.

Operation.Tool is the name instead. It is the one part of the marker the owner
says rather than core derives, because it is the one part a model reads. Empty
keeps today's behaviour exactly — the name falls back to the operationId — so
no existing artifact changes until its owner names something.

The name is validated, not sanitised: snake_case within 64 characters, refused
at Operation.validate. Core's own toolName keeps whatever it is handed (it
admits capitals and hyphens by design, being a sanitiser for method names), so
a name typed with either would reach a model as typed, and this is the last
place that can still say no.

The operationId does not move. Generated clients are named from it, so making
one tool read better would rename every caller's method.

README and CHANGELOG carry the field, the fallback and why the operationId
stays put.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review of the field found two ways a declaration could be accepted and
then mean nothing — both the kind of quiet wrong this marker exists to avoid.

**Two operations could claim one tool name.** Verified by rendering one: both
routes published `tool.name: "same_name"` and nothing refused. A tool name is
the identity a MODEL addresses, so two of them cannot share one — and nothing
downstream collapses them, because a derived package is keyed by the
operationId and Robin's catalogue dedupes on the operation, the contract digest
and the policy, never on the word. Refused now where a duplicate operationId
already was, naming the tool and both routes.

**A name on what is never marked was silently dropped.** `Tool` on a mutation
validated fine, the marker came back nil, and the name went nowhere — while
reading, to whoever wrote it, as an exposure. Only a read is marked, so only a
read may be named: a mutation, a stream, an excluded route and an
any-checked-caller route are each refused by name, and each says why it is
never a tool. The same operation without a name stays valid, which the test
pins: this refuses the name, not the operation.

Also verified rather than assumed: core's own maxToolName is 64, the same bound
this validates, and BoundedName returns a name of exactly 64 unchanged — so a
name that passes here is the name a model sees, which was the whole point of
validating instead of sanitising.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 72c04dd into main Oct 28, 2026
1 check 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