From 4fc86ec187172224e46c44274aa6f8eb22d10c49 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 22 Jul 2026 10:56:04 +0100 Subject: [PATCH] Hand off host-local uninstall commands for removed agents in Agent Doctor Removed targets previously dead-ended with no next step. Their row expansion now offers the host-local uninstall command, resolved from the retained diagnostic identity via the strict platform resolver, or both labeled Linux-family and Windows commands when the platform is unknown (the current wire reality, since removed records retain no platform). The handoff carries the diagnostic's agent id and hostname as uninstall identity flags and states the command runs on the affected host, not from Pulse. The governed uninstall builders now accept a narrowed AgentUninstallIdentity view so surfaces without a ledger-backed inventory row reuse the same transport. --- .../v6/internal/subsystems/agent-lifecycle.md | 19 ++++- .../v6/internal/subsystems/api-contracts.md | 9 ++ .../InfrastructureAgentDoctorPage.tsx | 47 ++++++++++ .../infrastructureAgentDoctorModel.test.ts | 85 +++++++++++++++++++ .../infrastructureAgentUpdateCommandsModel.ts | 42 ++++++++- .../useInfrastructureOperationsState.tsx | 18 ++-- 6 files changed, 210 insertions(+), 10 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 9276fd077..c81b2154f 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -1990,8 +1990,19 @@ only workload telemetry, e.g. Docker-only or Kubernetes-only agents) are now appended as diagnostics-only targets, honoring the scoped-agent filter, so a critical workload-only agent can no longer vanish from the fleet view. Diagnostics-only rows render the diagnostic's status, reasons, and evidence -but offer no host-local command (there is no ledger connection to derive one -from). Page presentation follows the same honesty rule: the +but offer no host-local update command (there is no ledger connection to +derive an update from). Removed rows are the deliberate exception in the +uninstall direction: a removed agent's one remaining next step is host-side +cleanup, so a removed target resolves its command platform from the retained +diagnostic identity through the strict platform resolver and its row +expansion hands off the host-local uninstall command +(`getInfrastructureAgentDoctorUninstallHandoff`) โ€” the single matching +platform command when the identity resolves, or both explicitly labeled +Linux-family and Windows commands when it does not, never one guessed +executable. The handoff carries the diagnostic's agent id and hostname as +uninstall identity flags, and its copy states the command runs on the +affected host itself, not from Pulse. Page presentation follows the same +honesty rule: the "Target" column renders a version only when one is actually published, the host-local command explainer renders only when at least one row offers a command, and the summary chips list only non-zero status counts. The summary @@ -2001,8 +2012,8 @@ also offers plain-text diagnostic reports (fleet-level over the currently visible rows and per-agent from a row expansion) via `formatInfrastructureAgentDoctorReport`; reports carry status, versions, last-seen, reasons, identity evidence, and non-command repair actions, and -must never embed host-local update commands because those can carry install -tokens. +must never embed host-local update or uninstall commands because those can +carry install tokens. ### Governed action readiness remains outside agent lifecycle authority diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index a2d04b6d8..6f9c5b172 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -5972,6 +5972,15 @@ canonical identity when inventory already has it: shell uninstall payloads must carry `--agent-id`, and PowerShell uninstall payloads must carry `PULSE_AGENT_ID`, so deregistration targets the intended governed agent record instead of depending on local fallback files or hostname lookup. +That uninstall identity seam is deliberately narrower than the unified +inventory row: the governed uninstall builders in +`frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` +accept a caller-supplied identity view carrying only +`agentActionId`/`agentId`/`hostname` (the exported `AgentUninstallIdentity` +pick), so lifecycle surfaces without a ledger-backed inventory row โ€” Agent +Doctor's removed-agent diagnostics handoff โ€” bind copied uninstall payloads to +the retained diagnostic identity through the same governed builders instead of +inventing a second uninstall transport. The same identity-preservation contract applies to copied upgrade transport: shell upgrade payloads must carry `--agent-id` and `--hostname`, and PowerShell upgrade payloads must carry `PULSE_AGENT_ID` and `PULSE_HOSTNAME`, diff --git a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx index 03c302b76..4d9db0c12 100644 --- a/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx @@ -18,6 +18,7 @@ import { } from '@/utils/unifiedAgentInventoryPresentation'; import { formatInfrastructureAgentDoctorReport, + getInfrastructureAgentDoctorUninstallHandoff, summarizeInfrastructureAgentDoctorTargets, type InfrastructureAgentDoctorStatus, type InfrastructureAgentDoctorTarget, @@ -373,6 +374,8 @@ export const InfrastructureAgentDoctorPage: Component action.code !== 'copy_upgrade_command', ); + const uninstallHandoff = () => + getInfrastructureAgentDoctorUninstallHandoff(target); const expanded = () => isExpanded(target); return ( @@ -526,6 +529,50 @@ export const InfrastructureAgentDoctorPage: Component + + {(handoff) => ( +
+

+ This agent was removed from Pulse, but the agent software may + still be installed on its host. Finish detaching it by running + the uninstall command on the affected host itself. Pulse does + not run commands remotely. +

+ + {(entry) => ( +
+
+ {entry.label} +
+
+ + void copyCommand( + operations.getPlatformUninstallCommand( + entry.platform, + handoff().identity, + ), + ) + } + title="Copy host-local agent uninstall command" + label={`Copy ${entry.label} uninstall command for ${target.displayName}`} + /> +
+                                            
+                                              {operations.getPlatformUninstallCommand(
+                                                entry.platform,
+                                                handoff().identity,
+                                              )}
+                                            
+                                          
+
+
+ )} +
+
+ )} +
+