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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

provenyn_verify-1.1.0.tar.gz (30.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

provenyn_verify-1.1.0-py3-none-any.whl (29.3 kB view details)

Uploaded Python 3

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

Hashes for provenyn_verify-1.1.0.tar.gz
Algorithm Hash digest
SHA256 9411370fa9f9332ac402cac5751f9fff271ef8a18cd3e5e51b4e064e5aa458ee
MD5 95748885b3c1af046769347df8a23dc9
BLAKE2b-256 dd0432cca8db240234944cc1d3b46130c6293cbe03b00bf7e22b81fc0e56c04a

See more details on using hashes here.

File details

Details for the file provenyn_verify-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for provenyn_verify-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4701785293e9d5a56927b22d120cd2daef402ae64710a6327bafbca2ad2fd21f
MD5 4e2028656b5ae3d2947dcb10083d61f6
BLAKE2b-256 dbdaab9243ac297a07eaa02bd0250dcf8b21e758c87dc56a64b6fd11df49bfb4

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 files

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