feat(agent): agent self update and scheduled updates#1726
feat(agent): agent self update and scheduled updates#1726Vladyslav Nikonov (vnikonov-devolutions) wants to merge 3 commits intomasterfrom
Conversation
Let maintainers know that an action is required on their side
|
There was a problem hiding this comment.
Pull request overview
Adds “Agent auto update” support across the Windows Agent MSI, the Agent updater runtime, and Gateway’s API surface so that the Agent can self-update (manually triggered and on a maintenance-window schedule).
Changes:
- Introduces an updater shim executable (
devolutions-agent-updater.exe) and packages it into the Windows Agent MSI + CI artifacts. - Adds Gateway API endpoints to trigger an Agent update and to read/write Agent auto-update configuration.
- Extends the Agent updater to support
Agentas an updateable product and implements periodic self-update checks using interval + maintenance window settings.
Reviewed changes
Copilot reviewed 30 out of 33 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| package/AgentWindowsManaged/Resources/Strings_en-US.json | Adds Service Configuration dialog strings for auto-update. |
| package/AgentWindowsManaged/Resources/Strings.g.cs | Regenerates strongly-typed string IDs for new resources. |
| package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl | Adds French localized strings + updates feature name/description for updater feature. |
| package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl | Adds English localized strings + updates feature name/description for updater feature. |
| package/AgentWindowsManaged/Properties/AgentProperties.g.tt | Formatting-only updates to the T4 template. |
| package/AgentWindowsManaged/Properties/AgentProperties.g.cs | Formatting-only updates to generated properties. |
| package/AgentWindowsManaged/Program.cs | Adds updater shim file into MSI layout under an updater feature folder. |
| package/AgentWindowsManaged/Dialogs/Wizard.cs | Minor formatting change. |
| package/AgentWindowsManaged/DevolutionsAgent.csproj | Adds WixSharp.wix.bin reference (WiX toolset payload). |
| package/AgentWindowsManaged/Actions/CustomActions.cs | Adds MSI custom action to write Updater.AgentAutoUpdate.Enabled into agent.json. |
| package/AgentWindowsManaged/Actions/AgentActions.cs | Schedules new auto-update configuration custom action in execute sequence. |
| devolutions-gateway/src/api/update_agent.rs | New Gateway endpoints: trigger agent update + get/set agent auto-update config. |
| devolutions-gateway/src/api/update.rs | Extends update manifest payload to include agent: None. |
| devolutions-gateway/src/api/mod.rs | Wires new update-agent routes into the main router. |
| devolutions-agent/src/updater/productinfo/mod.rs | Adds productinfo ID constant for Agent. |
| devolutions-agent/src/updater/product_actions.rs | Adds Agent-specific update actions (self-update path) and guards unreachable branches. |
| devolutions-agent/src/updater/product.rs | Adds Product::Agent and integrates with UpdateJson/productinfo mapping. |
| devolutions-agent/src/updater/package.rs | Implements Agent self-update install via detached shim; skips in-process uninstall for Agent. |
| devolutions-agent/src/updater/mod.rs | Adds periodic polling for auto-update, interval parsing, maintenance window logic + tests, and version gating. |
| devolutions-agent/src/updater/error.rs | Adds updater shim specific error variants. |
| devolutions-agent/src/updater/detect.rs | Adds registry detection for installed Agent version/product-code. |
| devolutions-agent/src/config.rs | Adds updater.agent_auto_update config field (documented) to agent config DTO. |
| devolutions-agent/src/agent-updater.rs | New Windows-only shim executable that runs msiexec for uninstall/install silently. |
| devolutions-agent/Cargo.toml | Adds second binary target + Windows-only deps for interval/window logic. |
| crates/devolutions-agent-shared/src/update_json.rs | Extends UpdateJson schema with optional Agent section. |
| crates/devolutions-agent-shared/src/lib.rs | Exposes new agent auto-update module + re-exports config type. |
| crates/devolutions-agent-shared/src/agent_auto_update.rs | New helpers to read/write Updater.AgentAutoUpdate in agent.json. |
| crates/devolutions-agent-shared/Cargo.toml | Adds serde_json dependency for new config helpers. |
| ci/tlk.ps1 | Adds Windows build packaging for the new updater shim binary. |
| ci/package-agent-windows.ps1 | Threads updater exe through MSI packaging script and env vars. |
| Cargo.lock | Locks new Rust dependencies (humantime, time). |
| .github/workflows/package.yml | Includes shim in agent artifacts and wires DAGENT_UPDATER_EXECUTABLE. |
| .github/workflows/ci.yml | Exposes/upstreams the shim executable path into Windows packaging steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // The first agent version with self-update support is 2026.2 (anything built after | ||
| // 2026.1.x lacks the updater shim and would permanently disable auto-update). | ||
| const AGENT_MIN_SELF_UPDATE_VERSION: DateVersion = DateVersion { | ||
| year: 2026, | ||
| month: 1, | ||
| day: 0, | ||
| revision: 0, | ||
| }; |
There was a problem hiding this comment.
The comment says the first version supporting self-update is 2026.2, but the constant is set to 2026.1.0.0. With the current <= checks, versions like 2026.1.5.0 would be treated as self-update capable (because they are > 2026.1.0.0), which could trigger an update to a build that lacks the updater shim and break future auto-updates. Consider setting the threshold to 2026.2.0.0 (or otherwise excluding all 2026.1.*) and using a < min_supported style comparison; also the comment likely meant “built before 2026.2”.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Thank you!
I roughly reviewed the API side and left a few questions to guide the design.
We’ll have to perform a dry run of the workflows to ensure everything is packaged as expected 💡
There was a problem hiding this comment.
I see the /jet/update route is already used to update the Devolutions Gateway itself and the Hub service. This PR writes to the same update.json file that /jet/update already writes to, but through a completely separate endpoint with a different API shape. That means we now have two routes that mutate the same file, with potentially different authorization, different parameter styles, and no coordination between them. Why not extend the existing endpoint instead?
The questions I have to direct the direction:
- Could we extend the existing
/jet/updateinstead? (In a clean way) - Is the Agent the only program we’ll ever want to auto-update?
- If we do add a separate endpoint, what prevents a proliferation of update endpoints as more products are added?
- If a separate endpoint is chosen, should it at least live under
/jet/update/agentto signal the relationship, rather than an orthogonal path? - Who should be authorized to trigger an Agent update? Same
gateway.updatescope, or a new one? This affects both approaches. - What happens if the Agent is asked to update itself while it's mid-way through processing another update (e.g.: Gateway)?
- The current
/jet/updateuses query params for version, should we take this opportunity to move version into the request body instead, for consistency and extensibility (e.g.: specifying multiple product versions in one call)?
There was a problem hiding this comment.
- Extended
/jet/update, deprecated query param, added new body request params, supporting multiple products update - Added new separate endpoint
/jet/update/scheduleto control new functionality - scheduled update - Decided to keep scope
gateway.updatefor all /jet/update* endpoints. I think its fair to think that if gateway update triggering is allowed for this scope, then agent/update scheduling should be implied IMHO - Added mechanism to reject any concurrent updates if there is already update in progress.
| let update_json = UpdateJson { | ||
| agent: Some(ProductUpdateInfo { target_version }), | ||
| gateway: None, | ||
| hub_service: None, | ||
| }; | ||
|
|
||
| let update_json = serde_json::to_string(&update_json).map_err( | ||
| HttpError::internal() | ||
| .with_msg("failed to serialize the update manifest") | ||
| .err(), | ||
| )?; | ||
|
|
||
| std::fs::write(updater_file_path, update_json).map_err( | ||
| HttpError::internal() | ||
| .with_msg("failed to write the new `update.json` manifest on disk") | ||
| .err(), | ||
| )?; |
There was a problem hiding this comment.
note: Here we handle the json serialization and filesystem write in Devolutions Gateway itself.
There was a problem hiding this comment.
thought: And now in this module we use serde_json directly, and perform I/O with the filesystem. Just thought it was worth pointing out the difference, although I see the nature of the operations is materially different.
devolutions-agent/Cargo.toml
Outdated
|
|
||
| [target.'cfg(windows)'.dependencies] | ||
| aws-lc-rs = "1.15" | ||
| humantime = "2" |
There was a problem hiding this comment.
issue: I would rather avoid humantime since this is purely machine-to-machine communication. We don’t need to add parsing overhead I think.
There was a problem hiding this comment.
Well, I think it still worth to use for more clean agent.json representation, so it could be edited manually on the machine; It is still possible to set interval as seconds without unit suffixes.
There was a problem hiding this comment.
Okay! Just confirming: is it something we really expect users to modify? I am under the impression we’ll only document the DVLS-path with UI. If you think it’s really worth it, you can leave humantime.
There was a problem hiding this comment.
removed dependency
devolutions-agent/Cargo.toml
Outdated
| [[bin]] | ||
| name = "devolutions-agent" | ||
| path = "src/main.rs" | ||
|
|
||
| [[bin]] | ||
| name = "devolutions-agent-updater" | ||
| path = "src/agent-updater.rs" | ||
|
|
There was a problem hiding this comment.
issue: Adding a second binary to this crate has the benefit of simplicity, but the agent crate pulls in ironrdp, devolutions-pedm, win-api-wrappers with heavy Windows API features, rand, rustls, and more. The updater binary needs almost none of that, yet both [[bin]] targets link against the same lib.rs, so we’re paying for all of it in compile time and binary size.
Same-crate is fine as a starting point with a note to extract later, but we should not ship it as-is.
|
Also, please link a Jira ticket! Thanks 🙂 |
e84e4ae to
f52d4f5
Compare
Implementation notes
|
|
Additional changes: new GET endpoints both for /jet/update and /jet/update/schedule. Agent now provides new update_status.json for querying product update and schedule status. |
| RecordingsRead, | ||
| #[serde(rename = "gateway.update")] | ||
| Update, | ||
| #[serde(rename = "gateway.update.read")] |
There was a problem hiding this comment.
new scope for read-only update operations
| license: | ||
| name: MIT/Apache-2.0 | ||
| version: 2025.3.2 | ||
| version: 2026.1.0 |
There was a problem hiding this comment.
note to self: this API version most likely should be updated too before gateway repo release cutting
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 30 changed files in this pull request and generated 8 comments.
Comments suppressed due to low confidence (1)
devolutions-agent/src/updater/mod.rs:819
init_update_jsonunconditionally overwritesupdate.jsonon every agent start. This can wipe pending update requests written by the Gateway (and any existing Products/Schedule state) before the file watcher loop processes them. Consider only creating the file when it does not exist, or reading the existing manifest and updating just the Schedule field while preserving Products/other fields.
async fn init_update_json(initial_schedule: Option<&UpdaterSchedule>) -> anyhow::Result<Utf8PathBuf> {
let update_file_path = get_updater_file_path();
// Write the current schedule from agent.json into update.json so the gateway can
// read it immediately via GET /jet/update/schedule without waiting for a poll cycle.
let v2 = UpdateManifestV2 {
schedule: initial_schedule.map(|s| UpdateSchedule::from(s.clone())),
..Default::default()
};
let initial_manifest = UpdateManifest::ManifestV2(v2);
let default_update_json =
serde_json::to_string_pretty(&initial_manifest).context("failed to serialize default update.json")?;
fs::write(&update_file_path, default_update_json)
.await
.context("failed to write default update.json file")?;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
devolutions-agent/src/updater/mod.rs
Outdated
| if product == Product::Agent && version <= AGENT_MIN_SELF_UPDATE_VERSION { | ||
| warn!( | ||
| %product, | ||
| %version, | ||
| min_version = %AGENT_MIN_SELF_UPDATE_VERSION, | ||
| "Target version does not support agent self-update; skipping to avoid breaking auto-update" | ||
| ); | ||
| return Ok(None); | ||
| } |
There was a problem hiding this comment.
The min-version guard for Agent self-update is inconsistent: the Latest branch allows remote_version == AGENT_MIN_SELF_UPDATE_VERSION, but the Specific branch rejects version == AGENT_MIN_SELF_UPDATE_VERSION via <=. If 2026.2.0.0 is the first supported version, this should likely be a strict < check so the minimum supported version is allowed.
4431b67 to
1a19165
Compare
|
Copilot Please read all not resolved copilot comments and prepare PR with fixes |
|
Vladyslav Nikonov (@vnikonov-devolutions) I've opened a new pull request, #1732, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
Poke me again when you are satisfied with the result! |
…1732) Fixes five bugs/inconsistencies identified in code review of the agent self-update and scheduled updates feature. ## Changes - **Min-version guard consistency** (`updater/mod.rs`): `Specific` branch used `<=` (incorrectly blocking `AGENT_MIN_SELF_UPDATE_VERSION` itself); changed to `<` to match the `Latest` branch. - **Shim log path** (`updater/package.rs`): Log path was derived from `shim_path`, but the shim writes its log relative to `msi_path` (`{msi_path}.shim.log`). Fixed to match actual file location. - **`POST /jet/update` empty products** (`api/update.rs`): Calling the endpoint with no query param and no body silently overwrote `update.json` with an empty `Products` map, clearing any pending update requests. Now returns `400 Bad Request`. - **`GET /jet/update/schedule` doc mismatch** (`api/update.rs`): Doc comment said the handler reads `update.json`; it actually reads `agent_status.json`. Comment corrected. - **`UpdateProduct` OpenAPI schema** (`api/update.rs`, `gateway-api.yaml`): Derived `ToSchema` generated an incorrect `oneOf` with an object variant for `Other`, mismatching the flat-string serde encoding. Replaced with a manual `ToSchema` impl emitting `type: string`. YAML regenerated. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vnikonov-devolutions <246051166+vnikonov-devolutions@users.noreply.github.com>
This PR includes the following:
/jet/updateendpoint improvements (moved params to request body, deprecated query param/jet/update/scheduleendpoint for controlling on-machine update schedulingIssue: ARC-447