Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/iii-hq/agentos/llms.txt

Use this file to discover all available pages before exploring further.

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