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
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
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 three normalized receipts and a single summary line:
{ "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-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: every 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 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.
#[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.
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(())
}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.
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.
{
"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 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:
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.
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 three receipts. - Canonical bytes of the normalized
verdictmatch 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
# 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
Why a Single Policy Works Across Providers
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
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. 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