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
| Thread | Body kind(s) | Pattern |
|---|---|---|
th_audit_consent | core.consent_state_change.v1 | LEARN per grant/revoke transition; mirrors th_consent |
th_audit_federation_pairs | syncropel.federation.pair_state_change.v1 | LEARN per pair lifecycle transition; mirrors th_federation_pairs |
th_audit_impersonation | core.impersonation.event.v1 | LEARN per impersonation entry/exit; no separate operational thread |
th_audit_graph_traversal_<namespace> | graph.consent_check.v1 (denials), related kinds | Per-namespace; sovereignty boundary; emits on peer-side denial |
th_secret_audit | secret.access.v1, secret.rotation.v1, secret.value_invalidated.v1 | DO synchronous BEFORE return; every read/write/list/delete |
th_audit_portability | core.portability.event.v1 | LEARN per export/import; per-actor and instance scope |
th_audit_snapshots | core.snapshot.event.v1 | LEARN per snapshot create / restore; security-sensitive instance-level op |
th_audit_tokens | core.token_lifecycle_event.v1 | LEARN per token mint / revoke + SA create / delete / rotate; mirrors api_tokens_thread(<ns>) / service_accounts_thread(<ns>) |
th_audit_provisioning | core.provisioning.requested.v1, core.provisioning.completed.v1 | LEARN at provisioning request + completion; lives on the platform instance |
th_audit_subscriptions | core.subscription.* (12-member family) | LEARN at every subscription lifecycle event (created, payment, cancellation, etc.); lives on the platform instance |
th_audit_api_usage | core.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)
| Domain | Operational thread | Forensic fold |
|---|---|---|
| Sync pair | th_sync_control | LEARN with topic: pair_state (lifecycle transitions) + topic: pair_removed (terminal) |
| Namespaces | th_namespace_registry | LEARN 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
doneA 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_tokensrecords (core.token_lifecycle_event.v1LEARN) emitted alongside the operational records onapi_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.
Audit export
The `spl audit export` CLI emits security-relevant records as JSON Lines for SIEM ingestion, compliance archives, and retention workflows.
Publishing an Extension
How to ship a Syncropel iframe extension today — naming, hosting, capability declarations, security expectations, versioning. What is available now.