audit

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 14 Imported by: 0

README

Audit Trail

Records a structured, queryable trail of every mutating action an AI agent takes against issue trackers through the daemon, using a CloudEvents 1.0 envelope. The trail answers what an agent did and why — durably, in a form tools and humans can query.

Capabilities

  • Rationale capture — every recorded action carries the agent's reasoning, not just the mechanical change.
  • CloudEvents envelope — each action is a standard event (id, source, type, time, specversion, subject, data), so the trail is machine-queryable rather than ad-hoc free text.
  • Actor / resource / operation / outcome — each event decomposes into which configured tracker credential acted, which ticket/project it targeted, which operation ran (create, edit, delete, comment, status, start), and the result (success / failure / denied).
  • At-decision-time capture — the model id/version, the inputs, and the reasoning are recorded as they existed at emission. Agent decisions are non-deterministic, so a past decision cannot be reconstructed by replaying the prompt later; it must be captured when it happens.
  • SQLite storage — the full envelope is stored as JSON alongside decomposed, indexed columns. Events are retained for 90 days (accountability records outlive the shorter stats trend window) and pruned automatically.
  • human audit list / human audit show — read the trail through the daemon, as a fixed-width table, raw CloudEvents JSON (--json), or a single full envelope by event id.

Decision context (HUMAN_AUDIT_*)

An agent harness forwards the at-decision-time context via environment variables, set once per session. The client forwards them to the daemon, which records them with each mutating action:

  • HUMAN_AUDIT_MODEL_ID — the exact model id (e.g. claude-opus-4).
  • HUMAN_AUDIT_MODEL_VERSION — the model version/date.
  • HUMAN_AUDIT_INPUTS — the inputs that drove the decision.
  • HUMAN_AUDIT_RATIONALE — the why behind the action.

All are optional: a missing rationale is allowed and recorded empty; the event still captures actor, resource, operation, outcome, and model context.

Known boundary

Only commands that flow through the daemon are audited — that is the single choke point that knows the authenticated actor identity and the outcome exit code. Direct CLI invocations with no daemon running are not recorded by this path. This matches the trust model: autonomous agents run against the daemon. Secrets passed via --*-token / --*-key flags are stripped before storage, so the trail is safe to review and share.

Documentation

Overview

Package audit records a structured, queryable trail of every mutating action an AI agent takes against issue trackers through the daemon. Each action is captured as a CloudEvents 1.0 envelope so the trail is machine-queryable and tool-friendly rather than ad-hoc free text.

Index

Constants

View Source
const RetentionDays = 90

RetentionDays is the rolling window for audit event retention. Audit trails warrant longer retention than the stats trend window: accountability records of what an agent did and why are evidence that must outlive the short-lived trend graphs, so 90 days rather than 30.

View Source
const SpecVersion = "1.0"

SpecVersion is the CloudEvents specification version every emitted envelope declares. Pinned to the core 1.0 attributes the trail relies on.

Variables

This section is empty.

Functions

func DefaultDBPath

func DefaultDBPath() string

DefaultDBPath returns the path to the audit database (~/.human/audit.db), creating the directory if needed.

Types

type Actor

type Actor struct {
	TrackerKind string `json:"tracker_kind"`
	TrackerName string `json:"tracker_name,omitempty"`
	User        string `json:"user,omitempty"`
}

Actor identifies which configured tracker credential performed the action. This is the CloudEvents source decomposed into queryable parts.

type Data

type Data struct {
	Operation string          `json:"operation"`
	Actor     Actor           `json:"actor"`
	Resource  Resource        `json:"resource"`
	Outcome   Outcome         `json:"outcome"`
	Args      []string        `json:"args,omitempty"`
	Decision  DecisionContext `json:"decision"`
}

Data is the decomposed payload carried in the CloudEvents data attribute.

type DecisionContext

type DecisionContext struct {
	ModelID      string `json:"model_id,omitempty"`
	ModelVersion string `json:"model_version,omitempty"`
	Inputs       string `json:"inputs,omitempty"`
	Rationale    string `json:"rationale,omitempty"`
}

DecisionContext captures the model id/version, inputs, and reasoning exactly as they existed at decision time. Agent decisions are non-deterministic: a past decision cannot be reconstructed by replaying the same prompt against the model later, so the inputs and rationale must be captured here at emission time or they are lost forever.

func DecisionFromEnv

func DecisionFromEnv(lookup func(string) string) DecisionContext

DecisionFromEnv reads the at-decision-time context an agent harness forwards via HUMAN_AUDIT_* environment variables. All fields are optional: a missing rationale is allowed and recorded empty (the event still captures actor/resource/operation/outcome/model).

type Event

type Event struct {
	SpecVersion     string    `json:"specversion"`
	ID              string    `json:"id"`
	Source          string    `json:"source"`
	Type            string    `json:"type"`
	Subject         string    `json:"subject,omitempty"`
	Time            time.Time `json:"time"`
	DataContentType string    `json:"datacontenttype"`
	Data            Data      `json:"data"`
}

Event is the CloudEvents 1.0 envelope persisted and surfaced to "human audit".

func BuildEvent

func BuildEvent(now time.Time, op MutatingOp, outcome Outcome, dc DecisionContext, args []string) (Event, error)

BuildEvent assembles a complete CloudEvents envelope from a detected op, the observed outcome, the at-decision-time context, and the original args. The args are stripped of credentials first so secrets never reach the durable, reviewable trail.

type Filter

type Filter struct {
	Since       time.Time
	Until       time.Time
	Subject     string
	TrackerKind string
	Limit       int
}

Filter narrows an audit query. A zero Limit defaults to 100.

type MutatingOp

type MutatingOp struct {
	Operation   string // "create","edit","delete","comment","status","start"
	TrackerKind string
	TrackerName string // from --tracker
	Key         string // issue key, empty for create
	Project     string // from --project, for create
}

MutatingOp is the skeleton of a detected mutating tracker command, decomposed into the parts the audit event needs.

func DetectMutating

func DetectMutating(args []string) (MutatingOp, bool)

DetectMutating classifies a forwarded command's args into a mutating-op skeleton, returning ok=false for read-only or non-tracker commands.

It is broader than the daemon's detectDestructive (which only gates confirmation-worthy destructive ops): audit must also cover non-destructive mutations like create and comment. It lives here rather than in the daemon so it is testable without a running daemon and reusable by a future client-side emitter. detectDestructive is deliberately left untouched.

type Outcome

type Outcome string

Outcome is the result of a mutating tracker action.

const (
	// OutcomeSuccess means the command executed and exited 0.
	OutcomeSuccess Outcome = "success"
	// OutcomeFailure means the command executed and exited non-zero.
	OutcomeFailure Outcome = "failure"
	// OutcomeDenied means the action was intercepted as destructive and the
	// human reviewer rejected (or let time out) the confirmation.
	OutcomeDenied Outcome = "denied"
)

type Resource

type Resource struct {
	Key     string `json:"key,omitempty"`
	Project string `json:"project,omitempty"`
}

Resource identifies the ticket or project the action targeted.

type Store

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

Store persists audit events in SQLite for a structured, queryable trail. The full CloudEvents envelope is stored as JSON alongside decomposed, indexed columns so queries stay fast while the envelope round-trips intact.

func NewStore

func NewStore(dbPath string) (*Store, error)

NewStore opens (or creates) a SQLite database at dbPath and ensures the schema is up to date. Use ":memory:" for in-memory databases in tests.

func (*Store) Close

func (s *Store) Close() error

Close closes the database connection.

func (*Store) Insert

func (s *Store) Insert(ctx context.Context, e Event) error

Insert persists a single audit event. The full envelope is marshalled to JSON for durable, lossless replay while the decomposed columns feed the indexed query paths.

func (*Store) Prune

func (s *Store) Prune(ctx context.Context) (int64, error)

Prune deletes events older than RetentionDays.

func (*Store) Query

func (s *Store) Query(ctx context.Context, f Filter) ([]Event, error)

Query returns matching audit events newest-first by reconstructing each stored envelope. Decomposed columns drive the WHERE clause so the indexes are used; the envelope JSON is the single source of truth for the result.

type Writer

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

Writer accepts audit events on a channel and inserts them into a Store in a single background goroutine. Call Close to drain remaining events and shut down.

func NewWriter

func NewWriter(ctx context.Context, store *Store, logger zerolog.Logger) *Writer

NewWriter creates a Writer and starts the background drain goroutine. The goroutine runs until ctx is cancelled or Close is called.

func (*Writer) Close

func (w *Writer) Close()

Close signals the writer to stop and waits for the background goroutine to finish draining. It is idempotent and safe to call after ctx cancellation has already stopped the goroutine.

func (*Writer) Send

func (w *Writer) Send(e Event)

Send enqueues an event for async persistence. If the channel is full the event is dropped with a warning rather than blocking the caller. The data channel is never closed, so Send is safe to call concurrently with — and after — Close without panicking; the quit case discards events once shut down.

Jump to

Keyboard shortcuts

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