;; lib/acl/audit.sx — append-only decision log. ;; ;; Every decision routed through acl-audit-decide! is appended to an in-memory ;; log with a monotonic sequence number (no wall-clock — deterministic and ;; testable; a host can stamp time at the serializer boundary). The log is ;; append-only: there is no mutate or delete, only append, tail, clear, ;; snapshot/restore, and serialize-for-disk. (define acl-audit-log (list)) (define acl-audit-seq 0) ;; Copy a list into a fresh, append!-able list. `map`/`rest`-derived lists are ;; NOT extensible by append! in this runtime (it silently no-ops), so the live ;; log must always be a list built with `list` + `append!`. (define acl-audit-copy (fn (xs) (let ((fresh (list))) (do (for-each (fn (e) (append! fresh e)) xs) fresh)))) (define acl-audit-clear! (fn () (do (set! acl-audit-log (list)) (set! acl-audit-seq 0) nil))) ;; Append a decision record. Returns the record. (define acl-audit-record! (fn (subj act res allowed?) (let ((entry {:allowed? allowed? :act act :subj subj :res res :seq acl-audit-seq})) (do (set! acl-audit-seq (+ acl-audit-seq 1)) (append! acl-audit-log entry) entry)))) ;; Decide against db, log the outcome, and return the boolean. This is the ;; audited path; acl-permit? remains the pure, side-effect-free decision. (define acl-audit-decide! (fn (db subj act res) (let ((allowed? (acl-permit? db subj act res))) (do (acl-audit-record! subj act res allowed?) allowed?)))) (define acl-audit-count (fn () (len acl-audit-log))) ;; Most recent n entries (in chronological order). n >= log size returns all. (define acl-audit-tail (fn (n) (let ((total (len acl-audit-log))) (if (<= total n) acl-audit-log (acl-audit-drop acl-audit-log (- total n)))))) (define acl-audit-drop (fn (xs k) (if (<= k 0) xs (acl-audit-drop (rest xs) (- k 1))))) ;; Structured snapshot for save/restore — a {:seq :entries} value carrying a ;; copy of the log (so later appends don't mutate a held snapshot). (define acl-audit-snapshot (fn () {:seq acl-audit-seq :entries (acl-audit-copy acl-audit-log)})) ;; Replace the live log from a snapshot. Restores both entries and the seq ;; counter so subsequent records continue numbering correctly. The log is ;; rebuilt as a fresh append!-able list (see acl-audit-copy). (define acl-audit-restore! (fn (snap) (do (set! acl-audit-log (acl-audit-copy (get snap :entries))) (set! acl-audit-seq (get snap :seq)) nil))) ;; Serialize the whole log to a disk-ready string: one record per line, ;; "seq\tsubj\tact\tres\tallowed?". A host writes this; structured reload is via ;; snapshot/restore. (define acl-audit-serialize (fn () (reduce (fn (acc e) (str acc (get e :seq) "\t" (get e :subj) "\t" (get e :act) "\t" (get e :res) "\t" (get e :allowed?) "\n")) "" acl-audit-log)))