Signed Tool Manifests
Declare a tool server's tools, schemas, permissions, and prices in one signed manifest the kernel verifies before any tool registers.
On this page
A tool provider can publish exactly what its server offers, and at what price, in one document the operator inspects before any tool goes live. The manifest names each tool, its input and output schemas, its side effects, and its advertised price, and the server's Ed25519 key signs the whole of it.
The kernel registers a server only after the manifest validates and its signature verifies against the key the operator registered for that server. Other operators find the same server later through a signed listing that points back to this manifest.
The kernel registers only a verified manifest
A tool server signs a SignedManifest with Ed25519. The kernel checks its structure, its tool definitions, and their uniqueness, verifies the signature against the server's registered key, then registers the tools with their schemas and advertised pricing. Signed tool manifests.
The pricing server declares a priced tool and signs it once
You publish quote on your pricing server at USD 0.01 a call, the price the outside buyer's grant in Quickstart part 2 pays. The native service builder declares the tool, its schemas, and its price in one place, and produces the manifest.
pub fn build_service(public_key_hex: String) -> Result<NativeChioService, ManifestError> {
NativeChioServiceBuilder::new("pricing", public_key_hex)
.server_name("Larkfield Pricing")
.server_version("1.0.0")
.tool(
NativeTool::new(
"quote",
"Quote a SKU at a discount, rounded half up to the cent",
serde_json::json!({
"type": "object",
"properties": {
"sku": { "type": "string" },
"percent": { "type": "number", "minimum": 0, "maximum": 100 }
},
"required": ["sku", "percent"]
}),
)
.read_only()
.per_invocation_price(1, "USD")
.latency_hint(LatencyHint::Instant),
// pricing.rs: parses sku and percent, returns the quote as JSON
|arguments| pricing::quote_json(&arguments),
)
.build()
}per_invocation_price(1, "USD") is 1 minor unit, one cent: MonetaryAmount counts whole minor units, and a dollar is 100 of them. The example then signs the manifest with the server keypair. On the kernel side, admission calls verify_manifest with the registered public key and registers the server; the tools keep their schemas, pricing, and side-effect flags.
use chio_manifest::verify_manifest;
// signed: &SignedManifest from the application's registration transport.
// registered_key: &PublicKey from operator-owned trust configuration.
verify_manifest(signed, registered_key)?;
// connection: Box<dyn ToolServerConnection> for the admitted server.
kernel.register_tool_server(connection);The unit of signing is the whole manifest. Adding or removing a tool, changing a price, or bumping a version needs a fresh signature, and the kernel rejects a manifest whose body differs from the signed bytes.
A known key must sign every tool and every price the kernel registers
The signature closes two attacks on a tool catalog.
- Unauthorized tool advertisement. A compromised server could add an entry that looks benign and route its invocations to a malicious backend. The kernel admits a manifest only under a known key, so the attacker would need both the binary and its signing key.
- Price changes after the fact. Pricing lives inside the signed manifest. An operator who set a budget against a published price knows that price changes only with a fresh signature, and verifying the signature exposes any drift.
A valid signature authenticates the catalog the kernel registers against. Each call to a registered tool is still authorized by the caller's capability token at runtime.
Read more in Capabilities.
Registration runs five checks, and any failure refuses the whole manifest
For each registration, the kernel walks the same path, in order.
- Read. Take the
SignedManifestfrom the registration payload. - Validate. Run
validate_manifest: schema, identity fields, a non-empty tool list, unique tool names, a unique server-tool allowlist. - Verify. Check the Ed25519 signature against the public key registered for that
server_id. - Cross-check. Confirm the manifest's own
public_keyfield matches the verifying key. - Register. Record the tools, schemas, pricing, and side-effect flags.
| Failure | Error | Producer's fix |
|---|---|---|
| Wrong schema id | UnsupportedSchema | Emit chio.manifest.v1 |
| Empty tool list | EmptyManifest | Declare at least one tool |
| Duplicate tool name | DuplicateToolName(name) | Collapse duplicates before signing |
| Bad signature | VerificationFailed | Re-sign with the registered key |
| Unknown top-level field | Serde deserialization error | Remove the field; rejected before validation |
A bad signature means tampering or the wrong signing key. No tool from a refused manifest registers, so a producer fixes the error and signs again.
Declared permissions tell the operator what the host must allow
A server can declare what it needs from its sandbox in required_permissions: the paths it reads and writes, the network hosts it reaches, and the environment variables it reads. The operator sees the request before registration and either grants it or refuses to admit the manifest. The host that runs the server enforces the grant.
Provider-native server tools, such as Anthropic's computer use, bash, and text editor, carry a larger trust boundary than ordinary tools. The manifest allowlists them by name in server_tools, and an absent entry is refused. Versioned wire names such as bash_20241022 map to their category, so a provider's version bump stays behind the same allowlist entry.
Read more in The assurance model.
A metered tool advertises a batch it can price in whole cents
You sell an outside buyer read access to your order history through the orders server's sql tool, billed by rows returned. The billing unit is a batch of a thousand rows, and the advertised unit_price is 100 cents per batch. The field names are the manifest's own, and the keys are shortened.
{
"manifest": {
"schema": "chio.manifest.v1",
"server_id": "orders",
"name": "Larkfield Orders",
"description": "Read-only SQL over the orders, skus, and customers tables",
"version": "1.0.0",
"tools": [
{
"name": "sql",
"description": "Run one read-only query and return its rows",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"max_rows": { "type": "integer" }
},
"required": ["query"]
},
"output_schema": null,
"pricing": {
"pricing_model": "per_unit",
"unit_price": { "units": 100, "currency": "USD" },
"billing_unit": "1000-rows"
},
"has_side_effects": false,
"latency_hint": "fast"
}
],
"required_permissions": null,
"public_key": "4f1c…a2e9"
},
"signature": "9f3c…20b1",
"signer_key": "4f1c…a2e9"
}The batch follows from the arithmetic. A manifest prices one billing unit at a time, and a MonetaryAmount has no representation for a tenth of a cent, so a tool that charges per row advertises a batch it can price in whole cents. A MeteredBillingQuote carries a quotedCost for the whole estimate, so the quote can name the finer unit and count a thousand of them. A quote for 1,000 rows comes to USD 1.00, the advertised price of one batch.
The buyer's kernel runs verify_manifest against your registered key, then carries the sql entry and its ToolPricing forward as the catalog input for a metered quote request. The fixed-price service purchase works differently: its MCP tool returns the quote as application data.
Read more in Pricing Models, which binds quoted units and cost into a governed intent.
Manifest fields and validation
Search for server_tools, PricingModel, or x-chio-budget-limit.
Top-level fields
The ToolManifest struct lives at crates/platform/chio-manifest/src/lib.rs:28-63 and serializes with #[serde(deny_unknown_fields)], so an unknown top-level field fails deserialization closed.
schema
MUST equal the constant TOOL_MANIFEST_SCHEMA (lib.rs:25). Any other value returns UnsupportedSchema.
Example"chio.manifest.v1"
server_id
Stable unique identifier. The registration path uses it to look up the registered key. Non-empty, equal to its trimmed form, free of control characters; a violation returns InvalidManifestField.
Example"pricing"
name
Human-readable server name, under the same non-empty, trimmed, control-character-free rule as server_id.
Example"Larkfield Pricing"
description
Optional server description.
version
Semantic version of this tool server, under the same rule as server_id.
Example"1.0.0"
tools
The tools this server provides. validate_tools (validation.rs:45-47) rejects an empty list with EmptyManifest and duplicate names with DuplicateToolName.
server_tools
Allowlist of provider-native server tools (Anthropic computer_use, bash, text_editor). Serialized with #[serde(default, skip_serializing_if = "Vec::is_empty")]. Absent entries default to deny; duplicates return DuplicateServerTool.
Example["bash"]
required_permissions
Filesystem, network, and environment-variable declarations, checked for well-formedness when present. The struct describes; the host sandbox enforces.
public_key
Hex-encoded Ed25519 public key of this tool server, cross-checked against the verifying key during admission.
Example"4f1c…a2e9"
Read the ToolManifest struct
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolManifest {
pub schema: String,
pub server_id: chio_core::ServerId,
pub name: String,
pub description: Option<String>,
pub version: String,
pub tools: Vec<ToolDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub server_tools: Vec<ServerTool>,
pub required_permissions: Option<RequiredPermissions>,
pub public_key: String,
}ToolDefinition
Each entry in tools is a ToolDefinition: the contract the kernel admits for one tool.
| Field | Type | Meaning |
|---|---|---|
name | String | Tool name, unique within the server. |
description | String | Human-readable description. |
input_schema | JSON value | JSON Schema for the tool's input arguments. |
output_schema | Option<JSON value> | Optional JSON Schema for the tool's output. |
pricing | Option<ToolPricing> | Optional advertised pricing, below. |
has_ | bool | Writes files, sends network requests, or modifies state. Read-only tools can be cached. |
latency_hint | Option<LatencyHint> | instant, fast, moderate, or slow. |
Latency hints are advisory: they feed scheduling heuristics and operator dashboards. Latency commitments live on the listing's ListingSla, described in Capability Discovery.
ToolPricing
The pricing block on a ToolDefinition lives at crates/platform/chio-manifest/src/lib.rs:149-157. Its four fields honor deny_unknown_fields, and the currency travels inside MonetaryAmount { units, currency }.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolPricing {
pub pricing_model: PricingModel,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_price: Option<MonetaryAmount>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit_price: Option<MonetaryAmount>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub billing_unit: Option<String>,
}PricingModel enum
Four variants, at crates/platform/chio-manifest/src/lib.rs:161-166, serialized in snake case.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PricingModel {
Flat,
PerInvocation,
PerUnit,
Hybrid,
}| Variant | Wire form | Fields to set |
|---|---|---|
Flat | "flat" | One fixed amount per call: base_price only. |
PerInvocation | "per_invocation" | unit_price and billing_unit = "invocation". |
PerUnit | "per_unit" | unit_price and a billing_unit such as tokens, rows, or bytes. |
Hybrid | "hybrid" | base_price, unit_price, and billing_unit. |
For PerInvocation, the metering pipeline emits one ledger entry per receipt. PerUnit charges by a count derived from the output. Hybrid is a flat connection fee plus a per-unit charge, and settlement adds the two.
Where SLA guarantees and per-call caps live
ToolPricing has no top-level currency, no max_cost_per_invocation, and no sla_guarantees. SLA commitments are carried by chio_listing::ListingSla (crates/economy/chio-listing/src/discovery.rs:113-136), paired into a ListingPricingHint:
/// Service-level advertisement paired with a pricing hint.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ListingSla {
pub max_latency_ms: u64,
/// Availability SLA expressed in basis points. `10_000` means 100.00%.
pub availability_bps: u32,
pub throughput_rps: u64,
}Per-call cost caps live on the issued capability. ToolGrant::max_cost_per_invocation (chio-core-types/src/capability/scope.rs:110) is an Option<MonetaryAmount> checked against parent grants in ToolGrant::is_subset_of (capability/scope.rs:129).
SignedManifest and signing functions
Manifests are signed with Ed25519 over the canonical JSON encoding of the manifest body. The envelope carries the signing key, so a verifier needs no out-of-band lookup to check it. A service built with NativeChioServiceBuilder signs the manifest its builder produced with the server keypair.
pub struct SignedManifest {
/// The tool manifest.
pub manifest: ToolManifest,
/// Ed25519 signature over the canonical JSON encoding of manifest.
pub signature: Signature,
/// The signing key (for verification without out-of-band lookup).
pub signer_key: PublicKey,
}pub fn sign_manifest(
manifest: &ToolManifest,
keypair: &Keypair,
) -> Result<SignedManifest, ManifestError>;
pub fn verify_manifest(
signed: &SignedManifest,
public_key: &PublicKey,
) -> Result<(), ManifestError>;Both functions call validate_manifest first, which enforces these invariants:
schemaequalschio.manifest.v1, elseUnsupportedSchema.server_id,name, andversionare non-empty, equal to their trimmed form, and free of control characters, elseInvalidManifestField.toolsis non-empty, elseEmptyManifest.- Tool names are unique, else
DuplicateToolName. - Server-tool allowlist entries are unique, else
DuplicateServerTool. required_permissionsentries are well-formed when present.
required_permissions
RequiredPermissions is optional. Permission overreach is a deployment-time decision: the operator grants the declared permissions or refuses to admit the manifest, and the host that runs the server enforces them.
| Field | Type | Meaning |
|---|---|---|
read_paths | Option<Vec<String>> | Filesystem paths the server reads. |
write_paths | Option<Vec<String>> | Filesystem paths the server writes. |
network_hosts | Option<Vec<String>> | Network hosts the server reaches. |
environment_ | Option<Vec<String>> | Environment variables the server reads. |
Provider-native server tools
The ServerTool enum names the provider-native tools a manifest may allowlist (Anthropic computer_use_*, bash_*, text_editor_*). The server_tools field is the allowlist, and absent entries default to deny.
#[serde(rename_all = "snake_case")]
pub enum ServerTool {
ComputerUse,
Bash,
TextEditor,
}
// Anthropic versions server-tool names with a trailing date,
// e.g. bash_20241022. Treat known categories as server tools so
// version bumps stay fail-closed behind the same allowlist entry.
ServerTool::from_anthropic_wire_name("bash_20241022")
== Some(ServerTool::Bash);x-chio-* extensions for OpenAPI sources
When chio-openapi generates a manifest from an OpenAPI document, five vendor extensions on each operation feed the manifest and the chio api protect proxy. spec/OPENAPI-INTEGRATION.md defines them in sections 2.1 to 2.5 and their precedence in section 3. Pricing on an OpenAPI-derived manifest is set by the operator at signing time; the extension set has no x-chio-pricing or x-chio-sla.
| Extension | Type | Effect | Spec |
|---|---|---|---|
x- | string | Log granularity: public, internal, sensitive, restricted. Default internal. | §2.1 |
x- | boolean | Overrides the method default: true forces deny-by-default, false forces session-scoped allow. | §2.2 |
x- | boolean | Forces deny-by-default and sets annotations. on the generated ToolDefinition. | §2.3 |
x- | u64 | Per-invocation cost cap in minor units, read by the budget guard at request time. | §2.4 |
x-chio-publish | boolean | false excludes the operation when respect_ is true. | §2.5 |
x-chio-approval-required: true beats both the HTTP method default and any x-chio-side-effects value, so a GET operation marked approval-required still produces deny-by-default. OpenAPI Integration carries the full precedence matrix.