From 8be1c2379f23ba1a5ba27f75cf36d1b3f2ba7911 Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Wed, 9 Sep 2026 22:56:21 -0400 Subject: [PATCH] Add one-click agent self-upgrade from the endpoint detail page An operator could see that an endpoint was behind the published build but had no way to act on it. update_app resolves against Application rows, and the agent lives in the version store, so there was nothing to push. Bridge the two: the staged build is packaged and registered as a reserved Application row, and a managed PowerShell script is seeded to install it. The endpoint downloads that package over the existing collector-authenticated app-package route, so no parallel delivery path is introduced. The new route prepares the delivery and then DELEGATES to enqueue_endpoint_command as a digest-pinned run_script. It deliberately does not implement its own enqueue: a second path would duplicate, and eventually drift from, the endpoint lock, the Configuration Policy gate, stale-command reaping and the at-most-one-in-flight rule. A fleet-wide agent swap is the last action that should skip a quarantine check. Tests pin that delegation rather than only the happy path. Because it lands as run_script, the caller needs endpoints.runscript on top of endpoints.agentupdate. That is intended: this delivers arbitrary SYSTEM PowerShell by the same mechanism as any other script push. The audit event is written only on an actual 201, so a refused push does not record an upgrade that never happened. Replacing the agent is self-modifying the control plane - the process receiving the command is the one being swapped. The script therefore re-launches itself detached and exits immediately, because the agent runs run_script as a direct child and holds its stdout/stderr pipes while waiting on it. Expect the command to report queued rather than success: the process that would report completion is stopped mid-run. Judge the result by the version the endpoint reports afterward. Tray relaunch does not use the installer's `explorer.exe ` trick. That works in install-shopdbagent.ps1 only because the installer runs as the user - note its own `-ne "SYSTEM"` guard. This script always runs as SYSTEM in session 0, where the hand-off reaches no shell and exits 0, so Start-Process reported success for a relaunch that never happened. It also cannot descend into the session with a token-duplicating launch: UserContextSpool documents that WTSQueryUserToken is blocked by endpoint protection on these hosts. Instead a one-shot scheduled task targets the pinned ShopFloor account, matching how the user-context spool already resolves that user, and the tray must run as that account or the mapped-drive probe reports the wrong (empty) drive table. The relaunch is verified by polling for the process rather than assumed, and stays best-effort: the Run key still starts the tray at next logon, and a late tray is not worth rolling back a good agent upgrade. panel_status now carries agentbehind/agentversion/agentcurrent so the UI gates the action on a flag rather than parsing "(behind X)" out of a label. The button is confirmed by typing UPDATE, naming both versions and the service interruption, matching how run_script and system_restart are gated. Verified on live endpoints: upgraded 1.13.0.0 and 1.15.0.0 machines to 1.16.0.0. run_script delivery, digest fetch and system context all predate 1.13.0.0, so no minimum agent version is declared for this path. --- frontend/src/api/index.js | 12 + plugins/computers/frontend/views/PCDetail.vue | 73 +++- plugins/endpoints/api/agentregistry.py | 9 + plugins/endpoints/api/agentupgradescript.py | 136 +++++++ plugins/endpoints/api/routes.py | 105 +++++- .../api/templates/upgrade-shopdbagent.ps1 | 332 ++++++++++++++++++ shopdb/api/__init__.py | 15 + .../test_endpoints_agent_upgrade_route.py | 281 +++++++++++++++ 8 files changed, 957 insertions(+), 6 deletions(-) create mode 100644 plugins/endpoints/api/agentupgradescript.py create mode 100644 plugins/endpoints/api/templates/upgrade-shopdbagent.ps1 create mode 100644 tests/test_plugins/test_endpoints_agent_upgrade_route.py diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 18cf5150..ec8cdba1 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -228,6 +228,18 @@ export const endpointsApi = { } return api.post(`/endpoints/${assetId}/commands`, body) }, + // Pushes the published agent build to one endpoint. Separate from + // enqueueCommand because the server prepares the delivery (packages the + // build, seeds the upgrade script) before queueing it as a pinned + // run_script; the caller has no script id to pass and must not invent one. + enqueueAgentUpgrade(assetId, opts = {}) { + const body = {} + if (opts.policyoverride) { + body.policyoverride = true + body.overridereason = opts.overridereason + } + return api.post(`/endpoints/${assetId}/agentupgrade`, body) + }, listCommands(assetId) { return api.get(`/endpoints/${assetId}/commands`) }, diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue index a958a62e..ea144433 100644 --- a/plugins/computers/frontend/views/PCDetail.vue +++ b/plugins/computers/frontend/views/PCDetail.vue @@ -218,7 +218,7 @@ :assetid="computer.assetid" :accessmethods="computer.accessmethods || []" > -