From 331758f32b6fececf1a80ea69fb9c70cdd53cab9 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Jul 2026 23:35:18 +0100 Subject: [PATCH] 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 --- internal/api/docker_container_action_executor.go | 2 +- internal/api/host_storage_cleanup_action_executor.go | 2 +- internal/api/host_update_action_executor.go | 2 +- internal/hostagent/commands.go | 8 ++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/api/docker_container_action_executor.go b/internal/api/docker_container_action_executor.go index 133150a91..9c3a76b8d 100644 --- a/internal/api/docker_container_action_executor.go +++ b/internal/api/docker_container_action_executor.go @@ -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 } diff --git a/internal/api/host_storage_cleanup_action_executor.go b/internal/api/host_storage_cleanup_action_executor.go index 33c608c1a..797316467 100644 --- a/internal/api/host_storage_cleanup_action_executor.go +++ b/internal/api/host_storage_cleanup_action_executor.go @@ -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": diff --git a/internal/api/host_update_action_executor.go b/internal/api/host_update_action_executor.go index 8a057a88d..32f12e9b9 100644 --- a/internal/api/host_update_action_executor.go +++ b/internal/api/host_update_action_executor.go @@ -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": diff --git a/internal/hostagent/commands.go b/internal/hostagent/commands.go index fea5756a8..96129a520 100644 --- a/internal/hostagent/commands.go +++ b/internal/hostagent/commands.go @@ -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,