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
- Variables
- func EffectiveTenant(t string) string
- func NewAuditLogger(s Store) audit.Logger
- type ApprovalRecord
- type ApprovalStore
- type AuditSink
- type BucketState
- type CostState
- type CostStore
- type RateLimitStore
- type SQLiteStore
- func (s *SQLiteStore) AppendAudit(ctx context.Context, entries []audit.Entry) error
- func (s *SQLiteStore) Close() error
- func (s *SQLiteStore) DeletePolicy(ctx context.Context, tenantID string) (bool, error)
- func (s *SQLiteStore) GetPolicyYAML(ctx context.Context, tenantID string) ([]byte, bool, error)
- func (s *SQLiteStore) ListPolicyTenants(ctx context.Context) ([]string, error)
- func (s *SQLiteStore) LoadApprovals(ctx context.Context) ([]ApprovalRecord, error)
- func (s *SQLiteStore) LoadBuckets(ctx context.Context) ([]BucketState, error)
- func (s *SQLiteStore) LoadCosts(ctx context.Context) ([]CostState, error)
- func (s *SQLiteStore) Migrate(ctx context.Context) error
- func (s *SQLiteStore) Path() string
- func (s *SQLiteStore) Ping(ctx context.Context) error
- func (s *SQLiteStore) PurgeBuckets(ctx context.Context, cutoff time.Time) (int, error)
- func (s *SQLiteStore) PurgeCosts(ctx context.Context, cutoff time.Time) (int, error)
- func (s *SQLiteStore) PurgeResolvedApprovals(ctx context.Context, cutoff time.Time) (int, error)
- func (s *SQLiteStore) PutPolicy(ctx context.Context, tenantID string, policyYAML []byte) error
- func (s *SQLiteStore) QueryAudit(ctx context.Context, tenantID string, filter audit.QueryFilter) ([]audit.Entry, error)
- func (s *SQLiteStore) UpsertApprovals(ctx context.Context, recs []ApprovalRecord) error
- func (s *SQLiteStore) UpsertBuckets(ctx context.Context, buckets []BucketState) error
- func (s *SQLiteStore) UpsertCosts(ctx context.Context, costs []CostState) error
- type Store
Constants ¶
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 ¶
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 ¶
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 ¶
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 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 (*SQLiteStore) Close ¶
func (s *SQLiteStore) Close() error
Close closes the database. Idempotent at the sql.DB layer.
func (*SQLiteStore) DeletePolicy ¶
DeletePolicy removes a tenant's policy. ok reports whether a row existed.
func (*SQLiteStore) GetPolicyYAML ¶
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 (*SQLiteStore) PurgeCosts ¶
func (*SQLiteStore) PurgeResolvedApprovals ¶
func (*SQLiteStore) PutPolicy ¶
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.