Skip to main content

Audit Log

Every capability call — successful, denied, or errored — is recorded in an AES-256-GCM encrypted in-memory buffer. The audit log is your source of truth for what agents did and when.

Reading the Log

// All entries (decrypted in-memory, no I/O)
const entries = chain.getAuditLog();

// Summary stats including auth overhead
const stats = chain.getStats();
// {
// agentId, hostId, agentName, hostname,
// totalCalls, successfulCalls, deniedCalls, errorCalls,
// registeredAt,
// authOverhead: { avgMs, maxMs }
// }

Audit Entry Fields

FieldTypeDescription
idstringUnique entry ID
agentIdstringAgent that made the call
agentNamestringHuman-readable agent name
capabilitystringCapability requested
argsobjectSanitized call arguments
result"success" | "denied" | "error"Outcome
denialReasonstring?Set when result === "denied"
jtistringJWT ID (replay protection key)
timestampnumberUnix ms
durationMsnumberExecution time in ms
authOverheadMsnumberTime spent in build + verify
modelMetadataModelMetadata?LLM metadata when the call went through an SDK wrapper (tokens, model, tool calls)

Argument Sanitization

Sensitive fields are automatically redacted before logging:

Keys matching: key, secret, token, password, auth, credential, bearer
→ replaced with "[REDACTED]"

This happens automatically — you don't need to configure it.

Draining

The buffer is capped at 1000 entries (O(1) appends, oldest evicted). Drain it to an external system periodically or on shutdown:

// Drain using the configured exporter
await chain.drain();

// Or override with an ad-hoc exporter
await chain.drain(new ConsoleAuditExporter());

Exporters

Two built-in exporters:

ConsoleAuditExporter

Logs entries to the console. Good for development:

import { ConsoleAuditExporter } from 'agents-chain';

const chain = await AppChain.create({
// ...
auditExporter: new ConsoleAuditExporter(),
});

HttpAuditExporter

Posts entries to an HTTP endpoint:

import { HttpAuditExporter } from 'agents-chain';

const chain = await AppChain.create({
// ...
auditExporter: new HttpAuditExporter({
endpoint: 'https://audit.mycompany.com/ingest',
apiKey: process.env.AUDIT_API_KEY,
}),
});

Custom Exporter

Implement the AuditExporter interface:

const myExporter = {
async export(entries) {
for (const entry of entries) {
await myDatabase.insert('audit_log', entry);
}
},
};

Traces vs Audit Entries

The audit log and the trace system share the same data source — every recordCall() / recordDenied() call writes to both. The difference is scope:

  • Audit entries — individual call records, persisted in the ring buffer, queryable with getAuditLog()
  • Trace spans — the same data, but grouped into a TraceRun that covers an entire agent session

Open a trace with chain.openTrace() and pass the traceId to wrap() or openai()/anthropic(). All calls within that session automatically become spans on the open trace. modelMetadata (token counts, model name, tool calls) is attached to both the audit entry and the span when the call goes through an LLM SDK wrapper.

See Tracing & Observability for the full API.

Internals

The buffer is never encrypted per-append. Encryption happens only when flush() or drain() is called. Reads are always in-memory.