0Read this first
v0.1 trust-root disclaimer
At v0.1 the chain LINKING is cryptographically verified (sha256 prev_hash + sha256 payload contents). The chain IDENTITIES are NOT. Every signature is a placeholder string of the form stub-ed25519:<hex>. There is no key authority, no sigstore, no fulcio, no rekor, no publisher enrollment, no key rotation. trustRoot is the literal stub-v0.1-no-trust-root in every chain and every verification response. Real signing lands in v0.2. Do not interpret a verified chain as a verified identity.
1Purpose
A Provenance Chain binds three attestations - the publisher's release, the registry's retrieval, and the agent's install - into one tamper-evident object linked to the package artifact by content hash.
The chain answers four narrow questions: who published this artifact, what the registry served, what the agent installed, and whether anyone has rewritten any of those records in the gap. At v0.1 the first three answers are stubs (no enrolled identities); the fourth is real (prev_hash + payload_sha256 catch in-chain tampering).
2The three steps
A canonical chain has three steps. Partial chains are valid; a publish-only chain (no retrieval, no install) verifies fine and is just less useful. The schema allows up to 16 steps per chain so additional install steps can be appended by different agents (see section 7).
- publish - the publisher attests to the build (SLSA-style materials + invocation). Signed by the publisher key. At v0.1 the publisher key is a placeholder.
- retrieval - the registry attests to what it served and to whom. Signed by the registry key. Catches a registry that serves different bytes than the publisher signed.
- install - the agent attests to what it actually installed. Links back to the install receipt (
rcpt_ULID). Signed by the agent key. Catches an agent that loaded a different artifact than what was served. A chain MAY contain more than one install step when the same artifact is installed by multiple agents (see section 7).
3Example chain
{
"schema": "apai.provenance.v0.1",
"chain_id": "prov_G0VBD42JEV47C8Z8CZ8Y2TWTTH",
"package": "coding-safe-mode",
"package_version": "0.1.0",
"package_sha256": "aaaaaaaa...64-hex...",
"trustRoot": "stub-v0.1-no-trust-root",
"steps": [
{
"step_type": "publish",
"actor": { "id": "apai-official", "kind": "publisher", "key_id": "stub-key-pub-1" },
"timestamp": "2026-05-14T01:00:00Z",
"payload_sha256": "<sha256 of canonical payload JSON>",
"prev_hash": "GENESIS",
"signature": "stub-ed25519:<hex>",
"payload": {
"builder": "github-actions",
"build_type": "https://github.com/...",
"reproducible": true
}
},
{
"step_type": "retrieval",
"actor": { "id": "apai.run", "kind": "registry" },
"timestamp": "2026-05-14T01:05:00Z",
"payload_sha256": "<...>",
"prev_hash": "<sha256 of canonical(payload_sha256+signature) of publish step>",
"signature": "stub-ed25519:<hex>"
},
{
"step_type": "install",
"actor": { "id": "demo-agent", "kind": "agent" },
"timestamp": "2026-05-14T01:10:00Z",
"payload_sha256": "<...>",
"prev_hash": "<sha256 of retrieval step>",
"signature": "stub-ed25519:<hex>",
"payload": {
"install_id": "rcpt_01HXY7G8K9M2P3Q4R5S6T7V8W9",
"install_mode": "local-tool",
"target_platform": "claude_code",
"approval_state": "granted_by_operator_at_install"
}
}
]
}Full schema: /schemas/apai.provenance.schema.json.
4Chain linking math
For each step, two hashes are computed and one is recorded:
payload_sha256= sha256( canonicalJson(payload) ). Recorded on the step. Recomputed at verification time; mismatch means the payload was edited after signing.prev_hash= sha256( canonicalJson({ payload_sha256, signature }) ) of the PRIOR step, or the literalGENESISon step 0. Recorded on the CURRENT step. Mismatch means a prior step was rewritten or removed.
canonicalJson sorts object keys recursively before serializing so two independent implementations reach the same hash.
5Verify endpoint
POST to /api/provenance/verify with a chain JSON in the body:
POST https://apai.run/api/provenance/verify
Content-Type: application/json
{ "chain": { ... full provenance chain ... } }Response (clean 3-step chain):
{
"schema": "apai.provenance-verify.v0.1",
"chainIntegrity": "verified",
"signatures": "stub-v0.1-no-trust-root",
"trustRoot": "stub-v0.1-no-trust-root",
"stepsPresent": ["publish", "retrieval", "install"],
"stepsVerified": [
"publish:chain-link", "publish:payload-hash",
"retrieval:chain-link", "retrieval:payload-hash",
"install:chain-link", "install:payload-hash"
],
"warnings": []
}Response (tampered retrieval step):
{
"schema": "apai.provenance-verify.v0.1",
"chainIntegrity": "broken",
"signatures": "stub-v0.1-no-trust-root",
"trustRoot": "stub-v0.1-no-trust-root",
"stepsPresent": ["publish", "retrieval", "install"],
"stepsVerified": [
"publish:chain-link", "publish:payload-hash",
"install:chain-link", "install:payload-hash"
],
"warnings": [
"step 1 (retrieval): prev_hash mismatch. expected ..., got ..."
]
}The response shape is enumerated, never binary. Each field tells the caller what kind of "verified" was performed and what was not.
6VerificationResult fields
| Field | Type | Req | Description |
|---|---|---|---|
| chainIntegrity | verified | broken | missing | yes | verified = all chain-link + payload-hash checks passed. broken = at least one prev_hash or payload_sha256 mismatched. missing = chain absent or malformed. |
| signatures | string | yes | Always the literal 'stub-v0.1-no-trust-root' at v0.1. Real signature verification (sigstore-style) lands in v0.2 and this field will name the verifying authority. |
| trustRoot | string | yes | Always the literal 'stub-v0.1-no-trust-root' at v0.1. v0.2 will surface the fulcio/rekor root URL or equivalent. |
| stepsPresent | string[] | yes | Ordered list of step_type values seen in the chain. e.g. ['publish', 'install'] for a chain missing the retrieval attestation. |
| stepsVerified | string[] | yes | List of '<step_type>:<check>' markers, e.g. 'publish:chain-link', 'retrieval:payload-hash'. Caller can tell exactly which checks passed. |
| warnings | string[] | yes | Human-readable defects found. Always present even on verified chains (may be empty array). |
7What this catches
- In-chain rewrites. A chain step edited after assembly fails its prev_hash check on the next step, or its payload_sha256 check on itself.
- Step removal or insertion. Removing a step breaks the prev_hash chain of the next step; inserting a step requires either rehashing everything downstream (visible because the published chain_id no longer matches what verifiers expect) or accepting that the chain immediately fails.
- Payload tampering without re-hashing. An attacker who edits a payload but does not recompute payload_sha256 is caught by the payload-hash check.
8Cross-agent attestation
The same artifact may be installed by more than one agent in an operator's fleet: Claude Code on one workstation, Codex on a different machine, Gemini CLI on a third. Each install produces its own receipt, but the chain accepts multiple install steps appended after the single publish and retrieval. This is cross-agent attestation.
Ordering rules (v0.1):
- Step 0 is
publish. Required for any non-partial chain. - Step 1 (if present) is
retrieval. The registry attests to what was served and to whom. - Steps 2 through 15 may be any number of
installsteps. Each install step references its ownrcpt_ULID, its own agent slug, and its own placeholderkey_id. Install timestamps SHOULD be monotonically non-decreasing as a chain-author convention, but the chain-link math does not depend on time. At v0.1/api/provenance/verifydoes NOT check timestamp ordering; external verifiers MAY add that check. prev_hashon each install step links to the step before it, NOT to the retrieval step. The chain is a single linked list, not a tree.- Chain length cap is
maxItems: 16at v0.1.
Example: one mcp-audit artifact installed by three fleet agents.
{
"schema": "apai.provenance.v0.1",
"chain_id": "prov_J3XEF65MGZ7AEBVBE2B2A4XVVH",
"package": "mcp-audit",
"package_version": "0.1.0",
"trustRoot": "stub-v0.1-no-trust-root",
"steps": [
{ "step_type": "publish", "actor": { "id": "apai-official", "kind": "publisher" }, ... },
{ "step_type": "retrieval", "actor": { "id": "apai.run", "kind": "registry" }, ... },
{ "step_type": "install", "actor": { "id": "agent-claude-code", "kind": "agent",
"key_id": "stub-key-agent-claude-code-1" }, "payload": { "install_id": "rcpt_01HXY8K2L3..." } },
{ "step_type": "install", "actor": { "id": "agent-codex-jwgh02", "kind": "agent",
"key_id": "stub-key-agent-codex-jwgh02-1" }, "payload": { "install_id": "rcpt_01HXY8M3N4..." } },
{ "step_type": "install", "actor": { "id": "agent-codex-griffin", "kind": "agent",
"key_id": "stub-key-agent-codex-griffin-1" }, "payload": { "install_id": "rcpt_01HXY8N4P5..." } }
]
}POST this chain to /api/provenance/verify and the response returns chainIntegrity: "verified" with 10 stepsVerified entries (2 per step: chain-link and payload-hash). One linked list, three attestations from three agents, one publish, one retrieval.
What cross-agent attestation does NOT prove at v0.1: that the three agent identities are distinct humans, distinct keys enrolled in any authority, or distinct machines. The actor.id and actor.key_id are opaque strings; a single attacker who controls a registry can mint a chain with any three key_id values they want and the chain will verify. Cross-agent attestation in v0.1 proves that three install records were assembled into one chain in one order and have not been rewritten since. Distinct verified signers land in v0.2 along with the trust root.
What this spec is NOT
- ·Cryptographic identity. Signatures at v0.1 are placeholder 'stub-ed25519:<hex>' strings. There is no key authority. An attacker who controls the registry can mint a brand-new chain end-to-end and it will verify; the chain only proves internal consistency, not authorship.
- ·Trust-root verification. There is no fulcio, no rekor, no enrolled-publisher database. The trustRoot literal is 'stub-v0.1-no-trust-root' on purpose. v0.2 lands a real trust root.
- ·Proof the package is safe. Provenance is who+what+when, not is-it-good. The Capability Passport, Scanner, and Policy Pack stories cover safety. A clean chain on a malicious package is a verified record of a malicious release.
- ·An audit log. Verify responses are not persisted at v0.1. Persistent audit lands in Phase 5 along with the install receipt audit trail.
- ·Cross-publisher chain merging. Each chain is single-package. Cross-agent attestation supports multiple install steps on one chain for one artifact; it does NOT merge chains across publishers or across artifacts.