Chio/Docs
LOGIN · JOIN

BuildConnect

Bridge Between Protocols

Bridge A2A, ACP, MCP, and OpenAI calls through one kernel while preserving capability scope and signed receipts.

Prerequisites

This is a Rust API, not a CLI surface: nothing under chio drives the orchestrator directly, so you reach it by embedding chio-cross-protocol or by running an edge crate that already does. You need at least one agent speaking the source protocol and one tool server speaking the target. You should already have read the relevant per-protocol guides: Wrap an MCP Server, Wrap an ACP Server, Govern OpenAI Tool Calls, and the A2A Adapter reference. Cross-protocol bridging uses those edges together.

Why Bridge Protocols

In a single-protocol deployment, traffic enters over MCP, the MCP server runs the tool call, and the kernel records one receipt. The capability chain and authority boundary remain local to that protocol.

Partner ecosystems may expose A2A skills, editor integrations may use ACP, and OpenAI-compatible models return tool_calls. When a call crosses a protocol boundary, avoid these failure modes:

  • Two independent governed sessions. One kernel on the inbound edge, another on the outbound edge, no shared capability chain. Receipts are signed on both sides but they do not reference each other. You cannot prove, post-hoc, that the A2A request and the MCP call were the same logical act.
  • An ad-hoc translator. A shim that rewrites envelopes between protocols without any policy or receipts of its own. The kernel sees two unrelated hops. Attenuation is not enforced. A capability can silently widen when it crosses the shim.
  • Protocol lock-in. Pick one protocol for your whole estate, refuse to integrate anything else, and lose access to the tooling gravity of the other ecosystems.

Cross-protocol bridging is Chio's fourth option. A single signed route through the kernel, with a CrossProtocolCapabilityEnvelope that records the source protocol, the target protocol, and the attenuated scope, and a CrossProtocolTraceContext that links receipts from each hop in one tree.


How the Shared Cross-Protocol Crate Works

Shared types live in chio-cross-protocol. Each edge crate uses them for provenance, attenuation, and receipt lineage instead of defining a different representation.

The main types are:

  • TargetProtocolRegistry. Maps each known DiscoveryProtocol (Native, Http, Mcp, A2a, Acp, OpenAi) to a registered TargetProtocolExecutor. Resolution is driven by the x-chio-target-protocol schema extension on the tool definition, read from the input schema and then the output schema, with a configured default for tools that declare neither. Two executors exist: OpenAiTargetExecutor and McpTargetExecutor. Since a target is supported only when it is Native or has a registered executor, the reachable targets are Native, OpenAi and Mcp. Ask for Http, A2a or Acp and the planner reports the target as unregistered.
  • plan_authoritative_route. The control-plane route planner. Given a source protocol, a requested target protocol, an optional GovernedTransactionIntent, and a map of RouteAvailabilityStatus values, it returns a RoutePlanningOutcome with a RouteSelectionDecision of Select, Attenuate, or Deny. The outcome is captured as signed RouteSelectionEvidence that rides in the receipt.
  • CrossProtocolOrchestrator. The runtime. It extracts a CrossProtocolCapabilityRef from the inbound envelope, builds the attenuated CrossProtocolCapabilityEnvelope, calls plan_authoritative_route, hands a CrossProtocolTargetRequest to the selected executor, and returns an OrchestratedToolCall. That value's metadata() renders the response envelope an edge attaches to its own protocol reply, and it is the envelope, not the receipt, that carries authorityPath: "cross_protocol_orchestrator".
rendering
The registry resolves an authoritative target protocol and the planner selects a route or attenuates to native. The kernel evaluates the caller's own capability with the route evidence attached, then the selected executor runs the call.

The kernel never sees two unrelated requests. It sees one evaluation whose route_selection metadata records the source protocol, the requested target, the selected target, and every candidate that was considered and rejected. If the route planner denies (for example, because a governed intent disallowed projected protocols and no native fallback existed), the receipt is a denial receipt with the reason populated; the target executor is never invoked.


The Four Bridges

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.

BridgeCrateDirectionWhat it governs
A2Achio-a2a-edgeInbound (A2A caller → native/MCP target)Claims on these endpoints: message/send, message/stream, task/get, task/cancel.
ACPchio-acp-edge, chio-acp-proxyInbound (ACP session → native/MCP target)These endpoints: tool/invoke, tool/stream, tool/resume, tool/cancel.
OpenAIchio-openai-adapterInbound (OpenAI tool_calls → native target)Projects allowed calls as function_call_output items with a receipt_ref for chain-of-custody.
MCPchio-mcp-edgeInbound (MCP tools/call → native target)Governs tools/call into a native target, and also ships the McpTargetExecutor that lets another bridge dispatch into MCP. It does not project outward to A2A or ACP.

HTTP is not a fifth bridge in its own right. It rides as a transport under the MCP and OpenAI edges. If you want to govern a REST API for agent consumption instead, see Bridge OpenAPI to MCP or Protect an API.

Shared types across protocol edges

All four bridges use the same cross-protocol types from chio-cross-protocol. To add a protocol edge, implement CapabilityBridge and optionally a TargetProtocolExecutor; you inherit the receipt envelope, the trace context, and the attenuation behavior from the shared crate.

Wire a Bridge

At the crate level, wiring a cross-protocol bridge has three moves: register the target executors your deployment should support, declare which routes are currently available, and invoke the authoritative route planner before executing the hop.

The following conceptual Rust sketch uses the public API in chio-cross-protocol. Exact wiring in a given edge crate adds a per-protocol CapabilityBridge implementation. See chio-a2a-edge, chio-acp-edge, and chio-openai-adapter for the production shapes.

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"),
        )
}

The planner is a standalone function you can call before dispatch to materialise the route decision and its evidence. Edge crates use this both inside the orchestrator and in tests:

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     => { /* ship it */ },
    RouteSelectionDecision::Attenuate  => { /* fell back to native or alt target */ },
    RouteSelectionDecision::Deny       => { /* policy said no path is safe */ },
}

route_selection_metadata is a free function, not a method: hand it the evidence and it returns the canonical {"route_selection": ...} object the orchestrator passes to the kernel as receipt metadata. That is the piece that makes a denied route as auditable as an allowed one: every candidate considered, with its availability status and reason, ends up inside the signed receipt.

The CapabilityBridge trait supplies the protocol-specific glue: extracting a CrossProtocolCapabilityRef from the inbound envelope, injecting it back out when building the outbound envelope, and optionally surfacing protocol context such as A2A task ids or ACP session ids. You do not implement route planning or attenuation there; the shared crate handles them.


Receipt Lineage Across a Bridge

A bridged call produces two nested things, and telling them apart is the whole of this section. The outer one is a response envelope: the JSON OrchestratedToolCall::metadata() renders, which an edge attaches to whatever its own protocol calls a metadata field. The inner one is an ordinary ChioReceipt, nested at metadata.chio.receipt. The envelope is camelCase because it is a protocol response; the receipt is snake_case because it is a receipt.

Here is the envelope from a real ACP-to-native call, projected to the bridge lineage. The example that produced it is examples/hello-acp, driven over stdio.

cross-protocol-bridge · envelopetranscript
$ 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

Read four things off that.

  • authorityPath and authoritative live here, on the envelope. They are not receipt fields, and a verifier looking for them inside the receipt will not find them.
  • attenuatedScope serializes the kernel's own ChioScope, which carries no case renaming, so its keys stay snake_case: grants, server_id, tool_name, operations. That is a camelCase envelope with a snake_case object inside it, and it is not a mistake. The empty resource_grants and prompt_grants vectors are absent rather than empty arrays, because both are skipped when empty.
  • capabilityRef is required on the envelope and carries the parent's id and a hash of the parent capability. It is what ties the attenuated scope to something.
  • Every hash is bare lowercase hex. There is no sha256: prefix on a fingerprint and no ed25519: prefix on a key or a signature. Ids are hashes too: traceId and sessionFingerprint are 64 hex characters, and a capability id is cap- followed by a UUIDv7.

The trace is the stitching. Each hop names its protocol, its request id and its bridge id, and the terminal hop additionally carries the receiptId of the receipt that hop produced. Two hops here, because an ACP call into a native target crosses one boundary.

These are the two envelope types, from the crate:

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,
}

The route-planning evidence is the other half. It rides inside the receipt, under metadata.route_selection, and the envelope surfaces the same object at metadata.chio.routeSelection for convenience:

cross-protocol-bridge · route-selectiontranscript
$ 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
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>,
}

Two details that surprise people. A routeId is built from the target alone, so it reads native-route or open_ai-route rather than naming the source. And selectedProtocols is the whole hop chain, not the target: a native target gives [source, native], while an MCP or OpenAI target gives [source, target, native].

The candidate list is also shorter than you might expect. Candidates are built from the requested target plus whatever a governed intent adds, so a plain request for one target evaluates exactly one candidate. A rejected alternative appears in the list only when something put it there.

And here is the receipt itself, pulled out of that same envelope with its metadata dropped. It is an ordinary ChioReceipt with no bridge fields of its own:

cross-protocol-bridge · receipttranscript
$ 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 id is content-addressed, 64 hex characters, not a ULID. The capability id is the caller's own token id, which is why the same value appears on the envelope, in capabilityRef.chioCapabilityId and on the receipt: one capability, three references to it.

Attenuation only narrows, and the check is the orchestrator's

The orchestrator asserts that the attenuated scope is a subset of the inbound capability's scope, and returns an error before it calls the kernel if it is not. That is a Rust error, not a deny receipt: the call is refused with nothing signed. The built-in attenuator filters the parent's grants, so the result is a subset by construction, and a tool outside the parent's scope produces an empty scope rather than a wider one.

What the kernel actually evaluates

The kernel is handed the caller's full capability token, not the attenuated envelope. The envelope is signed evidence of how the capability was narrowed for this hop, carried in receipt metadata; the authorization decision is made against the parent token's own scope, by the same scope matching every other call uses. If a bridged call is denied for scope, that is the parent token's scope talking.

Policy Patterns

Because the bridged call lands on the kernel as a normal tool invocation (with extra metadata), everything in your HushSpec policy still applies. But a few patterns are specific to cross-protocol routing and worth calling out.

  • Narrow the allowed target protocols per inbound caller. A public A2A endpoint should probably only reach Native targets. An internal ACP session might reach Native or Mcp, but never OpenAi. The control for this is the availability map you hand the orchestrator, plus which executors you register at all: a target with no registered executor plans as unavailable and never reaches an executor. HushSpec has no rule that matches on bridge metadata, so this is a deployment decision expressed in code, not a policy clause.
  • Require prior attenuation. Refuse to cross a bridge unless the inbound subject's capability has already been attenuated and is not the root delegation. This keeps the blast radius of a compromised token from crossing a protocol boundary untouched. Combine with capability delegation.
  • Disallow projected protocols. This is the one real kill-switch, and it lives in the governed intent's free-form context rather than as a typed field: set context.chioControlPlane.disallowProjectedProtocols to true. Its siblings in the same object are preferredTargetProtocol and allowNativeFallback. With it set and a non-native target requested, the planner Attenuates to Native when a native route is available and Denys otherwise, recording its reason in the evidence 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. Nothing in HushSpec reads that classification, so refusing Adapted bridges for workflows that cannot tolerate caveats is a decision the edge makes when it builds its outward manifest, not a rule you write in a policy file.

The mechanical rule is always the same: the kernel is the single decision point, and the cross-protocol envelope is evidence. A policy that does not want a call to happen denies it as it would any other, on the tool name and the parameters it can see. What the policy cannot do is reason about which protocol the call arrived over. That distinction is enforced upstream of it, by the registry and the availability map.


Fidelity and Truthful Publication

Not every tool maps cleanly into every protocol. A tool whose contract depends on streaming partial output cannot be faithfully projected onto an A2A compatibility interface that only returns collated terminal payloads. The shared crate supplies the vocabulary for saying so, BridgeFidelity and BridgeSemanticHints, and each edge crate does its own classifying against it:

  • Lossless: the tool can be 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 }: not published on that target protocol at all. The tool remains available through its source protocol and is omitted from the outward manifest for bridges that cannot represent it honestly.

The inputs to the fidelity decision come from the tool's x-chio-* schema extensions: x-chio-publish, x-chio-approval-required, x-chio-streaming, x-chio-cancellation, x-chio-partial-output, and x-chio-target-protocol. The hints that are unset are inferred from the tool's latency hint: a moderate or slow tool is assumed to stream, a slow one is assumed to support cancellation, partial output follows streaming, publication defaults to true and approval to false. Set these fields explicitly for tools with nontrivial lifecycle behavior rather than letting a latency hint decide.

One receipt lineage, and how to follow it

Whether a call entered as A2A, ACP, OpenAI tool_calls or MCP tools/call, every hop reaches the same kernel and produces a receipt you can verify with the same tooling, so audit, billing and incident response look the same on either side of a bridge. Following a trace takes one extra step, though: traceId lives in the response envelope, and chio receipt list has no trace filter. Capture the envelope where your edge returns it, read the receiptId off each hop, and fetch those receipts by id.


Verify the Result

There is no CLI that drives the orchestrator, so verification happens against the envelope your edge returns. Four checks, all readable from one response.

  1. metadata.chio.authorityPath is cross_protocol_orchestrator. That is the marker that the call went through the shared orchestrator rather than an edge dispatching on its own. If it is absent, you are looking at a direct edge call.
  2. The attenuated scope is narrower than the parent. Compare bridge.capabilityEnvelope.attenuatedScope.grants against the scope of the token whose id appears in capabilityRef.chioCapabilityId. One grant where the parent had many is the expected shape.
  3. The route evidence explains the target. routeSelection.decision should be select when you got what you asked for. An attenuate means the planner fell back, and the reason field says why.
  4. The terminal hop carries a receipt id, and that receipt verifies. Take bridge.trace.hops, find the last hop's receiptId, and check it against metadata.chio.receipt.id. The two agreeing is what makes the trace and the signature the same claim.

Failures and Recovery

What you seeWhat it means, and what to do
The imports do not resolveThe crate root re-exports nothing on purpose. Import from the owning module: discovery, execution, orchestrator, routing, capability_bridge, semantic_hints. GovernedTransactionIntent is not in this crate at all.
A borrow-checker error returning the orchestratorThe registry borrows its executors for the orchestrator's lifetime, so an executor constructed as a local cannot outlive the function that built it. Own executors in the caller and pass references in.
The planner reports a target as unregisteredOnly Native, plus whatever executors you registered, are supported targets. There is no A2A or ACP target executor to register, so those are always unavailable as targets even though both are supported as sources.
An InvalidAttenuation error and no receiptThe attenuated scope was not a subset of the parent capability. The orchestrator refuses before the kernel is called, so there is nothing signed to inspect. This is reachable only with a custom attenuator; the built-in one filters the parent's grants and cannot widen.
A bridged call denies for scopeThe kernel evaluated the caller's parent token, not the envelope. Widen the parent grant or narrow the request; editing the attenuated scope changes the evidence, not the decision.
The receipt has no bridge objectCorrect. Bridge lineage lives on the response envelope; the receipt carries only metadata.route_selection. Keep the envelope if you need the trace.
A tool never appears on a target protocol's manifestIts BridgeFidelity classified as Unsupported, which is a deliberate omission rather than a failure. The tool is still reachable through its source protocol. Set the x-chio-* hints explicitly if the inference was wrong.

Next Steps