Verification is only as meaningful as the assumptions it rests on. This page states them, so that a result is never taken for more than it is.
Under a trust registry the relying party supplies, a complete verification establishes that a package is unaltered, who signed each piece of evidence, that each signer was authorized for the system it speaks for, and that the contract yields the stated decision from that evidence. It never establishes that a source told the truth.
verification_status says how far the checks got. decision says what the
contract yields. They are independent.
verification_status |
decision |
Meaning |
|---|---|---|
complete |
SUPPORTED |
Every requirement is established by accepted, authentic evidence |
complete |
CONTRADICTED |
A requirement is conclusively false. The verification succeeded; the work did not |
complete |
UNDETERMINED |
The evidence does not settle a requirement. Missing evidence lands here |
incomplete |
None |
No registry was given. Integrity only. The decision in the package is its issuer's claim |
refused |
None |
The package failed a check. refusal.code names the first |
A package whose claim is contradicted is still a fully verified package: it is
well formed, its signatures count, and anyone re-running the contract gets
CONTRADICTED too. That is the protocol working. An API that reported it as a
"failed verification" would make an honest negative look like a broken file,
and would tempt callers to treat "verification passed" as "the work was done".
For the same reason a result cannot be used as a boolean, and
require_supported() exists so that the outcome a caller needs is written
down at the call site.
Two questions are answered by two different parties, and neither can answer for the other.
- The relying party's registry says which key may speak for which source system, for which tenant, on behalf of which issuer, and when.
- The contract says which source systems count for which kind of record.
So a record can be authentic and still not count (signed by a key the registry
trusts, from a source the contract does not accept: it is listed in ignored),
and a record can name an accepted source and still be refused (signed by a key
the registry does not authorize for that source: SOURCE_NOT_AUTHORIZED).
| Assumption | Who makes it | What the SDK does |
|---|---|---|
| The registry is the right one | The relying party | There is no default registry and no way to take one from a package. Verifier(trust=…) has no default; trust=None must be written out and yields incomplete. The registry's byte digest is on every result |
| A key belongs to who the registry says | The relying party | Nothing authenticates a registry. Protect it like any trust anchor |
| A key is used only by its owner | The key's owner | The SDK takes a Signer, never a key file. Custody is the caller's |
| The contract is the one that was agreed | The relying party | Pin it by digest with Expectations(contract=…). A package about another contract version is then refused, not silently accepted |
| The package is about the right tenant and claim | The relying party | Expectations(tenant=…, claim=…) |
| The sources told the truth | Nobody can check | Reported as source_truthfulness: not_established, always |
| A completeness statement was honest | Nobody can check | See below |
| Asserted times are right | Nobody can check | See below |
- Source truthfulness. A correctly signed statement can be false. The signature proves who said it.
- Honest completeness. "There is no posting" can only be concluded when a
source declares its listing complete. If that declaration is false, the
decision is wrong, and no verifier can tell. The SDK never infers absence
from silence: without a completeness statement from an accepted source, an
existsthat finds nothing isUNKNOWN, and the decision isUNDETERMINED. - That a package holds everything. A package discloses what it was evaluated against. It cannot show that nothing was left out, except through completeness statements.
- When something happened.
observedAtis asserted by the signer. - Finality. A decision answers one question: this contract, these inputs, this evidence. Later evidence can change it.
- Acceptance, payment or compliance. A reproduced
SUPPORTEDsays the evidence establishes the contract's requirements. Whether the work is accepted, billable, settled or compliant is decided by other parties under other rules. A signed package is not by itself an agreement between anyone.
A registry record binds one key to one issuer, one tenant, a list of source
systems and a validity period (validFrom, optional validUntil, optional
revokedAt). A registry holds each key identifier and each public key once.
- Scope is enforced by the verifier per statement: tenant, issuer and source must all match the record.
- Rotation is adding a record for the new key and giving the old one a
validUntil. Statements signed while the old key was valid keep verifying. - Revocation is
revokedAt. Statements whose asserted time is at or after it are refused (KEY_NOT_VALID).
The limit to state plainly: validity is judged at the statement's asserted time. Whoever holds a revoked key can backdate a statement into the key's validity period. The protocol has no trusted timestamp or freshness input. If a key is compromised, revoking it does not protect statements "from before"; treat everything signed by that key as suspect, or remove the record.
A statement names its tenant, a key is trusted for one tenant, and a package's
evaluation is for one tenant. Evidence of another tenant is refused at
admission (FOREIGN_TENANT_EVIDENCE), and a key trusted for tenant A does not
verify a statement for tenant B (TENANT_MISMATCH). Work.attach refuses
foreign-tenant evidence early. None of this proves a package is about your
tenant: state that with Expectations(tenant=…).
Verification is offline and stateless. It cannot know that a package was already presented, or that the same unit of work was already billed under another claim or contract version.
work_unit() computes the protocol's work-unit identity and nullifier, so
that two claims about the same unit can be recognized as such across contract
versions. Recognizing them requires state with a first-write-wins rule, which
belongs to whoever settles. This SDK keeps no such state and does not say
whether a unit was used before.
An agent process does not hold trusted credentials merely by running.
- Signatures are produced only through an explicit
Signer, scoped by an explicitSigningAuthority. EvidenceSourcerefuses, before signing, to speak for a system its authority does not name, andDecisionIssuerrequires thewvp-decisionsource. These are guards against mistakes.- A process can lie about its own authority. It then produces a statement that
relying parties refuse, and
Work.issuerefuses to seal it under the issuer's registry. The registry is the boundary, and a source cannot write it. WorkSession.trace_evidencesigns a trace as anAgentTracestatement from the runtime's own system. It can be authentic. It still says only what the agent says.
The package parser is the shared Rust one: bounded size (32 MiB by default,
lower with max_bytes), depth, value count and integer length. Exceeding a
bound is a ResourceLimitError, deliberately not a refusal: the input was not
examined. Malformed input of any kind is a refusal or an error and never a
partial result. Text from a package is untrusted input to whatever reads it
next; the SDK passes it through unchanged and does not interpret it.
If the native extension cannot be loaded, verification raises
NativeUnavailableError. There is no pure-Python fallback and no weaker mode.