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.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// Adapts *Store to api/agents.CommandLogger -- same shape as
|
||||
// ai_interaction_adapter.go's AIInteractionLogger, wired in by
|
||||
// enterprise/cmd/enterprise-api alongside it.
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/sentry/sentry/api/agents"
|
||||
"github.com/sentry/sentry/api/authz"
|
||||
)
|
||||
|
||||
// AgentCommandLogger implements agents.CommandLogger by translating its
|
||||
// CommandLogEntry into this package's Entry, reading tenant/user
|
||||
// identity from ctx -- same "read identity from ctx rather than the
|
||||
// interface growing tenant-awareness" shape as every other adapter in
|
||||
// this package.
|
||||
type AgentCommandLogger struct {
|
||||
store *Store
|
||||
source Source
|
||||
}
|
||||
|
||||
func NewAgentCommandLogger(store *Store, source Source) *AgentCommandLogger {
|
||||
return &AgentCommandLogger{store: store, source: source}
|
||||
}
|
||||
|
||||
type agentCommandDetail struct {
|
||||
Host string `json:"host"`
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
func (l *AgentCommandLogger) LogCommand(ctx context.Context, entry agents.CommandLogEntry) error {
|
||||
identity, ok := authz.IdentityFromContext(ctx)
|
||||
if !ok || identity.TenantID == "" {
|
||||
return fmt.Errorf("audit: no tenant identity in context, refusing to write an unattributable audit entry")
|
||||
}
|
||||
|
||||
var userID *string
|
||||
if identity.UserID != "" {
|
||||
userID = &identity.UserID
|
||||
}
|
||||
|
||||
detail, err := json.Marshal(agentCommandDetail{Host: entry.Host, Command: entry.Command})
|
||||
if err != nil {
|
||||
return fmt.Errorf("audit: marshaling agent command detail: %w", err)
|
||||
}
|
||||
|
||||
_, err = l.store.Append(ctx, Entry{
|
||||
TenantID: identity.TenantID,
|
||||
UserID: userID,
|
||||
Source: l.source,
|
||||
EventType: EventAgentCommand,
|
||||
Status: StatusSuccess,
|
||||
Detail: detail,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -53,6 +53,14 @@ const (
|
||||
// original input, confidence, and whether the user edited the
|
||||
// suggestion before using it -- see ai_interaction_adapter.go.
|
||||
EventAIInteraction EventType = "ai_interaction"
|
||||
// EventAgentCommand: a lifecycle command (restart) issued to an
|
||||
// agent. Detail carries the target host and the command; Status is
|
||||
// always success here -- this logs that the command was *issued* to
|
||||
// storage, not that the agent confirmed executing it (a restarting
|
||||
// agent's process can't send that confirmation -- see
|
||||
// agent_control.proto's CheckInResponse comment). See
|
||||
// agent_command_adapter.go.
|
||||
EventAgentCommand EventType = "agent_command"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
Reference in New Issue
Block a user