Skip to main content

Audit Trail — Virtual Breadcrumb Chain

The audit trail is GEIANT's core compliance mechanism. Every agent operation produces a virtual breadcrumb — a signed, hash-chained record that proves what happened, where, when, and under whose authority.

Block Structure​

Each breadcrumb block contains:

interface VirtualBreadcrumbBlock {
index: number; // Sequential block number
identity_public_key: string; // Agent Ed25519 PK (64 hex)
timestamp: string; // ISO 8601 UTC
location_cell: string; // H3 cell — jurisdictional binding
location_resolution: number; // H3 resolution (typically 5)
context_digest: string; // SHA-256 of tool input + output
previous_hash: string | null; // null for genesis block
meta_flags: AgentMetaFlags; // Tool duration, I/O hashes, tier
signature: string; // Ed25519 signature (128 hex)
block_hash: string; // SHA-256 of (dataToSign + signature)
delegation_cert_hash: string; // SHA-256 of governing delegation cert
tool_name: string; // MCP tool that fired
facet: string; // Capability scope
}

Hash Chain​

Blocks are SHA-256 chained. Each block's previous_hash field contains the block_hash of the preceding block. The genesis block (index 0) has previous_hash: null.

Block #0                    Block #1                    Block #2
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ prev_hash: null │ │ prev_hash: H(#0) │ │ prev_hash: H(#1) │
│ data... │──────►│ data... │──────►│ data... │
│ signature │ │ signature │ │ signature │
│ block_hash: H(#0)│ │ block_hash: H(#1)│ │ block_hash: H(#2)│
└──────────────────┘ └──────────────────┘ └──────────────────┘

Tamper detection: Modify any field in Block #1 and its block_hash changes. Block #2's previous_hash no longer matches → chain verification fails for every subsequent block.

Context Digest​

The context_digest field is a double hash of the tool's input and output:

input_hash  = SHA-256(canonical_json(tool_input))
output_hash = SHA-256(canonical_json(tool_output))
context_digest = SHA-256(input_hash + ":" + output_hash)

This proves what data the tool processed without storing the actual data in the audit trail — privacy-preserving by design.

Signing​

Every block is Ed25519-signed by the agent. The signing payload is the canonical JSON of all block fields (excluding signature and block_hash). Anyone with the agent's public key can verify the signature offline.

data_to_sign = canonical_json({
index, identity, timestamp, loc_cell, loc_res,
context, prev_hash, meta, delegation_cert_hash,
tool_name, facet
})
signature = Ed25519.sign(data_to_sign, agent_secret_key)
block_hash = SHA-256(data_to_sign + ":" + signature)

Epoch Rollups​

Breadcrumbs accumulate. For long-running agents, verifying thousands of individual blocks is expensive. Epochs solve this by Merkle-rolling blocks into compact summaries:

interface AgentEpochSummary {
epoch_index: number;
agent_pk: string;
start_block_index: number;
end_block_index: number;
block_count: number;
merkle_root: string; // Binary Merkle tree over block hashes
previous_epoch_hash: string; // Chain of epochs
tools_used: string[]; // Distinct tools in this epoch
jurisdiction_cells: string[]; // Distinct H3 cells operated in
tier_at_close: AgentTier;
signature: string; // Agent signs the epoch
epoch_hash: string;
}

The Merkle root is built from a binary tree over all block hashes in the epoch. This allows proving that a specific block was included in an epoch without revealing all other blocks (Merkle proof).

Verification Levels​

LevelWhat's CheckedCost
BlockSignature valid, hash matchesO(1) per block
ChainAll blocks linked, no gaps, timestamps monotonicO(n)
EpochMerkle root valid, epoch chain linkedO(log n) per proof
FullChain + delegation cert valid + jurisdiction checksO(n)

Storage​

Breadcrumbs are stored in Supabase PostgreSQL with PostGIS extensions. The agent_breadcrumbs table has Row Level Security: service role has full access, anon role has read-only access (public verifiability).

SELECT block_index, tool_name, location_cell, block_hash, created_at
FROM agent_breadcrumbs
WHERE agent_pk = 'c14094ea7efb6122...'
ORDER BY block_index DESC
LIMIT 10;