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
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
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
Set HMAC Key
Always set AUDIT_HMAC_KEY in production to a strong random value.
Regular Verification
Run security::verify_audit on a schedule (e.g., daily) to detect tampering.
Export Logs
Periodically export audit logs to immutable storage (S3, GCS) for compliance.
Monitor for Denials
Alert on capability_denied and quota_exceeded events to detect attacks.
Retain Indefinitely
Never delete audit entries. Archive if needed, but preserve the chain.
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:
Batch Verification : Verify chunks of the chain in parallel
Indexed Queries : Build secondary indexes on agentId, type, timestamp
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