audit

package
v0.0.0-...-27a64c2 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package audit defines the SPI for the immutable audit/trace store.

Every significant event in the control plane lifecycle — intent submission, policy decision, approval action, execution outcome — is recorded as an append-only trace event. This provides a verifiable audit trail for compliance, debugging, and trust.

The built-in implementation writes to a Postgres append-only table. Alternative implementations can target immutable object storage, a dedicated audit log service, or a blockchain-backed store.

Index

Constants

View Source
const (
	// AttrSigningKeyID is the public-key fingerprint that signed an
	// audit event under the Stage-1.5 hash-chained audit log (#398).
	// Empty / absent on every event today; populated when signing
	// lands.
	AttrSigningKeyID = "aileron.signing.key_id"

	// AttrPolicyDecision is the verdict a pre-execution policy hook
	// returned for the action that produced this event: "allow",
	// "deny", "require_approval", or an opaque decision-source
	// reference (#396, #399). Empty / absent on every event today;
	// populated when the policy hook lands.
	AttrPolicyDecision = "aileron.policy.decision"
)

Reserved attribute keys for the audit-event payload.

These names are declared as part of the ADR-0010 schema but no recorder emits them yet. They exist so future implementations of signed audit logs (#398) and the external policy hook (#396, #399) pick up the same spelling rather than choosing alternates that would force a downstream-visible rename later.

Both names follow the OTel-namespaced convention shared with the rest of the audit payload (#390 Phase 6.5). When the corresponding features ship, recorders import these constants instead of writing the string literally.

Reservations were committed in #404's "Schema lock-in" track and in the per-issue Phase 6.5 commitments on #398 and #399. The rename portion of Phase 6.5 shipped in #452; this file is the reservation portion.

Variables

This section is empty.

Functions

func AppendMessageEntry

func AppendMessageEntry(path string, entry MessageEntry) error

AppendMessageEntry appends one message audit entry to the JSONL log file.

func DailyDir

func DailyDir(stateDir string) string

DailyDir returns the subdirectory path the daily files live in: `<stateDir>/audit`. Useful for read-side callers that scan across every daily file (e.g., `aileron policy save`).

func DailyPath

func DailyPath(stateDir string) string

DailyPath returns today's audit JSONL file path inside stateDir, using the local clock. Production callers compute the path on each write so a session that crosses midnight rolls naturally to the next day's file.

func DailyPathAt

func DailyPathAt(stateDir string, t time.Time) string

DailyPathAt is the time-injectable variant of DailyPath. Tests use it to drive midnight-rollover scenarios without sleeping.

func DefaultIDFn

func DefaultIDFn() string

DefaultIDFn returns a UUIDv4-based audit_id prefixed with `audit-` per ADR-0010's example shape.

Types

type Clock

type Clock interface {
	Now() time.Time
}

Clock is the minimal interface Recorder needs from a clock. It matches the shape of internal/clock.Clock without taking the dependency, so this package stays small.

type Event

type Event struct {
	EventID   string
	EventType model.EventType
	Actor     model.ActorRef
	// Payload carries event-specific data. Must be serialisable to JSON.
	Payload   map[string]any
	Timestamp time.Time
}

Event is a single immutable audit record.

type EventFilter

type EventFilter struct {
	// Since is an inclusive lower bound on Event.Timestamp. Zero
	// disables the bound.
	Since time.Time
	// EventID, when non-empty, restricts the result to the single
	// event with this id (or empty when no such event exists).
	EventID string
	// ConnectorFQN, when non-empty, matches events whose payload
	// references this FQN under any of the recorder's connector
	// keys: `aileron.connector.fqn` (binding lifecycle and
	// connector install events), `aileron.action.fqn` (action
	// install events — when filtering by the action's own FQN),
	// or `aileron.failure.details.connector` (failure events).
	ConnectorFQN string
	// Class, when non-empty, matches events whose payload carries
	// this failure class. Has no effect on success events (they
	// don't record a class), so a Class filter naturally narrows
	// to failures.
	Class string
	// Limit caps the number of events returned. <=0 means no cap.
	Limit int
}

EventFilter scopes a flat-event list query against the audit store (as opposed to Filter, which scopes a trace-grouped query). The filter is the on-the-wire shape for `GET /v1/audit`: every field is optional, and they compose with AND semantics.

type EventStore

type EventStore interface {
	Store

	// AppendToTrace records an event scoped to a trace correlation
	// (intent/workspace).
	AppendToTrace(ctx context.Context, traceID, intentID, workspaceID string, event Event) error

	// GetByEventID returns the recorded event with this id and
	// reports whether one was found. Used by GET /v1/audit/{audit_id}.
	GetByEventID(eventID string) (Event, bool)

	// ListEvents returns flat events matching the filter, newest-first.
	// Used by GET /v1/audit.
	ListEvents(ctx context.Context, f EventFilter) ([]Event, error)
}

EventStore is the audit-store surface the daemon and the `/v1/audit` handlers depend on. It is the union of the trace-aware Store SPI and the flat-event API the read endpoint uses, so the in-memory and file-backed implementations are interchangeable at the wiring site in `internal/app/app.go`.

type FileStore

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

FileStore is a JSONL-backed audit store: one event per line, daily-rotated under <stateDir>/audit/audit-YYYY-MM-DD.jsonl per ADR-0012. Append computes today's path on every write; replay scans every daily file in the directory at startup. Reads are served from an in-memory index, so the on-disk files are authoritative and the in-memory copy is a cache.

Per ADR-0010 this is the v0.x persistence target — Postgres / signed log are post-MVP and operate over the same SPI.

func NewFileStore

func NewFileStore(stateDir string, log *slog.Logger) (*FileStore, error)

NewFileStore opens (or creates) the daily-rotated audit log under stateDir and returns a FileStore whose in-memory index is replayed from every audit-*.jsonl file in <stateDir>/audit/. Malformed lines are skipped with a warning so a single corrupt entry can't make the daemon refuse to start; the rest of the file is still loaded.

log may be nil; in that case warnings are routed to slog.Default.

func (*FileStore) Append

func (fs *FileStore) Append(ctx context.Context, event Event) error

Append persists the event to today's daily file and the in-memory index. Both must succeed for Append to return nil; a write to disk that fails surfaces to the recorder, which logs and continues per ADR-0010's best-effort recording.

func (*FileStore) AppendToTrace

func (fs *FileStore) AppendToTrace(ctx context.Context, traceID, intentID, workspaceID string, event Event) error

AppendToTrace persists the trace-correlated event to today's daily file and the in-memory index.

func (*FileStore) GetByEventID

func (fs *FileStore) GetByEventID(eventID string) (Event, bool)

GetByEventID delegates to the in-memory index.

func (*FileStore) GetTrace

func (fs *FileStore) GetTrace(ctx context.Context, traceID string) (Trace, error)

GetTrace delegates to the in-memory index.

func (*FileStore) ListEvents

func (fs *FileStore) ListEvents(ctx context.Context, f EventFilter) ([]Event, error)

ListEvents delegates to the in-memory index.

func (*FileStore) ListTraces

func (fs *FileStore) ListTraces(ctx context.Context, filter Filter) ([]Trace, error)

ListTraces delegates to the in-memory index.

func (*FileStore) StateDir

func (fs *FileStore) StateDir() string

StateDir returns the state directory the daily files live under. Useful for status surfaces (e.g. `aileron status`) and tests.

type Filter

type Filter struct {
	WorkspaceID string
	IntentID    string
	ActorID     string
	EventType   model.EventType
	From        *time.Time
	To          *time.Time
	PageSize    int
	PageToken   string
}

Filter scopes a trace list query.

type MemStore

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

MemStore is an in-memory implementation of the Store SPI suitable for v1 dev/test environments. Events are kept in a single ordered slice; lookups are filtered linearly. Trace IDs come from the caller — typically the audit_id of an envelope or an upstream trace correlation ID — and are stored alongside the event.

Persistence to Postgres is post-MVP; the SPI is identical so the switch is a wiring change.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore returns an empty MemStore.

func (*MemStore) Append

func (m *MemStore) Append(_ context.Context, event Event) error

Append records a new event.

func (*MemStore) AppendToTrace

func (m *MemStore) AppendToTrace(_ context.Context, traceID, intentID, workspaceID string, event Event) error

AppendToTrace records an event scoped to a specific trace (intent/workspace). The base Store interface deliberately keeps trace correlation out of the Event struct; this helper is the canonical way to associate an event with a trace.

func (*MemStore) GetByEventID

func (m *MemStore) GetByEventID(eventID string) (Event, bool)

GetByEventID returns the recorded event with the given EventID and reports whether one was found. Useful for tests asserting that an audit_id stamped on a failure.Failure resolves to a real event.

func (*MemStore) GetTrace

func (m *MemStore) GetTrace(_ context.Context, traceID string) (Trace, error)

GetTrace returns the trace with the given TraceID.

func (*MemStore) ListEvents

func (m *MemStore) ListEvents(_ context.Context, f EventFilter) ([]Event, error)

ListEvents returns a flat slice of recorded events matching the filter, ordered newest-first. The store is in-memory and the scan is linear; suitable for v1 dev/test volumes.

func (*MemStore) ListTraces

func (m *MemStore) ListTraces(_ context.Context, filter Filter) ([]Trace, error)

ListTraces returns traces matching the filter. Events without a trace ID are grouped under TraceID=="" and surface only when the filter is permissive.

type MessageEntry

type MessageEntry struct {
	Timestamp time.Time `json:"timestamp"`
	SessionID string    `json:"session_id"`
	Event     string    `json:"event"`   // message_received, message_sent, message_denied, message_dismissed, draft_requested, reply_sent
	Service   string    `json:"service"` // slack, discord
	Channel   string    `json:"channel"`
	Author    string    `json:"author,omitempty"`      // sender for inbound messages
	Body      string    `json:"body,omitempty"`        // message content
	InReplyTo string    `json:"in_reply_to,omitempty"` // original message ID
}

MessageEntry is a single audit record for a message event.

func ReadMessageEntries

func ReadMessageEntries(path string) ([]MessageEntry, error)

ReadMessageEntries reads all message JSONL entries from the audit log. Only entries with an "event" field are returned (other entries are skipped).

path may be either a single JSONL file or the daily-rotated directory `<stateDir>/audit/`. When it's a directory, every `audit-*.jsonl` file inside is read, sorted by name (chronological since filenames embed the date).

type Recorder

type Recorder interface {
	// RecordFailure persists the failure as an audit event and
	// returns the minted audit_id. The same id is also stamped onto
	// the failure via failure.SetAuditID so callers can write the
	// envelope without an extra lookup.
	RecordFailure(ctx context.Context, f *failure.Failure, actor model.ActorRef) string

	// RecordSuccess persists a non-failure event with the given type
	// and payload. Returns the minted event_id.
	RecordSuccess(ctx context.Context, eventType model.EventType, actor model.ActorRef, payload map[string]any) string
}

Recorder writes audit events for failures and successes flowing through the gateway, action installer, and connector installer.

The package-level Store SPI handles persistence; Recorder layers on top of it to mint the audit_id, format the Event payload, and stamp the audit_id back onto failure.Failure so the envelope returned to the caller carries a working back-reference.

func NewRecorder

func NewRecorder(s Store, clk Clock, idFn func() string) Recorder

NewRecorder returns a Recorder backed by the given Store. clk supplies timestamps; pass clock.System{} in production. idFn mints audit_ids; pass DefaultIDFn for the default UUID-based scheme.

type Store

type Store interface {
	// Append records a new event. Implementations must treat this as
	// append-only: existing events must never be modified or deleted.
	Append(ctx context.Context, event Event) error

	// ListTraces returns traces matching the filter.
	ListTraces(ctx context.Context, filter Filter) ([]Trace, error)

	// GetTrace returns the full trace for a given intent.
	GetTrace(ctx context.Context, traceID string) (Trace, error)
}

Store records and retrieves immutable trace events.

type Trace

type Trace struct {
	TraceID     string
	IntentID    string
	WorkspaceID string
	Events      []Event
}

Trace is the full ordered event history for one intent.

Jump to

Keyboard shortcuts

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