PhoenixKit.API.Tool (phoenix_kit v2.64.1)

Copy Markdown View Source

One operation a module offers through the API (REST today, MCP later).

A module returns a list of tools from the optional PhoenixKit.Module.api_tools/0 callback. Core collects them (PhoenixKit.API.Registry), checks the caller's rights, validates the input against input_schema, calls the handler and shapes the answer.

Fields

  • :name - "<namespace>.<verb>_<noun>", lowercase, stable ("catalogue.set_prices"), at most 100 characters.
  • :audience - :admin (a key with scopes) or :public (no key).
  • :scope - the scope a key must carry, "<module>.<resource>:<action>". nil for public tools and for core's own system.* tools, which every valid key may call. A public tool must have no scope: it is called without a key, so one could never be met, and validate/1 refuses the tool. The system. namespace belongs to core: a tool a module returns from api_tools/0 under that name is ignored, so a module cannot declare a tool that skips the scope and permission checks.
  • :permission - the role permission the key's owner must hold right now ("catalogue"). nil under the same rule as :scope; a public tool must have no permission either.
  • :title, :description - written for a person and for an agent. The title defaults to the name, the description to "".
  • :input_schema - JSON Schema (string keys) of the input, limited to the keywords PhoenixKit.API.Validator enforces (Validator.check_schema/2 refuses the rest). Defaults to an object with no properties and additionalProperties: false. Keep it static: build it from literals, not from data that changes at runtime (an enum of database values, say). The result of the schema check is remembered per distinct schema for the life of the node and never erased, so a schema that keeps changing adds an entry each time.
  • :output_schema - JSON Schema of a success answer, or nil.
  • :handler - {module, function}, called as apply(module, function, [input, ctx]).
  • :module - the module that declared the tool. Filled in by the registry.
  • :annotations - hints for clients: :read_only, :destructive, :idempotent, :open_world (all false unless given).
  • :danger - when a call needs a person's confirmation: nil (never, except the generic batch-size rule for admin writes), :always, or {module, function}, a check function(input, ctx) :: boolean() called with a dry-run context. Any other value is refused by validate/1. A key marked "requires approval" holds every write for a person regardless of :danger.
  • :replay - whether a retry of a write call with the same idempotency_key gets the first answer back. true by default. Set false on a write tool whose answer carries a credential (a session token, a claim URL): such an answer is never stored (PhoenixKit.API.Idempotency.credential_bearing?/2), so the key could not be honoured. The tool then takes no idempotency_key: the schemas, MCP and OpenAPI do not offer it, a body that sends it is a 422, and the Idempotency-Key header is ignored, as it is on a read tool. Say in the description that a repeated call is not the same call. The field is newer than core 2.63.0, where new/1 refuses it. A module that must also run there passes it only when Code.ensure_loaded?(PhoenixKit.API.ControlFields) and function_exported?(PhoenixKit.API.ControlFields, :replay_tool?, 1).
  • :client_token - for a :public tool, the name of the input field that carries the anonymous client's token ("session_token"), or nil (default). It must be a string property of input_schema. Core hashes the value (sha256, url-safe base64, never kept in clear) into PhoenixKit.API.Actor.client_token_hash and uses the hash as the caller's identity where there is no key: the per-token rate-limit bucket, the scope of the idempotency_key and the binding of a confirmation code. A public tool with a danger: must declare it and list it in required; a call without the token is a 422 naming the field. An :admin tool may not declare one. The field is a credential like a key and is redacted from stored answers. It is refused in a URL, and filtered from the request log, only when core's credential rule knows its name (the listed names and *_token; see PhoenixKit.API.Auth.credential_name?/1), so name it *_token. The field is newer than core 2.63.x, where new/1 refuses it. A module that must also run there sets it only when Map.has_key?(PhoenixKit.API.Tool.__struct__(), :client_token) (a struct key is not a function, so function_exported?/3 cannot tell; and %PhoenixKit.API.Tool{} does not compile, it has required keys).
  • :credentials - names of output fields in which the tool hands out a secret that is not named *_token (a link with a claim code in it, say). An answer carrying one of them is never stored for replay and is redacted from an approval's result. The tool's :client_token counts as one without being listed. Same rule as :client_token for older cores: pass it only when the struct has the key.
  • :max_items - the largest batch the tool accepts, or nil.
  • :cost - the rate-limit units one call costs. nil (default) means the executor's rule: 1, or for a non-read-only admin tool max(1, ceil(longest list / 50)). A positive integer up to 1000 is a fixed price. {Module, :function} is called as Module.function(input) with the raw, unvalidated input and returns a positive integer (a failure falls back to the default rule). Use it for a batch read: cost: {MyModule.API, :batch_cost}.

Building a tool

Always build it with new!/1 (or new/1 when the caller wants the error list) so the defaults above are filled in and the shape is checked:

PhoenixKit.API.Tool.new!(
  name: "catalogue.list_products",
  audience: :admin,
  scope: "catalogue.products:read",
  permission: "catalogue",
  title: "List products",
  handler: {MyModule.API, :list_products},
  annotations: %{read_only: true}
)

A tool returned from api_tools/0 that fails validate/1 is ignored with one warning per module; the module's other tools still work.

Summary

Types

Who may call the tool: :admin needs a key, :public needs nothing.

{module, function}, called as apply(module, function, [input, ctx]).

t()

Functions

The output field names whose values must never be stored or shown in clear for tool: its :credentials and its :client_token. PhoenixKit.API.Idempotency adds them to the names core already treats as credentials.

Builds a tool from a map or keyword list, fills the defaults and validates it.

Like new/1, but raises ArgumentError listing every problem.

True for core's own system.* tools.

Checks the shape of a tool struct (the registry runs this on everything a module returns).

Types

audience()

@type audience() :: :admin | :public

Who may call the tool: :admin needs a key, :public needs nothing.

handler()

@type handler() :: {module(), atom()}

{module, function}, called as apply(module, function, [input, ctx]).

t()

@type t() :: %PhoenixKit.API.Tool{
  annotations: term(),
  audience: term(),
  client_token: term(),
  cost: term(),
  credentials: term(),
  danger: term(),
  description: term(),
  handler: term(),
  input_schema: term(),
  max_items: term(),
  module: term(),
  name: term(),
  output_schema: term(),
  permission: term(),
  replay: term(),
  scope: term(),
  title: term()
}

Functions

credential_keys(tool)

@spec credential_keys(t()) :: [String.t()]

The output field names whose values must never be stored or shown in clear for tool: its :credentials and its :client_token. PhoenixKit.API.Idempotency adds them to the names core already treats as credentials.

new(attrs)

@spec new(map() | keyword()) :: {:ok, t()} | {:error, [String.t()]}

Builds a tool from a map or keyword list, fills the defaults and validates it.

Returns {:ok, tool} or {:error, messages} (every problem found, not only the first).

new!(attrs)

@spec new!(map() | keyword()) :: t()

Like new/1, but raises ArgumentError listing every problem.

system?(tool)

@spec system?(t()) :: boolean()

True for core's own system.* tools.

validate(tool)

@spec validate(t()) :: {:ok, t()} | {:error, [String.t()]}

Checks the shape of a tool struct (the registry runs this on everything a module returns).

An :admin tool needs a :scope and a :permission, except a core system.* tool, which every valid key may call. This check does not know who built the tool: that the system. namespace is core's alone is enforced where module tools are collected (PhoenixKit.ModuleRegistry.all_api_tools/0).