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:
Oliver Kieselbach
2026-08-12 16:13:34 +02:00
parent 84a0424789
commit 66d25f57db
6 changed files with 95 additions and 6 deletions
+8
View File
@@ -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
+4
View File
@@ -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.
@@ -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);
}
}
@@ -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">