Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 KiB
Log
2026-08-30 (12)
- Update:
web/portal-navigation-prefetch.md— hosting citation now points atdeploy-web.yml: the portal is built in CI (setup-node 22,npm ci,next build) and uploaded with the pinned SWA CLI; the Azure/static-web-apps-deploy action, its Oryx in-container build and the per-PR preview environments are gone. All non-GitHub-owned actions are SHA-pinned; the release and rules auto-commit steps use first-partygh/git.
2026-08-30 (11)
- New:
agent/ime-process-watcher.md—ImeProcessWatcherno longer attaches to the firstIntuneManagementExtensionname match:ImeProcessIdentityrequires session 0 + image under the IME install root (QueryFullProcessImageName, cross-bitness), prefers the oldest trusted candidate, logs untrusted matches once per PID, and re-arms discovery after every reported exit (previously one-shot — a fake in a user session could forgeime_process_exitedand mute the real signal for the session; even an IME service restart was never watched again). Registered inindex.md.
2026-08-30 (10)
- Update:
agent/ime-log-tracker-matching-budget.md— the per-line budget sums onlyRegex.Matchtime; the previous wall-clock window also counted match handlers and scheduler stalls and broke twice on genuine lines of a stalled Hyper-V guest (session 946ccbd6).backend/ime-pattern-health.md—ime_pattern_hitsis Debug severity (operator signal, hidden from the default timeline; the IME Tracker card and the ingest fold read it regardless). The session page's "IME Tracker" card moved to the right column.
2026-08-30 (9)
- New:
backend/ime-pattern-health.md— the pattern-drift loop:ImeTrackerHealthcounters + per-pattern histogram in the agent (restart-safe viaImeTrackerState),ime_*keys inagent_metrics_snapshot, one-shotime_tracker_degradedWarning, session-endime_pattern_hits(every enabled pattern, zeros included,imeVersion), backendImePatternStats(PK = IME version, batch upsert, permanent),ImePatternDriftEvaluator(baseline = biggest version ≥100 sessions, expected = ≥0.8 hit rate, drift = 0 hits in ≥25 sessions, once per cell) →ImePatternDriftSuspectedops event,GET metrics/ime-pattern-health(GlobalReadOrAdmin), web sectionadmin/metrics/ime-pattern-health+ session-page "IME Tracker" rows, MCPget_ime_pattern_health. Registered inindex.md.
2026-08-30 (8)
- Update:
agent/ime-log-tracker-matching-budget.md— pattern-pack hygiene against the IME decompiles (1.97.107.0 / 1.104.102.0 / 1.105.103.0, spot checks 1.50 / 1.83): no wording drift in any shipped pattern; the 11 disabled legacy patterns (IME-DETECTED-OLD, IME-SET-CURRENT-1/2/3, IME-SKIP-APPLICABILITY-NOTMET, IME-SKIP-DEPENDENCY-DETECT, IME-SKIP-DETECTED, IME-SKIP-UNINSTALL-NOTDETECTED-2, IME-UPDATE-NAME-1/2/3) are deleted — their literals last existed in IME 1.50, the fleet starts at 1.97; IME-DO-TIMEOUT-1/2 match the exact source literal (DO downloading is not finished within timeout, jobId = …, fileId = …) instead of two lookaheads; IME-DO-TEL is documented as dead from 1.104 on and kept only for the 1.101/1.103 fleet share.
2026-08-30 (7)
- New:
agent/ime-log-tracker-matching-budget.md— the shipped IME pattern pack is anchored (^on all 90 patterns,IME-APP-VERSIONwithout the chained[^}]*?scan) and the tracker's read loop is bounded:BoundedLineReader(32 MB per line, exact byte bookmarks), 2 s per-line matching budget, held-back unterminated EOF tails with a 1 s settle, one Warning per pass (oversizedLines/regexTimeouts/lineBudgetBreaks); the unanchoredPS-SCRIPT-CONTEXTfalse positives on[Win32App] SideCarScript…lines are gone.--run-ime-matchingassembles multiline entries like the tracker. Registered inindex.md.
2026-08-30 (6)
- Update:
rules/rule-id-namespace.md— guard 1 rewritten: gather RuleStats (tenant +global_*rows) are now resolved against the tenant's active gather catalog (EventIngestProcessor.ResolveFiredGatherRules), metadata catalog-sourced, unknown IDs dropped — same design as the analyze path; theIsReservedBuiltInIdpattern gate on device-supplied IDs is gone.
2026-08-30 (5)
- Update:
backend/admin-identity-binding.md— new "The MCP whitelist is a third UPN-keyed grant" section:McpUsersrows (grant,UsagePlanoverride and Disabled kill-switch) are honoured only for the identity the UPN is bound to (McpUserServicerow →IsBoundAsync,GetBoundMcpUserAsyncfor the quota plan);POST global/mcp-usersbinds first with the shared resolver / 422 / 409 contract; unbound legacy rows are inert.
2026-08-30 (4)
- Update:
backend/ime-msi-archive.md— new "Trust boundary of a sighting" section: the device-suppliedime_agent_versionstring is gated byImeMsiArchiver.IsPlausibleVersion(dotted digits within MSI ProductVersion bounds) before it can become a GLOBALImeVersionHistoryRowKey (ImeVersionSighting.Rejected→ no row/ops event/archive job); a session counts once (UpdateSessionImeAgentVersionAsyncreturns whether the version changed); a second tenant's sighting stampsCorroboratedAt; the MemberRead projection lists only fleet-confirmed versions (Archived, corroborated, or first seen before 2026-08-31), Global scope sees every row.
2026-08-30 (3)
-
Update:
agent/local-admin-analyzer.md— built-ins matched by well-known RID (500/501/503/504) so localized Guest/Administrator are not flagged now that disabled accounts are inventoried;kioskUser0built-in; shutdown-time dynamic allowance restricted to non-SAM identities (a logged-in local account is never exempted — CWE-807) with newlogged_in_local_accountsfield; junctions and$<32 hex>folders skipped by the profile check. -
New:
agent/local-admin-analyzer.md—LocalAdminAnalyzerno longer skips disabled accounts (a/active:nobackdoor re-enabled after enrollment was invisible at both scans) and reads Administrators-group membership from the SAM viaNetLocalGroupGetMembersonS-1-5-32-544(LocalGroupNativeMethods, WMI-independent). New event fieldsunexpected_admin_members(+40),account_details,administrators_group.{enumerated,error_code,members},builtin_administrator_enabled(reported, not scored). Non-local members (Entra role SIDs, domain groups) are listed, never flagged.ANALYZE-ID-0021.2.0 explanation names the new fields. Registered inindex.md.
2026-08-30 (2)
- Update:
agent/cmtrace-time-resolution.md— new "Line parsing (bounded cost)" section:CmTraceLogParser.TryParseLineis no longer one greedy-.*regex (quadratic on hostile lines, no timeout) but a linear first-open / last-parseable-trailer search with a\G-anchored, timeout-backed trailer regex that returns exactly what the old regex did; the agent's multiline buffer gains a 1 MB char cap next to the 100-line cap, both logged at Warning, and the tail of a dropped entry is skipped instead of raw-matched.
2026-08-30 (1)
- Update:
agent/diagnostics-package.md— reads are validated on the handle, not the path:PinnedSourceFolderpins each section folder (its final path must equal the validated path; held open so the chain cannot be renamed) and opens every file withFILE_FLAG_OPEN_REPARSE_POINT, requiring the handle's final path to be<canonical folder>\<relative path>. A subdirectory swapped for a junction between enumeration and open is skipped (SKIPPED (resolved outside validated folder)) instead of copied into the package.
2026-08-29 (12)
- Update:
backend/tenant-activation-welcome-mail.md—PUT preview/notification-emailmoved toAuthenticatedUserWithRole; roleless callers may write the address only while the tenant has no enabled member (signup window), afterwards a member role is required.
2026-08-29 (11)
- Update:
rules/gather-rule-phase-scoping.md—on_eventfeedback-loop guard:TimelineEventStreampublishes the event source andGatherRuleExecutorHostnever dispatcheson_eventfor gather-emitted events (a rule triggering on its own output type looped for the whole session); per-rule on_event execution cap (100/session); backend rejectson_eventrules whose trigger equals their output type orgather_result.
2026-08-29 (10)
- Update:
backend/tenant-activation-welcome-mail.md— welcome/farewell mails HTML-encode the domain at substitution;DomainNameis frozen on the full-model config PUT (was persisted verbatim) and the first-login seed only accepts a strict DNS host name.
2026-08-29 (9)
- Update:
agent/decision-engine.md,agent/overview.md—SignalIngress.Postis worker-thread re-entrancy-safe: signals the worker posts to itself from insideApplyStep(EffectRunnerClassifierVerdictIssued) go to a worker-local follow-up queue drained ahead of the bounded channel instead of blocking on it, so a full channel can no longer deadlock the single consumer.
2026-08-29 (8)
- Creation: Added
backend/session-owner-binding.md— SESSION-OWNER-BINDING stage 1 (shadow): Sessions rows carry the creating device identity (OwnerKind/OwnerThumbprint/OwnerDeviceId/OwnerBootstrapCode/OwnerSerial/OwnerBoundAt, primary-only),SessionOwnershipPolicyevaluates every register / telemetry / error-report request against it, outcomes ride on the request row (SessionOwnerBinding), warnings and a throttledSessionOwnerMismatchops event; nothing is refused yet. Agent ships the dormant counterpart:errorCodeparsing on 401/403, one-shot session rotation onsession_owner_mismatchwithout feeding the auth-failure tracker, spool drop,agent_started.sessionRotated.
2026-08-29 (7)
- Update:
backend/client-cert-tenant-binding.md— new "Certificate transport and proof of possession" section:X-ARR-ClientCert(platform-set after the mTLS handshake, inbound copies stripped even on excluded paths) is the only certificate source; the client-writableX-Client-Certificatefallback is gone;/api/bootstrap/*routes are token-only and fail closed on a missing/empty token or unwired token service instead of falling through to the certificate stage.
2026-08-29 (6)
- Update:
backend/admin-identity-binding.md— grants are self-maintaining:AdminIdentityResolverderives the home tenant + object id from sign-in history (UserActivity), falls back to the UPN domain of an onboarded tenant, refuses to guess when a UPN was seen under two tenants; 422HomeTenantUnresolved+ portal tenant picker only for a never-seen domain. Identity fields and pills removed from the delegated / tenant-group forms; inspect/correct lives in Tenant Management → Edit Tenant Configuration → Identity bindings (collapsed).
2026-08-29 (5)
- Creation: Added
backend/admin-identity-binding.md— cross-tenant roles (GlobalAdmin / GlobalReader, delegated MSP) were resolved from the UPN string alone while the API accepts tokens from every Entra tenant. NewAdminIdentityBindingstable (RK = UPN → homeTenantId+ObjectId),AdminIdentity(upn + tid + oid) as the key both role services resolve on, binding checked after the row and before the Pro gate, object id pinned on the first sign-in from the bound tenant (ETag-guarded), grants requirehomeTenantIdand refuse to re-home (409),global/identity-bindingsGET/PUT/DELETE as the audited rebind path,RequestContext.ObjectId, throttle bucket keyed on oid. Legacy rows are inert until bound — seed bindings before the backend deploy.
2026-08-29 (4)
- Creation: Added
backend/ime-msi-archive.md— the version-verified, multi-host IME installer archive: MsiProductVersionReader (managed CFB/MSI Property reader), candidate host walk, Failed:VersionMismatch, sighting re-queue with 24 h backoff, and the 2026-08-29 1.105.103.0 poisoning that motivated it.
2026-08-29 (3)
- Update:
backend/vulnerability-correlation-sources.md- the admin page's Data Sync Status is complete: next to Sync KEV / Sync MSRC there are now Refresh NVD Cache and Sync EPSS, each with its own last-sync line (NvdCacheLastRefreshUtc,EpssLastSyncUtcon AdminConfiguration; shared manifest regenerated). The NVD and EPSS walks moved out of the timer intoVulnerabilityCacheRefreshService(singleton, one in-flight slot per walk) so the button and the nightly run are literally the same code; the NVD walk is minutes long and rate-limited, soPOST vulnerability/sync-nvdanswers 202 and the page pollssync-status(nvdRefreshRunning,nvdLastRun) while it runs,sync-epssruns synchronously. 409 while a walk is in flight; the timer skips a slot a manual run holds. Operator-only surface - no customer docs.
2026-08-29 (2)
- New:
backend/vulnerability-correlation-sources.md- FIRST EPSS joins NVD / CISA KEV / MSRC as the fourth vulnerability source: the probability + percentile + date live on the cached CVE (one batch of <= 100 ids per CPE fill, everycve_*row re-scored by the nightly sync, only changed rows rewritten and with their originalCachedAt), the CVSS vector is persisted instead of dropped, and each CVE carries an act / attend / track priority (KEV; EPSS >= 0.1 or CVSS >= 9.0; rest) next to the unchanged severity bands - two visible signals, no composite score. The comparison with an open-source CVE MCP server surfaced the real 429 defect: Polly already retried, but a refused NVD answer came back as an empty response and was cached as "0 CVEs" for seven days.NvdFetchResultnow separates "answered" from "refused" (nothing persists a refusal - CPE cache, negative auto-resolve cache, admin auto-resolve), and a 429 opens a process-wide cooldown (Retry-Aftercapped at 120 s, else 30 s) so the run fails fast instead of paying a retry chain per software item; hard-expired rows keep serving meanwhile. NVD -> cache projection and the row builder are now single functions shared by the live fill and the sync. CveIndex / fleet summary carryepssScore+priority; portal shows EPSS pill, priority pill, vector; MCP tool descriptions and the CVE-audit prompt name the fields. ExploitDB recorded as a deliberate not-now. Backend before web; trust pages (terms attribution, privacy, data-flows), settings and portal-guide docs updated in the same change.
2026-08-29
- New:
mcp/protocol-eras-and-cimd.md- the MCP server moved to the 2026-07-28 protocol revision (TypeScript SDK v2:@modelcontextprotocol/server+/node, zod 4; the monolithic@modelcontextprotocol/sdkis gone). One per-request factory (mcp-server-factory.ts) feeds both eras throughmcp-http.ts: 2026 clients are served by the SDK entry (createMcpHandler,legacy: 'reject', JSON response mode), 2025 clients keep the existing stateless transport withenableJsonResponse- routed on the SDK's ownisLegacyRequestpredicate, because the built-in stateless fallback would answer today's Claude/VS Code clients with SSE and lose the gzip win. Capabilities now declarelistChanged: false(the SDK default advertised a notification a per-request server cannot send);cacheHintsstampttlMs/cacheScope: 'private'on discover/list results (5 min) and resource reads (60 min) - never public, the catalog is role-dependent. Client registration:client_id_metadata_document_supported: true; an httpsclient_idis a Client ID Metadata Document resolved bycimd.ts(SSRF gate on the resolved addresses before any request, no redirects, 5 s / 16 KB, exactclient_idmatch, boundedredirect_uris, 10-60 min cache), the requested redirect_uri still has to pass the dynamic-registration allowlist; HMAC-signed DCR client_ids stay as the fallback.mcp-oauth-flow.mdupdated. Package version 1.5 -> 1.6. Pinned bymcp-http.test.ts(real v2 client over loopback HTTP, both eras, role parity),cimd.test.ts,oauth-cimd.test.ts.
2026-08-28
- Update:
CmTraceSkewTripwire- the backend regression tripwire additionally requires >= 80 % of the individual IME deltas (relative to the other side's median) to sit within 2 min of some 15-minute grid multiple beforeCmTraceTimeSkewRegressionfires; the ops event and its details now carry that fraction. Motivation: the first soak week produced 7 fires, all backlog replays - a relaunched agent re-tailing the IME log uploads one burst holding most of the session's IME samples with a continuum of ages whose median hit the grid by chance (session d832560a: 508 of 627 samples, 46...733 min, median 91). Era-mixing regressions (every line on the grid) still fire. Doc:docs/agent/cmtrace-time-resolution.md.
2026-08-27 (2)
- Update:
SecurityValidator/DeviceAssociationValidator- Microsoft released Windows Autopilot device association on 2026-08-25 (learn.microsoft.com/autopilot/device-preparation/device-association/overview: TPM-backed UEFI marker, device-targeted device preparation policies, associated devices are marked corporate-owned by Intune with no corporate identifier upload). The DevPrep association lookup therefore leaves shadow mode and becomes the third accepting method of the device-validation gate, evaluated inline after the Autopilot and Corporate Identifier lookups and before the Cloud PC fallback; a match setsValidatedBy = DeviceAssociation, a miss joins the combined 403 detail, a transient failure the 503 Retry-After - the same contract as its siblings, so enabling it can only widen acceptance, never reject a device the other methods would have taken.ValidateDeviceAssociationcounts toward "at least one validation enabled" and is no longer GA-only anywhere (UpdateTenantConfigurationFunctionrevert,TenantConfigPatchService.GaOnlyFields, the Global-Admin gate on the settings toggle); the toggle sits under Device Preparation next to Corporate Identifier and follows the shared consent rule (first serial-based validation carries the consent, any further one is a plain persisted toggle). Known gap carried over from the shadow phase: the/beta/deviceManagement/tenantAssociatedDevicesendpoint is still undocumented in Graph reference and every production lookup so far answeredmatched=Falsefor Autopilot v1 devices, i.e. the positive match path is unproven in the field - first associated enrollment must be checked forValidatedBy = DeviceAssociationon the session row. Landing hero carries a release pill linking to the customer docs; dashboard tip added. Backend before web; customer docs updated inautopilotmonitor-docs.
2026-08-27
-
Update:
backend/silent-session-classification.md,backend/verdict-calibration.md- the first real calibration read tuned what it measures. Rule 5 grew a past-grace fork 5a: Device Setup done + real-user desktop + zeroapp_install_failedreconciles to Succeeded "completed (assumed)" (r5_assumed) - r5-Incomplete devices re-enrolled at 2.6 %, the succeeded background, and the sampled desktop half had clean app records; no desktop or any failed app stays Incomplete. The max-lifetime watchdog (either event shape) now skips the AwaitingUser grace entirely - all five observed maxlife parks expired unhealed, and late telemetry heals a terminal verdict anyway - while WhiteGlove rule 1b keeps its park (its one episode healed). The drift radar got floors (≥ 10 window hits; per-path share regression needs ≥ 5 baseline hits, so new/renamed vocabulary cannot "regress") and legacy continuity: rule-shapedlegacy:*paths count into the silence and classifier-verdict group sums on both sides of the 2026-08-23 cutover, which dissolves the lift-124 silence artifact and lets sub-floor episodes clear themselves. Backend only; the new path shows up in the matrix on its own. -
Update:
backend/tenant-activation-welcome-mail.md- the reverse lookup. A delivery log names only the recipient, so an address had to lead back to its tenant: the tenant console's search now matches the notification and contact address as well as domain and tenant id, with the quoted exact mode unchanged. The addresses sit inPreviewWhitelist, not on the tenant config, so a new GA/Reader-onlyGET /api/preview/notification-emailsreturns them as one map (single cross-partition scan ofRowKey eq 'notification-email') instead of a point read per tenant; both sides join on the lowercased tenant id because the address rows carry the JWTtidwhile a config PartitionKey's casing is not guaranteed, and matching is case-insensitive because stored addresses keep the casing the user typed. The map is best-effort in the portal - a failed read costs only the search-by-address, never the tenant list - and the addresses stay out of the list rows (search key, not a column). Match rules moved intotenantSearch.tswith unit pins. Backend before web.
2026-08-26
- Update:
agent/wifi-ssid-location-gate.md- the network timeline now explains its blank signal too, with no agent change:buildSegmentsalready mergeswifi_signal_infointo the covering WiFi segment, sowifiDataLimitedReasonrides in with the sample. Segments that never got a sample (a mid-session reconnect emits none) inherit it - the gate is a device-level setting and cannot flip mid-enrollment - while a segment with a real reading is never overwritten. Tooltip showsSignal: unavailable - Location services off on this device; the "Avg WiFi signal" stat readsn/a (Location services off)instead of vanishing.network_state_changeis deliberately left alone: it has never carriedsignalPercentorradioType(onlybefore_/after_wifiSsidreaches its payload), so there is no absent field there for a reason to explain, and the SSID it does carry is unaffected by the gate. Web-only change.
2026-08-26
- Update:
agent/wifi-ssid-location-gate.md- the gate is now surfaced instead of only logged. When the WinRT tier wins and the native tier reportedrc=5,wifi_signal_infocarrieswifiDataLimitedReason = "location_services_off"and the message gains- signal unavailable, Location services off, so the reason is readable straight from the timeline; the portal WiFi card (DeviceDetailsCard) matches that literal and names the Intune setting that restores the full payload. The claim is evidence-only - any other native failure leaves the field null rather than blaming a privacy setting, withIsLocationGateDeniedpinned at its boundaries. Newwifi_signal_infogroup in the MCPDEVICE_PROPERTIES_CATALOGso a fleet can be sized viasearch_sessionsbefore any policy changes. Agent release + web deploy + MCP redeploy; backend untouched (the payload is opaque DataJson).
2026-08-26
- New:
agent/wifi-ssid-location-gate.md-wifi_signal_infowas missing on most Windows 11 24H2+ WiFi enrollments (build 22631 3/3, 26100 37/178, 26200 43/95 over n=276 sessions from 08-01). Not a parser or language problem: every event that exists carries a full SSID, so the failure is binary at the reader. Cause is the 24H2 location gate onWlanQueryInterfacewithwlan_intf_opcode_current_connection- the only opcode the agent uses - which returnsERROR_ACCESS_DENIED; the consent prompt only fires for a process in the user context outsideSystem32, so the agent (SYSTEM, session 0, pre-privacy-page) is denied silently, and it resolves consent from the HKLM ConsentStore because theS-1-5-18hive has none - while the interactive user on the same box reads the SSID fine via the per-user NonPackaged value.netsh wlanhangs off the same gate, so the 2026-07 netsh fallback (and its multi-language labels) never covered the case it was built for; more labels cannot fix it. NewWinRtWifiSsidReaderuses the documented replacementWlanConnectionProfileDetails.GetConnectedSsid(ungated, SSID only), bound via the CLR WinRT projection with no winmd/NuGet likeOobeStateReader; newWifiInfoProviderowns the tier order (native, WinRT, netsh) that both collectors had duplicated. The step diagnostics added in 2026-07 were logged atDebugwhile the default level isInfo, so they never reached a field log - the all-tiers-failed case is now a Warning that decodesrc=5into the location-gate explanation. Agent-only change; needs an agent release.
2026-08-25
- Update:
backend/client-cert-tenant-binding.md— stage 2 enforced:Mismatchnow returns 403 viaLogRequestRejection(stage=certtenant). Switched on against 35,684 requests / 32 tenants at 100 %Match, with the Warning channel verified to be carrying other traffic so the zero was not an artefact. The rule is one function (CertTenantBinding.Rejects) and onlyMismatchblocks;ExtensionMissing/Unparseable/RequestTenantNotAGuidare tolerated on purpose (the first is the candidate for later tightening and has a test pinning the tolerance). Evaluation fails open on its own exception, and the 403 body does not echo the certificate's tenant. Device binding (stage 3) stays shadow + Global-Admin-only and is unaffected.
2026-08-25
- New:
agent/zero-user-apps-completion-gate.md— a Classic enrollment whose user has zero targeted Intune Win32 apps could not produce the evidence any user-phase completion path required: IME's zero-app branch (ApplicationPoller, verified against decompiled 1.104.102.0 and three older builds)continues before itsCompleted user sessionstatement, soImeUserSessionCompletedUtcstayed null, all four arms ofShouldTransitionToAwaitingHellowere dead, and the 30-minadvisory_completionbackstop resolved the session asesp_exit_without_completion_evidence— with a DAD-validated desktop, Hello provisioned and RealmJoin still installing. One tenant: 10 of 30 enrollments in a week, all false positives. Two fixes. (1) New patternIME-USER-SESSION-ZERO-APPSreads the line IME does write and routes it through the sameOnUserSessionCompletedcallback, guarded to the AccountSetup phase and replayed if the observation predates the phase marker;AreUserEspAppsSettlednow accepts an empty app list only with that explicit observation. (2)HandleAdvisoryCompletionDeadlineFiredre-arms instead of failing while the RealmJoin gate is closed and its own deadline is still armed — session 75d6ae8e installed 25 RealmJoin packages inside the window, the last 2 min before the failure.ime_user_session_completedgains anevidencefield (zero_user_apps_enumerated/user_session_completed); no new event type. The pattern reaches existing agents via the config API (an unknown action is a Debug line), the behaviour needs an agent release.
2026-08-24
-
Change:
DeviceAssociationValidator/SecurityValidator— the DevPrep association shadow now logs one line per real Graph lookup instead of one per request (DeviceAssociationResult.ServedFromCache,AsCacheHit()copies rather than marking the shared cached instance), the same gate the cert-device-binding shadow uses. Measured: one enrollment produced 263 identical lines off a handful of calls. Status of that preview, for the record: the/beta/deviceManagement/tenantAssociatedDevicesendpoint answers (6 calls, all HTTP 200,transient=False), but every result so far ismatched=Falsefrom a single Autopilot v1 device — which is the correct answer for a device that was never part of a Device Preparation policy. The matching itself is therefore still unproven, and the endpoint remains undocumented in both v1.0 and beta Graph reference, so it is not a basis for enforcement. -
Update:
backend/client-cert-tenant-binding.md— stage 3 verified live (ea6a375, gktatooine): 136 requests, allMatch, device object ~72 s older than the session start, so no enrollment race in that run. Measured Graph cost: 1 call for 136 requests (30 min positive cache), versus 25 for the per-request Autopilot validator — the positive TTL needs no change. The shadow log is now gated onServedFromCache(one line per real lookup instead of per request); never-cached outcomes still log every time. -
Update:
backend/client-cert-tenant-binding.md— stage 3, the cert-to-device binding: newIntuneDeviceBindingValidatorresolves the Subject-CN managedDevice id against the tenant's owndeviceManagement/managedDevices(point-GET, no filter literal), modelled onCloudPcDeviceValidatorand wired fire-and-forget like the DevPrep shadow so a cold Graph round-trip never enters the agent request path. New GA-only tenant toggleValidateIntuneDeviceBinding(shadow — never touchesdeviceValidated), new optional Graph featureIntuneDeviceBinding→DeviceManagementManagedDevices.Read.AllinGraphFeatureCatalog+ the customer grant script, andenrolledAgeSecondson every log line so the enrollment race can be measured before this could ever become a gate. Grep marker:CERT-DEVICE-BINDING-SHADOW. -
Update:
backend/client-cert-tenant-binding.md— shadow telemetry moved off the deadLogInformationpath: theMatchoutcome now rides on the request row as theCertTenantBindingdimension (SecurityValidator→FunctionContext.Items→RequestTelemetryMiddleware), which is unsampled and already exists per request, so the denominator costs nothing. Non-Match outcomes keep their Warning trace. Post-deploy verification of249e39bcfound zero rows across 32k requests — workerLogInformationnever reaches App Insights (provider default rule Warning+), making "everything matched" indistinguishable from "never ran".CertificateValidationResult.FromCacheremoved with the sampling path it existed for. -
New:
backend/client-cert-tenant-binding.md— the agent's mTLS certificate chains to Microsoft's shared Intune roots, so certificate validation alone never proved tenant membership; tenant scoping rested entirely on the (non-secret) device serial in the downstream validators. The Intune MDM leaf carries the Entra tenant id in OID1.2.840.113556.5.14(nested OCTET STRING), which is NOT…5.6(Intune Account id, a different GUID) and NOT…5.4(device id, stored as 16 bare bytes) — all decoded by one sharedMsDeviceCertificateOids.TryParseGuidnow used by agent and backend. Stage 1 ships as shadow only:CertTenantBinding.Evaluate(pure) plusSecurityValidator.ObserveCertTenantBinding(returns void, swallows its own exceptions, cannot change an authorization outcome), withMatchsampled viaCertificateValidationResult.FromCacheso the denominator survives without per-request volume. Grep marker for the stage-2 enforcement change:CERT-TENANT-BINDING-SHADOW.
2026-08-23
- New:
backend/telemetry-ingest-shaping.md—RequestTelemetryMiddlewaresetsSamplingPercentage=100on its own item (worker request rows were adaptively sampled, ItemCount 2–5 on weekdays, despite host.jsonexcludedTypes), the redundant host HTTP request copy is dropped by a workspace transformation DCR (infra/appinsights-workspace-transforms.bicep, timer/queue rows kept), andStorageDependencyFilterProcessornow also drops expected storage outcomes (404/412/409, both InProc and HTTP shapes) and successful SignalR REST calls; the host-emitted SignalR binding calls (bulk of the volume, SDKrdddsc) are dropped by a secondAppDependenciesflow in the same DCR. Measured ≈ −1.3 GB/week. - Update:
backend/table-schema-sentinel.md— "Measuring it":StartupTelemetryServiceemitsBackendStartupMs(process start → ApplicationStarted) andBackendTableInitMsas App Insights custom metrics with version/commit/fullPass dimensions; the MCP server writes one{"type":"boot","readyMs"}stderr line per start (query in.claude/commands/mcp-telemetry.md§A2). - New:
backend/table-schema-sentinel.md— startup table initialization gated by a schema sentinel (SHA-256 overTableNames.AllinAdminConfiguration/SchemaSentinel/tables): matching sentinel ⇒ zeroCreateTableIfNotExistscalls on a cold start, missing/stale ⇒ one full pass + sentinel rewrite. SessionsIndex startup backfill only after a full pass,IsSessionIndexEmptyAsyncno longer treats errors as "empty", conditional-insert claim so one scaled-out instance backfills. Daily maintenance runsIStorageInitializer.EnsureAllAsyncto repair out-of-band table deletions. - Update:
backend/verdict-calibration.md— MCPget_verdict_calibrationresponse shaping: per-row trend denominators dropped, explicit nulls,minSharePct/topwith anomittedreport. - Update:
backend/verdict-calibration.md— share-regression radar restricted to backend-decided paths; agent-declared/registration paths (customer workflow mix) and derived legacy paths no longer alert, stale episodes re-arm themselves. - Update:
backend/time-attribution.md,backend/device-journeys.md,backend/verdict-calibration.md— the rolling maintenance sweeps (time attribution, device journeys, verdict calibration, calibration-radar discovery) now share ONE projected 35-day cross-tenant Sessions scan per tick (LoadSweepWindowSessionsAsync+MaintenanceSweepSessionProjection, pinned byMaintenanceSweepProjectionEquivalenceTests) instead of four full-table drains; each sweep slices its own window by StartedAt. - New:
backend/verdict-calibration.md— the classifier thermometer: every status write now stampsVerdictPath(required parameter onUpdateSessionStatusAsync;EnrollmentTimeoutClassifierreturns its rule id), overriding a verdict preservesPriorStatus/PriorVerdictPath,VerdictPathDerivationattributes pre-instrumentation rows read-side (legacy:*when ambiguous). New daily aggregateVerdictCalibrationAggregates(per-path count/derived/eligible7d/reEnrolled7d/overridden-by-admin|late|other, sweep after the device-journey pass, 180d retention),GET global/metrics/verdict-calibration, MCPget_verdict_calibration, admin metrics section, and theVerdictCalibrationRadar(share / silence-share / r6 evidence-gap episodes in the tracker keyspaceverdictcalibration|,VerdictCalibrationDriftops event, no tenant bell). - New:
backend/silent-session-classification.md— one place for the silent-session verdict:EnrollmentTimeoutClassifierrule order incl. the new rule 1cIsSelfDeployingProvisioned(self-deploying profile + Device Setup all-succeeded + no explicit failure ⇒ Succeeded reconcile, no grace/AwaitingUser — kiosk tenant aebdce78 audit: 836 finished devices parked AwaitingUser/Incomplete), the sweep stage-1 gates (TryMarkWhiteGloveAwaitingUserAsync, newTryReconcileSelfDeployingAsync), the ingest callers (max-lifetime, late-telemetry heal, stall probe) and the new admin retro modeself_deploying_silent(GetSelfDeployingSilentSessionsAsync). Agent companion:SessionRegistrationHelperwaits up to 15s for a network link and retries 6× (~62s) instead of 5× (~30s) so the reboot relaunch on Wi-Fi no longer exits 7 before the link is back. - New:
agent/diagnostics-package.md— the diagnostics ZIP's built-in collection is now ONE catalog in Shared (DiagnosticsBuiltInSections.All): the agent iterates it in order and the backend serves the same list to the portal (GET diagnostics/paths, MemberRead), so what administrators see is exactly what the agent collects. New conditional sections: RealmJoin client/package/Chocolatey/per-user logs behind the tenant's RealmJoin Watcher toggle (mirrored ontoAgentConfiguration.EnableRealmJoinWatcherby the merger) and the Device Preparation bootstrapper event log behind the deterministic WDP marker (probe once per build, fail toward Classic); every skip is a manifest line. Built-ins bypassDiagnosticsPathGuardsby design (reviewed code,%ProgramData%\RealmJoinis not allowlisted); caps/reparse/evtx export still apply.MergeDiagnosticsLogPathsdedups global ∪ tenant paths. Portal: collapsed read-only catalog + one-line rows in both cards, tenant context dropped the GA-only global fetch. - Update:
rules/gather-rule-guardrails.md—DiagnosticsPathGuardsscope clarified: configured diagnostics paths only, never the built-in catalog. - Update:
backend/tenant-activation-welcome-mail.md— new "Template overrides" section: Global Admins can replace the welcome/farewell HTML in the portal (Settings → Email Templates);EmailTemplateServiceresolves override-or-built-in per send (PreviewConfig table, partitionEmailTemplates, 5-min cache,{{domainName}}placeholder, 30k limit, fail-soft send path), GA endpointsglobal/email-templates/{kind}(+/testto the caller's tenant contact address). Also: the mail header is image-free (CSS-drawn brand mark, gradient stripes and clip-path edge as progressive enhancement).
2026-08-22
-
Update:
backend/tenant-activation-welcome-mail.md— new "Mail transport" section: transactional mail now goes through the provider-neutralEmailService(IEmailService+IOffboardFarewellEmailSender) with the provider confined to one private transport method and theEmail:*settings (ApiKey,Endpoint,FromAddress,FromName); current provider Mailchimp Transactional (Mandrill) replacing Resend (NuGet dropped, typedHttpClient+ResiliencePolicies.Notification). Result-array semantics (sent/queued/scheduledok,rejected/invalidwarn), open/click tracking explicitly off because the privacy/trust claim depends on it. -
New:
backend/plan-downgrade-retention-grace.md— plan downgrade policy: every Pro entitlement gates read-time and reversibly on downgrade ("gate, don't destroy"), except retention, which now gets a 30-day grace (FeatureEntitlementCatalog.RetentionDowngradeGraceDays) before the Community cap starts hard-deleting the 90–365-day band. Anchor model:ProDowngradedUtcstamped/cleared by the plan endpoint's pureApplyPlanChangescore on EFFECTIVE-edition transitions (covers explicitly ended trials; planTier downgrade under an active trial deliberately doesn't stamp), trial expiry anchors read-time viaTrialExpiresUtc. New Warning ops eventsTenantPlanDowngraded(PATCH handler) andTenantRetentionGraceExpiring/TenantRetentionGraceEnded(TrialExpirySweep window mechanics, only when stored retention exceeds the Community cap). Field is patch-denied + revert-protected; retention WRITE cap deliberately ignores the grace.
2026-08-21
-
New:
agent/do-group-id-auto-set.md— optional DO peering-group seed (EnableDoGroupIdAutoSet, default off, ConfigVersion 40):DoGroupIdServicederives a deterministic GUID from the default gateway's IPv4 + SendARP MAC in RealmJoin's exact byte layout (cross-product peering) and writesDOGroupIdin the 64-bit GPO Policies store with readback verification. Conservative by design: no random-GUID fallback (skip on missing gateway/MAC),DOGroupIdSource/MDM-managed values/foreign GPO values always win (skip reasons in thedo_group_id_auto_setevent), own stale values are recognized via thewritten:<guid>StartupEventGate fingerprint and updated after a gateway change. Gateway heuristic extracted into the sharedNetworkInterfaceLocator(dedup of NetworkChangeDetector + DeviceInfoCollector copies). -
New:
agent/system-timeline-watcher.md— clock-step and sleep-episode ground truth from the System event log: oneEventLogWatcher(Kernel-General 1, Power-Troubleshooter 1, Kernel-Power 507; 506 deliberately unconsumed) emitssystem_clock_changed(oldTime/newTime/signed timeDeltaMs/reason/process; 2 s micro-slew floor, ≥5 min = Warning+immediate) andsystem_sleep_episode(kind sleep/hibernate/modern_standby, authoritativeenteredAt/exitedAtpayload instants, 60 s Modern-Standby floor). WindowsUpdateTracker-style backfill (24 h default,system-timeline-watermark.json) recovers pre-agent records; ConfigVersion 39 (Collectors.EnableSystemTimelineWatcher). Consumers: TimeJumpBadge causeclock-set(ground truth beats inference), SessionInfoCard standby note next to the unchanged wall-clock duration,SleepSpans in time attribution (AttributionVersion 3, RebootSpan clone), and ANALYZE-DEV-012 (info, single, ≥5 min episode; deliberately the only new rule — clock issues stay with DEV-008). -
New:
agent/power-state-watcher.md— live AC/battery observation during enrollment:PowerStateWatcherHostsubscribes to the WMI pushWin32_PowerManagementEvent EventCode 10(no polling; re-probesGetSystemPowerStatusper push, 5 s trailing-edge debounce, never armed on battery-less devices,Collectors.EnablePowerStateWatcherkill-switch, ConfigVersion 38). The purePowerStateTransitionTrackeremitspower_state_changewithtransition= ac_to_battery (Warning, immediate) / battery_to_ac (Info) / threshold_crossed at 50/30/15% (each level latched once per lifetime, lowest-crossed-only on jumps, baseline-below-level covered, emission cap 20). Classified non-activity on both sides (SignalActivityClassifier + IsPeriodicOrStallEvent). New analyze rules ANALYZE-DEV-009 (thresholdPercent=15, high) and ANALYZE-DEV-010 (transition=ac_to_battery, warning), both withon_event:power_state_changeinterim triggers and deliberately single-required-condition (event_data conditions have no same-instance join). -
Update:
rules/rule-id-namespace.md— the export→paste round-trip is no longer lossy: the JSON editors now map rule-shaped exports into the form (gatherRuleToFormunflattens the nested gatherparametersobject, analyzejsonToFormmapsevaluateOnonto the eval checkboxes;tagsand the effectivemarkSessionAsFailedcarry into the created rule). Both editors still accept the serialized-form shape; edit-mode pastes merge, with an explicitparameters/evaluateOnwinning over stale flat fields. -
New:
rules/rule-id-namespace.md— rule identity model: human-readableruleIdstrings (no GUIDs), tenant isolation via Table Storage partitioning (global= built-in/community, tenantId = custom), the reserved built-in namespace fromRuleIdPolicy, and the four collision guards: catalog-only globalRuleStatsaggregate (same-ID custom rules of unrelated tenants no longer sum into oneglobal_{ruleId}row), combine.js duplicate/namespace fail, GitHub-reseed rejection of non-reserved IDs into the global partition, and the namespace gate on the cross-tenant RuleState GC. -
New:
backend/app-install-duration-attempt.md— attempt-scoped app install durations:DurationSeconds= status-defining (last) attempt anchored on the new max-foldedLastAttemptStartedAt(StartedAt stays the earliest-wins window/version-ordering key; legacy rows without the anchor keep the span fallback);InstallPassCountwith replay-guarded additive fold; weaker-terminal guard (completed-with-Skipped never overrides Installed/Error, in-batch and cross-batch); attempt-scopedDownloadDurationSecondspairing; the version-regression radar's both-sides cutover gate; time attribution counts the union of per-pass active segments (occupancy merges segments, hulls stay for display/what-if); agent payload timing re-arms both endpoints on terminal→active. No backfill — pre-cutover rows keep span semantics. -
Update:
agent/decision-engine.md— activity-based RealmJoin timeout extension (report 55e6afd61c9d: a large RJ package catalog outlived the 60-min window mid-install — the timer arms at RJ-agent-MSI detection, so the first deployment had effectively ~44 min). When theRealmJoinTimeoutdeadline fires withLastDeploymentPhase100/101 and deployment activity (phase change or package observation, persisted inRealmJoinFacts.LastActivityUtc; the detection-time phase capture deliberately excluded) inside the last 60 min, the deadline re-arms tolastActivity + 60 min(Info eventrealmjoin_timeout_extended), clamped at detection + 4 h.realmjoin_timeoutnow carriesreason=hard_timeout/inactivity/absolute_cap; a fire of a superseded deadline incarnation dead-ends. Idle detection and phase-200/210 sessions keep the original 60-min behavior. -
New:
backend/send-time-frame-separation.md— P14: the agent stamps one device-clock send timestamp per upload attempt (X-Send-Time-Utc), the ingest function server-stamps it asSentAton every event of the request.SentAt − OccurredUtcis pure spool delay,ReceivedAt − SentAtis network + device-vs-server clock offset — the two were indistinguishable before and had to be separated statistically. Theclock_skewcondition measures SentAt-carrying batches directly (spool-immune, no spread cap, IME-only sessions become measurable; evidence carriessentAtBatchCount), legacy batches keep the median path; the CMTrace tripwire deliberately stays on its differential math (common-mode clock offset cancels there).
2026-08-20
-
Update:
agent/cmtrace-time-resolution.md— new "Backend regression tripwire" section: the P6 split. Operator side:CmTraceSkewTripwirepiggybacks on the terminal counter reconcile's partition scan (Δ = ReceivedAt − OccurredUtcper source, 15-min grid + 2-min residual, ≥20 samples / ≥3 distinct batches per side, bias-dominated sessions suppressed,measuredWriterOffsetMinutesnever consulted) and emits theCmTraceTimeSkewRegressionops event — goal state: never fires; kill switchCmTraceSkewTripwireDisabled. Customer side: newclock_skewanalyze condition source (skewMetric: clock_jump | sustained_offset, threshold in seconds, batch-median frames with spool-spread filter, plateau step detection, end-state persistence, IME-derived events excluded) behind the new built-inANALYZE-DEV-008(300 s = the Kerberos/Entra 5-minute skew boundary) — the during-enrollment complement of ANALYZE-DEV-007's one-shot NTP check. P5 (agent-side observability event) is subsumed by the tripwire. -
New:
agent/cmtrace-time-resolution.md— the era-aware answer to the CMTrace timezone skew: a bias-less line read in a provably fresh pass anchors its own UTC offset against the agent clock (15-minute grid, 2-minute residual), so interleaved writer eras in one file resolve line by line; everything not provably fresh keeps the uniform reader-zone fallback and says so viasourceOffsetOrigin. Freshness is a per-fileLastCheckedUtcstamped on every poll look (empty first sights included) with a 30-second window, never restored across restarts. Replaces the reverted per-file offset application (04b1a7c6); the per-file measurement stays observational, now with the anchor timestamp logged as a tripwire for the still-unexplained e9753578 label/anchor crossing. -
Update:
agent/post-reboot-completion-recovery.md— retitled to "ESP Completion Starvation — the sits-d Cloud-PC complex" and reframed on the evidence: the reboot was a red herring. The five stuck Cloud PCs carriedSkipUserStatusPage=True(read 10+ times across three agent processes,Trueevery time) and Windows honoured it — no user ESP page, hence no Shell-Core 62407 and no AccountSetup provisioning registry, verified over a six-hour uninterrupted post-reboot run with the watcher armed. Arm B ofShouldTransitionToAwaitingHellowas open from the bootstrap onward, but every evaluation site hangs off a signal a skip-user flow cannot produce, and theAdvisoryCompletionbackstop arms exclusively from anEspExitingsignal — an open gate nobody knocks on. Fix:HandleImeUserSessionCompletedV1accepts the observed skip in place of the final-exit evidence (skipUserExitEquivalent; anchor + genuine IME completion + real-user desktop stay mandatory, WDP excluded). The knock deliberately lives on the IME edge, not desktop arrival — ordering-robust against Fix 10's AccountSetup bounce-back and semantically "user-phase work finished" rather than mid-install.ClassicSkipUserEspUserSessionCompletionTestsreplays the real 8110e262 ordering end to end. Docs and comments no longer claim an ESP-exit recovery that does not exist (stray duplicate summary inShellCoreTracker,ImeLogHostrestore-callback comment); the residualSkipUser=falseexit-in-downtime gap is named honestly and surfaced byANALYZE-ESP-005viasession_stalledinstead of being patched with an unorderable replay.
2026-08-19
-
New:
agent/post-reboot-completion-recovery.md— a forced mid-ESP reboot (DeviceGuard/DmaGuard) used to cost the session its AccountSetup completion permanently. Two defects:ShellCoreTracker.BackfillRecentEspExitEvents()existed but had no caller, so every agent restart dropped an ESP exit (62407) written while it was down; and the user-apps-settled synthesis was edge-triggered onEspExitedonly, so apps settling afterwards never re-opened the gate.EspAndHelloHost.Start()now backfills with a lookback sized to the downtime (previousExit=reboot_kill→ sinceLastBootUtc, clamped to 360 min), andReevaluateUserAppsSettledSynthesis()is chained intoImeLogTracker.OnAppStateChanged. No new completion path — the gate conditions are unchanged. Evidence: sits-d Cloud PCs, fiveIncompletesessions on healthy devices, two of them at138/138 apps, 0 failed. Hardened over two review rounds: the remembered edge must be a live exit that is confirmed post-AccountSetup (a replay carries no evidence of its own position — Shell-Core writes the sameCommercialOOBE_ESPProgress_Page_Exitingfor the intermediate and the final exit, and the registry check reads now, not the event's time); the replay hands over the newest exit rather than the oldest; the replay is narrowed to the Hello-wizard start (62404) alone — a conservative fact that can only make the agent wait longer. A replayed ESP exit is never re-raised at all: Windows writes the identical description for the intermediate and the final exit, the reducer orders exits by ingest ordinal (fresher than reality for a replay) andHandleEspExitingV1passesespFinalExitInFlight: truefor every arriving exit, so arm C ofShouldTransitionToAwaitingHellowould open on a historic handoff once the restored state carries AccountSetupEntered + IME user session + desktop. A replayed ESP failure is likewise dropped (it could fail a session that recovered). Skipped records are reported as anagent_trace. The window is measured from the previous run's last known liveness (snapshot mtime, widened byLastBootUtc) instead of a constant — the scheduled task has a BootTrigger only and no restart-on-failure, so a crashed agent returns hours later and five minutes would lose the exit every time. Onfirst_runthe replay stays off, leaving the happy path untouched. -
Update:
rules/analyze-rule-triggers.md—on_event:session_stalleddocumented as an established interim trigger for the never-terminal stuck session (one emission per session, no throttle needed).ANALYZE-ESP-005carries it: both its conditions are monotonic and it has no terminal precondition, and it fires at confidence 95 on the sits-d stalled stream.ANALYZE-APP-015deliberately does NOT — it is the worked example of the terminal-precondition trap (lint 1) — andANALYZE-ESP-001does not either, because itsphase_durationgate on a still-open phase is unverified mid-run. NewANALYZE-APP-016("Required Apps Never Started — ESP Apps Gate Starved") turns the existingapp_install_starvedevents into a customer-facing finding and, unlikeANALYZE-APP-015, fires even when the session succeeded — session3b9291aawas recordedSucceededwith 99 required apps that never started installing. -
New:
script-publishing.md— the customer script publishing chain..github/scripts/Publish-BootstrapScripts.ps1becomes the single owner (dev render, version-bump guard, uploads, version oracles, alias verification), called both by the newpublish-scripts.yml(push tomainonscripts/Bootstrap/**andscripts/CustomerSetup/**) and by thepublish_as_stablebranch ofbuild-agent.yml. Script publishing no longer waits for an agent release. Blobs are published in a deterministic CRLF/UTF-8-no-BOM form so byte comparisons do not depend on the checkout; the Pester + ASCII gates move into the reusablebootstrap-script-gates.ymland block the upload;AdminConfiguration.LatestBootstrapV2ScriptVersionnow has exactly one writer. The version-bump guard compares a code fingerprint (PowerShell token stream without comments, newlines and line continuations), so only behaviour changes require a$ScriptVersionbump — comment and formatting changes publish with a warning.
2026-08-18
-
New:
agent/wdp-scenario-gates.md— WDP scenario gates (afee7ae0 audit): deterministic ExecutionContext marker as the only gate key (fail-toward-Classic), suppressed Autopilot/ESP-only emissions and hosts (autopilot_profile*/ZTD probe, esp_config unknown null-gate, EspPolicyProviderStallHost, ProvisioningStatusTracker, ModernDeployment noise IDs 100/417/418/1005 + error-backfill skip, starved sweep), and the WDP completion model (High-confidence v2 seed viaenrollmentTypeDeterministic, desktop-based arm D, 30-mindevice_prep_completionbackstop, parked-tripwire WDP branch). -
Update:
backend/time-attribution.md— AttributionVersion 1 → 2: new duration-critical quality flagPriorEnrollmentResidue(fires onhistoric_ime_replay_detected; a re-enrollment without wipe pulls the phase anchors ahead of the real ESP page, so the segments are excluded from fleet stats;registry_app_baselinesuccesses deliberately not a trigger — by-design normal for WDP). Added the quality-flags section. -
Update:
agent/logs-and-persistence.md,agent/overview.md,agent/gather-rule-debug-log.md— log files renamed to unified kebab-case:agent.log(date rotation dropped; size segmentsagent-NNN.log),bootstrap-script.log(wasbootstrap_agent.log),bootstrap-msi.log(wasmsi-bootstrap.log),crash-*.log,gather-rules-debug.log,ime-pattern-matches.log. Old names still appear in archives from pre-rename agents. -
New:
agent/registry-app-state-pillar.md— the registry second pillar for IME app state (audit follow-up): snapshot-and-diff watcher onHKLM\...\IntuneManagementExtension(Win32Apps / EspTrackingWin32Apps / StatusServiceReports),registry_app_state+app_state_reconciliationevents, silent startup baseline, WDP reuse notes.
2026-08-17
- Update:
mcp/tool-telemetry.md— thetool_callstderr line now carrieserrorMessage(capped at 300 chars; thrown message or the leadingtoolErrortext, absent on success), so error drilldowns name the failure class instead of guessing from args. Same change:get_rule_statsomits the per-rule dailytrendrows by default (they drove 50% of calls over the response cap at platform scope);includeTrends=trueopts back in, the response carries atrendsNotemarker when stripped.
2026-08-14 (later)
- Update:
rules/analyze-rule-triggers.md— status SPEC → IMPLEMENTED. Shipped:evaluateOnon the analyze-rule schema (grammar-validated server-side and in validate_rule),AnalyzeRunContext-driven engine runs (Terminal/Interim/Reanalyze) with the RuleResult lifecycle columns (FirstDetectedAt/LastEvaluatedAt/IsInterim/ResolvedAt/NotifiedAt), interim rule filtering + KO/stat suppression, resolve/finalize update-semantics, the NotifiedAt one-send-per-(session,rule) notification dedupe (preserved across the manual reanalyze rebuild), the ingest-sideInterimTriggerRegistry(5-min TTL, fail-soft) gatinginterim_trigger/whiteglove_sealedenqueues, the three MCP authoring lints, web preliminary/resolved rendering, and ANALYZE-ID-004 as the first interim-enabled rule.
2026-08-14
- Creation: Added
rules/analyze-rule-triggers.md— SPEC for opt-inevaluateOninterim evaluation of analyze rules:enrollment_enddefault preserves today's behavior,whiteglove_sealed+on_event:<type>close the WhiteGlove notification gap and the never-terminal stuck-session gap; RuleResults update-semantics (FirstDetectedAt/IsInterim/ResolvedAt/NotifiedAt) replace the permanent dedupe freeze; interim runs suppress KO + stats, notifications fire on first detection; three new validate_rule lints (terminal-precondition trap, unknown trigger type, KO-on-interim).
2026-08-13 (late evening)
-
Creation: Added
drift-guards.md— the fragility-audit follow-up contracts (P5.x + P7): SignalR message-name catalog inConstants.SignalRMessages, exported through the shared manifest and typed into the web'sSignalRContext.on/offunion; theRowKeyCodecsingle encoder for inverted-tick RowKeys (persisted widths pinned, incl. UserActivity's legacy D20);DeletionRowDump.Tableon tombstone rows withDeletionTombstoneTables.Resolvereplacing the duplicatedContains('_')heuristic (legacy fallback retained one retention cycle); the shrink-onlyOkAsyncBaselineGuardTestsper-file baseline (44 sites frozen); the Settings per-section PATCH (section field map + diff-basedconfig/{tenantId}/fieldswrites on the TenantAdmin caller tier); and the web'sscopedApiendpoint-pair builders, single admin-scope hook,navVisibilitymatrix and guard-consistency ratchet. -
Update:
versioning.md— the web portal joins the version contract commit-only:public/version.jsongenerated at build time fromGITHUB_SHA, served withCache-Control: no-store, polled bydeploy-web.ymluntil the deployed commit matches beforeversions/web.jsonis published.
2026-08-13 (evening)
-
Creation: Added
backend/lifecycle-manifests-and-session-scope.md— the fragility-audit P5/P6 completeness nets: (1)TableLifecycleBucketTestsclassifies everyTableNamesconstant into offboarding-wipe / per-session-cascade / kept-by-design (fixed en route: UsageMetrics wiped via the new RowKey-anchored SafeWipe Variant D — the exact-PK wipe matched 0 rows; PlatformStats tenant partition + ScriptNameCache added to the tenant-PK bucket); (2)SessionIndexFieldManifestas the declared SessionsIndex column set with bidirectional builder/reader test pinning, Replace-mode full rebuilds, and a fail-soft merge-key guard; (3)ResolveSessionScopeAsyncas the single session-scope fallback (16 former copies), backed by the newSessionTenantLookuppoint-read table (scan fallback + self-heal, cascade step before the tombstone), plus theRecomputeTriggerGate403 for reanalyze/rescan by read-only roles; (4) server-side dashboard free-text search (q=) with page-backfill loop and fingerprint binding, replacing the client's full-history loadAll sweep. -
Creation: Added
backend/app-version-regression-radar.md— app-version duration regression radar: per (tenant, app) the newest version's median measured install duration (nearest-rank; population Succeeded + non-skip +HasMeasuredDuration+ no AppId collision + non-empty AppVersion) is compared against the previous version (first-seen ordering, never string sort); fires at median lift ≥2× AND ≥300s absolute with ≥10 measured installs on both sides. Episodes live asappversionregression|{app}|{version}tracker rows (one bellapp_version_duration_regression+ oneAppVersionDurationRegressionops event per episode, refresh keepsFirstNotifiedAt, re-arm on drain/median-recovery/30d retention). Surfaces:versionRegressions[]+ per-versionmeasuredInstalls/medianDurationSeconds/p95DurationSecondsin the apps analytics responses, amber banner + duration-by-version chart on the apps detail page. Kill switchAppVersionRegressionRadarDisabled.
2026-08-13
- Update:
backend/session-annotations.md— the/annotationsportal overview page now carries the standard tenant switcher (GA/Reader: "All tenants" aggregate + per-tenant drill-down viaGET global/session-annotations; delegated: managed subset), with a tenant column and tenant-qualified session deep links. The global list endpoint now derivesexcludeGlobalAdminLanefromHasGlobalScope, so delegated-rescue callers on their managed?tenantId=path never receive the platform-internalglobaladminlane (endpoint-test-pinned).
2026-08-12
- Update:
agent/autopilot-ztd-diagnostics.md— Windows 365 Cloud PC exception documented: on Cloud PCsProfileAvailable=0is expected (provisioned by the Windows 365 service, not Autopilot), soautopilot_profile_missingis emitted as Info with a Cloud-PC-specific message and anisCloudPcpayload marker (ResolveAutopilotProfileMissingPresentation); the portal's Autopilot Profile card renders a neutral note instead of the amber warning for Cloud PC sessions (session.isCloudPcor the event marker).
2026-08-11 (later)
- Creation: Added
backend/session-annotations.md— session annotations: the newSessionAnnotationstable (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-internalglobaladminlane cross-tenant; tenant callers never see that lane). Surfaces: portalSessionAnnotationsCard(session detail, after Analysis), MCPlist_session_annotations(ga) +annotate_session(strictGa, always the GA lane) +annotationskey inget_session_summary. Lifecycle: per-session cascade steps 17–19, offboarding wipe, critical-backup member.
2026-08-11
- Update:
rules/gather-rule-guardrails.md— hard-block mirroring closed across all pre-flight layers:rules/guardrails.jsonnow also mirrorsblockedCommandPatterns+maxCommandLengthand carriesC:\Windows\System32\configinblockedFilePrefixes; portal (guardValidation.ts) and MCP (rule-validation.ts) apply them in agent order (before the unrestricted-mode early-return), so a command the agent hard-blocks no longer shows "Allowed" in unrestricted tenants.GuardrailsParityTestspins the JSON mirror to the C# constants. MCPvalidate_ruleadditionally validatesjson/xmltargets with file semantics (the schema'scollectorTypeenum was missing both) and rejectsactivePhases+activeFromPhaseset together (backend 400 parity). Author stamping extended to PUT-upserts: tenant and global gather-rule updates stamp the caller's JWT display name before the service call, so an upsert of a non-existing ruleId can no longer store a payload-supplied author (true updates still preserve the original author).
2026-08-10
-
Update:
rules/gather-rule-guardrails.md— WMI matcher generalized from literal prefix matching to shape parsing:SELECT <* | property-list> FROM <class>is allowed when the class is derived from the existingSELECT * FROM <class>entries (a projection is a strict subset; motivated bySELECT BatteryStatus FROM Win32_Battery+on_changebattery polling thatSELECT *defeats via fluctuating charge %). Same algorithm mirrored agent/portal/MCP; guardrails.json unchanged. Portal now also gates enabling on the client-side validation (invalid-target custom rules create disabled, cannot be toggled on, force-disable on invalid edit, "Blocked on devices" badge) — UX only, the agent guard stays the boundary. Gather-rule Author is now stamped from the creator's JWT (display name → UPN) at create and immutable on update. -
New:
agent/continue-anyway-observation.md— Continue-Anyway observation mode (EnableEspContinueAnywayObservation, ConfigVersion 37, operator-set): Device-phase ESP terminal failures on Continue-Anyway profiles are defanged into a 60-min observation window (Classic/Unknown, non-pre-prov only); real-user desktop + Hello gate complete the session as Succeeded +EspSoftFailure/CompletionSource(amber "with issues" badge + detail banner), category recovery resolves clean, expiry un-defangs to the originalesp_terminal_failure. Registered in the Agent index section. Related backend fix (no doc): the auto-analyze enqueue now carries a 30 svisibilityTimeoutso rule evaluation no longer races the agent's terminal flush (ANALYZE-ESP-004's{{appName}}placeholder).
2026-08-07 (later)
- Update:
backend/cloudpc-device-validation.md— thirdIsCloudPctransport rail: the pre-auth distress channel.DistressReport.IsCloudPc(CloudPcDetector verdict captured atDistressReporterconstruction) →DistressReportEntry/table column → "Devices Not Registered" aggregation (sticky-true per serial, same OR semantics as the Sessions merge) → portal Cloud PC badge + actionable hint ("enable Windows 365 Cloud PC Validation + W365CloudPcValidation add-on"). Motivated by the first W365 field run: the rejected Cloud PC showed up as anonymous "Microsoft Corporation / Virtual Machine" with no clue that the fix is the Cloud PC validator, not Autopilot registration. UNVERIFIED like all distress fields; ≤18 bytes, fits the 1536-byte cap.
2026-08-07
- Update:
backend/cloudpc-device-validation.md— the "deliberately out of scope" follow-up landed in the same PR: agent-sideIsCloudPcflag (CloudPcDetector, bootstrap-verified marker AND: Windows365 key + CloudManagedDesktopExtension service key, SKIP-safe) rides two rails —SessionRegistration→ Sessions/Index (sticky-true) →SessionSummary+isCloudPcsearch filter (portal/raw/MCP), and theEnrollmentFactsObservedpayload →EnrollmentScenarioObservations.CloudPc(set-once, both values) with profile reasoncloud_pc_first_connect:no_device_esp_expected+cloud_pc_markerin the audit-trail census. Reason+observation only: no completion arm depends on DeviceSetup, Mode stays AccountSetup/IME-classified, EspConfig keeps configured-FirstSync semantics. Never an auth input.
2026-08-06
- Creation: Added
backend/cloudpc-device-validation.md— the CloudPc fallback validator (ValidatorType.CloudPc = 5, tenant flagValidateCloudPcDevice): W365 Cloud PCs are structurally never Autopilot-registered, so stage 4 gains a third stage that binds the chain-validated MDM cert's Subject CN (= Intune device id, field-verified on a real Cloud PC) to the tenant'svirtualEndpoint/cloudPCsinventory — admits exactly the service-provisioned Cloud PCs, no device-type routing, local W365 markers never consulted for auth. New optional Graph add-onW365CloudPcValidation→CloudPC.Read.All(catalog + grant script now pinned byGraphFeatureCatalogSyncTests; Graph 403 = definitive "grant missing", not transient). Bootstrap dev script v2.3-dev.3 exempts guard 4 (uptime) while the Cloud-PC relax is active — the fresh-profile window is the time anchor at first connect, not boot uptime. Follow-up deliberately out of scope: agent-sideIsCloudPcclassification + decision-engine "no Device ESP" expectation.
2026-08-05 (later)
- Update:
rules/rule-authoring-surface.md— new sectionPOST /api/rules/gather/test-pattern: the logparser pattern tester (agent-exact .NET matching: no IgnoreCase, first-match-per-line, cmtrace vs text mode, verbatim format=text hint), motivated by a field incident where a PHP-verified regex behaved differently under .NET.CmTraceLogParsermoved Agent →AutopilotMonitor.Shared.Loggingso agent and backend share the ONE parsing implementation (timestamp caveat: AssumeLocal = server TZ on backend, endpoint reports no timestamps). MCP: newtest_log_patterntool, logparser lint invalidate_rule, guide gains the case-sensitivity/engine-mismatch pitfalls.
2026-08-05
- Creation: Added
mcp/tool-telemetry.md— the three MCP tool-telemetry layers (App InsightsMcpToolName, per-user usage table,MCP_TOOL_LOGGINGstderr lines) and the new quality signals: soft-error capture (toolErrorreturns instead of throwing, so backend telemetry counted those calls as successes),resultChars/overCapvs. the inline-size hint, bounded args summaries, caller scope,search_zero_hiton the three search tools (unmet-demand signal), andtool_call_rejectedfor JSON-RPC-level Zod/unknown-tool rejections sniffed at the/mcproute (previously invisible everywhere). Bicep default fortoolLoggingEnabledflipped totrue(documented desired state; live flag set viaaz containerapp update). - Creation: Added
rules/rule-authoring-surface.md— the AI-assisted rule-authoring stack: three deterministic MCP resources (rule_authoring_guidehand-written,rule_schemas+rule_guardrailsgenerated intorule-authoring.generated.tsby a new combine.js emitter, drift-guarded twice), the MCP-localvalidate_rule(ajv 2020-12 against the single-rule $defs, guardrail pre-flight mirroring GatherRuleGuards matching semantics incl. hard blocks, semantic lint for schema-valid-but-dead rules), and the side-effect-freePOST /api/rules/analyze/dryrun(TenantAdminOrGlobalReader;RuleEngine.DryRun.cstrace twin sharing the production evaluators, all-conditions trace with per-condition evidence + matchingEventCount, parity and side-effect-freedom pinned by RuleEngineDryRunTests). MCPtest_analyze_rulewraps the endpoint with{{token}}-interpolated previews;author-ruleprompt drives the draft → validate → dry-run loop. Deliberately absent: gather-rule remote execution, rule writes via MCP.
2026-08-04
- Update:
rules/gather-rule-phase-scoping.md— new section "Which phase feeds the triggers":GatherRuleExecutorHostnow drivesphase_change/phase_exit/on_eventgather triggers (and the phase-scope gates) from the post-reduceTimelineEventStream(every emitted event's(eventType, phase)published byEventTimelineEmitter) instead of the pre-reduceSignalIngress.SignalPostedpayloads. Trigger: session32312a32(RealmJoin tenant rsneuffen.de) — a Phase-Start rule on FinalizingSetup fired at the raw ESP-exitEspPhaseChanged(FinalizingSetup)signal, 7 minutes before the engine's RealmJoin-gatedphase_transition(FinalizingSetup), readingHKLM\SOFTWARE\RealmJoin\Custom\BIOSbefore the RealmJoin package created it. Phase triggers now fire exactly when the timeline shows the phase;on_eventadditionally sees engine-emitted event types (enrollment_complete,realmjoin_resolved, …) for the first time in V2, restoring V1 semantics and the documented end-of-enrollment pattern. Other raw-signal hosts (DeviceInfo, keep-awake, PPKG) deliberately unchanged.
2026-08-02
- Creation: Added
backend/tenant-activation-welcome-mail.md— triggered by the first production auto-approve (activation at signup+64 s, notification address saved at +69 s → welcome mail silently skipped, since the approval path is the only sender and reads the address once). Fix documented in the doc: the notification-email save path now also triggers the send (PUT /api/preview/notification-email→ fresh approval checkIsApprovedFreshAsync, bypassing the 30 s negative cache), and a conditional-insertwelcome-email-sentmarker inPreviewWhitelistmakes the send once-only across both racing paths (marker consumed strictly after the address check; revoke clears it, offboarding wipes the partition, the GA resend endpoint sends unconditionally and consumes it best-effort).
2026-07-29
-
Update:
agent/gather-rule-debug-log.md— three trace enrichments: (1) newphasestage writing an enrollment-phase transition marker (phase change: Old -> New, ruleId-) so every rule-level line reads against the current phase; (2) logparser now writes one line per regex match — line number, matched text, capture groups — capped at 10 per file per run (the per-file summary keeps the full count); (3) the invalid-regexerrorline includes the offending pattern string. -
Update:
url-registry.md(+web/portal-navigation-prefetch.mdcontext) — the portal is now a full static export (output: 'export',trailingSlash: true): the SWA serves plain files, the managed SSR runtime — the portal's last cold-start source ("Backend call failure" > 45 s) — no longer exists. Detail routes moved from path params to query strings (/sessions?id=,/sessions/inspector?id=,/diagnosis?id=,/apps/detail?name=,/admin/backups/detail?id=,/admin/customs-archive/detail?tenantId=&rowKey=) with all 21 link sites going through the new single-definition builders inlib/routes.ts; the 9[section]families prerender viagenerateStaticParamsserver shells (37 pages,dynamicParams=false), serverredirect()index pages becameClientRedirectshells. Legacy PATH-shaped deep links (sent Teams/Slack/webhook notifications hold/sessions/{id}forever) are preserved permanently: SWA wildcard rewrites serve the family page and the globally mountedLegacyPathRedirectrewrites to the canonical query URL (hash + query preserved; mapping pinned bylib/__tests__/legacyPathRedirect.test.ts).middleware.tshost routing became the client-sideHostRoutingGuard(prefix list moved tolib/hostRouting.ts); the legacy/go/[code]route +bootstrapValidation.tsare deleted (BootstrapSessions table verified free of pre-cutover active sessions).staticwebapp.config.jsonnow carries the 19 docs/marketing redirects (order-sensitive), the legacy rewrites,404.htmloverride and ALL security headers incl. the literalized prod CSP — guarded by the newswaConfig.guard.test.ts; maintenance mode changed from whole-file swap to a first-routes-entry insert (README rewritten). Backend notification deep links go through the newConstants.PortalSessionUrl(query shape, pinned). Deploy:deploy-web.ymluploadsout/withis_static_export: true. -
Creation: Added
backend/oobe-bootstrap-go-flow.md(and aBootstrap go-URLsection + web-registry update inurl-registry.md) — the OOBE bootstrap script endpoint moved from the portal's Next.js/go/[code]SSR route into the Function App (GET /api/bootstrap/go/{code},GetBootstrapScriptFunction), unblocking the portal's static export; the customer URL becamego.autopilotmonitor.com/{code}(Front Door custom domain,/*route with origin-path rewrite, caching disabled — the script inlines the bearer token). NewConstants.BootstrapGoBaseUrl(single producer viaCreateBootstrapSessionFunction) + webBOOTSTRAP_GO_URL(display only); both guard host lists extended. Security invariants ported to C#:BootstrapScriptValueValidator(exact-GUID tenantId/token, https + host-allowlist +/agent/….zippath, expiry ≤ 14 d) andOobeBootstrapScriptGenerator(Replace-token raw-string template, single-quoted PS literals, 200-char error cap before quote-doubling; always-HTTP-200 text/plain contract forirm | iex). Endpoint isPublicAnonymousin the catalog,NoStoreCacheMiddleware-covered, self-rate-limitedbootstrap-script:{ip}20/min (rate-limit reply is also a 200 error script). Tests: golden byte-parity against the TS template, validator invariants incl. hostile-URL cases, format-gate table, and the previously missing BootstrapUrl shape pin. Transition: the legacy www route +middleware.ts/goprefix stay alive for pre-migration URLs (validity ≤ 168 h) and are removed in the static-export PR no earlier than 168 h after the last www URL was issued. Customer docsnetwork-endpoints.mdgained thego.host (device-facing, Bootstrap-Sessions-only). -
Update:
agent/autopilot-ztd-diagnostics.md— presentation layer for the startup error backfill (customer feedback: an unannounced wall of pre-agent Error events with a single identical timestamp read like a live failure). Agent: the backfill scan now buffers the capped record set, emits an Info summary event first (modern_deployment_log,backfillSummary: true+replayedCount/droppedOverCap/lookbackMinutes), then replays oldest-first so the timeline sequence follows event-log chronology. Web:EventTimeline.tsxbadgesbackfilled: trueevents („⟲ Backfilled", tooltip) and renders the originaltimeCreatedas the row time with a "(reported …)" suffix plus aRecordedmetadata line — works retroactively for already-stored events. Severity is deliberately not downgraded. -
Creation: Added
agent/gather-rule-debug-log.md— the new per-tenant settingEnableGatherRuleDebugLog(ConfigVersion 35 → 36, portal toggle under Settings → Agent,EnableImeMatchLog-style bool→path projection inRemoteConfigMerger). When enabled, the agent writes%ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log: one pipe-delimited line per evaluation decision across ten previously silent "produced nothing" exits — disabled rules,intervalwithoutintervalSeconds(never runs), the first-tick-after-one-full-interval window, phase-scope skips incl.currentPhase=Unknown, per-(rule, phase) dedup, empty collector results (emitOnlyIfExists),on_changehash suppression (hash prefix + streak), guard blocks, logparser per-file outcomes (position skips, parse failures with aformat=texthint when every line fails CMTrace parsing,maxLines/wildcard caps), and full exception stacks that the agent log drops. 10 MB cap + one.oldgeneration; no hot-reload (per-enrollment lifecycle); flag survives the offline config cache;--run-gather-rulesalways traces and echoes under--console; zero-rules delivery leaves a one-line file instead of nothing. Side fix:GatherRuleExecutorHostnow passes the logger intoInformationalEventPost, so gather-event emissions appear in the agent DEBUG log. -
Correction + Update:
web/portal-navigation-prefetch.md. The HTTP/1.1 claim in the 2026-07-27 entry below is wrong and has been struck from the document:portal.autopilotmonitor.comnegotiates h2, verified byopenssl s_client -alpn h2,http/1.1and a .NETHttpClientreturningHTTP/2.0. The false reading came from the Windows/Schannel curl build in this environment, which has no HTTP/2 support and therefore always reports1.1— do not use it to determine HTTP versions. There is no six-connection cap in play; the queue is server-side in the SWA SSR runtime, which does not change the fix or the evidence, only the mechanism. Added anOutcomesection with 20 h of post-change telemetry: the Free → Standard plan change (16:02:44 UTC) produced no measurable latency gain once stratified by the gap to the previous request (p50 Free → Standard:<10s85 → 95 ms,5-30min429 → 441 ms; tail rates 1.64 %/0.374 % over 2 s/10 s vs 6 and 1 events at n = 258 against 4.2 and 1.0 expected;browserTimingsMann-Whitney z = −0.24, p = 0.81). The freezes were removed byprefetch={false}(16:34:13 UTC): worstbrowserTimings.totalDuration225,277 → 4,856 ms. The prefetch storm was also an accidental keep-alive — requests under 10 s apart fell from 85 % to 46 %, and on the identical never-cacheable/sessions/{id}route the median rose 87 → 211 ms and p90 465 → 1,480 ms, with one 11,441 ms abandoned request (resultCode 0) after 517 minutes idle. Synthetic bursts warm the runtime and must not be used to benchmark this (round 1 p50 288 ms vs rounds 4–25 p50 113 ms).
2026-07-27
-
Creation: Added
web/portal-navigation-prefetch.md(and aWeb Portalsection to the index) — root cause of the minute-long portal freezes.GlobalSidebarandNavbarrender the whole navigation on every page, so the App Router's default viewport prefetch issued one?_rsc=request per visible link againstportal.autopilotmonitor.com(~10,400 in 14 days). (The original entry attributed the freeze to HTTP/1.1 and a six-connection-per-origin cap — see the 2026-07-28 correction; the queue is server-side.) When the Static Web Apps managed runtime queued the prefetches (p50 91 ms, p95 896 ms, but max 234 s / 21 samples over 30 s), the navigation the user wanted was stuck behind that queue. Measured on the 2026-07-27 17:25 CEST incident:/admin/presence?_rsc=151 s, three siblings finishing within 340 ms of each other at ~37.5 s, all200, while the backend API answered in 1.6–2.2 s andbrowserTimingsshowednetworkDuration16 ms againstprocessingDuration225 s. Every navigation Link now setsprefetch={false}— these routes are client components behindProtectedRoutethat fetch their own data, so the prefetched RSC payload carried no benefit. The doc also records that browser telemetry lives in a separate App Insights resource (AUTOPILOT_MONITOR_WEB_APPINSIGHTS_ID) that the MCPquery_backend_logstool cannot reach, which is why the freezes left no trace in the backend logs. -
Creation: Added
backend/rule-regression-radar.md— F3 regression radar (analyze rules only; gather rules wait for their per-batch stats dedup fix): per (tenant, rule) daily hit rate fromRuleStats, firing only when the trailing 7-day window has ≥5 hit sessions and ≥20 evaluated sessions, the rate is ≥2× the prior 28-day baseline AND the Wilson 95 % intervals are disjoint (deterministicMetricsMathprimitive with hand-computed test vectors). Suppressed for deleted rules, rules younger than baseline+window (CreatedAtgrace), rules edited inside the window (UpdatedAt), empty baselines, and theRuleRegressionRadarDisabledapp-setting kill switch. Alert episodes live asruleregression|{ruleId}tracker rows (one bell + oneRuleFrequencyRegressionops event per episode; numbers refreshed while active; re-armed under 1.5× baseline, on fire-stop, or by the 30-day tracker retention). On-fire dimension correlation over osBuild/model/agentVersion/imeVersion with the "correlated, not necessarily causal" wording contract. Surfaces:regressions[]in the rule-stats response (MCP inherits), the analyze-rules page's 30-day sparkline and regression badge. -
Creation: Added
backend/device-journeys.md— F2 device history / First-Time-Right: the device key (trim + case-fold serial, junk/placeholder exclusion incl. the agent'sUnknownWMI sentinel), journey grouping over the terminal set Succeeded/Failed/Incomplete (first success ends a journey, a >30-day gap from the previous attempt's CompletedAt starts one, WhiteGlove's single session row is ONE attempt, open sessions are never attempts), theDeviceHistorieschain (cap 20, written inline at the terminal seam, healed by the sweep, deleted-session refs pruned tombstone-driven, deliberately not in the per-session deletion manifest) and the additiveDeviceJourneyAggregatesdaily rows (window rate = ratio of sums — no rolling row needed, 180-day retention). Surfaces: the session-detail "Attempt N" banner with server-computed attempt numbers (virtual-attempt rule for live sessions), the Fleet Health First-time-right section (≥20-journey gate, attempt histogram, repeat-devices violator list), and the MCPget_device_historytool (serial mode / fleet FTR mode).
2026-07-25
- Update:
agent/secureboot-ca2023-detection.md— review follow-up on90012d9hardened the privilege scope, the repo's only privilege-adjustment code and therefore the precedent for any future one. The process token moved from a rawIntPtrto aSafeHandleassigned by the P/Invoke marshaler, so the CLR owns it from the momentOpenProcessTokenreturns and the critical finalizer releases it even whenDisposenever runs — previously an async exception between the successful open and the caller'susingleaked the handle with the privilege enabled and no owner left to restore it.Disposebecame idempotent and now releases the handle in afinally, so a throwing restore cannot strand it; the restore obligation is recorded immediately after the successfulAdjustTokenPrivilegeswith nothing throwable in between. The whole enable → read → restore sequence runs under a static gate because the privilege is adjusted on the process token: a concurrent reader would see "already enabled", correctly skip its own restore, and then lose the privilege mid-read when the first reader disposed, reportingprivilege_deniedon a machine that holds it.
2026-07-24
-
Creation: Added
backend/server-actions-on-demand-diagnostics.md— the ServerAction channel (queue onSessions.PendingActionsJson, deliver piggy-backed on the next ingest response, at-least-once withType|RuleId|QueuedAtdedup and sequential dispatch) and the on-demand log collection built on it: the portal's always-visible Collect Logs button, the newEndpointPolicy.TenantAdminOrOperatortier with the mandatory in-function per-type re-gate (Operators queue onlyrequest_diagnostics), the member-readablediagnosticsUploadConfiguredfeature flag, the admin quick-config path that must queuerotate_configbeforerequest_diagnostics(works against the deployed fleet — a one-time override would not, the mode gate is agent-side), sequence-based progress matching immune to agent clock skew, and the offboarding cascade's hosted-prefix sweep closing the latest-wins orphan-blob gap. -
Update:
agent/esp-policy-provider-stall.md— the customer's follow-up on issue #106 corrected the field-case mechanics:TrackingPoliciesCreated=1WAS set under the foreignConfigMgrprovider, so the ESP ignores a foreign provider's completion values and waits on the Intune-registeredSidecarprovider by name (the rename toSidecarcreates no tracking entries yet unblocks instantly). The completeness-keyed detector would have missed exactly this — the incident it was built for. The tripwire is now two-armed under one event type discriminated byreason:provider_incomplete(unchanged dwell on continuously-incomplete providers) andsidecar_provider_missing(≥ 1 Setup/Apps provider registered withoutSidecar, completeness deliberately ignored, clock reset when Sidecar registers later). Sidecar-missing wins thereasonwhen both arms cross in one round; the cross-restart fingerprint gains asidecarMissingsentinel; payload addsreasonandsidecarMissingForMinutes. Evidence for the name-keyed wait: Niehaus' documented Intune WAP payload always registersSidecar(ConfigMgr only via a co-management settings policy) and Microsoft's ESP troubleshooting models app tracking exclusively through the Sidecar subkey. -
Creation: Added
agent/esp-policy-provider-stall.md— a customer diagnosed that pre-provisioned devices with the ConfigMgr client as a blocking app occasionally park the user ESP at "Apps (Identifying)" for days: only aConfigMgrprovider is registered underEnrollmentStatusTracking\Device\Setup\Apps\PolicyProviders(noSidecar= IME) andTrackingPoliciesCreatednever becomes 1 — a wait the CSP defines without any timeout; renaming the key toSidecarunblocks immediately. The agent now ships the always-onesp_policy_provider_stalledtripwire (60-s tick, 15-min continuous-incompleteness dwell, one-shot per provider,StartupEventGatefingerprint across restarts, purely observational via the InformationalEvent rail — no toggle, no ConfigVersion bump), generalized to any provider in bothSetup/AppsandDevicePreparationkinds rather than hardcoding the ConfigMgr case. Companion change:SOFTWARE\Microsoft\Windows\Autopilotjoins the gather-rule registry allowlist so tenants can target the subtree with custom rules (e.g.not_existson the Sidecar key). -
Creation: Added
agent/secureboot-ca2023-detection.md— ANALYZE-SEC-001 warned on devices whose firmware already carried the 2023 certificate, because the agent only read theSecureBoot\Servicing\UEFICA2023Statusregistry value, which mirrors the Windows Update rollout rather than the firmware (customer report). The agent now reads the UEFIdb/KEKvariables natively (GetFirmwareEnvironmentVariableExWwithSeSystemEnvironmentPrivilegeenabled on the process token — first privilege-adjustment code in the repo) and emits per-certificate booleans onsecureboot_status. Suppression rides on a one-sided marker,uefiCa2023FirmwareConfirmed, emitted only on positive confirmation: the engine'snot_existsprecondition fails on any present value, so a two-sided boolean could not distinguish "confirmed" from "checked and missing", while the marker's absence keeps old agents and firmware-missing devices on the v2 path. Rule v3.0.0 is therefore safe to seed before the agent release. -
Update:
url-registry.md— the first release after the alias migration surfaced Front Door caching as a correctness hazard: the alias served the previousversion.json(X-Cache: TCP_REMOTE_HIT) after the origin had rotated, and since manifest and ZIP are independent cache entries, a stale manifest with a fresh ZIP fails the bootstrap SHA-256 check. Route caching on/agent/*is now disabled and every blob upload stampsCache-Control: no-cache(CISend-Blob, localbuild.ps1,versions/*.jsonmanifests) — all agent-container blobs rotate in place, so nothing there is safely cacheable. -
Update:
agent/decision-engine.md— the RealmJoin completion gate has a third terminal,FirstDeploymentIncomplete. Session 224b2087 showed why: an interactive logon 17 s after DeploymentPhase 101 makes RealmJoin reclassify the run as a secondary-user deployment, whose branch writes 200/210 and never 110 — the gate stayed closed until the 60-min hard timeout although Hello and desktop were long resolved.realmjoin_phase_changedis now a typedRealmJoinPhaseChangedsignal, soRealmJoinFacts.LastDeploymentPhaseis persisted across agent restarts (this also fixes everyrealmjoin_timeoutevent claiming "last phase: 0"); a transition out of 100/101 into 200/210 releases the gate with therealmjoin_first_deployment_incompleteWarning, and theRealmJoinDetectedreplay path evaluates the same rule for restarts that swallow the transition. A first observation of 210 deliberately does not release — session 6f1959c0 started at 210 and only deployed afterwards. -
Creation: Added
url-registry.md— all well-known hosts (own subdomains, both blob accounts, API origin, Graph, Entra login) are now defined once per component (Constants.cs, webutils/config.ts, MCPsrc/config.ts) and referenced everywhere else; three guard tests fail the build on any literal outside the registries. Written because the EU cutover missed two hardcoded copies of the legacy blob host —LatestVersionsService(feeds the portal's version badge) andValidateBootstrapCodeFunction— which kept working only through the fail-soft legacy mirror inbuild-agent.yml; those two plus the agentSelfUpdaternow use thedownload.autopilotmonitor.comalias (verified serving all four artifacts first). The portal's bootstrap allow-list accepts alias AND legacy host during the transition, which imposes a deploy order: web before (or with) backend. The sweep also surfaced a fourth copy of the www/portal host pair inmiddleware.ts(now derived from the registry viahostRouting.ts) and a backend short-link built on the bare apex domain, which only worked through a registrar-level 301.
2026-07-22
-
Creation: Added
versioning.md— agent, backend and MCP now share one version shape (<major>.<minor>.<build>+ 7-char commit + build timestamp) and one implementation of the build-number reservation (.github/scripts/Request-BuildNumber.ps1, previously copy-pasted betweenbuild-agent.ymlandbuild.ps1). The backend had been pinned at a hand-written1.0.0since inception and now starts at1.5.1000; the MCP server'spackage.jsonversion was bumped by hand and now starts at1.5.300, and it reports its own commit on/healthinstead of only the documentation bundle's. Both deploy workflows poll their health endpoint until it reports the build just deployed and publishversions/<component>.jsononly then, so a manifest that disagrees with the live endpoint means the instance drifted rather than the pipeline failing halfway.agent/build-counter-blob.mdkeeps the agent-specific parts and now points at the shared protocol; its counter URL was still the pre-cutover storage account. -
Creation: Added
rules/gather-rule-guardrails.md— the gather-rule allowlists are enforced only on the agent (the backend validates nocollectorType,target, orparameters), so a collector that forgets to call its guard is the vulnerability. Written because two had:LogParserCollectorread any path inrule.Target, bypassing even theC:\Usershard block that every other file collector honours, andEventLogCollectorread any channel includingSecurity— while the customer-facing documentation already promised both were allowlisted. Both now guard, withlogparserchecking the directory for wildcard targets (Path.GetFullPaththrows on*) and re-checking each resolved path. Added an event-log channel allowlist (eventLogChannels) plus hard-blocked channels held in C# rather than JSON, so a parse error cannot lift them. Also fixed the command timeout, which was inert:ReadToEnd()blocks until the pipe closes, soWaitForExit(30000)was only reached after the process had already exited and a hung command pinned its worker forever — now read asynchronously and killed at the timeout. -
Update: Added
.github/workflows/ci.yml— the full suite (backend, agent V2 Core, DecisionCore, SummaryDialog, web, MCP) now runs on every pull request and every push to main, plus a job that fails if the generated guardrail files drift fromrules/guardrails.json. Previously no workflow ran tests on a PR: the deploy workflows build without testing, andbuild-agent.ymltested onlyDecisionCore.Testson manual dispatch — whiletrust/security-faq.mdalready told customers that automated tests run in CI. Added CodeQL (C# and TypeScript) and Dependabot (nuget, npm ×2, actions). MarkedOfficeBinaryWatcherTests[Collection("SerialThreading")]: its bounded re-probe races the shared ThreadPool and failed once under full-suite load.
2026-07-21
-
Update:
search_docsandsearch_knowledgenow state in their tool descriptions that queries must be in English. Both corpora andall-MiniLM-L6-v2are English-only, and the failure is total rather than gradual — a German question returns zero results, not weak ones. The caller is a language model, so translating the question before the call and answering in the user's language costs nothing; a multilingual embedding model would be the alternative but is much larger and would have to be measured against the eval harness first, since it changes English retrieval too. -
Update: Added
scripts/eval-docs-search.ts, an offline retrieval evaluation harness (2465 queries in three classes: chunk headings as a regression detector, rare identifiers, and hand-written questions as the reality anchor), and used it to close a thirdsearch_docsgap found during the live test. The keyword fallback only fired when semantic search returned fewer thantopK; a pasted identifier likeWOW6432Nodeinstead returned three chunks at ~0.29 that did not contain the word while the one chunk that did was never considered. It now also fires on a weak semantic head (<0.35), letting exact matches take the front while keeping one semantic result. The threshold is measured, not guessed: 0.35 fixes 335 queries and breaks 0 (identifier recall 66.2% → 81.1%), while 0.40 fixes more but starts demoting hand-written questions. The measurement also resized the problem — identifier queries were only succeeding 66.2% of the time, so the reported case was one of roughly 750. -
Update: Closed the two measured recall gaps in
search_docs(mcp/docs-corpus.md). (1) Token windows — 87 of 324 chunks exceeded the model's 256-token context and 13% of all tokens never reached the ranking; a character budget cannot fix this (the corpus ranges 2.43–5.81 chars/token), so long text is now embedded as 250-token windows with 40 overlap and scored by its BEST window (embeddings: number[][], max not mean). Token coverage 87% → 100%; "proxy configuration blocking app downloads" went from absent to 0.559 at rank 1; the ten reference queries kept byte-identical scores. Costs +31% vectors, index 2.63 → 3.12 MB. The rules corpus gained the same treatment (128 rules → 172 vectors). (2) Keyword fallback — when the semantic pass returns fewer thantopK, results are topped up via literallexicalMatchon distinctive query terms, requiring two matching terms for corroboration (one incidental substring match returned a page about LLM providers for a sourdough query). Fuzzy matching was measured and rejected: at a threshold low enough to surface "RealmJoin" it invents 5 hits for "SCEPman", which the corpus never mentions. Also sharpened the tool description with the measured context argument for preferring it over fetching the docs site. -
Creation: Added
mcp/docs-corpus.md(newmcp/section) — the MCP server gainssearch_docs, a third semantic corpus built from the published customer documentation repository (autopilotmonitor-docs, checked out into the Docker build context by the deploy workflow). Documents the h2/h3 +<details>chunking with breadcrumb prefixes, the two deliberate size exceptions (tables never split, unbroken changelog lists split at line level), the CRLF precondition (.does not match\rin JS — headings silently stop matching, which produced 286 wrong-but-plausible chunks before it was found), the optionaldocssection insearch-index.jsonand its validation asymmetry, embedding rounding to 6 decimals, and the cross-repo coupling: a docs edit reaches clients only after an MCP redeploy, made checkable viaDOCS_COMMITon/health. Container resources raised to 0.5 vCPU / 1.0 GiB for the cold-start query-embedder load.
2026-07-20
-
Update:
rules/gather-rule-phase-scoping.md— the gather-rule form now shows the phase-scope and emit-mode controls only where they can change behavior (supportsPhaseScope/supportsEmitMode), nulls hidden-control state out of the payload, and rejects a scope mode selected but left unfilled. Offering all three "when" controls unconditionally allowed combinations that could only suppress a one-shot rule, plus a silent no-op state. -
Update: Extended
rules/gather-rule-phase-scoping.mdwith thephase_exittrigger — the closing bookend tophase_change, for one-shot collection when a phase is LEFT (e.g. "state at the end of Account Setup"). Documents the evaluation-before-phase-advance rule (triggerPhase match and scope gate see the phase being left), firing intoFailed, the disjointexit:dedup key space, and why no ConfigVersion bump is needed (no new field — old agents simply never fire the rule). -
Update: Reworked
agent/mdm-reboot-coalescing.mdafter the first real enrollment (session b2e890c1): 2800 records are now aggregated per burst into ONE neutral Info event (rebootUris[]/uriCount/firstRebootUri; watermark persisted at flush so a pre-emit reboot kill loses nothing); the raw event no longer claims a reboot — ANALYZE-ESP-005 is gated on an actually-observedsystem_reboot_detected. Removed the Shell-Coreesp_reboot_coalescingevent entirely: the "RebootCoalescing" token appears in the routineSubcategoryProcessing_Startedbootstrap marker on every enrollment (false positive by construction). Verified 2800 description shape recorded. -
Creation: Added
agent/mdm-reboot-coalescing.md— the agent now attributes the "unexpected mid-ESP reboot + second sign-in" pattern (PMPC research) to the device-assigned policy URIs that forced it: newMdmRebootPolicyTracker(DM-Enterprise EventID 2800, per-URImdm_policy_reboot_requiredevents, cross-restart RecordId watermark), aRebootCoalescingbranch inShellCoreTracker(esp_reboot_coalescing, backfill-emitting by design), and theANALYZE-ESP-005advisory (reassign device → user groups). ConfigVersion 35; event types deliberately distinct fromsystem_reboot_detectedso RebootCount stays uninflated. -
Creation: Added
backend/business-timestamps.md(newbackend/section) — the 2026-07-18 migration reset every row's system Timestamp, exposing that AuditLogs/OpsEvents/Events had (unknowingly) displayed and filtered on it all along; documents theOccurredUtccolumn, the RowKey decoders and RowKey-range date/retention clauses inBusinessTimestamp, the GUID-guard trap, the audit/ops backfill endpoint, and the "system Timestamp is never authoritative" migration rule. -
Creation: Added
rules/gather-rule-phase-scoping.md(newrules/section) — gather rules gainactivePhases(run only during listed phases),activeFromPhase(sticky latch from a phase onwards, never via Failed) andemitMode: "on_change"(canonical-hash result dedup withsuppressedPollsobservability), ConfigVersion 34. Includes the prerequisite backend fix: the custom-rule toggle PUT no longer wipes the stored rule definition (partial-merge inGatherRuleService.UpdateRuleAsync).
2026-07-18
- Creation: Added
agent/endpoint-migration.md— config-channel agent re-home (MigrateToApiBaseUrlonGET agent/config, ConfigVersion 33): backend or per-tenant API-URL moves without DNS indirection (impossible due to mTLS/Flex-TLS). Live-fetch-only with cache strip,AgentEndpointMigrationRulesallowlist validated on both sides, one hop, kill-switch wins. Built as the lesson from the 2026-07-18 WEU→GWC cutover, which hard-cut all in-flight agents.
2026-07-26
- Creation: Added
backend/time-attribution.md— the F1 enrollment time attribution: exact-partition invariant against the session's authoritative DurationSeconds, two-window WhiteGlove model, segment mapping from phase-declaration events, positive-evidence ESP-blocking join with "up to" what-if bounds, reboot spans from event gaps aroundlastBootUtc, and the SessionTimeBreakdowns / TimeAttributionAggregates tables with their terminal-seam + maintenance-sweep compute paths and read surfaces (web lane, fleet panel, MCPget_time_attribution).
2026-07-17
- Creation: Added
agent/build-counter-blob.md— agent build numbers now come from a shared public counter blob (buildcounter-v{N}.txt, reserve-before-build with ETag-CAS) so localbuild.ps1and CIbuild-agent.ymlbuilds can never mint the same number. Documents the CI parity fixes (SummaryDialog.exe.config in the ZIP, EXE-vs-manifest version check, hash-oracle update gated on stable cutover) and the Sigstore provenance attestation on CI-built ZIPs.
2026-07-16
- Creation: Added
agent/autopilot-ztd-diagnostics.md— reference for Windows' own ZTD diagnostic surfaces (ModernDeployment Autopilot event IDs incl. 807/809/815/908, theDiagnostics\Autopilotregistry key, deployment-service endpoints, known-issue error-code map with KB fixes). Backs the agent'sZtdEvidencecollector (ztdVerdictonautopilot_profile_missing) and the backend known-issue rules; sources carry a re-check RSS feed.
2026-07-13
-
Removal: Moved
customer-graph-add-on-permissions.mdout of this contributor bundle into the customer documentation repository (reference/optional-graph-permissions.md). It was customer-facing product content and did not belong in the tech-docs bundle; removed the entry fromindex.md. -
Creation: Added
agent/hello-wizard-unskip.md— session 772fe502 fix: theHelloWizardStarteddecision signal (prevention veto + un-skip cure for the policy-disabled Hello shortcut) and the HelloTracker confirmation second read against flip-flopping WHfB CSP values.
2026-07-11
-
Creation: Added the
agent/section — three concept documents on the V2 agent:agent/overview.md(runtime, collectors, telemetry pipeline),agent/decision-engine.md(DecisionCore reducer, signals, A/B/C completion arms, invariants),agent/logs-and-persistence.md(persisted artifacts, signal log vs spool, recovery/replay flow) plusagent/index.md. -
Creation: Converted
docs/into an OKF v0.1 knowledge bundle — added YAML frontmatter toarchitecture.md,mcp-oauth-flow.md, andcustomer-graph-add-on-permissions.md; createdindex.mdand this log.