Tool Input Schemas

Copy Markdown

Backplane.AgentRuntime.InputSchema is the MCP tool-call boundary. Schema semantics are provided by jsonschex 0.10.0, whose target is JSON Schema Draft 2020-12. The runtime keeps the existing validate_schema/1 and validate/2 contracts and adds optional opts forms for explicit external schema registries/loaders.

Dialect And Vocabularies

The supported dialect is exactly:

https://json-schema.org/draft/2020-12/schema

The $schema keyword may be omitted or may include the canonical URI with a trailing #. Older drafts, unknown dialect URIs, and unsupported required vocabularies return :unsupported_capability; they are never interpreted as a different draft. The original schema is retained by the tool descriptor and is never made less restrictive to pass validation.

The standard core, applicator, validation, unevaluated, meta-data, format-annotation, and content vocabularies are supported. format is annotation-only by default. contentEncoding, contentMediaType, and contentSchema are also annotation-only by default; the runtime never decodes or mutates tool arguments. A host that explicitly needs assertions can pass format_assertion: true or content_assertion: true to the opts form. An explicit required format-assertion vocabulary follows the dialect contract.

Draft 2020-12 Coverage

The engine covers the complete Draft 2020-12 core, applicator, validation, unevaluated, and content vocabularies, including:

  • boolean schemas, all seven JSON types, unions, enum, const, and JSON numeric/structural equality;
  • multipleOf, inclusive and exclusive numeric bounds, Unicode string lengths, ECMA-262-compatible pattern, and format policy above;
  • properties, patternProperties, additionalProperties, propertyNames, required, minProperties, maxProperties, dependentRequired, and dependentSchemas;
  • prefixItems, items, contains, minContains, maxContains, minItems, maxItems, and uniqueItems;
  • arbitrary nesting and coexistence of allOf, anyOf, oneOf, not, if/then/else, including sibling keywords next to $ref;
  • $id, $schema, $defs, $ref, $anchor, $dynamicRef, $dynamicAnchor, reference scopes, recursive references, and dynamic scope;
  • evaluated-result propagation for unevaluatedProperties and unevaluatedItems across composition branches and references.

The package's official Draft 2020-12 test suite is vendored at test/fixtures/json_schema_test_suite and pinned by COMMIT. The required non-optional suite is run by json_schema_draft202012_test.exs; optional format and content assertion behavior is tested separately according to the policy above.

Runtime Boundary

Tool schemas must be maps with an object root. A missing root type retains the MCP object boundary; an explicit root union is rejected unless it is exactly "object" (or ["object"]). Nested schemas may be maps or boolean schemas.

Elixir host schemas may use atom keys and atom values only for the type keyword. Keys are converted to existing strings with Atom.to_string/1; no untrusted atoms are created. The input argument term is returned unchanged: there are no defaults, coercions, unknown-property trimming, or default-based authorization decisions.

External references are never fetched implicitly. Pass either:

InputSchema.validate(schema, arguments,
  schema_registry: %{"https://example.test/name" => %{"type" => "string"}}
)

or an explicit schema_loader: &loader/1. Missing local references and failed explicit loads are reported before catalog publication or backend invocation.

Schema size, nesting, node, reference, and input-shape limits fail closed with :execution_failure. Validator execution failures are not converted into ordinary oneOf/anyOf/not mismatches. Schema compilation errors are :validation, while unsupported dialect/vocabulary capability errors are :unsupported_capability.

ToolCatalog preflight and the Execution/Conversation gateways call this same boundary. The check occurs before authorization, approval, budget reservation, durable intent publication, or backend dispatch.

Compatibility Evidence

  • Issue #45 oneOf/object sibling composition remains covered by the existing catalog and execution regressions.
  • Issue #46 and the captured Sigma/MCP schema fixtures remain catalog-preflight inputs; schemas are retained unchanged and validated by Draft 2020-12.
  • The package now has a production dependency on jsonschex and ex_json_pointer, with decimal for arbitrary-precision numeric assertions; it is no longer a dependency-free artifact.

Batch admission and quarantine

Backplane.AgentRuntime.ToolCatalog.admit_batch/2 is the public batch boundary. It is strict by default; pass mode: :quarantine explicitly to omit only tools whose direct InputSchema.validate_schema/1 result is %Error{class: :unsupported_capability}. Validation, metadata, backend, authority, duplicate-name, and unresolved-reference errors remain fatal.

The result is one executable bundle: registry, provider tools, narrowed authority, accepted names, and ordered rejected diagnostics containing the tool name, descriptor revision, and original structured error. Grants are only removed for rejected tools; a tool_revisions map is narrowed to the same accepted set while caller/run and other authority fields are retained. A legacy single tool_revision remains supported. Pass run_id: expected_run_id when a direct admission must prove the authority belongs to a particular run; initial Conversation and dynamic publication paths bind this automatically. An empty input or an all-rejected quarantine batch returns an empty registry, provider list, and grants. Rejected diagnostics are trusted-host data and must not be serialized into model requests, subscriber events, or checkpoints.

Draft 2020-12 local $defs/$ref schemas are supported by the current runtime and therefore are not quarantined merely for using references. Unsupported dialects, required vocabularies, keywords, or non-object tool roots are current examples of quarantine-eligible capability errors. Invalid or unresolved references remain fatal validation errors.

Use schema_admission: :strict | :quarantine when starting a Conversation or on a dynamic catalog update; both paths consume the same admitted bundle.