mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
Cloud-PC-aware autopilot_profile_missing: Info instead of Warning on W365
On Windows 365 Cloud PCs a missing Autopilot profile (ProfileAvailable=0) is the expected state - Cloud PCs are provisioned by the Windows 365 service, not by Autopilot - so the warning shown for physical devices is misleading there. - Agent: emit autopilot_profile_missing as Info with a Cloud-PC-specific message when CloudPcDetector matches; payload now carries isCloudPc. Evidence collection (ZTD verdict, registry, endpoint probe) unchanged. - Web: DeviceDetailsCard renders a neutral info note instead of the amber warning for Cloud PC sessions (session.isCloudPc, sticky, covers existing sessions; or the new event marker). - MCP: resource-catalog documents the new isCloudPc data field. - Docs: Cloud PC exception noted in agent/autopilot-ztd-diagnostics.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,14 @@ three of them to turn the `autopilot_profile_missing` warning from a guess into
|
||||
evidence-backed verdict, and the backend rule engine uses the error-code map for
|
||||
known-issue enrichment.
|
||||
|
||||
Windows 365 Cloud PC exception: on Cloud PCs (`CloudPcDetector.DetectIsCloudPc()`)
|
||||
`ProfileAvailable=0` is the expected state — Cloud PCs are provisioned by the Windows 365
|
||||
service, not by Autopilot. The event is still emitted with the full evidence payload plus
|
||||
an `isCloudPc` marker, but as severity Info with a "this is expected" message instead of
|
||||
a Warning (`DeviceInfoCollector.ResolveAutopilotProfileMissingPresentation`). The portal's
|
||||
Autopilot Profile card mirrors this: a neutral note for Cloud PC sessions
|
||||
(`session.isCloudPc` or the event marker), the amber warning otherwise.
|
||||
|
||||
Consumers in this repo:
|
||||
|
||||
* `ZtdEvidence.cs` (agent) — one-shot event-log query + `Diagnostics\Autopilot` registry
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Log
|
||||
|
||||
## 2026-08-12
|
||||
|
||||
* **Update**: `agent/autopilot-ztd-diagnostics.md` — Windows 365 Cloud PC exception documented: on Cloud PCs `ProfileAvailable=0` is expected (provisioned by the Windows 365 service, not Autopilot), so `autopilot_profile_missing` is emitted as Info with a Cloud-PC-specific message and an `isCloudPc` payload marker (`ResolveAutopilotProfileMissingPresentation`); the portal's Autopilot Profile card renders a neutral note instead of the amber warning for Cloud PC sessions (`session.isCloudPc` or the event marker).
|
||||
|
||||
## 2026-08-11 (later)
|
||||
|
||||
* **Creation**: Added `backend/session-annotations.md` — session annotations: the new `SessionAnnotations` table (PK=tenantId, RK=`{sessionId}_{lane}`, lanes operator/tenantadmin/globaladmin) storing structured verdicts (`root_cause_confirmed | analysis_wrong | different_problem | inconclusive`) + notes with server-stamped authorship and a fired-rule-id snapshot for join-free rule-quality evaluation. Per-lane write matrix re-gated in-function with own-tenant binding (a GA writes only the platform-internal `globaladmin` lane cross-tenant; tenant callers never see that lane). Surfaces: portal `SessionAnnotationsCard` (session detail, after Analysis), MCP `list_session_annotations` (ga) + `annotate_session` (strictGa, always the GA lane) + `annotations` key in `get_session_summary`. Lifecycle: per-session cascade steps 17–19, offboarding wipe, critical-backup member.
|
||||
|
||||
+25
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Telemetry.DeviceInfo;
|
||||
@@ -109,4 +110,28 @@ public class DeviceInfoCollectorAutopilotProfileMissingTests
|
||||
Assert.False(DeviceInfoCollector.TryExtractZeroTouchTenantDomain(null, out var domain));
|
||||
Assert.Null(domain);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveAutopilotProfileMissingPresentation_PhysicalDevice_WarnsAboutAssignment()
|
||||
{
|
||||
var (message, severity) = DeviceInfoCollector.ResolveAutopilotProfileMissingPresentation(isCloudPc: false);
|
||||
|
||||
Assert.Equal(EventSeverity.Warning, severity);
|
||||
Assert.Equal(
|
||||
"No Autopilot profile was available during OOBE (ProfileAvailable=0) — most likely no deployment profile was assigned to the device, or the assignment had not finished propagating when OOBE ran. OOBE proceeded as a standard Entra ID join instead of an Autopilot deployment.",
|
||||
message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveAutopilotProfileMissingPresentation_CloudPc_InformationalAndExpected()
|
||||
{
|
||||
// On a Windows 365 Cloud PC a missing Autopilot profile is the normal state —
|
||||
// the event must not read as an alarm there.
|
||||
var (message, severity) = DeviceInfoCollector.ResolveAutopilotProfileMissingPresentation(isCloudPc: true);
|
||||
|
||||
Assert.Equal(EventSeverity.Info, severity);
|
||||
Assert.Contains("Windows 365 Cloud PC", message);
|
||||
Assert.Contains("expected", message);
|
||||
Assert.DoesNotContain("no deployment profile was assigned", message);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-4
@@ -305,10 +305,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
|
||||
// (see docs/agent/autopilot-ztd-diagnostics.md for the evidence model).
|
||||
if (IsAutopilotProfileMissing(data))
|
||||
{
|
||||
// On Windows 365 Cloud PCs a missing profile is the expected state — Cloud PCs
|
||||
// are provisioned by the Windows 365 service, not by Autopilot — so the event
|
||||
// is demoted to Info there instead of raising a misleading warning.
|
||||
var isCloudPc = CloudPcDetector.DetectIsCloudPc();
|
||||
|
||||
var missingData = new Dictionary<string, object>
|
||||
{
|
||||
{ "profileAvailable", "0" },
|
||||
{ "likelyCauses", "profile_not_assigned,profile_assignment_not_propagated" }
|
||||
{ "likelyCauses", "profile_not_assigned,profile_assignment_not_propagated" },
|
||||
{ "isCloudPc", isCloudPc }
|
||||
};
|
||||
if (TryExtractZeroTouchTenantDomain(data, out var ztdTenantDomain))
|
||||
{
|
||||
@@ -347,12 +353,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
|
||||
missingData["ztdEndpointLatencyMs"] = probe.LatencyMs;
|
||||
missingData["ztdEndpointDetail"] = probe.Detail;
|
||||
|
||||
var presentation = ResolveAutopilotProfileMissingPresentation(isCloudPc);
|
||||
EmitDeviceInfoEvent(Constants.EventTypes.AutopilotProfileMissing,
|
||||
"No Autopilot profile was available during OOBE (ProfileAvailable=0) — most likely no deployment profile was assigned to the device, or the assignment had not finished propagating when OOBE ran. OOBE proceeded as a standard Entra ID join instead of an Autopilot deployment.",
|
||||
presentation.Message,
|
||||
missingData,
|
||||
EventSeverity.Warning);
|
||||
presentation.Severity);
|
||||
|
||||
_logger.Warning($"EnrollmentTracker: no Autopilot profile available during OOBE (ProfileAvailable=0) — ztdVerdict={missingData["ztdVerdict"]}, endpointReachable={probe.Reachable}");
|
||||
var logLine = $"EnrollmentTracker: no Autopilot profile available during OOBE (ProfileAvailable=0) — isCloudPc={isCloudPc}, ztdVerdict={missingData["ztdVerdict"]}, endpointReachable={probe.Reachable}";
|
||||
if (isCloudPc)
|
||||
_logger.Info(logLine);
|
||||
else
|
||||
_logger.Warning(logLine);
|
||||
}
|
||||
|
||||
// Emit dedicated enrollment_type_detected event for easy filtering. Routed through
|
||||
@@ -396,6 +407,26 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
|
||||
&& profileAvailable?.ToString() == "0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message and severity for the autopilot_profile_missing event. On Windows 365
|
||||
/// Cloud PCs the missing profile is expected (Cloud PCs are provisioned by the
|
||||
/// Windows 365 service, not by Autopilot), so the event is informational there;
|
||||
/// on physical devices it stays a warning about a missing profile ASSIGNMENT.
|
||||
/// </summary>
|
||||
internal static (string Message, EventSeverity Severity) ResolveAutopilotProfileMissingPresentation(bool isCloudPc)
|
||||
{
|
||||
if (isCloudPc)
|
||||
{
|
||||
return (
|
||||
"No Autopilot profile was available during OOBE (ProfileAvailable=0). This device is a Windows 365 Cloud PC — Cloud PCs are provisioned by the Windows 365 service and do not use Autopilot deployment profiles, so this is expected. OOBE ran as a standard Entra ID join, which is the normal path for Cloud PCs.",
|
||||
EventSeverity.Info);
|
||||
}
|
||||
|
||||
return (
|
||||
"No Autopilot profile was available during OOBE (ProfileAvailable=0) — most likely no deployment profile was assigned to the device, or the assignment had not finished propagating when OOBE ran. OOBE proceeded as a standard Entra ID join instead of an Autopilot deployment.",
|
||||
EventSeverity.Warning);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts ZeroTouchConfig.CloudAssignedTenantDomain from the cached profile data.
|
||||
/// The value lives in the CloudAssignedAadServerData JSON — either as a top-level
|
||||
|
||||
@@ -277,6 +277,7 @@ export const DEVICE_PROPERTIES_CATALOG = {
|
||||
'autopilot_profile_missing.profileAvailable': { type: 'string', description: 'Always "0" — Windows recorded that no Autopilot profile was available during OOBE' },
|
||||
'autopilot_profile_missing.zeroTouchTenantDomain': { type: 'string', description: 'CloudAssignedTenantDomain from the cached ZeroTouchConfig (empty when no profile was downloaded)' },
|
||||
'autopilot_profile_missing.likelyCauses': { type: 'string', description: 'Comma-separated candidate causes (profile_not_assigned, profile_assignment_not_propagated) — the device itself passed backend device-registration validation, so the gap is the profile assignment, not the registration' },
|
||||
'autopilot_profile_missing.isCloudPc': { type: 'boolean', description: 'true when the device is a Windows 365 Cloud PC — there the missing profile is expected (Cloud PCs are provisioned by the Windows 365 service, not Autopilot) and the event is emitted as Info instead of Warning' },
|
||||
},
|
||||
hardware_spec: {
|
||||
'hardware_spec.cpuName': { type: 'string', description: 'CPU model name (e.g. "Intel Core i7-1265U")' },
|
||||
|
||||
@@ -170,9 +170,14 @@ export default function DeviceDetailsCard({ events, latestAgentVersion, session
|
||||
const autopilotProfile = normalizeAutopilotProfile(getEventData<AutopilotProfileData>("autopilot_profile"));
|
||||
// Explicit agent signal (new sessions) OR ProfileAvailable=0 in the cached profile
|
||||
// (also covers sessions recorded before autopilot_profile_missing existed).
|
||||
const autopilotProfileMissingEvent = getEventData("autopilot_profile_missing");
|
||||
const autopilotProfileMissing =
|
||||
getEventData("autopilot_profile_missing") !== null ||
|
||||
autopilotProfileMissingEvent !== null ||
|
||||
(autopilotProfile !== null && `${autopilotProfile.ProfileAvailable}` === "0");
|
||||
// Windows 365 Cloud PCs have no Autopilot profile by design — session flag (covers
|
||||
// historic sessions, sticky-true) OR the event's own isCloudPc marker (newer agents).
|
||||
const profileMissingIsCloudPc =
|
||||
session?.isCloudPc === true || `${autopilotProfileMissingEvent?.isCloudPc}` === "true";
|
||||
const aadJoinStatus = getEventData("aad_join_status");
|
||||
const imeVersion = getEventData<ImeVersionData>("ime_agent_version");
|
||||
const realmJoinInfo = getEventData<RealmJoinInfoData>("realmjoin_detected");
|
||||
@@ -456,7 +461,22 @@ export default function DeviceDetailsCard({ events, latestAgentVersion, session
|
||||
{/* Autopilot Profile */}
|
||||
{autopilotProfile && (
|
||||
<DetailSection title="Autopilot Profile">
|
||||
{autopilotProfileMissing && (
|
||||
{autopilotProfileMissing && profileMissingIsCloudPc && (
|
||||
<div className="my-1 rounded-md border border-blue-200 bg-blue-50 px-2.5 py-2 text-xs text-blue-800">
|
||||
<div className="flex items-start gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>
|
||||
<span className="font-semibold">No Autopilot profile — expected for this Cloud PC.</span>{" "}
|
||||
Windows 365 Cloud PCs are provisioned by the Windows 365 service and do
|
||||
not use Autopilot deployment profiles. OOBE ran as a standard Entra ID
|
||||
join, which is the normal path for Cloud PCs.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{autopilotProfileMissing && !profileMissingIsCloudPc && (
|
||||
<div className="my-1 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-2 text-xs text-amber-800">
|
||||
<div className="flex items-start gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
||||
Reference in New Issue
Block a user