# ============================================================================= # Projet : AIR — Accounting Intermediate Representation # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : audit.py # Description : Append-only, hash-chained audit log — every agent syscall is journaled here. # ============================================================================= """The audit log: who did what, in order, tamper-evident. Every syscall an agent makes (successful OR failed) appends one record. Records are hash-chained exactly like the ledger (SHA-256 over the canonical JSON of the record plus the previous hash), so any after-the-fact edit, deletion, or reordering breaks verification. Nothing is ever rewritten. """ from __future__ import annotations import hashlib import json from dataclasses import dataclass, field from pathlib import Path from typing import Any GENESIS_HASH = "0" * 64 _AUTHOR = "Simon-Pierre Boucher " def _canonical(record: dict[str, Any]) -> str: return json.dumps(record, sort_keys=True, separators=(",", ":"), default=str) @dataclass class AuditLog: path: Path | None = None _records: list[dict[str, Any]] = field(default_factory=list) def __post_init__(self) -> None: if self.path is not None and self.path.exists(): for raw in self.path.read_text(encoding="utf-8").splitlines(): if raw.strip(): self._records.append(json.loads(raw)) def append( self, actor: str, syscall: str, params: dict[str, Any], status: str, # "ok" | "error" detail: str = "", at: str | None = None, ) -> dict[str, Any]: record: dict[str, Any] = { "seq": len(self._records), "actor": actor, "syscall": syscall, "params": params, "status": status, "detail": detail, "at": at, } prev = self._records[-1]["hash"] if self._records else GENESIS_HASH record["prev_hash"] = prev record["hash"] = hashlib.sha256( (prev + _canonical({k: v for k, v in record.items() if k != "hash"})) .encode("utf-8") ).hexdigest() self._records.append(record) if self.path is not None: self.path.parent.mkdir(parents=True, exist_ok=True) with self.path.open("a", encoding="utf-8") as f: f.write(_canonical(record) + "\n") return record def verify_chain(self) -> bool: prev = GENESIS_HASH for record in self._records: expected = hashlib.sha256( (prev + _canonical({k: v for k, v in record.items() if k != "hash"})) .encode("utf-8") ).hexdigest() if record.get("hash") != expected or record.get("prev_hash") != prev: return False prev = str(record["hash"]) return True @property def records(self) -> list[dict[str, Any]]: return list(self._records)