Skip to content

ProvenanceEntry

A ProvenanceEntry is a cryptographically signed record of an agent action. Entries chain together via content hashes to form a tamper-evident directed acyclic graph (DAG).

Structure

json
{
  "agent_did": "did:agent:a1b2c3d4e5f6...",
  "action": "merge",
  "entity_ids": ["entity-1", "entity-2"],
  "parent_ids": ["abc123..."],
  "metadata": { "reason": "duplicate detected", "confidence": 0.97 },
  "signed_envelope": {
    "payload": { ... },
    "signer_did": "did:agent:a1b2c3d4e5f6...",
    "nonce": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-03-15T12:00:00.000Z",
    "signature": "3a4b5c..."
  }
}

The signed_envelope contains the same data as the outer fields, signed by the agent's Ed25519 key. The outer fields exist for convenient access without parsing the envelope. Verification ensures both layers match.

Creating Entries

rust
use kanoniv_agent_auth::{AgentKeyPair, ProvenanceEntry, ActionType};

let keypair = AgentKeyPair::generate();

let entry = ProvenanceEntry::create(
    &keypair,
    ActionType::Merge,
    vec!["entity-1".into(), "entity-2".into()],
    vec![],  // no parents (root entry)
    serde_json::json!({"reason": "duplicate detected"}),
)?;

println!("Agent: {}", entry.agent_did);
println!("Hash:  {}", entry.content_hash());
typescript
import { generateKeyPair, createProvenanceEntry, provenanceContentHash } from "@kanoniv/agent-auth";

const keypair = generateKeyPair();

const entry = createProvenanceEntry(
  keypair,
  "merge",
  ["entity-1", "entity-2"],
  [],  // no parents (root entry)
  { reason: "duplicate detected" },
);

console.log("Agent:", entry.agent_did);
console.log("Hash: ", provenanceContentHash(entry));
python
import json
from kanoniv_agent_auth import AgentKeyPair, ProvenanceEntry

keypair = AgentKeyPair.generate()

entry = ProvenanceEntry.create(
    keypair,
    "merge",
    ["entity-1", "entity-2"],
    [],  # no parents (root entry)
    json.dumps({"reason": "duplicate detected"}),
)

print(f"Agent: {entry.agent_did}")
print(f"Hash:  {entry.content_hash()}")

Chaining Entries

Each entry's content_hash() returns a SHA-256 hex string (64 characters). Pass this as a parent_id to the next entry in the chain.

rust
// Step 1: Resolve two records
let resolve_a = ProvenanceEntry::create(
    &keypair,
    ActionType::Resolve,
    vec!["entity-a".into()],
    vec![],
    serde_json::json!({"source": "crm"}),
)?;

let resolve_b = ProvenanceEntry::create(
    &keypair,
    ActionType::Resolve,
    vec!["entity-b".into()],
    vec![],
    serde_json::json!({"source": "billing"}),
)?;

// Step 2: Merge - references both resolves as parents
let merge = ProvenanceEntry::create(
    &keypair,
    ActionType::Merge,
    vec!["entity-a".into(), "entity-b".into()],
    vec![resolve_a.content_hash(), resolve_b.content_hash()],
    serde_json::json!({"confidence": 0.95}),
)?;

assert_eq!(merge.parent_ids.len(), 2);
typescript
import { createProvenanceEntry, provenanceContentHash } from "@kanoniv/agent-auth";

// Step 1: Resolve two records
const resolveA = createProvenanceEntry(
  keypair, "resolve", ["entity-a"], [], { source: "crm" },
);

const resolveB = createProvenanceEntry(
  keypair, "resolve", ["entity-b"], [], { source: "billing" },
);

// Step 2: Merge - references both resolves as parents
const merge = createProvenanceEntry(
  keypair,
  "merge",
  ["entity-a", "entity-b"],
  [provenanceContentHash(resolveA), provenanceContentHash(resolveB)],
  { confidence: 0.95 },
);
python
# Step 1: Resolve two records
resolve_a = ProvenanceEntry.create(
    keypair, "resolve", ["entity-a"], [],
    json.dumps({"source": "crm"}),
)

resolve_b = ProvenanceEntry.create(
    keypair, "resolve", ["entity-b"], [],
    json.dumps({"source": "billing"}),
)

# Step 2: Merge - references both resolves as parents
merge = ProvenanceEntry.create(
    keypair,
    "merge",
    ["entity-a", "entity-b"],
    [resolve_a.content_hash(), resolve_b.content_hash()],
    json.dumps({"confidence": 0.95}),
)

Verifying Entries

Verification confirms both the cryptographic signature and the integrity of outer fields.

rust
use kanoniv_agent_auth::AgentIdentity;

// Full verification: signature + field integrity
entry.verify(&keypair.identity())?;

// Signature-only (skips integrity check - use with caution)
entry.verify_signature_only(&keypair.identity())?;
typescript
import { verifyProvenanceEntry, verifyProvenanceSignatureOnly } from "@kanoniv/agent-auth";

// Full verification: signature + field integrity
verifyProvenanceEntry(entry, keypair.identity);

// Signature-only (skips integrity check - use with caution)
verifyProvenanceSignatureOnly(entry, keypair.identity);
python
# Full verification: signature + field integrity
# Raises ValueError on failure
entry.verify(keypair.identity())

Tamper Detection

If someone modifies an outer field after signing, full verification catches it:

rust
let mut entry = ProvenanceEntry::create(
    &keypair,
    ActionType::Resolve,
    vec!["entity-1".into()],
    vec![],
    serde_json::json!({}),
)?;

// Tamper with outer entity_ids
entry.entity_ids = vec!["entity-999".into()];

// Full verify catches the mismatch
assert!(entry.verify(&keypair.identity()).is_err());

// Signature-only still passes (signature is valid for original payload)
assert!(entry.verify_signature_only(&keypair.identity()).is_ok());

This is why verify() (not verify_signature_only()) should be the default choice.

Custom Actions

For application-specific actions not covered by the built-in types, use custom actions:

rust
let entry = ProvenanceEntry::create(
    &keypair,
    ActionType::Custom("compliance_review".into()),
    vec!["entity-1".into()],
    vec![],
    serde_json::json!({"reviewer": "agent-auditor", "result": "approved"}),
)?;
typescript
const entry = createProvenanceEntry(
  keypair,
  { custom: "compliance_review" },
  ["entity-1"],
  [],
  { reviewer: "agent-auditor", result: "approved" },
);
python
entry = ProvenanceEntry.create(
    keypair,
    "custom:compliance_review",
    ["entity-1"],
    [],
    json.dumps({"reviewer": "agent-auditor", "result": "approved"}),
)

Content Hash

The content hash is a SHA-256 digest of the signed envelope's canonical JSON representation. Fields are sorted alphabetically (nonce, payload, signature, signer_did, timestamp) to ensure deterministic hashing across all three language implementations.

content_hash = SHA-256(canonical_json(signed_envelope))
             = 64 hex characters (32 bytes)

The same entry always produces the same hash, regardless of which language computed it. This cross-language determinism is what makes DAG chaining work in mixed-language deployments.

Cross-Agent Verification

Any party with an agent's public identity can verify that agent's provenance entries. The verifier does not need the agent's secret key - only the AgentIdentity (which contains the public key and DID).

rust
// Agent A creates an entry
let entry = ProvenanceEntry::create(&agent_a_keypair, ...)?;

// Agent B verifies it using A's public identity
entry.verify(&agent_a_identity)?;      // OK
entry.verify(&agent_b_identity).is_err(); // fails - wrong agent

This enables third-party auditing: a compliance agent can verify every entry in the provenance DAG without access to any agent's secret keys.

The identity and delegation layer for AI agents.