Chio/Docs
LOGIN · JOIN

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.

Guide chio@5f63e6e

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.

Start

A 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.

Protocol edgesChoose a route.
tools/callmessage/sendtool/invokeattenuatedsignsa1MCP clienta2A2A clienta3ACP clientd1MCP edged2A2A edged3ACP edgeσorchestratorKρreceipts1registered targets2tool serverattenuatedsignsa1MCP clienta2A2A clienta3ACP clientd1MCP edged2A2A edged3ACP edgeσorchestratorKρreceipts1registered targets2tool server

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.

Three edges over one kernel, with the route each protocol’s governed call takes. The passthrough is drawn from the ACP edge, and the A2A edge carries the same route; discovery, deferred task methods, and output checks after dispatch are left out.Listeners & Edges

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.

bridge_example.rsrust
// 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.

plan_route.rsrust
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
    &registry,
    &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.

The response envelope of one ACP-to-native calltranscript
$ 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"
    }
  }
}
exit 0

You should see authorityPath: "cross_protocol_orchestrator" on the envelope, then four details:

  • Envelope fields. authorityPath and authoritative live on the envelope, which is camelCase because it is a protocol response; the receipt carries neither.
  • The scope inside. attenuatedScope serializes the kernel's own ChioScope, so its keys stay snake_case: grants, server_id, tool_name, operations. Empty resource_grants and prompt_grants are skipped.
  • The parent. capabilityRef is 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: or ed25519: prefix. traceId and sessionFingerprint are 64 hex characters, and a capability ID is cap- 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.

Route selection evidencetranscript
$ 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"
}
exit 0

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.

The nested receipt, metadata droppedtranscript
$ 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"
}
exit 0allow

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.

  1. The orchestrator decided the route. metadata.chio.authorityPath is cross_protocol_orchestrator. Without it, the call came straight from an edge.
  2. The scope fits inside the parent. Compare bridge.capabilityEnvelope.attenuatedScope.grants with the scope of the token named in capabilityRef.chioCapabilityId; an equal scope also passes. Check the signed parent token, and keep the envelope beside the receipt.
  3. The evidence explains the target. routeSelection.decision is select when you got the target you asked for; attenuate means the planner fell back, and reason says why.
  4. The terminal hop names the signed receipt. The last entry in bridge.trace.hops carries a receiptId equal to metadata.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 session Native or Mcp. 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.disallowProjectedProtocols to true in the governed intent's free-form context, beside preferredTargetProtocol and allowNativeFallback. With a non-native target requested, the planner then attenuates to Native when 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 BridgeFidelity is Lossless or Adapted; the edge can refuse Adapted when 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.

BridgeCrateGoverned callsDispatches into
A2Achio-a2a-edgemessage/send, message/stream, task/get, task/cancelNative, MCP
ACPchio-acp-edge, chio-acp-proxytool/invoke, tool/stream, tool/resume, tool/cancelNative, MCP
OpenAIchio-openai-adaptertool_callsNative
MCPchio-mcp-edgetools/callNative

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

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:

  1. Extract. Read a CrossProtocolCapabilityRef from the inbound envelope.
  2. Attenuate. Build the CrossProtocolCapabilityEnvelope, recording the source protocol, the target protocol, and the attenuated scope.
  3. Plan. Call plan_authoritative_route, the control-plane route planner, for signed RouteSelectionEvidence. Edge crates also call the planner directly in tests.
  4. Execute. Hand a CrossProtocolTargetRequest to the selected executor once the kernel allows the call.
  5. Return. Produce an OrchestratedToolCall, whose metadata() renders the response envelope with its CrossProtocolTraceContext.

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

crates/protocol/chio-cross-protocol/src/capability_bridge.rs41-48rust
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

crates/protocol/chio-cross-protocol/src/routing.rs73-88rust
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>,
}
crates/protocol/chio-cross-protocol/src/routing.rs52-59rust
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. GovernedTransactionIntent lives in chio-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 Native plus the executors you registered. A2A and ACP are supported sources with no target executor.
  • An InvalidAttenuation error 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 BridgeFidelity classified as Unsupported, a deliberate omission. Set the x-chio-* hints explicitly if the inference was wrong.

Next: govern the edges you bridge