audit

package
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package audit provides the pluggable audit persistence interface and concrete implementations for the Pasture epoch workflow audit trail.

The Trail interface is the core abstraction; all persistence is routed through it. Two implementations are provided:

  • InMemoryAuditTrail — for testing and local development (non-durable).
  • SqliteAuditTrail — for production use (durable, CGO-free via modernc.org/sqlite).

Temporal activity wrappers (RecordAuditEvent, QueryAuditEvents) delegate to a module-level Trail singleton that must be initialized via InitTrail before the worker starts.

Package audit — migrate.go

Migrate is the public entry point for the audit-database schema migration framework introduced by PROPOSAL-2 (§7.10). It is invoked by NewSqliteAuditTrail on every open and (in later slices) by the `pasture migrate` CLI command — both paths share this single implementation, so behaviour is identical.

Versioning model

  • Version is stored in the audit_schema_meta table (see schema_meta.go).
  • A database with no audit_schema_meta table is treated as version 1 (legacy, pre-PROPOSAL-2).
  • MaxKnownSchemaVersion is the highest version this binary knows how to produce. A database whose recorded version exceeds MaxKnownSchemaVersion is rejected with an actionable *StructuredError (Scenario 5).

Transactional guarantees

  • Each forward step runs inside a single transaction acquired via BEGIN IMMEDIATE so that a concurrent writer cannot interleave with the migration.
  • The audit_schema_meta version bump is the LAST statement in the transaction, so a crash mid-migration leaves the database observably at the previous version (rollback).

Migration table

  • v1 → v2: bootstrap audit_schema_meta (S1, landed).
  • v2 → v3: new tables context_edges, pasture_agent_categories, pasture_well_known_agents (S2, landed); audit_events.agent_id add + role-backfill + role-drop triple (S3, landed).
  • v3 → v4: EpochContext backfill from audit_events.epoch_id into context_edges; audit_events.epoch_id column dropped (S4, landed — migrate_v3_v4.go).

This binary tops out at v4. Future slices (e.g. v4 → v5) extend the dispatch table in migrationSteps() below by appending a new step and bumping MaxKnownSchemaVersion.

Package audit — migrate_v1_v2.go

The v1→v2 migration is the bootstrap step that introduces the audit_schema_meta table. It is intentionally a near-no-op: it creates the meta table (if not already created by an earlier partial run) and seeds (version=2, applied_at=<now>) so that subsequent migrations can branch on the recorded version.

All schema work is wrapped in the migrator's enclosing transaction (see migrate.go). v1→v2 itself does NOT touch audit_events or any other existing table — pre-PROPOSAL-2 data is preserved verbatim.

Package audit — migrate_v2_v3.go

The v2→v3 migration adds three new tables that underpin the unified pasture workflow record (PROPOSAL-2 §7.2):

  • context_edges — many-to-many event⇄context attachment, BCNF (composite-key, no non-key columns; §7.8).
  • pasture_agent_categories — typed categorisation rows for Provenance agents (R8); written by SetAgentCategories.
  • pasture_well_known_agents — stable logical-name → AgentId mapping for idempotent automaton registration at daemon startup (BLOCKER A2). UAT-1 schema invariant: (agent_id PK, name UNIQUE).

IMPORTANT scope boundary: this slice (S2) does NOT touch existing audit_events rows. The audit_events.agent_id column add + role-backfill + role-drop triple lives in S3 (migrate_v3 backfill) per BLOCKER A1, which requires the entire (create-column → backfill → drop-role) sequence to run in one BEGIN IMMEDIATE transaction. Workers integrating with this file downstream must not insert any audit_events DDL into migrateV2toV3.

Layer Integration Points exposed by this slice:

  • pasture_well_known_agents — DDL here; rows written by S7 (startup registration); cached AgentIDs read by S8 (activity attribution).
  • context_edges — DDL here; consumed by S5 (AttachContext writes), S6 (`pasture task contexts` reads), S8 (epoch attachment), S9 (free-floating contexts).
  • pasture_agent_categories — DDL here; written by S5 SetAgentCategories; read by S6 (`pasture task agents` listing).

Package audit — migrate_v3_backfill.go

S3 backfill: extends the v2→v3 transition (begun by S2 in migrate_v2_v3.go) with the audit_events.agent_id add + role-backfill + role-drop triple.

All five steps below run inside the SAME BEGIN IMMEDIATE transaction owned by migrate.runStep — including the audit_schema_meta version bump from 2 to 3, which is the LAST statement before tx.Commit per BLOCKER A1 (PROPOSAL-2 §7.10.2). A crash mid-way leaves the file at v2 (the transaction is rolled back); on the next open, Migrate observes MAX(version)=2 and re-runs the v3 step from scratch with no orphan agents_software rows (idempotency guaranteed by the find-or-create branch in step 3).

Pseudocode parity with PROPOSAL-2 §7.10.2:

  1. ALTER TABLE audit_events ADD COLUMN agent_id TEXT
  2. SELECT DISTINCT role FROM audit_events
  3. For each role: find-or-create a SoftwareAgent in agents_software named "pasture/legacy-role/<role>" via raw SQL (no Provenance Go API call). Find query: SELECT a.id FROM agents a JOIN agents_software s ... WHERE a.kind_id = 2 AND s.name = ? LIMIT 1. If absent, INSERT into agents (id, kind_id=2) and agents_software (agent_id, name, version, source). The INSERT shape MUST match Provenance's RegisterSoftwareAgent (provenance/internal/sqlite/agents.go:96-121) byte-for-byte so a future Provenance lookup of the agent succeeds.
  4. UPDATE audit_events SET agent_id = ? WHERE role = ? AND agent_id IS NULL
  5. SQLite table-rebuild to drop the role column: CREATE NEW → INSERT SELECT → DROP OLD → RENAME → recreate indexes.

C4 compliance: Provenance is NOT modified. The audit migrator opens its own *sql.DB against the same shared SQLite file and writes raw SQL that matches Provenance's INSERT shape exactly. Find-or-create on (kind_id=2, name) is keyed precisely so a partial run that crashed after agent creation but before the version bump reuses the existing row on retry rather than creating a duplicate.

Package audit — migrate_v3_backfill_export.go

Test-only public entry point so the cmd/pasture-migrate-crash binary can run the v3 backfill body (steps 0–5: ensure Provenance tables, add agent_id column, distinct roles, find-or-create agents, backfill agent_id, table-rebuild) WITHOUT the audit_schema_meta version bump or tx.Commit. The crash binary uses this to stage the partial v3 transaction state Scenario 11 needs to assert on.

Why this isn't gated by build tags

The cmd/pasture-migrate-crash binary is built by the standard `make build` target (HANDOFF §7) so it ships in dist/ alongside the production binaries. Build-tag gating would require a parallel build path; given the binary is small, useful only in test contexts, and imports from internal/audit (so accidental production use in any surface besides cmd/pasture-migrate-crash is not a concern), we expose this function unconditionally.

Workers extending the v3 backfill MUST keep the body of migrateV3Backfill in sync with what this function exposes — it is a thin re-export of the same private helper.

Package audit — migrate_v3_v4.go

S4 EpochContext migration: extends the schema chain with the v3→v4 step.

The v4 step does two pieces of work in the SAME BEGIN IMMEDIATE transaction owned by migrate.runStep:

  1. Backfill context_edges from audit_events.epoch_id. For every row in audit_events with non-NULL epoch_id, INSERT a context_edges (event_id, 'EpochContext', epoch_id) row. Uses INSERT OR IGNORE so a defensive replay (impossible in practice — the whole step is one transaction — but safe defensively) is a no-op against the composite PK (event_id, context_kind, context_id).
  2. SQLite table-rebuild to drop the epoch_id column from audit_events: CREATE NEW (without epoch_id) → INSERT SELECT → DROP OLD → RENAME → recreate post-v3 indexes.

The audit_schema_meta version bump from 3 to 4 is the LAST statement in the transaction (per BLOCKER A1 / PROPOSAL-2 §7.10.1 v4 row). A crash before tx.Commit rolls everything back, leaving the file observably at v3; the next open re-runs the v4 step from scratch (idempotent because step 1 uses INSERT OR IGNORE and step 2 lives entirely inside the rolled-back transaction).

Migration-note guarantee (PROPOSAL-2 §7.12, last paragraph)

Legacy audit_events.epoch_id values are migrated AS-IS into context_edges, regardless of whether the string parses as a Provenance TaskId. Free-string epoch IDs (e.g. "epoch-2026-04-22-mvp") and valid TaskIDs (e.g. "aura-plugins--01968a3c-1234-7000-8000-...") are both preserved because they are historical records and rejecting them would lose audit data. The §7.12 ParseTaskId validation applies only to NEW workflow starts post-migration; that validation is owned by S8.

Pseudocode parity with PROPOSAL-2 §7.10.1 v4 row:

  1. INSERT INTO context_edges (event_id, context_kind, context_id) SELECT id, 'EpochContext', epoch_id FROM audit_events WHERE epoch_id IS NOT NULL
  2. CREATE TABLE audit_events_new (no epoch_id column)
  3. INSERT INTO audit_events_new SELECT (no epoch_id) FROM audit_events
  4. DROP TABLE audit_events
  5. ALTER TABLE audit_events_new RENAME TO audit_events
  6. CREATE INDEX idx_audit_events_agent ON audit_events (agent_id)
  7. CREATE INDEX idx_audit_events_timestamp ON audit_events (timestamp)
  8. (Caller — migrate.runStep wrapper — calls writeVersion(4, ...) and Commit.)

Package audit — schema_meta.go

The audit_schema_meta table records the on-disk schema version of the pasture audit database. It is the single source of truth read by the migrator on every open to decide which forward migrations (if any) need to run, and to detect a database that is newer than the running binary supports.

Schema (per PROPOSAL-2 §7.10.1):

version    INTEGER PRIMARY KEY  — monotonically increasing schema version
applied_at INTEGER NOT NULL     — Unix nanoseconds UTC when the row landed

The table is created lazily by audit.Migrate (NOT by ensureSchema) so that pre-existing v1 databases — which predate this table — are still openable and detectable as "no audit_schema_meta row yet ⇒ version 1".

Index

Constants

View Source
const MaxKnownSchemaVersion = 4

MaxKnownSchemaVersion is the highest schema version this binary can produce. Bumped by S2 (→3, landed) and S4 (→4, landed).

Layer Integration Point owned by S1: any caller that needs to know "what version does my binary support?" reads this constant. The §11 Scenario 5 newer-schema rejection error reports this value as the "supported version" — bumping it here automatically updates the assertion.

Variables

This section is empty.

Functions

func InitTrail

func InitTrail(t Trail)

InitTrail injects the Trail implementation used by the activity wrappers.

Must be called once before the Temporal worker starts. Passing nil resets the singleton (useful in tests to isolate state between test cases).

This function is not concurrency-safe with activity execution; call it during worker startup, before any activities can run.

func Migrate

func Migrate(db *sql.DB) error

Migrate brings the audit database at db up to MaxKnownSchemaVersion. It is safe to call repeatedly: an already-current database is a no-op.

Behaviour summary:

  • If the database has no audit_schema_meta row yet (legacy v1), Migrate runs forward steps starting from v1.
  • If the database is already at MaxKnownSchemaVersion, Migrate returns nil without opening a transaction.
  • If the database is at a version higher than MaxKnownSchemaVersion (a future binary wrote it), Migrate returns a *StructuredError with Category=CategoryStorage. This is the "newer-schema rejection" path asserted by §11 Scenario 5.

Each step runs in its own BEGIN IMMEDIATE transaction so a crash between steps leaves the database at a consistent intermediate version.

Layer Integration Point: this signature is owned by S1 and consumed by S5 (OpenTaskTracker calls it) and S6 (`pasture migrate` calls it). Callers should treat the returned error as a *pasterrors.StructuredError (use errors.As).

func MigrateV3BackfillForCrashTest

func MigrateV3BackfillForCrashTest(tx *sql.Tx) error

MigrateV3BackfillForCrashTest runs the full v3 backfill body (migrateV3Backfill) against the supplied transaction WITHOUT calling writeVersion(3) and WITHOUT committing the transaction.

The caller is responsible for either:

  • Calling writeVersion(3, ...) and tx.Commit() — production parity, or
  • Calling tx.Commit() / tx.Rollback() / os.Exit() to inject a crash between the body's last statement and the commit (Scenario 11 test path).

nowUnixNano is reserved for future signature parity with migrateV3Backfill; this entry point ignores it.

This is the ONLY exported function in the audit package that bypasses the migration framework's transaction lifecycle. Production callers MUST use audit.Migrate or NewSqliteAuditTrail; this entry point is reserved for the crash-test binary.

func ReadSchemaVersion

func ReadSchemaVersion(db *sql.DB) (int, error)

ReadSchemaVersion returns the highest schema version recorded in audit_schema_meta. Returns 1 if the table does not exist (legacy pre-PROPOSAL-2 database). Returns 0 only on infrastructure failure (with a *pasterrors.StructuredError).

This is the public face of readVersion (which is package-private) so the `pasture migrate` CLI handler can probe the on-disk version WITHOUT running the migrator. Used by the dry-run path AND by the post-apply success line (which prints "migrated <db> from v<from> to v<to>").

Layer Integration Point owned by S1 (read helpers) + S6 (CLI consumer).

Types

type InMemoryAuditTrail

type InMemoryAuditTrail struct {
	// contains filtered or unexported fields
}

InMemoryAuditTrail is a Trail implementation backed by an in-memory slice.

Intended for testing and local development. Events are not persisted across process restarts. All methods are safe for concurrent use.

Synthetic event ids

RecordEventReturningId returns a synthetic monotonic counter value (lastEventId, starting at 1 on the first call). The counter is per-trail and increments under m.mu, so two concurrent callers always observe two distinct ids — matching the SQLite-backed trail's per-statement-LastInsertId guarantee. The id is NOT a real audit_events row id and MUST NOT be persisted across processes; it exists so test code that exercises AttachContext-style flows against the in-memory trail can rely on per-call uniqueness.

func NewInMemoryAuditTrail

func NewInMemoryAuditTrail() *InMemoryAuditTrail

NewInMemoryAuditTrail returns an empty, ready-to-use InMemoryAuditTrail.

func (*InMemoryAuditTrail) Events

func (m *InMemoryAuditTrail) Events() []protocol.AuditEvent

Events returns a defensive copy of all recorded events.

Intended for use in tests and assertions — callers receive a snapshot that is safe to inspect without holding the internal lock.

func (*InMemoryAuditTrail) QueryEvents

func (m *InMemoryAuditTrail) QueryEvents(_ context.Context, epochId string, phase *protocol.PhaseId, role *string) ([]protocol.AuditEvent, error)

QueryEvents returns all events matching the filters in insertion order.

epochId is required. phase and role are optional; nil means "no filter".

func (*InMemoryAuditTrail) QuerySessionEntries

func (m *InMemoryAuditTrail) QuerySessionEntries(_ context.Context, sessionId string) ([]protocol.SessionEntry, error)

QuerySessionEntries returns all session entries for the given sessionId in ascending entry_index order (ties broken by insertion order). This matches the SqliteAuditTrail contract (ORDER BY entry_index ASC, id ASC) so callers get the same ordering regardless of backend. Returns an empty (non-nil) slice when no entries exist. Safe for concurrent use.

func (*InMemoryAuditTrail) RecordEvent

func (m *InMemoryAuditTrail) RecordEvent(ctx context.Context, event protocol.AuditEvent) error

RecordEvent appends event to the in-memory list, discarding the synthetic row id. It is safe for concurrent use. See RecordEventReturningId for the id-returning variant; callers that need the id MUST use that.

func (*InMemoryAuditTrail) RecordEventReturningId

func (m *InMemoryAuditTrail) RecordEventReturningId(_ context.Context, event protocol.AuditEvent) (int64, error)

RecordEventReturningId appends event and returns a synthetic monotonic id (per-trail, starting at 1, incremented on every successful call). The id-and-append are performed atomically under m.mu so concurrent callers always observe distinct ids — matching the SQLite-backed trail's per-statement-LastInsertId race-safety guarantee.

The returned id is NOT a real audit_events row id and is only meaningful for the lifetime of this in-memory trail; do not persist it.

func (*InMemoryAuditTrail) RecordSessionEntries

func (m *InMemoryAuditTrail) RecordSessionEntries(_ context.Context, entries []protocol.SessionEntry) error

RecordSessionEntries appends the given entries to the in-memory session entry list. Nil or empty slices are accepted as no-ops. Safe for concurrent use.

type MigrationStepSummary

type MigrationStepSummary struct {
	FromVersion int
	ToVersion   int
	Description string
}

MigrationStepSummary describes one forward step the migrator WOULD apply against a database currently at FromVersion. Used by the `pasture migrate --dry-run` CLI subcommand to render the plan.

Description is the human-readable summary keyed by FromVersion + ToVersion; it MUST stay in sync with the actual migration body in migrate_v*.go (the CI dry-run integration test in S6 asserts the wording). When workers add a new vN→vN+1 migration, they ALSO add a Description entry to the stepDescription() map below — there is no other place to add it.

func PlanMigrations

func PlanMigrations(currentVersion int) []MigrationStepSummary

PlanMigrations returns the ordered list of forward steps the migrator WOULD apply against a database currently at currentVersion. An empty slice means the database is already at MaxKnownSchemaVersion (no work to do).

The function is pure: it touches no on-disk state. It iterates the same registry as Migrate so the dry-run is guaranteed to match the apply.

If currentVersion is greater than MaxKnownSchemaVersion (a future binary wrote the database), PlanMigrations returns an empty slice; the CLI handler is responsible for surfacing the newer-schema rejection error separately via Migrate (which returns the actionable *StructuredError).

Layer Integration Point owned by S1 + S6.

type SqliteAuditTrail

type SqliteAuditTrail struct {
	// contains filtered or unexported fields
}

SqliteAuditTrail is a Trail implementation backed by a SQLite database.

Uses modernc.org/sqlite (pure Go, CGO_ENABLED=0 compatible — no C toolchain required). Events survive process restarts. The database file and any intermediate parent directories are created on first open.

Schema (audit_events table, post-v3 — see PROPOSAL-2 §7.10.1):

id         INTEGER PRIMARY KEY AUTOINCREMENT
epoch_id   TEXT             (legacy column, dropped in v4)
phase      TEXT             (NULLABLE in v3; legacy v1 had NOT NULL)
agent_id   TEXT NOT NULL    (FK-shaped reference to agents.id)
event_type TEXT NOT NULL    (protocol.EventType string value)
payload    TEXT NOT NULL    (JSON-encoded map[string]any)
timestamp  INTEGER NOT NULL (Unix nanoseconds UTC)

Legacy-role compatibility shim

PROPOSAL-2 §7.11 plans for S8 to replace direct Trail.RecordEvent(role) calls with TaskTracker.RecordEvent(agent_id) at the workflow boundary. Until S8 lands, callers continue to pass a free-string Role on protocol.AuditEvent — the v3 schema drops role but keeps agent_id, so SqliteAuditTrail bridges the two by find-or-creating a SoftwareAgent named "pasture/legacy-role/<role>" via the same raw-SQL path the v3 migration uses (migrate_v3_backfill.go), and writes the resulting agent_id into the new column. QueryEvents joins back through agents_software to repopulate event.Role for the caller, preserving the existing API byte-for-byte.

The mapping is cached in roleToAgentId so a write-heavy workload pays the find-or-create cost only on the first event per role per process.

All methods are safe for concurrent use; SQLite WAL mode is enabled to allow concurrent readers alongside a single writer. The roleToAgentId cache is guarded by roleMu.

func NewSqliteAuditTrail

func NewSqliteAuditTrail(dbPath string) (*SqliteAuditTrail, error)

NewSqliteAuditTrail opens (or creates) the SQLite database at dbPath, applies the schema, and enables WAL mode for concurrent access.

dbPath: Filesystem path to the SQLite database file. Parent directories are created if they do not exist.

Returns an error if:

  • The parent directory cannot be created (permissions, disk full).
  • The database file cannot be opened.
  • Schema migration fails.

The caller must call Close when done to release the file handle.

func (*SqliteAuditTrail) Close

func (s *SqliteAuditTrail) Close() error

Close releases the underlying database connection. Must be called when the trail is no longer needed to avoid resource leaks.

func (*SqliteAuditTrail) QueryEvents

func (s *SqliteAuditTrail) QueryEvents(_ context.Context, epochId string, phase *protocol.PhaseId, role *string) ([]protocol.AuditEvent, error)

QueryEvents returns audit events matching the given filters in chronological order (ascending row id, which equals insertion order).

epochId is required and is always part of the WHERE clause. phase and role are optional; nil means "no filter".

Legacy-role compatibility: the v3 schema dropped audit_events.role and replaced it with agent_id. To preserve the existing API where callers filter by role and read event.Role on the result, this method LEFT JOINs audit_events with agents_software (via agent_id) and:

  • When role != nil, restricts the JOIN target to s.name = "pasture/legacy-role/<role>".
  • When reading rows, strips the "pasture/legacy-role/" prefix from the joined name to repopulate event.Role. Agents whose name does not match the legacy prefix (e.g. S7 well-known automaton agents) report the full name as-is so the caller still gets a non-empty Role.

LEFT JOIN (rather than INNER JOIN) defends against orphan agent_id values that have no agents_software row — those rows are returned with an empty Role rather than dropped silently.

func (*SqliteAuditTrail) QuerySessionEntries

func (s *SqliteAuditTrail) QuerySessionEntries(_ context.Context, sessionId string) ([]protocol.SessionEntry, error)

QuerySessionEntries returns all session entries for the given sessionId in ascending entry_index order (matching insertion order for well-formed data).

Returns an empty (non-nil) slice when no entries exist for sessionId.

func (*SqliteAuditTrail) RecordEvent

func (s *SqliteAuditTrail) RecordEvent(ctx context.Context, event protocol.AuditEvent) error

RecordEvent persists a single AuditEvent to the SQLite database, discarding the inserted row id. It is a thin wrapper over RecordEventReturningId for callers that record-and-forget; callers that need the inserted id (e.g. to follow up with AttachContext) MUST use RecordEventReturningId directly.

See RecordEventReturningId for the full INSERT semantics, transaction boundary, validation rules, and error categories.

func (*SqliteAuditTrail) RecordEventReturningId

func (s *SqliteAuditTrail) RecordEventReturningId(_ context.Context, event protocol.AuditEvent) (int64, error)

RecordEventReturningId persists a single AuditEvent and returns the audit_events.id of the newly-inserted row.

Race safety: the row id is recovered via sql.Result.LastInsertId on the SAME INSERT statement that wrote the row, INSIDE the same transaction. This is race-free under any level of write contention — the driver tracks the rowid per-statement, not per-connection — and replaces the older "RecordEvent then SELECT MAX(id)" workaround that could return a row id belonging to a concurrent writer (PROPOSAL-2 §7.11 future-work, realised in Phase 11 R1-B).

Timestamp is stored as Unix nanoseconds (INTEGER) for exact round-trip without format parsing overhead.

Legacy-role compatibility (PROPOSAL-2 §7.10.2 + this file's type doc): the v3 schema dropped audit_events.role and replaced it with agent_id. Until S8 wires TaskTracker.RecordEvent(agent_id) at the workflow boundary, callers continue to set event.Role on protocol.AuditEvent. This method bridges by resolving Role to a SoftwareAgent named "pasture/legacy-role/<role>" via raw SQL on agents_software (find or create) — the same shape the v3 migration uses — and writes that agent_id into the new column. The role→agent_id mapping is cached in s.roleToAgentId for write-amortised cost.

An empty event.Role is rejected with CategoryValidation; the v3 schema requires a non-NULL agent_id and there is no sensible default.

Returns (id, nil) on success or (0, *pasterrors.StructuredError) on failure.

func (*SqliteAuditTrail) RecordSessionEntries

func (s *SqliteAuditTrail) RecordSessionEntries(_ context.Context, entries []protocol.SessionEntry) error

RecordSessionEntries persists a batch of SessionEntry records to the SQLite database in a single transaction. Nil or empty slices are accepted as no-ops.

All entries are written atomically; if any INSERT fails the entire batch is rolled back so callers can retry safely.

type Trail

type Trail interface {
	// RecordEvent persists a single audit event.
	//
	// Returns an error if the underlying store is unavailable or the write
	// fails. The caller (Temporal activity) is responsible for retry policy.
	//
	// Callers that need the inserted row id (so they can attach context_edges
	// rows in the same logical step) MUST use RecordEventReturningId instead.
	// RecordEvent is retained for callers that record-and-forget; it is a
	// thin wrapper over RecordEventReturningId that discards the id.
	RecordEvent(ctx context.Context, event protocol.AuditEvent) error

	// RecordEventReturningId persists a single audit event and returns the
	// audit_events.id of the newly-inserted row.
	//
	// Implementations MUST recover the id from the same INSERT statement
	// that wrote the row (e.g. via sql.Result.LastInsertId for SQLite-backed
	// trails) so the returned id is race-safe under concurrent writes.
	// A separate post-write SELECT MAX(id) workaround is forbidden — that
	// pattern races with a concurrent writer's INSERT and can return a row
	// id that belongs to a DIFFERENT goroutine.
	//
	// On the SQLite backend this is the canonical write path; RecordEvent
	// delegates here and discards the id. On the in-memory backend the id
	// is a synthetic monotonic counter scoped to the trail's lifetime — it
	// is NOT a real audit_events row id and MUST NOT be persisted across
	// processes; the counter exists so test code that asserts unique-id-
	// per-call behaviour works against the in-memory trail too.
	//
	// Returns the new id and a nil error on success; on failure returns 0
	// and an error (typically *pasterrors.StructuredError on the SQLite
	// backend so callers can errors.As() it for category-based handling).
	RecordEventReturningId(ctx context.Context, event protocol.AuditEvent) (int64, error)

	// QueryEvents returns all audit events matching the given filters.
	//
	// epochId is required and filters by exact match. phase and role are
	// optional; a nil pointer means "no filter on this field". Results are
	// returned in chronological order (insertion order for in-memory,
	// ascending id order for SQLite).
	QueryEvents(ctx context.Context, epochId string, phase *protocol.PhaseId, role *string) ([]protocol.AuditEvent, error)

	// RecordSessionEntries persists a batch of SessionEntry records.
	//
	// Entries are appended; existing entries for the same session are not
	// replaced. The caller should pass all entries for a turn in a single call
	// for efficient batch INSERT in the SQLite backend.
	//
	// Returns an error if the underlying store is unavailable or the write
	// fails. The caller (Temporal activity) is responsible for retry policy.
	RecordSessionEntries(ctx context.Context, entries []protocol.SessionEntry) error

	// QuerySessionEntries returns all session entries for the given sessionId
	// in ascending entry_index order (ties broken by insertion order). Both
	// backends honor this contract identically: SQLite via
	// "ORDER BY entry_index ASC, id ASC" and in-memory via a stable sort
	// (previously the two backends diverged).
	//
	// Returns an empty (non-nil) slice when no entries exist for sessionId.
	// Returns an error if the underlying store is unavailable.
	QuerySessionEntries(ctx context.Context, sessionId string) ([]protocol.SessionEntry, error)
}

Trail is the pluggable audit persistence interface.

All implementations must be safe for concurrent use (multiple goroutines may call RecordEvent and QueryEvents simultaneously inside a Temporal worker).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL