Chio/Docs

BuildProtocols

Cross-Provider Policy

Compare normalized verdict bytes from one HushSpec policy replayed against eight provider fixtures.

Prerequisites

Rust toolchain and Cargo. The example is offline-only: it reads fixtures under crates/protocol/chio-provider-conformance/fixtures/{openai,anthropic,bedrock,gemini,mistral,groq,ollama,cohere} and does not call a provider API. No provider or cloud credentials are required. See Installation if you have not built the workspace yet.

What it shows

The fixture test checks one property across eight provider shapes:

  • A HushSpec policy (policy.yaml) declares which tool is allowed and which arguments and verdict the fixture contract expects.
  • OpenAI, Anthropic, and Bedrock run through the deep adapter replay harness (replay_openai_fixture, replay_anthropic_fixture, replay_bedrock_fixture). Gemini, Mistral, Groq, Ollama, and Cohere use the NDJSON-capture path directly, which backs the same cross-provider verdict-equality oracle without a full adapter round-trip.
  • The example reads the kernel verdict record from each fixture, normalizes a receipt body, and asserts policy-id, scenario-id, tool-name, arguments, and verdict are byte-equal across all eight providers after canonical JSON normalization.
  • Provider-specific provenance ( provider, request_id, api_version, principal, received_at) is kept intact on the receipts, so audit can still trace which provider produced each call.

A Chio policy is provider-agnostic. The kernel evaluates the same tool_access rule regardless of whether the tool call arrived from a Chat Completions tool_calls entry, an Anthropic tool_use block, or a Bedrock toolUse content item. One policy, no per-provider duplication.


Run It

terminal
cargo run -p cross-provider-policy --quiet -- --dry-run

The --dry-run flag is required: the example refuses to start without it because there is no live-provider mode. The command prints each normalized receipt (pretty-printed), a provenance.provider line per receipt, and a single summary line:

output (truncated)
{
  "receipt_id": "rcpt_openai_basic_single_tool_call_allow",
  "body": { "policy_id": "cross-provider-policy-demo", ... }
}
provenance.provider: "open_ai"
{
  "receipt_id": "rcpt_anthropic_basic_single_tool_use_allow",
  "body": { "policy_id": "cross-provider-policy-demo", ... }
}
provenance.provider: "anthropic"
# ... bedrock, gemini, mistral, groq, ollama, cohere ...
cross-provider verdict equality: 8 receipts validated for policy cross-provider-policy-demo

Walkthrough

The Policy

The policy is a HushSpec document with a tool allowlist and an embedded fixture contract that pins the tool name, the exact arguments, and the expected verdict. The contract is what makes equality across providers checkable: each fixture must produce the same shape.

policy.yaml
hushspec: "0.1.0"
name: cross-provider-policy-demo
description: Dry-run policy proving equivalent tool verdicts across native provider adapters.

rules:
  tool_access:
    enabled: true
    default: block
    allow:
      - get_weather
  fixture_contract:
    scenario_id: weather_lookup_allow
    required_tool: get_weather
    required_arguments:
      location: "San Francisco, CA"
      unit: celsius
    expected_verdict: allow

The example loads and validates the policy at startup. If the required tool is not on the allow list, validation fails before any fixture is read.

Provider Routing

Eight fixture cases cover the eight providers. The fixture file under crates/protocol/chio-provider-conformance/fixtures/<provider> holds a provider-shaped capture. ProviderKind::OpenAi, Anthropic, and Bedrock route through a replay_* helper that drives the deep adapter; ProviderKind::Capture (Gemini, Mistral, Groq, Ollama, Cohere) skips the adapter and reads the kernel verdict straight out of the NDJSON capture, so replay_case returns None for those.

src/lib.rs
#[derive(Debug, Clone, Copy)]
struct ProviderCase {
    provider: &'static str,
    fixture_id: &'static str,
    kind: ProviderKind,
}

#[derive(Debug, Clone, Copy)]
enum ProviderKind {
    OpenAi,
    Anthropic,
    Bedrock,
    // Gemini, Mistral, Groq, Ollama, Cohere: NDJSON-capture path, no
    // deep adapter replay exposed on chio-provider-conformance.
    Capture,
}

fn provider_cases() -> [ProviderCase; 8] {
    [
        ProviderCase { provider: "openai",    fixture_id: "openai_basic_single_tool_call",     kind: ProviderKind::OpenAi },
        ProviderCase { provider: "anthropic", fixture_id: "anthropic_basic_single_tool_use",   kind: ProviderKind::Anthropic },
        ProviderCase { provider: "bedrock",   fixture_id: "bedrock_basic_single_tool_use",     kind: ProviderKind::Bedrock },
        ProviderCase { provider: "gemini",    fixture_id: "gemini_basic_single_function_call", kind: ProviderKind::Capture },
        ProviderCase { provider: "mistral",   fixture_id: "mistral_basic_single_tool_call",    kind: ProviderKind::Capture },
        ProviderCase { provider: "groq",      fixture_id: "groq_basic_single_tool_call",       kind: ProviderKind::Capture },
        ProviderCase { provider: "ollama",    fixture_id: "ollama_basic_single_tool_call",     kind: ProviderKind::Capture },
        ProviderCase { provider: "cohere",    fixture_id: "cohere_basic_single_tool_call",     kind: ProviderKind::Capture },
    ]
}

fn replay_case(kind: ProviderKind, path: &Path) -> Result<Option<ReplayOutcome>, ReplayError> {
    match kind {
        ProviderKind::OpenAi => replay_openai_fixture(path).map(Some),
        ProviderKind::Anthropic => replay_anthropic_fixture(path).map(Some),
        ProviderKind::Bedrock => replay_bedrock_fixture(path).map(Some),
        // Capture providers verify verdict bytes straight from the NDJSON
        // capture; there is no deep adapter replay to run.
        ProviderKind::Capture => Ok(None),
    }
}

Receipt Unification

For each fixture the example reads the single kernel verdict record (CaptureDirection::KernelVerdict), unwraps the ComparableInvocation from payload.invocation, and builds a normalized receipt body that decouples the policy view from the provider view.

src/lib.rs
Ok(DemoReceipt {
    receipt_id,
    body: ReceiptBody {
        policy_id: policy.name.clone(),
        scenario_id: policy.rules.fixture_contract.scenario_id.clone(),
        tool_name: invocation.tool_name,
        arguments: invocation.arguments,
        verdict: VerdictView {
            verdict,
            reason: record.payload.get("reason").cloned(),
            redactions: record.payload.get("redactions")
                .and_then(Value::as_array).cloned().unwrap_or_default(),
        },
        provenance: invocation.provenance,
    },
})

The receipt has two halves: a policy-shaped body (policy_id, scenario_id, tool_name, arguments, verdict) and a provider-shaped provenance view (ComparableProvenance). The equality check operates on the first half only.

Enforcing the Fixture Contract

Per fixture the example asserts the contract. Tool name, arguments, and verdict must each match the policy. Any mismatch is a hard error.

src/lib.rs
fn enforce_policy(policy: &DemoPolicy, receipt: &DemoReceipt) -> Result<(), DemoError> {
    let contract = &policy.rules.fixture_contract;
    if receipt.body.tool_name != contract.required_tool {
        return Err(DemoError::ToolMismatch { ... });
    }
    if receipt.body.arguments != contract.required_arguments {
        return Err(DemoError::ArgumentsMismatch { ... });
    }
    if receipt.body.verdict.verdict != contract.expected_verdict {
        return Err(DemoError::VerdictMismatch { ... });
    }
    Ok(())
}

Byte equality across providers

After all eight fixtures have been replayed and individually validated against the contract, the example does the cross- provider equality check. It strips the provenance field from each receipt body, canonicalizes the rest as JSON, and asserts each receipt produces identical bytes.

src/lib.rs
fn assert_receipt_equivalence(receipts: &[DemoReceipt]) -> Result<(), DemoError> {
    let Some(first) = receipts.first() else { return Ok(()); };
    let first_body = body_without_provenance(&first.body);
    let first_body_bytes = canonical_json_bytes_for("first receipt body", &first_body)?;
    let first_verdict_bytes = canonical_json_bytes_for("first normalized verdict", &first.body.verdict)?;

    for receipt in receipts.iter().skip(1) {
        let body = body_without_provenance(&receipt.body);
        let body_bytes = canonical_json_bytes_for("normalized receipt body", &body)?;
        assert_canonical_bytes_eq("receipt body excluding provenance", &first_body_bytes, &body_bytes)?;

        let verdict_bytes = canonical_json_bytes_for("normalized verdict", &receipt.body.verdict)?;
        assert_canonical_bytes_eq("normalized verdict", &first_verdict_bytes, &verdict_bytes)?;
    }
    Ok(())
}

The test compares the receipt body without provenance and the normalized verdict. Provenance differs by provider; the other compared fields must match.


Sample Fixture (OpenAI)

Each fixture is an NDJSON capture with three lines: upstream request, upstream response, and the kernel verdict the adapter produced. The OpenAI fixture for get_weather is the starting point of the equality chain.

crates/protocol/chio-provider-conformance/fixtures/openai/openai_basic_single_tool_call.ndjson (line 2: tool_call response)
{
  "id": "resp_openai_basic_single_tool_call",
  "object": "response",
  "output": [
    {"type": "message", "content": [{"type": "output_text", "text": "Checking the forecast."}]},
    {
      "type": "function_call",
      "call_id": "call_weather_1",
      "name": "get_weather",
      "arguments": "{\"location\":\"San Francisco, CA\",\"unit\":\"celsius\"}"
    }
  ]
}
same fixture, line 3: kernel_verdict
{
  "direction": "kernel_verdict",
  "provider": "openai",
  "invocation_id": "call_weather_1",
  "verdict": "allow",
  "receipt_id": "rcpt_openai_basic_single_tool_call_allow",
  "payload": {
    "invocation": {
      "provider": "open_ai",
      "tool_name": "get_weather",
      "arguments": {"location": "San Francisco, CA", "unit": "celsius"},
      "provenance": {
        "provider": "open_ai",
        "request_id": "call_weather_1",
        "api_version": "responses.2026-04-25",
        "principal": {"kind": "open_ai_org", "org_id": "org_chio_demo"},
        "received_at": "2026-04-25T00:00:01.150Z"
      }
    }
  }
}

The Anthropic and Bedrock fixtures wrap tool_use and toolUse content blocks, and the five capture-path providers (Gemini, Mistral, Groq, Ollama, Cohere) each carry their own request ids and principals. Across all eight the payload.invocation.tool_name and payload.invocation.arguments are identical. That is what the equality check rests on.


What the byte-equality check actually checks

The check operates on a stripped struct that drops the provenance fields. The five surviving fields are the policy-shaped half of the receipt:

examples/cross-provider-policy/src/lib.rs
fn body_without_provenance(body: &ReceiptBody) -> ReceiptBodyWithoutProvenance {
    ReceiptBodyWithoutProvenance {
        policy_id:   body.policy_id.clone(),
        scenario_id: body.scenario_id.clone(),
        tool_name:   body.tool_name.clone(),
        arguments:   body.arguments.clone(),
        verdict:     body.verdict.clone(),
    }
}

Fields normalized away (allowed to differ across providers): provenance.provider, provenance.request_id, provenance.api_version, provenance.principal, provenance.received_at, and the top-level receipt_id.

Fields that must match byte-for-byte after canonical JSON serialization (sorted keys, no whitespace): policy_id, scenario_id, tool_name, arguments, and the normalized verdict block (verdict, reason, redactions).


Smoke assertions

This example has no separate smoke.sh. The dry-run command executes its assertions in the binary.

terminal
cargo run -p cross-provider-policy --quiet -- --dry-run

Per fixture (enforce_policy):

  • tool_name == required_tool (else DemoError::ToolMismatch).
  • arguments == required_arguments (else DemoError::ArgumentsMismatch).
  • verdict == expected_verdict (else DemoError::VerdictMismatch).

Across fixtures (assert_receipt_equivalence):

  • Canonical bytes of body_without_provenance match across all eight receipts.
  • Canonical bytes of the normalized verdict match across all eight receipts.

Exit 0 indicates that the assertions passed; the final stdout line is cross-provider verdict equality: 8 receipts validated for policy cross-provider-policy-demo.

Inspect after

The binary prints one provenance.provider line per receipt, so the provider matrix is one grep away:

bash
# Capture the dry-run output
cargo run -p cross-provider-policy --quiet -- --dry-run \
  | tee cross-provider.out

# One provenance.provider line per receipt: eight distinct provenances
grep '^provenance.provider:' cross-provider.out | sort -u
# provenance.provider: "anthropic"
# provenance.provider: "bedrock"
# provenance.provider: "cohere"
# provenance.provider: "gemini"
# provenance.provider: "groq"
# provenance.provider: "mistral"
# provenance.provider: "ollama"
# provenance.provider: "open_ai"

grep -c '^provenance.provider:' cross-provider.out
# 8

# The summary line pins the policy and the receipt count
grep '^cross-provider verdict equality:' cross-provider.out
# cross-provider verdict equality: 8 receipts validated for policy cross-provider-policy-demo

Decision rule

Use this when: you ship a policy that must hold across multiple LLM providers and you want a repeatable proof that one HushSpec rule yields the same verdict and the same receipt body bytes for each. Don't use this if you only run one provider, or you want a live HTTP example: see Govern OpenAI Tool Calls for the in-process Rust path, or LangChain and Provider SDKs for the runnable client examples.

Why a Single Policy Works Across Providers

rendering…
Three deep adapters plus one NDJSON-capture path reduce eight provider request shapes to a ComparableInvocation; the kernel runs the same HushSpec policy against the result.

Each provider shape lands as a single ComparableInvocation with tool_name, arguments, and provenance — via a deep adapter for OpenAI, Anthropic, and Bedrock, and via the kernel-verdict capture for the other five. The kernel evaluates HushSpec against that normalized shape, not against the raw provider payload. Eight provider tool-call surfaces converge on one input format, which is why one allowlist applies to all of them.

The receipts then split back along the same line. The verdict and the policy-shaped body are uniform; the provenance keeps provider, request id, and principal so audit can answer which provider made the call without affecting whether the call was allowed.

One policy across model loops

Your agents can talk to OpenAI today and Anthropic tomorrow and Bedrock the day after, without rewriting policy. The same HushSpec rules that say get_weather is on the allowlist apply to providers that invoke it.

Extending the Example

Useful variants after the dry run passes:

  • More fixtures. The provider matrix is already at eight. Add fixtures for additional tool names and arguments across that set and assert the same policy denies the calls that should be denied, or deepen the five capture-path providers (Gemini, Mistral, Groq, Ollama, Cohere) into full adapter replays to match OpenAI, Anthropic, and Bedrock.
  • More guards. The policy here uses only tool_access. Add shell_commands, secret_patterns, or egress to test argument- shape rules across providers.
  • Live mode. Replace the fixture replay path with the in-process Rust adapters from Govern OpenAI Tool Calls and the equivalent Anthropic and Bedrock adapters; assert receipt equality the same way.

Next Steps