Chio/Docs

Cross-Provider Policy

One Chio policy, three provider adapters, byte-equal verdicts. The example replays semantically equivalent OpenAI, Anthropic, and Bedrock tool-call fixtures through the native provider replay harness, asserts that the kernel produces the same tool_name, arguments, and verdict for each, and emits three normalized receipt bodies that compare canonical-byte-equal once provider provenance is stripped. It lives at examples/cross-provider-policy.

Prerequisites

Rust toolchain and Cargo. The example is offline-only: it reads fixtures under crates/chio-provider-conformance/fixtures/{openai,anthropic,bedrock} and never calls a provider API. No OpenAI, Anthropic, AWS, or STS credentials are required. See Installation if you have not built the workspace yet.

What It Shows

The example proves a single property under three provider shapes:

  • A HushSpec policy (policy.yaml) declares which tool is allowed and which arguments and verdict the fixture contract expects.
  • The native provider replay harness (replay_openai_fixture, replay_anthropic_fixture, replay_bedrock_fixture) drives each fixture through the kernel.
  • 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 three 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.

Why this matters: 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 three normalized receipts and a single summary line:

output (truncated)
{ "receipt_id": "...", "body": { "policy_id": "cross-provider-policy-demo", ... } }
{ "receipt_id": "...", "body": { "policy_id": "cross-provider-policy-demo", ... } }
{ "receipt_id": "...", "body": { "policy_id": "cross-provider-policy-demo", ... } }
cross-provider verdict equality: 3 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: every 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 Adapter Routing

Three fixture cases cover the three native provider adapters. The fixture file under crates/chio-provider-conformance/fixtures/<provider> contains a provider-shaped capture; the corresponding replay_* helper drives it through the matching adapter.

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

fn provider_cases() -> [ProviderCase; 3] {
    [
        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,
        },
    ]
}

fn replay_case(kind: ProviderKind, path: &Path) -> Result<ReplayOutcome, ReplayError> {
    match kind {
        ProviderKind::OpenAi => replay_openai_fixture(path),
        ProviderKind::Anthropic => replay_anthropic_fixture(path),
        ProviderKind::Bedrock => replay_bedrock_fixture(path),
    }
}

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/main.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/main.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(())
}

Canonical Byte-Equality Across Providers

After all three 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 every receipt produces identical bytes.

src/main.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(())
}

Two assertions, both byte-equal: the receipt body without provenance, and the normalized verdict. Provenance varies by construction; everything else must converge.


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/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 with different request ids and principals, but 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/main.rs:403
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

There is no separate smoke.sh for this example: the binary itself is the smoke. Every assertion runs inside the dry-run command.

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 three receipts.
  • Canonical bytes of the normalized verdict match across all three receipts.

Exit 0 means every assertion passed; the final stdout line is cross-provider verdict equality: 3 receipts validated for policy cross-provider-policy-demo.

Inspect after

bash
# Capture the three receipts emitted by the binary
cargo run -p cross-provider-policy --quiet -- --dry-run \
  | tee cross-provider.out

# Strip the summary line, parse the three receipts
grep '^{' cross-provider.out | jq -s '
  {
    count: length,
    policies: [.[].body.policy_id] | unique,
    tools: [.[].body.tool_name] | unique,
    verdicts: [.[].body.verdict.verdict] | unique,
    providers: [.[].body.provenance.provider] | unique
  }'
# expect:
# {
#   "count": 3,
#   "policies": ["cross-provider-policy-demo"],   # one policy
#   "tools":    ["get_weather"],                  # one tool
#   "verdicts": ["allow"],                        # one verdict
#   "providers": ["anthropic","bedrock","open_ai"]  # three provenances
# }

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…
Provider adapters reduce three different request shapes to a ComparableInvocation; the kernel runs the same HushSpec policy against the result.

Each adapter normalizes its provider-specific shape into a single ComparableInvocation that has tool_name, arguments, and provenance. The kernel evaluates HushSpec against that normalized shape, not against the raw provider payload. Three 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 enforcement surface, many 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 every provider that ever invokes it.

Extending the Example

Three useful variants once the dry run passes:

  • More fixtures. Add per-provider fixtures for additional tool names and arguments; assert that the same policy denies the calls that should be denied.
  • 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