Probity ML-BOM (Execution-Grounded Model Bill-of-Materials), v1
predicateType: https://getprobity.dev/predicate/v1/ml-bom
An ML-BOM is a signed, offline-verifiable execution-grounded bill of the model weights a frozen microVM actually loaded. Unlike a declared software BOM (which lists what an artifact is supposed to contain), this records what a guest process demonstrably mapped into its address space at the moment the VM was frozen - the ACCESS proof - and resolves each mapped region to a full-file identity through host-side sources - the IDENTITY proof. The distinguishing property is that intent comes from the guest (a VMA walk of what the process mmap'd) while identity comes from a host layer the guest cannot forge (a dm-verity manifest, an egress-proxy stream hash, or a host-block read), so a guest cannot misrepresent which weights it ran.
An ML-BOM is consumed by the a2a-mesh-scorecard as the per-member
model_bom bound by digest. This document specifies the ML-BOM shape and the resolution rule; the
signing envelope is in signing-envelope.md. The Go shape this mirrors is the reference
implementation's ModelBOM, whose host-side identity sources are cited below.
The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in RFC 2119.
Subject
When an ML-BOM is the subject of its own attestation, the subject is the model surface it describes,
bound by the BOM's canonical SHA-256 digest. More commonly the ML-BOM is referenced by digest from
an a2a-mesh-scorecard member entry (model_bom_digest), and the full BOM is
served alongside, keyed by member_id and that digest.
"subject": [
{ "name": "<member-or-snapshot-label>", "digest": { "sha256": "<64-hex-model-bom-digest>" } }
]
BOM shape
{
"entries": [
{
"pid": 101,
"comm": "router-agent",
"vm_start": 140737488355328,
"vm_end": 140737505132544,
"inode": 524289,
"mapped_bytes": 16777216,
"full_file_size": 16777216,
"identity": {
"source": "host-block",
"algorithm": "sha256",
"digest": "<64-hex full-file digest>",
"full_size": 16777216,
"note": "full-file sha256 from host path /var/lib/.../model.safetensors"
}
}
],
"unresolved": [
{
"region": { "pid": 202, "inode": 524290, "vm_start": 0, "vm_end": 0 },
"reason": "verity-catalog: (s_dev, inode) is not a declared model file; host-block: no host path for inode 524290"
}
]
}
Field semantics
entries (REQUIRED, array)
Each entry is one file-backed region a guest process had mmap'd (the ACCESS proof from a VMA walk), resolved to a full-file identity (the WHAT). Fields:
-
Access proof (from the guest VMA walk):
pid(REQUIRED, integer) - the guest process id that mapped the region.comm(REQUIRED, string) - the process command name.vm_start/vm_end(REQUIRED, integers) - the mapped virtual-address range.inode(REQUIRED, integer) - the inode of the file backing the mapping.mapped_bytes(REQUIRED, integer) -vm_end - vm_start: how much of the file was mapped into the address space. This is NOT how much was resident in RAM, and NOT the identity basis - see the partial-residency rule.full_file_size(REQUIRED, integer) - the inodei_size: the size of the whole backing file, carried explicitly so a reader can contrast the full file against the mapped/resident bytes.
-
Identity proof (from a host-side source), under
identity:source(REQUIRED, string) - which host source resolved the identity and, for the verity path, the CONFIDENCE with which the guest device was paired to its signed lower (see the source rule). One ofverity-exact-root-digest,verity-heuristic-size,proxy-stream,host-block.algorithm(REQUIRED, string) - the digest algorithm (for examplesha256).digest(REQUIRED, 64-hex for sha256) - the model file's own full-file digest, lowercase hex. It is NEVER a hash of resident RAM, and NEVER the dm-verity root hash (the root hash identifies the LOWER and pairs the device; the identity digest is the per-file content hash).full_size(OPTIONAL, integer) - the number of bytes the digest covers (the full file), or 0 if unknown.note(OPTIONAL, string) - provenance detail (the model name, or the backing host path).copied_up(OPTIONAL, bool) - true when the model was copied up to the writable upper and re-attributed by host-side hashing (ahost-blocksource), bypassing the dm-verity block layer. A policy MUST treat acopied_upentry'shost-blocksource as authoritative over any run-level verity pairing confidence: its bytes were host-hashed, not verity-enforced.
unresolved (OPTIONAL, array)
The file-backed model-candidate regions for which no host source could establish a full-file
identity. Each entry carries the access proof (the region) and a reason aggregating why each
source declined. This array is the fail-loud record the partial-residency rule requires: an unresolved
region is recorded honestly, never silently dropped and never assigned a hash of resident RAM.
An absent unresolved field (or an empty array) means every candidate region resolved.
The host-resolution rule
A mapped region's identity is established by trying host-side sources in order; the first source that resolves wins. The rule is satisfied if ANY host source speaks for the region. The sources, in the order the reference implementation's host-identity resolver tries them:
-
verity-exact-root-digest/verity-heuristic-size(image-baked catalog) - the inode is a DECLARED model file on a dm-verity workload lower. Its identity is the model file's per-file SHA-256, computed by the builder from the root-hash-pinned uncompressed EROFS bytes and looked up O(1) by the VMI-recovered(s_dev, inode)key (PrecomputedVerityTable). The two forms differ ONLY in how the guest device was paired to its signed contract lower:verity-exact-root-digestwhen the enforced dm-verity root hash recovered from guest RAM (the dm-mapper walk) exactly matched the signed lower's root hash (cryptographically unambiguous),verity-heuristic-sizewhen that walk was unavailable and the device was paired by a bidirectional-unique data-region size (the honest-degrade fallback, which a policy may down-weight). Status: live. -
proxy-stream(download-time) - the egress proxy computed a streaming SHA-256 of the model file as it was downloaded in transit; the BOM consumes that per-inode (or per-URL) streaming hash. Status: interface seam only in v1 - the proxy lives on a separate boot/proxy effort; until that path is wired the proxy-stream source resolves nothing, and a region it would have covered falls through to the next source or tounresolved. -
host-block(host-view read) - a full-file SHA-256 read from the host's own view of the backing store (block device / overlay upperdir), fully independent of what is resident in guest RAM. It is also the source for a model copied up to the writable upper and re-attributed by digest (which setscopied_uptrue), bypassing the dm-verity block layer. The inode-to-host-path mapping is environment-specific (an overlay scan, a/proc/<vmm-pid>/mapscross-reference, or a known model-cache path). Status: live.
A source that cannot speak to an inode returns "unresolved" and the resolver tries the next source. A
source that hits a real read failure (for example a host-block read error) is surfaced LOUDLY as the
reason - it is never masked into a silent unresolved. A region no source resolves goes to unresolved.
INTENT-via-VMI, IDENTITY-via-host-layer (the grounding argument)
The security property an ML-BOM provides, and the reason it is execution-grounded rather than declared:
-
INTENT comes from the guest, via VM introspection (VMI). Walking the guest VMAs tells you which files a process actually mapped - not what a manifest claims it would use. A model file present on disk but never mapped does not appear; a file mapped under an unexpected process does. The BOM reflects the run, not the declaration.
-
IDENTITY comes from a host layer the guest cannot forge. The full-file digest is established by a host-side source (a verity-anchored per-file digest, an egress-proxy stream hash, or a host-block read), none of which the guest can tamper with from inside its own address space. So the BOM records WHAT was run with an identity basis a compromised guest cannot rewrite.
The two halves together defeat the failure mode of a declared BOM: a guest cannot claim to have loaded benign weights while running tampered ones, because the mapped inode's identity is read from outside the guest.
Partial-residency rule (honesty)
A mapped region is not necessarily fully resident in RAM, and the BOM never hashes resident RAM.
The identity digest is always a full-file digest from a host source. This is why mapped_bytes
and full_file_size are both carried and are explicitly distinguished from the digest: the digest
covers the whole file (or the verity-covered image), regardless of how many pages were resident at
freeze time. A region whose full-file identity no host source can establish is recorded in
unresolved with the access proof intact - the BOM fails loud on an unresolvable region rather than
substituting a partial or RAM-derived hash.
Digest-alignment stability contract (training-provenance join)
An entry's identity digest under algorithm: sha256 is the SHA-256 of the raw, uncompressed,
full-file bytes of the model weight file (a .gguf / .bin / .safetensors), taken in logical
byte order. It is NOT a hash of an OCI layer tarball, NOT a hash of a compressed blob, and NOT a
HuggingFace repo hash. This is the identical byte string an ML training or publishing pipeline hashes
when it records sha256(weight file), so the ML-BOM digest and the pipeline digest are equal by
construction. (In the Go shape this is ModelBOMEntry.Identity.Digest.)
For this equality to hold the weight file MUST be stored uncompressed in the substrate's EROFS
verity lower: a compressed extent would make the on-image bytes differ from the raw file bytes and
break the equality. The substrate builder already enforces this - the workload lowers are built
without compression and the extent reader fails closed on any compressed extent - so a conforming
producer never emits a sha256 identity digest computed over compressed bytes.
This equality is a stability contract, not an incidental implementation detail. It exists so a downstream training-provenance attestation - for example an in-toto attestation whose subject is the weight file keyed by its raw-file SHA-256 - can JOIN to this execution evidence purely by digest equality: the ML-BOM states "process P mapped a file whose raw-file SHA-256 is D at freeze time", the provenance attestation states "the file with raw-file SHA-256 D was trained by pipeline L", and a consumer chains "an agent ran model M" to "model M's training lineage is L" with no shared naming and no live environment. Changing the digest basis (hashing an OCI layer, a compressed blob, a repo manifest, or a Merkle root instead of the raw file) silently breaks that join and is therefore a breaking change to this predicate, held to the same bar as any change to the full-file-only identity rule.
The contract applies to EVERY entry carrying a full-file sha256 identity digest - which is every
verity-anchored entry (verity-exact-root-digest / verity-heuristic-size, whose identity digest is
the builder's per-file SHA-256 of the raw uncompressed EROFS-lower bytes, NOT the lower's root hash)
and every host-block entry. All are raw-file-SHA-256 join-ready. The dm-verity root hash pins the
lower's integrity and pairs the device to its signed lower (the verity-exact-/verity-heuristic-
distinction) but is never itself an entry's identity digest.
Verification (third-party, offline)
After the envelope checks in signing-envelope.md succeed (when the ML-BOM is a
standalone subject), or after resolving the BOM referenced by an
a2a-mesh-scorecard member's model_bom_digest:
- Confirm the served BOM canonically hashes (SHA-256 over sorted-key, compact-separator JSON) to the
bound
model_bom_digest. - For each entry, confirm the identity
digestis a full-file digest (itsalgorithmis declared and, forhost-block, thefull_sizematchesfull_file_size), and that thesourceis one of the three registered sources. - Confirm every
unresolvedentry carries its access proof and a non-emptyreason- an unresolved region MUST NOT be silently absent. - If the consumer has an auditor-approved set of model-file digests, compare each entry's identity digest against it (value-binding, as in the security-verdict).
No live execution environment is required.
What a verifier can and cannot independently check (honesty)
- A verifier CAN confirm the BOM is authentic and bound to its subject digest, that every resolved entry carries a full-file identity from a declared host source, and that unresolved regions are recorded rather than dropped.
- A verifier CANNOT, from the BOM alone, confirm that the VMA walk reflects the real guest address
space, nor that a
host-blockdigest was read from the genuine backing file. These are host self-assertions carried under the signature. The BOM's guarantee is integrity, provenance, subject binding, and the structural honesty rules (full-file-only identity, fail-loud unresolved) - not external re-execution of the VMI walk.
Why no perimeter sandbox can emit it (rationale)
A perimeter sandbox sees an application's API calls, not its address space. It cannot perform a VMA walk of what a guest process mmap'd (no VMI from outside the guest at the API layer), and it cannot resolve a mapped inode to a host-side full-file identity (no host view of the guest's backing store). The INTENT half requires VM introspection at the hypervisor boundary; the IDENTITY half requires a host layer below the guest. A competitor can copy the BOM JSON; it cannot fill an entry's access proof or a host-sourced full-file identity without the hypervisor-boundary substrate.
Status and maturity
- Format and the BOM shape: stable, real. The
entries/unresolvedstructure, the host-side resolution rule, the full-file-only identity rule, and the partial-residency honesty rule are implemented in the reference implementation's Go introspection layer. - Identity sources: verity catalog and host-block live, proxy a seam. The verity-anchored catalog
(
verity-exact-root-digest/verity-heuristic-size) andhost-blockare live resolvers;proxy-streamis an interface seam only in v1 (the egress-proxy path is a separate effort) and resolves nothing until wired. - VMI heap/region extraction maturity: partial. The VMA-walk access-proof half is groundwork; the
fail-loud
unresolvedrecording is the honest disclosure for regions the current extraction cannot resolve. A live ML-BOM from an N>=2 jailed mesh run is bound by an a2a-mesh-scorecard; the shipped scorecard now binds a real N=2 jailed run whose members carry an honestly-empty ModelBOM (model_bom.entries == null— the bench guests are static-Go init images with no model-weight regions), not synthetic illustrative entries.
Related standards
- CycloneDX - RELATED, not overlapping. CycloneDX describes what an artifact declares it contains (including its ML-BOM extension). This predicate describes what a frozen VM demonstrably mapped and ran, with identity grounded in a host layer. A CycloneDX BOM and an execution-grounded ML-BOM are complementary: the former is the declaration, the latter is the execution-time evidence.
- a2a-mesh-scorecard - the consumer that binds a per-member ML-BOM by digest.
- in-toto / DSSE
- the envelope; see signing-envelope.md.
Versioning
This is the ml-bom v1 predicate. Adding a new optional field - including an additive identity-source kind or an additive per-entry field - is additive and stays within v1; a verifier ignores fields it does not recognize. A rename or removal of a field, a change to the identity-resolution rule, or a change to the full-file-only identity rule is breaking and requires a new major version.
Changelog
- 2026-07-19 - ModelIdentity wire keys changed from PascalCase to snake_case (including
CopiedUp->copied_up) in the reference producer's host-identity resolver, changing the ModelBOM / EnrichmentDigest JCS digest basis. Pre-release and box-gated; no committed golden pinned the old shape.