"""Immutable, hash-chained audit trail. Every risk decision, mode change, order and kill-switch flip goes here.""" from __future__ import annotations import hashlib import json from typing import Any import asyncpg from ktrade.logging import correlation_id def _hash(prev: str | None, actor: str, action: str, entity: str | None, details: dict[str, Any]) -> str: payload = json.dumps({"p": prev, "a": actor, "x": action, "e": entity, "d": details}, sort_keys=True, default=str) return hashlib.sha256(payload.encode()).hexdigest() async def audit(pool: asyncpg.Pool, actor: str, action: str, entity: str | None = None, details: dict[str, Any] | None = None) -> int: details = details or {} async with pool.acquire() as conn, conn.transaction(): prev = await conn.fetchval("SELECT hash FROM audit_log ORDER BY id DESC LIMIT 1 FOR UPDATE") h = _hash(prev, actor, action, entity, details) return await conn.fetchval( "INSERT INTO audit_log (actor, action, entity, details, cid, prev_hash, hash) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id", actor, action, entity, json.loads(json.dumps(details, default=str)), correlation_id.get(), prev, h, ) async def verify_chain(pool: asyncpg.Pool, limit: int = 10000) -> tuple[bool, int]: rows = await pool.fetch("SELECT actor, action, entity, details, prev_hash, hash FROM audit_log ORDER BY id ASC LIMIT $1", limit) prev = None for i, r in enumerate(rows): if r["prev_hash"] != prev or _hash(prev, r["actor"], r["action"], r["entity"], r["details"]) != r["hash"]: return False, i prev = r["hash"] return True, len(rows)