SSyncropel Docs

Audit threads

Per-domain `th_audit_*` threads carry audit-grade records for governance operations. This page maps every audit thread to its source ADR, body kind, and operator subscription pattern.

TL;DR

Some governance domains (consent, federation pairs, secrets, impersonation, graph traversal, portability, snapshot/restore, tokens + service accounts) emit a dedicated audit record on a th_audit_<domain> thread in addition to the operational record. Other domains (sync pairs, namespaces, AITL) use the operational thread itself as the audit trail. Both are audit-grade — operators get the same forensic content either way. The asymmetry follows the criteria documented in ADR-083.

Why two patterns

The substrate doctrine "state = fold(records)" guarantees that every governance op emits a record fully capturing the mutation. An operator can fold the operational thread (th_namespace_registry, th_sync_control, etc.) for the same forensic answer they would get from a dedicated audit thread. The choice between patterns is operator subscription ergonomics, not correctness.

A domain SHOULD have a dedicated th_audit_<domain> thread when at least one of these criteria holds (per ADR-083 D1):

  • C1 — Cross-actor / cross-instance visibility. The mutation has consequences visible to parties beyond the invoking actor (consent grants bind two namespaces; federation pairs bind two instances).
  • C2 — Security-sensitive synchronous emission. The audit record MUST land on the substrate before the operation result returns (secret read/write, impersonation entry/exit).
  • C3 — High-frequency operator subscription stream. Roughly ≥ 100 events/day in normal operation, AND those events have consequence enough to warrant a dedicated stream (graph traversal denials per namespace, secret access).

If none of C1/C2/C3 holds, the operational thread serves as the audit trail.

Catalog of audit threads (today)

ThreadBody kind(s)Source ADRPattern
th_audit_consentcore.consent_state_change.v1ADR-078 D5LEARN per grant/revoke transition; mirrors th_consent
th_audit_federation_pairssyncropel.federation.pair_state_change.v1ADR-063 D7LEARN per pair lifecycle transition; mirrors th_federation_pairs
th_audit_impersonationcore.impersonation.event.v1ADR-032 D5LEARN per impersonation entry/exit; no separate operational thread
th_audit_graph_traversal_<namespace>graph.consent_check.v1 (denials), related kindsADR-080 D7Per-namespace; sovereignty boundary; emits on peer-side denial
th_secret_auditsecret.access.v1, secret.rotation.v1, secret.value_invalidated.v1ADR-062, SKL-0448DO synchronous BEFORE return; every read/write/list/delete
th_audit_portabilitycore.portability.event.v1SKL-0660LEARN per export/import; per-actor and instance scope
th_audit_snapshotscore.snapshot.event.v1SKL-0676LEARN per snapshot create / restore; criterion C2 (security-sensitive instance-level operation)
th_audit_tokenscore.token_lifecycle_event.v1ADR-083 D4, SKL-0677LEARN per token mint / revoke + SA create / delete / rotate; mirrors api_tokens_thread(<ns>) / service_accounts_thread(<ns>)

Eight audit-thread families today. Each was justified at its source ADR against criteria C1/C2/C3 (formalized retroactively by ADR-083).

Catalog of operational-thread audit (no separation)

DomainOperational threadForensic fold
Sync pair (ADR-030)th_sync_controlLEARN with topic: pair_state (lifecycle transitions) + topic: pair_removed (terminal)
Namespacesth_namespace_registryLEARN with topic: namespace_entry (create / archive / unarchive / delete)
AITL approve/reject(the original AITL proposal thread)DO with body.fulfills or body.cancels — the decision IS the audit per the AITL spec

These domains satisfy none of the C1/C2/C3 criteria strongly enough to warrant separation today. The operational thread is the canonical audit trail; folding it gives operators the same forensic content they would get from a hypothetical sibling audit thread.

Operator subscription patterns

Subscribe to one audit thread

Use the records HTTP endpoint with thread= filter and (optionally) since= for cursor-based pagination:

TOKEN=$(cat ~/.syncro/token)

# Every consent state-change event in the last 24 hours
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:9100/v1/records?thread=th_audit_consent&since=$(date -u -d '24 hours ago' +%s)" \
  | jq '.data[]'

# Every secret access event (synchronous-before-return; high frequency)
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:9100/v1/records?thread=th_secret_audit&limit=100" \
  | jq '.data[] | {clock, actor, body}'

# Per-namespace graph traversal denials
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:9100/v1/records?thread=th_audit_graph_traversal_default" \
  | jq '.data[]'

Fold an operational thread for audit

For the unseparated domains, the same pattern works against the operational thread. The records carry the same fields you would expect on an audit thread (actor, clock, body):

# Every namespace lifecycle event
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:9100/v1/threads/th_namespace_registry/records" \
  | jq '.data[] | select(.body.topic == "namespace_entry") | {clock, actor, body}'

# AITL decisions on a specific proposal thread
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:9100/v1/threads/<proposal_thread_id>/records" \
  | jq '.data[] | select(.act == "DO") | select(.body.fulfills or .body.cancels)'

Bulk export to SIEM

spl audit export ships a curated default that includes system actors, AITL verdicts, dispatch completions, and governance denials. It does NOT yet include the per-domain audit threads above; for full coverage today, combine the export with thread-by-thread record reads.

See Audit export for the SIEM bridge.

# Curated SIEM stream (system + AITL + dispatch + governance)
spl audit export --since 24h --categories system,aitl,dispatch,governance > /var/log/spl-audit.jsonl

# Add per-domain audit threads as needed
for thread in th_audit_consent th_audit_federation_pairs th_audit_impersonation \
              th_secret_audit th_audit_portability th_audit_snapshots th_audit_security \
              th_audit_tokens; do
  curl -s -H "Authorization: Bearer $TOKEN" \
    "http://localhost:9100/v1/threads/$thread/records" \
    | jq -c '.data[]' >> /var/log/spl-audit.jsonl
done

A future SDK affordance (client.audit.subscribe()) is under consideration to collapse this into a single subscription. Track the SDK roadmap for ETA.

Compliance posture notes

For SOC2 / PCI / NIST 800-53 auditor-facing reports, the simplest narrative is:

  • Cross-actor and cross-instance governance (consent grants, federation pairs, graph traversal denials) is audit-traced via the dedicated th_audit_* threads listed above.
  • Security-sensitive credential and identity operations (secrets, impersonation) are audit-traced via dedicated audit threads with synchronous-before-return emission.
  • Single-instance administrative operations (namespace lifecycle, AITL decisions, sync pair lifecycle) are audit-traced via the operational record on the relevant operational thread; folding the thread gives the canonical forensic answer.
  • Token + service-account lifecycle is audit-traced via dedicated th_audit_tokens records (core.token_lifecycle_event.v1 LEARN) emitted alongside the operational records on api_tokens_thread(<namespace>) / service_accounts_thread(<namespace>). Per ADR-083 D4, this satisfies criterion C2 marginally with compliance-posture uniformity as the primary motivation.

ADR-083 is the canonical reference an auditor or operator can cite when asking "why is the audit pattern asymmetric across domains?" — the criteria-driven hybrid is the documented answer.

Future changes

  • Future SDK affordanceclient.audit.subscribe() to subscribe to all audit threads in one call. Tracked when SDK demand surfaces.

Any future audit-thread proposal MUST justify the choice against ADR-083 D1's criteria (C1 / C2 / C3). New th_audit_* threads without that justification are out of pattern.

On this page