Chio/Docs
LOGIN · JOIN

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

A valid signature confirms the manifest was produced by the named key. It does not authorize tool calls. Authorization flows through capability tokens at runtime; the manifest is the catalog the kernel registers against, not a grant of access.

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:

crates/platform/chio-manifest/src/lib.rsrust
#[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,
}
FieldTypeMeaning
schemaStringMUST equal the constant TOOL_MANIFEST_SCHEMA = "chio.manifest.v1" (lib.rs:25).
server_idchio_core::ServerIdStable unique identifier; the registration path uses it to look up the registered key.
nameStringHuman-readable server name.
descriptionOption<String>Optional server description.
versionStringSemantic version of this tool server.
toolsVec<ToolDefinition>The tools this server provides. validate_manifest rejects an empty vec with EmptyManifest in validation.rs:45-47 (validate_tools).
server_toolsVec<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_permissionsOption<RequiredPermissions>Optional. Filesystem, network, and environment-variable declarations. The struct is descriptive only; the host sandbox does the enforcing.
public_keyStringHex-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.

FieldTypeMeaning
nameStringTool name. Unique within the server.
descriptionStringHuman-readable description.
input_schemaJSON valueJSON Schema for the tool's input arguments.
output_schemaOption<JSON value>Optional JSON Schema for the tool's output.
pricingOption<ToolPricing>Optional advertised pricing metadata. See Pricing Models.
has_side_effectsboolWhether this tool writes files, sends network requests, or modifies state. Read-only tools can be cached.
latency_hintOption<LatencyHint>Estimated latency category: instant, fast, moderate, slow.

Latency hints are advisory

Latency hints feed scheduling heuristics and operator dashboards. They are not SLAs. Latency commitments live on the marketplace listing's 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:

crates/platform/chio-manifest/src/lib.rsrust
#[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:

crates/platform/chio-manifest/src/lib.rsrust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PricingModel {
    Flat,
    PerInvocation,
    PerUnit,
    Hybrid,
}
VariantWire formUse 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 a ListingPricingHint:
    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_invocation at 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).

Signing

Manifests are signed with Ed25519 over the canonical JSON encoding of the manifest body. The signed envelope is SignedManifest:

rust
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:

rust
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:

  • schema equals chio.manifest.v1. Any other value returns UnsupportedSchema.
  • server_id, name, and version are non-empty, equal to their own trimmed form, and free of control characters. A violation returns InvalidManifestField.
  • tools is non-empty. An empty list returns EmptyManifest.
  • Tool names are unique. Duplicates return DuplicateToolName.
  • Server-tool allowlist entries are unique. Duplicates return DuplicateServerTool.
  • required_permissions entries 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:

FieldTypeMeaning
read_pathsOption<Vec<String>>Filesystem paths the server reads.
write_pathsOption<Vec<String>>Filesystem paths the server writes.
network_hostsOption<Vec<String>>Network hosts the server reaches.
environment_variablesOption<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.

rust
#[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.

ExtensionTypeEffectSpec
x-chio-sensitivitystringTag for log granularity: public, internal, sensitive, restricted. Default internal.OPENAPI-INTEGRATION.md §2.1
x-chio-side-effectsbooleanOverride the method-based default. true forces deny-by-default; false forces session-scoped allow.§2.2
x-chio-approval-requiredbooleanForces deny-by-default regardless of method or side-effects. Sets annotations.requires_approval on the generated ToolDefinition.§2.3
x-chio-budget-limitu64Per-invocation cost cap in minor currency units, consumed by the budget guard at request time.§2.4
x-chio-publishbooleanWhen 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.

  1. Read the SignedManifest from the registration payload.
  2. Run validate_manifest: schema check, identity fields, non-empty tools, unique names, unique server-tool allowlist.
  3. Verify the Ed25519 signature against the registered public key for that server_id.
  4. Cross-check that the manifest's public_key field matches the verifying key.
  5. Register tools, schemas, pricing, and side-effect flags internally.
rendering
Tool server signs the manifest, sends it on registration, kernel validates the structure, verifies the Ed25519 signature, and registers tools only when every check passes.

Failure modes

FailureErrorBehavior
Wrong schema idUnsupportedSchemaReject. Operator updates the producer to emit chio.manifest.v1.
Empty tool listEmptyManifestReject. A server with zero tools has nothing to register.
Duplicate tool nameDuplicateToolName(name)Reject. Producer must collapse duplicates before signing.
Bad signatureVerificationFailedReject. Indicates either tampering or signing with the wrong key.
Unknown top-level fieldSerde deserialization errorReject before validation runs.
Permission overreachOperator decisionOut 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:

examples/hello-tool/src/lib.rs29-67rust
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.

rust
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, &registered_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

Any change to the manifest, including adding or removing a tool, changing a price, or bumping a version, requires a fresh signature. The kernel will reject a manifest whose body does not match the bytes that were signed. The kernel has no partial update; the unit of signing is the whole manifest.

In the procurement tour

This is station 2 of the Procurement Tour: provider publishes the signed manifest the buyer's kernel reads.

Picks up from previous station. The buyer's capability has been issued; the buyer kernel now needs to learn what Vanguard offers and at what rate.

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.

json
{
  "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.

Continue at next station, where the buyer assembles a GovernedTransactionIntent that pins the quoted units and cost.

  • Pricing Models covers the ToolPricing block 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.
Signed Tool Manifests · Chio Docs