Bridge Protocols
Route A2A, ACP, MCP, and OpenAI calls through one kernel so each bridged call keeps its caller's authority and one receipt lineage.
On this page
An A2A message or an ACP tool call can reach a tool that speaks another protocol and still be decided by one kernel, under the caller's own capability. chio-cross-protocol narrows that authority to the target, plans the route, and hands the kernel route evidence that its receipt signs.
You embed the crate in a Rust host, or run an edge crate that already embeds it. Every envelope and receipt shown comes from one ACP call into a native tool, driven by the hello-acp example.
StartA bridged call is one kernel evaluation
A2A message/send and ACP tool/invoke reach CrossProtocolOrchestrator::execute, which validates the request identity fields, binds capability lineage, narrows the scope to the target server and tool, and plans a route before the kernel evaluates. MCP tools/call enters the kernel directly through chio mcp serve or chio mcp serve-http.
The orchestrator narrows authority before the kernel evaluates
message/send and tool/invoke reach CrossProtocolOrchestrator::execute. It validates the request identity fields, binds capability lineage, narrows the scope to the target server and tool, and plans a route: Select keeps the requested target, and Attenuate falls back to another available one. Route Plans.
The planner selects the requested target, attenuates to another available one, or denies. A denied route still ends in a refusal the kernel signs through sign_planned_deny_response, with the route and trace evidence returned to the caller; the target executor never runs. A shared envelope and trace link every hop of the call. Edge kernels with no shared orchestrator would sign receipts that never reference each other, and a translator shim could widen a capability between hops.
The compatibility passthrough is the one route around the kernel: it compiles only in tests or with the compatibility-surface feature, is reached only through ChioA2aEdgeCompatibility or ChioAcpEdgeCompatibility, and returns no verdict, receipt, or counter, with metadata reading authoritative: false.
1. Register the executors your deployment dispatches into
TargetProtocolRegistry maps each DiscoveryProtocol (Native, Http, Mcp, A2a, Acp, OpenAi) to a registered TargetProtocolExecutor. Build the orchestrator over your kernel with the executors and the protocol availability your deployment allows.
// The crate root re-exports nothing: import from the owning module.
use chio_cross_protocol::discovery::{DiscoveryProtocol, TargetProtocolRegistry};
use chio_cross_protocol::execution::OpenAiTargetExecutor;
use chio_cross_protocol::orchestrator::CrossProtocolOrchestrator;
use chio_cross_protocol::routing::RouteAvailabilityStatus;
use chio_kernel::ChioKernel;
// The registry borrows its executors for 'a, so the executor has to
// outlive the orchestrator. Own it in the caller, not in this function.
fn build_orchestrator<'a>(
kernel: &'a ChioKernel,
openai: &'a OpenAiTargetExecutor,
) -> CrossProtocolOrchestrator<'a> {
// 1. Register target executors for every non-native protocol this
// deployment is willing to dispatch into.
let registry = TargetProtocolRegistry::new(DiscoveryProtocol::Native)
.with_executor(openai);
// .with_executor(mcp_executor) // McpTargetExecutor, from chio-mcp-edge
// 2. Declare which protocol families are available right now. The
// route planner refuses to select an unavailable target and will
// attenuate to the native fallback where allowed.
CrossProtocolOrchestrator::new(kernel)
.with_registry(registry)
.with_protocol_availability(
DiscoveryProtocol::Native,
RouteAvailabilityStatus::available(),
)
.with_protocol_availability(
DiscoveryProtocol::OpenAi,
RouteAvailabilityStatus::available(),
)
.with_protocol_availability(
DiscoveryProtocol::A2a,
RouteAvailabilityStatus::unavailable("no a2a target executor exists"),
)
}You should see the orchestrator build against the owning modules, since the crate root re-exports nothing. Two executors exist, OpenAiTargetExecutor and McpTargetExecutor, so the reachable targets are Native, OpenAi, and Mcp; a request for Http, A2a, or Acp plans as unregistered. The registry resolves a tool's target from its x-chio-target-protocol schema extension, read from the input schema and then the output schema, with the configured default for tools that declare neither.
Before you begin
You need one agent that speaks the source protocol and one tool server that speaks the target, each already governed by its own edge: Wrap an MCP Server, Wrap an ACP Server, Govern OpenAI Tool Calls, and the A2A Adapter. A cross-protocol bridge uses those edges together.
2. Plan the route before dispatch
plan_authoritative_route takes a source protocol, a requested target, an optional GovernedTransactionIntent, and a map of RouteAvailabilityStatus values. It returns a RoutePlanningOutcome whose RouteSelectionDecision is Select, Attenuate, or Deny, captured as RouteSelectionEvidence.
use std::collections::BTreeMap;
use chio_cross_protocol::routing::{plan_authoritative_route, RouteSelectionDecision};
// GovernedTransactionIntent lives in chio-core-types, not in this crate.
use chio_core::capability::governance::GovernedTransactionIntent;
let mut availability = BTreeMap::new();
availability.insert(DiscoveryProtocol::Native, RouteAvailabilityStatus::available());
availability.insert(DiscoveryProtocol::OpenAi, RouteAvailabilityStatus::available());
let planning = plan_authoritative_route(
"req-01HXYZ", // origin request id
DiscoveryProtocol::A2a, // source protocol
DiscoveryProtocol::OpenAi, // requested target
governed_intent.as_ref(), // optional GovernedTransactionIntent
®istry,
&availability,
)?;
match planning.evidence.decision {
RouteSelectionDecision::Select => { /* dispatch to the requested target */ },
RouteSelectionDecision::Attenuate => { /* fell back to native or another target */ },
RouteSelectionDecision::Deny => { /* no available route; the deny is signed */ },
}You should see one decision with its evidence. route_selection_metadata is a free function that turns the evidence into the canonical {"route_selection": ...} object the orchestrator passes to the kernel as receipt metadata. Every candidate considered, with its availability and reason, ends up inside the signed receipt, so a denied route is as auditable as an allowed one.
Read more in Route Plans.
3. Read the envelope a bridged call returns
A bridged call returns a response envelope, the JSON OrchestratedToolCall::metadata() renders, which the edge attaches to its own protocol's metadata field. The ordinary ChioReceipt nests inside it at metadata.chio.receipt. This is the envelope from the hello-acp call over stdio, projected to its bridge lineage.
$ jq '.result.metadata.chio | {decision, capabilityId, authorityPath, authoritative,
$ bridge: {sourceProtocol: .bridge.sourceProtocol,
$ targetProtocol: .bridge.targetProtocol,
$ terminalProtocol: .bridge.terminalProtocol,
$ capabilityEnvelope: .bridge.capabilityEnvelope,
$ trace: .bridge.trace}}' invoke.json{
"decision": "allow",
"capabilityId": "cap-01a070d4-c5ea-7462-9df9-2a29fd157034",
"authorityPath": "cross_protocol_orchestrator",
"authoritative": true,
"bridge": {
"sourceProtocol": "acp",
"targetProtocol": "native",
"terminalProtocol": "native",
"capabilityEnvelope": {
"attenuatedScope": {
"grants": [
{
"operations": [
"invoke"
],
"server_id": "hello-acp-srv",
"tool_name": "hello_tool"
}
]
},
"bridgeId": "chio-bridge-acp-native-acp-request-hello_tool-1788599387",
"bridgedAt": 1788599387,
"capabilityRef": {
"chioCapabilityId": "cap-01a070d4-c5ea-7462-9df9-2a29fd157034",
"originProtocol": "acp",
"parentCapabilityHash": "3c0179f43e9f60499e57f696ccc02d9260edf694a934c87050e01ca8f2ee73b8",
"protocolContext": {
"capabilityId": "hello_tool"
}
},
"schema": "chio.cross-protocol-cap.v1",
"targetProtocol": "native"
},
"trace": {
"hops": [
{
"bridgeId": "chio-bridge-acp-native-acp-request-hello_tool-1788599387",
"protocol": "acp",
"requestId": "acp-request-hello_tool-1788599387",
"timestamp": 1788599387
},
{
"bridgeId": "chio-bridge-acp-native-acp-request-hello_tool-1788599387",
"protocol": "native",
"receiptId": "f571fe0bc2cd9764e4bc7b564efb1e0b1518feb2adea50553ab5b87fcfad7c39",
"requestId": "acp-hello_tool-1788599387",
"timestamp": 1788599387
}
],
"sessionFingerprint": "cc3241e0657582716c0c4bf696bb27e2bfe6c26119666656e1a9d90401941bb7",
"traceId": "9b405892116fcbd1d61a5b993f5ec94a5dad79c526008157ffc452a61e2866fa"
}
}
}You should see authorityPath: "cross_protocol_orchestrator" on the envelope, then four details:
- Envelope fields.
authorityPathandauthoritativelive on the envelope, which is camelCase because it is a protocol response; the receipt carries neither. - The scope inside.
attenuatedScopeserializes the kernel's ownChioScope, so its keys stay snake_case:grants,server_id,tool_name,operations. Emptyresource_grantsandprompt_grantsare skipped. - The parent.
capabilityRefis required and carries the parent capability's ID and hash, which ties the attenuated scope to the token it came from. - Bare hex. Every hash is lowercase hex with no
sha256:ored25519:prefix.traceIdandsessionFingerprintare 64 hex characters, and a capability ID iscap-followed by a UUIDv7.
The trace stitches the hops. Each names its protocol, request ID, and bridge ID, and the terminal hop adds the receiptId of the receipt it produced. An ACP call into a native target crosses one boundary, so this trace has two hops.
4. Match the route evidence to the signed receipt
The route evidence rides inside the receipt at metadata.route_selection, and the envelope repeats it at metadata.chio.routeSelection.
$ jq '.result.metadata.chio.routeSelection' invoke.json{
"candidates": [
{
"available": true,
"routeId": "native-route",
"selectedProtocols": [
"acp",
"native"
],
"targetProtocol": "native"
}
],
"decision": "select",
"requestedTargetProtocol": "native",
"routeSelectionId": "0ac9b0e99a5bff720313dc0ce1958094ea520a7410f1cef1ba5170f80bcaab93",
"selectedProtocols": [
"acp",
"native"
],
"selectedRouteId": "native-route",
"selectedTargetProtocol": "native",
"sourceProtocol": "acp"
}You should see one candidate, because candidates come from the requested target plus whatever a governed intent adds. routeId is built from the target alone (native-route, open_ai-route), and selectedProtocols is the whole hop chain: [source, native] for a native target, [source, target, native] for MCP or OpenAI.
$ jq '.result.metadata.chio.receipt | del(.metadata)' invoke.json{
"action": {
"parameter_hash": "c05f3d430e01e24c936243d1e2525b8077c5649863eba0384ca2d860922b24e3",
"parameters": {
"name": "world"
}
},
"boundary_class": "prevent",
"capability_id": "cap-01a070d4-c5ea-7462-9df9-2a29fd157034",
"content_hash": "98f164497bff463c9240fde7b03e123b0a54044d2e99fe898a95636244de4a43",
"decision": {
"verdict": "allow"
},
"id": "f571fe0bc2cd9764e4bc7b564efb1e0b1518feb2adea50553ab5b87fcfad7c39",
"kernel_key": "afcb9b875e04d449adb7d99962de26e27edc5f3d21efdc36e280fc740c336fdf",
"policy_hash": "hello-acp-policy",
"receipt_kind": "mediated_decision",
"redaction_mode": "none",
"signature": "7c2af0b9595ea7cc88b86a9631a4ef8f5616edb2c10867de715883dab497ae63c819fb5db5dbbff1a7b0a402d35e0a1f6671285b2591b42d72914d77b55bcd09",
"timestamp": 1788599387,
"tool_name": "hello_tool",
"tool_origin": "caller_executed",
"tool_server": "hello-acp-srv",
"trust_level": "mediated"
}The receipt is an ordinary ChioReceipt with no bridge fields; its ID is content-addressed, 64 hex characters. Its capability_id is the caller's own token ID, the same value as the envelope's capabilityId and capabilityRef.chioCapabilityId. The kernel evaluated that full parent token, not the envelope: the envelope reports the narrowed scope without being signed, and a scope refusal on a bridged call is the parent token's scope.
5. Verify the lineage from one response
The edge's response holds everything to check.
- The orchestrator decided the route.
metadata.chio.authorityPathiscross_protocol_orchestrator. Without it, the call came straight from an edge. - The scope fits inside the parent. Compare
bridge.capabilityEnvelope.attenuatedScope.grantswith the scope of the token named incapabilityRef.chioCapabilityId; an equal scope also passes. Check the signed parent token, and keep the envelope beside the receipt. - The evidence explains the target.
routeSelection.decisionisselectwhen you got the target you asked for;attenuatemeans the planner fell back, andreasonsays why. - The terminal hop names the signed receipt. The last entry in
bridge.trace.hopscarries areceiptIdequal tometadata.chio.receipt.id, and that receipt's signature checks against the kernel key.
You should see all four hold. The trace lives in the envelope and receipts are fetched by ID, so capture the envelope where your edge returns it, read each hop's receiptId, and fetch those receipts with the same tooling as any other.
Deployment code decides which routes exist; policy decides each call
A bridged call reaches the kernel as an ordinary tool invocation with extra metadata, so your HushSpec policy applies unchanged on the tool name and parameters it sees. Which protocol a call arrived over is settled before policy runs, by the registry and the availability map.
- Narrow targets per inbound caller. A public A2A endpoint might reach only
Native, an internal ACP sessionNativeorMcp. Set this with the availability map and the executors you register, since HushSpec has no rule that matches bridge metadata. - Require prior attenuation. Refuse to cross a bridge unless the inbound capability is already attenuated below the root delegation, so a compromised token never crosses a protocol boundary at full width. Combine with capability delegation.
- Disallow projected protocols. Set
context.chioControlPlane.disallowProjectedProtocolsto true in the governed intent's free-formcontext, besidepreferredTargetProtocolandallowNativeFallback. With a non-native target requested, the planner then attenuates toNativewhen a native route is available and denies otherwise, recording its reason either way. Use it for regulated workflows and evidence-only calls. - Gate publication on fidelity. A tool is published to a target protocol only when its
BridgeFidelityisLosslessorAdapted; the edge can refuseAdaptedwhen it builds its outward manifest.
Read more in Write a Policy.
Four inbound bridges share one crate
Six crates depend on chio-cross-protocol: chio-a2a-edge, chio-acp-edge, chio-acp-proxy, chio-mcp-edge, chio-openai-adapter, and chio-http-core. Four of them are inbound bridges, each governing a different caller vocabulary against the same kernel.
| Bridge | Crate | Governed calls | Dispatches into |
|---|---|---|---|
| A2A | chio-a2a-edge | message/send, message/stream, task/get, task/cancel | Native, MCP |
| ACP | chio-acp-edge, chio-acp-proxy | tool/invoke, tool/stream, tool/resume, tool/cancel | Native, MCP |
| OpenAI | chio-openai-adapter | tool_calls | Native |
| MCP | chio-mcp-edge | tools/call | Native |
The OpenAI bridge projects allowed calls as function_call_output items with a receipt_ref. The MCP edge also ships the McpTargetExecutor that lets another bridge dispatch into MCP, and projects nothing outward to A2A or ACP. HTTP rides as a transport under the MCP and OpenAI edges; to govern a REST API for agents, use Bridge OpenAPI to MCP or Protect an API.
Cross-protocol reference
Try CapabilityBridge, attenuatedScope, BridgeFidelity, x-chio-streaming, or InvalidAttenuation.
Orchestrator and shared types
The shared types live in chio-cross-protocol, and each edge crate uses them for provenance, attenuation, and receipt lineage. The orchestrator is a Rust API with no chio command of its own: a host reaches it by embedding the crate or by running an edge crate that does. CrossProtocolOrchestrator runs one bridged call in this order:
- Extract. Read a
CrossProtocolCapabilityReffrom the inbound envelope. - Attenuate. Build the
CrossProtocolCapabilityEnvelope, recording the source protocol, the target protocol, and the attenuated scope. - Plan. Call
plan_authoritative_route, the control-plane route planner, for signedRouteSelectionEvidence. Edge crates also call the planner directly in tests. - Execute. Hand a
CrossProtocolTargetRequestto the selected executor once the kernel allows the call. - Return. Produce an
OrchestratedToolCall, whosemetadata()renders the response envelope with itsCrossProtocolTraceContext.
To add a protocol edge, implement CapabilityBridge and optionally a TargetProtocolExecutor, and inherit the receipt envelope, trace context, and attenuation from the shared crate. CapabilityBridge supplies the protocol glue: extracting a CrossProtocolCapabilityRef from the inbound envelope, injecting it into the outbound one, and optionally surfacing protocol context such as A2A task IDs or ACP session IDs. Route planning and attenuation stay in the shared crate.
Envelope types
pub struct CrossProtocolCapabilityEnvelope {
pub schema: String,
pub capability_ref: CrossProtocolCapabilityRef,
pub target_protocol: DiscoveryProtocol,
pub attenuated_scope: ChioScope,
pub bridged_at: u64,
pub bridge_id: String,
}Route evidence types
pub struct RouteSelectionEvidence {
pub route_selection_id: String,
pub decision: RouteSelectionDecision,
pub source_protocol: DiscoveryProtocol,
pub requested_target_protocol: DiscoveryProtocol,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selected_route_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selected_target_protocol: Option<DiscoveryProtocol>,
pub selected_protocols: Vec<DiscoveryProtocol>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub governed_intent_id: Option<String>,
pub candidates: Vec<RouteCandidateEvidence>,
}pub struct RouteCandidateEvidence {
pub route_id: String,
pub target_protocol: DiscoveryProtocol,
pub selected_protocols: Vec<DiscoveryProtocol>,
pub available: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub availability_reason: Option<String>,
}Attenuation check
The orchestrator asserts that the attenuated scope is a subset of the inbound capability's scope and returns an InvalidAttenuation error before it calls the kernel when it is not. That is a Rust error with nothing signed, never a deny receipt. The built-in attenuator filters the parent's grants, so its result is a subset by construction, and a tool outside the parent's scope yields an empty scope. One grant where the parent had many is a common shape. The kernel then evaluates the caller's full token by the same scope matching every other call uses; the receipt signs its route-selection metadata, and the envelope is neither signed nor included wholesale in it.
Fidelity and publication
Some tools map onto a protocol with loss: a tool whose contract depends on streaming partial output has no faithful projection onto an A2A compatibility interface that returns only collated terminal payloads. The shared crate supplies BridgeFidelity and BridgeSemanticHints, and each edge crate classifies its tools against them.
Lossless. Projected with no loss of semantics; published by default.Adapted { caveats }. Published with caveats in the outward discovery metadata, for example "streaming output is collected into a final payload".Unsupported { reason }. Omitted from that target protocol's outward manifest and still reachable through its source protocol.
The inputs come from the tool's schema extensions: x-chio-publish, x-chio-approval-required, x-chio-streaming, x-chio-cancellation, x-chio-partial-output, and x-chio-target-protocol. Unset hints are inferred from the tool's latency hint: a moderate or slow tool is assumed to stream, a slow one to support cancellation, partial output follows streaming, publication defaults to true, and approval to false. Set the fields explicitly for tools with nontrivial lifecycle behavior. Bridges specifies each level.
Symptoms and fixes
- The imports do not resolve. Import from the owning module:
discovery,execution,orchestrator,routing,capability_bridge,semantic_hints.GovernedTransactionIntentlives inchio-core-types. - A borrow-checker error returning the orchestrator. The registry borrows its executors for the orchestrator's lifetime. Own executors in the caller and pass references in.
- The planner reports a target as unregistered. Supported targets are
Nativeplus the executors you registered. A2A and ACP are supported sources with no target executor. - An
InvalidAttenuationerror and no receipt. The attenuated scope was not a subset of the parent, and the orchestrator refused before the kernel. Only a custom attenuator reaches this. - A bridged call denies for scope. The kernel evaluated the parent token. Widen the parent grant or narrow the request; editing the attenuated scope changes the evidence, not the decision.
- The receipt has no bridge object. Bridge lineage lives on the response envelope; the receipt carries
metadata.route_selection. Keep the envelope for the trace. - A tool never appears on a target protocol's manifest. Its
BridgeFidelityclassified asUnsupported, a deliberate omission. Set thex-chio-*hints explicitly if the inference was wrong.