CX · Reference guideaudit
PlaygroundDownloadsAboutv0.18.0-pre.1

audit

The audit sink: the one durable, attributed, append-only, queryable record of what the platform did, who asked, under whose authority, on what, and what was decided. An audit record is a closed-core [audit module= action= actor= authority= subject= decision= at= [detail …]] element — seven mandatory members, a decision from a closed five, an at that is an RFC 3339 instant with an explicit offset, and exactly one module-owned [detail] child this module moves verbatim and never parses. The sink is the reserved per-tenant cx:audit stream of a journal the deployment binds at boot from an already-open handle; durability, ordering, the hash chain, retention behind a signed snapshot, tamper-evidence and erasure are the journal's and are never re-specified here. A module whose specification says an act MUST be audited calls require at start-up and a deployment with no sink does not boot — never a first-use refusal, because a deployment that learns at 03:00 on its first break-glass login that nothing was recording has already lost the record. With no binding emit neither fails nor goes silent: it writes the record's one-line rendering to the effects-trace witness channel under the audit: prefix and answers a present receipt saying it was unbound — a fallback that is not durable, not attributed, not queryable and not retained. query is the one cross-module fold — what did this agent do last Tuesday and under whose grant — across the sink's stream and every module stream the deployment names, ordered by (at, stream, seq), which is a total order and explicitly not a linearization; summary counts those same records so a dashboard number and the rows behind it cannot disagree; verify answers a finding, never a fault; and export produces a re-derivable document. Three Ring 1 verbs — record, validate, fold-records — are pure: the constructor refuses and the checker reports, and the whole filtering surface is gradeable with no journal at all.

audit:record

[$audit:record] ($core::map $detail::element) -> element — Construct a validated [audit] record from a core map and the emitting module's own detail element; a malformed core is refused, never recorded.

      [?lib 'cx-platform/audit']
[?let [= $r [$audit:record {module: smtp action: :tls-policy actor: "service:mta-1"
                            authority: "boot" subject: "tcp://0.0.0.0:25"
                            decision: :announced at: "2026-09-12T04:10:00Z"}
                           [tls-downgrade setting=:none bind="tcp://0.0.0.0:25"]]]
  [probe [head [$name $r]] [module [$string $r@module]] [action $r@action]
         [decision $r@decision]]]
    
      [probe [head audit] [module smtp] [action :tls-policy] [decision :announced]]
    

audit:validate

[$audit:validate] ($record::element) -> element — Report whether a record in hand is well-formed as a present [audit-check] finding — never an [err], because a fault would destroy the very finding being sought.

      [?lib 'cx-platform/audit']
[$audit:validate [audit module=sso action=:login actor='principal:dana'
                    authority='d-1' subject='/sso/acme/login'
                    decision=:permit at='2026-09-12T03:00:00Z']]
    
      [audit-check valid=false reason=:detail-cardinality member=detail]
    

audit:fold-records

[$audit:fold-records] ($records::[sequence element] $filter::map {}) -> element — Filter and order an already-materialized sequence of [audit] values with no backend access — the pure core query is read-then-fold over.

      [?lib 'cx-platform/audit']
[?let [= $a [audit module=authz action=:check actor='principal:dana' authority='d-1'
               subject='/r/1' decision=:permit at='2026-09-08T09:00:00Z' [detail [authz-decision]]]]
      [= $b [audit module=authz action=:check actor='principal:dana' authority='d-1'
               subject='/r/2' decision=:deny at='2026-09-08T09:01:00Z' [detail [authz-decision]]]]
      [= $c [audit module=imap action=:autologout actor='service:imapd' authority=boot
               subject='tcp://0.0.0.0:143' decision=:refused at='2026-09-08T09:02:00Z' [detail [autologout]]]]
      [= $res [$audit:fold-records ($a, $b, $c) {decision: :deny}]]
  [probe [head [$name $res]] [total $res@total] [subject [?splice [?for [in $q $res/audit] [yield $q@subject]]]]]]
    
      [probe [head audit-result] [total 1] [subject /r/2]]
    

audit:bind

[$audit:bind] ($journal::element $opts::map {}) -> element — Install the process's one audit sink from an already-open [journal] handle, at boot; a re-bind to a different journal after a record exists is refused.

      [?lib 'cx-platform/audit']
[?lib 'cx-platform/journal']
[?let [= $fresh [$audit:unbind]]
      [= $j [$journal:open "mem://a040-acme" "acme"]]
      [= $s [$audit:bind $j {tenant: "acme"}]]
  [probe [head [$name $s]] [tenant [$string $s@tenant]] [stream [$string $s@stream]]
         [state [$string $s@state]] [head-seq $s@head-seq]]]
    
      [probe [head audit-sink] [tenant acme] [stream cx:audit] [state bound] [head-seq 0]]
    

audit:unbind

[$audit:unbind] () -> null — Release the process's audit binding; the journal handle is the caller's and is not closed.

      [?lib 'cx-platform/audit']
[?lib 'cx-platform/journal']
[?let [= $fresh [$audit:unbind]]
      [= $j [$journal:open "mem://a049-acme" "acme"]]
      [= $s [$audit:bind $j {tenant: "acme"}]]
      [= $u [$audit:unbind]]
  [probe [unbind-result [$present $u]] [sink-present [$present [$audit:sink]]]
         [journal-open [= [$string $j@state] "open"]]]]
    
      [probe [unbind-result true] [sink-present false] [journal-open true]]
    

audit:sink

[$audit:sink] () -> element — Answer the installed [audit-sink], or the absence channel when the process has no binding — never null.

      [?lib 'cx-platform/audit']
[?let [= $fresh [$audit:unbind]]
  [probe [present [$present [$audit:sink]]] [count [$count [$audit:sink]]]]]
    
      [probe [present false] [count 0]]
    

audit:require

[$audit:require] ($module::string $opts::map {}) -> element — Assert at BOOT that this deployment has an audit sink for a module whose specification requires one; unbound, it refuses and the deployment does not start.

      [?lib 'cx-platform/audit']
[?lib 'cx-platform/journal']
[?let [= $fresh [$audit:unbind]]
      [= $j [$journal:open "mem://a091-acme" "acme"]]
      [= $s [$audit:bind $j {tenant: "acme"}]]
      [= $r [$audit:require "sso"]]
  [probe [head [$name $r]] [module [$string $r@module]] [satisfied $r@satisfied]
         [stream [$string $r@stream]] [tenant [$string $r@tenant]]]]
    
      [probe [head audit-requirement] [module sso] [satisfied true] [stream cx:audit] [tenant acme]]
    

audit:emit

[$audit:emit] ($record::element $opts::map {}) -> element — Validate a record, lift its actor and authority into the journal attribution and append it to the bound sink; unbound it writes the fallback line and answers a receipt saying so.

      [?lib 'cx-platform/audit']
[?let [= $fresh [$audit:unbind]]
      [= $rc [$audit:emit [audit module=smtp action=:tls-policy actor='service:mta-1'
                             authority=boot subject='tcp://0.0.0.0:25' decision=:announced
                             at='2026-09-12T04:10:00Z'
                             [detail [tls-downgrade setting=:none bind='tcp://0.0.0.0:25']]]]]
  [probe [head [$name $rc]] [bound $rc@bound] [channel [$string $rc@channel]]]]
    
      [probe [head audit-receipt] [bound false] [channel audit:]]
    

audit:query

[$audit:query] ($filter::map {} $opts::map {}) -> element — The one cross-module fold: the records matching a closed filter set, across the sink's stream and every module stream named, windowed and ordered by (at, stream, seq).

      [?lib 'cx-platform/audit']
[?lib 'cx-platform/journal']
[?let [= $fresh [$audit:unbind]]
      [= $j [$journal:open "mem://a060-acme" "acme"]]
      [= $s [$audit:bind $j {tenant: "acme"}]]
      [= $e1 [$audit:emit [audit module=sso action=:login actor='principal:dana' authority='d-1'
                             subject='/sso/acme/session/7' decision=:permit
                             at='2026-09-08T09:00:00Z' [detail [login method=:saml]]]]]
      [= $e2 [$audit:emit [audit module=sso action=:login actor='principal:sam' authority='d-2'
                             subject='/sso/acme/session/8' decision=:permit
                             at='2026-09-08T09:05:00Z' [detail [login method=:saml]]]]]
      [= $res [$audit:query {actor: "principal:dana"}]]
  [probe [head [$name $res]] [total $res@total] [subject [?splice [?for [in $q $res/audit] [yield $q@subject]]]]]]
    
      [probe [head audit-result] [total 1] [subject /sso/acme/session/7]]
    

audit:summary

[$audit:summary] ($filter::map {} $opts::map {}) -> element — Count the records themselves over the same filter — by actor, by module, by decision — so a dashboard number and its rows are one fold and its projection.

      [?lib 'cx-platform/audit']
[?lib 'cx-platform/journal']
[?let [= $fresh [$audit:unbind]]
      [= $j [$journal:open "mem://a080-acme" "acme"]]
      [= $s [$audit:bind $j {tenant: "acme"}]]
      [= $e1 [$audit:emit [audit module=mailbox action=:label actor='agent:triage-1' authority='cap:1'
                             subject='/mail/acme/dana@acme.example/message/1' decision=:applied
                             at='2026-09-08T09:00:00Z' [detail [labelled count=1]]]]]
      [= $e2 [$audit:emit [audit module=mailbox action=:label actor='agent:triage-1' authority='cap:1'
                             subject='/mail/acme/dana@acme.example/message/2' decision=:applied
                             at='2026-09-08T09:01:00Z' [detail [labelled count=1]]]]]
      [= $e3 [$audit:emit [audit module=smtp action=:tls-policy actor='agent:triage-1' authority='cap:1'
                             subject='tcp://0.0.0.0:25' decision=:announced
                             at='2026-09-08T09:02:00Z' [detail [tls-downgrade setting=:none]]]]]
      [= $sum [$audit:summary {actor: "agent:triage-1"}]]
  [probe [head [$name $sum]] [total $sum@total]
         [by-module [?splice [?for [in $q $sum/by-module/count] [yield $q@n]]]]
         [by-decision [?splice [?for [in $q $sum/by-decision/count] [yield $q@n]]]]]]
    
      [probe [head audit-summary] [total 3] [by-module 2 1] [by-decision 2 1]]
    

audit:verify

[$audit:verify] ($opts::map {}) -> element — Verify the sink's chain and answer a FINDING — a present [audit-verification] whose valid= says what the chain is; an [err] only when the check cannot be performed.

      [?lib 'cx-platform/audit']
[?lib 'cx-platform/journal']
[?let [= $fresh [$audit:unbind]]
      [= $j [$journal:open "mem://a110-acme" "acme"]]
      [= $s [$audit:bind $j {tenant: "acme"}]]
      [= $e [$audit:emit [audit module=sso action=:login actor='principal:dana' authority='d-1'
                            subject='/sso/acme/login' decision=:permit
                            at='2026-09-12T03:00:00Z' [detail [login method=:saml]]]]]
      [= $v [$audit:verify]]
  [probe [head [$name $v]] [valid $v@valid] [streams $v@streams] [records $v@records]]]
    
      [probe [head audit-verification] [valid true] [streams 1] [records 1]]
    

audit:export

[$audit:export] ($filter::map {} $opts::map {}) -> element — Produce a re-derivable document over a window pinned by chain position — the artifact a counterparty verifies without access to the journal.

      [?lib 'cx-platform/audit']
[?lib 'cx-platform/journal']
[?let [= $fresh [$audit:unbind]]
      [= $j [$journal:open "mem://a118-acme" "acme"]]
      [= $s [$audit:bind $j {tenant: "acme"}]]
      [= $e1 [$audit:emit [audit module=sso action=:login actor='principal:dana' authority='d-1'
                             subject='/sso/acme/session/1' decision=:permit
                             at='2026-09-12T03:00:00Z' [detail [login method=:saml]]]]]
      [= $e2 [$audit:emit [audit module=sso action=:logout actor='principal:dana' authority='d-1'
                             subject='/sso/acme/session/1' decision=:applied
                             at='2026-09-12T03:30:00Z' [detail [logout]]]]]
      [= $f {from: "2026-09-01T00:00:00Z" to: "2026-10-01T00:00:00Z"}]
      [= $x1 [$audit:export $f {format: :cx sign: false}]]
      [= $x2 [$audit:export $f {format: :cx sign: false}]]
  [probe [records $x1@records] [same-head [= $x1@at-head-set $x2@at-head-set]]
         [same-id [= $x1@id $x2@id]]]]
    
      [probe [records 2] [same-head true] [same-id true]]