mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
dd7ae299a8cb7a26de4c13dadfc7e92a2ce9967b
132 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dc45694611 |
docs(okf): email template overrides and image-free mail header
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8675e7ee06 |
feat(backend): provider-neutral EmailService on Mailchimp Transactional (Mandrill), replaces Resend
- EmailService + IEmailService: Mandrill messages/send via typed HttpClient with the Notification resilience policy; provider confined to one transport method and the Email:* settings (ApiKey, Endpoint, FromAddress, FromName). Resend NuGet dropped. - open/click tracking explicitly off; sent/queued/scheduled = ok, rejected/invalid = warn - callers (TenantApprovalService, PreviewWhitelistFunction) depend on IEmailService - EmailServiceTests pin wire format, result semantics and fail-soft contract - privacy page + OKF welcome-mail doc updated for the new sub-processor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
445ce4e75b |
feat(backend): 30-day retention grace period on Pro downgrade
Retention was the only enforcement point that destroyed state on a Pro -> Community downgrade: the 12-hourly sweep clamps to the Community cap at read time and would immediately start hard-deleting the 90-365-day band (trial expiry identical, silently). New policy: for 30 days after losing Pro (FeatureEntitlementCatalog.RetentionDowngradeGraceDays) the Pro cap keeps applying. Anchor model: ProDowngradedUtc (backend-only field, patch-denied + revert-protected) stamped/cleared by the plan endpoint's new pure ApplyPlanChanges core on EFFECTIVE-edition transitions (covers explicitly ended trials; a planTier downgrade under an active trial deliberately does not stamp); trial expiry anchors read-time via TrialExpiresUtc. days<=0 (infinite) stays untouched. Ops visibility: TenantPlanDowngraded (PATCH handler, new OpsEventService dep) plus TenantRetentionGraceExpiring/Ended from the daily TrialExpirySweep with the same stateless window mechanics, emitted only when stored retention exceeds the Community cap; all dual-registered in the web OPS_EVENT_TYPES catalog. Fanout now uses its _utcNow seam instead of inline DateTime.UtcNow. OKF doc backend/plan-downgrade-retention-grace.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
27fdd19568 |
feat(agent): optional Delivery Optimization GroupId from network fingerprint
EnableDoGroupIdAutoSet (ConfigVersion 40, default off): the agent seeds the DOGroupId policy value with a deterministic GUID derived from the default gateway's IPv4 + SendARP MAC, RealmJoin-compatible byte layout so mixed fleets peer together. Foreign DOGroupId/DOGroupIdSource policies always win (ownership tracked via StartupEventGate fingerprint); no random-GUID fallback. New do_group_id_auto_set event, portal toggle, admin report row, manifest/catalog sync, OKF doc. Gateway heuristic deduplicated into NetworkInterfaceLocator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
06cf1c4c12 |
feat(agent): system timeline watcher — clock steps + sleep episodes as ground truth
Two enrollment blind spots become timeline ground truth from the System event log via one EventLogWatcher (WindowsUpdateTracker pattern, 24h backfill with RecordId watermark recovering pre-agent records): - system_clock_changed (Kernel-General 1): oldTime/newTime/signed timeDeltaMs, reason and the setting process. 2s floor suppresses w32time 1ms micro-slews; steps >= 5min are Warning + immediate upload. Payload instants are the authoritative source (event timestamps can be clamped server-side). - system_sleep_episode (Power-Troubleshooter 1 classic sleep/hibernate, Kernel-Power 507 Modern Standby; 506 deliberately unconsumed): kind, enteredAt/exitedAt, durationSeconds, wake source / AC state. 60s real-sleep floor filters Modern Standby screen-off scenarios. ConfigVersion 39 (Collectors.EnableSystemTimelineWatcher + SystemEventBackfillLookbackMinutes). Both types are non-activity on agent and backend (idle/stall clocks unaffected; a sleeping device stays Stalled). Consumers: TimeJumpBadge gains the ground-truth cause "clock-set" (classifyTimeJump matches backward display steps against recorded clock sets); EventTimeline badge blocks for both events plus the previously bare power_state_change; SessionInfoCard keeps the wall-clock duration and adds a standby note; TimeAttributionCalculator builds SleepSpans (RebootSpan clone, AttributionVersion 3) persisted as SleepSeconds/SleepSpansJson and rendered in TimeAttributionLane; ANALYZE-DEV-012 (info, single) fires once per session for episodes >= 5min - deliberately the only new rule, clock findings stay with DEV-008 to avoid duplicate-finding flood. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ba2a390a83 |
fix(agent): battery-instance fallback for the dead Win32_PowerManagementEvent provider
On Windows 11 24H2/25H2 (observed on builds 26200 and 26220, session 161b838c) the MS_Power_Management_Event_Provider is still registered in the WMI repository but activating the extrinsic push query fails with WBEM_E_NOT_FOUND - the legacy provider no longer loads on exactly the OS builds new devices enroll with, so the power watcher degraded fleet-wide. Arm chain: try the push event first (still alive on older builds), then fall back to the intrinsic __InstanceModificationEvent WITHIN 30 on Win32_Battery. The WITHIN sampling runs inside WinMgmt, not in the agent; the handler path is unchanged (payload ignored, re-probe + diff), worst-case detection latency ~35s incl. debounce. collector_degraded now fires only when BOTH attempts fail; the arm log line names the active mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8f24876682 |
feat(agent): live power-state watcher + battery analyze rules DEV-009/010
Event-driven AC/battery observation during enrollment (no polling): a WMI Win32_PowerManagementEvent (EventCode 10) push subscription re-probes GetSystemPowerStatus and diffs snapshots through a pure tracker into power_state_change events - transition ac_to_battery (Warning, immediate) / battery_to_ac (Info) / threshold_crossed at 50/30/15% (per-level lifetime latch, lowest-crossed-only on jumps, arm-time baseline covers sessions starting low, 5s trailing-edge debounce, emission cap 20). Never armed on devices without a battery; arm failure surfaces as collector_degraded. Kill-switch Collectors.EnablePowerStateWatcher (ConfigVersion 38). power_state_change is classified non-activity on both sides (SignalActivityClassifier + IsPeriodicOrStallEvent) so battery drain can neither keep idle collectors alive nor heal a Stalled session. New analyze rules, both with on_event:power_state_change interim triggers and deliberately single-required-condition (event_data conditions scan events independently - no same-instance join): - ANALYZE-DEV-009 (high): thresholdPercent equals 15 - ANALYZE-DEV-010 (warning): transition equals ac_to_battery Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2d5ee887d7 |
fix(web): make rule JSON paste-import lossless in both rule editors
The JSON editors serialize the FORM object, but what users paste is usually a rule EXPORT (e.g. received from another tenant). The create/edit whitelists rebuilt collector parameters and evaluateOn from form fields the export does not carry, so a pasted gather rule silently lost every collector parameter and a pasted analyze rule silently reverted to the enrollment-end default. Both rules pages get a single shape-tolerant mapper in types.ts: - gatherRuleToForm unflattens the nested parameters object (authoritative when present, so edit-merge leftovers cannot leak), maps nullable rule fields to form defaults, imports a missing emitMode as 'always' (the rule's actual behavior) instead of the empty-form on_change default, and now also backs startEditing so edit and paste share one mapping. - analyze jsonToForm maps evaluateOn onto the eval* checkboxes (an explicit evaluateOn wins over stale form flags in edit-mode merges); terminal-only exports fall back to the enrollment-end default. - tags and the folded effective markSessionAsFailed now survive the import: both forms carry tags into the create/save payloads, analyze creates carry markSessionAsFailedDefault. notify/notifyChannelIds stay excluded — channel ids are tenant-specific. 9 new vitest cases pin both shapes, the merge precedence, and the round-trips; full web suite 824/824, tsc and eslint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
81c14bb3ed |
fix(rules): close the rule-ID namespace gaps around the shared global keyspace
Custom rules are tenant-partitioned and functionally collision-free, but three
surfaces still keyed on the bare non-unique ruleId string:
- RuleStats global aggregate (global_{ruleId}) summed same-ID custom rules of
unrelated tenants (title/severity last-writer-wins). The global row is now
catalog-only: analyze gates on IsBuiltIn/IsCommunity, gather gates on the
reserved-namespace pattern (agent events carry no flag; custom rules can
never occupy the reserved pattern).
- The 'never ship a built-in in the CUSTOM namespace' commitment from
RuleIdPolicy was documented but unenforced: combine.js now fails on
duplicate or non-reserved built-in IDs, BuiltInRuleCatalogPolicyTests pins
the same invariants on the embedded catalog, and the GitHub reseed rejects
non-reserved IDs before writing to the global partition.
- DeleteRuleStatesForRuleIdAcrossTenantsAsync (cross-partition delete by bare
RowKey) now refuses IDs outside the reserved namespace with failed=-1 so
callers abort the follow-up global catalog delete.
Verified: prod global partitions (45 analyze + 11 gather) are fully inside the
reserved namespace, so the guards touch no existing data. New OKF doc
docs/rules/rule-id-namespace.md captures the identity/collision model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
50e7590060 |
feat: app install durations measure the status-defining attempt
The IME processes the ESP app list in multiple passes (device-ESP evaluation ending Skipped, real install up to an hour later), and AppInstallSummary folded first observation -> last terminal across them: a 23-second app late in the queue reported 4002 s (session 44862cd3, "Set TimeZone"). Backend ingest: new max-folded LastAttemptStartedAt anchors DurationSeconds (CompletedAt - last attempt start; legacy rows without the anchor keep the span fallback), replay-guarded additive InstallPassCount, attempt-scoped DownloadDurationSeconds pairing, and a weaker-terminal guard (completed with state Skipped/Postponed never overrides Installed/Error, in-batch and in the cross-batch reconcile). StartedAt stays the earliest-wins window/version- ordering key. AppsDashboardProjection carries the new columns. AppVersionRegressionRadar: cutover gate — only attempt-anchored rows enter the duration population on BOTH comparison sides, so mixed-semantics medians can neither mask real regressions nor falsely re-arm active episodes. Time attribution: per-pass segment pairing replaces the first-start -> last-terminal hull. An app's seconds = sum of its ACTIVE segments (failed attempts and evaluation passes count; inter-pass idle gaps don't), occupancy merges the segments, hull endpoints remain for display and what-if bounds. Agent: a terminal->active transition re-arms BOTH timing endpoints, so event payload durationSeconds also describes the attempt. Web/PDF/MCP: labels and tooltips say "install time (final attempt)", the apps sessions table gets a xN multi-pass chip, the time-series chart carries a cutover footnote, MCP tool descriptions document the semantics split between get_app_install_metrics and get_time_attribution. No backfill: pre-cutover rows keep span values (documented in the public changelog). OKF doc: docs/backend/app-install-duration-attempt.md. Suites: Functions 4383, Agent 2407, MCP 564, Web 814 - all green, build 0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2e20476559 |
feat(agent): extend RealmJoin timeout while first deployment is demonstrably active
The 60-min RealmJoinTimeout deadline arms at RJ detection (the RJ agent MSI install during DeviceSetup) and used to cut off monitoring mid-install: report 55e6afd61c9d (session 46a96078) lost the tail of a large package catalog whose last observed package completed 3 seconds before the deadline, with the first deployment only starting 16 minutes into the window. When the deadline now fires with LastDeploymentPhase 100/101 and deployment activity (phase change or package observation, persisted restart-safe in RealmJoinFacts.LastActivityUtc; the detection-time phase capture deliberately does not count) inside the last 60 min, the handler re-arms to lastActivity + 60 min instead of timing out — Info event realmjoin_timeout_extended — clamped at an absolute cap of detection + 4 h. realmjoin_timeout carries a reason payload (hard_timeout / inactivity / absolute_cap); a fire of a deadline incarnation superseded by a re-arm dead-ends via the OccurredAtUtc == DueAtUtc scheduler contract. The idle case (no activity ever) still times out at 60 min, and phase-200/210 activity never extends. Packages are only registry-visible at completion, so the inactivity window keeps the full original 60 min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
990641b19e |
feat: P14 - per-upload-batch send time separates spool delay from clock offset
Agent stamps X-Send-Time-Utc (device clock, ISO-8601 round-trip) per upload ATTEMPT; ingest parses it (year>=2000 guard, deliberately no upper clamp) and server-stamps SentAt onto every event of the request. SentAt - OccurredUtc is pure spool delay, ReceivedAt - SentAt is network + device-vs-server clock offset - previously indistinguishable and only separable via median statistics. clock_skew condition: batches carrying SentAt measure the frame directly (spool-immune by construction - no spread cap, IME/clamped exclusions irrelevant, IME-only sessions become measurable; evidence sentAtBatchCount); legacy batches keep the per-event median path unchanged. CmTraceSkewTripwire deliberately untouched: its differential median math cancels clock offset as common mode. Full persistence chain: Store x2 + Map, fields= projection (sentAt), MCP get_session_events field list + rule-authoring-guide. New OKF doc backend/send-time-frame-separation.md. Suites: Functions 4370 (+16), Agent 2407 (+1), MCP vitest 564 + tsc 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ab61839685 |
feat(backend): P6 split - CMTrace skew tripwire + customer clock_skew rule
Operator side: CmTraceSkewTripwire piggybacks on the terminal counter
reconcile's partition scan (zero extra reads) and compares median
ReceivedAt-OccurredUtc deltas of IME-derived vs other events. A clean
15-min-grid divergence (residual < 2 min, >=20 samples / >=3 distinct
batches per side) emits the CmTraceTimeSkewRegression ops event
(Agent/Warning, goal: never fires). Bias-dominated sessions are
suppressed; measuredWriterOffsetMinutes is never consulted (sticky
after era flip-backs). Kill switch: CmTraceSkewTripwireDisabled.
Customer side: new clock_skew analyze condition source (skewMetric
clock_jump | sustained_offset, threshold seconds, gt/gte magnitude)
behind built-in ANALYZE-DEV-008 (300 s = the Kerberos/Entra 5-min skew
boundary). Batch-median frames with spool-spread filter, plateau step
detection (a gradual backlog-drain ramp cannot fake a jump) and
end-state persistence; IME-derived events are excluded so a CMTrace
anchoring regression can never fire the customer rule. Evidence carries
field=skewSummary/value for {{skewSummary}} interpolation.
Full mirroring chain: schema enum + skewMetric property, shared
manifest + combine.js regeneration, MCP authoring guide + semantic
lint, web SOURCES/form/card, ops-event dual-register, docs. P5 is
subsumed by the tripwire. PercentileMath is now the single nearest-rank
implementation (Apps/PlatformMetrics delegate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9d677db2a8 |
docs(okf): CMTrace time resolution — per-line self-anchoring concept
Why the reader-zone assumption and the per-file offset application both failed, the per-line self-anchoring rule with its strict freshness contract (LastCheckedUtc on every poll look, 30 s window, no freshness across restarts), the provenance vocabulary that keeps every event re-derivable, and the documented limits: the bounded grid-replay edge, snapshot readers staying on the marked fallback, and the still-open e9753578 label/anchor crossing with its tripwire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
022cc0354b |
fix(engine): let the observed user-ESP skip stand in for the exit evidence
The five stuck sits-d Cloud PCs carried SkipUserStatusPage=True — verified across three agent processes (10+ registry reads, True every time) and six hours of live Shell-Core watching with zero 62407: Windows never rendered a user ESP page, so no exit and no AccountSetup provisioning registry can ever exist on this flow. Arm B of ShouldTransitionToAwaitingHello was open from the bootstrap EspConfigDetected onward, but every evaluation site hangs off a signal the flow cannot produce, and the AdvisoryCompletion backstop arms exclusively from an EspExiting signal — an open gate nobody knocks on, six hours to the max-lifetime watchdog. The earlier lost-exit-through-reboot diagnosis is retired by the same evidence. HandleImeUserSessionCompletedV1's completion attempt now accepts the observed skip in place of IsPostAccountSetupFinalExit (anchor + genuine IME user-session completion + real-user desktop stay mandatory; Device Preparation excluded). The knock deliberately lives on the IME edge, not on desktop arrival: Fix 10's AccountSetup bounce-back would undo a desktop-side promotion two signals later, while the IME phase line always precedes the user-session-complete line in log order, so a bounced promotion is re-knocked by the very next re-emission — restart re-parse included. Docs and comments no longer claim an ESP-exit recovery that does not exist (stray duplicate summary in ShellCoreTracker, ImeLogHost restore-callback comment); the OKF doc is reframed on the evidence and names the residual SkipUser=false exit-in-downtime gap honestly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3aa8b95676 |
fix(agent): narrow the Shell-Core replay to the Hello-wizard rail
Round-four review, confirmed — and the cause sits a layer below where I had been fixing. The IsBackfill return added last round only guarded the coordinator's own completion logic. Upstream of it, ShellCoreTracker still raised FinalizingSetupPhaseTriggered, the coordinator still forwarded EspExited to the adapter before that return, and the adapter discards the args entirely (OnEspExited(...) => EmitEspExiting()). So a replayed exit still arrived at the reducer as an ordinary, unmarked EspExiting. There it is decisive: HandleEspExitingV1 passes espFinalExitInFlight: true for every arriving exit, and IsPostAccountSetupFinalExit orders exits by ingest ordinal rather than timestamp — so a historic exit replayed today reads as post-AccountSetup by construction. With state restored from the snapshot (AccountSetup entered, a genuine IME user session, desktop arrived) arm C of ShouldTransitionToAwaitingHello opens on the Device→Account handoff. The reducer is itself a completion gate and cannot tell the two apart, so the fix has to be upstream. The replay is therefore narrowed to what is provably safe: the Hello-wizard start (62404). That fact is conservative — it vetoes a premature "Hello is disabled" skip and can never by itself complete a session, so replaying it can only make the agent wait longer. It is also the observation session 772fe502 wrote the replay for. A 62407 is no longer raised at all, neither exit nor failure: the exit because Windows writes the identical description for the intermediate and the final one and nothing downstream can order it, the failure because re-injecting it as fresh can fail a session that recovered on retry (ANALYZE-ESP-006). Skipped records are reported once as an agent_trace with counts and bounds — silently dropping evidence is how this class of bug survives. The cross-layer proof is now a pair, as the review asked: ClassicEspExitingOnRestoredStateTests pins that the reducer opens on any arriving exit (including one with an hour-old source timestamp), and ShellCoreTrackerReplayScopeTests pins that the replay never produces one. Dead machinery removed with it: EspExitedEventArgs.IsBackfill, allowEspExit, the newest-exit selection, the IsBackfill return. The method is now called BackfillRecentHelloWizardStart. One pre-existing integration test drove EspExited through HandleBackfillRecord because the replay is easier to call than the event-log watcher, and its class doc claimed that was the production path — it never was, the replay had no caller when that test was written. Switched to the live path and the doc corrected. Agent V2.Core 2361, DecisionCore 538, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ec945a93d2 |
fix(agent): bar replayed ESP exits from the completion gate entirely
Round-three review, confirmed. The previous round stopped a replayed exit from arming the DEFERRED re-check but left the immediate synthesis attempt right below it untouched. My justification for that — "pre-existing behaviour" — was simply wrong: the immediate attempt on a replayed exit never existed, because the backfill had no caller. I introduced it, and by moving the replay behind the IME state restore I made the dangerous state reachable by design: if the newest 62407 in the window is only the intermediate DeviceSetup→AccountSetup exit while the restored app states are already terminal, that attempt opens the strong AccountSetup gate before the final exit has even happened. A test was pinning the faulty assumption as expected behaviour. A replayed exit now returns before touching completion at all — no deferred edge, no immediate attempt. Nothing available to it can establish its position in time: Shell-Core writes the identical description for both exits, and every probe that could order it reads state as it is now rather than at the event's time. The replay keeps the job it was written for in session 772fe502 — feeding EspExiting, FinalizingSetup and the Hello wizard to the reducer for the window in which no agent process was observing. Completion is the exclusive business of exits this agent watched live. With the synthesis attempt gone, the replay no longer needs restored app states, so it moves back into EspAndHelloHost.Start(); that also keeps this host's startup independent of the IME host's. ImeLogHost.onStateRestored goes back to doing just the re-check, which is still needed for a live exit observed before the restore finishes. The test asserting the faulty assumption is inverted, plus coverage that the replay is not neutered (EspExiting still forwards) and that a live exit following a replayed one completes normally. Agent V2.Core 2364, DecisionCore 533, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b36cf8ff36 |
fix(agent): order the ESP-exit replay by evidence, size its window by measurement
Round-two review, both findings confirmed against the code. A replayed exit was being confirmed as post-AccountSetup by reading the registry at replay time. That read reflects NOW, not the event's time, so an agent that was down across the DeviceSetup→AccountSetup transition would confirm the stale intermediate exit and, once the apps settled, synthesise a completion while the AccountSetup page was still up. Windows offers no way to tell the two apart from the event itself — Shell-Core writes the identical description CommercialOOBE_ESPProgress_Page_Exiting for both. So the deferred edge is now armed by LIVE exits only (EspExitedEventArgs.IsBackfill): only for those is the continuous-observation argument valid. A replayed exit keeps the single immediate synthesis attempt it always had. For that attempt to be worth anything it must see real data, so the replay moved out of EspAndHelloHost.Start() into ReplayEspExitBackfill(), invoked from ImeLogHost.onStateRestored — after LoadState() has restored the package states. That is what makes "the agent came back long after the ESP finished" recoverable. The five-minute window for non-reboot restarts was unsound: the scheduled task carries a BootTrigger only, with no restart-on-failure, so after an exception_crash the agent does not return until the next boot — hours later, long after Windows wrote the final exit. The window is now measured rather than guessed: back to the last moment the previous run is known to have been alive, taking the wider of the snapshot mtime (rewritten every decision step, available for every exit type) and LastBootUtc. The five-minute default is the floor, ClampLookbackMinutes the ceiling, and a future timestamp from clock skew is ignored instead of producing a negative window. Also switched the backfill tests to the real Shell-Core wording — the invented string matched the regex but hid the fact that both exits look identical. 9 new tests. Agent V2.Core 2362, DecisionCore 533, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c6ee7808f1 |
fix(agent): scope the ESP-exit replay to restart recovery only
Follow-up to the review hardening: the replay ran on EVERY agent start with a 5-minute window, first run included. On a first run there is no gap to recover — no earlier agent process existed — and a 62407 already sitting in the Shell-Core log belongs to a phase transition this agent was never meant to observe. Replaying it pushes an EspExiting into the reducer that the pre-fix agent never saw, i.e. a happy-path behaviour change for no gain. ResolveEspExitBackfillLookbackMinutes now keys the window to the previous exit type: first_run (or unknown) disables the replay entirely, clean/hard_kill/ exception_crash keep the 5-minute default because the process is back within seconds, and reboot_kill widens to "since LastBootUtc" because that gap spans the forced restart until the post-reboot logon. The result is still clamped to BackfillLookbackMaxMinutes, so policy and backstop together cannot build an unbounded event-log query. With this the happy path is byte-identical to pre-fix behaviour: on a first run nothing replays, no edge is remembered, and the deferred re-check returns on a volatile bool read. No other backfill is affected — the Hello, MDM-reboot, Windows-Update and ModernDeployment trackers each own a private backfill invoked from their own Start(), none of which this change touches. 13 policy tests including clock skew across the reboot, a missing boot timestamp and the clamp backstop. Agent V2.Core 2353, DecisionCore 533, Backend 4287. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b61d7023bc |
fix(agent): harden post-reboot completion recovery after review
Three real regressions, all confirmed against the code before touching it — two of them introduced by the recovery itself. The remembered exit edge was unqualified. Shell-Core raises 62407 at every ESP phase transition, so OnEspExited also latched the DeviceSetup→AccountSetup one; the deferred re-check would then open the strong AccountSetup gate as soon as the last user app settled, while the AccountSetup page was still up and its other subcategories still running. IsConfirmedPostAccountSetupExit() now demands positive evidence (AccountSetup activity observed, or SkipUser == true) and is deliberately stricter than the existing IsIntermediateDeviceEspExit() forward guard: an unknown SkipUser counts as not confirmed. Erring strict costs the pre-fix stall; erring loose costs a premature Succeeded. The immediate on-edge synthesis keeps its previous behaviour verbatim. The replay picked the oldest exit, not the final one. The reader walks oldest-first and the exit branch is fire-once, so the widened downtime window — which routinely holds both the intermediate and the final exit — handed over the intermediate one and the fire-once guard swallowed the exit that matters. ReplayBackfillRecords buffers the batch and lets only the newest exit-matching record through; ESP failures and the Hello-wizard rail keep replaying chronologically. The replay ran before the IME state restore. ImeLogTracker.Start() restores the persisted package states via LoadState() without raising OnAppStateChanged, and ImeLogHost starts after EspAndHelloHost, so the replay evaluated an empty app list — leaving the sharpest case unfixed: apps already terminal before the reboot, only the final exit lost to the downtime. ImeLogHost gained an onStateRestored callback wired to the re-check, and the host order is now pinned by DefaultComponentFactoryOrderingTests so a reshuffle fails loudly. 11 new tests. Agent V2.Core 2340, DecisionCore 533, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e1b03de5b3 |
fix(agent): recover ESP completion after a forced mid-ESP reboot
Seven sits-d Cloud PCs enrolled the same day with identical configuration (~138 required apps at the user-ESP gate, DeviceGuard/DmaGuard device-assigned). The two that finished AccountSetup before the coalesced reboot landed succeeded; the five the reboot killed in Stage=EspAccountSetup never derived a completion again and ended Incomplete on healthy devices. Two defects behind that: ShellCoreTracker.BackfillRecentEspExitEvents() has existed since session 772fe502 but no caller ever invoked it, so the Shell-Core watcher only saw records written after Start(). Every agent restart silently dropped an ESP exit (62407) or Hello-wizard start (62404) from its downtime. EspAndHelloHost.Start() now calls it, and the fixed 5-minute window is sized to the actual downtime when the previous run died to a reboot (LastBootUtc, clamped to 360 min). The user-apps-settled synthesis ran only on the EspExited edge. When the ESP page exits with apps still in flight the single attempt misses and nothing re-checks — 8110e262 and a89aac2d both reached 138/138 apps, 0 failed, and still never completed. The exit edge is now recorded and the synthesis re-evaluated from ImeLogTracker.OnAppStateChanged. No new completion path: the gate conditions are unchanged, this only gives the existing conservative check a second look. The fire-once claim moves to Interlocked (edge and re-check run on different threads) and the re-check does not re-emit app_install_starved. Rules: ANALYZE-ESP-005 gains on_event:session_stalled so a hung session produces a finding at the 60-minute stall instead of staying finding-less for hours (verified against 8110e262 — fired, confidence 95). New ANALYZE-APP-016 turns the already-emitted app_install_starved events into a customer finding and, unlike ANALYZE-APP-015, fires even on a succeeded session: 3b9291aa was recorded Succeeded with 99 required apps that never started installing. ANALYZE-APP-015 and ANALYZE-ESP-001 were considered for the stall trigger and deliberately left alone — APP-015 is the documented worked example of the terminal-precondition trap, and ESP-001's phase_duration gate on a still-open phase is unverified mid-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c0be389a95 |
ci(scripts): version-bump guard compares behaviour, not bytes
Byte comparison made the guard fire on comment fixes, typos in comments and
reflow -- changes no consumer of the version can act on, so forcing a bump for
them is friction without a payoff.
The guard now compares a code fingerprint: the PowerShell token stream with
comments, newlines and line continuations removed. A behaviour change under an
unchanged $ScriptVersion still fails; comment and formatting changes publish
with a warning. That is also the rule the source file's own comment on
$ScriptVersion already states ("bump on meaningful changes").
Tokenising rather than regex-stripping keeps a '#' inside a string literal from
being mistaken for a comment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
91f9d656ae |
ci(scripts): publish customer scripts on push, decoupled from the agent release
The scripts behind download.autopilotmonitor.com/agent/ rotated only on a publish_as_stable run of build-agent.yml, so a bootstrap fix sat unpublished until the next agent cutover -- while the WDP bootstrap MSI downloads the published copy on every enrolling device. Publish-BootstrapScripts.ps1 becomes the single owner of the chain: dev render with anchor check, $ScriptVersion bump guard, uploads (eu + fail-soft legacy mirror, no-cache), the two version oracles, and verification through the alias. publish-scripts.yml calls it on every push to main touching the sources; the publish_as_stable branch of build-agent.yml calls the same code, so a release still lands a consistent set. Blobs are published in a deterministic CRLF/UTF-8-without-BOM form. Git normalises line endings on checkout, so raw working-copy bytes would differ per environment and make every byte comparison -- guard and verify alike -- depend on where the workflow ran. The form reproduces the currently published bytes exactly. The Pester (PS 5.1) and ASCII gates move into the reusable bootstrap-script-gates.yml and now block the upload; pester.yml keeps the pull-request view and drops its push trigger. AdminConfiguration .LatestBootstrapV2ScriptVersion gets exactly one writer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a369ff99cd |
feat(wdp): scenario gates for Device Preparation + completion backstop
afee7ae0 audit: WDP sessions ran the full Classic/ESP apparatus. Gates key on the deterministic BootstrapperAgent ExecutionContext marker only (CloudAssigned* fallbacks keep Classic behavior). Paket A (event hygiene): no autopilot_profile/-missing + ZTD query/probe on WDP; esp_config_detected null-gate (no more unknown/unknown); starved sweep skipped on WDP (data stays in app_tracking_summary/final-status). Paket B (resources): EspPolicyProviderStallHost + ProvisioningStatusTracker not started on WDP; ModernDeployment noise IDs 100/417/418/1005 fully suppressed (Critical always passes) + Autopilot error backfill skipped; StallProbe skips ESP category scan and treats noise IDs as harmless. Paket C (DecisionCore): enrollmentTypeDeterministic payload key seeds DevicePreparation at High confidence (fallbacks stay Medium); completion_waiting drops the unsatisfiable account_setup_provisioning_complete gate on WDP; arm D promotes on real-user desktop; new device_prep_completion 30-min backstop closes the park-until-watchdog hole; parked tripwire gets a WDP branch. 17 new tests; suites green (532/2307/4287). OKF doc agent/wdp-scenario-gates.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3629728d93 |
feat(time-attribution): PriorEnrollmentResidue quality flag (v2) + WG-signal comment fix
Session f475e697: a device re-enrolled without a wipe (IME log residue from a previous enrollment) completes its pre-installed apps as instant detections, pulling the phase anchors ~6.5 min ahead of the real ESP page — the breakdown was mechanically exact but semantically wrong, and entered fleet stats as clean. - New duration-critical flag PriorEnrollmentResidue, set on historic_ime_replay_detected; excluded from fleet segment stats via the existing DurationCriticalFlags mask. registry_app_baseline successes are deliberately NOT a trigger (by-design normal for WDP Batch-1) — pinned by test. - AttributionVersion 1 -> 2 so the maintenance sweep recomputes in-retention breakdowns and flags old residue sessions retroactively (rule 8). - Web: flag label in TimeAttributionLane; docs: quality-flags section. - Stale comment fix: SaveWhiteGloveSuccessResult is NOT a WhiteGlove indicator on Win11 25H2+ (validated 2026-08-18 against user-driven sessions) — observability only, consumed nowhere for classification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6ebdfa41af |
refactor(agent): unified kebab-case log file naming + date-less agent.log
The agent log directory mixed three conventions (agent_20260817.log, bootstrap_agent.log, msi-bootstrap.log) while everything else in the diagnostics ZIP is already kebab-case. All log files now follow <component>-<content>.log: agent.log (was agent_YYYYMMDD.log; segments agent-NNN.log) bootstrap-script.log (was bootstrap_agent.log; script v2.4) bootstrap-msi.log (was msi-bootstrap.log) crash-<utc>.log (was crash_<utc>.log) gather-rules-debug.log (was gather_rules_debug.log) ime-pattern-matches.log (was ime_pattern_matches.log) The date is dropped from the main log: the agent only lives for one enrollment, and date rotation split the chronology at local midnight - which can fall mid-enrollment while the clock is still on the OOBE default timezone. The existing 50MB size rotation stays as the growth bound. Legacy dated files are ignored by the rotation probe; crash detection still picks up legacy crash_*.log once so an in-place self-update cannot lose a pending crash classification. Diag collectors glob *.log and need no change. MCP zip-map and docs note the legacy names for archives from pre-rename agents. Settings UI hint paths updated. MSI rebuild + bootstrap script publish (v2.4) + MCP redeploy required to ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c05ea4d63b |
Registry second pillar: IME app state from HKLM with log reconciliation
P2 of the 2026-08-17 IME code audit. IME log wording is an unstable contract (audit found 9 silently dead patterns, reworded lines, quote drift, [DO TEL] removal); the registry is IME's internal product contract - the ESP page and Intune reporting consume it. ImeRegistryAppStateObserver + ImeRegistryAppStateHost (always-on observability, no DecisionEngine coupling): - one recursive RegistryWatcher on HKLM\...\IntuneManagementExtension covers Win32Apps (EnforcementStateMessage JSON, ExitCode, Intent), EspTrackingWin32Apps and SideCarPolicies\StatusServiceReports; 2 s debounced snapshot-and-diff + 60 s periodic fallback tick (poll-only degraded mode if the watcher fails to arm) - silent startup baseline: Win32Apps keys survive re-enrollments, pre-existing state never replays as fresh events - registry_app_state (Info, state-change-only, 200/session cap with one-shot cap notice) - app_state_reconciliation (Warning, once per app, immediate upload): a changed entry terminal for >= 90 s is compared against the log-derived AppPackageState - divergence is the built-in log-pattern drift alarm (registry = truth, logs = narrative) Event types dual-registered (Constants + MCP resource-catalog + shared manifests regenerated). OKF concept doc agent/registry-app-state-pillar.md incl. WDP Paket-3 reuse notes. Tests: pure parse/diff/divergence logic + tick flow via snapshot override (baseline silence, emission, dwell, fire-once, cap). All suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
57775d6ba2 |
mcp: errorMessage in tool_call telemetry + get_rule_stats trend opt-in
Telemetry follow-ups from the 7d tool-quality review: - tool_call log lines now carry a capped errorMessage (thrown message or leading toolError text) so error drilldowns name the failure class instead of guessing from args; absent on success. - get_rule_stats omits per-rule daily trend rows by default (they drove 50% of calls over the response cap at platform scope); new includeTrends arg opts back in, trendsNote marks the omission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22a8c7a51d |
rules: hard-block high-frequency event types as evaluateOn on_event triggers
A warning does not stop an end user — telemetry-cadence types (performance_snapshot, agent_metrics_snapshot, download_progress, network_state_change/connectivity_check, log_entry, agent_trace, stall_probe_check) would turn every ingest batch into an analyze run. Now enforced in three layers: - AnalyzeRuleTriggers.BlockedOnEventTypes (code-only enforcement): CRUD write is rejected with an explicit message, and OnEventTypes() drops blocked types at runtime so even a persisted row (pre-validation data, direct table write) is inert for the trigger registry and the engine alike. - rules/guardrails.json gains blockedInterimTriggerEventTypes as the display/ pre-flight mirror (baked into the MCP RULE_GUARDRAILS via combine.js); a parity test pins mirror and code set against drift — same pattern as the gather hard blocks. - MCP validate_rule: the high-frequency finding is now an ERROR sourced from the baked mirror, not a hardcoded warning. Suites: Backend 4155, MCP 548. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
72136f0273 |
backend+mcp+web: evaluateOn interim triggers for analyze rules
Implements docs/rules/analyze-rule-triggers.md (spec approved today): - AnalyzeRule.EvaluateOn (absent = [enrollment_end], today's behavior bit-for-bit) with the shared AnalyzeRuleTriggers grammar/matching helpers; grammar enforced in tenant rule CRUD and the analyze-rule JSON schema. - RuleEngine: AnalyzeRunContext (Terminal/Interim/Reanalyze), interim rule filter, KO suppression on interim runs, and RuleResult update-semantics replacing the permanent dedupe freeze — interim rows refresh, the terminal pass finalizes or resolves them (ResolvedAt, kept for audit), Reanalyze preserves lifecycle markers. - Notification dedupe moves onto RuleResult.NotifiedAt (one send per session+rule, stamped after a successful send; survives the manual reanalyze rebuild). - Handler: whiteglove_sealed / interim_trigger reasons — notify + SignalR yes, IssuesDetected + rule stats terminal-only (Rule Regression Radar baselines clean). - Ingest: InterimTriggerRegistry (per-tenant 5-min TTL cache, fail-soft) gates the interim_trigger enqueue (batch eventType intersection) and the whiteglove_sealed enqueue on the first genuine WhiteGlove seal. - Storage: EvaluateOnJson on AnalyzeRules; FirstDetectedAt/LastEvaluatedAt/ IsInterim/ResolvedAt/NotifiedAt on RuleResults (nullable — legacy rows read as final). - MCP: three authoring lints (terminal-precondition trap, unknown/high-frequency on_event types, KO-on-interim), authoring guide + baked schema updated; get_session_summary marks interim and excludes resolved findings. - Web: preliminary badge on interim findings, resolved findings behind a toggle, issue counts exclude resolved (analysis section + diagnosis flow). - ANALYZE-ID-004 v1.1.0: first interim-enabled rule (on_event:hybrid_login_pending), threshold raised to 75 so only repeated overdue-login signals fire mid-run. Suites: Backend 4152, MCP 548, Web 762 + tsc + lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9c96881242 |
docs: spec for analyze-rule evaluateOn triggers (interim evaluation)
OKF concept doc (status SPEC): opt-in interim analyze runs via evaluateOn (enrollment_end default, whiteglove_sealed, on_event:<type>), RuleResults update-semantics replacing the permanent dedupe freeze, KO/stat suppression on interim runs, notification marker decoupled from row existence, and the validate_rule lints that keep interim rules monotonic. Closes the design for both the sits-d WhiteGlove notification gap and the esa never-terminal stuck-session gap with one mechanism. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c4280e823c |
fragility audit P5.x + P7: drift guards across SignalR names, RowKeys, config sections, scoped routing
P5.x-A: Constants.SignalRMessages catalog (13 names) + SignalRGroupHelper.HubName;
all backend send sites and web subscriptions typed against the shared-manifest
union (SignalRContext.on/off) — unknown names are tsc errors. Dead eventStream
session-payload handling removed from diagnosis page.
P5.x-B: RowKeyCodec as the single inverted-tick encoder (persisted widths pinned,
UserActivity D20 legacy); DeletionRowDump.Table on tombstone rows with
DeletionTombstoneTables.Resolve replacing the duplicated Contains('_') heuristic
(legacy fallback retained for pre-field manifests).
P7.1: PATCH config/{tenantId}/fields flipped to TenantAdminOrGA (phase 2);
Settings sections save via per-section diff PATCH from sectionFieldMap.ts —
the ~39-field stale round-trip vector is gone; legacy webhook clearing now only
on the notifications save; agent-settings reset no longer discards contact edits.
P7.2/7.3: useGlobalAdminScope is a projection over useAggregatedAdminScope
(resolveConcreteScopeView, test-pinned); scopedApi builders own the tenant/global
endpoint-pair routing for all 11 former hand-rolled call sites; scopedFetch adds
the shared JSON fetch layer.
P7.4: navVisibility extraction + roles x nav-config matrix and nav<->route-guard
consistency test with a ratchet for the two known pre-existing mismatches
(Operator and GlobalReader vs /settings — user decision pending).
P7.5: web build stamp public/version.json (prebuild, GITHUB_SHA), SWA no-store
route, deploy-web verify poll + versions/web.json manifest, Portal Build card.
P7.6: OkAsyncBaselineGuardTests freezes the 44 anonymous-response sites
(shrink-only ratchet).
Verified: dotnet build 0W/0E, Functions.Tests 4123, DecisionCore 519, V2.Core
2206, SummaryDialog 44; tsc 0, ESLint 0, vitest 762/762; mutation proofs for all
new drift nets. Docs: docs/drift-guards.md + versioning.md web row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
88cd29d3b9 |
backend: lifecycle completeness nets + session-scope point-read + server-side dashboard search (audit P5/P6)
P5.1 Table lifecycle buckets: TableLifecycleBucketTests classifies every TableNames constant (offboarding buckets via reflection, per-session set from a real DeletionManifestBuilder run, reviewed KeptByDesign list). Fixes found by the audit: UsageMetrics offboarding wipe matched 0 rows (PK=date/RK=tenantId) -> new SafeWipe Variant D (RowKey-anchored, verify protects the "global" row); PlatformStats tenant partition + ScriptNameCache never wiped -> tenant-PK bucket. P5.2 SessionsIndex field manifest: SessionIndexFieldManifest (32 always / 18 conditional / 4 primary-only) pinned bidirectionally against BuildSessionIndexEntity and MapToSessionSummary; full rebuilds now TableUpdateMode.Replace (heals stale conditional columns); merge sites warn on non-manifest keys; force-path now mirrors StalledAt write + clear heals. P5.3/P6.1 Session scope resolution: ResolveSessionScopeAsync (RequestContext) replaces the 16-site copy-paste fallback; ResolveSessionTenantIdAsync does a point-read on the new SessionTenantLookup table (PK=sessionId) with legacy index-scan fallback + self-heal, replacing the cross-partition scan; the "Count == 0 -> rescan" heuristics are gone. Write paths gate cross-tenant resolution on IsGlobalAdmin. RecomputeTriggerGate enforces the decided read-only semantics API-side: reanalyze/rescan now 403 for Viewer/GlobalReader/ cross-tenant tenant roles. Lookup rows ride the deletion cascade (step 22). P6.2 Server-side dashboard search: q= on search/sessions (+global) matching exactly the client-side searchable fields, page-backfill loop (max 10 Azure pages/request), q bound into the pagination fingerprint; the web dashboard's searchAll() replaces the loadAll() full-history sweep (dedupe-merge, delegated callers keep the legacy sweep). P6.3 Context value memoization: TenantConfigContext + Auth/Notification/ Tenant/Theme providers (cuts the externally-triggered re-render cascade). Verified: solution build 0W/0E; Functions.Tests 4106 (+37), DecisionCore 519, V2.Core 2206, SummaryDialog 44; tsc/eslint clean; vitest 722 (+4). Drift detection of both new manifest tests proven by mutation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
321ae63c63 |
insights: app-version duration regression radar + per-version duration stats
versionBreakdown on the app analytics responses now carries measuredInstalls,
medianDurationSeconds and p95DurationSeconds per version (measured population:
succeeded, non-skip, plausible duration, no AppId collision). A new pure
AppVersionRegressionRadar detects "median install duration rose from X to Y min
after version Z": versions ordered by first-seen install time (never string
sort), fires at median lift >=2x AND >=300s absolute with >=10 measured
installs on both sides.
Episodes follow the rule-radar pattern in a fourth tracker keyspace
appversionregression|{app}|{version} (409-dedup, refresh keeps FirstNotifiedAt,
re-arm on drain / median back under 1.5x, 30d retention sweep re-arms): one
tenant bell app_version_duration_regression (Admin audience, appDetailUrl href)
plus one AppVersionDurationRegression ops event per episode, dual-registered in
the web OPS_EVENT_TYPES catalog. Runs in the maintenance timer after the rule
radar over one cross-tenant 35d install-summary read; kill switch
AppVersionRegressionRadarDisabled (fail-open). Apps detail page shows an amber
regression banner and a median-duration-by-version chart; OKF concept doc added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ade6c4948f |
annotations: tenant switcher on the overview page + GA-lane guard on the global list
/annotations now carries the standard cross-tenant scope (GA/Reader: "All tenants" aggregate + per-tenant drill-down via GET global/session-annotations, delegated: managed subset), with a tenant column and tenant-qualified session deep links. The global list endpoint now derives excludeGlobalAdminLane from HasGlobalScope so delegated-rescue callers never receive the platform-internal globaladmin lane (endpoint-test-pinned). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
90b8243894 |
annotations: move tenant list route under the /api/sessions cert-exclusion prefix
/api/session-annotations sat outside every clientCertExclusionPaths
prefix, so browsers got the client-cert picker. Rename-over-exclusion
per the diag-files precedent (the Azure list is portal-only config and
dies on app recreation). Three segments (sessions/annotations/list) on
purpose: a two-segment literal would be eaten by sessions/{sessionId}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
66d25f57db |
Cloud-PC-aware autopilot_profile_missing: Info instead of Warning on W365
On Windows 365 Cloud PCs a missing Autopilot profile (ProfileAvailable=0) is the expected state - Cloud PCs are provisioned by the Windows 365 service, not by Autopilot - so the warning shown for physical devices is misleading there. - Agent: emit autopilot_profile_missing as Info with a Cloud-PC-specific message when CloudPcDetector matches; payload now carries isCloudPc. Evidence collection (ZTD verdict, registry, endpoint probe) unchanged. - Web: DeviceDetailsCard renders a neutral info note instead of the amber warning for Cloud PC sessions (session.isCloudPc, sticky, covers existing sessions; or the new event marker). - MCP: resource-catalog documents the new isCloudPc data field. - Docs: Cloud PC exception noted in agent/autopilot-ztd-diagnostics.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
84a0424789 |
annotations: tenant-scoped overview page listing judged sessions
New GET /api/session-annotations (MemberRead + QueryParam, tenant always the middleware-validated target) backing the portal /annotations page in the Monitoring sidebar group (deliberately no new Insights group). For callers without global scope the repository excludes the globaladmin lane server-side (Lane ne clause), so hidden rows never consume page budget and an explicit lane filter self-contradicts to empty. Rows deep-link to the session annotation section. Verdict pill styling moved to the shared logic module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b2723168fd |
reports: snapshot session annotations into the report ZIP
SubmitReportAsync adds annotations.json (all lanes) to the package and annotationCount to report-metadata.json. All lanes are safe: report blobs are readable only through the GA-only session-reports routes. Fail-soft like the tenant-context snapshot; the copy outlives the session=s retention/delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2c48968ec6 |
session annotations: role-separated human verdicts on sessions (flywheel labels)
New SessionAnnotations table (PK=tenantId, RK={sessionId}_{lane}, lanes
operator/tenantadmin/globaladmin) storing structured verdicts + 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; the globaladmin lane is platform-internal and filtered
from tenant callers. Portal: SessionAnnotationsCard on session detail.
MCP: list_session_annotations (ga) + annotate_session (strictGa, GA lane
only) + annotations key in get_session_summary. Lifecycle: cascade steps
17-19, offboarding wipe, critical-backup member. Privacy page gains the
customer-entered-content line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
2f8ad82b0a |
gather-rules: mirror hard blocks in portal/MCP, stamp author on PUT-upsert; agent: bootstrap retry tests
Codex follow-ups to
|
||
|
|
1ebc2675b1 |
gather-rules: WQL property projections, portal enable-gating, author from JWT
- WMI guard accepts SELECT <property-list> FROM <class> for any class already
allowed via SELECT * (strict data subset); same algorithm in agent, portal
validator, and MCP validate_rule (incl. trim fix); guardrails.json unchanged
- Portal refuses to enable custom rules with not-allowed targets: create lands
disabled, invalid edit force-disables, "Blocked on devices" badge, GitHub-issue
hint for allowlist requests; agent stays the enforcement boundary
- Rule Author is stamped from the creator's token (display name > UPN) at create
and preserved through every update, incl. cross-tenant GA edits
- bumpVersion keeps the segment count ("1.0.0" -> "1.1.0")
Customer feedback: SELECT BatteryStatus FROM Win32_Battery + on_change was
impossible because SELECT * re-emits on every charge-percent fluctuation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
54044c5d9c |
esp: continue-anyway observation mode — bounded 60-min defang for device-phase failures
Classic UD fleets held at the Device-ESP timeout wall by one slow blocking app showed ~100% Failed (tenant c9787ba2: 948/1000 esp_terminal_failure) although the profile allows "Continue anyway" and users demonstrably reach the desktop — the classic advisory defang requires AccountSetupEnteredUtc and never applied there. New operator-set tenant opt-in EnableEspContinueAnywayObservation (ConfigVersion 36 -> 37, default off): - DecisionCore: a Device-phase EspTerminalFailure with ContinueAnyway allowed + opt-in observed (EspConfigDetected payload fact — kernel stays config-free) + Classic/Unknown non-pre-prov scenario defangs into the advisory rail with a 60-min AdvisoryCompletion window. Real end via the DAD-validated real-user desktop + Hello gate (IME user-session gate is structurally unavailable without the AccountSetup anchor): eager completion on DesktopArrived, category recovery via the 4910a5a5 hook, expiry un-defangs to the original esp_terminal_failure — every observed session still gets a terminal verdict. - Honest outcome: enrollment_complete under an unresolved advisory carries espSoftFailure/completionSource (also on the classic post-AccountSetup advisory path); backend stamps EspSoftFailure/CompletionSource on the Sessions row (+ index mirror, terminal-tuple preservation); web renders an amber "with issues" badge plus a session-detail banner. No new terminal status — Succeeded + marker. - Agent: remote-config field merged into AgentConfiguration; stamped into both esp_config_detected emitters (orchestrator bootstrap via live Func<bool> seam, DeviceInfoCollector via host/factory pass-through). - Admin UI: GA-only toggle in the tenant management modal (next to Bootstrap Token) + stored/runtime rows in the tenant config report; GA-gated on both write paths (PUT silently reverts for tenant admins, field patch rejects with 400). Tests: 13 new ContinueAnywayObservationTests; DecisionCore 513, Agent V2 2162, Functions 3948, SummaryDialog 44, vitest 632, tsc clean. Docs: docs/agent/continue-anyway-observation.md (OKF) + index/log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d86ae18a5e |
distress: carry the W365 Cloud PC marker into Devices Not Registered
First W365 field run: the rejected Cloud PC appeared in the report as an anonymous "Microsoft Corporation / Virtual Machine" row — nothing told the admin that the fix is the Cloud PC validator, not an Autopilot-registration hunt. The agent's CloudPcDetector verdict (Windows365 key AND CloudManagedDesktopExtension service) now rides the pre-auth distress channel: - Agent: DistressReporter captures isCloudPc at construction (optional ctor arg, test fakes stay source-compatible); BackendClientFactory feeds it from CloudPcDetector; DistressReport.IsCloudPc on the wire (~18 bytes, fits the 1536-byte payload cap without a bump — comment updated) - Backend: DistressReportEntry.IsCloudPc, table column + read mapping (legacy rows -> false), structured log + AgentDistressReport AI event key; GetDeviceNotRegisteredFunction aggregates sticky-true per serial (same OR semantics as the Sessions IsCloudPc merge) and the data-quality notice names the new self-reported field. GA global/distress-reports serializes the full entry and picks it up automatically. - Web: Cloud PC badge in the Devices Not Registered table (same sky badge as the session table) + conditional hint pointing at the Windows 365 Cloud PC Validation toggle and the W365CloudPcValidation Graph add-on. - Tests: sticky-true + no-marker aggregation cases; MakeEntry helper gains the optional flag. Docs: third transport rail in backend/cloudpc-device-validation.md + log entry. Unverified like every distress field — triage context only, never an auth input. Verified: web tsc clean, eslint 0 errors (file's single warning pre-exists). .NET build/tests run in CI (no SDK in this sandbox). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqEFrcAHhWgCvgJgHGzMuv |
||
|
|
1c5f69a9fe |
w365: CloudPc device validator — cert-CN-bound fallback for Cloud PCs (#127)
* w365: CloudPc device validator — cert-CN-bound fallback for Cloud PCs Windows 365 Cloud PCs are never Autopilot-registered, so the serial lookup (and the Corporate Identifier fallback) structurally reject them and Cloud PC first-connect enrollment could not be monitored. New stage-3 fallback in the device-validation chain: extract the Intune device id from the chain-validated MDM client certificate's Subject CN (field-verified: CN == managedDevice id == cloudPC.managedDeviceId) and require a matching cloudPC object in the claimed tenant (Graph v1.0 virtualEndpoint/cloudPCs, $filter=managedDeviceId eq). Only service-provisioned Cloud PCs have such an object — a regular enrolled non-Autopilot device still ends in the 403, and local W365 markers are never consulted for auth. - Shared: ValidatorType.CloudPc = 5, TenantConfiguration.ValidateCloudPcDevice (default false, tenant-admin writable), GraphAppPermissions.CloudPCReadAll, GraphFeatureCatalog feature W365CloudPcValidation -> CloudPC.Read.All - Backend: CloudPcDeviceValidator (AutopilotDeviceValidator sibling contract: 30/5 min cache, 2 attempts, transient->503; GUID gate before the OData filter; exact-match guard; Graph 403 = definitive "grant missing", not transient), SecurityValidator CN extraction + fallback stage + hard gate, wiring through the 5 agent endpoints, TableConfigRepository persistence - Consent script: W365CloudPcValidation in catalog/ValidateSet/help; NEW GraphFeatureCatalogSyncTests pins C# catalog <-> grant script (previously comment-only lock-step) - Bootstrap dev script v2.3-dev.3: guard 4 (uptime window) exempted while the Cloud-PC relax is active — Cloud PCs run headless for days before first connect; the fresh-profile window is the time anchor there (+ Pester tests) - Web: validation toggle + context plumbing, ValidatedBy "CloudPc" label, privacy-page validation claim generalized (Last updated bumped) - Docs: OKF concept doc backend/cloudpc-device-validation.md + index + log Verified: web tsc clean, ESLint 0 errors (99 pre-existing warnings = ratchet cap, none added). .NET build/tests and Pester run in CI — SDK download and pwsh are unavailable in this sandbox's network policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqEFrcAHhWgCvgJgHGzMuv * w365: agent-side IsCloudPc flag + engine expectation "no Device-ESP phase" Companion to the CloudPc validator: the agent now classifies Windows 365 Cloud PCs locally with the bootstrap's field-verified marker AND (HKLM\SOFTWARE\Microsoft\Windows365 key + installed CloudManagedDesktopExtension service, probed via its Services registry key so net48 needs no System.ServiceProcess). SKIP-safe: any error resolves false. Never an auth input - backend admission stays exclusively the cert-CN-bound Graph lookup (ValidatedBy=CloudPc); the two signals are independent and may disagree. Two transport rails: - Session metadata: SessionRegistration.IsCloudPc -> Sessions table (sticky-true OR across re-registrations, IsSelfDeployingProfile pattern) -> SessionsIndex full mirror -> SessionSummary -> new isCloudPc search filter end to end (SearchSessions + QueryRawSessions + pagination fingerprint + all three in-memory predicates; MCP search_sessions / query_raw_sessions params + summary projection; web type, Cloud PC pill, SessionInfoCard enrollment label, CSV export appended column). - Decision engine: isCloudPc rides the EnrollmentFactsObserved payload, recorded set-once for BOTH values in EnrollmentScenarioObservations.CloudPc and annotates the scenario profile reason cloud_pc_first_connect:no_device_esp_expected - the engine-recorded expectation that Device-ESP already ran headless at provisioning and the session starts at Account Setup. Deliberately reason+observation only: no completion arm depends on DeviceSetup, Mode stays AccountSetup/IME- classified (Classic), EspConfig keeps configured-FirstSync semantics. Positive marker surfaces as cloud_pc_marker in the audit-trail census. Updater idempotency: the cloudPcAnnotates gate keeps the generic v1 reason branch from re-claiming the slot on repeat posts (would bump EvidenceOrdinal every post). Additive-nullable observation - no state schema bump (v4 convention), serializer roundtrip pinned. Tests: CloudPcDetector resolver (marker AND, throwing probe), registration body, facts-signal payload shape, updater annotation/idempotency/ self-deploying interplay, reducer observation both values + census fold, serializer roundtrip, index projection superset, sticky-true both directions. Suites: DecisionCore 500, Functions 3941, Agent V2 2154, vitest 632 - all green; tsc web+MCP clean; ESLint 0 errors (cap 99). Docs: cloudpc-device-validation.md follow-up section replaced with the shipped mechanism + log entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
75197a4184 |
Logparser pattern testing with the agent's exact .NET engine (test_log_pattern) (#125)
* updated commands * feat(backend): logparser pattern test endpoint with agent-exact semantics POST /api/rules/gather/test-pattern evaluates a logparser gather-rule regex against pasted sample lines exactly the way the agent would on the device. Motivated by a field incident: a customer verified a pattern with a PHP regex tester and the .NET engine matched differently. - CmTraceLogParser moves Agent -> AutopilotMonitor.Shared.Logging so agent (IME tracker, logparser collector, stall probe, IME matching mode) and backend run the SAME parsing implementation. Pure mechanical move, agent behavior unchanged (V2.Core suite 2147/2147 green). - TestLogPatternFunction mirrors LogParserCollector verbatim: Regex(Compiled, 1s timeout, NO IgnoreCase -> case-sensitive), first match per line, group "0" excluded / unsuccessful groups omitted, format cmtrace (default, match against parsed message) vs text (raw line), per-line outcomes incl. parse_failed/regex_timeout, and the agent's "set parameter format=text" hint verbatim. Pure compute, no tenant data. Caps: 200 lines, 8KB/line. - Policy: TenantAdminOrGlobalReader (same tier as the analyze dry-run). - Tests pin the parity showcases: case-sensitivity, envelope invisibility in cmtrace mode, optional-group omission, format hint, request validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(mcp): test_log_pattern tool + logparser lint and guide hardening - test_log_pattern: wraps POST /api/rules/gather/test-pattern — tests a logparser regex against pasted sample lines with the agent-exact .NET semantics (case-sensitive, cmtrace/text mode). Description steers clients away from JS/PHP/Python regex testing. - validate_rule: logparser parameter lint (missing/non-compiling pattern, unknown format value = silently cmtrace on the agent) + info pointing to test_log_pattern; nextStep for valid logparser drafts routes there too. - rule_authoring_guide: logparser section now states the matching semantics (case-SENSITIVE, .NET, first match per line, cmtrace default) and two new pitfalls (engine mismatch, plain-text log under cmtrace default). - Catalog snapshot + handler/validator tests extended (MCP suite 532 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(okf): test-pattern endpoint + shared CmTraceLogParser in rule authoring surface Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
29ced81d6e |
docs(okf): tool-telemetry — record the real SDK 1.30 rejection shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
afa8750cbb |
feat(mcp): tool-call quality telemetry — soft errors, result size, zero-hit searches, arg rejections
withToolTelemetry now receives the tool args and inspects the RESULT: handlers never throw (toolError returns isError:true), so the previous isError flag was effectively always false. Each MCP_TOOL_LOGGING line now carries soft-error status, resultChars + overCap (vs. the anthropic/maxResultSizeChars hint), a size-bounded args summary and the caller scope — the only telemetry that covers backend-free tools (search_docs, validate_rule, get_resource). New signals: search_zero_hit on the three search tools (unmet-demand signal) and tool_call_rejected for JSON-RPC-level Zod/unknown-tool rejections, sniffed at the /mcp route since the SDK answers those before any handler runs. Bicep default toolLoggingEnabled=true (desired state; live flag via az containerapp update). Docs: docs/mcp/tool-telemetry.md with KQL examples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3c1229a301 |
MCP rule authoring: deterministic context, validate_rule, and session dry-run (#124)
* feat(backend): dry-run endpoint for draft analyze rules (rule authoring)
POST /api/rules/analyze/dryrun evaluates a DRAFT analyze rule against one
session's events and returns a full diagnostic trace: per-condition
matched/evidence (incl. per-eventType occurrence counts), per-factor
confidence breakdown, verdict, and the would-be MatchedConditions map for
{{token}} interpolation preview. Persists nothing: no RuleResult row, no
session-status change, no notifications.
- RuleEngine.DryRun.cs: DryRunRuleAsync / DryRunEvaluate share the exact
evaluators (EvaluatePrecondition/EvaluateCondition/EvaluateConfidenceFactor)
with the production path but evaluate ALL conditions instead of breaking on
the first failed required one; parity with EvaluateRule is pinned by tests.
- DryRunAnalyzeRuleFunction: TenantAdminOrGlobalReader (read-in-effect POST),
GA/GlobalReader cross-tenant via session tenant resolution, 1 MB body cap,
structural draft validation (unknown source/operator, correlation without
joinField, invalid regex, non-evaluable confidenceFactor DSL) with
actionable error messages.
- EndpointAccessPolicyCatalog: register the new route.
- RuleEngineDryRunTests: trace semantics, production parity matrix,
side-effect freedom (VerifyNoOtherCalls), draft validation matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(mcp): rule-authoring resources — guide, schemas, guardrails
Deterministic authoring context for customer AIs, replacing chunked
search_docs retrieval for rule creation: get_resource (and the MCP-protocol
resources) now serve
- rule_authoring_guide: hand-written, complete authoring knowledge (collector
types, triggers, condition sources, operators, confidence model incl. the
exact factor-condition DSL shapes, {{token}} interpolation order, guardrail
matching semantics, pitfalls, validate->dry-run workflow)
- rule_schemas: the JSON Schemas (2020-12) for gather + analyze rules
- rule_guardrails: the on-device collection allowlists
Schemas + guardrails are generated into rule-authoring.generated.ts by a new
combine.js emitter (same pattern as the web guardrails mirror; rules/ stays
the single source of truth and CI guardrails-in-sync guards the file).
rule-authoring-drift.test.ts additionally pins generated == repo source,
guide enums == schema enums, and the get_resource size cap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(mcp): validate_rule + test_analyze_rule tools and author-rule prompt
Closes the authoring loop for customer AIs: draft -> validate -> dry-run
against a real session -> iterate -> create in the portal.
- validate_rule (MCP-local, no backend call): ajv 2020-12 validation against
the baked schemas, guardrail pre-flight mirroring the agent matching
semantics (hive-stripped segment-bounded registry/file prefixes,
whitespace-bounded WMI, exact commands, /-bounded event log channels, hard
blocks), and semantic lint for schema-valid-but-dead rules (unreachable
threshold, non-evaluable factor DSL, unknown eventTypes, unanchored
not_regex allowlists, unresolvable {{tokens}}, trigger cross-fields,
outputEventType conventions/collisions).
- test_analyze_rule: calls the new POST /api/rules/analyze/dryrun and adds
the {{token}}-interpolated explanation/remediation preview via the
existing interpolate-rule-template.
- author-rule MCP prompt guiding scenario -> resources -> draft -> validate
-> dry-run.
- Both tools are visible to every role (backend enforces
TenantAdminOrGlobalReader on the dryrun route); catalog snapshot updated.
- deps: ajv + ajv-formats promoted to direct dependencies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(okf): rule authoring surface concept doc
docs/rules/rule-authoring-surface.md: the AI-assisted authoring stack
(deterministic MCP resources via combine.js baking, MCP-local validate_rule
layers, the dry-run trace twin with its parity + side-effect invariants, and
what is deliberately absent); registered in index.md, logged 2026-08-05.
Customer docs (rules/ai-assisted-rule-authoring.md + SUMMARY + FAQ entry) are
committed in the autopilotmonitor-docs repository separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
eb94c612e7 |
fix(agent): gather phase/event triggers follow the engine-reduced timeline
phase_change/phase_exit/on_event gather triggers (and the phase-scope gates) were driven by the pre-reduce SignalIngress.SignalPosted stream, so any raw signal carrying an EspPhase payload fired them before the decision engine had applied its gates. In session 32312a32 (RealmJoin tenant) a Phase Start rule on FinalizingSetup fired at the raw ESP-exit EspPhaseChanged(FinalizingSetup) signal, 7 minutes before the engine's RealmJoin-gated phase_transition(FinalizingSetup) - and read HKLM\SOFTWARE\RealmJoin\Custom\BIOS before the RealmJoin package that creates it had run (exists=false). GatherRuleExecutorHost now subscribes to the new TimelineEventStream: EventTimelineEmitter publishes every emitted event's (eventType, phase) post-reduce, so phase triggers fire exactly when the UI timeline shows the phase, and on_event sees engine-emitted event types (enrollment_complete, phase_transition, realmjoin_resolved, ...) that never existed on the raw signal stream - restoring V1 semantics and the documented "On Event with enrollment_complete" pattern. Raw-signal consumers with OS-state semantics (DeviceInfoHost, UserEspKeepAwakeHost, ProvisioningPackageHost) stay on the raw feed deliberately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |