improved agent self update flow

This commit is contained in:
Oliver Kieselbach
2026-04-08 13:07:02 +02:00
parent 7f1db3d317
commit b3e9790be9
5 changed files with 378 additions and 38 deletions
@@ -104,6 +104,19 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
private bool _isFirstConfigApply = true;
private int? _deferredSecurityAuditConfigVersion; // deferred until after agent_started
// Runtime self-update trigger (hash mismatch recovery)
// Set by the agent host (Program.cs) to inject the SelfUpdater implementation since
// .Core cannot reference the .Agent exe project. Once-per-process guard via Interlocked.
private static int _integrityUpdateAttempted; // 0 = not yet, 1 = attempted
/// <summary>
/// Callback invoked by the runtime hash-mismatch path to trigger a forced self-update.
/// Host wires this to <c>SelfUpdater.CheckAndApplyUpdateAsync(..., forceUpdate: true,
/// triggerReason: "runtime_hash_mismatch", downloadTimeoutMsOverride: 60_000)</c>.
/// Receives the backend-provided ZIP SHA-256 hash so the updater can use it as the
/// trusted-channel integrity source.
/// </summary>
public Func<string, Task> RuntimeSelfUpdateTriggerAsync { get; set; }
public MonitoringService(AgentConfiguration configuration, AgentLogger logger, string agentVersion,
string previousExitType = "first_run", string previousCrashException = null,
DateTime? lastBootTimeUtc = null)
@@ -288,6 +301,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
// If the agent was self-updated on the previous run, emit a timeline event
EmitSelfUpdateEventIfMarkerExists();
// If the startup self-update was skipped (network timeout etc.), emit telemetry
// so we can measure how often this happens across the fleet.
EmitSelfUpdateSkippedEventIfMarkerExists();
// Emit deferred security_audit (initial UnrestrictedMode) now that agent_started has its sequence
if (_deferredSecurityAuditConfigVersion.HasValue)
{
@@ -604,6 +621,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_ = _emergencyReporter?.TrySendAsync(
AgentErrorType.IntegrityCheckFailed,
$"Binary hash mismatch: expected={expectedHash.Substring(0, 12)}..., actual={actualHash.Substring(0, 12)}...");
// Trigger a forced self-update on hash mismatch (once per process).
// Runs as fire-and-forget on a background task so monitoring keeps running —
// on success, SelfUpdater calls Environment.Exit via RestartAgent;
// on failure we log and continue with the stale binary until next restart.
TriggerSelfUpdateOnMismatch();
}
}
catch (Exception ex)
@@ -612,6 +635,44 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
}
/// <summary>
/// Invokes the host-provided <see cref="RuntimeSelfUpdateTriggerAsync"/> delegate on a
/// background task. Guarded by <c>_integrityUpdateAttempted</c> so we only try once
/// per agent process (prevents download loops if the update repeatedly fails).
/// </summary>
private void TriggerSelfUpdateOnMismatch()
{
if (Interlocked.Exchange(ref _integrityUpdateAttempted, 1) == 1)
{
_logger.Info("Self-update: already attempted in this process — skipping");
return;
}
if (RuntimeSelfUpdateTriggerAsync == null)
{
_logger.Warning("Self-update: no RuntimeSelfUpdateTriggerAsync wired by host — cannot recover from hash mismatch");
return;
}
// Hand the ZIP hash from the current config to the updater as trusted-channel source.
// Null is acceptable — SelfUpdater falls back to the version.json hash.
var zipHash = _remoteConfigService?.CurrentConfig?.LatestAgentSha256;
Task.Run(async () =>
{
try
{
_logger.Info("Self-update: triggering forced update due to runtime hash mismatch");
await RuntimeSelfUpdateTriggerAsync(zipHash).ConfigureAwait(false);
_logger.Warning("Self-update: returned without restart — update did not apply, agent continues with stale binary");
}
catch (Exception ex)
{
_logger.Error("Self-update: unexpected failure during forced update", ex);
}
});
}
private void ApplyRuntimeSettingsFromRemoteConfig()
{
var config = _remoteConfigService?.CurrentConfig;
@@ -729,6 +790,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
/// <summary>
/// Reads the self-update marker file left by SelfUpdater, emits an agent_self_updated event,
/// and deletes the marker. Best-effort — failures are logged but never block startup.
/// Includes phase timings, trigger reason, and a computed downtime (new process startup
/// time minus old process exit time) so we can aggregate update duration and event-loss
/// window across the fleet.
/// </summary>
private void EmitSelfUpdateEventIfMarkerExists()
{
@@ -739,33 +803,76 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
return;
var json = File.ReadAllText(markerPath);
var marker = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
if (marker == null)
var marker = Newtonsoft.Json.Linq.JObject.Parse(json);
var previousVersion = (string)marker["previousVersion"] ?? "unknown";
var newVersion = (string)marker["newVersion"] ?? "unknown";
var updatedAtUtc = (string)marker["updatedAtUtc"] ?? "unknown";
var triggerReason = (string)marker["triggerReason"] ?? "startup";
var exitAtUtc = (string)marker["exitAtUtc"];
long? ReadLong(string key)
{
TryDeleteMarker(markerPath);
return;
var token = marker[key];
if (token == null || token.Type == Newtonsoft.Json.Linq.JTokenType.Null)
return null;
try { return token.ToObject<long>(); }
catch { return null; }
}
marker.TryGetValue("previousVersion", out var previousVersion);
marker.TryGetValue("newVersion", out var newVersion);
marker.TryGetValue("updatedAtUtc", out var updatedAtUtc);
var versionCheckMs = ReadLong("versionCheckMs");
var downloadMs = ReadLong("downloadMs");
var zipSizeBytes = ReadLong("zipSizeBytes");
var verifyMs = ReadLong("verifyMs");
var extractMs = ReadLong("extractMs");
var swapMs = ReadLong("swapMs");
var totalUpdateMs = ReadLong("totalUpdateMs");
_logger.Info($"Self-update detected: {previousVersion} → {newVersion}");
// Downtime = new process start (_agentStartTimeUtc) minus old process exit.
// Measured across the PowerShell Wait-Process gap + new agent startup up to
// MonitoringService init. Closest proxy for "events potentially missed".
long? downtimeMs = null;
if (!string.IsNullOrEmpty(exitAtUtc) &&
DateTime.TryParse(exitAtUtc, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.RoundtripKind, out var exitDt))
{
downtimeMs = (long)(_agentStartTimeUtc - exitDt).TotalMilliseconds;
}
_logger.Info($"Self-update detected: {previousVersion} → {newVersion} (trigger={triggerReason}, downtimeMs={downtimeMs?.ToString() ?? "n/a"})");
var data = new Dictionary<string, object>
{
{ "previousVersion", previousVersion },
{ "newVersion", newVersion },
{ "updatedAtUtc", updatedAtUtc },
{ "triggerReason", triggerReason }
};
if (exitAtUtc != null) data["exitAtUtc"] = exitAtUtc;
if (versionCheckMs.HasValue) data["versionCheckMs"] = versionCheckMs.Value;
if (downloadMs.HasValue) data["downloadMs"] = downloadMs.Value;
if (zipSizeBytes.HasValue) data["zipSizeBytes"] = zipSizeBytes.Value;
if (verifyMs.HasValue) data["verifyMs"] = verifyMs.Value;
if (extractMs.HasValue) data["extractMs"] = extractMs.Value;
if (swapMs.HasValue) data["swapMs"] = swapMs.Value;
if (totalUpdateMs.HasValue) data["totalUpdateMs"] = totalUpdateMs.Value;
if (downtimeMs.HasValue) data["downtimeMs"] = downtimeMs.Value;
// Runtime-triggered updates are bumped to Warning so they stand out in the UI —
// startup updates stay Info because they are the normal rollout path.
var severity = string.Equals(triggerReason, "runtime_hash_mismatch", StringComparison.Ordinal)
? EventSeverity.Warning
: EventSeverity.Info;
EmitEvent(new EnrollmentEvent
{
SessionId = _configuration.SessionId,
TenantId = _configuration.TenantId,
EventType = Constants.EventTypes.AgentSelfUpdated,
Severity = EventSeverity.Info,
Severity = severity,
Source = "Agent",
Message = $"Agent self-updated from {previousVersion} to {newVersion}",
Data = new Dictionary<string, object>
{
{ "previousVersion", previousVersion ?? "unknown" },
{ "newVersion", newVersion ?? "unknown" },
{ "updatedAtUtc", updatedAtUtc ?? "unknown" }
},
Message = $"Agent self-updated from {previousVersion} to {newVersion} (trigger={triggerReason})",
Data = data,
ImmediateUpload = true
});
@@ -777,6 +884,60 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
}
/// <summary>
/// Reads the self-update-skipped marker file left by SelfUpdater (startup trigger path only),
/// emits an agent_self_update_skipped event, and deletes the marker. Best-effort — failures
/// are logged but never block startup.
/// </summary>
private void EmitSelfUpdateSkippedEventIfMarkerExists()
{
try
{
var markerPath = Environment.ExpandEnvironmentVariables(Constants.SelfUpdateSkippedMarkerFile);
if (!File.Exists(markerPath))
return;
var json = File.ReadAllText(markerPath);
var marker = Newtonsoft.Json.Linq.JObject.Parse(json);
var reason = (string)marker["reason"] ?? "unknown";
var currentVersion = (string)marker["currentVersion"] ?? "unknown";
var latestVersion = (string)marker["latestVersion"];
var skippedAtUtc = (string)marker["skippedAtUtc"] ?? "unknown";
var errorDetail = (string)marker["errorDetail"] ?? string.Empty;
_logger.Warning($"Startup self-update skipped: reason={reason}, current={currentVersion}, latest={latestVersion ?? "n/a"}");
var data = new Dictionary<string, object>
{
{ "reason", reason },
{ "currentVersion", currentVersion },
{ "skippedAtUtc", skippedAtUtc },
{ "errorDetail", errorDetail }
};
if (latestVersion != null)
data["latestVersion"] = latestVersion;
EmitEvent(new EnrollmentEvent
{
SessionId = _configuration.SessionId,
TenantId = _configuration.TenantId,
EventType = Constants.EventTypes.AgentSelfUpdateSkipped,
Severity = EventSeverity.Warning,
Source = "Agent",
Message = $"Startup self-update skipped (reason={reason})",
Data = data,
ImmediateUpload = true
});
TryDeleteMarker(markerPath);
}
catch (Exception ex)
{
_logger.Warning($"Could not process self-update-skipped marker: {ex.Message}");
}
}
private static void TryDeleteMarker(string path)
{
try { File.Delete(path); } catch { }
@@ -325,6 +325,24 @@ namespace AutopilotMonitor.Agent
using (var service = new MonitoringService(config, logger, agentVersion, previousExitType, previousCrashException, lastBootUtc))
{
// Wire the runtime self-update trigger: on hash mismatch, MonitoringService
// invokes SelfUpdater with forceUpdate=true and a longer download timeout
// (60s, vs. 10s at startup) because monitoring is already running and we
// know an update is required.
service.RuntimeSelfUpdateTriggerAsync = zipHash =>
{
if (!string.IsNullOrEmpty(zipHash))
SelfUpdater.BackendExpectedSha256 = zipHash;
return SelfUpdater.CheckAndApplyUpdateAsync(
currentVersion: agentVersion,
agentDir: agentDir,
consoleMode: consoleMode,
forceUpdate: true,
triggerReason: "runtime_hash_mismatch",
downloadTimeoutMsOverride: 60000);
};
service.Start();
if (consoleMode)
+173 -21
View File
@@ -91,7 +91,26 @@ namespace AutopilotMonitor.Agent
/// On success, restarts the process and never returns.
/// On any failure, returns normally so the current version continues.
/// </summary>
public static async Task CheckAndApplyUpdateAsync(string currentVersion, string agentDir, bool consoleMode)
/// <param name="forceUpdate">
/// When true, bypasses the IsNewerVersion check — used by the runtime-hash-mismatch trigger
/// to cover hotfixes that reuse the same version number.
/// </param>
/// <param name="triggerReason">
/// "startup" (default) or "runtime_hash_mismatch". Recorded in the marker file so the
/// agent_self_updated event can distinguish the two paths.
/// </param>
/// <param name="downloadTimeoutMsOverride">
/// Overrides the ZIP download timeout. Startup path leaves null (fast-fail, 10s) to preserve
/// Autopilot boot speed. Runtime trigger passes 60000 because monitoring is already running
/// and we know an update is required.
/// </param>
public static async Task CheckAndApplyUpdateAsync(
string currentVersion,
string agentDir,
bool consoleMode,
bool forceUpdate = false,
string triggerReason = "startup",
int? downloadTimeoutMsOverride = null)
{
Action<string> log = msg =>
{
@@ -99,26 +118,54 @@ namespace AutopilotMonitor.Agent
if (consoleMode) Console.WriteLine(msg);
};
var isStartupTrigger = string.Equals(triggerReason, "startup", StringComparison.Ordinal);
var downloadTimeoutMs = downloadTimeoutMsOverride ?? DownloadTimeoutMs;
var totalSw = Stopwatch.StartNew();
long versionCheckMs = 0, downloadMs = 0, verifyMs = 0, extractMs = 0, swapMs = 0;
long zipSizeBytes = 0;
string latestVersion = null;
try
{
// Step 1: Fetch version.json (1s timeout)
var (latestVersion, manifestSha256) = await GetLatestVersionAsync(log);
if (latestVersion == null)
return; // Could not determine latest version — continue with current
var versionSw = Stopwatch.StartNew();
string manifestSha256;
(latestVersion, manifestSha256) = await GetLatestVersionAsync(log);
versionSw.Stop();
versionCheckMs = versionSw.ElapsedMilliseconds;
// Step 2: Compare versions
if (!IsNewerVersion(currentVersion, latestVersion))
if (latestVersion == null)
{
log($"Self-update: current version {currentVersion} is up to date (latest: {latestVersion})");
return;
if (isStartupTrigger)
WriteSkipMarker("version_check_failed", currentVersion, null, "version.json fetch failed or timed out", log);
return; // Could not determine latest version — continue with current
}
log($"Self-update: newer version available — current={currentVersion}, latest={latestVersion}");
// Step 2: Compare versions (skipped when forceUpdate=true)
if (!forceUpdate && !IsNewerVersion(currentVersion, latestVersion))
{
log($"Self-update: current version {currentVersion} is up to date (latest: {latestVersion})");
return; // happy path — no skip marker
}
// Step 3: Download ZIP (10s timeout)
if (forceUpdate)
log($"Self-update: FORCE mode (trigger={triggerReason}) — current={currentVersion}, latest={latestVersion}, skipping version check");
else
log($"Self-update: newer version available — current={currentVersion}, latest={latestVersion}");
// Step 3: Download ZIP (timeout = downloadTimeoutMs)
var zipPath = Path.Combine(Path.GetTempPath(), "AutopilotMonitor-Agent-Update.zip");
if (!await DownloadZipAsync(zipPath, log))
var downloadTimerSw = Stopwatch.StartNew();
var downloadOk = await DownloadZipAsync(zipPath, downloadTimeoutMs, log);
downloadTimerSw.Stop();
downloadMs = downloadTimerSw.ElapsedMilliseconds;
if (!downloadOk)
{
if (isStartupTrigger)
WriteSkipMarker("download_failed", currentVersion, latestVersion, $"ZIP download failed within {downloadTimeoutMs}ms", log);
return;
}
try { zipSizeBytes = new FileInfo(zipPath).Length; } catch { /* best-effort */ }
// Step 3b: Verify SHA-256 integrity (backend hash has priority over version.json hash)
string expectedSha256;
@@ -137,16 +184,31 @@ namespace AutopilotMonitor.Agent
expectedSha256 = null;
}
if (!VerifyZipIntegrity(zipPath, expectedSha256, log))
var verifySw = Stopwatch.StartNew();
var integrityOk = VerifyZipIntegrity(zipPath, expectedSha256, log);
verifySw.Stop();
verifyMs = verifySw.ElapsedMilliseconds;
if (!integrityOk)
{
CleanupStaging(null, zipPath);
if (isStartupTrigger)
WriteSkipMarker("integrity_mismatch", currentVersion, latestVersion, "SHA-256 mismatch on downloaded ZIP", log);
return;
}
// Step 4: Extract to staging directory
var stagingDir = Environment.ExpandEnvironmentVariables(Constants.AgentUpdateStagingDir);
if (!ExtractToStaging(zipPath, stagingDir, log))
var extractSw = Stopwatch.StartNew();
var extractOk = ExtractToStaging(zipPath, stagingDir, log);
extractSw.Stop();
extractMs = extractSw.ElapsedMilliseconds;
if (!extractOk)
{
if (isStartupTrigger)
WriteSkipMarker("extract_failed", currentVersion, latestVersion, "ZIP extraction failed", log);
return;
}
// Step 5: Validate staging
var stagedExe = Path.Combine(stagingDir, "AutopilotMonitor.Agent.exe");
@@ -154,13 +216,21 @@ namespace AutopilotMonitor.Agent
{
log("Self-update: staging validation failed — AutopilotMonitor.Agent.exe not found in ZIP");
CleanupStaging(stagingDir, zipPath);
if (isStartupTrigger)
WriteSkipMarker("extract_failed", currentVersion, latestVersion, "AutopilotMonitor.Agent.exe not found in ZIP", log);
return;
}
// Step 6: Swap files (rename locked → .old, copy new)
if (!SwapFiles(agentDir, stagingDir, log))
var swapTimerSw = Stopwatch.StartNew();
var swapOk = SwapFiles(agentDir, stagingDir, log);
swapTimerSw.Stop();
swapMs = swapTimerSw.ElapsedMilliseconds;
if (!swapOk)
{
CleanupStaging(stagingDir, zipPath);
if (isStartupTrigger)
WriteSkipMarker("swap_failed", currentVersion, latestVersion, "file swap failed", log);
return;
}
@@ -168,7 +238,19 @@ namespace AutopilotMonitor.Agent
CleanupStaging(stagingDir, zipPath);
// Step 8: Write marker file so the new agent can emit an event on next startup
WriteSelfUpdateMarker(currentVersion, latestVersion, log);
totalSw.Stop();
WriteSelfUpdateMarker(
previousVersion: currentVersion,
newVersion: latestVersion,
triggerReason: triggerReason,
versionCheckMs: versionCheckMs,
downloadMs: downloadMs,
zipSizeBytes: zipSizeBytes,
verifyMs: verifyMs,
extractMs: extractMs,
swapMs: swapMs,
totalUpdateMs: totalSw.ElapsedMilliseconds,
log: log);
// Step 9: Restart via PowerShell Wait-Process
log($"Self-update: files swapped successfully, restarting agent (v{latestVersion})...");
@@ -179,6 +261,10 @@ namespace AutopilotMonitor.Agent
catch (Exception ex)
{
log($"Self-update: unexpected error — {ex.Message}. Continuing with current version.");
if (isStartupTrigger)
{
try { WriteSkipMarker("unexpected_error", currentVersion, latestVersion, ex.Message, log); } catch { }
}
}
}
@@ -300,7 +386,7 @@ namespace AutopilotMonitor.Agent
/// <summary>
/// Downloads the agent ZIP to a temp path. Returns false on failure.
/// </summary>
private static async Task<bool> DownloadZipAsync(string zipPath, Action<string> log)
private static async Task<bool> DownloadZipAsync(string zipPath, int timeoutMs, Action<string> log)
{
try
{
@@ -311,7 +397,7 @@ namespace AutopilotMonitor.Agent
File.Delete(zipPath);
using (var handler = new HttpClientHandler())
using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(DownloadTimeoutMs) })
using (var client = new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(timeoutMs) })
{
using (var response = await client.GetAsync(zipUrl))
{
@@ -328,7 +414,7 @@ namespace AutopilotMonitor.Agent
}
catch (TaskCanceledException)
{
log("Self-update: ZIP download timed out (10s) — aborting update");
log($"Self-update: ZIP download timed out ({timeoutMs}ms) — aborting update");
try { File.Delete(zipPath); } catch { }
return false;
}
@@ -467,8 +553,21 @@ namespace AutopilotMonitor.Agent
/// <summary>
/// Writes a small JSON marker so the next agent startup can emit an agent_self_updated event.
/// Best-effort — failure here must not block the update.
/// Includes phase timings, trigger reason, and exit timestamp so the downstream event
/// can carry aggregable telemetry (downtime, total update duration, etc.).
/// </summary>
private static void WriteSelfUpdateMarker(string previousVersion, string newVersion, Action<string> log)
private static void WriteSelfUpdateMarker(
string previousVersion,
string newVersion,
string triggerReason,
long versionCheckMs,
long downloadMs,
long zipSizeBytes,
long verifyMs,
long extractMs,
long swapMs,
long totalUpdateMs,
Action<string> log)
{
try
{
@@ -477,8 +576,26 @@ namespace AutopilotMonitor.Agent
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var json = $"{{\"previousVersion\":\"{previousVersion}\",\"newVersion\":\"{newVersion}\",\"updatedAtUtc\":\"{DateTime.UtcNow:O}\"}}";
File.WriteAllText(markerPath, json);
var now = DateTime.UtcNow;
var payload = new JObject
{
["previousVersion"] = previousVersion ?? "unknown",
["newVersion"] = newVersion ?? "unknown",
["triggerReason"] = triggerReason ?? "startup",
["updatedAtUtc"] = now.ToString("O"),
// exitAtUtc is written here — actual Environment.Exit happens a few hundred ms later
// via PowerShell Wait-Process, which is close enough for downtime telemetry.
["exitAtUtc"] = now.ToString("O"),
["versionCheckMs"] = versionCheckMs,
["downloadMs"] = downloadMs,
["zipSizeBytes"] = zipSizeBytes,
["verifyMs"] = verifyMs,
["extractMs"] = extractMs,
["swapMs"] = swapMs,
["totalUpdateMs"] = totalUpdateMs
};
File.WriteAllText(markerPath, payload.ToString(Newtonsoft.Json.Formatting.None));
log("Self-update: marker file written for next-startup event");
}
catch (Exception ex)
@@ -487,6 +604,41 @@ namespace AutopilotMonitor.Agent
}
}
/// <summary>
/// Writes a small JSON marker so the next agent startup can emit an agent_self_update_skipped
/// event. Only called on the startup trigger path (runtime-triggered failures are logged
/// normally because the full logger is already up). Best-effort — never throws.
/// </summary>
private static void WriteSkipMarker(string reason, string currentVersion, string latestVersion, string errorDetail, Action<string> log)
{
try
{
var markerPath = Environment.ExpandEnvironmentVariables(Constants.SelfUpdateSkippedMarkerFile);
var dir = Path.GetDirectoryName(markerPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var detail = errorDetail ?? string.Empty;
if (detail.Length > 200) detail = detail.Substring(0, 200);
var payload = new JObject
{
["reason"] = reason ?? "unknown",
["currentVersion"] = currentVersion ?? "unknown",
["latestVersion"] = latestVersion, // may be null if version check failed
["skippedAtUtc"] = DateTime.UtcNow.ToString("O"),
["errorDetail"] = detail
};
File.WriteAllText(markerPath, payload.ToString(Newtonsoft.Json.Formatting.None));
log($"Self-update: skip marker written (reason={reason})");
}
catch (Exception ex)
{
log($"Self-update: could not write skip marker — {ex.Message} (non-critical)");
}
}
/// <summary>
/// Cleans up staging directory and temp ZIP file.
/// </summary>
@@ -1 +1 @@
887
889
@@ -35,6 +35,14 @@ namespace AutopilotMonitor.Shared
/// </summary>
public const string SelfUpdateMarkerFile = @"%ProgramData%\AutopilotMonitor\State\self-update-info.json";
/// <summary>
/// Marker file written by SelfUpdater when the startup update path is skipped (network timeout,
/// download failure, integrity mismatch, etc.); read by MonitoringService on next startup to emit
/// an agent_self_update_skipped event. Only written on the startup trigger path — runtime-triggered
/// failures are logged normally because the full logger is already up.
/// </summary>
public const string SelfUpdateSkippedMarkerFile = @"%ProgramData%\AutopilotMonitor\State\self-update-skipped.json";
/// <summary>
/// Default path for the IME pattern match log file (debugging/diagnostics)
/// </summary>
@@ -168,6 +176,7 @@ namespace AutopilotMonitor.Shared
public const string SoftwareInventoryAnalysis = "software_inventory_analysis";
public const string VulnerabilityReport = "vulnerability_report";
public const string AgentSelfUpdated = "agent_self_updated";
public const string AgentSelfUpdateSkipped = "agent_self_update_skipped";
}
// -----------------------------------------------------------------------