Tool flow declarations
Look up the signed contract that states what a tool returns, what it accepts, whether it sends data out, and which disclosures it permits.
On this page
A tool flow declaration is the signed information contract of one tool: the label its output carries, the most restricted input it may receive, whether invoking it sends data out of the session, and the disclosure purposes it accepts.
The declaration travels in the tool’s signed manifest, so admission and release read the same declared behavior. The operator’s policy and the host’s runtime topology bound it, and classification adds to its output label.
The operator’s policy and topology bound the declaration
The operator pins the manifest signer and supplies the admitted policy and the runtime topology at registration. The declared input clearance stays within the policy, the output floor stays in force, and outbound I/O the topology records counts as egress. The signed research contract.
Information contract
An invocation schema describes the arguments a tool accepts. The flow declaration describes what sending those arguments and receiving the result mean for information flow: orders/sql may return rows from the customers table while web/fetch discloses its input to another host, and both can expose ordinary string fields. With the declaration the host refuses an incompatible input before the external effect and keeps the declared output restrictions when the response becomes visible.
Review the declaration when you integrate a tool. Identify the source of returned data, the recipients of outbound data, and the policy owner for each. A tool that moves from local processing to a remote API has a new contract even when its JSON schema is unchanged. The signature authenticates the admitted declaration; deployment review establishes that the executor and its configuration match it.
Policy owners
Three inputs stay distinct: the publisher’s signed declaration, the operator’s admitted policy, and the host’s knowledge of the executor. The publisher describes its output. The operator’s policy sets the widest clearance the tool may ask for, and the host counts every outbound connection it makes as egress whatever the declaration says. A tool cannot lower the operator’s output floor, and a classifier adds restrictions to the declared output label while leaving the declared ones in place.
Try input_clearance, x-chio-flow, BridgeSecurityMetadata, or FlowRuntimeUnavailable. Each entry has its own link.
Declaration
pub struct ToolFlowDeclaration {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_label: Option<InformationLabel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_clearance: Option<InformationLabel>,
pub egress: bool,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub declassification_purposes: BTreeSet<DeclassificationPurpose>,
}A verified declaration supplies output classification, input clearance, egress behavior, and allowed declassification purposes. It travels in a signed manifest and the verified registry. Changing it means re-signing and updating the registry; a caller cannot override the security contract for one request.
The effective egress decision also includes the host’s runtime knowledge of the executor. A missing or misleading declaration leaves outbound I/O the host knows about counted as egress.
Fields
| Field | Declares | Refused when |
|---|---|---|
output_label | The label every result carries | The label is Top, which could never be released |
input_clearance | The most restricted input the tool may receive | It is Top, or a manifest clearance widens the operator policy |
egress | Whether the tool sends data outside the session | The tool declares egress and the operator configured no clearance policy for it |
declassification_ | Purposes a disclosure grant may name | A grant names a purpose the tool or the operator policy did not declare |
The pricing lab's contract
The pricing lab's host admits five tools across three signed manifests. After initializing .research, list the signed bodies it retained; the declaration of workspace/read_terms, which returns Harbor Goods’ terms, is shown.
jq '[.[] | .manifest | {server_id, tools: [.tools[] | {name, flow}]}]' \
.research/signed-manifests.json{
"output_label": {
"kind": "known",
"owners": {
"larkfield-pricing": [
"larkfield-pricing"
]
},
"compartments": [
"harbor-goods-terms"
]
},
"input_clearance": {
"kind": "known",
"owners": {
"larkfield-pricing": [
"larkfield-pricing"
]
},
"compartments": [
"harbor-goods-terms"
]
},
"egress": false
}read_terms declares the contract label. team-inbox/publish, the pricing team's channel, keeps a contract-restricted input clearance with egress true. public-inbox/publish, the public pull request, has a Bottom input clearance and accepts the disclosure purpose reviewed-summary. Both publishers declare Bottom output because their acknowledgment carries no customer data, and that output declaration leaves the terms read the workflow already made in force.
The host signs each manifest, retains the signed envelopes in signed-manifests.json, and registers each envelope with the manifest signer’s public key, its policies, and its topologies, all from the trusted host. Registration checks their consistency with the signed envelope. The verified registry then goes into NativeFlowResolver through the security-context entry point, the embedded native flow path; the manifest-security entry points use a published governed runtime, described below.
Manifest opt-in
pub struct ToolDefinition {
/// Tool name (must be unique within the server).
pub name: String,
/// Human-readable description. Sanitized before the LLM sees it.
/// Maximum 500 characters, validated at manifest verification time.
pub description: String,
/// JSON Schema for the tool's input parameters.
pub input_schema: serde_json::Value,
/// JSON Schema for the tool's output (optional).
#[serde(
default,
deserialize_with = "deserialize_present_option",
skip_serializing_if = "Option::is_none"
)]
pub output_schema: Option<serde_json::Value>,
/// Optional advertised pricing metadata for operator and agent planning.
#[serde(
default,
deserialize_with = "deserialize_present_option",
skip_serializing_if = "Option::is_none"
)]
pub pricing: Option<ToolPricing>,
/// Behavioral annotations for policy and scheduling decisions.
pub annotations: ToolAnnotations,
/// Categorical execution-time hint.
#[serde(
default,
deserialize_with = "deserialize_present_option",
skip_serializing_if = "Option::is_none"
)]
pub latency_hint: Option<LatencyHint>,
/// Publisher-authenticated information-flow constraints.
#[serde(
default,
deserialize_with = "deserialize_present_option",
skip_serializing_if = "Option::is_none"
)]
pub flow: Option<ToolFlowDeclaration>,
}Setting flow on any tool makes the registry require flow mediation, whatever it declares. A tool without one also requires it when its runtime topology egresses, or when its operator policy sets a clearance or output floor above bottom. chio mcp serve, chio mcp serve-http, chio mcp wrap, and a process host without a native broker refuse such a manifest at startup. The Linux process host with a native broker serves it through its native flow resolver. An embedder calling a *_with_manifest_security kernel entry point gets KernelError::FlowRuntimeUnavailable unless the kernel has a published governed security runtime.
The manifest signature covers the whole signed body, including flow. Changing a tool’s flow metadata after signing invalidates the signature, so a changed declaration is re-signed into a new manifest.
OpenAPI extension
An OpenAPI operation carries the same contract in the x-chio-flow extension field. ChioExtensions::from_operation parses it into a ToolFlowDeclaration beside the other x-chio-* extensions, and manifest generation reads each operation’s x-chio-flow into the tool’s flow before signing. export_tool_flow_extension writes a signed tool’s declaration back into an operation, replacing any existing value and removing it when the tool declares none.
Bridge metadata
pub struct BridgeSecurityMetadata {
flow: Option<ToolFlowDeclaration>,
effective_egress: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
manifest_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
server_id: Option<chio_core::ServerId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tool_name: Option<String>,
}A protocol bridge whose wire format has no room for the manifest’s security fields carries them in BridgeSecurityMetadata: the flow declaration, whether egress is effective, and the manifest digest, server, and tool that produced it. The registry validates that metadata against the live signed-manifest entry for the same server and tool and refuses the request when they disagree.
Three tool contracts
A reader of customer terms, the pricing team's channel, and the public pull request need different contracts. The reader returns restricted terms, and the team channel after it accepts that accumulated input even when its acknowledgment is public. The program builds all three declarations and applies their clearances to the same retained read; run it from the security-research project.
use chio_flow::{authorize_egress, EgressDenial};
use chio_security_types::{InformationLabel, PrincipalId, ToolFlowDeclaration};
use std::collections::{BTreeMap, BTreeSet};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let owner = PrincipalId::new("larkfield-pricing")?;
let internal = InformationLabel::try_known(
BTreeMap::from([(owner.clone(), BTreeSet::from([owner]))]),
BTreeSet::new(),
)?;
let reader = ToolFlowDeclaration::new(
Some(internal.clone()), // returned contract terms carry this classification
None, false, BTreeSet::new(), // this declaration promises no egress
)?;
let team_channel = ToolFlowDeclaration::new(
Some(InformationLabel::bottom()), // public acknowledgment after the send
Some(internal.clone()), true, BTreeSet::new(),
)?;
let pull_request = ToolFlowDeclaration::public_egress();
let retained = InformationLabel::bottom().join_restrictions(reader.output_label.as_ref().unwrap())?;
assert_eq!(authorize_egress(&retained, team_channel.input_clearance.as_ref()), Ok(()));
assert_eq!(authorize_egress(&retained, pull_request.input_clearance.as_ref()), Err(EgressDenial::FlowViolation));
assert!(team_channel.output_label.as_ref().unwrap().is_bottom());
// If runtime topology identifies outbound I/O, a missing declaration cannot hide it.
assert_eq!(authorize_egress(&retained, None), Err(EgressDenial::MissingClearance));
assert!(ToolFlowDeclaration::new(None, Some(InformationLabel::Top), true, BTreeSet::new()).is_err());
println!("READ declaration: retained contract label constructed");
println!("ALLOW pricing-team channel input; its public acknowledgment does not widen input");
println!("REFUSE public pull request input: contract restriction retained");
println!("REFUSE missing clearance; reject Top at contract construction");
Ok(())
}cargo run --locked --quiet --example tool_contractREAD declaration: retained contract label constructed ALLOW pricing-team channel input; its public acknowledgment does not widen input REFUSE public pull request input: contract restriction retained REFUSE missing clearance; reject Top at contract construction
The team channel keeps the input restriction while its acknowledgment stays public, and the pull request refuses the same contract input. The missing-clearance case applies when runtime topology identifies outbound I/O on its own, so omitting a declaration conceals nothing. A valid declaration still needs a verified manifest, policy-owned context, and the selected execution profile before the tool takes part in a governed call.
Refusals
| Refusal | Raised by | Condition |
|---|---|---|
Tool "tool output label cannot be top" | Tool or validate | A Top output label |
Tool "tool input clearance cannot be top" | Tool or validate | A Top input clearance |
Verified "manifest input clearance widens authenticated policy for tool: <tool>" | Verified registration | More clearance than the operator allows |
Verified "bridge security does not match live registry entry for <server>/<tool>" | The registry’s validate_ | Adapter metadata disagrees with the signed declaration |
Kernel "admitted manifest flow policy or topology requires an installed active defense runtime" | A *_with_ kernel entry point | A flow-required registry without a governed security runtime |
The chio mcp commands refuse a flow-required manifest at startup with the errors in bypass resistance. A declared clearance uses the labels labels and clearances defines, and an operator’s HushSpec policy can add per-origin egress overrides and tool access on top of what a tool declares.