syntax = "proto3"; package sentry.agent.v1; option go_package = "github.com/cairnobs/cairnobs/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; // Extra file paths this agent should tail in addition to whatever its // local [source] already is -- never a replacement for the primary // source (an agent whose local source is journald can still be told // to also tail a file, and vice versa). Unlike every field above, // there's no real "unset" state for a list: the web UI/CLI always // resubmit the complete desired list on every edit (same "PUT // replaces the whole override" convention every other field already // follows -- see api/agents/handler.go's handleSetConfig), so an // empty list unambiguously means "no extra paths right now," not // "don't touch this." repeated string extra_file_paths = 7; } // 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; }