Adds an opt-in second destination for agent diagnostics uploads:
- CustomerSas (default, unchanged): tenant's own SAS-backed container
- Hosted (new, opt-in via explicit admin click): backend's storage account
under {tenantId}/AgentDiagnostics-...zip, with per-upload 15-min
blob-scoped Write+Create SAS minted via the Function App's MI
Closes the long-standing gap where Session.DiagnosticsBlobName was captured
into the deletion-manifest but the blob was never actually cleaned up by
the cascade. Hosted blobs are always deleted (we own the storage); customer
blobs are deleted only when the SAS includes the 'd' permission, otherwise
logged + skipped.
Existing tenants change nothing: the new TenantConfiguration.DiagnosticsUploadDestination
field defaults to CustomerSas, no silent flip. Hosted requires an explicit
admin click + Save with a clearly marked "data leaves your tenant" disclosure.
Tests: Backend 1844, V2 1076, V1 538, vitest 380 (+78 new across all suites).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V1 carried the same two bugs as V2: the per-user profile enumeration filter
only matched S-1-5-21-* SIDs, silently skipping the AAD/MSA cloud SIDs
(S-1-12-1-*) that Win10 1903+ uses for AAD-joined users — so on Win11 AAD
boxes the per-user Uninstall hive of the actually-logged-in user was never
read. Additionally, CollectFromKey logged "{source}\{subKeyPath}" which
mashed the telemetry tag (HKU_<RID>) with the relative path, producing a
non-path string like "HKU_1000\S-1-5-21-...\SOFTWARE\..." in debug logs.
Same patch as V2 commit 6c4f7250: extend SID filter to include S-1-12-1-*,
log rootKey.Name (HKEY_USERS / HKEY_LOCAL_MACHINE / ...) as the path prefix
and report the telemetry tag separately as "(source=...)".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds EntraDeviceCertHelper that reads the AAD-Join device certificate's
TenantId extension (OID 1.2.840.113556.1.5.284.5) from LocalMachine\My.
Wired as the 5th and final synchronous probe in TenantIdResolver, after
the four registry probes:
1. enrollments_registry (Type=6, Intune MDM)
2. enrollments_registry_fallback (any Type, AADTenantID present)
3. cloud_domain_join_tenant_info
4. cloud_domain_join_join_info
5. ms_organization_access_cert <- new
Targets the Autopilot pre-provisioning / hybrid window where the cert
can land before the CloudDomainJoin registry writes complete. Lets the
agent boot without falling through to the registry-watcher awaiter and
burning the 600s wait window.
Helper hardening over the original draft:
- Filters to currently-valid certs (NotBefore <= now <= NotAfter) and
picks latest NotAfter when multiple match, defending against an
expired sibling from a prior re-enrollment.
- If the highest-priority cert lacks the OID extension, walks through
the remaining candidates instead of bailing on the first miss.
- Logger emits Info on hit (thumbprint + NotAfter) and Warning on miss
with the specific reason (no cert / extension missing / parse failed).
Mirrored across V1 (Agent.Core) and V2 (Agent.V2.Core).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-pass scan of HKLM\SOFTWARE\Microsoft\Enrollments: pass 1 keeps the
existing Type=6 (Intune MDM) preference, pass 2 falls back to any
non-Type-6 sub-key carrying a non-empty AADTenantID. Targets the
Autopilot pre-provisioning / hybrid window where Type=6 sits at
EnrollmentState=1 with AADTenantID still missing while a Type=26
AAD-Join entry already carries the TenantID. Observed at customer
where the agent burned ~30min waiting for CloudDomainJoin\TenantInfo
instead of resolving immediately from Type=26.
Captures the enrollment UPN and classifies it. Known Autopilot
pre-provisioning placeholders (fooUser@... Intune-MDM leg,
autopilot@... Entra-Join leg, both generated by the Windows
MakeFakeUserEmail code path) are logged in full with annotation;
anything else is treated as a real user UPN and redacted to
<redacted_real_user_upn> since UPNs are PII.
Mirrored across V1 (Agent.Core) and V2 (Agent.V2.Core).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EventID 1010 (Autopilot.dll WIL hardwareinfo.cpp HRESULT 0x80070002 /
file-not-found probe) is a known noise event with no enrollment impact.
Downgraded to Debug across V1+V2 trackers, shared default list, admin
config fallback, and web UI defaults. ConfigVersion bumped 26 -> 27.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registry reads now go through RegistryKey.OpenBaseKey(.., Registry64)
explicitly. SDK-style net48 EXEs can resolve AnyCPU to 32-bit at runtime
depending on MSBuild defaults; in that case
HKLM\SOFTWARE\Microsoft\Enrollments redirects to the WOW6432Node mirror,
a different (and usually stale/empty) hive that never carries the active
MDM enrollment. Forcing Registry64 makes the resolver bitness-independent
regardless of how AnyCPU resolves.
Diagnostic-on-miss is now compact:
* Sub-keys without an EnrollmentType value (Context, Ownership, Status,
ValidNodePaths) are recognized as structural and skipped silently.
* The detail list contains only Type=6 (Intune MDM) entries with their
EnrollmentState and AADTenantID presence — that's the only enrollment
flavor that ever carries the TenantId.
* Non-Type-6 enrollments collapse into a single counter so a heavily
enrolled device (e.g. dev box with 30+ historical enrollments) no
longer floods the agent log with one Warning per sub-key.
Mirrored across the V1 + V2 resolver shipped in 99ec2517 / fc950757.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the V2 resolver shipped in fc950757 to V1 so the legacy rail
gets the same OOBE-race protection. Probe order: Enrollments registry
→ CloudDomainJoin\TenantInfo → CloudDomainJoin\JoinInfo, source-tagged
log line on hit, full diagnostic block on miss (which roots exist,
which enrollment GUIDs were seen with their EnrollmentType /
EnrollmentState, which JoinInfo thumbprints).
Removes the inline GetTenantIdFromRegistry helper from V1 Program.cs
and routes all three callsites (LoadConfiguration, await-enrollment
re-resolve, pre-flight bail) through the new resolver. The bail site
re-probes with a logger when the await branch was skipped, so the
diagnostic block lands in the agent log on a real miss instead of
just the generic "registry empty" error.
Updates a stale doc comment in V2 Program.GatherRulesMode.cs that
referenced the removed V1 method name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Customer gardnermedia (2026-04-30) hit a fatal V1 startup crash:
"InvalidOperationException: Invalid agent configuration" thrown from
MonitoringService..ctor. Root cause: LoadConfiguration returned a config
with TenantId=null because HKLM\SOFTWARE\Microsoft\Enrollments did not
yet carry an EnrollmentType=6\AADTenantID entry and no bootstrap-config
.json was present. Without a guard, control reached MonitoringService
construction and IsValid() threw an unhandled exception → crash log →
"exception_crash" classification on the next start.
V2 already bails cleanly in this case (AgentBootstrap.Run line 123-127).
This commit gives V1 the same safety net plus a more diagnostic message
so any future occurrence is immediately analyzable from the crash log.
Changes:
* Program.cs: pre-flight TenantId guard right after the
--await-enrollment block. Logs an Error and returns cleanly so the
ProcessExit handler writes clean-exit.marker — no spurious crash log,
Scheduled Task simply retries on the next trigger.
* MonitoringService..ctor: replace the opaque "Invalid agent
configuration" throw with one that names the missing fields
(ApiBaseUrl / SessionId / TenantId).
* MonitoringServiceConfigValidationTests: 5 new xUnit cases covering
each missing-field path plus the multi-missing case.
* V1 buildcounter bump.
V2 unchanged — its existing AgentBootstrap guard already covers this
class of crash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a coarse virtual-machine flag to every hardware_spec event so the rule
engine's preconditions feature can filter device-specific rules. Wires the
new flag into ANALYZE-SEC-001 (Secure Boot UEFI CA 2023): the rule no longer
fires on Hyper-V / VMware / VirtualBox / Xen / QEMU / Parallels / KVM
guests, where the firmware DB is host-controlled and the inside-the-VM
UEFICA2023Status registry value is not actionable.
Agent (V1 + V2)
- Centralized VM detection in DeviceInfoProvider.IsVirtualMachine(mfr, model)
alongside the existing GetManufacturer / GetModel helpers (which already
handle Lenovo's Win32_ComputerSystemProduct.Version quirk). Reused, not
duplicated — same source of truth as SessionRegistration / DiagnosticsPackage
/ GatherRulesMode.
- New CollectSystemInfo step in DeviceInfoCollector.HardwareSpec emits
systemManufacturer, systemModel, isVirtualMachine onto the hardware_spec
event. No new WMI query — calls into DeviceInfoProvider.
- Conservative bias: Microsoft Corporation manufacturer is shared between
Hyper-V VMs and physical Surface devices, disambiguated via model
("Virtual Machine" → VM, "Surface Laptop 5" → physical).
Rule
- ANALYZE-SEC-001 gains a precondition (equals isVirtualMachine=false) and
bumps to v1.1.0 so EnsureBuiltInRulesSeededAsync re-saves it for tenants
on next backend cold start.
- Old agents (no isVirtualMachine field on hardware_spec) → fail-closed →
rule is silently skipped on those devices until the agent updates. Safe
default for a security rule.
Tests
- DeviceInfoProviderVmDetectionTests (V1 + V2 mirror, 18 cases each):
Hyper-V / Cloud PC / VMware / VirtualBox / Xen / QEMU / Parallels / KVM
positive + Surface Laptop/Pro/Book + Lenovo / Dell / HP negative + null/empty
edge cases. Pinned at the central DeviceInfoProvider so all consumers benefit.
- 781/781 backend, 525/525 V1 agent, 800/800 V2 agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New netstandard2.0 project src/Shared/AutopilotMonitor.DecisionCore/ with
14 scaffolding types across Signals/, State/, Classifiers/, Engine/.
Agent.Core and Functions reference the library without call-sites; the
full solution builds 0 errors, 0 warnings. DecisionEngine.Reduce is an
intentional NotImplementedException stub — M3 fills in the partial-class
reducer.
Invariants wired into the scaffolding:
- Immutable DTOs (sealed + get-only + With-methods) per L.3
- Pflicht-Evidence validation in Evidence ctor per L.5
- KindSchemaVersion enforcement per L.6
- AssemblyVersion 2.0.0.0 as ReducerVersion source per §2.10
Architecture principles L.1–L.18 extracted to
.claude/architecture-principles.md as persistent Claude-session context.
Plan: plans/REFACTOR_AGENT_V2.md §4 M1. Zero runtime risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WhiteGlove (Pre-Provisioning) sessions were incorrectly classified as
self-deploying because OnDeviceSetupProvisioningComplete and the esp_exiting
handler both race ahead of the signal-correlated WhiteGlove detector (30s
timer + 10min stability window). Two WhiteGlove signals existed but were
never consumed by completion logic:
1. EventID 509 (whiteglove_started) — already detected by
ModernDeploymentTracker but not exposed to EnrollmentTracker
2. SaveWhiteGloveSuccessResult=succeeded in DeviceSetup registry JSON —
present in esp_provisioning_raw but invisible to ParseSubcategories
(no *Subcategory suffix)
Adds WhiteGlove guards at both device-only completion paths:
- OnDeviceSetupProvisioningComplete (registry polling path)
- OnFinalizingSetupPhaseTriggered esp_exiting + SkipUserStatusPage path
Exposes IsWhiteGloveStartDetected and HasSaveWhiteGloveSuccessResult
through EspAndHelloTracker. Mirrors guards in CompletionStateMachine.
Backend safety net: classifies whiteglove_started events and sets
IsPreProvisioned=true early as fallback for older agents.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Part 5 of 5 in Monitoring/ bounded-context restructure. StallProbeCollector
lands in Enrollment/SystemSignals/. Empty Core/Tracking/Collectors/Network/
Analyzers/Flows/Replay/ directories removed. No behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Part 3 of 5 in Monitoring/ bounded-context restructure. Split by purpose
(periodic metrics, hardcoded analyzers, backend-driven gather rules,
device info). Pure folder move + namespace update; no behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Part 2 of 5 in Monitoring/ bounded-context restructure. Network/* plus
Core/EventSpool + Core/EventUploadOrchestrator moved into Monitoring/Transport/.
Pure folder move + namespace update; no behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Part 1 of 5 in Monitoring/ bounded-context restructure. Moves Monitoring/Core/*
(minus EventSpool/EventUploadOrchestrator/DeviceInfoProvider/EnrollmentCompletionHandler
which belong to later PRs) into Monitoring/Runtime/. Interop/ stays on
Monitoring root. Pure folder move + namespace update; no behavior change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves WHfB policy detection, UDR event handling (300/301/358/360/362/376),
HelloForBusiness watcher (3024/6045), Hello wait/completion timers and
HelloOutcome state machine into a dedicated HelloTracker class (820 LOC).
Cross-boundary with ESP (Shell-Core 62404/62407) is narrowed to two
explicit notify methods: NotifyHelloWizardStarted + NotifyEspExited.
Public API of EspAndHelloTracker is unchanged — IsPolicyConfigured,
IsHelloCompleted, HelloOutcome, StartHelloWaitTimer, ForceMarkHelloCompleted,
ResetForEspResumption and the HelloCompleted event now delegate to the
composed HelloTracker. CollectorCoordinator + EnrollmentTracker integration
requires no changes.
Adds 29 xUnit tests covering: UDR terminal events, 6045 skip HRESULT,
wait/completion timer paths (policy-enabled extended wait, policy-disabled
immediate completion, wizard-started vs wizard-not-started outcomes),
cross-boundary notifications, ForceMarkHelloCompleted semantics and
ResetForEspResumption state reset.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Solves the timing gap where WhiteGlove Event 509 fires during OOBE
before the agent starts (pre MDM-enroll). On startup the agent now
scans the ManagementService event log for targeted events within a
configurable lookback window (default 30 min) and emits them with
a backfilled=true marker for trace analysis.
Key changes:
- Extract ProcessModernDeploymentRecord from live handler for reuse
by both EventLogWatcher callback and EventLogReader backfill
- Add BackfillTargetedModernDeploymentEvents() with persisted state
(whiteglove-backfill.json) so subsequent agent restarts skip the
scan and avoid duplicate emissions
- Add config fields ModernDeploymentBackfillEnabled and
ModernDeploymentBackfillLookbackMinutes to CollectorConfiguration
- Wire up stateDirectory and config in MonitoringService
- All emitted events now carry Data["backfilled"] = true/false
https://claude.ai/code/session_01ToApHGezTGcSkA6NVbVZkG