ledger

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package ledger is the agent session ledger (ADR-0031): one normalized, identity-linked record per agent session, built from several evidence sources (halo-proxy spans, Claude Code OTel events, the Anthropic Compliance API, the GitHub enterprise audit log, a JSONL import).

A record holds identifiers, counts and timestamps only. Every string field must match a token pattern with no whitespace, so prose (a prompt, a tool input, an error message) cannot be stored in any field; file paths and repositories are dropped or salted-hashed by the redaction policy before a record leaves Normalize. Validate re-checks all of it on the server, which refuses the whole batch on the first violation (fail closed).

Index

Constants

View Source
const (
	SourceGateway   = "gateway"   // halo-proxy GenAI spans (authenticated collector receiver)
	SourceOTel      = "otel"      // Claude Code OTel log events (client-reachable receiver)
	SourceAnthropic = "anthropic" // Anthropic Compliance API activities
	SourceGitHub    = "github"    // GitHub enterprise audit log
	// SourceCloudAgents is cloud agent tasks halo-server saw (ADR-0034):
	// HMAC-verified GitHub webhooks and vendor task APIs.
	SourceCloudAgents = "cloudagents"
	SourceJSONL       = "jsonl" // operator-supplied import
)

Evidence sources.

View Source
const (
	High   = "high"   // server-side or vendor-authenticated evidence
	Medium = "medium" // client-reported telemetry
	Low    = "low"    // operator import of unknown provenance
)

Confidence is the trust of the best evidence behind a record.

View Source
const (
	OutcomeCompleted = "completed"
	OutcomeError     = "error"
	OutcomeCancelled = "cancelled"
	OutcomeUnknown   = "unknown"
)

Outcomes.

View Source
const (
	PathsDrop = "drop"
	PathsHash = "hash"
)

Redaction modes.

View Source
const AnthropicBaseURL = "https://api.anthropic.com"

AnthropicBaseURL is the Compliance API's host.

View Source
const AnthropicVersion = "2023-06-01"

AnthropicVersion is the anthropic-version header the Compliance API examples send (checked 2026-10-27).

View Source
const GitHubAPIVersion = "2026-03-10"

GitHubAPIVersion is the X-GitHub-Api-Version sent to the audit log API.

View Source
const Table = "halo_session_ledger"

Table is the ClickHouse table stored records are copied to for analytics (deploy/observability/clickhouse/schema.sql). The audit chain stays the record of truth; the table is a query copy, one row per stored version (ReplacingMergeTree on stored_at keeps the latest). record_sha256 is the SHA-256 of the record JSON halo-server put in the audit entry's details.record, so a row can be checked against the chain.

Variables

This section is empty.

Functions

func IDFor

func IDFor(harness, sessionID string) string

IDFor is the ledger id of a harness session: stable across sources and imports, opaque and URL-safe.

func Insert

func Insert(ctx context.Context, ch *promote.ClickHouse, recs []Record, storedAt time.Time) error

Insert copies records halo-server stored (POST /api/v1/sessions answers them) into the ClickHouse table. A record that does not validate is refused.

func Join

func Join(recs []Record, lookup func(vendorUser string) (user string, ok bool))

Join sets Who.User from the identity directory, keyed by the identity the source reported. A Who.User the caller supplied is discarded first: the Halos identity on a stored record only ever comes from the directory.

func ParseTime

func ParseTime(s string, now time.Time) (time.Time, error)

ParseTime accepts RFC 3339 or a duration back from now ("24h").

Types

type Anthropic

type Anthropic struct{ Client *vendoradmin.Client }

Anthropic reads Claude Code cloud sessions from the Compliance API activity feed (GET /v1/compliance/activities, type ccr_session_created), one record per activity. The activity names the actor and the agent but carries no session id, model, tool or usage field, so the record's session id is the activity id and nothing else is filled in: it proves who started a cloud session when, not what it did. The client is vendoradmin's, so the key is only ever in the Authorization header and scrubbed from errors.

func (Anthropic) Fetch

func (a Anthropic) Fetch(ctx context.Context, w Window) ([]Record, error)

func (Anthropic) Name

func (Anthropic) Name() string

type ClaudeOTel

type ClaudeOTel struct{ CH *promote.ClickHouse }

ClaudeOTel reads Claude Code's log events (claude_code.*) from the collector's otel_logs table, one record per session.id. It selects named attributes only: never prompt, tool_input, error text or the raw tool_parameters (which holds Bash commands); the MCP server name is extracted from tool_parameters inside the query. Repositories come from vcs.repository.url.full (OTEL_METRICS_INCLUDE_REPOSITORY=1). These events arrive on the client-reachable receiver, so the confidence is medium.

func (ClaudeOTel) Fetch

func (c ClaudeOTel) Fetch(ctx context.Context, w Window) ([]Record, error)

func (ClaudeOTel) Name

func (ClaudeOTel) Name() string

type Filter

type Filter struct {
	User, Harness, Ring, Tool string
	Since, Until              time.Time // on Start: Since inclusive, Until exclusive
}

Filter selects records for list and export. Zero fields match everything.

func (Filter) Match

func (f Filter) Match(r *Record) bool

Match reports whether r passes f. User matches the Halos identity or, when the session is unresolved, the vendor identity.

type Gateway

type Gateway struct{ CH *promote.ClickHouse }

Gateway reads halo-proxy's GenAI spans (telemetry.genai.traces) from the collector's otel_traces table, one record per trace. Only spans stamped halo.source=gateway count: the collector sets that on the authenticated gateway receiver, never from a client. halo-proxy exports no user or session id, so the session is the W3C trace (the caller's traceparent, or one call when it sent none) and Who carries the halo.unit pseudonym only.

func (Gateway) Fetch

func (g Gateway) Fetch(ctx context.Context, w Window) ([]Record, error)

func (Gateway) Name

func (Gateway) Name() string

type GitHubAudit

type GitHubAudit struct {
	Client     *vendoradmin.Client
	Enterprise string
	Phrase     string // "" = "action:agent_session"
}

GitHubAudit reads cloud agent sessions from GET /enterprises/{enterprise}/audit-log (read:audit_log). Events that carry agent_session_id (documented on pull_request.create, checked 2026-10-27) are grouped by that id; every other event is ignored. Phrase narrows the search server-side; the default asks for agent_session.* events in the window.

func (GitHubAudit) Fetch

func (g GitHubAudit) Fetch(ctx context.Context, w Window) ([]Record, error)

func (GitHubAudit) Name

func (GitHubAudit) Name() string

type JSONL

type JSONL struct{ R io.Reader }

JSONL reads one Record per line. Decoding is strict: a field the ledger does not define (a "prompt", say) fails the import rather than being silently dropped, and Normalize then rejects any value that is not a token. Sources, confidence and id are set by the ledger, not the file.

func (JSONL) Fetch

func (j JSONL) Fetch(_ context.Context, w Window) ([]Record, error)

func (JSONL) Name

func (JSONL) Name() string

type Record

type Record struct {
	ID             string   `json:"id"` // IDFor(Harness, SessionID)
	SessionID      string   `json:"sessionId"`
	Who            Who      `json:"who"`
	Harness        string   `json:"harness"`
	HarnessVersion string   `json:"harnessVersion,omitempty"`
	Release        string   `json:"release,omitempty"`
	Ring           string   `json:"ring,omitempty"`
	Experiment     string   `json:"experiment,omitempty"`
	Variant        string   `json:"variant,omitempty"`
	Models         []string `json:"models,omitempty"`
	MCPServers     []string `json:"mcpServers,omitempty"`
	Tools          []string `json:"tools,omitempty"`
	Repos          []string `json:"repos,omitempty"` // "h:<hex>" once redacted
	Files          []string `json:"files,omitempty"` // "h:<hex>" once redacted
	FilesTouched   int      `json:"filesTouched,omitempty"`
	// DLPSecrets and DLPInjections count the gateway's DLP findings in the
	// session (ADR-0033): secrets and prompt-injection signals, never content.
	DLPSecrets    int64     `json:"dlpSecrets,omitempty"`
	DLPInjections int64     `json:"dlpInjections,omitempty"`
	InputTokens   int64     `json:"inputTokens,omitempty"`
	OutputTokens  int64     `json:"outputTokens,omitempty"`
	CostUSD       float64   `json:"costUsd,omitempty"`
	Start         time.Time `json:"start"`
	End           time.Time `json:"end,omitzero"`
	Outcome       string    `json:"outcome"`
	Sources       []string  `json:"sources"`
	Confidence    string    `json:"confidence"`
	// Redacted counts values the redaction policy withheld (paths dropped,
	// identifiers that were not tokens).
	Redacted int `json:"redacted,omitempty"`
}

Record is one agent session.

func Normalize

func Normalize(recs []Record, p Redaction) ([]Record, error)

Normalize merges records of the same session (by ID), applies the redaction policy and validates. Any invalid record fails the whole call, so nothing unredacted is ever printed, sent or stored. Records without an ID get IDFor(Harness, SessionID).

func (*Record) Validate

func (r *Record) Validate() error

Validate is the fail-closed check every stored record passes.

type Redaction

type Redaction struct {
	// Paths is "drop" (the default: files and repos are counted, not kept) or
	// "hash" (HMAC-SHA256 with Salt, truncated; equal paths stay comparable
	// within one salt).
	Paths string
	Salt  []byte
}

Redaction is the path policy. Prompt text has no policy: no field can hold it.

type Source

type Source interface {
	Name() string
	Fetch(ctx context.Context, w Window) ([]Record, error)
}

Source is one evidence adapter. Fetch returns raw records (unredacted, unvalidated): pass them through Normalize before anything else sees them.

type Stored

type Stored struct {
	Record
	AuditSeq  uint64 `json:"auditSeq"`
	AuditHash string `json:"auditHash"`
}

Stored is a record as halo-server keeps it: the audit chain entry holding it is the tamper evidence (ADR-0016's chain, exported to SIEM).

type Who

type Who struct {
	// User is the Halos identity (the SCIM userName). Only halo-server sets it,
	// from its directory; a value sent by a client is discarded (Join).
	User string `json:"user,omitempty"`
	// VendorUser is the identity the source reported: an email, a GitHub login,
	// an Anthropic account id.
	VendorUser string `json:"vendorUser,omitempty"`
	// Unit is halo-proxy's halo.unit pseudonym (a salted hash of the verified
	// subject) when that is all the evidence carries.
	Unit string `json:"unit,omitempty"`
	// Agents is the delegated agent chain, root side first (ADR-0024).
	Agents []string `json:"agents,omitempty"`
}

Who is the session's principal.

type Window

type Window struct{ Since, Until time.Time }

Window bounds a fetch: Since inclusive, Until exclusive.

Jump to

Keyboard shortcuts

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