Wymcp.Tool.Actions (Wymcp v0.4.0)

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 three 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 two validators.

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 three 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 is a different layer and stays in Wymcp.Tool, under the callback-surface invariant there.

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 rejectedvalidate_known_keys!/3 subtracts the vocabulary from an action schema's keys and raises on whatever is left, so a misspelling fails at one of the moments in Wymcp.Tool's "When schemas are validated" instead of vanishing silently from help and tools/list.
  • known implies validatedWymcp.ActionSchemaInvariantTest derives one cell per key from Wymcp.Tool.action_schema_keys/0 and 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/0 or compared against it by a cell in that same test: Wymcp.Tool.action_schema/0's key set, its mandatory/optional split against Wymcp.Tool.mandatory_action_schema_keys/0, and Wymcp.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 contract Wymcp.Tool publishes and the chain this module runs can disagree while the build stays green.

The invariant is about coverage, not depth: a validator satisfies it by rejecting a wrong type. What a well-typed value may contain is that key's own contract — the framework validates no property values at all.

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!/3 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. 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 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 mount tool's definition is built at the registration moment, so there the malformation aborts the mount module's compile instead (Wymcp.Router), and a tool registered on a live session passed this same validation at registration (Wymcp.Session.register_tool/2), so the serve-time fallback that assembles its entry never sees an invalid tool — its raise is defence in depth, not a live surface. Help's action level is the one-schema surface, and is consistent with dispatch: both hold the actions map as name material and obtain only the schema they render. Each entry point obtains once and passes the map it obtained to whatever it calls — a path that obtained twice could observe two different maps. 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

Obtain module's actions map as name and membership material, checking the container's own shape and nothing else. Raises ArgumentError when Wymcp.Tool.actions/0 does not return a map.

Obtain one action's schema out of an actions map already obtained through fetch!/1, checking its mandatory keys. Raises ArgumentError naming the tool, the action and the key when one is missing or 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.

Functions

fetch!(module)

Obtain module's actions map as name and membership material, checking the container's own shape and nothing else. Raises ArgumentError when Wymcp.Tool.actions/0 does not return a map.

No schema field may be read off what this returns — it is deliberately not an obtaining accessor. The two callers that hold an actions map as names — Wymcp.Tool.dispatch/3 and help's action level — cannot go through fetch_schemas!/1 without reinstating the sibling contamination fetch_schema!/3 exists to prevent, so they take the container here and obtain the one schema they render there.

fetch_schema!(module, actions, action)

Obtain one action's schema out of an actions map already obtained through fetch!/1, checking its mandatory keys. Raises ArgumentError naming the tool, the action and the key when one is missing or malformed.

Takes the map rather than obtaining its own, so a caller that already holds one as name material — Wymcp.Tool.dispatch/3, help's action level — reads the schema it dispatches out of the very map it matched the name in, and a tool whose Wymcp.Tool.actions/0 varies between calls cannot answer the two questions from two different maps.

fetch_schemas!(module)

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!/3 instead, which leaves a malformed sibling to fail at its own call rather than at this one.

validate!(module)

Validate every action schema in module. Raises ArgumentError with a descriptive message on the first malformed action.

Called at all three of the moments a tool's schemas are checked: by __after_verify__/1 while a use Wymcp.Tool module itself compiles, by Wymcp.Router.init/1 while a mount module compiles, and by Wymcp.Session.register_tool/2 at runtime registration — so a misconfigured tool fails as early as its own build, and no later than the point it is wired in.