BuildProtocols
Cross-Provider Policy
Compare normalized verdict bytes from one HushSpec policy replayed against eight provider fixtures.
Prerequisites
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
cargo run -p cross-provider-policy --quiet -- --dry-runThe --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:
{
"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-demoWalkthrough
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.
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: allowThe 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.
#[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.
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.
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.
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.
{
"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\"}"
}
]
}{
"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:
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.
cargo run -p cross-provider-policy --quiet -- --dry-runPer fixture (enforce_policy):
tool_name == required_tool(elseDemoError::ToolMismatch).arguments == required_arguments(elseDemoError::ArgumentsMismatch).verdict == expected_verdict(elseDemoError::VerdictMismatch).
Across fixtures (assert_receipt_equivalence):
- Canonical bytes of
body_without_provenancematch across all eight receipts. - Canonical bytes of the normalized
verdictmatch 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:
# 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-demoDecision rule
Why a Single Policy Works Across Providers
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
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. Addshell_commands,secret_patterns, oregressto 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
- Govern OpenAI Tool Calls · the in-process Rust adapter for OpenAI clients
- HushSpec reference · the policy language used here, in full
- LangChain and Provider SDKs · the runnable client-side examples this policy applies to