Files
cairnobs/proto/sentry/agent/v1/agent_control.proto
T
jcoffey-dev 93c160ec51 Add agent restart lifecycle command
Extends the existing CheckIn RPC with a one-shot AgentCommand
(restart only -- stop/uninstall need real per-platform OS
service-manager integration and stay deliberately out of scope),
delivered at-most-once: cleared the instant it's handed to the agent
in a response, since a restarting agent's process is gone before it
could ever confirm receipt. On restart, the agent flushes whatever's
buffered, aborts its source task, and exits cleanly, relying entirely
on the host's own service manager to bring it back up.

Issuing a command is gated at RoleAdmin (stricter than config
editing's RoleEditor) and logged into the same audit_log table Phase
7's AI interactions use, via a new agent_command event type.

A real bug was found and fixed during live verification: the first
implementation tried to atomically read-and-clear pending_command in
a single INSERT...ON CONFLICT statement using a sibling CTE
referenced only from RETURNING, on the assumption that Postgres
evaluates every part of a WITH query against one pre-statement
snapshot. That's wrong specifically for FOR UPDATE, which always
reads the latest row version including one written earlier in the
same statement -- confirmed empirically (a restart command was
always coming back empty even when genuinely pending, so the agent
never received it). Fixed by splitting into two real, ordered
statements inside one explicit transaction.

See /docs/agent-management-design.md's "Lifecycle commands" section.
2026-08-16 20:30:07 -07:00

110 lines
5.2 KiB
Protocol Buffer

syntax = "proto3";
package sentry.agent.v1;
option go_package = "github.com/sentry/sentry/proto/sentry/agent/v1;agentv1";
// AgentControl is the control-plane counterpart to logs.v1.LogIngest's
// data-plane PushBatch -- the same mTLS channel/connection an agent
// already has open to ingest, a second gRPC service on the same
// listener rather than a second protocol or connection the agent would
// need to maintain (see /docs/agent-management-design.md). CheckIn is
// agent-initiated, called on the agent's own heartbeat ticker: there is
// still no path for the platform to reach into an agent uninvited. An
// agent asks "what should I be running" on its own schedule -- the same
// push-not-pull posture the heartbeat feature this builds on already
// established.
service AgentControl {
rpc CheckIn(CheckInRequest) returns (CheckInResponse);
}
// ReportedConfig is what an agent tells the platform about itself --
// read-only, for inventory/visibility. Deliberately excludes tls/ingest
// endpoint fields: those are never reported and never remotely
// overridable (see DesiredOverride's comment) -- reporting the ingest
// endpoint back to itself would be redundant (that's exactly the
// connection this request arrived over), and TLS material has no
// business leaving the host at all.
message ReportedConfig {
string agent_version = 1;
string source_kind = 2; // "journald", "file", "eventlog", "etw"
string source_detail = 3; // human-readable summary: unit name, file path, or channel list
uint64 batch_max_size = 4;
uint64 batch_flush_interval_ms = 5;
bool heartbeat_enabled = 6;
uint64 heartbeat_interval_ms = 7;
}
message CheckInRequest {
string host = 1;
string service = 2;
ReportedConfig current_config = 3;
// The DesiredOverride.version this agent last successfully applied,
// empty if it has never applied one. Lets the server distinguish
// "pending" (an edit exists the agent hasn't picked up yet) from
// "applied" for the web UI, without the agent needing to know
// anything about that distinction itself.
string applied_override_version = 4;
}
// DesiredOverride is the remotely-editable subset of an agent's config
// -- batch/heartbeat tuning, and, for journald sources, the unit
// filter. Every field is optional: unset means "no override for this
// field, keep whatever agent.toml says locally" -- a partial edit only
// touches the fields it sets. Never includes tls/ingest: those stay
// local-file-only, permanently, a deliberate security boundary (see
// /docs/agent-management-design.md) so a bad or malicious remote edit
// can never strand an agent or redirect where its logs go.
message DesiredOverride {
optional uint64 batch_max_size = 1;
optional uint64 batch_flush_interval_ms = 2;
optional bool heartbeat_enabled = 3;
optional uint64 heartbeat_interval_ms = 4;
// Only meaningful when the agent's local source is journald; ignored
// otherwise. Empty string means "no unit filter" (tail the whole
// journal), same semantics as the local config's own unit field.
optional string journald_unit = 5;
// Opaque version stamp the platform assigns on every edit. The
// agent's only obligation is to echo it back as
// CheckInRequest.applied_override_version once applied -- it never
// interprets the value itself.
string version = 6;
}
// AgentCommand is a one-shot action, not a persistent desired state like
// DesiredOverride -- delivered at-most-once (see CheckInResponse's
// comment). Scoped deliberately narrow: only RESTART exists today.
// STOP and UNINSTALL are real, disclosed future work, not oversights --
// both need genuine OS service-manager integration (systemd's
// Restart=/RestartPreventExitStatus= semantics vs. Windows SCM recovery
// options are different enough per platform that hand-waving them would
// be dishonest), which RESTART doesn't: a graceful shutdown followed by
// a clean process exit, relying on whatever restart policy the host's
// service manager already has configured -- the same contract systemd/
// SCM already expect from any well-behaved service.
enum AgentCommand {
AGENT_COMMAND_UNSPECIFIED = 0;
AGENT_COMMAND_RESTART = 1;
}
message CheckInResponse {
// False when no override has ever been set for this agent -- it
// should be running whatever agent.toml already has, untouched.
bool has_override = 1;
DesiredOverride override = 2;
// AGENT_COMMAND_UNSPECIFIED when there's nothing to do. Unlike
// DesiredOverride, there is no "applied_command_version" echoed back
// in CheckInRequest: the server clears a pending command the moment
// it hands it out in a response (see
// ingest/internal/agentregistry.Registry.CheckIn), not once the agent
// confirms execution -- a restarting agent's process is gone before
// it could ever send that confirmation. This is an honest at-most-
// once delivery, not at-least-once: a command lost to a network
// failure between this response and the agent acting on it is simply
// lost, same as any fire-and-forget signal. Re-issuing (PUT
// /agents/{host}/command again) is the operator's recourse, same as
// it would be for a `systemctl restart` that silently failed to reach
// its target.
AgentCommand pending_command = 3;
}