// Package agentstore is the transport boundary between pulse-agent and pulse-api. // // The agent runs with the narrow read-only host access it needs and never exposes a // network endpoint; the API never reaches out to the host. Both processes already share // PostgreSQL on the internal-only Compose network (see deploy/compose.yaml and ADR-0010), // so the agent writes bounded, normalized snapshots into the database and the API reads // the most recent one per capability. This keeps the privilege separation required by // SYSTEM_ARCHITECTURE section "pulse-agent" without adding an inbound port to the agent. // // Freshness is deliberately the reader's problem, not the writer's: a snapshot carries // the time the agent observed it, and the reader decides whether that is still usable. // A capability with no snapshot, or one older than its freshness window, resolves to // Unknown and never to Healthy (ADR-0008). package agentstore import ( "context" "encoding/json" "errors" "time" ) // Capability identifies one bounded telemetry surface an agent can report. The set is // closed: a reader must never accept a capability it does not recognize, because an // unknown capability cannot be normalized or bounded. type Capability string const ( CapabilityHost Capability = "host" CapabilityProcesses Capability = "processes" CapabilityContainers Capability = "containers" CapabilityArray Capability = "array" CapabilityDisks Capability = "disks" CapabilityPools Capability = "pools" CapabilityShares Capability = "shares" ) // Capabilities lists every capability the platform recognizes, in a stable order. func Capabilities() []Capability { return []Capability{ CapabilityHost, CapabilityProcesses, CapabilityContainers, CapabilityArray, CapabilityDisks, CapabilityPools, CapabilityShares, } } // Valid reports whether the capability is one this platform recognizes. func (c Capability) Valid() bool { for _, known := range Capabilities() { if c == known { return true } } return false } // MaxPayloadBytes bounds a single snapshot. The largest realistic payload is the process // inventory at the documented scale target; this leaves generous headroom while keeping a // misbehaving or compromised agent from filling the database. const MaxPayloadBytes = 2 << 20 // ErrNoSnapshot is returned by Reader.Latest when the capability has never been reported. // It is an expected condition on a fresh install, not a failure: callers translate it into // an Unknown status with an explicit reason. var ErrNoSnapshot = errors.New("no agent snapshot recorded") // Snapshot is one bounded observation of a single capability. type Snapshot struct { // AgentID identifies the reporting agent. AgentID string // Capability is the telemetry surface this payload describes. Capability Capability // ObservedAt is when the agent read the underlying source, in UTC. ObservedAt time.Time // ReceivedAt is when the store accepted the snapshot, in UTC. It is set by the // writer implementation, never by the agent, so a skewed agent clock cannot make // stale data look fresh. ReceivedAt time.Time // Payload is the domain RawSnapshot for this capability, JSON encoded. Payload json.RawMessage } // Age reports how long ago the agent observed this snapshot. func (s Snapshot) Age(now time.Time) time.Duration { return now.Sub(s.ObservedAt) } // Writer is the narrow interface pulse-agent depends on. The agent must not be able to // read other agents' data or mutate anything else. type Writer interface { // Put records the newest snapshot for one capability, replacing any previous one. // Implementations reject an unknown capability, an oversized payload, a zero or // future ObservedAt, and payloads that are not valid JSON objects. Put(ctx context.Context, snapshot Snapshot) error } // Reader is the narrow interface pulse-api depends on. type Reader interface { // Latest returns the most recent snapshot for the capability, or ErrNoSnapshot. Latest(ctx context.Context, capability Capability) (Snapshot, error) } // Store is the combined boundary. Only the migration-owning implementation satisfies it. type Store interface { Writer Reader }