Skip to content

Tracker audit log

Every state move made by a Bernstein agent against a ticket tracker (claim, comment, transition, attach, fail) is recorded as a signed content-addressed JSONL entry. The artefact is auditor-readable and maps directly to the per-decision evidence requirements asked for by SOX, SOC 2 Type II, and the EU AI Act.

On-disk layout

  • Path: .sdd/lineage/tracker_audit.jsonl
  • Format: append-only JSONL, one entry per line, RFC 8785 JCS canonical bytes.
  • The file is gitignored; operator-side WORM storage is out of scope of this module (see the Out of scope section below).

Entry schema (schema_version: 1)

Field Type Description
schema_version int Always 1 in this release. Bumping the value requires a parallel reader for the prior version.
id string UUIDv7 hex; time-sortable when running on Python 3.13+.
ts_ns int Nanoseconds since the Unix epoch.
prev_entry_hash string sha256: digest of the previous entry, or sha256:000...0 for genesis.
entry_hash string sha256: digest of the entry's canonical bytes with entry_hash and signature blanked.
tracker_name string Adapter name (jira, linear, github, ...).
ticket_id string Stable tracker-side identifier.
etag_before string|null Optimistic-concurrency etag observed before the call.
etag_after string|null Etag returned by the tracker after the call.
action string One of claim, comment, transition, attach, fail.
actor.session_id string Bernstein session id.
actor.role string Role prompt that produced the call (backend, qa, ...).
actor.model string Model identifier used for the call.
input_prompt_hash string sha256: digest of the input prompt bytes.
output_blob_hash string sha256: digest of the response payload bytes.
cost_usd float USD spend attributed to the call.
tokens_in int Input tokens.
tokens_out int Output tokens.
idempotency_key string|null Tracker-side idempotency key, when supplied.
lifecycle_event_id string|null Cross-link to the originating lifecycle hook event.
signature string HMAC-SHA256 over the canonical bytes (with signature blanked) under the operator secret.
failure_category string|null Exception class name when action == "fail".
failure_detail string|null Truncated failure message (max 512 chars).

The auditor verifies the chain end-to-end by walking each line, hashing the canonical bytes, and comparing both the entry_hash and the HMAC signature.

Field-to-question mapping

Auditor question Field(s)
SOX-2026: which automated control changed state at <time>? ts_ns, tracker_name, ticket_id, action
SOC 2 Type II CC7.1 (change identification): who initiated the action? actor.session_id, actor.role, actor.model
EU AI Act Article 12 logging: which model and prompt produced the output? actor.model, input_prompt_hash, output_blob_hash
SOC 2 CC6.1 (logical access): is the chain tamper-evident? prev_entry_hash, entry_hash, signature
Cost-attribution: how much did the change cost? cost_usd, tokens_in, tokens_out
Concurrency control: what etags did we observe? etag_before, etag_after
Replay: how do I match this entry to the orchestrator's lifecycle event? lifecycle_event_id
Idempotency: did a retry collide with an earlier call? idempotency_key

Operator commands

# Show the chronological view, optionally filtered by tracker / ticket.
bernstein lineage tracker-audit show --tracker jira --ticket PROJ-1

# Periodic export window (nanoseconds since the Unix epoch).
bernstein lineage tracker-audit show --since 1715000000000000000

# Auditor bundle.
bernstein lineage tracker-audit export --output /tmp/bundle.jsonl

# Tamper detection. Exits non-zero on the first offending line.
bernstein lineage tracker-audit verify

All commands read the HMAC operator secret from the BERNSTEIN_OPERATOR_SECRET environment variable by default; override with --secret-env.

Wiring an adapter

Adapters opt in by being wrapped at the orchestrator boundary. The shipped wrapper:

from bernstein.core.lineage import LineageCtx, TrackerActor, TrackerAuditLog, wrap_adapter

log = TrackerAuditLog(Path(".sdd/lineage/tracker_audit.jsonl"), hmac_key=key)
ctx = LineageCtx(log=log, actor=TrackerActor(session_id=sid, role=role, model=model))
adapter = wrap_adapter(jira_adapter, ctx)
adapter.add_comment("PROJ-1", "hello", idempotency_key="idem-1")

The wrapper emits one entry on success and one entry with action="fail" on exception, then re-raises so the orchestrator's retry policy keeps working.

Out of scope

  • WORM storage of the JSONL. Pipe the file to your evidence store of choice (S3 Object Lock, Azure Immutable Blob, GCS Bucket Lock).
  • Auto-ingestion into vendor SIEMs. File a separate ticket per SIEM.
  • Sigstore attestation on top of the HMAC envelope. Tracked as a follow-up.