Skip to main content
AgentOS maintains an immutable Merkle-linked audit chain that cryptographically links every security event, making tampering detectable.

Overview

The audit chain uses HMAC-SHA256 to create a cryptographically secure chain of events where each entry includes the hash of the previous entry, forming a tamper-evident log.
Entry 0 (Genesis)                Entry 1                      Entry 2
┌─────────────────┐             ┌─────────────────┐          ┌─────────────────┐
│ prevHash: 0000  │             │ prevHash: abc123│          │ prevHash: def456│
│ data: ...       │──hash──────▶│ data: ...       │──hash───▶│ data: ...       │
│ hash: abc123    │             │ hash: def456    │          │ hash: ghi789    │
└─────────────────┘             └─────────────────┘          └─────────────────┘
Each entry’s hash is computed from its data plus the previous entry’s hash, creating a chain that breaks if any historical entry is modified.

Audit Entry Structure

interface AuditEntry {
  id: string;              // UUID
  timestamp: number;       // Milliseconds since epoch
  type: string;            // Event type
  agentId?: string;        // Optional agent ID
  detail: Record<string, unknown>;  // Event-specific data
  hash: string;            // SHA-256 hash of this entry
  prevHash: string;        // Hash of previous entry
}

Appending to the Audit Chain

TypeScript Implementation

src/security.ts
registerFunction(
  { id: "security::audit" },
  async ({ type, agentId, detail }) => {
    // Get the latest entry to retrieve previous hash
    const prev = await trigger("state::get", {
      scope: "audit",
      key: "__latest",
    }).catch(() => ({ hash: "0".repeat(64) }));

    const entry: AuditEntry = {
      id: crypto.randomUUID(),
      timestamp: Date.now(),
      type,
      agentId,
      detail: detail || {},
      prevHash: prev.hash,
      hash: "",  // Computed below
    };

    // Compute HMAC-SHA256 hash linking to previous entry
    entry.hash = createHash("sha256")
      .update(JSON.stringify({ ...entry, hash: undefined }) + prev.hash)
      .digest("hex");

    // Store the entry
    await trigger("state::set", {
      scope: "audit",
      key: entry.id,
      value: entry,
    });

    // Update latest pointer
    await trigger("state::set", {
      scope: "audit",
      key: "__latest",
      value: { hash: entry.hash, id: entry.id, timestamp: entry.timestamp },
    });

    return { id: entry.id, hash: entry.hash };
  }
);

Rust Implementation

The Rust worker uses HMAC for stronger cryptographic guarantees:
crates/security/src/main.rs
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;

fn audit_hmac_key() -> &'static [u8] {
    static KEY: OnceLock<Vec<u8>> = OnceLock::new();
    KEY.get_or_init(|| {
        std::env::var("AUDIT_HMAC_KEY")
            .unwrap_or_else(|_| "dev-default-hmac-key-change-in-prod".to_string())
            .into_bytes()
    })
}

async fn append_audit(iii: &III, input: Value) -> Result<Value, IIIError> {
    let prev: Value = iii
        .trigger("state::get", json!({ "scope": "audit", "key": "__latest" }))
        .await
        .unwrap_or(json!({ "hash": "0".repeat(64) }));

    let prev_hash = prev["hash"].as_str().unwrap_or(&"0".repeat(64)).to_string();
    let id = uuid::Uuid::new_v4().to_string();
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;

    let entry_data = json!({
        "id": &id,
        "timestamp": timestamp,
        "type": input.get("type"),
        "agentId": input.get("agentId"),
        "detail": input.get("detail").unwrap_or(&json!({})),
        "prevHash": &prev_hash,
    });

    // Compute HMAC-SHA256
    let mut mac = HmacSha256::new_from_slice(audit_hmac_key())
        .map_err(|e| IIIError::Handler(format!("HMAC key error: {}", e)))?;
    mac.update(entry_data.to_string().as_bytes());
    mac.update(prev_hash.as_bytes());
    let hash = hex::encode(mac.finalize().into_bytes());

    let full_entry = json!({
        "id": &id,
        "timestamp": timestamp,
        "type": input.get("type"),
        "agentId": input.get("agentId"),
        "detail": input.get("detail").unwrap_or(&json!({})),
        "hash": &hash,
        "prevHash": &prev_hash,
    });

    iii.trigger("state::set", json!({
        "scope": "audit",
        "key": &id,
        "value": &full_entry,
    })).await?;

    iii.trigger("state::set", json!({
        "scope": "audit",
        "key": "__latest",
        "value": { "hash": &hash, "id": &id, "timestamp": timestamp },
    })).await?;

    Ok(json!({ "id": id, "hash": hash }))
}
Set AUDIT_HMAC_KEY environment variable in production. The default key is only for development.

Verifying Chain Integrity

TypeScript Verification

src/security.ts
registerFunction(
  { id: "security::verify_audit" },
  async (req) => {
    const entries = await trigger("state::list", { scope: "audit" });
    const chain: AuditEntry[] = entries
      .filter((e) => e.key !== "__latest" && e.value?.hash)
      .map((e) => e.value)
      .sort((a, b) => a.timestamp - b.timestamp);

    let prevHash = "0".repeat(64);
    const violations: string[] = [];

    for (const entry of chain) {
      // Check chain linkage
      if (entry.prevHash !== prevHash) {
        violations.push(
          `Chain break at ${entry.id}: expected ${prevHash}, got ${entry.prevHash}`
        );
      }

      // Recompute hash
      const computed = createHash("sha256")
        .update(JSON.stringify({ ...entry, hash: undefined }) + entry.prevHash)
        .digest("hex");

      // Verify integrity
      if (computed !== entry.hash) {
        violations.push(`Tampered entry ${entry.id}: hash mismatch`);
      }

      prevHash = entry.hash;
    }

    return {
      valid: violations.length === 0,
      entries: chain.length,
      violations,
    };
  }
);

Rust Verification

crates/security/src/main.rs
async fn verify_audit(iii: &III) -> Result<Value, IIIError> {
    let entries: Value = iii
        .trigger("state::list", json!({ "scope": "audit" }))
        .await?;

    let mut chain: Vec<AuditEntry> = entries
        .as_array()
        .unwrap_or(&vec![])
        .iter()
        .filter_map(|e| {
            let val = e.get("value")?;
            if e["key"].as_str() == Some("__latest") {
                return None;
            }
            serde_json::from_value(val.clone()).ok()
        })
        .collect();

    chain.sort_by_key(|e| e.timestamp);

    let zeros = "0".repeat(64);
    let mut prev_hash = zeros.as_str();
    let mut violations = Vec::new();

    for entry in &chain {
        // Check linkage
        if entry.prev_hash != prev_hash {
            violations.push(format!(
                "Chain break at {}: expected {}, got {}",
                entry.id, prev_hash, entry.prev_hash
            ));
        }

        // Recompute HMAC
        let check_data = json!({
            "id": &entry.id,
            "timestamp": entry.timestamp,
            "type": &entry.entry_type,
            "agentId": &entry.agent_id,
            "detail": &entry.detail,
            "prevHash": &entry.prev_hash,
        });

        let mut mac = HmacSha256::new_from_slice(audit_hmac_key())
            .map_err(|_| IIIError::Handler("HMAC key error".into()))?;
        mac.update(check_data.to_string().as_bytes());
        mac.update(entry.prev_hash.as_bytes());
        let computed = hex::encode(mac.finalize().into_bytes());

        // Detect tampering
        if computed != entry.hash {
            violations.push(format!("Tampered entry {}: hash mismatch", entry.id));
        }

        prev_hash = &entry.hash;
    }

    Ok(json!({
        "valid": violations.is_empty(),
        "entries": chain.len(),
        "violations": violations,
    }))
}

Common Audit Event Types

Security Events

// Capability denied
triggerVoid("security::audit", {
  type: "capability_denied",
  agentId: "researcher-001",
  detail: { resource: "tool::file_write", reason: "tool_not_allowed" },
});

// Quota exceeded
triggerVoid("security::audit", {
  type: "quota_exceeded",
  agentId: "coder-001",
  detail: { used: 105000, limit: 100000 },
});

// Capabilities updated
triggerVoid("security::audit", {
  type: "capabilities_updated",
  agentId: "ops-001",
  detail: { tools: 12 },
});

Vault Events

// Vault unlocked
triggerVoid("security::audit", {
  type: "vault_unlocked",
  detail: { autoLockMs: 1800000 },
});

// Secret accessed
triggerVoid("security::audit", {
  type: "vault_get",
  detail: { key: "ANTHROPIC_API_KEY" },
});

// Vault rotated
triggerVoid("security::audit", {
  type: "vault_rotated",
  detail: { credentialsRotated: 15 },
});

Authentication Events

// MAP challenge issued
triggerVoid("security::audit", {
  type: "map_challenge_issued",
  detail: { sourceAgent: "agent-1", targetAgent: "agent-2" },
});

// MAP verification failed
triggerVoid("security::audit", {
  type: "map_verify_failed",
  detail: { reason: "replay_detected", responderAgent: "agent-2" },
});

Querying the Audit Log

Get All Entries

const entries = await trigger("state::list", { scope: "audit" });
const auditLog = entries
  .filter((e) => e.key !== "__latest")
  .map((e) => e.value)
  .sort((a, b) => a.timestamp - b.timestamp);

console.log(`Total entries: ${auditLog.length}`);

Filter by Agent

const agentEvents = auditLog.filter((e) => e.agentId === "researcher-001");
console.log(`Events for researcher-001: ${agentEvents.length}`);

Filter by Type

const denials = auditLog.filter((e) => e.type === "capability_denied");
console.log(`Capability denials: ${denials.length}`);

Time Range Query

const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
const recentEvents = auditLog.filter((e) => e.timestamp > oneDayAgo);
console.log(`Events in last 24h: ${recentEvents.length}`);

CLI Commands

# View full audit log
agentos security audit

# Verify chain integrity
agentos security verify

# Filter by agent
agentos security audit --agent researcher-001

# Filter by event type
agentos security audit --type capability_denied

# Last N entries
agentos security audit --tail 100

# Export to JSON
agentos security audit --export audit.json

Detecting Tampering

The verification function returns violations when tampering is detected:
const result = await trigger("security::verify_audit", {});

if (!result.valid) {
  console.error(`⚠️  Audit chain compromised!`);
  console.error(`Total entries: ${result.entries}`);
  console.error(`Violations: ${result.violations.length}`);
  
  result.violations.forEach((v) => console.error(`  - ${v}`));
  
  // Alert security team
  await trigger("alert::send", {
    severity: "critical",
    message: "Audit chain integrity violation detected",
    details: result.violations,
  });
}
If security::verify_audit returns valid: false, your audit log has been tampered with. Investigate immediately.

Best Practices

1

Set HMAC Key

Always set AUDIT_HMAC_KEY in production to a strong random value.
2

Regular Verification

Run security::verify_audit on a schedule (e.g., daily) to detect tampering.
3

Export Logs

Periodically export audit logs to immutable storage (S3, GCS) for compliance.
4

Monitor for Denials

Alert on capability_denied and quota_exceeded events to detect attacks.
5

Retain Indefinitely

Never delete audit entries. Archive if needed, but preserve the chain.

Performance Considerations

The audit chain is optimized for append-only workloads:
  • Write: O(1) — only updates latest entry
  • Verify: O(n) — must iterate entire chain
  • Query: O(n) — scan all entries
For large deployments (>1M entries), consider:
  1. Batch Verification: Verify chunks of the chain in parallel
  2. Indexed Queries: Build secondary indexes on agentId, type, timestamp
  3. Archival: Move old entries to cold storage while preserving chain

Next Steps

RBAC

Review capability events in the audit log

Vault

Track vault access events in audit chain

Build docs developers (and LLMs) love