agent

package
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: AGPL-3.0 Imports: 47 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RuntimeDocker     = "docker"
	RuntimeSwarm      = "swarm"
	RuntimeKubernetes = "kubernetes"
)

Runtime labels reported by the agent during enrollment. "docker" / "kubernetes" come from runtime.Runtime.Name(); "swarm" is derived from the swarm.Detector check applied to the docker runtime.

View Source
const (
	HealthInterval = 15 * time.Second
	HealthMaxAge   = 60 * time.Second
)

HealthInterval is how often a running agent refreshes its liveness file, and HealthMaxAge how stale that file may be before the agent counts as stuck. The gap between them absorbs a slow tick without flapping.

View Source
const CapabilityLogs = "logs"

CapabilityLogs mirrors agentserver.CapabilityLogs. Declared here rather than imported: the agent must not depend on the server package.

View Source
const TokenPrefixLen = 14

TokenPrefixLen is how much of a token is kept in the clear for display. The prefix is `mnt_enr_` plus six base32 characters: enough for an operator to tell two tokens apart in a list, and 30 bits out of 256 — worthless to someone holding only the hash, which is the whole point of storing it.

Variables

View Source
var (
	ErrAgentNotFound        = errors.New("agent not found")
	ErrTokenNotFound        = errors.New("enrollment token not found")
	ErrTokenAlreadyConsumed = errors.New("enrollment token already consumed")
	ErrTokenExpired         = errors.New("enrollment token expired")
	ErrLabelTooLong         = errors.New("label exceeds 64 characters")
	ErrAgentRevoked         = errors.New("agent is revoked")
	ErrBadSignature         = errors.New("invalid Ed25519 signature")
	ErrClockSkew            = errors.New("clock skew exceeds 300s")
	ErrAgentUnknown         = errors.New("agent unknown")
	ErrHostLimitReached     = errors.New("agent host limit reached")
)

Sentinel errors for agent store operations.

View Source
var ErrAgentRevokedServer = errors.New("agent revoked by server")

ErrAgentRevokedServer is returned by RunWithReconnect when the server revokes the agent. The caller should exit without retrying.

View Source
var ErrNoStream = errors.New("no agent stream available")

ErrNoStream is returned by a disabled spool with no stream attached.

Functions

func Capabilities added in v1.3.8

func Capabilities() []string

Capabilities lists the command families this build understands. Sent at auth so the server can reject unsupported requests immediately instead of timing out.

func CheckHealth added in v1.4.0

func CheckHealth(dataDir string, maxAge time.Duration, now time.Time) error

CheckHealth reports whether an agent refreshed its liveness file within maxAge. A missing file is returned as an fs.ErrNotExist-wrapped error so callers can tell "no agent runs here" from "the agent is stuck".

func HashToken added in v1.3.9

func HashToken(token string) string

HashToken returns the hex-encoded SHA-256 of an enrollment token. This is the only form of the token the database ever sees.

func HealthPath added in v1.4.0

func HealthPath(dataDir string) string

HealthPath returns the liveness file path inside an agent data directory.

func NewToken added in v1.3.9

func NewToken() (cleartext, hash, id, prefix string, err error)

NewToken mints an enrollment token and returns its cleartext, the hash that gets persisted, the id derived from that hash, and the display prefix.

The cleartext is returned once and never stored: it exists in the creation response and nowhere else. Everything the server needs afterwards to accept, list or revoke the token is derivable from the hash.

func Run

func Run(ctx context.Context, cfg AgentConfig, logger *slog.Logger) error

Run is the main agent entry point (mode=agent). It detects the local runtime, loads or creates the agent identity, enrolls if needed, then enters the long-lived Push streaming loop (US2).

func RunCollector

func RunCollector(ctx context.Context, id *Identity, rt runtime.Runtime, label, nodeName string, spool *Spool, logger *slog.Logger) error

RunCollector starts collecting events from the local runtime and pushing them to stream. rt is the already-connected runtime resolved by agent.Run; label is the reported runtime kind ("docker", "swarm" or "kubernetes"); nodeName is the Kubernetes node the agent runs on, empty unless the operator set it. Blocks until ctx is cancelled or a fatal push error occurs.

func RunEnrollment

func RunEnrollment(
	ctx context.Context,
	id *Identity,
	dataDir string,
	enrollmentToken string,
	runtimeLabel string,
	label string,
	agentVersion string,
	client *Client,
) error

RunEnrollment sends a RegisterRequest to the server using the enrollment token. On success it marks the identity as registered and persists it to dataDir.

func RunWithReconnect

func RunWithReconnect(
	ctx context.Context,
	c *Client,
	id *Identity,
	logger *slog.Logger,
	hooks StreamHooks,
	onStream func(ctx context.Context, stream *PushStream) error,
) error

RunWithReconnect runs onStream with exponential backoff reconnect. Returns nil when ctx is cancelled, ErrAgentRevokedServer when the server revokes the agent. Backoff: min(60s, 1s * 2^attempt) ±25% jitter. Attempt resets to 0 if stream was stable >30s.

func StartHealthReporter added in v1.4.0

func StartHealthReporter(ctx context.Context, dataDir string, interval time.Duration, logger *slog.Logger) <-chan struct{}

StartHealthReporter refreshes the liveness file every interval until ctx is done. The returned channel is closed once the reporter has stopped writing, so a caller that needs the directory settled can wait for it: cancelling the context only asks the goroutine to stop, it does not mean an in-flight write has landed.

Reachability of the server is deliberately not part of the signal: an agent that cannot reach the server is already reported as disconnected there, and letting an orchestrator restart the agent over it would fix nothing while hiding the actual outage.

func TokenIDFromHash added in v1.3.9

func TokenIDFromHash(hash string) string

TokenIDFromHash derives the opaque id used in API paths from the token hash. Truncating is safe: the id identifies a row, it never authorises anything — enrollment matches on the full hash.

func TokenPrefix added in v1.3.9

func TokenPrefix(token string) string

TokenPrefix returns the leading, non-secret part of a token.

func WriteHealth added in v1.4.0

func WriteHealth(dataDir string, t time.Time) error

WriteHealth records that the agent loop was alive at t. The file is replaced atomically so a healthcheck reading it concurrently never sees a partial stamp.

Types

type Agent

type Agent struct {
	AgentID         string
	PublicKey       []byte // raw 32 bytes Ed25519
	Hostname        string
	Label           string
	OSArch          string
	AgentVersion    string
	DetectedRuntime string // "docker"|"swarm"|"kubernetes"
	Status          string // "active"|"revoked"
	LastSeenAt      *time.Time
	CreatedAt       time.Time
	RevokedAt       *time.Time
	RevokedBy       *string

	OSID                string
	OSVersionID         string
	OSPrettyName        string
	OSSource            string // ""|"host_file"|"kubernetes_node"
	OSUnavailableReason string // ""|"mount_missing"|"file_unreadable"|"node_not_found"
	OSReportedAt        *time.Time
}

Agent represents a remote agent registered with the server.

type AgentConfig

type AgentConfig struct {
	DataDir             string
	ServerURL           string
	EnrollmentToken     string
	RuntimeOverride     string
	Label               string
	NodeName            string
	AgentVersion        string
	InsecureSkipVerify  bool
	ProxyLabels         bool
	SpoolMaxMemoryBytes int64
	SpoolMaxDiskBytes   int64
	SpoolMaxAgeSeconds  int64
}

AgentConfig holds runtime configuration for an agent process.

type Client

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

Client wraps the gRPC IngestClient with connection lifecycle management.

func NewClient

func NewClient(ctx context.Context, serverURL string, insecureSkipVerify bool, logger *slog.Logger) (*Client, error)

NewClient dials the server at serverURL and returns a ready-to-use Client. serverURL should start with "grpcs://" (TLS) or "grpc://" (plaintext, not recommended). If insecureSkipVerify is true, TLS certificate validation is skipped (debug only).

func (*Client) Close

func (c *Client) Close() error

Close closes the underlying gRPC connection.

func (*Client) DialPush

func (c *Client) DialPush(ctx context.Context, id *Identity, logger *slog.Logger, hooks StreamHooks) (*PushStream, error)

DialPush opens the bidirectional Push stream, performs the Ed25519 auth handshake, and returns a PushStream ready to send events.

func (*Client) EnableCommands added in v1.3.8

func (c *Client) EnableCommands(rt runtime.Runtime, agentVersion string, logger *slog.Logger)

EnableCommands lets the server issue commands (log reads) against rt, and makes the running build known at each connect. Must be called before DialPush; the receive loop starts inside it, so a later hook-up would race.

func (*Client) Register

Register calls the unary RegisterAgent RPC.

type CommandRunner added in v1.3.8

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

CommandRunner executes server-issued commands against the local runtime. One instance is shared across reconnections; in-flight work is keyed by the server-minted request id so a Cancel can reach it.

func NewCommandRunner added in v1.3.8

func NewCommandRunner(rt runtime.Runtime, logger *slog.Logger) *CommandRunner

NewCommandRunner builds a runner over rt. rt may be nil, in which case every command is answered with an error rather than silently dropped.

func (*CommandRunner) CancelAll added in v1.3.8

func (r *CommandRunner) CancelAll()

CancelAll aborts every in-flight command, used when the stream goes away.

func (*CommandRunner) Handle added in v1.3.8

func (r *CommandRunner) Handle(ctx context.Context, out resultSender, cmd *agentpb.AgentCommand)

Handle dispatches cmd, returning immediately: work runs in its own goroutine so the stream's receive loop is never blocked by a log tail.

type EnrollmentToken

type EnrollmentToken struct {
	TokenID           string
	TokenHash         string
	TokenPrefix       string
	CreatedAt         time.Time
	ExpiresAt         time.Time
	ConsumedAt        *time.Time
	ConsumedByAgentID *string
}

EnrollmentToken represents a one-time token for enrolling an agent. The cleartext is deliberately absent: only its SHA-256 is persisted, so a copy of the database file yields nothing replayable. TokenPrefix is the leading, non-secret slice kept so a read path can still name the token it matched.

func (*EnrollmentToken) Masked added in v1.3.9

func (t *EnrollmentToken) Masked() string

Masked renders a token for display from the stored prefix alone. The rest is unrecoverable by design, so this is all any read path can ever show.

type Identity

type Identity struct {
	AgentID      string     `json:"agent_id"`
	PublicKey    []byte     `json:"public_key"`
	PrivateKey   []byte     `json:"private_key"`
	Registered   bool       `json:"registered"`
	RegisteredAt *time.Time `json:"registered_at,omitempty"`
}

Identity holds the persistent agent identity stored on disk.

func LoadOrCreate

func LoadOrCreate(dataDir string) (*Identity, error)

LoadOrCreate loads an existing identity from dataDir/identity.json, or generates and persists a new Ed25519 keypair if no file exists. File is created with mode 0600.

func (*Identity) Save

func (id *Identity) Save(dataDir string) error

Save persists updated identity fields (e.g. Registered) back to disk with mode 0600.

func (*Identity) Sign

func (id *Identity) Sign(message []byte) []byte

Sign returns an Ed25519 signature over message using the stored private key.

type OSIdentity added in v1.7.0

type OSIdentity struct {
	ID                string
	VersionID         string
	PrettyName        string
	Source            string
	UnavailableReason string
}

OSIdentity is the operating system identity an agent reports for its host.

type PushStream

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

PushStream wraps an authenticated bidirectional gRPC stream for pushing agent events. Multiple goroutines may call Send concurrently.

func (*PushStream) Close

func (ps *PushStream) Close()

Close signals the end of the send side of the stream.

func (*PushStream) Send

func (ps *PushStream) Send(evt *agentpb.AgentEvent) error

Send wraps evt in a ClientMessage and delivers it unchanged.

func (*PushStream) SendResult added in v1.3.8

func (ps *PushStream) SendResult(res *agentpb.CommandResult) error

SendResult delivers a reply to a server-issued command. Safe to call from the per-request goroutines, which share the stream with the telemetry senders.

func (*PushStream) SendStatus added in v1.6.0

func (ps *PushStream) SendStatus(st *agentpb.SpoolStatus) error

SendStatus reports the spool's state. It is not telemetry: the server neither rate-limits it nor dispatches it.

func (*PushStream) Wait

func (ps *PushStream) Wait(ctx context.Context) error

Wait blocks until the receive goroutine exits and returns its error (nil on clean close). If ctx is cancelled before recvLoop exits, it returns ctx.Err() immediately so the caller can proceed with shutdown. The leaked recvLoop goroutine terminates when the underlying gRPC stream is finally closed (typically by the deferred grpcClient.Close()).

type Spool added in v1.6.0

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

Spool is the agent's bounded outbound queue.

func NewSpool added in v1.6.0

func NewSpool(dataDir string, cfg SpoolConfig, logger *slog.Logger) *Spool

NewSpool opens the spool for dataDir, degrading to memory only if it cannot.

func (*Spool) Acked added in v1.6.0

func (s *Spool) Acked(seq uint64)

Acked records the row the server has taken everything up to.

func (*Spool) Attach added in v1.6.0

func (s *Spool) Attach(sink eventSink)

Attach binds the spool to a live stream, rewinding to the last ack.

func (*Spool) Close added in v1.6.0

func (s *Spool) Close() error

Close flushes what is buffered and releases the store.

func (*Spool) Depth added in v1.6.0

func (s *Spool) Depth() (int64, error)

Depth reports how many events are waiting, buffered and stored.

func (*Spool) Detach added in v1.6.0

func (s *Spool) Detach()

Detach unbinds the current stream. Queued events wait for the next one.

func (*Spool) Discard added in v1.6.0

func (s *Spool) Discard() error

Discard drops the whole spool, for a revoked agent.

func (*Spool) Drain added in v1.6.0

func (s *Spool) Drain(ctx context.Context) error

Drain sends everything queued on the attached stream, oldest first.

func (*Spool) Dropped added in v1.6.0

func (s *Spool) Dropped() (int64, error)

Dropped reports how many events were abandoned since the last connection.

func (*Spool) PurgeExpired added in v1.6.0

func (s *Spool) PurgeExpired(ctx context.Context) error

PurgeExpired drops everything observed before the retention window.

func (*Spool) RateLimited added in v1.6.0

func (s *Spool) RateLimited(retryAfter time.Duration)

RateLimited holds the drain for the delay the server asked for and rewinds to the last ack: the server drops a refused event without failing the stream.

func (*Spool) ResetDropped added in v1.6.0

func (s *Spool) ResetDropped()

ResetDropped clears the per-connection drop counter.

func (*Spool) Rewind added in v1.6.0

func (s *Spool) Rewind()

Rewind moves the send cursor back to the last acknowledged event.

func (*Spool) Send added in v1.6.0

func (s *Spool) Send(evt *agentpb.AgentEvent) error

Send queues an event; a disabled spool passes it straight to the stream.

type SpoolConfig added in v1.6.0

type SpoolConfig struct {
	MaxMemoryBytes int64
	MaxDiskBytes   int64
	MaxAge         time.Duration
}

SpoolConfig bounds what the spool may hold; both budgets at zero disable it.

func (SpoolConfig) Enabled added in v1.6.0

func (c SpoolConfig) Enabled() bool

Enabled reports whether events are queued rather than passed straight through.

type StreamHooks added in v1.6.0

type StreamHooks struct {
	Acked       func(uint64)
	RateLimited func(time.Duration)
}

StreamHooks lets the spool react to what the server says about the stream. A nil field ignores that signal.

Jump to

Keyboard shortcuts

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