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-sha256receipt. 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 thekeyslist — a receipt older than the org's last rotation matches a retired key, not the active one. - Read a
chain_idas provenance. An agent names its own chain. Whendelegation_grant_idis absent,chain_self_assertedis 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file provenyn_verify-1.1.0.tar.gz.
File metadata
- Download URL: provenyn_verify-1.1.0.tar.gz
- Upload date:
- Size: 30.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9411370fa9f9332ac402cac5751f9fff271ef8a18cd3e5e51b4e064e5aa458ee
|
|
| MD5 |
95748885b3c1af046769347df8a23dc9
|
|
| BLAKE2b-256 |
dd0432cca8db240234944cc1d3b46130c6293cbe03b00bf7e22b81fc0e56c04a
|
File details
Details for the file provenyn_verify-1.1.0-py3-none-any.whl.
File metadata
- Download URL: provenyn_verify-1.1.0-py3-none-any.whl
- Upload date:
- Size: 29.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4701785293e9d5a56927b22d120cd2daef402ae64710a6327bafbca2ad2fd21f
|
|
| MD5 |
4e2028656b5ae3d2947dcb10083d61f6
|
|
| BLAKE2b-256 |
dbdaab9243ac297a07eaa02bd0250dcf8b21e758c87dc56a64b6fd11df49bfb4
|