Signed Tool Manifests
Every tool server in chio publishes a signed manifest declaring what tools it provides, the schemas for their inputs and outputs, and the permissions it needs from its host. The manifest format lives in chio-manifest; the schema identifier is chio.manifest.v1. The runtime kernel verifies the signature against a registered public key before the server is admitted, so a compromised server cannot quietly advertise tools it should not expose or change prices after registration.
Why Manifests Are Signed
Two failure modes motivate the signature.
- Unauthorized tool advertisement: a compromised server might add a tool entry that looks benign and route invocations to a malicious backend. The kernel admits only the manifest a known key signed, so the attacker would need both the binary and the signing key to succeed.
- Post-hoc price changes: pricing lives inside the signed manifest. An operator who set a budget against a published quote knows that quote cannot change without a fresh signature. Detecting drift is a single key check.
The signature is not authorization
Top-Level Fields
The struct lives at chio-manifest/src/lib.rs:23-58 and serializes with #[serde(deny_unknown_fields)] so unknown top-level fields fail deserialization closed:
#[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,
}| Field | Type | Meaning |
|---|---|---|
schema | String | MUST equal the constant TOOL_MANIFEST_SCHEMA = "chio.manifest.v1" (lib.rs:20). |
server_id | chio_core::ServerId | Stable unique identifier; the registration path uses it to look up the registered key. |
name | String | Human-readable server name. |
description | Option<String> | Optional server description. |
version | String | Semantic version of this tool server. |
tools | Vec<ToolDefinition> | The tools this server provides. validate_manifest rejects an empty vec with EmptyManifest (lib.rs:240-242). |
server_tools | Vec<ServerTool> | Provider-native server tools allowlisted (Anthropic computer_use, bash, text_editor). Defaults to empty (#[serde(default, skip_serializing_if = "Vec::is_empty")]); absent entries default to deny. |
required_permissions | Option<RequiredPermissions> | Optional. Filesystem, network, and environment-variable declarations. The struct is descriptive only; the host sandbox does the enforcing. |
public_key | String | Hex-encoded Ed25519 public key of this tool server. Cross-checked against the verifying key during admission. |
ToolDefinition
Each entry in tools is a ToolDefinition. It captures the contract the kernel admits.
| 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 metadata. See Pricing Models & SLAs. |
has_side_effects | bool | Whether this tool writes files, sends network requests, or modifies state. Read-only tools can be cached. |
latency_hint | Option<LatencyHint> | Estimated latency category: instant, fast, moderate, slow. |
Latency hints are advisory
ListingSla (see Capability Discovery).ToolPricing
The pricing block on a ToolDefinition lives at chio-manifest/src/lib.rs:142-152. It carries four fields, all of them honoring deny_unknown_fields:
#[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>,
}The currency travels inside MonetaryAmount { units, currency }: ToolPricing itself has no top-level currency field. It also has no max_cost_per_invocation (that field belongs to ToolGrant at chio-core-types/src/capability.rs:1749) and no sla_guarantees (those belong to ListingSla at chio-listing/src/discovery.rs:118-138). See the dedicated section below.
PricingModel enum
The four variants at chio-manifest/src/lib.rs:154-161:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PricingModel {
Flat,
PerInvocation,
PerUnit,
Hybrid,
}| Variant | Wire form | Use when |
|---|---|---|
Flat | "flat" | A single fixed amount per call. Set base_price; leave unit_price and billing_unit unset. |
PerInvocation | "per_invocation" | Charge per call with a per-call unit_price and billing_unit = "invocation". The metering pipeline emits one ledger entry per receipt. |
PerUnit | "per_unit" | Charge by an output-derived count (tokens, rows, bytes). Set unit_price and a meaningful billing_unit string. |
Hybrid | "hybrid" | A flat connection fee plus per-unit charge. Set base_price, unit_price, and billing_unit. Settlement adds them. |
Where SLA Guarantees and Per-Call Caps Live
Operators commonly look on ToolPricing for two adjacent concepts that do not live there:
- SLA commitments are carried by
chio_listing::ListingSla(chio-listing/src/discovery.rs:118-138), paired into aListingPricingHint:crates/chio-listing/src/discovery.rs#[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, not on the manifest.
ToolGrant::max_cost_per_invocationatchio-core-types/src/capability.rs:1747-1749is anOption<MonetaryAmount>checked against parent grants inToolGrant::is_subset_of(capability.rs:1805-1813).
Signing
Manifests are signed with Ed25519 over the canonical JSON encoding of the manifest body. The signed envelope is SignedManifest:
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,
}Signing and verification are exposed through two free functions:
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. Validation enforces three structural invariants:
schemaequalschio.manifest.v1. Any other value returnsUnsupportedSchema.toolsis non-empty. An empty list returnsEmptyManifest.- Tool names are unique. Duplicates return
DuplicateToolName. - Server-tool allowlist entries are unique.
required_permissions
A tool server can declare what it needs from its sandbox so the host operator sees the surface up front. RequiredPermissions is optional, with these fields:
| 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_variables | Option<Vec<String>> | Environment variables the server reads. |
Permission overreach is a deployment-time decision: the operator either grants the declared permissions or refuses to admit the manifest. required_permissions is descriptive, not enforced inside the manifest itself; the sandbox or host that runs the tool server is the enforcement point.
Provider-Native Server Tools
Some upstream model providers offer server-side tools beyond the regular client-hosted tool surface (Anthropic computer_use_*, bash_*, text_editor_*). These carry a larger trust boundary than ordinary tools, so the manifest must explicitly allowlist them. The ServerTool enum names them; the server_tools field on the manifest is the allowlist; 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. The vocabulary lives in spec/OPENAPI-INTEGRATION.md sections 2.1 through 2.5; sections 3.x define precedence. The five shipped extensions are sensitivity, side-effects, approval-required, budget-limit, and publish. There is no x-chio-pricing or x-chio-sla; pricing on OpenAPI-derived manifests is set by the operator at signing time.
| Extension | Type | Effect | Spec |
|---|---|---|---|
x-chio-sensitivity | string | Tag for log granularity: public, internal, sensitive, restricted. Default internal. | OPENAPI-INTEGRATION.md §2.1 |
x-chio-side-effects | boolean | Override the method-based default. true forces deny-by-default; false forces session-scoped allow. | §2.2 |
x-chio-approval-required | boolean | Forces deny-by-default regardless of method or side-effects. Sets annotations.requires_approval on the generated ToolDefinition. | §2.3 |
x-chio-budget-limit | u64 | Per-invocation cost cap in minor currency units, consumed by the budget guard at request time. | §2.4 |
x-chio-publish | boolean | When false, the operation is excluded from the generated manifest. Honored only when respect_publish_flag is true. | §2.5 |
Precedence rules from OPENAPI-INTEGRATION.md §3: 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. See OpenAPI integration in the spec for the full precedence matrix.
Verification Flow
When a tool server registers with a kernel, the kernel walks the same path on every admission.
- Read the
SignedManifestfrom the registration payload. - Run
validate_manifest: schema check, non-empty tools, unique names, unique server-tool allowlist. - Verify the Ed25519 signature against the registered public key for that
server_id. - Cross-check that the manifest's
public_keyfield matches the verifying key. - Register tools, schemas, pricing, and side-effect flags internally.
Failure Modes
| Failure | Error | Behavior |
|---|---|---|
| Wrong schema id | UnsupportedSchema | Reject. Operator updates the producer to emit chio.manifest.v1. |
| Empty tool list | EmptyManifest | Reject. A server with zero tools has nothing to register. |
| Duplicate tool name | DuplicateToolName(name) | Reject. Producer must collapse duplicates before signing. |
| Bad signature | VerificationFailed | Reject. Indicates either tampering or signing with the wrong key. |
| Unknown top-level field | Serde deserialization error | Reject before validation runs. |
| Permission overreach | Operator decision | Out of band: the manifest declares what it needs; the host decides whether to admit. |
Worked Example: Sign and Register
A small native tool server publishes one tool, greet, with per-invocation pricing.
use chio_core::capability::MonetaryAmount;
use chio_core::crypto::Keypair;
use chio_manifest::{
sign_manifest, LatencyHint, PricingModel, ToolDefinition,
ToolManifest, ToolPricing, TOOL_MANIFEST_SCHEMA,
};
fn main() -> anyhow::Result<()> {
let keypair = Keypair::generate();
let manifest = ToolManifest {
schema: TOOL_MANIFEST_SCHEMA.into(),
server_id: "srv-hello".into(),
name: "Hello Tool Server".into(),
description: Some("A demo tool server".into()),
version: "0.1.0".into(),
tools: vec![ToolDefinition {
name: "greet".into(),
description: "Returns a greeting".into(),
input_schema: serde_json::json!({
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}),
output_schema: None,
pricing: Some(ToolPricing {
pricing_model: PricingModel::PerInvocation,
base_price: None,
unit_price: Some(MonetaryAmount {
units: 50,
currency: "USD".to_string(),
}),
billing_unit: Some("invocation".into()),
}),
has_side_effects: false,
latency_hint: Some(LatencyHint::Instant),
}],
server_tools: Vec::new(),
required_permissions: None,
public_key: keypair.public_key().to_hex(),
};
let signed = sign_manifest(&manifest, &keypair)?;
let json = serde_json::to_string_pretty(&signed)?;
println!("{json}");
Ok(())
}The kernel-side admission code calls verify_manifest with the registered public key. On success the tool is registered with its schemas, pricing, and side-effect flag intact.
use chio_manifest::verify_manifest;
let signed = read_signed_manifest_from_registration()?;
let registered_key = lookup_registered_key(&signed.manifest.server_id)?;
verify_manifest(&signed, ®istered_key)?;
// Manifest is now admissible. Register tools with the kernel.
for tool in &signed.manifest.tools {
kernel.register_tool(&signed.manifest.server_id, tool)?;
}Re-signing on every change
In the Procurement Tour
This is station 2 of the Procurement Tour: provider publishes the signed manifest the buyer's kernel reads.
Vanguard Security signs and publishes a ToolManifest declaring its soc2-review tool with per-row metered pricing. The advertised unit_price is denominated in cents per 1000-row batch, so a single billing unit at this rate is $0.001 per evidence row:
{
"manifest": {
"schema": "chio.manifest.v1",
"server_id": "vanguard-security",
"name": "Vanguard SOC 2 Review Tool",
"description": "Per-row evidence inspection for SOC 2 type II reviews",
"version": "1.4.0",
"tools": [
{
"name": "soc2-review",
"description": "Inspect a control evidence row and emit a finding",
"input_schema": {
"type": "object",
"properties": {
"evidence_uri": { "type": "string" },
"control_id": { "type": "string" }
},
"required": ["evidence_uri", "control_id"]
},
"output_schema": null,
"pricing": {
"pricing_model": "per_unit",
"unit_price": { "units": 100, "currency": "USD" },
"billing_unit": "1000-evidence-rows"
},
"has_side_effects": false,
"latency_hint": "fast"
}
],
"required_permissions": null,
"public_key": "c87a..."
},
"signature": "ed25519:...",
"signer_key": "did:chio:c87a..."
}The buyer kernel runs verify_manifest against Vanguard's registered key, then carries the soc2-review entry plus its ToolPricing forward as the catalog input for the next station's quote request.
GovernedTransactionIntent that pins the quoted units and cost.Related
- Pricing Models & SLAs covers the
ToolPricingblock and metered billing. - Capability Discovery covers how registered manifests surface through the marketplace listing layer.
- Bilateral Federation covers how a manifest registered at one kernel becomes visible across a federation boundary.