store

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package store is AgentGuard's durable, multi-tenant persistence tier — the "cold path" behind the in-memory fast path described in docs/v0.6-ARCHITECTURE-PLAN.md (§2.3 write-behind dual-tier).

CONTRACT (see CLAUDE.md):

  • Implementations MUST NOT be called on the streaming proxy hot path. The in-memory maps in pkg/ratelimit, pkg/policy (Engine.sessionCosts) and pkg/proxy (ApprovalQueue) stay authoritative for /v1/check; a Store is reconciled asynchronously (write-behind by the syncer) and read only at boot (hydration) or from operator/query paths.
  • Zero-trust: every persisted row carries a non-empty tenant_id. The mutating batch methods reject any record whose TenantID is empty with ErrTenantRequired; the request-driven read (QueryAudit) takes an explicit tenantID. The all-tenant Load*/Purge* methods are SYSTEM operations (boot hydration / GC), never reached from a per-request handler.

Index

Constants

View Source
const TenantLocal = "local"

TenantLocal is the canonical id of the default single-tenant deployment. The proxy stores the local tenant as "" on the wire for byte-identity; the syncer normalizes that to TenantLocal before it reaches a Store so every persisted row is attributed.

Variables

View Source
var ErrTenantRequired = errors.New("store: tenant_id is required")

ErrTenantRequired is returned by a mutating Store method when a record's TenantID is empty. Enforces the zero-trust rule at the lowest layer so a caller that forgets to stamp the tenant fails loudly rather than silently writing an unattributed row. The default tenant is the literal "local" (TenantLocal) — never the empty string at the persistence layer.

Functions

func EffectiveTenant

func EffectiveTenant(t string) string

EffectiveTenant coerces an empty tenant id to TenantLocal. Callers building Store records from in-memory state (where local may be "") route through this so persisted rows are never unattributed.

func NewAuditLogger

func NewAuditLogger(s Store) audit.Logger

NewAuditLogger returns an audit.Logger backed by the given Store.

Types

type ApprovalRecord

type ApprovalRecord struct {
	TenantID   string
	ID         string
	Request    policy.ActionRequest
	Result     policy.CheckResult
	CreatedAt  time.Time
	Resolved   bool
	Decision   string
	ResolvedAt time.Time
}

ApprovalRecord is the persisted form of pkg/proxy.PendingAction plus its owning tenant. Kept here (rather than importing the proxy type) to avoid a pkg/store → pkg/proxy import cycle; the syncer maps between the two.

type ApprovalStore

type ApprovalStore interface {
	// UpsertApprovals writes new/updated approvals (write-behind from
	// ApprovalQueue snapshots). Idempotent on (tenant_id, id). Rejects any
	// record with an empty TenantID.
	UpsertApprovals(ctx context.Context, recs []ApprovalRecord) error
	// LoadApprovals returns every approval (pending and still-retained
	// resolved) across all tenants — boot hydration only.
	LoadApprovals(ctx context.Context) ([]ApprovalRecord, error)
	// PurgeResolvedApprovals deletes resolved approvals resolved before cutoff
	// (durable analogue of the in-memory LRU eviction). Returns rows deleted.
	PurgeResolvedApprovals(ctx context.Context, cutoff time.Time) (int, error)
}

ApprovalStore persists the approval queue (restart-survival + per-tenant pending lists).

type AuditSink

type AuditSink interface {
	AppendAudit(ctx context.Context, entries []audit.Entry) error
	QueryAudit(ctx context.Context, tenantID string, filter audit.QueryFilter) ([]audit.Entry, error)
}

AuditSink persists the audit trail. AppendAudit is batched write-behind fed by the existing BufferedAsyncLogger workers; QueryAudit backs the tenant-scoped /v1/audit read.

type BucketState

type BucketState struct {
	TenantID   string
	Key        string
	Tokens     int
	Max        int
	Window     time.Duration
	LastRefill time.Time
}

BucketState is one token-bucket's persisted state. Key is the limiter's opaque "scope:tenant:agent" key; TenantID is the parsed-out tenant so the row is attributable and queryable without re-parsing the key.

type CostState

type CostState struct {
	TenantID    string
	SessionID   string
	Cost        float64
	LastUpdated time.Time
}

CostState is one session's persisted cost accumulator.

type CostStore

type CostStore interface {
	UpsertCosts(ctx context.Context, costs []CostState) error
	LoadCosts(ctx context.Context) ([]CostState, error)
	// PurgeCosts deletes cost rows last updated before cutoff (durable analogue
	// of Engine.SweepSessionCosts). Returns rows deleted.
	PurgeCosts(ctx context.Context, cutoff time.Time) (int, error)
}

CostStore persists session-cost accumulators.

type RateLimitStore

type RateLimitStore interface {
	UpsertBuckets(ctx context.Context, buckets []BucketState) error
	LoadBuckets(ctx context.Context) ([]BucketState, error)
	// PurgeBuckets deletes buckets whose last refill predates cutoff (fully
	// refilled, equivalent to evicted). Returns rows deleted.
	PurgeBuckets(ctx context.Context, cutoff time.Time) (int, error)
}

RateLimitStore persists token buckets so a restart does not reset every limiter to full. Snapshot/restore only — the authoritative check stays in pkg/ratelimit (CLAUDE.md rule #2: async syncs only for rate limits/costs).

type SQLiteStore

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

SQLiteStore is the single-node, zero-config Store backend (docs/v0.6- ARCHITECTURE-PLAN.md §2.4). It owns one database file holding every persistence table, opened in WAL mode so the eventual direct-reading dashboard can read concurrently with the write-behind syncer.

Concurrency: MaxOpenConns is pinned to 1. The Store is a cold-path component (write-behind flushes + boot hydration + occasional audit queries), so serializing its handle is the simplest correct guard against SQLITE_BUSY and costs nothing on the proxy's request path, which never touches it.

func NewSQLiteStore

func NewSQLiteStore(path string) (*SQLiteStore, error)

NewSQLiteStore opens (creating if absent) the SQLite database at path, sets WAL + a busy timeout, and runs the schema migration. A ":memory:" path (or any modernc in-memory DSN) yields an ephemeral store, used by tests.

func (*SQLiteStore) AppendAudit

func (s *SQLiteStore) AppendAudit(ctx context.Context, entries []audit.Entry) error

func (*SQLiteStore) Close

func (s *SQLiteStore) Close() error

Close closes the database. Idempotent at the sql.DB layer.

func (*SQLiteStore) DeletePolicy

func (s *SQLiteStore) DeletePolicy(ctx context.Context, tenantID string) (bool, error)

DeletePolicy removes a tenant's policy. ok reports whether a row existed.

func (*SQLiteStore) GetPolicyYAML

func (s *SQLiteStore) GetPolicyYAML(ctx context.Context, tenantID string) ([]byte, bool, error)

GetPolicyYAML returns the stored policy document for a tenant. ok=false when the tenant has no policy row (distinct from an error).

func (*SQLiteStore) ListPolicyTenants

func (s *SQLiteStore) ListPolicyTenants(ctx context.Context) ([]string, error)

ListPolicyTenants returns every tenant id that has a stored policy, sorted. Used by MultiTenantProvider to eager-load all tenants on boot.

func (*SQLiteStore) LoadApprovals

func (s *SQLiteStore) LoadApprovals(ctx context.Context) ([]ApprovalRecord, error)

func (*SQLiteStore) LoadBuckets

func (s *SQLiteStore) LoadBuckets(ctx context.Context) ([]BucketState, error)

func (*SQLiteStore) LoadCosts

func (s *SQLiteStore) LoadCosts(ctx context.Context) ([]CostState, error)

func (*SQLiteStore) Migrate

func (s *SQLiteStore) Migrate(ctx context.Context) error

Migrate creates the schema. Idempotent (CREATE ... IF NOT EXISTS), so it is safe to run on every boot.

func (*SQLiteStore) Path

func (s *SQLiteStore) Path() string

Path returns the database path (for logging / health).

func (*SQLiteStore) Ping

func (s *SQLiteStore) Ping(ctx context.Context) error

Ping verifies connectivity.

func (*SQLiteStore) PurgeBuckets

func (s *SQLiteStore) PurgeBuckets(ctx context.Context, cutoff time.Time) (int, error)

func (*SQLiteStore) PurgeCosts

func (s *SQLiteStore) PurgeCosts(ctx context.Context, cutoff time.Time) (int, error)

func (*SQLiteStore) PurgeResolvedApprovals

func (s *SQLiteStore) PurgeResolvedApprovals(ctx context.Context, cutoff time.Time) (int, error)

func (*SQLiteStore) PutPolicy

func (s *SQLiteStore) PutPolicy(ctx context.Context, tenantID string, policyYAML []byte) error

PutPolicy stores (or replaces) the policy document for a tenant. The bytes are stored verbatim; validation is the caller's responsibility (the CLI and MultiTenantProvider validate before/after). Rejects an empty tenant.

func (*SQLiteStore) QueryAudit

func (s *SQLiteStore) QueryAudit(ctx context.Context, tenantID string, filter audit.QueryFilter) ([]audit.Entry, error)

func (*SQLiteStore) UpsertApprovals

func (s *SQLiteStore) UpsertApprovals(ctx context.Context, recs []ApprovalRecord) error

func (*SQLiteStore) UpsertBuckets

func (s *SQLiteStore) UpsertBuckets(ctx context.Context, buckets []BucketState) error

func (*SQLiteStore) UpsertCosts

func (s *SQLiteStore) UpsertCosts(ctx context.Context, costs []CostState) error

type Store

type Store interface {
	ApprovalStore
	RateLimitStore
	CostStore
	AuditSink

	// Migrate creates/updates the schema. Idempotent; safe on every boot.
	Migrate(ctx context.Context) error
	// Ping verifies connectivity for health checks without touching data.
	Ping(ctx context.Context) error
	// Close releases the underlying handle(s). Idempotent.
	Close() error
}

Store is the composed, durable persistence abstraction. SQLiteStore (and a future PostgresStore) satisfy it.

Jump to

Keyboard shortcuts

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