Skip to main content

provenyn-verify

Two packages, one job: let somebody who is not Provenyn decide whether to serve an AI agent that is calling them.

package install
Python provenyn-verify pip install provenyn-verify
TypeScript @provenyn/verify npm i @provenyn/verify

Why this exists

Everything else Provenyn signs proves what an agent did, and is worth something to the customer running it. A portable credential states what an agent may do, and is worth something to everyone that customer does business with — which is the difference between a feature and a protocol.

That only works if the other side can check one, and until now they could not. The verifier existed twice in this repo: a CLI an auditor runs by hand (api/scripts/verify_receipt.py) and the browser verifier on the public verify page (web/src/lib/receiptVerify.ts). Neither is installable, and neither answers the question a partner actually has, which is not "is this receipt genuine" but "should I serve this request".

The honest sentence

A credential proves the agent was authorized when it was issued and has not expired. It is not a statement that the agent is authorized right now.

That is not a caveat bolted on: it is Verdict.guarantee, returned with every verdict, so the sentence cannot get lost between this README and yours. If you need better, pass check_revocation=True / checkRevocation: true and accept that you have just made your API depend on ours being reachable — which is the thing verifying offline exists to avoid, so the SDK keeps serving the offline verdict when it is not, and tells you that is what happened.

Python

from fastapi import Depends, FastAPI
from provenyn_verify import InMemoryReplayCache
from provenyn_verify.fastapi import ProvenynCredential, require_credential

app = FastAPI()

agent = require_credential(
    org_id="org_abc123",          # the customer whose agents you serve
    audience="acme-refunds",      # the identifier THEY used for you
    leeway_seconds=5,             # clock skew you will forgive, both directions
    replay_cache=InMemoryReplayCache(),   # makes a credential single-use — read below
)

@app.post("/refunds")
def create_refund(caller: ProvenynCredential = Depends(agent)):
    if not caller.allows(action="issue_refund"):
        return {"error": "this agent's passport does not cover refunds"}
    return {"ok": True, "agent": caller.agent}

Verifying without a web framework:

from provenyn_verify import Issuer, verify

verdict = verify(token, issuer=Issuer("org_abc123"), audience="acme-refunds")
if not verdict.valid:
    print(verdict.code, verdict.reason)

TypeScript

import express from "express";
import { InMemoryReplayCache } from "@provenyn/verify";
import { requireProvenyn } from "@provenyn/verify/express";

const app = express();

app.post(
  "/refunds",
  requireProvenyn({
    orgId: "org_abc123",
    audience: "acme-refunds",
    leewaySeconds: 5,
    replayCache: new InMemoryReplayCache(),
  }),
  (req, res) => {
    if (!req.provenyn!.allows({ action: "issue_refund" })) {
      return res.status(403).json({ error: "not covered by this agent's passport" });
    }
    res.json({ ok: true, agent: req.provenyn!.agent });
  },
);

Watching the transparency log (1.1.0)

A credential or a receipt tells you about one call. The transparency log is the claim that the record of every call is append-only — and until 1.1.0 neither package could check it, so that claim rested on Provenyn's word.

The shape of the check is: pin a head once, somewhere Provenyn cannot reach, and later demand a proof that the head you pinned is still sitting inside the current one, unchanged.

import json, urllib.request
from provenyn_verify import verify_consistency_proof, verify_tree_head

get = lambda p: json.load(urllib.request.urlopen("https://api.provenyn.com/api" + p))

pinned = json.load(open("pinned_head.json"))     # saved on an earlier run
head = get("/transparency/head")
proof = get(f"/transparency/consistency?first={pinned['tree_size']}&second={head['tree_size']}")

assert proof["first_root"] == pinned["root_hash"], "the log rewrote its own history"
assert verify_consistency_proof(
    proof["first"], proof["first_root"],
    proof["second"], proof["second_root"], proof["proof"]), "proof does not fold"
assert verify_tree_head(head, get("/transparency/log-key")["keys"]), "head not signed by the log"

json.dump(head, open("pinned_head.json", "w"))   # only after it verified
import { verifyConsistencyProof, verifyTreeHead } from "@provenyn/verify";

const ok = await verifyConsistencyProof(
  proof.first, proof.first_root, proof.second, proof.second_root, proof.proof);

Three things about this that are easy to get wrong:

  • The pinned root is the load-bearing line. Without that first assertion the proof shows only that Provenyn served two roots consistent with each other, which a log rebuilt overnight also does. Move the pin forward only after the old one verified.
  • Both sizes travel with the proof, and neither is optional. This tree does not commit to its own size — [A,B,C] and [A,B,C,C] hash identically — so a root compared without its size is a statement about two different logs.
  • It is not RFC 6962. The construction is duplicate-last (bitcoin-style). A Certificate Transparency verifier pointed at this log will reject intact proofs; that is the CT verifier being wrong about which tree it is reading.

verify_tree_head returns False for a head with no log_signature. That is "nobody signed this with a key you can check", not "forged": heads sealed before the log key existed carry only an internal HMAC. A monitor that exits non-zero on that will alarm on Provenyn's own history and teach its owner to ignore the mail.

Replay checking makes a credential single-use

This is the one option that can break a working integration, so it is off by default and worth a paragraph.

The credential carries a nonce and a unique credential_id, and nothing anywhere rejected a second presentation of the same one — because only the party being presented to can know it has seen it before. Turning the cache on closes that. It also means the holder must mint one credential per call, which is what a 300-second TTL is for, but is not something the credential itself says. If the holder mints one and presents it on twenty requests, this refuses nineteen and everybody reads that as our bug.

The in-memory cache is single-process: two instances behind a load balancer do not share it. The ReplayCache interface is small enough that Redis SET key value NX EX ttl is a faithful implementation.

What it will not do

  • Tell you the agent is authorized now. See above. The TTL bounds the gap; the revocation endpoint closes it at the cost of a network call.
  • Verify an hmac-sha256 receipt. Those need the customer's own API key, so they are private by design rather than portable. You are told which algorithm you are holding instead of being told "forged".
  • Trust a receipt's own embedded key. A receipt handed over by an untrusted party can pair tampered content with the attacker's keypair and verify against the key it carries. Pass a key fetched from /api/orgs/{org_id}/pubkey, and match against the keys list — a receipt older than the org's last rotation matches a retired key, not the active one.
  • Read a chain_id as provenance. An agent names its own chain. When delegation_grant_id is absent, chain_self_asserted is true and the lineage is the holder's own word.
  • Prove a log is complete. A consistency proof shows nothing was rewritten or removed between two heads you have seen. It cannot show that everything which happened was written down in the first place — no transparency log can, and the Bitcoin anchors are what bound how far back the published history can move.

Releasing

Both packages carry the same version, and sdk/tests/test_packaging.py fails if they drift. Two verifiers of the same signature that disagree about which version they are is the first thing that makes a bug report unanswerable.

Metadata

Release files for provenyn-verify 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for provenyn-verify 1.1.0
File Size Uploaded Provenance Actions
provenyn_verify-1.1.0.tar.gz 30.2 kB Without provenance Details for provenyn_verify-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for provenyn-verify 1.1.0
File Interpreter ABI Platform Actions
provenyn_verify-1.1.0-py3-none-any.whl Python 3 none any Details for provenyn_verify-1.1.0-py3-none-any.whl

Total release size: 59.4 kB

Release files / provenyn_verify-1.1.0.tar.gz

Download URL provenyn_verify-1.1.0.tar.gz
Size 30.2 kB
Tags Source
SHA-256 checksum
How to use checksums
9411370fa9f9332ac402cac5751f9fff271ef8a18cd3e5e51b4e064e5aa458ee
BLAKE2b-256 checksum
How to use checksums
dd0432cca8db240234944cc1d3b46130c6293cbe03b00bf7e22b81fc0e56c04a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / provenyn_verify-1.1.0-py3-none-any.whl

Download URL provenyn_verify-1.1.0-py3-none-any.whl
Size 29.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4701785293e9d5a56927b22d120cd2daef402ae64710a6327bafbca2ad2fd21f
BLAKE2b-256 checksum
How to use checksums
dbdaab9243ac297a07eaa02bd0250dcf8b21e758c87dc56a64b6fd11df49bfb4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Security for provenyn-verify 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page