EconomyTools
Signed Tool Manifests
A signed chio.manifest.v1 declares a tool server's tools, schemas, and required permissions before the kernel admits it.
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 a manifest only when a known key signed it. An attacker would need the binary and its signing key.
- 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. The kernel detects drift by verifying the signature.
The signature is not authorization
Top-level fields
The struct lives at crates/platform/chio-manifest/src/lib.rs:28-63 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:25). |
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 in validation.rs:45-47 (validate_tools). |
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. |
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 crates/platform/chio-manifest/src/lib.rs:149-157. 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/scope.rs:110) and no sla_guarantees (those belong to ListingSla at chio-listing/src/discovery.rs:116-121). See the dedicated section below.
PricingModel enum
The four variants at crates/platform/chio-manifest/src/lib.rs:161-166:
#[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(crates/economy/chio-listing/src/discovery.rs:113-136), paired into aListingPricingHint:crates/economy/chio-listing/src/discovery.rs113-121rust/// 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, not on the manifest.
ToolGrant::max_cost_per_invocationatchio-core-types/src/capability/scope.rs:110is anOption<MonetaryAmount>checked against parent grants inToolGrant::is_subset_of(capability/scope.rs:129).
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 these structural invariants:
schemaequalschio.manifest.v1. Any other value returnsUnsupportedSchema.server_id,name, andversionare non-empty, equal to their own trimmed form, and free of control characters. A violation returnsInvalidManifestField.toolsis non-empty. An empty list returnsEmptyManifest.- Tool names are unique. Duplicates return
DuplicateToolName. - Server-tool allowlist entries are unique. Duplicates return
DuplicateServerTool. required_permissionsentries are well-formed when present.
required_permissions
A tool server can declare what it needs from its sandbox so the host operator sees the requested capabilities before registration. 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 interface (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 validation path for each admission.
- Read the
SignedManifestfrom the registration payload. - Run
validate_manifest: schema check, identity fields, 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
The hello-tool example publishes one tool, greet, with per-invocation pricing. It builds the manifest through the native service builder rather than by filling the struct in by hand, so the priced tool and its schemas are declared in one place:
pub fn build_service(
public_key_hex: String,
) -> Result<NativeChioService, chio_manifest::ManifestError> {
NativeChioServiceBuilder::new("srv-hello", public_key_hex)
.server_name("Hello Tool Server")
.server_version("0.1.0")
.server_description("A tiny native Chio service that exposes a tool, resource, prompt, and priced manifest")
.tool(
NativeTool::new(
"greet",
"Returns a personalized greeting",
serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name to greet"
}
},
"required": ["name"]
}),
)
.output_schema(serde_json::json!({
"type": "object",
"properties": {
"greeting": { "type": "string" }
}
}))
.read_only()
.per_invocation_price(25, "USD")
.latency_hint(chio_manifest::LatencyHint::Instant),
|arguments| {
let name = greeting_name(&arguments)?;
Ok(serde_json::json!({
"greeting": format!("Hello, {name}! This greeting was served by a native Chio service.")
}))
},
)
.static_resource(per_invocation_price(25, "USD") is 25 minor units, twenty-five cents, because MonetaryAmount counts whole minor units and a dollar is 100 of them. The example then signs the manifest the builder produced with the server keypair (examples/hello-tool/src/lib.rs:134).
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-sign after each 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 metered pricing. The billing unit is a batch of a thousand evidence rows and the advertised unit_price is 100 cents for one batch. The field names below are the manifest's own; the values are made up for the example, and the two keys are shortened so the shape stays readable.
The batch is not a stylistic choice. A manifest prices one billing unit at a time, and unit_price is a MonetaryAmount, which counts whole minor units of a currency. A tenth of a cent has no representation, so a tool charging per row has to advertise a batch it can price in whole cents. A quote is not under that constraint: MeteredBillingQuote carries a quotedCost for the whole estimate rather than a price per unit, so it can name the finer unit and count a thousand of them. The same tool therefore advertises a batch and quotes a row, and both numbers land on the same dollar. Pricing shows the quote side.
{
"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": "9f3c...",
"signer_key": "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 covers the
ToolPricingblock and metered billing. - Capability Discovery covers how registered manifests appear through the marketplace listing layer.
- Bilateral Federation covers how a manifest registered at one kernel becomes visible across a federation boundary.