SSyncropel Docs

Audit threads

Per-domain `th_audit_*` threads carry audit-grade records for governance operations. Use this catalog to map each governance domain to its audit thread, body kind, and operator subscription pattern.

TL;DR

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

Why two patterns

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 has a dedicated th_audit_<domain> thread when at least one of these criteria holds:

  • 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 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

ThreadBody kind(s)Pattern
th_audit_consentcore.consent_state_change.v1LEARN per grant/revoke transition; mirrors th_consent
th_audit_federation_pairssyncropel.federation.pair_state_change.v1LEARN per pair lifecycle transition; mirrors th_federation_pairs
th_audit_impersonationcore.impersonation.event.v1LEARN per impersonation entry/exit; no separate operational thread
th_audit_graph_traversal_<namespace>graph.consent_check.v1 (denials), related kindsPer-namespace; sovereignty boundary; emits on peer-side denial
th_secret_auditsecret.access.v1, secret.rotation.v1, secret.value_invalidated.v1DO synchronous BEFORE return; every read/write/list/delete
th_audit_portabilitycore.portability.event.v1LEARN per export/import; per-actor and instance scope
th_audit_snapshotscore.snapshot.event.v1LEARN per snapshot create / restore; security-sensitive instance-level op
th_audit_tokenscore.token_lifecycle_event.v1LEARN per token mint / revoke + SA create / delete / rotate; mirrors api_tokens_thread(<ns>) / service_accounts_thread(<ns>)
th_audit_provisioningcore.provisioning.requested.v1, core.provisioning.completed.v1LEARN at provisioning request + completion; lives on the platform instance
th_audit_subscriptionscore.subscription.* (12-member family)LEARN at every subscription lifecycle event (created, payment, cancellation, etc.); lives on the platform instance
th_audit_api_usagecore.api.usage.v1, core.api.error.v1, etc.LEARN per managed inference call; cost + token + tier tracking

Eleven audit-thread families today.

Catalog of operational-thread audit (no separation)

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

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}'

# decision-gate 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, decision 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 + the decision gate + dispatch + governance)
spl audit export --since 24h --categories system,decision,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 single-call SDK affordance (client.audit.subscribe()) is planned for a future release to collapse this into one subscription. For now, poll the audit threads with the loop above.

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, decision-gate 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>) for compliance-posture uniformity.

The criteria-driven hybrid above is the documented answer when an auditor asks "why is the audit pattern asymmetric across domains?"

Future changes

  • Future SDK affordance — client.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 the C1 / C2 / C3 criteria above. New th_audit_* threads without that justification are out of pattern.

On this page