Model Cards
A signed model card bounds which weights a provider may serve and which scopes and tools its binding may request.
On this page
A capability constrains what an agent may do. A model card constrains which model may do it. The card is a signed declaration about one set of weights: their digest, the largest scope they are cleared for, and the tools they must never be bound to. A capability grants authority to a subject and says nothing about the model behind the agent that uses it; the card supplies that fact, and Chio refuses a provider binding whose loaded weights, requested scope, or tool set falls outside it.
Two checks give the card its meaning. The signature authenticates the declaration against a signer identity the operator pins. A digest recomputed from the loaded model bytes connects it to what the provider actually runs. The embedding must supply both checks; a card alone cannot establish which weights a remote service loaded.
A card is a signed ceiling on one set of weights
A model card constrains the provider binding behind an agent. Four fields supply the declaration used at binding:
| Field | What it declares |
|---|---|
weights_hash | The loaded weights this card describes. |
allowed_capability_set | The maximum scope the kernel may grant under this card. |
banned_tools | Tools that must not appear in the requested binding. |
training_data_class | A coarse data classification carried for operator compliance reasoning. |
Provenance and liveness fields, issuer, issued_at, and expires_at, round out the declaration. The schema example, formatted for reading, declares the SHA-256 of an empty blob; its signed form is one line of canonical JSON with sorted keys and no whitespace:
{
"allowed_capability_set": ["tool:read", "tool:write"],
"banned_tools": ["tool:exec"],
"card_version": "1",
"expires_at": "2026-05-30T12:00:00Z",
"issued_at": "2026-04-30T12:00:00Z",
"issuer": "https://example.com/issuer",
"training_data_class": "public-internet",
"weights_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}The card crate is chio-weights, at crates/trust/chio-weights in the repository. The kernel composes its types into the bind-time decision but does not own the schema.
The first failing gate determines the refusal
chio_kernel::weights_binding evaluates a WeightsBindingRequest against a cosign-verified card. It returns Ok(()) only when three checks pass in order. The provider’s loaded weights_hash must byte-equal the card’s. Each requested scope must be in the card’s allowed_capability_set. No requested tool may appear in the card’s banned_tools.
Gate 2 fails first: ScopeNotSubset, urn:chio:error:weights:scope-not-subset. Later gates are not evaluated.
In Rust · weights_binding.rs:7635 lines
pub fn evaluate_weights_binding(
card: &ModelCard,
request: &WeightsBindingRequest<'_>,
) -> Result<(), WeightsBindingError> {
// Gate 1: weights_hash byte-equality. Lowercase hex on both sides
// (the card schema enforces lowercase at validate; the provider
// normalises before passing in).
if card.weights_hash != request.loaded_weights_hash {
return Err(WeightsError::CardMismatch {
expected: card.weights_hash.clone(),
found: request.loaded_weights_hash.to_string(),
});
}
// Gate 2: scope subset containment. We surface the FIRST scope that
// fails so the error message is actionable.
for scope in request.requested_scopes.iter() {
if !card.allowed_capability_set.contains(scope) {
return Err(WeightsError::ScopeNotSubset {
scope: scope.to_string(),
});
}
}
// Gate 3: banned-tools intersection. Surface the FIRST overlap.
for tool in request.requested_tools.iter() {
if card.banned_tools.contains(tool) {
return Err(WeightsError::ToolBanned {
tool: tool.to_string(),
});
}
}
Ok(())
}
Each condition can hold or fail, so a request falls into one of eight rows. The first failing check stops evaluation and later gates are never read, which is why four of the eight rows report a weights mismatch: when the loaded weights differ from the card, the scope and tool checks do not run. The order is stable, so a request that fails several checks always reports the earliest. Within a gate, the kernel names the first scope or tool that fails, so the error points at something the integration can change.
An integration applies the decision when it binds the provider. The binding helper takes the provider’s own digest result, which hashes the loaded bytes or returns an error, rather than a hash string. A request for tool:exec under the example card stops at the third gate:
use chio_kernel::weights_binding::evaluate_weights_binding_with_loaded_hash;
// verified.card is the card verify_model_card_bundle returned.
evaluate_weights_binding_with_loaded_hash(
&verified.card,
provider.loaded_weights_hash(),
&requested_scopes,
&requested_tools,
)?;Error: ToolBanned { tool: "tool:exec" }Each gate maps to a stable URN for telemetry and audit records:
| Gate | Error | URN |
|---|---|---|
| Weights mismatch | CardMismatch | urn:chio:error:weights:card-mismatch |
| Scope not a subset | ScopeNotSubset | urn:chio:error:weights:scope-not-subset |
| Tool banned | ToolBanned | urn:chio:error:weights:tool-banned |
The gates run at bind time. An integration that applies this decision refuses a banned-tool binding before admitting any call under it, not at the first call. Call-time capability and policy checks remain separate.
Hosted weights fail closed. The provider-side loaded-weights helper recomputes the digest from model bytes, and the kernel binding helper compares that result with the card. A hosted or protocol adapter that cannot expose recomputable loaded bytes passes an error into the loaded-hash gate, and the kernel maps that unavailable state to a schema-level refusal. It does not accept a caller-supplied hash in its place. Required modes reject missing evidence.
The gates read a card only after its bundle verifies
Before the kernel trusts a card, verify_model_card_bundle verifies its cosign bundle. It wraps the existing Sigstore verifier in chio-attest-verify, so model cards use the same trust root, signature path, and bundle verifier as other Chio attestations, including its PQ-hybrid interface. A successful verification returns a VerifiedModelCard, which pairs the parsed card with the upstream VerifiedAttestation: the Fulcio certificate identity, OIDC issuer, Rekor log index, and signing time.
A valid signature from the wrong identity
The bundle verifies, but its certificate names ci@example.net, which the SAN pin does not admit. A signature establishes what a key signed; the pin decides whether that key speaks for the card.
In Rust · bundle.rs:7122 lines
pub fn verify_model_card_bundle<V: AttestVerifier + ?Sized>(
verifier: &V,
card_bytes: &[u8],
bundle_json: &[u8],
expected: &ExpectedIdentity,
now: DateTime<Utc>,
) -> Result<VerifiedModelCard, WeightsError> {
let attestation = verifier
.verify_bundle(card_bytes, bundle_json, expected)
.map_err(|err| WeightsError::BundleRejected(format!("{err}")))?;
let card = ModelCard::from_canonical_json(card_bytes)?;
if card.issuer != attestation.certificate_identity {
return Err(WeightsError::BundleRejected(format!(
"model card issuer {:?} does not match verified certificate identity {:?}",
card.issuer, attestation.certificate_identity
)));
}
card.require_live(now)?;
Ok(VerifiedModelCard { card, attestation })
}
verify_bundle checks the signature and both certificate pins in one call; the figure separates them. Sigstore's trust root and Rekor inclusion are checked inside that call and left out.An Ok result establishes five facts, in the order the verifier checks them: the bundle signs these bytes; the certificate identity matches the caller's expected identity and its OIDC issuer matches; the card decodes from its canonical bytes; the card's issuer equals the verified certificate identity; and its expires_at is strictly after the verifier's current time. An otherwise valid expired card returns urn:chio:error:weights:card-expired.
The signature covers the RFC 8785 canonical bytes, not a re-serialized object. A card presented in any other byte form fails verification even when every field value is correct, so store and transmit the exact bytes the issuer signed. The Sigstore trust root and confidential-compute context are covered in the TEE deployment guide.
Card schema and canonical encoding
Card schema
The v1 schema is at spec/schemas/model-card.v1.json. All fields are required and unknown fields are rejected. A decoded card must re-encode to the input bytes or validation rejects it.
| Field | Type | Meaning |
|---|---|---|
card_version | string | Schema version. Must be the literal "1" for v1 cards; any other value rejects at validation. |
weights_hash | string | Lowercase 64-character hexadecimal SHA-256 digest (^[0-9a-f]{64}$) of the canonical weights blob. |
allowed_capability_set | string set | Sorted unique scopes. Upper bound on the requested binding scope. The binding decision rejects a set that is not a subset. |
banned_tools | string set | Sorted unique tool identifiers the kernel must not route to. Any intersection with the requested set rejects at bind. |
training_data_class | string | Free-form data classification. Surfaced to operators; not interpreted by the kernel. |
issuer | string | Logical issuer identity (URI, OIDC subject, or SAN). Cosign verification pins this against the verified certificate identity. |
issued_at | RFC 3339 UTC | Timestamp at which the issuer minted the card. |
expires_at | RFC 3339 UTC | Expiry. Must be at or after issued_at. Bundle verification rejects expired cards. |
Canonical encoding
The byte form on the wire is RFC 8785 canonical JSON (JCS): object keys sorted by UTF-16 code units, string sets serialized as sorted unique arrays, timestamps as RFC 3339 UTC, and no whitespace between tokens. Two implementations produce the same bytes for the same card. The cosign signature covers those bytes, so a noncanonical re-encoding changes the digest and fails bundle validation.
Set fields are typed as StringSet, a sorted unique collection: covers tests subset containment and intersects tests overlap. Card validation rejects empty entries and surrounding whitespace. Deserialization rejects duplicate entries; StringSet::new instead sorts and deduplicates its input. Decoding through ModelCard::from_canonical_json parses the bytes, re-encodes them and byte-compares against the input, then validates the structure, so pretty-printed, key-reordered, or duplicate-key bytes do not decode as valid signed material.
Lineage anchors and error codes
Lineage anchoring
The anchor_model_card helper constructs a ModelCardLineageAnchor from verified card inputs; verify_model_card_anchor checks it. The anchor reuses the chio-lineage digest and signing-state shapes verbatim (schema chio.weights.lineage-anchor/v1), so an embedding can retain comparable records for lineage graphs and model cards. Constructing the anchor does not publish it to a registry. The digest binds the SHA-256 of the canonical card bytes together with the attestation’s subject digest, certificate identity, OIDC issuer, Rekor log index, and inclusion-verified flag.
A consumer reading an anchor recomputes the digest from the original inputs and rejects mismatches. These anchors do not construct a signed state themselves. An unavailable hybrid signature is recorded as UnsignedSoftDepAbsent, and an imported signed state is rejected until a local verifier can authenticate it against a trusted key.
Error codes
Each Err from the card API is typed and carries a stable URN, drawn from the registry in the schemas and errors reference.
| Variant | URN | Cause |
|---|---|---|
Input | urn:chio:error:attest:signed-json-noncanonical and the other signed-JSON rules | The bytes are not the card's canonical form, or fail untrusted-JSON parsing. |
Encoding | urn:chio:error:weights:internal-encoding | Canonical JSON encoding failed, or a lineage anchor could not be encoded. |
MissingField, SchemaRejected | urn:chio:error:weights:schema-rejected | A structural invariant failed, such as a weights_hash that is not 64 lowercase hex characters. |
Expired | urn:chio:error:weights:card-expired | The card expired before the verifier evaluated it. |
BundleRejected | urn:chio:error:weights:bundle-rejected | The cosign bundle verify path rejected the bundle. |
CardMismatch | urn:chio:error:weights:card-mismatch | The loaded weights_hash does not match the card. |
ScopeNotSubset | urn:chio:error:weights:scope-not-subset | A requested scope is not in allowed_capability_set. |
ToolBanned | urn:chio:error:weights:tool-banned | A requested tool is in banned_tools. |
A policy makes the card mandatory
The WeightsCardConfig type describes the enforcement contract an embedding can apply. Its mode is a snake_case value inside a configuration map, and the default is disabled.
| Mode | Behavior |
|---|---|
disabled | The default. The model-card contract adds no requirement to the binding. |
required | Require verified card evidence, matching loaded weights, admissible scopes, and no banned tools. |
required_with_pin | Everything required checks, plus the certificate SAN must match a configured issuer_san_regex. |
mode: required_with_pin
issuer_san_regex: 'https://issuer\.example\.com/.*'An embedding must deserialize this value, call validate(), configure its verifier, and apply the binding decision. Pin mode requires an issuer regex, and validation rejects an empty or malformed regex in every mode. Adding this map to an arbitrary policy file does not establish that the running integration consumes it.
Runtime health evaluation also exposes WeightsBindingMode and consumes card and loaded-hash evidence. Required modes report missing evidence or mismatches as binding failures. The integration must verify cryptographic provenance before it supplies that evidence; the health evaluator does not replace bundle verification.
The CLI inspects a card without touching a provider
The chio bind command loads a card, verifies the cosign bundle when one is supplied, and prints a binding summary. It does not connect to a provider, load model weights, or change a running kernel’s configuration. With no bundle, it checks the card's structure and reports that it skipped attestation:
$ chio bind srv-model --card ./model-card.jsonprovider: srv-model card: ./model-card.json card_version: 1 issuer: https://example.com/issuer weights_hash: 0000000000000000000000000000000000000000000000000000000000000001 allowed_capability_set: - tool:read - tool:write banned_tools: - tool:exec training_data_class: public-internet issued_at: 2026-04-30T12:00:00+00:00 expires_at: 2026-05-30T12:00:00+00:00 cosign: skipped weights_binding_mode: not_required
Before you promote a card to production policy, run the check the way the binding will: with the bundle, both issuer pins, and a required mode. Here your security team signed the card for the models provider:
chio bind models --card ./model-card.json --bundle ./model-card.bundle.json \
--issuer-san-regex '^security@larkfield\.example$' \
--issuer-oidc https://id.larkfield.example \
--weights-binding-mode required_with_pinprovider: models card: ./model-card.json card_version: 1 issuer: security@larkfield.example weights_hash: 7d1e…b3a0 allowed_capability_set: - tool:read - tool:write banned_tools: - tool:exec training_data_class: public-internet issued_at: 2026-10-01T09:00:00+00:00 expires_at: 2027-01-01T00:00:00+00:00 cosign: verified weights_binding_mode: required_with_pin
cosign now reads verified: the helper attested the card through chio-attest-verify against your SAN regex and OIDC issuer. Present the same bytes with a bundle that another identity signed, and the binding stops at the pin, however valid that signature is:
chio bind models --card ./model-card.json --bundle ./model-card.ci.bundle.json \
--issuer-san-regex '^security@larkfield\.example$' \
--issuer-oidc https://id.larkfield.example \
--weights-binding-mode required_with_pinerror[CHIO-WEIGHTS-BUNDLE-REJECTED]: cosign bundle verify failed: model card cosign bundle rejected: certificate identity does not match expected regexp help: Treat the card as untrusted; do not rebind without a fresh, correctly-signed cosign bundle. code: urn:chio:error:weights:bundle-rejected docs: https://chio.computer/docs/reference/errors#error-weights-bundle-rejected
weights_binding_mode echoes the value given to --weights-binding-mode, which accepts four values:
--weights-binding-mode | Meaning |
|---|---|
not_required | The default. |
required, required_with_pin | Reject the binding unless --bundle and the issuer pins are present. |
unavailable | An explicit hosted-provider failure state for health evaluation, not a schema error. |
A card the helper cannot read is refused before any of that, with the path it tried:
$ chio bind srv-model --card ./no-such-card.jsonerror[CHIO-CLI-INPUT-NOT-FOUND]: model card not found: ./no-such-card.json help: pass --card the path of a signed model card code: urn:chio:error:cli:input-not-found docs: https://chio.computer/docs/reference/errors#error-cli-input-not-found
Card-library errors use the urn:chio:error:weights:* namespace. A card the CLI cannot find reports urn:chio:error:cli:input-not-found, with the path in its message and the flag that fixes it in its help line. The CLI reference lists every flag.
A card constrains the provider behind the agent
Model cards let an operator make model selection part of an application’s operating rules. They add a provider-binding check to the assurance model. A capability constrains what an agent may request; a card constrains what the model behind the provider is cleared to serve. The card’s allowed_capability_set constrains the scope presented to the binding evaluator. It does not itself mint a capability or enforce every later call. The integration must preserve that boundary when it issues authority and routes work.