Audit
Every decision becomes a record. Hash-chained, replayable, queryable, recoverable.
Every refusal, every grant, every dispatched operation enters the audit chain. The chain is hash-linked so any consumer can verify it independently; the records are structured so any consumer can query them.
The protocol that owns the chain is MAX. The pipeline that writes to it is AuditPipeline inside the engine.
Why audit is load-bearing
Three reasons MAP makes audit a first-class primitive:
- Accountability. Every autonomous action traces back to a human root via lineage and a record via the chain. Both are required.
- Precedent.
MIMESISwatches the chain for recurring patterns and proposes formalizations toMAXIM. The institution learns by doing. - Dispute resolution.
MOOTrules on contested actions by replaying the chain. A verdict that cannot point to its record is not a verdict.
Event taxonomy
pub enum AuditEventType {
ProtocolInvocation,
CapabilityGrant,
AuthorizationCheck,
RateLimitExceeded,
CircuitBreakerOpen,
SecurityViolation,
Error,
}Every record carries:
event_typecorrelation_id(UUID of the request)timestamp(RFC3339)tenant_idcaller_didprotocol+operation(if applicable)outcome(success | refused | error)latency_msprevious_hash(chain link)record_hash(self hash, signed)- arbitrary
metaJSON
Hash chaining
The chain is built incrementally:
record_hash_n = sha256(previous_hash_n-1 || canonical_json(record_n))
chain_head = record_hash_latestThe chain head is returned in every response's metadata so clients can store and later verify. MAX::compliance_report produces a signed attestation of the chain head for a given window.
Replay
const audit = await map.dispatch({
protocol: 'MAX',
operation: 'audit_query',
input: {
correlation_id: 'req_abc123',
include_inputs: true
}
});Returns the full ordered sequence of audit records for that correlation ID. Includes which stage refused (if any) and why. The replay is byte-identical to the original chain insertion.
Traceability graph
MAX::traceability_graph reconstructs the graph of the request — not just the linear chain. For a multi-protocol flow (e.g., MARC calling MIND calling MAVEN), the graph shows the parent → child relationships, the timing, the resolved identities, the verdicts and refusals.
Disk-backed buffer
When the audit sink is degraded (Kafka unreachable, MAX partition rebalancing), the engine writes audit records to a local disk buffer at $MAP_AUDIT_BUFFER_DIR (default /var/lib/map/audit/). The buffer drains in the background.
If the buffer overflows, the engine refuses state-changing operations at Stage 3 (returns RateLimited) until the sink recovers. Read-only operations continue. This is by design — MAP will not accept work it cannot audit.
This refusal is itself audited. The disk buffer always has room for refusal records — they are small. The buffer overflow triggers only for normal operation records.
Cross-org audit
When a request crosses a MOAT treaty boundary, both organizations record the event into their own chains. The treaty fixes the metadata schema for the shared record. Disputes about cross-org actions can be resolved by MOOT by replaying the relevant slices of both chains.
Signing
Every record is signed by the tenant's audit key (separate from the engine's TLS key). Verification is offline-possible: the chain + the tenant's public key suffice. This is what enables sovereign and air-gapped deployments.
Reading audit inside a protocol module
async fn invoke(&self, op: &str, payload: Value, ctx: &InvokeContext)
-> Result<Response, ProtocolError>
{
// Optional: record a custom audit event in addition to the engine's automatic record
ctx.audit().record("CustomDecision", &json!({
"step": "policy_match",
"matched_rule": "rule_id"
}));
// ... continue
}Most protocols rely on the engine's automatic recording at Stage 8. Custom records are useful for protocols with multi-step internal flows (e.g., MACE records each delegate's reasoning during deliberation).
See also
MAXprotocol — the chain itself- Engine audit pipeline — the writer
- Governance — Refusal carries reasons