Skip to content
New kind of attack — caught and fixed before it was publicly known·Read the timeline
Probity Open Predicate Standard
/predicate/v2/kernel-substrate
View raw .md →

Probity Kernel-Substrate Attestation, v2

predicateType: https://getprobity.dev/predicate/v2/kernel-substrate

A kernel-substrate attestation is a signed, offline-verifiable statement that a publisher approved exactly one set of guest-kernel bytes, identified by the immutable OCI manifest-index digest of the published kernel image, and that the publisher labels those bytes with a named kernel version. Where the verdict predicates in this standard describe what happened to an artifact that was run, this predicate describes the substrate the run happens on: the kernel image a runtime is permitted to boot a microVM with. It is the anchor the launch-chain attestation extends.

v2 is a different wire form, not a revision of the v1 one. The v1 predicate signs a bespoke three-member JSON object whose member declaration order is the format, under a private payload media type, with no _type, no in-toto subject, and no DSSE envelope; a v1 verifier reconstructs those bytes and compares them whole. A v2 statement is an in-toto Statement v1 canonicalized under RFC 8785 and carried in the DSSE envelope specified in signing-envelope.md; a v2 verifier checks the signature and then reads what it verified. Because the approved bytes move out of the predicate body and become the Statement subject, the predicate loses a member, and removing a member is breaking under this standard's own versioning rule. Hence a new URI rather than a revision.

The reference implementation is the engine's attestation package (statement construction, envelope construction, verification and decoding), its producer-side minting sibling, and the probity-attest command, which is the only producer of these bytes.

The key words MUST, MUST NOT, REQUIRED, SHOULD, MAY, and OPTIONAL are to be interpreted as in RFC 2119.

Subject

The subject is the approved guest-kernel image, by OCI manifest-index digest, carried in the in-toto subject array rather than inside the predicate.

"subject": [{"digest": {"sha256": "<64 lowercase hex>"}, "name": "guest-kernel"}]
  • The digest is the multi-platform manifest-index digest of the kernel image as published to its registry. It is deliberately the index digest and not a mutable registry tag: a force-pushed tag can substitute different bytes, and a digest cannot.
  • The digest value is bare lowercase hex with no sha256: prefix. That is the in-toto convention, and it is deliberately not the OCI form the pin manifests are written in. Normalizing between the two spellings is the single most likely implementation error in this format, which is why the reference implementation normalizes in one exported helper (attest.BareSHA256) rather than at each call site. Uppercase hex is rejected rather than folded to lowercase: the signed bytes carry one spelling, and quietly accepting a second would make two distinct statements compare equal.
  • name is REQUIRED, non-empty, and conventionally guest-kernel. It is inside the signed bytes and so must be deterministic, but it is never a binding input: a verifier binds on the digest map and MUST NOT bind on the name. The mutable registry tag is deliberately not used as the name, on the principle that a string a registry operator controls has no business inside a signature.
  • The array MUST be non-empty. In-toto permits several subjects; a verifier that reports which subject matched MUST report the one that actually carried the digest it bound, not whichever subject happens to be first.

Statement and canonicalization

The signed payload is the RFC 8785 canonical serialization of the Statement:

{"_type":"https://in-toto.io/Statement/v1","predicate":{"version":"6.1.137"},"predicateType":"https://getprobity.dev/predicate/v2/kernel-substrate","subject":[{"digest":{"sha256":"04d1...9c8d"},"name":"guest-kernel"}]}
  • Member order is RFC 8785 order, which is sorted by member name. At the Statement level that is _type, predicate, predicateType, subject. This is not the field order of any struct and nothing in the format depends on one. It is the observable difference from the v1 wire form, where the producer's declaration order was the format and predicateType came first; here it comes third because p sorts after _ and before s.
  • There is no insignificant whitespace, no indentation, and no trailing newline.
  • A producer MUST emit RFC 8785 canonical bytes. A verifier MUST NOT re-canonicalize the payload as part of verification and MUST NOT reconstruct the payload in order to compare it; see Verification.

The <, > and & prohibition of the v1 documents is retired here, and the retirement is a fix rather than a relaxation. It existed because Go's encoding/json rewrites those three characters as the six-character sequences \u003c, \u003e and \u0026 while other encoders emit them literally, so two conforming-looking producers disagreed on the signed bytes and a byte-equality verifier failed with no diagnostic pointing at the cause. RFC 8785 fixes the escaping: the transform decodes those escapes and re-emits the raw characters, so both encoders land on the same bytes and the divergence has no place left to occur. The prohibition was a producer rule standing in for a fix; the fix exists, so the rule is removed rather than restated. The reference implementation pins this with TestCanonicalBytesNeutralizeGoHTMLEscaping rather than assuming it.

Parse rules a verifier MUST apply to the verified bytes

These are checks on the already-verified payload, and every one of them is about two verifiers being unable to disagree about what a signed statement says:

  • Duplicate object members are rejected, anywhere in the document. Go's encoding/json silently takes the last occurrence of a repeated member and a strict parser elsewhere takes the first, so a payload carrying predicateType twice would mean different things to two conforming readers. Exploiting the ambiguity requires a compromised signer rather than a network attacker, and the fix is cheap.
  • Trailing data after the top-level value is rejected, and input holding no complete top-level value is rejected.
  • Nesting is capped at 128 levels. That is the same bound the producer's canonicalizer applies, and the agreement is the point: a payload a verifier accepts must be one the producer's canonicalizer could have produced. A conforming statement nests three levels.
  • Unknown members are ignored, never rejected. See Versioning; this tolerance is the reason the format moved off byte-equality at all. The reference implementation scans for duplicates with an iterative frame stack rather than a recursive walk, because the input is attacker-influenced and a Go stack overflow is a fatal error no deferred recover catches.

Predicate

{"version": "<kernel-version>"}
  • version (REQUIRED, string, non-empty after trimming) - the kernel version the publisher associates with the approved bytes, for example 6.1.137. It is a label under signature, not a derived fact. Nothing in the attestation, and nothing a verifier can do with it, establishes that the named version is what produced those bytes. It exists so the digest can be read against a human-meaningful release, and so the launch-chain statement can be tied to the same release.

The predicate carries the version and nothing else, which is the honest shape: the whole content of the statement is "the publisher approved these bytes", and the bytes are the subject. A producer MAY add optional members; a verifier MUST ignore members it does not recognize. The v1 form could not offer that, because any additional member changed the signed bytes and broke byte-equality verification outright.

Signature construction

The signature is a raw ed25519 signature over the DSSE Pre-Authentication Encoding (PAE) of the canonical Statement bytes, never over the bare payload.

PAE = "DSSEv1 " || LEN(payloadType) || " " || payloadType || " " || LEN(body) || " " || body

where LEN is the ASCII decimal byte length, body is the canonical Statement above, and

payloadType = "application/vnd.in-toto+json"   (28 bytes)

so a conforming pre-image begins DSSEv1 28 application/vnd.in-toto+json .

The domain separator moves in v2, and this is a real relocation of a security property rather than a cosmetic change. The v1 forms used a per-class private media type (application/vnd.probity.kernel-attestation.v1+json, 50 bytes, for this predicate; application/vnd.probity.launch-chain.v1+json, 44 bytes, for its companion), so separation between the two substrate classes lived in the PAE. Both v2 statements are signed under the single in-toto payload type, and separation between them now lives in predicateType, which a v2 verifier MUST check as a field. Under byte-equality it never had to be examined, because the type string sat inside bytes that were compared whole; here it is an explicit, mandatory check and a verifier that skips it accepts a launch-chain statement where it demanded a kernel approval.

Separation from every other Probity artifact class is unaffected and still lives in the payload type: a catch record is signed under its own payload type, a Statement under this one, and PAE's length prefixes make the pair injective, so neither signature verifies as the other.

A verifier MUST reconstruct the PAE with the payload-type constant above, never with the value read off the wire, even after confirming the two agree. A verifier that fed the wire value into its own reconstruction would have handed the attacker the choice of domain-separation label.

Envelope format and distribution

The statement is carried in the DSSE envelope specified in signing-envelope.md:

{
  "payload": "<base64(canonical Statement bytes)>",
  "payloadType": "application/vnd.in-toto+json",
  "signatures": [{"keyid": "<64 lowercase hex>", "sig": "<base64(raw 64-byte ed25519 signature over the PAE)>"}]
}
  • payloadType (REQUIRED) - MUST equal the constant above. A verifier MUST reject any other value before doing any signature work. This is the cheapest place to catch a cross-class replay: a genuine Probity-key envelope of another class, served at a substrate statement's address, carries a signature that is perfectly valid for its own (type, payload) pair. It would be caught later by the _type and predicateType checks, but there is no reason to let it get that far. This condition was unrepresentable in the v1 bundle, which carried no on-wire payload type to disagree with.
  • signatures (REQUIRED) - a non-empty array. Each entry MUST carry a keyid; an entry without one makes the envelope non-conforming.
  • keyid (REQUIRED) - the lowercase-hex SHA-256 of the RFC 7638 JWK thumbprint input for the ed25519 public key, per signing-envelope.md. It is excluded from the PAE and therefore unauthenticated, so it is a selector and nothing more: a verifier resolves it against configured anchors, independently re-derives it from the resolved key, and rejects when it does not resolve. It never fetches a key, never falls back to trial verification on an unresolvable id, and never treats a matching keyid as evidence of anything. The v1 bundle named no key at all, which is why "I do not trust this signer" and "this signature is forged" were indistinguishable there and are distinct findings here.
  • A producer MUST derive the keyid from the public key rather than accepting one from a caller. A hand-passed keyid can drift from the key a verifier actually holds, and deriving it means a CI step can assemble the envelope from the committed public half while the private half never reaches that code.

A consumer MUST cap the envelope bytes it will consider. The reference implementation caps at 1 MiB, matching the cap the v1 read paths already applied, so a file one rail will assemble cannot be one the other refuses for size alone. A conforming statement for this predicate is a few hundred bytes.

Distribution for the v2 form is not yet fixed by the reference implementation. The v1 bundles are published as one-layer OCI artifacts at a deterministic attestation-<digest-hex> tag in the same private registry as the kernel, under the same authentication, so the attestation is exactly as private as the image. No publishing path for a v2 envelope exists in the reference implementation today, so this document specifies the artifact and not its address. A publisher SHOULD make the envelope resolvable from the pinned kernel digest alone, with no out-of-band tag lookup, and SHOULD keep it under the same authentication as the image it describes.

Trust root

Verification is against a configured set of ed25519 public keys, indexed by the keyid derived from each key. Indexing by the derived value is what makes "resolve the keyid, then independently re-derive it from the resolved key and confirm it matches" hold by construction: there is no path that resolves a key under an id the key does not hash to.

The set is supplied as configuration. An implementation MAY ship one key as a convenient default, and the reference implementation embeds one in PKIX/PEM form in the verifying binary (pkg/kernel/keys/probity.pub), but nothing in the format names or assumes a particular key. An empty anchor set is a configuration mistake and MUST be refused at construction rather than silently rejecting every envelope for a reason unrelated to the envelope.

Multiple signature entries are permitted and are the rotation path. An entry whose keyid does not resolve to a configured anchor is skipped rather than fatal, so an envelope carrying an old and a new signature does not brick a verifier configured with only one of them; an entry whose sig is not decodable base64 is likewise skipped. If nothing resolved, the failure is "untrusted signer"; if something resolved but nothing verified, the failure is a tamper signal. These MUST be distinct outcomes.

The key is cosign-compatible, so a third party with the public key can verify with no Probity code:

cosign verify-blob --key probity.pub --signature <sig-file> <pae-file>

where <pae-file> holds the PAE bytes reconstructed per the construction above and <sig-file> holds the base64 sig from the envelope. The producer signs the same <pae-file> with cosign sign-blob --yes --key env://<secret> --tlog-upload=false, so the in-process verifier and the CLI verify over identical bytes. --tlog-upload=false is load-bearing: no entry is written to a public transparency log, which keeps the attestation as private as the artifact and means there is no third-party inclusion proof to appeal to.

Anchor lifecycle (reference implementation)

The reference implementation's "configured set" above is not a bare list of keys: each embedded key carries a declared role, status (active / retired / revoked), a not_before instant, an optional not_after instant, and the list of predicate types it is authorized to act for (pkg/kernel/keys/anchors.json, loaded and validated by pkg/kernel/keys). This generalizes the bare keyid-indexed anchor set above with a lifecycle a deployment can reason about, and is required because one key in the reference deployment signs more than one predicate class (both versions of both kernel-substrate and launch-chain) — authorized_predicates is the field that keeps that scoped rather than implicit.

The manifest never gets to ASSERT a key's identity. Each entry names a key file; the loader independently re-derives the keyid from the referenced key's own bytes and refuses to start if the declared key_id does not match what the key actually hashes to (ErrAnchorKeyIDMismatch). There is no code path on which a hand-edited or mismatched key_id is trusted.

The status/window policy a verifier applies is deliberately asymmetric, and the asymmetry is the point, not an oversight:

  • status: revoked is excluded unconditionally, including at boot, with no carve-out. A revocation only takes effect once a verifier's own configured anchor set is rebuilt (for the reference deployment, when a new binary ships) — it cannot reach a verifier that never re-reads the manifest, which is the same "revocation is a distribution problem, not a decree" property every other section of this spec that touches revocation already states.
  • not_before is boot-enforced: an anchor is not valid before its declared start.
  • not_after is deliberately NOT boot-enforced. A signature minted while a key was valid remains a valid historical approval after that key's window closes; hard-failing it at boot would convert an ordinary key-lifecycle event (a window elapsing on a calendar date) into a fleet-wide boot outage on that date. not_after is enforced at the CI signing gate (a producer MUST NOT mint a new statement under an anchor whose window has closed) and MAY be enforced by a non-boot verifier that chooses to check it.

A validity window is evaluated against a trusted timestamp when one is present on the envelope (see the timestamping extension this predicate's Bundle carrier supports) and against the verifier's own clock otherwise; a verifier MUST report which of the two it used, since a window judged against an untrusted local clock is a materially weaker claim than one judged against a trusted time source.

Verification (third-party, offline)

The v2 procedure is verify-then-read, and the ordering is the substantive change from v1. A v1 verifier reconstructs the signed bytes from values it already holds and compares them whole, so binding happens before the signature check. A v2 verifier never reconstructs anything; it checks the signature and then reads the bytes that verified, and binding is a comparison after the signature check. Both orderings have exactly one chokepoint and differ only in which side of the signature check it sits on. What must never happen, under either, is a path that returns "verified" while the attested value differs from the value the verifier pinned.

  1. Cap and parse the envelope. Reject anything over the byte cap and anything that is not a DSSE envelope.
  2. Assert payloadType equals application/vnd.in-toto+json. Reject before any signature work.
  3. Assert signatures is non-empty and every entry carries a keyid.
  4. Base64-decode payload.
  5. Resolve each entry's keyid against the configured anchors and verify the entry's signature as raw ed25519 over PAE("application/vnd.in-toto+json", payload), reconstructed from the constant. Skip entries that do not resolve or whose signature is not decodable base64. Report "no configured anchor was named" and "an anchor was named but nothing verified" as different failures.
  6. Parse the decoded bytes, applying the parse rules above (duplicate members, trailing data, depth bound), and assert _type is https://in-toto.io/Statement/v1, predicateType is exactly https://getprobity.dev/predicate/v2/kernel-substrate, subject is non-empty, and predicate is present. A verifier MUST demand a predicate type explicitly; accepting whatever class it is handed is a caller defect that is silent at runtime.
  7. Bind, and this step is MANDATORY. Compare the subject digest against the kernel digest the verifier independently intends to trust, and the predicate version against the version it intends to trust. Steps 1 to 6 establish that the bytes are authentic, that they are an in-toto Statement of the demanded class, and nothing whatsoever about which artifact the statement is about. Skipping the comparison leaves a verifier holding a good signature over something else, which is the whole shape of a downgrade attack. Report a binding mismatch with a distinct sentinel from a signature failure: "signed by someone I trust, about something else" and "forged" are different facts.

No network access, no registry, and no live execution environment is required once the envelope and the key are in hand.

CANON-EQ, and why the boot verifier omits it

A payload can be signed and yet not be RFC 8785 canonical, if a producer is defective. Asserting that the verified payload is already canonical (re-canonicalizing it and requiring equality) is called CANON-EQ here, and where it runs is a deliberate asymmetry:

  • A boot-critical verifier deliberately does NOT run it. Enforcing it there would import a JSON canonicalizer, and its unbounded recursive-descent parser, into a boot path in order to defend against a defect that originates in the producer. The reference implementation keeps the canonicalizer out of the verifier's import graph entirely by splitting the producer into a separate package, and pins the split with a test, because a Go import is resolved per package and a single shared package would silently undo it.
  • A producer, a CI promotion gate, an auditor, and any third-party verifier that is not boot-critical SHOULD run it. The reference probity-attest verify runs it and refuses a payload that fails, and the producer refuses to sign non-canonical bytes in the first place.

The honest cost is that a producer bug emitting non-canonical-but-signed bytes would reach a boot path; the mitigation is that the promotion gate will not publish it, and that the mandatory subject binding contains what it could do if it did.

Enforcement posture

The v2 form is not on any enforcement path in the reference implementation. Nothing in the boot path calls the v2 verifier; the v1 bespoke bundle path is untouched and is still the only thing that runs there. Consequently:

  • No environment variable governs the v2 form. The two named in the v1 documents, PROBITY_DEV_ALLOW_UNATTESTED_KERNEL and PROBITY_REQUIRE_LAUNCH_CHAIN, govern the v1 kernel package alone and have no effect on a v2 statement. This document names no others, because none exist in current source.
  • The v1 enforcement posture is unchanged and remains live. Kernel-substrate enforcement is mandatory by default there, a present-but-invalid bundle is an unconditional hard failure, and the single narrow escape covers an absent bundle only. See the v1 document.

A consumer implementing this specification independently SHOULD adopt the same shape when it does enforce: default-deny on absence, unconditional-deny on a present-but-invalid envelope, and any relaxation named explicitly rather than folded into a general warn-only mode.

Cached copies deserve one note, because the failure they prevent is quiet. A locally cached envelope MUST be keyed by the kernel digest it binds, not by the version. A republished kernel keeps its version and mints a new digest, so a version-keyed cache would silently reuse the previous digest's statement; a digest-keyed cache makes a digest change a cache miss by construction.

What this predicate does NOT assert (coverage)

The coverage statement is part of the contract. A conforming kernel-substrate attestation says nothing about any of the following, and a consumer that reads any of them into it has over-read the statement:

  • That the named version produced these bytes. version is an unverified label.
  • Anything about the rest of the boot. The initrd, the kernel command line, and the dm-verity root hashes are outside this statement entirely; they are the launch-chain predicate's subject. An approved kernel booted with a substituted command line is fully consistent with a valid kernel-substrate attestation.
  • Anything about how the kernel was built. There is no build provenance here: no source revision, no builder identity, no toolchain, no reproducibility claim. This is an approval statement, not a SLSA provenance statement.
  • Anything about the kernel's behaviour, configuration, or security properties. The statement is that the publisher approved these bytes, not that the bytes are good.
  • That the subject name identifies anything. It is inside the signature and it is not a binding input; a verifier that dispatches on it has bound on a string instead of on a digest.
  • Public discoverability or non-repudiation via a transparency log. Signing deliberately does not upload to a public log, so there is no third-party inclusion proof and no independent timestamp.
  • Revocation of a key. Revocation takes effect only when a verifier's configured anchor set is updated — for the reference deployment, when a new binary ships. A revoked key does not stop verifying at deployed verifiers that have not been updated; a consumer that needs faster revocation obtains it out of band.
  • Expiry of the statement. The statement itself never expires. An anchor's not_after (see Trust root, above) bounds when that key may SIGN; it does not bound how long a statement it already signed remains readable or how long its signature remains valid to check.
  • That the publisher's key is the right key. Key identity is a trust-anchor configuration decision. The keyid selects among keys already trusted; it establishes nothing on its own.
  • That the payload was canonical, unless the verifier ran CANON-EQ. A boot-critical verifier deliberately does not.

What a verifier can and cannot independently check (honesty)

  • A verifier CAN confirm the envelope is authentic under a configured anchor, which anchor admitted it, that the payload is an in-toto Statement v1 of exactly this predicate type, and that it binds exactly the kernel digest and version the verifier intended. All of that is checkable offline from the envelope and the public key alone.
  • A verifier CANNOT, from the statement alone, confirm that the digest names a kernel with any particular property, that the version string is accurate, that the approval decision behind the signature was sound, that a more recent approval has superseded this one, or that the signing key is the one the publisher intends to be trusted today. The guarantee is integrity, provenance under a named key, and exact subject binding - not a judgement about the artifact.

Status and maturity

  • Format, canonicalization, signing construction, and verification: implemented. One implementation produces the canonical bytes and one verifies them, and they are the same packages, so a producer and a consumer cannot diverge by construction. Committed cross-language conformance vectors carry the public key, the payload type, the canonical bytes, the PAE pre-image, the signature and the assembled envelope for each case, so a verifier in another language needs that file and nothing else.
  • Producer: a command exists; no CI step runs it. probity-attest builds the statement, emits the PAE for cosign sign-blob, assembles the envelope from the committed public key, and verifies totally. No workflow in the reference implementation mints or publishes a v2 envelope yet, and no distribution address is defined. This is stated plainly rather than left to be inferred from an empty registry tag.
  • Consumer: not wired. No boot path calls the v2 verifier; the v1 path is still the enforcing one.
  • Trust model: configured anchor set, single static key in the reference deployment. Multi-anchor rotation is supported by the format and by the verifier's skip-unresolved behaviour; a keyless or certificate-based model is not part of this predicate.

Relationship to the v1 wire form

The v1 predicate remains normative for the v1 URI, and bundles already minted under .../predicate/v1/kernel-substrate keep addressing it. A verifier presented with a v1 bundle MUST apply the v1 rules - reconstruct the bespoke payload and compare bytes - rather than in-toto Statement parsing. A consumer dispatches on the full predicateType string, so the two never collide, and a v1 bundle carries no payloadType member for a v2 verifier to accept in the first place.

Every difference, in one place:

v1v2
Containerbespoke {payload, signature} JSON objectDSSE envelope {payload, payloadType, signatures[]}
On-wire payload typeabsentapplication/vnd.in-toto+json, checked before any signature work
Signed payloadbespoke 3-member object, no _typein-toto Statement v1
Approved digestsubjectDigest member inside the payload, OCI sha256: formin-toto subject[].digest.sha256, bare hex
Predicate body(none; the payload is the predicate){"version": ...}
Canonicalizationproducer's field declaration orderRFC 8785, sorted member names
predicateType positionfirstthird (_type, predicate, predicateType, subject)
Key identity on the wirenonemandatory keyid, RFC 7638 thumbprint, selector only
Domain separation, class to classprivate per-class payload typepredicateType, checked as a field
Verification orderreconstruct, compare bytes, then check signaturecheck signature, then read, then bind
Unknown membersforbidden; any extra member breaks verificationMUST be ignored
Duplicate membersunreachable (bytes compared whole)explicitly rejected
<, >, & in string valuesprohibited by producer rulepermitted; RFC 8785 fixes the escaping
Multiple signersnot representablepermitted; unresolved keyids skipped
Untrusted-signer vs forgedindistinguishabledistinct failures
Enforcementlive, fail-closed by defaultnot wired to any enforcement path

Related standards

  • in-toto attestation - USED. The payload is an in-toto Statement v1 and the subject convention (bare-hex digest map, name never a binding input) is in-toto's.
  • DSSE - USED, envelope and PAE both, unlike the v1 form which borrowed the PAE alone.
  • RFC 8785 (JCS) - USED as the canonicalization, which is what retires the v1 escaping prohibition.
  • RFC 7638 (JWK thumbprint) - USED as the keyid derivation; see signing-envelope.md.
  • Sigstore / cosign - PARTIALLY ALIGNED. Static-key cosign sign-blob / verify-blob over the PAE bytes, deliberately without Fulcio certificates and without transparency-log upload.
  • SLSA provenance - RELATED, not overlapping. Provenance describes how an artifact was built; this predicate records that a publisher approved specific bytes.
  • launch-chain - the companion predicate that binds the rest of the boot pre-image. This one is its anchor, and in v2 that relationship is cryptographic: the launch-chain statement's subject is this statement's subject.
  • signing-envelope.md - the envelope this predicate rides, and which its v1 form deliberately did not.

Versioning

The versioning rules in the standard's README apply to this predicate in full, and they could not apply to its v1 form:

  • Major version in the URI. A consumer dispatches on the full predicateType string and MUST reject a type it does not recognize.
  • Additive-optional members are a minor change and stay within the major version. A producer MAY add optional members to the predicate or to the Statement, and a verifier MUST ignore what it does not recognize. This works here precisely because verification never rebuilds the bytes: a member the verifier does not know about is a member it does not read. The conformance vectors carry a case with an unknown predicate member and an unknown statement member for exactly this reason.
  • Rename, removal, or any semantic change of an existing member is a new major version URI, never a silent in-place change a deployed verifier is expected to tolerate.
Get access

Run your agents through Probity.

A few details about your setup and we'll get you gating agents fast.