Skip to main content

Merkle Hash-Chain Audit Trail

OpenFang maintains a tamper-evident audit log of all agent actions using a Merkle hash-chain structure. Every audit entry includes a cryptographic hash of the previous entry, making it impossible to modify past events without detection.

Why Merkle Hash-Chains?

Traditional logs can be silently modified by attackers with file system access. Merkle hash-chains make tampering detectable:
  • Each entry contains: hash(prev_hash + current_data)
  • Modifying any entry breaks the chain
  • Verification is fast (O(n) single pass)
  • No trusted third party required

Example Chain

Entry 1: hash(genesis + data_1) = abc123
Entry 2: hash(abc123 + data_2) = def456
Entry 3: hash(def456 + data_3) = ghi789
If an attacker modifies Entry 2:
Entry 2: hash(abc123 + modified_data_2) = xyz999  # Different hash!
Entry 3: hash(def456 + data_3) = ghi789            # Expects def456, gets xyz999 → BROKEN
Verification detects the mismatch and rejects the chain.

Audit Log Structure

Each audit entry is a JSON object stored in SQLite:
pub struct AuditEntry {
    pub id: u64,                    // Auto-incrementing ID
    pub timestamp: DateTime<Utc>,   // UTC timestamp
    pub agent_id: Option<AgentId>,  // Agent that performed the action
    pub event_type: AuditEventType, // Type of event
    pub event_data: serde_json::Value, // Event-specific data
    pub hash: String,               // SHA-256 hash of this entry
    pub prev_hash: String,          // SHA-256 hash of previous entry
}

Event Types

pub enum AuditEventType {
    // Agent lifecycle
    AgentSpawned,
    AgentKilled,
    AgentSuspended,
    AgentResumed,

    // Capability events
    CapabilityGranted,
    CapabilityRevoked,

    // Tool invocations
    ToolInvoked,
    ToolFailed,

    // Memory operations
    MemoryWrite,
    MemoryRead,

    // Network operations
    NetworkRequest,
    NetworkBlocked,  // SSRF protection triggered

    // Sandbox violations
    SandboxViolation,

    // Authentication
    AuthSuccess,
    AuthFailure,

    // Configuration
    ConfigChanged,

    // Workflow execution
    WorkflowStarted,
    WorkflowCompleted,
    WorkflowFailed,
}

Hash Calculation

The hash is calculated using SHA-256:
use sha2::{Sha256, Digest};

fn calculate_hash(entry: &AuditEntry, prev_hash: &str) -> String {
    let mut hasher = Sha256::new();

    // Hash components (in order):
    hasher.update(prev_hash.as_bytes());
    hasher.update(entry.timestamp.to_rfc3339().as_bytes());
    hasher.update(entry.event_type.to_string().as_bytes());
    hasher.update(serde_json::to_string(&entry.event_data).unwrap().as_bytes());

    if let Some(agent_id) = &entry.agent_id {
        hasher.update(agent_id.to_string().as_bytes());
    }

    hex::encode(hasher.finalize())
}

Genesis Hash

The first entry in the chain uses a well-known genesis hash:
const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";

Audit Log Format

Example: Agent Spawned

{
  "id": 1,
  "timestamp": "2026-03-07T10:15:30.123456Z",
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "event_type": "AgentSpawned",
  "event_data": {
    "agent_name": "researcher",
    "parent_id": null,
    "capabilities": [
      {"ToolInvoke": "web_search"},
      {"ToolInvoke": "web_fetch"},
      {"MemoryWrite": "self.*"}
    ]
  },
  "hash": "a3f5b8c9d1e2f4a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0",
  "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000"
}

Example: Tool Invoked

{
  "id": 2,
  "timestamp": "2026-03-07T10:15:31.456789Z",
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "event_type": "ToolInvoked",
  "event_data": {
    "tool_name": "web_search",
    "parameters": {
      "query": "OpenFang security architecture"
    },
    "success": true,
    "duration_ms": 234
  },
  "hash": "b4c6d8e0f2a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6",
  "prev_hash": "a3f5b8c9d1e2f4a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0"
}

Example: Sandbox Violation

{
  "id": 3,
  "timestamp": "2026-03-07T10:15:32.789012Z",
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "event_type": "SandboxViolation",
  "event_data": {
    "violation_type": "ResourceExhausted",
    "details": "WASM fuel limit exceeded (10M instructions)",
    "skill_name": "untrusted-skill",
    "action_taken": "Terminated execution"
  },
  "hash": "c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b1c3d5e7f9a1b3c5d7",
  "prev_hash": "b4c6d8e0f2a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6"
}

Example: Capability Granted

{
  "id": 4,
  "timestamp": "2026-03-07T10:15:33.012345Z",
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "event_type": "CapabilityGranted",
  "event_data": {
    "capability": {"ToolInvoke": "file_read"},
    "granted_by": "system",
    "reason": "Spawn-time manifest declaration"
  },
  "hash": "d6e8f0a2b4c6d8e0f2a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8",
  "prev_hash": "c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b1c3d5e7f9a1b3c5d7"
}

Querying the Audit Trail

REST API Endpoints

Get All Audit Entries

curl http://localhost:4200/api/audit

Filter by Agent

curl http://localhost:4200/api/audit?agent_id={agent_id}

Filter by Event Type

curl http://localhost:4200/api/audit?event_type=ToolInvoked

Filter by Time Range

curl "http://localhost:4200/api/audit?start_time=2026-03-07T00:00:00Z&end_time=2026-03-07T23:59:59Z"

Combined Filters

curl "http://localhost:4200/api/audit?agent_id={agent_id}&event_type=SandboxViolation&limit=100"

Pagination

curl "http://localhost:4200/api/audit?limit=100&offset=200"

Tamper Detection

Verify Chain Integrity

curl http://localhost:4200/api/audit/verify
Response (Valid Chain):
{
  "valid": true,
  "entries_checked": 1234,
  "last_hash": "d6e8f0a2b4c6d8e0f2a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8"
}
Response (Tampered Chain):
{
  "valid": false,
  "entries_checked": 1234,
  "first_invalid_entry": 456,
  "expected_hash": "abc123...",
  "actual_hash": "def456...",
  "details": "Hash mismatch at entry 456: expected hash based on previous entry does not match stored hash"
}

Verification Algorithm

pub fn verify_audit_chain(entries: &[AuditEntry]) -> VerificationResult {
    let mut prev_hash = GENESIS_HASH.to_string();

    for (idx, entry) in entries.iter().enumerate() {
        // 1. Check that entry's prev_hash matches what we expect
        if entry.prev_hash != prev_hash {
            return VerificationResult::Invalid {
                first_invalid_entry: idx,
                expected_prev_hash: prev_hash,
                actual_prev_hash: entry.prev_hash.clone(),
            };
        }

        // 2. Recalculate hash for this entry
        let calculated_hash = calculate_hash(entry, &prev_hash);

        // 3. Check that calculated hash matches stored hash
        if calculated_hash != entry.hash {
            return VerificationResult::Invalid {
                first_invalid_entry: idx,
                expected_hash: calculated_hash,
                actual_hash: entry.hash.clone(),
            };
        }

        // 4. Update prev_hash for next iteration
        prev_hash = entry.hash.clone();
    }

    VerificationResult::Valid {
        entries_checked: entries.len(),
        last_hash: prev_hash,
    }
}

Export for Compliance

Export as JSON

curl http://localhost:4200/api/audit/export > audit_trail.json

Export as CSV

curl http://localhost:4200/api/audit/export?format=csv > audit_trail.csv
CSV Format:
id,timestamp,agent_id,event_type,event_data,hash,prev_hash
1,2026-03-07T10:15:30.123456Z,550e8400-e29b-41d4-a716-446655440000,AgentSpawned,"{...}",a3f5b8c9...,00000000...
2,2026-03-07T10:15:31.456789Z,550e8400-e29b-41d4-a716-446655440000,ToolInvoked,"{...}",b4c6d8e0...,a3f5b8c9...

Export with Verification Proof

curl http://localhost:4200/api/audit/export?include_proof=true > audit_with_proof.json
Response:
{
  "entries": [ /* all audit entries */ ],
  "verification": {
    "valid": true,
    "entries_checked": 1234,
    "last_hash": "d6e8f0a2...",
    "verified_at": "2026-03-07T12:00:00Z"
  },
  "signature": "Ed25519 signature of last_hash" // Future feature
}

Compliance Use Cases

1. SOC 2 Compliance

Requirement: “Maintain audit logs of all access to customer data.” OpenFang Solution:
  • All MemoryRead and MemoryWrite events logged
  • Tamper-evident chain prevents retroactive log modification
  • Export logs for external auditor review
# Export all memory access events for Q1 2026
curl "http://localhost:4200/api/audit/export?event_type=MemoryRead,MemoryWrite&start_time=2026-01-01T00:00:00Z&end_time=2026-03-31T23:59:59Z" > soc2_audit_q1.json

2. GDPR Right to Audit

Requirement: “Provide users with a record of all processing activities involving their data.” OpenFang Solution:
  • Filter audit trail by agent_id or custom user_id tag
  • Export user-specific audit history
# Export all actions for user's agent
curl "http://localhost:4200/api/audit/export?agent_id={user_agent_id}" > user_audit_history.json

3. HIPAA Audit Controls

Requirement: “Implement hardware, software, and/or procedural mechanisms that record and examine activity in information systems that contain or use electronic protected health information.” OpenFang Solution:
  • All tool invocations logged with parameters (sanitized)
  • Network requests logged with destinations
  • Authentication events logged
# Export all network requests for HIPAA audit
curl "http://localhost:4200/api/audit/export?event_type=NetworkRequest" > hipaa_network_audit.json

4. ISO 27001 Monitoring

Requirement: “Audit logs shall be produced, kept and regularly reviewed.” OpenFang Solution:
  • Continuous audit logging (no gaps)
  • Integrity verification via hash chain
  • Automated weekly exports
# Automated weekly export (add to cron)
0 0 * * 0 curl http://localhost:4200/api/audit/export > /backups/audit_$(date +\%Y\%m\%d).json

Performance Considerations

Write Performance

  • Audit writes are asynchronous (non-blocking)
  • Buffered in memory, flushed to SQLite every 100ms
  • Minimal impact on agent loop latency (<1ms)

Query Performance

  • SQLite indexes on timestamp, agent_id, event_type
  • Queries with filters use index scans (O(log n))
  • Full chain verification is O(n) but cached (1hr TTL)

Storage Growth

  • Typical entry size: ~500 bytes
  • 1M events ≈ 500 MB
  • Automatic rotation after 10M entries (configurable)
[audit]
max_entries = 10_000_000
rotation_strategy = "archive"  # or "delete"
archive_path = "~/.openfang/audit_archive/"

Audit Configuration

[audit]
enabled = true

# Events to log (default: all)
logged_events = [
    "AgentSpawned",
    "AgentKilled",
    "CapabilityGranted",
    "CapabilityRevoked",
    "ToolInvoked",
    "SandboxViolation",
    "NetworkBlocked",
    "AuthFailure"
]

# Exclude noisy events
excluded_events = [
    "AuthSuccess"  # Too frequent for high-traffic APIs
]

# Retention
max_entries = 10_000_000
rotation_strategy = "archive"  # or "delete"
archive_path = "~/.openfang/audit_archive/"

# Performance
flush_interval_ms = 100  # Buffer flush frequency
verification_cache_ttl_secs = 3600  # 1 hour

Monitoring Audit Health

Check Audit Status

curl http://localhost:4200/api/audit/status
Response:
{
  "enabled": true,
  "total_entries": 123456,
  "last_entry_timestamp": "2026-03-07T12:00:00Z",
  "last_hash": "d6e8f0a2b4c6d8e0...",
  "chain_valid": true,
  "last_verification": "2026-03-07T11:00:00Z",
  "storage_size_mb": 61.7,
  "rotation_threshold": 10000000
}

Alerts

Set up monitoring alerts for:
  • chain_valid: falseCRITICAL: Tamper detected
  • High rate of SandboxViolation events → Potential attack
  • High rate of AuthFailure events → Brute force attempt
  • Storage approaching rotation threshold → Plan archival

Best Practices

1. Verify Regularly

Run verification weekly (or daily for high-security environments):
# Add to cron
0 2 * * 0 curl http://localhost:4200/api/audit/verify | jq '.valid' | grep -q true || send_alert

2. Archive Before Rotation

Before audit rotation, export and archive:
curl http://localhost:4200/api/audit/export?include_proof=true > archive_$(date +%Y%m%d).json

3. Monitor for Anomalies

Track baseline event rates:
# Count events by type
curl http://localhost:4200/api/audit/stats
Response:
{
  "AgentSpawned": 42,
  "AgentKilled": 38,
  "ToolInvoked": 12456,
  "SandboxViolation": 3,  // ← Investigate if spike
  "NetworkBlocked": 12,   // ← Investigate if spike
  "AuthFailure": 156      // ← Investigate if spike
}

4. Restrict Export Access

Audit exports may contain sensitive data. Require authentication:
[audit]
export_requires_auth = true
export_allowed_roles = ["Owner", "Admin"]  # RBAC roles

Overview

All 16 security systems

Capabilities

Capability-based access control

Sandbox

WASM and subprocess isolation

Architecture

How audit integrates across subsystems

Build docs developers (and LLMs) love