JsonToolContract::new(input_schema, output_schema) compiles a bounded,
executable schema subset for a JSON tool. Register it with
CapabilityRuntime::register_validated_json_tool or
register_validated_protocol_json_tool. The typed Rust bridge also requires a
contract through register_typed_json_tool.
The runtime validates the ordinary JSON object/array envelope first, then the input contract before reserving a call or invoking the handler. It validates the output envelope and output contract before returning a result to Octoscript. Rejected input is recorded as denied and does not consume the tool's call budget. The same path is used by synchronous calls and deferred calls, whether they are host-pumped or externally completed.
- Types:
null,boolean,number,integer,string,array, andobject. - Object constraints:
properties,required, and booleanadditionalProperties. - Array constraints: one
itemsschema,minItems, andmaxItems. - Scalar constraints:
minimum,maximum,minLength,maxLength, andenum. - Non-enforcing annotations:
title,description,default,examples,$schema, and$id.
All other keywords are rejected during construction. In particular, this is
not general JSON Schema: $ref, schema composition (allOf, anyOf,
oneOf, not), regex patterns, conditional schemas, and schema-valued
additionalProperties are unavailable.
The implementation limits each schema source to 32 KiB, nesting to 32 schema levels, properties per object to 128, and enum values to 128.
minimum and maximum are compared as decimal digits times a power of ten,
never through a floating-point conversion. How many digits survive depends on
the exact-numbers feature of octoscript-schema, which turns on serde_json's
arbitrary_precision:
Without exact-numbers (default) |
With exact-numbers |
|
|---|---|---|
Integers in the i64/u64 range |
exact | exact |
Decimals that round-trip through f64 (0.1, 2.5e-3, 1e21) |
exact | exact |
Integers beyond u64 (a 30-digit bound) |
rounded to the nearest f64 |
exact |
Decimals past f64 precision (0.30000000000000000001) |
rounded to the nearest f64 |
exact |
Without the feature, serde_json rounds such a literal when it parses the JSON,
in the schema and in the validated value alike, so two literals that differ
only past f64 precision compare equal. Nothing else about validation changes.
The feature is off by default because arbitrary_precision is global: Cargo
unifies it into every crate in the final build and it changes serde_json's
Number representation for all of them, which breaks untagged or flattened
numbers and code that inspects Number values. Libraries (including
octoscript-capabilities and octoscript-workflow, which depend on
octoscript-schema with default-features = false) leave it off and expose
an exact-numbers passthrough. A leaf binary that owns its dependency graph
can turn it on; the octoscript CLI and octoscript-media-host do:
octoscript-workflow = { version = "0.1.0", features = ["exact-numbers"] }use octoscript_capabilities::{json, JsonToolContract, ToolError, ToolMetadata, ToolPolicy};
let contract = JsonToolContract::new(
json!({
"type": "object",
"properties": {
"left": {"type": "integer"},
"right": {"type": "integer"}
},
"required": ["left", "right"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {"total": {"type": "integer"}},
"required": ["total"],
"additionalProperties": false
}),
)?;
runtime.register_validated_json_tool(
ToolPolicy::json("math.add"),
ToolMetadata::new("Adds two integer fields."),
contract,
|request| {
let left = request.input["left"]
.as_i64()
.ok_or_else(|| ToolError::Denied("left must be an integer".to_owned()))?;
let right = request.input["right"]
.as_i64()
.ok_or_else(|| ToolError::Denied("right must be an integer".to_owned()))?;
Ok(json!({"total": left + right}))
},
)?;The contract schemas are copied into the host-side tool catalog. In contrast,
schemas attached only through ToolMetadata::with_input_schema or
with_output_schema remain prompt metadata and do not perform validation.
register_typed_json_tool and register_typed_json_tool_with_metadata are
the ergonomic bridge for a reviewed Rust crate API. They convert the already
validated input envelope to a Deserialize type and serialize a Serialize
output type. There is intentionally no uncontracted typed registration API.
use serde::{Deserialize, Serialize};
use octoscript_capabilities::{json, JsonToolContract, ToolMetadata, ToolPolicy};
#[derive(Deserialize)]
struct AddInput {
left: i64,
right: i64,
}
#[derive(Serialize)]
struct AddOutput {
total: i64,
}
runtime.register_typed_json_tool_with_metadata(
ToolPolicy::json("math.add"),
ToolMetadata::new("Adds two integer fields."),
JsonToolContract::new(
json!({
"type": "object",
"properties": {
"left": {"type": "integer"},
"right": {"type": "integer"}
},
"required": ["left", "right"],
"additionalProperties": false
}),
json!({
"type": "object",
"properties": {"total": {"type": "integer"}},
"required": ["total"],
"additionalProperties": false
}),
)?,
|input: AddInput| Ok(AddOutput {
total: input.left + input.right,
}),
)?;The schema is still the wire authority: Octoscript validates it before Serde
deserializes input and after Serde serializes output. This catches drift such
as a Rust default for an omitted required field, Serde's default acceptance of
unknown fields, incompatible enum encodings, and numeric-range differences.
Keep the schema and Rust representation aligned; additionalProperties: false remains necessary when unknown input fields are not part of the
capability. Serde attributes such as deny_unknown_fields are useful defense
in depth, but do not replace the executable contract.