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
- func AppendMessageEntry(path string, entry MessageEntry) error
- func DailyDir(stateDir string) string
- func DailyPath(stateDir string) string
- func DailyPathAt(stateDir string, t time.Time) string
- func DefaultIDFn() string
- type Clock
- type Event
- type EventFilter
- type EventStore
- type FileStore
- func (fs *FileStore) Append(ctx context.Context, event Event) error
- func (fs *FileStore) AppendToTrace(ctx context.Context, traceID, intentID, workspaceID string, event Event) error
- func (fs *FileStore) GetByEventID(eventID string) (Event, bool)
- func (fs *FileStore) GetTrace(ctx context.Context, traceID string) (Trace, error)
- func (fs *FileStore) ListEvents(ctx context.Context, f EventFilter) ([]Event, error)
- func (fs *FileStore) ListTraces(ctx context.Context, filter Filter) ([]Trace, error)
- func (fs *FileStore) StateDir() string
- type Filter
- type MemStore
- func (m *MemStore) Append(_ context.Context, event Event) error
- func (m *MemStore) AppendToTrace(_ context.Context, traceID, intentID, workspaceID string, event Event) error
- func (m *MemStore) GetByEventID(eventID string) (Event, bool)
- func (m *MemStore) GetTrace(_ context.Context, traceID string) (Trace, error)
- func (m *MemStore) ListEvents(_ context.Context, f EventFilter) ([]Event, error)
- func (m *MemStore) ListTraces(_ context.Context, filter Filter) ([]Trace, error)
- type MessageEntry
- type Recorder
- type Store
- type Trace
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
GetByEventID delegates to the in-memory index.
func (*FileStore) ListEvents ¶
ListEvents delegates to the in-memory index.
func (*FileStore) ListTraces ¶
ListTraces delegates to the in-memory index.
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 (*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 ¶
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) ListEvents ¶
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.
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.
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.