Wymcp. Tool. Actions
(Wymcp v0.8.7)
View Source
Answers every question about the actions a tool module declares: validating
its Wymcp.Tool.actions/0 declaration wherever that declaration is
checked, and obtaining action schemas out of it wherever one is read.
Both halves are one validation layer. Action-schema validation runs the
same chain at each of its two moments, and runs once more — restricted to
the mandatory pair — at every runtime obtaining moment, which is why
validating and obtaining live together rather than in two modules: one
definition of carrying a key has to serve both sides, and here it does,
because check_mandatory_keys!/3 calls the chain's own validators: the
schema value's shape, then the mandatory pair.
What a consumer may write into Wymcp.Tool.actions/0 is not stated here.
The format catalogue belongs with the behaviour a consumer implements, in
Wymcp.Tool's moduledoc under "Action schema format", and the two moments
this chain runs at are listed there under "When schemas are validated". This
module states the rule those moments enforce and holds the code enforcing
it.
The vocabulary itself also stays with the behaviour: Wymcp.Tool is the one
code home for the key list, read through Wymcp.Tool.action_schema_keys/0,
and the chain here reads it through that accessor at each use rather than
snapshotting it into an attribute of its own — one owner, read at run time,
so the two modules carry no compile-time dependency in this direction.
The callback-surface check and the callback-shape check are different
layers and stay in Wymcp.Tool, under their invariants there; validate!/1
is the callback-shape check's first part.
The action-schema invariant
Every key in the action-schema vocabulary is validated: a key outside
Wymcp.Tool.action_schema_keys/0 is rejected wherever validate!/1 runs
(unknown implies rejected), and every key inside it is checked by a
validator in that chain which rejects at least a wrong-type value (known
implies validated). That rule is the action-schema invariant.
Three clauses, each with its own enforcement:
- unknown implies rejected —
validate_known_keys!/3subtracts the vocabulary from an action schema's keys and raises on whatever is left, so a misspelling fails at one of the moments inWymcp.Tool's "When schemas are validated" instead of vanishing silently from help andtools/list. - known implies validated —
Wymcp.ActionSchemaInvariantTestderives one cell per key fromWymcp.Tool.action_schema_keys/0and asserts each one rejects a wrong-type value, so a key joining the vocabulary without a validator fails that test rather than shipping unchecked. - restated implies pinned — every other statement of the vocabulary is
derived from
Wymcp.Tool.action_schema_keys/0or compared against it by a cell in that same test:Wymcp.Tool.action_schema/0's key set, its mandatory/optional split againstWymcp.Tool.mandatory_action_schema_keys/0, andWymcp.Tool's "Action schema format" catalogue, which must carry a bullet for every key. The published type has no other enforcement — nothing writes@spec— so without that cell the contractWymcp.Toolpublishes and the chain this module runs can disagree while the build stays green.
The invariant is about coverage: a validator satisfies it by rejecting a
wrong type. How deep validation reaches is a separate rule — wymcp
validates exactly what it interprets. Below an action schema's own keys,
the keywords Wymcp.Tool.KeySet walks — "properties", an "items" that
is a single schema, and an "additionalProperties" schema — are the
validated surface, and no other property value is checked: what else a
well-typed value may contain is that key's own contract. Key type is part of
the type: :properties is a map from
property name to property schema, so a
non-string key is a wrong type there, as a non-binary entry is in
:required, and every map the walk visits below it is held to the shape
the closure is read from, because that shape decides the closure dispatch
enforces and help publishes. What a nested schema may hold is catalogued in
Wymcp.Tool's "Action schema format", and the offences the walk reports
are listed at Wymcp.Tool.KeySet.schema_offences/1. :defaults is checked against that closure as the unknown-key
gate checks a call's data: every key in it, and every key inside a
default wherever the walk descends, is a string the object it sits in
declares (Wymcp.Tool.KeySet.default_offences/2), so no default merged
into data carries a key the gate would refuse from a caller.
A schema that breaks either rule raises one ArgumentError naming every
offence — the first ten, sorted by location, then a count of the rest —
one per line, each with its fix; shape offences come first, and :defaults
is checked only once there are none, because that check reads a
well-formed schema. A location is the map holding the offence, spelled in
Elixir access syntax from the action-schema key with every segment
inspected — :properties["o"]["items"], or :defaults["opts"] for a key
inside a default — so it names the map literal the author edits. A
data path names a place in data instead,
and has no spelling for a schema under "items", which no array index
reaches.
A read-side corollary follows: an action schema obtained for reading
carries its mandatory keys, and that is checked where it is obtained — not
where a field is read. fetch_schema/2 and fetch_schemas!/1 are the
only paths by which a reader obtains an action schema, and each checks the
mandatory keys — the ones Wymcp.Tool.action_schema/0 writes bare — by
calling the same validators the wire-in chain runs, so one definition of
carrying a key serves both sides: carrying :properties means a map whose
shape passes at every depth the closure walk reads. :defaults is not
mandatory, and is checked at the registration moments alone. A reader
holding a schema may therefore access those keys directly; a reader
meeting a schema without them is looking at a tool no validator ever saw,
and obtaining raises rather than let it publish an entry whose silence
about :properties would read as a claim that the action takes none.
Optional keys carry no such guarantee and are read with their default.
The corollary holds by construction, not by review. This module hands out
two things only — a tool's action names, through action_names/1, and
checked schemas — so no caller holds a schema the check has not seen. The
one way left around the check is a module in lib/ calling
Wymcp.Tool.actions/0 itself, and Wymcp.ObtainingMomentTest closes it:
every such call sits in this module.
The check covers exactly what was obtained: the one-schema form checks one,
the all-schemas form checks all. That scope is the design, and over the wire
it reads as malformed-sibling isolation: a tool with one bad action
schema still serves its healthy actions through tools/call, which obtains
only the schema of the action it dispatches. The malformation surfaces on
every surface that renders a whole tool — both of Wymcp.Help's whole-tool
answers, its server index and its tool level, each of which obtains every
schema a tool declares — and at the bad action's own call, which raises and
is answered in the tool dialect. tools/list is on neither list: a
tool's definition is built at the registration moment, so there the
malformation aborts the mount module's compile instead (Wymcp.Router).
Help's action level is the one-schema surface, and is
consistent with dispatch: both resolve the action through fetch_schema/2,
which checks only the schema it resolves.
Each entry point obtains once: fetch_schema/2 resolves the name and reads
the schema out of one call to Wymcp.Tool.actions/0, and a path that
needs only the names takes them from action_names/1 and obtains no schema.
A Wymcp.Tool.actions/0 whose result varies between calls violates the
contract Wymcp.Router states, so obtaining once is defence in depth for
the runtime readers rather than support for such a tool.
Summary
Functions
The action names module declares, as the wire spells them, sorted. Raises
ArgumentError naming the tool when Wymcp.Tool.actions/0 does not
return a map, or returns a struct.
Obtain the schema of the action named action_str on the wire, checking
its mandatory keys. Returns {:ok, action, schema}, or
{:error, {:unknown_action, names}} carrying the tool's action names, as
action_names/1 spells them, when it declares no such action. Raises
ArgumentError naming the tool, the action and the key when the resolved
schema is missing one or carries it malformed.
Obtain every action schema module declares, checking each one's mandatory
keys as it is obtained. Raises ArgumentError naming the tool, the action
and the key on the first schema that is missing one or carries it malformed.
Validate every action schema in module. Raises ArgumentError with a
descriptive message on the first malformed action — and first of all when
Wymcp.Tool.actions/0, or a schema in it, is not a map or is a struct.
Functions
The action names module declares, as the wire spells them, sorted. Raises
ArgumentError naming the tool when Wymcp.Tool.actions/0 does not
return a map, or returns a struct.
Obtains no schema, so a malformed action schema never fails it: the missing-action answer lists a tool's actions without vouching for any of them.
Obtain the schema of the action named action_str on the wire, checking
its mandatory keys. Returns {:ok, action, schema}, or
{:error, {:unknown_action, names}} carrying the tool's action names, as
action_names/1 spells them, when it declares no such action. Raises
ArgumentError naming the tool, the action and the key when the resolved
schema is missing one or carries it malformed.
Only the resolved schema is checked, so a malformed sibling fails its own
call and never this one. The name is resolved and the schema read out of
one obtained map, so a tool whose Wymcp.Tool.actions/0 varies between
calls cannot answer the two questions from two different maps.
Obtain every action schema module declares, checking each one's mandatory
keys as it is obtained. Raises ArgumentError naming the tool, the action
and the key on the first schema that is missing one or carries it malformed.
The whole-tool form, for a caller that renders or publishes every action a
tool declares. A caller that needs one action's schema uses
fetch_schema/2 instead, which leaves a malformed sibling to fail at its
own call rather than at this one.
Validate every action schema in module. Raises ArgumentError with a
descriptive message on the first malformed action — and first of all when
Wymcp.Tool.actions/0, or a schema in it, is not a map or is a struct.
The first part of Wymcp.Tool.validate_callback_shape!/1, and run by it at
both moments a tool's schemas are checked: while a use Wymcp.Tool module
itself compiles, and in Wymcp.Router.init/1 while a mount module compiles
— so a misconfigured tool fails as early as its own build, and no later
than the point it is wired in.