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.

Execution deadlines accept a finite absolute millisecond timestamp, explicit :infinity, or legacy unset nil. JSON persistence uses the exact string "infinity" for the sentinel and explicit normalization when reading. Public runtime options accept run_timeout: :infinity; other timeout options remain positive finite integers. provider_idle_timeout additionally accepts nil (the default total-provider-effect policy).

Goal tools and records

The existing Goal tools retain object schemas: create_goal requires objective and accepts optional integer token_budget (the Goal owner validates its positive configured bound); get_goal accepts no properties; update_goal requires status from complete, blocked or paused. Unknown properties are rejected. Host bindings, execution mode, thread/run identity and resume controls are not model arguments.

Persistent Goal responses use the runtime's snake_case names, adapting Codex's camelCase wire names. They contain goal, remaining_tokens and completion_budget_report. A present goal exposes thread_id, objective, status, token_budget, tokens_used, time_used_seconds, created_at and updated_at; public timestamps are Unix seconds. Remaining tokens are nil without a budget or while accounting is uncertain, otherwise clamped to zero. Uncertain accounting adds accounting_uncertain: true and suppresses the completion report so partial known usage is not advertised as final totals. Ordinary responses retain the three source-compatible fields. The completion report is present only for a complete Goal with a token budget or positive elapsed time.

Private snapshots retain goal_id, millisecond timestamps, revision, receipt and invocation ledgers, attribution and accounting state. They must not become tool responses or provider metadata. Receipt types are turn_started, usage, tool_finished, item_completed, turn_stopped, turn_aborted and turn_error; their identity comes from committed runtime transitions, never model parameters. See PERSISTENCE.md.