provenance-protocol
v0.8.0
Published
The Provenance Protocol — an open standard for AI agent identity — with offline verification, signing and attestations
Maintainers
Readme
provenance-protocol
The Provenance Protocol: an open standard for declaring and verifying AI agent identity — and a reference implementation of it.
An agent publishes a signed declaration: what it is, what it can do, what it will never do, who answers for it, and — optionally — what happens to data, who else touches it, what needs a human, and what it depends on. Third parties publish signed attestations about it: what they observed, what was reported, what they approved. Both verify offline — no account, no API key, and no call to any service. Nothing here depends on any particular company, this one included.
npm install provenance-protocolThe standard
| | | |---|---| | SPEC.md | The specification: declarations, where they live, signing and verification, attestations, conformance. | | schema/ | JSON Schemas for declarations (0.1, 0.2) and attestations (0.1). | | test-vectors/ | Normative vectors. Pass these and you interoperate. |
MIT-licensed. Implement it in any language, for any purpose, without permission or notification. Indexes, monitors and attesters are applications built on the standard, not part of it.
Accept an agent on the strength of its declaration
import { checkDeclaration, locateDeclaration } from 'provenance-protocol';
const url = locateDeclaration('provenance:domain:agent.example.com');
// → https://agent.example.com/.well-known/provenance.json
const declaration = await (await fetch(url, { redirect: 'error' })).json();
const { allowed, reason } = await checkDeclaration(declaration, {
retrievedFrom: url,
requireConstraints: ['no:financial:transact'], // example — use your own
});
if (!allowed) throw new Error(reason);checkDeclaration refuses unless the signature covers the whole declaration,
the file was served from the location its id names, and it promises what you
require. Every refusal says why. It answers is this genuinely the operator's,
and does it promise what I need? — not is it in good standing today, which
no document can carry and which comes from attesters you choose to trust.
GitHub-hosted declarations are YAML; parse them with any YAML library and pass the object.
Verify, in detail
import { verifyDeclaration } from 'provenance-protocol';
const result = await verifyDeclaration(declaration, { retrievedFrom: url });
result.valid // the signature verifies against the key in the file
result.coverage // 'declaration' (0.2) | 'identity' (0.1)
result.location // 'match' | 'mismatch' | 'unchecked'
result.trustworthy // valid AND served from the location it claims
result.fingerprint // SHA-256 of the key — pin it to detect rotationUnder spec 0.2 the signature covers every field, so deleting a constraint breaks it. Under 0.1 it covered only the identity and key; a valid 0.1 signature says nothing about whether the constraints were edited.
Attestations
A signed statement by a third party about an agent. Verify it against the issuer's public key — normally from the issuer's own verified declaration.
import { verifyAttestation } from 'provenance-protocol';
const r = await verifyAttestation(attestation, { issuerPublicKey });
r.status // 'valid' | 'expired' | 'not_yet_valid' | 'invalid' | 'unchecked'expired means genuine but stale; invalid means forged or altered. They are
never confused: the signature is checked before the dates.
Issuing one (Node):
import { signAttestation } from 'provenance-protocol/keygen';
import { keyFingerprint, declarationDigest } from 'provenance-protocol';
const attestation = {
attestation: '0.1',
id: 'att-0001',
kind: 'declaration-check',
issuer: { provenance_id: 'provenance:domain:attester.example', key_fingerprint: await keyFingerprint(myPublicKey) },
subject: { provenance_id: declaration.provenance_id, declaration_digest: await declarationDigest(declaration) },
issued_at: '2026-09-21T09:00:00Z',
valid_until: '2026-09-24T09:00:00Z',
scope: 'Covers the declaration as retrieved at the stated time and location only.',
claims: { retrieved_from: url, retrieved_at: '2026-09-21T08:59:12Z', signature: 'declaration', location: 'match' },
};
attestation.signature = signAttestation(myPrivateKey, attestation);What changed, and does it matter?
import { compareDeclarations } from 'provenance-protocol';
for (const c of compareDeclarations(before, after)) {
// { field: 'data.retention', change: 'modified', from: 'P30D', to: 'P1Y',
// direction: 'weakened', announced: false }
}Every watcher applying the spec's rules reports the same changes and agrees on
which weaken the agent's promises: a constraint removed, retention lengthened, a
subprocessor or region added, human approval dropped, a limit raised. A
weakening the operator listed in advance under changes.pending is marked
announced.
Notices — the operator's own signed updates
import { verifyNotice } from 'provenance-protocol';
const r = await verifyNotice(notice, { publicKey }); // for key-rotation: the OLD key
r.status // 'valid' | 'invalid' | 'unchecked'
r.newKeyFingerprint // after a valid key-rotation, pin thisEvents: declaration-published, release, key-rotation (signed by the old
key, so a new key arrives vouched for), incident. Operators sign them with
signNotice from provenance-protocol/keygen;
provenance-middleware
publishes them for you.
Links to A2A and MCP
import { checkInteropLinks } from 'provenance-protocol';
checkInteropLinks(declaration); // { a2a: 'confirmed' | 'claimed' | 'none', mcp: … }A declaration can point to the same agent's A2A Agent Card and MCP Registry entry. A link is confirmed only when the same party provably controls both ends; otherwise it is claimed, so nobody can attach their declaration to a well-known agent.
Internal agents
A company's own agents, on hosts nobody outside can reach, deliver their
declaration inside a signed notice, and the company vouches for them with an
affiliation signed by its own key:
PROVENANCE_ORG_PRIVATE_KEY=… npx provenance-protocol affiliate PROVENANCE.yml \
--org provenance:domain:corp.example --unit "Human Resources" --out affiliation.jsonimport { openDeliveredDeclaration, checkDeclaration } from 'provenance-protocol';
const { declaration } = await openDeliveredDeclaration(notice);
const r = await checkDeclaration(declaration, {
affiliation: { attestation: affiliation, issuerPublicKey: orgPublicKey },
});
r.anchor // 'affiliation' — tied to the organisation, not to a locationSign your own declaration
From the command line — no service involved:
npx provenance-protocol keygen # once; keep the private key in your env
PROVENANCE_PRIVATE_KEY=… npx provenance-protocol sign PROVENANCE.yml
npx provenance-protocol verify provenance:domain:agent.example.com
npx provenance-protocol validatesign edits the file in place, keeps your comments, and reads the result back
to confirm it verifies. For a hosted service,
provenance-middleware
serves and signs the declaration at startup instead.
Or in code:
import { generateProvenanceKeyPair, signDeclaration } from 'provenance-protocol/keygen';
declaration.identity.signature = signDeclaration(privateKey, declaration);Exit codes: 0 ok, 1 checked and failed, 2 could not check. "Could not
check" is never reported as a failure, or as a pass.
Entry points
| Import | Runs in | What |
|---|---|---|
| provenance-protocol | anywhere with Web Crypto | verify, check, locate, compare, attestations, notices |
| provenance-protocol/keygen | Node | keys, signing declarations, challenges, attestations, notices |
| provenance-protocol/validate | Node | schema validation for declarations, attestations and notices |
| provenance-protocol/index-client | anywhere | client for an index service you choose (below) |
The main entry has no dependencies. yaml is used only by the CLI.
Live proof of key control
A signed file proves where the declaration came from. To check that the service answering you right now holds the key, send it a single-use nonce:
import { verifyAgentChallenge } from 'provenance-protocol';
const ok = await verifyAgentChallenge(publicKey, provenanceId, nonce, signature);The agent answers with signAgentChallenge (or provenance-middleware does it
for them).
Index client (optional)
An index is a service that crawls or accepts registrations and answers questions about current standing — open incidents, revocation, age. It is an application of the standard, not part of it. You must name the one you trust; there is no default.
import { Provenance } from 'provenance-protocol/index-client';
const index = new Provenance({ apiUrl: 'https://index.example.com', onApiError: 'deny' });
const result = await index.gate(provenanceId, { requireConstraints: ['no:pii'] });The CLI's register, status and revoke commands likewise need
--index <url> or PROVENANCE_INDEX_URL.
Upgrading from 0.5
- The main entry is now offline.
Provenancemoved toprovenance-protocol/index-clientand requiresapiUrl; the defaultprovenanceinstance is gone. - The index client's
verifySignatureandgate({ requireSignedProof })now expect the domain-separated challenge (signAgentChallenge), matchingprovenance-middleware. provenance validateruns locally against the bundled schema.- The schemas accept
provenance:domain:identifiers and ignore unknown top-level fields, as the conformance rules always required.
Related
| Package | Purpose |
|---|---|
| provenance-middleware | One line that makes a service serve and sign its own declaration |
| provenance-action | Verify a declaration in CI |
| ajp-protocol | Agent Job Protocol — agent-to-agent job delegation |
