;; 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, and ;; serialize-for-disk. (define acl-audit-log (list)) (define acl-audit-seq 0) (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))))) ;; Serialize the whole log to a disk-ready string: one record per line, ;; "seq\tsubj\tact\tres\tallowed?". A host writes this; reload is out of scope. (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)))