fix(agent): surface the real cause when reviewed actions go unavailable

Two diagnosability gaps found by exercising the docker-update vertical
live with a real agent, each of which turns 'approved action cannot
execute' into a dead end with no visible cause:

- The durable operation-receipt store failing to open (e.g. unwritable
  state dir) silently registered the agent with receipt version 0; the
  only symptom was a server-side claim that the agent was too old. The
  agent now logs the real cause and state dir loudly at startup.
- The server readiness copy asserted 'older agent version' for any
  receipt-version mismatch. It now names both causes (old version or
  unavailable state directory) and points at the agent logs.

(The related docker collect-cycle watchdog landed separately in
a0f75b1bb with a hung-daemon reproducer.)

Contract-Neutral: agent-side diagnosability: receipt-store failure warning + honest readiness copy; no public contract delta
This commit is contained in:
rcourtman
2026-07-17 23:35:18 +01:00
parent e4e43c3f11
commit 331758f32b
4 changed files with 11 additions and 3 deletions
@@ -326,7 +326,7 @@ func (e dockerContainerActionExecutor) CheckActionAvailable(ctx context.Context,
agentID, _ := e.connectedDockerCommandAgentID(resource)
liveCapability, supported := e.agents.(agentOperationReceiptCapability)
if !supported || liveCapability.AgentOperationReceiptVersion(agentID) != operationreceipt.ProtocolVersion {
return unavailableDockerActionReadiness(operation, "operation_receipt_unsupported", "The Pulse agent on this host is still on an older version that cannot run reviewed actions. Wait for the agent to update itself or update it manually, then retry.")
return unavailableDockerActionReadiness(operation, "operation_receipt_unsupported", "The Pulse agent on this host cannot run reviewed actions: it is on an older version, or its durable state directory is unavailable. Update the agent, or check the agent logs if it is already current, then retry.")
}
return readiness
}
@@ -265,7 +265,7 @@ func hostStorageCleanupUnavailableReason(err error) string {
case "host_commands_disabled":
return "Typed host operations are disabled for this agent."
case "operation_receipt_unsupported":
return "The Pulse agent on this host is still on an older version that cannot run reviewed actions. Wait for the agent to update itself or update it manually, then retry."
return "The Pulse agent on this host cannot run reviewed actions: it is on an older version, or its durable state directory is unavailable. Update the agent, or check the agent logs if it is already current, then retry."
case "unsupported_cleanup_provider":
return "This host does not expose a supported package-cache cleanup provider."
case "cleanup_inventory_error":
+1 -1
View File
@@ -272,7 +272,7 @@ func hostUpdateUnavailableReason(err error) string {
case "host_commands_disabled":
return "Typed host operations are disabled for this agent."
case "operation_receipt_unsupported":
return "The Pulse agent on this host is still on an older version that cannot run reviewed actions. Wait for the agent to update itself or update it manually, then retry."
return "The Pulse agent on this host cannot run reviewed actions: it is on an older version, or its durable state directory is unavailable. Update the agent, or check the agent logs if it is already current, then retry."
case "unsupported_package_manager":
return "This host does not expose a supported package manager."
case "package_inventory_error":
+8
View File
@@ -132,6 +132,14 @@ func NewCommandClient(cfg Config, agentID, hostname, platform, version string) *
}
receipts, receiptErr := operationreceipt.Open(filepath.Join(stateDir, "operation-receipts.db"), hostOperationReceiptConfig())
if receiptErr != nil {
// Without the durable receipt store the agent registers with operation
// receipt version 0 and Pulse refuses every reviewed action for this
// host. Surface the real cause loudly; otherwise the only symptom is a
// misleading "agent is too old" readiness message on the server.
logger.Warn().Err(receiptErr).Str("state_dir", stateDir).
Msg("Operation receipt store unavailable — Pulse will refuse reviewed actions for this agent until its state directory is writable")
}
return &CommandClient{
pulseURL: strings.TrimRight(cfg.PulseURL, "/"),
apiToken: cfg.APIToken,