mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
v2.0.1413
45 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
a7a1cc43c1 |
fix(backend): welcome mail survives the auto-approve race - send-on-save + once-only marker
First production auto-approve (mueller-steinag.ch) activated 64s after signup, 5s before the user saved their notification address on the activation page - the approval path found no address and silently skipped the welcome mail. Both racing paths now write-then-read: the notification-email save path checks approval fresh (bypassing the 30s negative cache) and triggers the send when activation already happened. A conditional-insert welcome-email-sent marker in PreviewWhitelist makes the send once-only across both paths; it is consumed strictly after the address check so it can never be burned without an address. Revoke clears the marker, the GA resend endpoint sends unconditionally and consumes it best-effort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
26bcab9054 |
feat: move OOBE bootstrap script endpoint into the backend (go.autopilotmonitor.com)
The portal's Next.js /go/[code] route was the web app's only request-time
server code and blocked the planned static export. The script generation
now lives in the Function App as GET /api/bootstrap/go/{code}
(GetBootstrapScriptFunction), reached by customers as
irm https://go.autopilotmonitor.com/{code} | iex via a Front Door custom
domain whose /* route rewrites onto the API path.
- New Constants.BootstrapGoBaseUrl; CreateBootstrapSessionFunction now
hands out the go URL (previously unguarded — CreateBootstrapSessionUrlShapeTests
pins it now). go host added to both URL-guard enforced lists.
- BootstrapScriptValueValidator ports the bootstrapValidation.ts injection
invariants (exact GUIDs, https + host allowlist + /agent/*.zip path,
expiry <= 14d) as defense-in-depth for in-process values.
- OobeBootstrapScriptGenerator ports the PS template as a Replace-token
raw string; golden test pins byte parity with the TS template
(TestData/oobe-bootstrap-golden.ps1, captured from route.ts).
- Always-HTTP-200 text/plain contract incl. rate limit (own bucket
bootstrap-script:{ip} 20/min); disabled tenant indistinguishable from
unknown code. Catalog: PublicAnonymous; NoStoreCacheMiddleware covers
/api/bootstrap/go/ (script inlines the bearer token).
- Web: BOOTSTRAP_GO_URL registry entry, sessions list shows the go host,
legacy route marked LEGACY (alive for pre-migration URLs <= 168h,
removal in the static-export PR).
- Docs: new OKF backend/oobe-bootstrap-go-flow.md, url-registry go
section, index/log entries.
Backend tests 3611/3611, web vitest 651/651, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
21b8e054df |
docs: correct portal-prefetch root cause (h2, server-side queue) + measured outcome
The HTTP/1.1 / six-connection-cap claim was wrong — it came from the
Schannel curl build that cannot speak HTTP/2. ALPN (openssl s_client)
and .NET HttpClient confirm the portal negotiates h2; the queue is
server-side in the SWA SSR runtime. Doc, index, log and the
GlobalSidebar comment now describe the real mechanism.
Adds the 2026-07-28 outcome section: Standard plan changed nothing
(stratified p50s match, Mann-Whitney p=0.81); prefetch={false} removed
the freezes (worst totalDuration 225s -> 4.9s) and also removed an
accidental keep-alive (~150ms added to cold navigations).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4ebd60fb1e |
feat: EnableGatherRuleDebugLog — per-tenant gather-rule debug trace (ConfigVersion 36)
Customers cannot tell why a gather rule produces nothing in the timeline (e.g. a logparser rule with emit_mode on_change that never emits). New per-tenant boolean EnableGatherRuleDebugLog (EnableImeMatchLog-style plumbing end to end): when enabled, the agent writes %ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log with one pipe-delimited line per evaluation decision across all previously silent "produced nothing" exits — disabled rules, interval without intervalSeconds, first-tick-after-one-full-interval, phase-scope skips incl. currentPhase=Unknown, per-(rule, phase) dedup, empty collector results (emitOnlyIfExists), on_change hash suppression (hash prefix + streak), guard blocks, logparser per-file outcomes (position skips, format=text hint when every line fails CMTrace parsing, maxLines and wildcard caps), and full exception stacks the agent log drops. - Backend: ConfigVersion 35 -> 36; TenantConfiguration/AgentConfigResponse field + TableConfigRepository Store+Map - Agent: GatherRuleDebugLog writer (10 MB cap, one .old generation, failures swallowed, optional console echo), GatherRuleContext.DebugLog (null-safe no-op), trace points in executor + collectors; never writes under _scopeLock (reasons captured inside, written after release); merger bool->path projection with --gather-debug-log CLI override tolerance; deliberately NOT in the offline-cache strip lists; --run-gather-rules always traces; zero-rules delivery leaves a one-line file instead of nothing - Side fix: GatherRuleExecutorHost now passes the logger into InformationalEventPost so gather-event emissions hit the DEBUG log - Web: tenant settings toggle (Settings -> Agent) + admin tenant-config report rows - Tests: merger on/off/override, writer format+rotation, executor debug lines, logparser debug lines (agent 2131, backend 3561, tsc green) - Docs: docs/agent/gather-rule-debug-log.md + index/log entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7680bdff7b |
fix(web): disable RSC prefetch on nav links - portal freeze root cause
Navbar + GlobalSidebar render the full navigation on every page, so the
App Router's default viewport prefetch fired one ?_rsc= request per
visible link against the SWA runtime (~10.4k requests / 14 days). That
origin serves document, chunks and RSC payloads over HTTP/1.1 (6
connections per origin); when the runtime queued the prefetches (p95
896ms, tail up to 234s), they held every socket and the portal froze
until the queue drained. Measured on the 2026-07-27 17:25 incident:
/admin/presence?_rsc= at 151s while the backend API answered in <2.2s.
All 16 nav links now set prefetch={false}. Every target route is a
client component behind ProtectedRoute that fetches its own data after
mount, so the prefetched RSC payload carried no benefit.
New OKF doc docs/web/portal-navigation-prefetch.md records the full
evidence chain and where browser telemetry lives (separate App Insights
resource, not reachable via MCP query_backend_logs).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1bf37f6761 |
feat(insights): F3 regression radar - Wilson-gated rule hit-rate drift detection (PR6)
Analyze rules only (gather stats stay per-batch-deduped until Q6 is
fixed - never silently included). Per (tenant, rule) the radar compares
the trailing 7-day hit rate (FireCount/SessionsEvaluated from the daily
RuleStats rows) against the prior 28-day baseline and fires only when
the window has >=5 hit sessions and >=20 evaluated sessions, the rate
is >=2x baseline AND the Wilson 95% intervals are disjoint
(MetricsMath.WilsonInterval/RateIncreaseSeparated, hand-computed test
vectors). Suppressed for deleted rules, rules inside the CreatedAt
grace period (<35d), rules edited inside the window, empty baselines,
and the RuleRegressionRadarDisabled app-setting kill switch.
Alert episodes live as ruleregression|{ruleId} rows in the notification
tracker (third keyspace): AddEntity-409 dedup = one bell + one
RuleFrequencyRegression ops event (category Tenant, Warning, full
numbers, dual-registered in the web OPS_EVENT_TYPES) per episode;
numbers refresh while active without moving FirstNotifiedAt; episodes
re-arm under 1.5x baseline, on fire-stop, or via the tracker's 30d
retention prune (spec-intended reminder). On-fire dimension correlation
(osBuild/model/agentVersion/imeVersion; hit set from a RuleResults
range scan intersected with the window sessions) reports both shares
with the "correlated, not necessarily causal" wording contract or
honestly "no clear dimension concentration". Radar runs in the daily
maintenance pass after the rule-stats aggregation, anchored on
yesterday (whole days only).
Surfaces: regressions[] in the rule-stats response (MCP get_rule_stats
inherits it), tenant bell deep-linking to the rule card, and the
analyze-rules page gains a densified 30-day fire sparkline plus a
regression badge while an episode is active. OKF doc
backend/rule-regression-radar.md + index/log.
Backend 3474/3474 (27 new: Wilson vectors, every suppression branch,
window boundary dates, re-arm matrix, dimension gates, tracker
round-trips incl. tri-states, keyspace dedup/prune, wording pins),
web tsc + vitest 627/627, MCP tsc + 455 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
88da317bf4 |
feat(insights): F2 surfaces - device-history banner, fleet FTR panel, API routes, MCP tool (PR5)
Three read routes over the PR4 data: metrics/device-history
(MemberRead + tenant query param - one route serves the web banner AND
MCP, GA cross-tenant via ?tenantId= with a sessionId-based owner
fallback; history:null is a normal outcome), metrics/device-journeys
and global/metrics/device-journeys (days selector honored because FTR
counts are additive - window rate = ratio of summed daily rows; the
tenant-less global variant serves the "global" partition and discloses
repeatDevices:null). Attempt numbers are server-computed
(DeviceJourneyCalculator.ComputeAttemptNumber: chain position within
the journey; live sessions via a virtual non-successful attempt so the
redeploy and 30d-gap rules apply) - no consumer re-derives journey
semantics. Repeat-devices violator list: last journey >=2 attempts with
newest terminal ref in-window, attempts desc then recency, cap 10,
failure reason from the newest failed attempt (bounded point-reads).
Web: DeviceHistoryBanner on session detail ("Attempt N for this device
- View history", expandable terminal chain, durations strictly the
authoritative durationSeconds, WG/DevPrep/admin-marked badges) and the
Fleet Health "First-time-right" section (rate gated at >=20 completed
journeys with disclosed n, junk-serial exclusion note, Monday-UTC
weekly trend, attempt histogram, repeat devices) following the 7/30/90
selector. MCP: get_device_history (serial mode / fleet FTR mode,
role-aware routing, all roles - GA_FULL privilege snapshot extended
deliberately). Docs: OKF backend/device-journeys.md + index/log.
Backend 3447/3447 (14 new: attempt-number vectors incl. gap boundaries
and live sessions, additive sums, days clamping, repeat-device
gate/sort/cap), web tsc + vitest 627/627, MCP tsc + 455 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6de21e821a |
feat(insights): F1 surfaces — session lane, fleet panel, API routes, MCP tool (PR3)
Insights expansion PR3 (tasks/insights-expansion-spec.md §F1 "Surfaces").
Completes F1: schema (PR1) -> persistence/aggregation (PR2) -> surfaces.
- Rolling range rows: the sweep now also writes "rolling30|{class}"
aggregate rows — range statistics over the window's actual sessions. A
median of per-day medians is not the range median, so the fleet panel
reads these and is deliberately fixed to "last 30 days"; the daily rows
stay in the payload for trends/MCP.
- Routes (all registered in EndpointAccessPolicyCatalog):
GET sessions/{sessionId}/time-attribution (MemberRead + tenant query
param, GA fallback via FindSessionTenantId; breakdown=null is SUCCESS —
a missing lane is a normal state, not an error), GET
metrics/time-attribution (MemberRead) and GET
global/metrics/time-attribution (GlobalReadOrAdmin + tenantId filter),
both returning { windowDays, classes (rolling), daily }.
- Web: TimeAttributionLane under the session-detail phase timeline —
proportional stacked bar of segment totals incl. the explicit
unattributed remainder (bar always sums to the session's authoritative
duration), ESP-blocking apps with critical-path occupancy, reboot badge
(cross-cutting, never a bar slice), quality-flag chips; fetched
fail-soft for terminal sessions only. Fleet Health gains the "Time
attribution" section: per-class stacked medians gated at n>=20
("insufficient data (n=...)" below), flagged/missing exclusions
disclosed, and a top-blocking-apps table with class-scoped rows and
"up to X (p75 Y)" what-if wording. Deliberate deviation: the spec's
per-app trend column is omitted — per-day app rows are too sparse under
the daily n>=5 gate; revisit once rolling history exists.
- MCP: get_time_attribution (one tool, session + fleet mode, role-aware
routing like get_app_install_metrics, registered for all roles) — the
privilege-leak-guard catalog snapshot extended deliberately.
- Docs: OKF concept doc backend/time-attribution.md (invariant, windows,
segment mapping, positive-evidence blocking, Q7 reboot spans,
persistence + surfaces) registered in index.md + log.md.
Backend 3382/3382 (+1 rolling-aggregate test), web tsc clean + vitest
627/627, MCP tsc clean + 455 passed. Post-deploy: web deploy +
MCP redeploy (new tool + docs corpus).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9acd10e1dc |
docs(okf): ServerActions & on-demand diagnostics concept doc
Covers the queue-on-session/deliver-on-ingest channel, the TenantAdminOrOperator tier with in-function per-type re-gating, the portal Collect Logs flow (quick-config orders rotate_config before request_diagnostics), sequence-based progress matching, and the hosted-prefix sweep in the offboarding cascade. Registered in index.md, logged in log.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fd7d3973e7 |
fix(agent): esp_policy_provider_stalled gains sidecar-missing arm (issue #106)
Customer follow-up corrected the field-case mechanics: TrackingPoliciesCreated=1 WAS set under the foreign ConfigMgr provider - the ESP ignores a foreign provider's completion values and waits on the Intune-registered "Sidecar" provider by name (renaming the key unblocks instantly without creating any tracking entries). The completeness-keyed detector would have missed exactly the incident it was built for. - Arm 2: >=1 Setup\Apps provider registered without Sidecar, continuously for the 15-min dwell -> reason=sidecar_provider_missing; clock resets when Sidecar registers later (legitimate startup ordering); DevicePreparation-only snapshots don't activate it - Arm 1 (continuously-incomplete providers) unchanged -> reason=provider_incomplete; sidecar-missing wins when both arms cross in one round (single event) - Cross-restart fingerprint gains a "sidecarMissing" sentinel; payload adds reason + sidecarMissingForMinutes - Regression test: ConfigMgr complete + Sidecar absent -> fires (previously classified healthy); probe/doc headers and OKF doc corrected, web event-type description updated Evidence (research session c7e3db08): Intune's ESP-policy WAP payload always registers Sidecar (ConfigMgr only via co-management settings policy, per Niehaus' documented payload); Microsoft's ESP troubleshooting models app tracking exclusively through the Sidecar subkey. Agent tests 2043/2043, web tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e24b92fbf |
feat(agent): ESP policy-provider stall detector + Autopilot registry guardrail
Customer field case: pre-provisioned devices with the ConfigMgr client as a
blocking app occasionally park the user ESP at "Apps (Identifying)" for days.
Root cause (verified against the EnrollmentStatusTracking CSP doc): only a
"ConfigMgr" provider is registered under Device\Setup\Apps\PolicyProviders
(no "Sidecar" = IME) and TrackingPoliciesCreated never becomes 1 - a wait the
CSP defines WITHOUT any timeout. Renaming the key to Sidecar unblocks
immediately. Microsoft documents that ESP providers are not aware of each
other and that pre-provisioning is unsupported with co-management.
New always-on esp_policy_provider_stalled tripwire, generalized to any
provider in both kinds (Setup/Apps TrackingPoliciesCreated != 1,
DevicePreparation InstallationState not in {2,3}):
- EspPolicyProviderProbe: fail-soft registry read of the
EnrollmentStatusTracking mirror (device + user-SID scopes), defensive
value parsing (missing/unparseable = incomplete), ScopedOverride test seam.
Research and CSP contract quotes captured in the XML doc header.
- EspPolicyProviderStallDetector: pure EvaluateStallTransition on a 15-min
continuous-incompleteness dwell (resets on completion/disappearance;
absent key = normal early enrollment), one-shot per provider,
StartupEventGate fingerprint over the sorted stalled key set for
cross-restart dedup. Warning + ImmediateUpload via InformationalEventPost;
purely observational, no decision-engine coupling, no config toggle,
no ConfigVersion bump (disk_space_low precedent).
- EspPolicyProviderStallHost: 60-s wall-clock tick, deliberately outside the
StallProbe idle gate (idle clock resets on session activity) and the
idle-stopped periodic collectors.
- Registered in Constants.EventTypes, MCP resource-catalog (health_events),
web gather-rule autocomplete (stall category).
Guardrail companion: SOFTWARE\Microsoft\Windows\Autopilot joins the
gather-rule registry allowlist ("Autopilot / OOBE / Setup") so tenants can
build custom rules on the subtree (e.g. not_exists on the Sidecar provider
key); generated web mirror regenerated via combine.js; positive + sibling-
spoof guard tests added.
Docs: OKF concept doc agent/esp-policy-provider-stall.md (+ index/log).
Tests: 22 new detector tests (pure transition matrix + emission surface incl.
the ConfigMgr-only customer scenario and gate-across-restart dedup); agent
suite 2013/2013, web vitest 589/589 + tsc clean, MCP 449 passed incl.
event-types-drift.
Both the detector and the embedded allowlist take effect with the next agent
release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
90012d9a66 |
feat(agent): verify Secure Boot CA-2023 state against firmware, not just servicing registry
Customer report: ANALYZE-SEC-001 warned on devices whose firmware already carried the Windows UEFI CA 2023 certificate — the agent only read the SecureBoot\Servicing registry value, which mirrors the Windows Update rollout and lags behind reality (OEM factory image, WinCS, WU not run yet). Agent: new UefiSecureBootCertReader reads the UEFI db/KEK variables natively (GetFirmwareEnvironmentVariableExW; SeSystemEnvironmentPrivilege enabled on the process token and restored — first privilege-adjustment code in the repo). Fail-soft, never throws; legacy BIOS maps to not_uefi. secureboot_status gains per-certificate booleans, uefiFirmwareReadStatus, and the one-sided marker uefiCa2023FirmwareConfirmed (emitted only on positive confirmation, never as false). Rule v3.0.0: not_exists precondition on the marker suppresses the rule when the firmware confirms the certificate; the marker's absence keeps old agents on exact v2 behavior, so the rule is safe to seed before the agent rollout. Firmware-verified-missing raises confidence (+20 db, +10 KEK 2K CA 2023). Verified: full solution build; backend 3201, agent 1988, web 582 tests green; tsc/ajv clean; elevated cross-check on real firmware matches Get-SecureBootUEFI on all seven certificate probes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e0ad51b965 |
refactor(urls): one guard-enforced URL registry per component, agent downloads move to the alias
Every well-known host -- own subdomains, both blob accounts, the API origin, Graph, Entra login -- now lives in exactly one registry per component: Constants.cs (*BaseUrl) for C#, utils/config.ts for the web app, src/config.ts for the MCP server. lib/hostRouting.ts derives its hostnames from the registry and middleware.ts imports from it, retiring what turned out to be a fourth independent copy of the www/portal host pair. Three new guard tests (backend xUnit, web vitest, MCP vitest) scan non-test source and fail on any enforced host outside its registry. This is the lesson of the EU cutover: the registry existed then too, but LatestVersionsService (feeds the portal's version badge) and ValidateBootstrapCodeFunction carried their own literal copies of the legacy blob host and were missed -- kept working only by the fail-soft legacy mirror in build-agent.yml, whose silent failure would have pinned badge and self-update to stale bits with a green release. Both guards proved themselves on their first run, flagging the middleware host copy and a literal in an MCP tool description. Test files stay exempt on purpose: literals there are independent oracles, and ValidateBootstrapCodeResponseShapeTests now pins the full alias URL against the Constants-built one so registry changes must be acknowledged consciously. The two legacy readers plus the agent SelfUpdater now use Constants.AgentDownloadBaseUrl (download.autopilotmonitor.com/agent); the alias was verified live for version.json, version-v2.json and both ZIPs before the switch. The portal's bootstrap allow-list accepts the alias AND the legacy host (AGENT_DOWNLOAD_HOSTNAMES) for already-deployed customer scripts, which imposes a deploy order: web before or with backend. The legacy keepalive probes and the release mirror stay untouched -- they exist for exactly this transition. Also: the bootstrap short-link now targets www directly instead of riding the registrar's apex 301, and the welcome-mail docs button points at docs. instead of bouncing through www./docs. Suites: backend 3194, agent core 1925, DecisionCore 474, web 582, MCP 449 -- all green; next build verified (it, not tsc, proves the next.config.ts registry import). Docs: docs/url-registry.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bbb55034e3 |
feat(versioning): one version contract for agent, backend and MCP
All three components now report <major>.<minor>.<build> plus a 7-char commit and a build timestamp. The backend had been pinned at a hand-written 1.0.0 since inception and starts at 1.5.1000; the MCP server's package.json version was bumped by hand and starts at 1.5.300, and it now reports its own commit on /health instead of only the documentation bundle's. The build number keeps coming from a counter blob reserved before the build with ETag-CAS, but there is now one implementation instead of two copies: .github/scripts/Request-BuildNumber.ps1, called by all three workflows and by the local build.ps1. It lives under .github/ because scripts/Deployment/ is gitignored (those scripts carry SAS tokens) and would therefore be absent from a CI checkout; scripts/Build/ and scripts/Release/ are shadowed by the [Bb]uild/ and [Rr]elease/ artifact ignores. The counter cannot be replaced by the published manifest, which is the obvious simplification to reach for: version.json is written only on stable publish, so two consecutive dev builds would derive the same number and the second would overwrite the first's artifact. git rev-list --count fails the same way, two builds of one commit collide. Both deploy workflows now poll their health endpoint until it reports exactly the version and commit just deployed, and publish versions/<component>.json only then. A green deploy step alone means the package was accepted, not that it is being served -- so a manifest that disagrees with the live endpoint now means the running instance drifted (rollback, stale revision) rather than a pipeline that stopped halfway. The agent manifest gains the same commit and buildUtc fields additively; the bootstrap reads it by field name, so older installers ignore them, and sha256/bootstrapVersion stay untouched. Requires the versions container plus AZURE_BLOB_VERSIONS_SAS_TOKEN and the two seeded counters (backend 999, mcp 299) before the first deploy run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3f9576d4a9 |
fix(agent): enforce the gather-rule allowlist in every collector
LogParserCollector and EventLogCollector called no guard at all, while File/Json/Xml did — and rules/gather-rules.md already promised customers that log-parser paths were allowlisted. A tenant admin could author a logparser rule against C:\Users via POST /api/rules/gather and have up to 1000 lines uploaded as telemetry, bypassing the hard C:\Users block the same page guarantees; eventlog could read any channel, Security included. The backend validates no collectorType, target, or parameters, so the agent is the only place this can be enforced. Both now guard. logparser cannot check rule.Target directly (GetFullPath throws on a wildcard), so it checks the directory for wildcard targets and re-checks each resolved path. Event log channels get an allowlist matched on a "/" boundary. Hard-blocked channels live in a C# constant rather than guardrails.json: a parse error drops the allowlists to empty, which is fail-closed, but would have lifted a block sourced from the same file. Also fixes an inert timeout in CommandCollector: ReadToEnd() blocks until the pipe closes, so WaitForExit(30000) was only reached once the process had already exited and a hung command pinned its worker indefinitely. Adds the first real CI gate. No workflow ran tests on a pull request — the deploy workflows build without testing and build-agent.yml covered only DecisionCore.Tests on manual dispatch — while trust/security-faq.md already told customers automated tests run in CI. ci.yml now runs all six suites on every PR and merge, plus a job that fails on generated-guardrail drift, alongside CodeQL and Dependabot. OfficeBinaryWatcherTests is marked SerialThreading; its bounded re-probe races the shared ThreadPool and failed once under full-suite load. Verified: 3101 backend, 1911 agent V2, 474 decision core, 44 dialog, 572 web, 447 MCP. The timeout fix is verified by runtime, not assertion — a Start-Sleep 120 command now returns after ~30s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
63ea2e3cee |
feat(mcp): search_docs — customer documentation as a third search corpus
The MCP server could answer telemetry questions ("why did this session fail")
but not product ones ("how do I deploy the agent", "where is my data stored").
Those answers already exist in the autopilotmonitor-docs bundle; search_docs
makes them semantically searchable.
Deliberately a separate index from search_knowledge: rules and prose answer
different questions, and sharing one topK would dilute both. Registered for
every role — published documentation carries no tenant data — and skipped
entirely when no corpus was baked in, so a doc-less build advertises no
broken tool.
Corpus handling:
- Chunked at h2/h3 and <details>/<summary>, each chunk prefixed with a
"Page > Section > Subsection" breadcrumb so a mid-page hit is self-describing.
- Newlines are normalized before any parsing. This is a correctness
precondition, not cosmetics: the bundle ships CRLF and JS `.` does not match
\r, so /^(#{2,3})\s+(.*)$/ matched zero headings — silently, degrading every
page to unlabelled paragraph splits.
- Two size exceptions: tables are never split (a split table leaves `| --- |`
rows with no header), and unbroken changelog lists split at line level (left
whole, their tails fall outside the model window and become unsearchable).
- Short sections are kept — a one-line answer is often the best hit, and the
breadcrumb supplies the context a short body lacks.
Build and delivery:
- Docs are embedded at image build time into an optional third
PrecomputedSection. Optional so a checkout without the docs repo does not
invalidate the rules and event-type vectors; the reverse (server has docs,
file does not) is staleness and is rejected.
- The deploy workflow checks the public docs repo into the build context and
passes its SHA as DOCS_COMMIT, which /health reports alongside the chunk
count — editing the docs does not update a running server, so staleness is
made checkable rather than memorable.
- Embeddings are rounded to 6 decimals on serialize: 55% smaller index (2.63
vs 5.88 MB) for a ~1e-5 norm error against a 1e-3 tolerance. indexPrecomputed
now asserts unit norm, since the hydrate path never checked it.
Infra: the Bicep template was two changes behind the live deployment (EU
cutover and ACR hardening, both applied directly in Azure). Reconciled against
the running resources — managed-identity image pull replaces the admin
credentials and the acr-password secret — and infra/ is no longer gitignored so
it stops drifting. Container raised to 0.5 vCPU / 1.0 GiB for the cold-start
query-embedder load. The template does not model the mcp.autopilotmonitor.com
custom domain, so a full deployment would remove it; the header warns about it.
Verified: 428 tests pass; 45 pages produce 324 chunks; sample queries hit the
right page top-1/2; all 8 derived URLs return 200 live; the precompute output
is accepted by the boot validator and a mutated corpus is rejected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
dbf6e50e2d |
feat(agent,web,shared): phase_exit gather trigger — one-shot collection at a phase's end
Follow-up to gather-rule phase scoping. One-shot collection at a phase START
already existed (phase_change dedups per (rule, phase)) and at enrollment END
(on_event + enrollment_complete); the END of a specific phase had no trigger.
New trigger value "phase_exit": fires once when TriggerPhase is LEFT (empty =
every phase transition). Together with phase_change it forms the two one-shot
bookends of a phase.
Agent (GatherRuleExecutor.OnPhaseChanged):
- Exit rules are evaluated BEFORE _currentPhase advances, so both the
TriggerPhase match and the phase-scope gate see the phase being LEFT. A rule
scoped to DeviceSetup therefore fires on its own exit even though the phase
being entered is out of scope.
- A transition into Failed fires the exit rules of the failing phase — the
snapshot at a failure boundary is the point of the trigger.
- Exit of Unknown never fires; a repeated identical phase signal is not a
transition.
- Dedup key "exit:{ruleId}|{phase}" shares _phaseRulesExecuted with the
phase_change keys but stays in a disjoint key space, so an enter-rule and an
exit-rule on the same phase cannot consume each other's slot.
Portal: trigger labels are now "Phase Start" / "Phase End"; the canonical phase
dropdown and the create/save payloads cover both via PHASE_TRIGGERS; the card
renders "entering X" / "leaving X".
No ConfigVersion bump: no new field is transported. Old agents do not know the
value and simply never fire the rule (silent no-op).
Tests: GatherRuleExecutorPhaseExitTests (11) — enter-vs-exit, no refire on
re-entry, empty-triggerPhase, exit-of-Unknown, same-phase-signal, into-Failed,
Complete, scope-against-left-phase, out-of-scope slot preservation, key-space
disjointness, on_change interplay. Build 0 Errors; V2.Core.Tests 1884/1884;
Functions.Tests 3099/3099; vitest 557/557; tsc silent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a97888ebd5 |
fix(agent): neutral aggregated 2800 observations; reboot claim gated on observed reboot
Rework after the first real enrollment (session b2e890c1) exposed three
defects in
|
||
|
|
a654c7309e |
feat(agent): attribute mid-ESP coalesced reboots to MDM policy URIs (second sign-in detection)
Device-assigned MDM policies whose CSP URIs match the OS reboot-required
catalog force a coalesced reboot at the end of ESP DeviceSetup - the user
sees an unexpected restart and a second sign-in before AccountSetup.
Invisible in the Intune console; admins simply lived with it. Now attributed
and actionable (PMPC research).
- MdmRebootPolicyTracker (+host): watches DeviceManagement-Enterprise-
Diagnostics-Provider/Admin EventID 2800, emits one mdm_policy_reboot_required
per reboot-forcing URI (Warning, ImmediateUpload; tolerant EventData-first
URI extraction with regex fallback; fail-soft OMADM RebootRequired probe).
Cross-restart RecordId watermark (mdm-reboot-watermark.json) - mandatory,
the coalesced reboot restarts the agent and the backfill re-reads the log.
- ShellCoreTracker: RebootCoalescing branch on the already-watched 62407
stream -> esp_reboot_coalescing corroboration event; deliberate deviation:
emits on backfill too (live emit races the reboot kill).
- ANALYZE-ESP-005 advisory (warning): names the first URI via {{rebootUri}},
remediation = reassign profiles device -> user groups; +20 confidence on
coalescing corroboration, +10 on >=2 URIs.
- Config: Collectors.MdmRebootPolicyWatcherEnabled (default ON, internal
steering only - agent-native collector, NOT an IAgentAnalyzer, no admin UI),
MdmRebootPolicyBackfillLookbackMinutes=60; ConfigVersion 35.
- Event types deliberately distinct from system_reboot_detected so the
backend RebootCount (exact-string match, 2 sites) stays uninflated.
- Docs: docs/agent/mdm-reboot-coalescing.md (+index/log). Exact 2800/62407
message texts are unverified until E2E VM validation (tolerant matching).
Tests: Agent.V2.Core 1868/1868 (21 new), Functions 3099/3099, MCP 400 passed
(event-types drift), solution build 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
da958b5398 |
fix(backend): business timestamps survive storage migrations (OccurredUtc + RowKey decode)
The 2026-07-18 storage migration reset every row's system Timestamp, exposing that AuditLogs, OpsEvents and Events displayed, filtered and retention-swept on it (custom "Timestamp" writes were silent no-ops — reserved system property). All migrated rows read as 2026-07-18. - New Helpers/BusinessTimestamp: RowKey decoders (audit !revtick_guid, ops bare revtick, event yyyyMMddHHmmssfff_seq), RowKey-range OData clause builders (tick-exact, with the mandatory RowKey lt '"' guard so no lower bound can ever match legacy bare-GUID audit rows), and dual DateTimeOffset/DateTime column reads. - Writes in all three tables now persist OccurredUtc (custom column, copied verbatim by future migrations); no-op ["Timestamp"] removed. - Read mappers resolve OccurredUtc -> RowKey decode -> system Timestamp; GetEarliestSessionEventTimestampAsync uses the same resolution so StartedAt/Duration reconciliation can no longer absorb the migration date on migrated sessions. - Date-window filters and retention moved off the system Timestamp onto RowKey ranges; ops retention loses its full-table scan and the post-migration mass-delete cliff; audit retention runs a temporary second pass for legacy GUID rows (TODO: remove after 2027-03-01). - New GlobalAdmin backfill endpoint POST /api/maintenance/ backfill-occurred-utc (dry-run by default, batched, idempotent MERGE of only OccurredUtc, table=audit|ops) — Events rows are deliberately never mutated; their read path decodes the RowKey instead. - Tests: decoders, clause boundary proofs incl. GUID-safety pins, mapper precedence, backfill row decisions (Functions.Tests 3078). - Docs: docs/backend/business-timestamps.md (system Timestamp is never authoritative; migration checklist rule). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e17eb607ca |
docs: gather-rule phase scoping + emit-on-change concept doc (OKF)
New rules/ section in the knowledge bundle: semantics table, from-phase latch behavior (never via Failed, sticky through it), on_change x emitOnlyIfExists composition, restart re-emit, no-phase-signal caveat, old-agent degradation, and the prerequisite toggle-wipe fix. Index + log updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b005c20598 |
feat(agent,backend,web): config-channel agent endpoint migration (MigrateToApiBaseUrl)
Re-home agents to a new API base URL through the agent/config response (backend move or per-tenant EU->US region move) - the lesson from the 2026-07-18 WEU->GWC cutover, where the compiled-in API hostname hard-cut every in-flight agent (no DNS indirection possible: mTLS breaks behind a proxy, Flex custom-domain TLS is broken). Backend (ConfigVersion 33): - AdminConfiguration.AgentMigrateApiBaseUrl (global) + AgentMigrateTenantOverridesJson (tenantId->URL; empty value pins a tenant against the global target; override wins) - GetAgentConfigFunction.ResolveMigrateTarget serves the validated target; AgentMigrateServed / AgentMigrateRejected warnings as delivery evidence - TableConfigRepository Store+Map; admin portal Global Settings round-trip Agent (kill-switch-mirrored hardening): - Shared AgentEndpointMigrationRules: https-only, *.azurewebsites.net on a label boundary, no path/port/userinfo - enforced on BOTH sides - Live fetch only: RemoteConfigService cache strips the field on write AND read; EndpointMigration.ResolveTarget re-checks the fetch outcome - AgentRuntimeConfig.Resolve returns before merge/integrity/bootstrap cleanup on a pending migration (no spurious IntegrityCheckFailed against a stale migration stub), Program.RunAgent rebuilds Phase 3+4 against the new URL - single hop, kill signal wins before and after the hop Tests: AgentEndpointMigrationTests (backend rules + resolution), EndpointMigrationTests + cache-hardening strip (agent). Full suites green (Functions 2992, Agent Core 1822, vitest 557, tsc silent). Doc: docs/agent/endpoint-migration.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6d7783566f |
feat(ci): shared build-counter blob, CI build parity, ZIP provenance attestation
Build numbers now come from a public counter blob (buildcounter-v2.txt, reserve-before-build with ETag-CAS) shared by local build.ps1 and CI, so the two paths can never mint the same number. The CI step that committed buildcounter.txt bumps to main is gone; the repo file is now a gitignored offline fallback for -SkipUpload smoke builds only. CI parity fixes vs the local build: - ship SummaryDialog.exe.config in the ZIP (WPF AppContextSwitchOverrides; without it the dialog dies with 0x8007045A when spawned from SYSTEM) - verify EXE version == manifest == reserved build number before zipping - update the AdminConfiguration hash-oracle only on publish_as_stable CI-built release ZIPs now carry a GitHub Artifact Attestation (Sigstore keyless provenance, actions/attest-build-provenance@v4). Doc: docs/agent/build-counter-blob.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cd870e639d |
feat(agent+rules): evidence-backed ZTD diagnostics for missing Autopilot profiles
Agent (autopilot_profile_missing evidence package): - ZtdEvidence collector: one-shot all-level query of the ModernDeployment Autopilot event channel (807/809/815/908 errors + 100/153/160/161/163/164 flow events) -> ztdVerdict field (device_not_registered / no_profile_assigned / download_attempted_no_profile_returned / ...); the continuous watcher can't supply this (filters out Info) - Diagnostics\Autopilot registry dump (IsAutopilotDisabled, TenantMatched, CloudAssignedTenantDomain/Id) on every autopilot_profile event, flat evidence fields on the missing event - ztd.dds.microsoft.com reachability probe (official endpoint per the requirements page - several blogs cite ztd.ssd.*, which is wrong); volatile probe fields excluded from the restart-dedup fingerprint; Intune diag-upload endpoint deliberately NOT probed (user decision) Analyze rules (known-issue map from Microsoft Learn): - ANALYZE-ENRL-003: TPM attestation known error codes (0x800705B4, 0x801C03EA, 0x81039001/23/24, 0x80070490) - ANALYZE-ENRL-004: enrollment-blocked known codes (0x80180014, 0x80180018, 0xC1036501, 0x801C03F3, 0x80070774) - ANALYZE-ENRL-005: hybrid-join 0x80004005 timeout with per-version KB fix builds (gated on isHybridJoin via confidence threshold) - ANALYZE-DEV-007: NTP clock skew > 2min (documented TPM-attestation/ESP timeout cause) using existing ntp_time_check offsetSeconds Docs: new OKF reference docs/agent/autopilot-ztd-diagnostics.md (event-ID table, diagnostic registry, endpoints, error-code map, sources + re-check RSS feed), linked from ZtdEvidence/ModernDeploymentTracker code comments, indexed in docs/index.md + agent/index.md + log.md. Tests: 11 ZtdEvidence verdict tests; full V2 suite 1799 green; rule schema validated (ajv draft2020); backend rule tests 123 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
108c52be5e | updated docs - moved customer graph permission to customer docs | ||
|
|
977ad37463 |
fix(agent): HelloWizardStarted signal — never complete while the Hello wizard is running (session 772fe502)
A flip-flopping user-scoped WHfB CSP read "disabled" once, the arm-C completion synthesized HelloOutcome="Skipped" and the session completed 5s later while the user sat inside the Hello wizard that had started 230ms earlier (Shell-Core 62404, previously InformationalEvent-only). Layer 1 — engine (DecisionCore): - New DecisionSignalKind.HelloWizardStarted + set-once fact DecisionState.HelloWizardStartedUtc (additive-nullable, census + snapshot wired). - Prevention: centralized HelloSatisfiedForCompletion / HelloPolicyDisabledWithoutWizard predicates veto the policy-disabled stand-in at all five Skipped-synthesis sites and in the completion_waiting prerequisites; the AdvisoryCompletion handler gets a HelloWizardObservedPromote branch (its fallthrough is re-arm-or-fail). - Cure: HandleHelloWizardStartedV1 retracts an engine-synthesized "Skipped" (exact-case discriminator vs the lowercase tracker vocabulary; first fact-nulling reducer, stage-agnostic because the RealmJoin gate-defer parks synthetic skips pre-Finalizing), cancels finalizing_grace, returns to AwaitingHello and arms HelloSafety. - Agent path: ShellCoreTracker raises HelloWizardStarted before FinalizingSetupPhaseTriggered; 62404 included in the startup backfill (fire-once); coordinator timestamp mirror; adapter emit (fire-once). Layer 2 — HelloTracker confirmation read: - ApplyPolicyRead state machine: "disabled" commits only after a second confirming read (~10s poll); "enabled" commits immediately; disabled→enabled flags flipFlopDetected=true; pending keeps the policy-unknown wait cadence. Audit fields confirmedReads / flipFlopDetected on hello_policy_detected. Fixtures: signal-kinds/hello-wizard-started-v1.json + enrollment-sessions/userdriven-hello-wizard-unskip-v1.jsonl (772fe502 ordering, ends Completed with HelloOutcome=completed). Tests: DecisionCore 462/462, V2.Core 1775/1775. Docs: OKF concept doc docs/agent/hello-wizard-unskip.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ec5d5661ec |
docs: use relative links instead of OKF bundle-absolute links
GitHub resolves /-prefixed links against the repo root (missing /docs), so cross-doc navigation broke. Switched all agent docs to standard relative links and updated the bundle convention in index.md + CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4ccb3b2317 |
docs(agent): OKF concept docs for the V2 agent
Three evidence-based documents under docs/agent/: runtime overview (lifecycle, collector hosts, single-rail telemetry pipeline), decision engine (DecisionCore reducer, signals/effects, A/B/C completion arms, invariants, threading), and logs & persistence (artifact inventory, signal log vs spool rationale, replay-based crash recovery, quarantine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
923a1c793a |
docs: convert /docs into an OKF v0.1 knowledge bundle
Adopt the Open Knowledge Format (GoogleCloudPlatform/knowledge-catalog) for contributor/AI-facing technical docs: YAML frontmatter on the three existing concept documents, new index.md (progressive disclosure, root carries okf_version) and log.md, plus the maintenance convention in .claude/CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |