Commit Graph

2313 Commits

Author SHA1 Message Date
Oliver Kieselbach 0ffcb2fde2 command update v2.0.1400 2026-08-17 21:44:26 +02:00
Oliver Kieselbach a3089753d3 IME patterns: repair drift found by decompiled-source audit (IME 1.97 Dec-25)
Audit vs decompiled IME 1.97.107: ~55/79 patterns verified verbatim, but
several were broken or dead against the current V3 pipeline.

Fixes:
- IME-ESP-TRACK-STATUS: drop trailing anchor (line grew ' with name: <n>.')
- HS-COMPLIANCE: tolerate both 'remdiation' typo and corrected wording;
  new HS-COMPLIANCE-POST for the reworded post-detection line
- IME-DETECTION-RESULT: capture DetectedDifferentVersion (ordered first)
- IME-ENFORCEMENT-SUCCESS: unpin OldValue (Error/PendingReboot/null paths)
- IME-ERROR-REPORT: allow unquoted null OldValue on first write
- Disable 11 dead patterns (emitters removed with IME's old V1/V2 pipeline;
  IME-DETECTED-OLD additionally collided with IME-SKIP-INSTALL-DETECTED)

New signals (existing actions, tailed files only):
- IME-ESP-APP-REGISTERED + IME-CACHE-MISS as app-name sources (replacing
  dead IME-UPDATE-NAME-1/2/3)
- IME-WINGET-ERROR (WinGet failures were invisible)
- IME-EXITCODE-MAPPED-FAILED (normal mapped-exit-code failure path)
- IME-STOP-SIGNAL (service stop != OS shutdown)
- IME-RESTART-STOP-PROCESSING (reboot-gated processing abort)

Backend seed diff now also compares Enabled + Parameters - enable/disable
flips and parameter changes previously never propagated to the table.

All exact wordings verified against decompiled source; regex smoke-tested
via validate-ime-patterns.py (engine=regex, 75/75 compiled, all new
patterns fire on synthetic lines with the exact IME format strings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 21:33:37 +02:00
Oliver Kieselbach 26cbc897c5 WDP: unify enrollment_type_detected with detector + export locked evtx
Two fixes from the a11102f4 diag analysis:

- enrollment_type_detected re-implemented the old CloudAssigned*-only
  detection and kept reporting "Autopilot v1 (Classic ESP)" on WDP devices
  while the session row already said v2. DeviceInfoCollector now uses
  EnrollmentRegistryDetector.DetectEnrollmentTypeWithRule() - the same
  detector as registration - and the event names the rule that fired.
- Tenant-configured .evtx paths never made it into the diagnostics ZIP:
  active event-log channels hold their file exclusively locked. The
  package service now exports the channel via wevtutil epl (channel name
  derived from the %4-encoded file name) and streams the temp export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 21:28:43 +02:00
Oliver Kieselbach c88629dcf2 Bootstrap MSI: thin MDM LOB delivery vehicle for WDP
WDP batch order is fixed (User -> SLDM/MDM apps -> Scripts -> Win32Apps),
so a WDP-policy script can never observe the app phase live, and a hard
Batch-1 failure leaves zero visibility. LOB MSIs install via OMA-DM during
the first sync - before the SLDM batch - making them the earliest delivery
channel. The MSI carries no logic: it installs one fail-soft runner script
that downloads and executes the CURRENT server-hosted
Install-AutopilotMonitor.ps1, so all real bootstrap logic stays updatable
without re-shipping the MSI. Skips via the Deployed regkey when another
channel won the race. Build: wix build Package.wxs -arch x64 (WiX 5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 20:35:37 +02:00
Oliver Kieselbach 517041a51c WDP detection: probe DevicePreparation BootstrapperAgent orchestration key
Field-verified on a live WDP enrollment (session 7b072755): WDP devices
carry NO Autopilot profile, so both CloudAssigned* v2 signals are
structurally absent and every WDP session was classified v1. The reliable
marker is the Device Preparation orchestration tree
AutopilotSettings\DevicePreparation\BootstrapperAgent, whose
ExecutionContext JSON carries the WDP policyId - present on WDP, absent on
classic v1 (verified there too; classic only has ESP category VALUES named
DevicePreparation.* on the parent key, hence a subkey probe).

CloudAssigned* checks stay as defence in depth. Profile seeding and the
downgrade guards from earlier today take over once registration carries v2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.0.1399
2026-08-17 19:08:26 +02:00
Oliver Kieselbach eeaeb9ea7e Corp identifier: match in Intune-normalized space
Field-verified against the portal list: Intune normalizes corporate
identifiers on upload - uppercased, all non-alphanumerics stripped per
component ("Microsoft Corporation,Virtual Machine,7801-5131-..." is stored
as "MICROSOFTCORPORATION,VIRTUALMACHINE,78015131...") and the device
matched + enrolled against that normalized entry. Our exact-match against
the raw device values (and the contains() filter using the dashed serial)
could therefore never hit.

Both sides now normalize before comparison, and the contains() narrowing
uses the normalized serial. Raw-form entries created directly via Graph
still match. The not-found message shows raw AND normalized form so
rejection logs line up with the portal list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 18:25:05 +02:00
Oliver Kieselbach 3983402983 Corp identifier: read importedDeviceIdentities via GET, no new Graph scope
The searchExistingIdentities ACTION requires
DeviceManagementServiceConfig.ReadWrite.All, which the app deliberately does
not hold - the feature was broken in every tenant. Listing the same data via
GET works with Read.All, which the app already has for
windowsAutopilotDeviceIdentities. No new permission, no customer re-consent.

- contains(importedDeviceIdentifier,serial) filter for server-side
  narrowing (mirrors AutopilotDeviceValidator), exact case-insensitive
  match client-side on manufacturerModelSerial identities only
- 400 filter rejection falls back to an unfiltered paged scan
  (60 x 1000 covers the portal maximum of 10 CSVs x 5000 rows);
  page-cap overrun is transient, never a cached false negative
- Graph 401/403 stays a definitive, negative-cached failure with an
  actionable message (now naming Read.All)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:47:25 +02:00
Oliver Kieselbach 4deb5e6a04 Corp identifier: MMS-only matching, Graph 401/403 = definitive failure
Two changes from the WDP live-test follow-up:

- Remove the serialNumber-type probe again (decision: only the documented
  Windows manufacturerModelSerial identifier authorizes; a serial-type entry
  is an Intune-side admin misconfiguration and must not count).
- Classify Graph 401/403 on searchExistingIdentities as DEFINITIVE with an
  actionable message naming the missing permission
  (DeviceManagementServiceConfig.ReadWrite.All / admin consent) and
  negative-cache it. Field case: the missing consent was classified
  transient and trapped the agent in an endless 503 Retry-After loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:36:21 +02:00
Oliver Kieselbach 5734eb576e Correct corp identifier dual-probe rationale in comments
Docs research showed the serial-only corporate identifier type is NOT the
documented WDP path (Windows only supports manufacturer,model,serial; Intune
ignores serial-type for Windows ownership). The dual probe stays because the
serial type is the only one the portal offers for MANUAL entry, so admins
land there by mistake and an uploaded serial still expresses authorization
intent. Comments and test docs now state that rationale instead of the
incorrect "WDP guidance uses serial type" claim. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 17:25:35 +02:00
Oliver Kieselbach 7fbfea8f4e Corp identifier validation: probe serialNumber type + unmask validator errors
First live WDP enrollment 403d at device validation. Root causes:

- CorporateIdentifierValidator only searched the manufacturerModelSerial
  identifier type, but the Device Preparation guidance has admins upload
  SERIAL-type corporate identifiers - those never matched. The Graph
  searchExistingIdentities call now probes both types in one round-trip.
- SecurityValidator overwrote deviceValidationError per validator, so the
  403 Details only showed the LAST failure (CloudPc) and hid the corporate
  identifier miss. Errors now aggregate in chain order and the transient
  flag is sticky-OR (any transient failure yields 503 Retry-After instead
  of a definitive 403).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 16:41:57 +02:00
Oliver Kieselbach e5f955354d Hourly SessionSweep interleave: stalled-session sweep every hour instead of 2h
New SessionSweepFunction (cron 0 30 * * * *, offset from the 2h Maintenance
timer at minute 0) runs only MarkStalledSessionsAsTimedOutAsync, halving the
sweep-induced classification latency; thresholds unchanged. The sweep now
returns a SessionSweepResult (counts + catastrophic-failure message) so the
new timer emits SessionSweepCompleted heartbeats / SessionSweepFailed errors
(dual-registered in the ops-alert web catalog). No lease: the sweep is
idempotent, overlap with the 2h chain is a no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 14:33:26 +02:00
Oliver Kieselbach 65651dd599 ANALYZE-ID-004 v1.2.0: pre-logon VPN + Hello-as-symptom self-service guidance
From the 3b7ba31f (succeeded) vs gardner/esa (parked) hybrid session comparison:
explanation now covers the cached-credentials/pre-logon-VPN variant and why the
Hello wizard never appearing is downstream of the missing PRT; new remediation
block for VPN machine tunnels, cloud Kerberos trust, explicit Hello disable,
update deferral and pre-provisioning. Links the new docs article.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:36:10 +02:00
Oliver Kieselbach d0ab3eee40 WDP hardening: sticky-v2 EnrollmentType + DevicePreparation downgrade guards
Two low-risk fixes ahead of the first live Device Preparation enrollment:

- StoreSessionAsync: EnrollmentType is now sticky-v2 across re-registration
  (same contract as the sticky-true booleans) so a degraded AutopilotSettings
  read after a reboot can no longer flip an observed v2 session back to v1.
- EnrollmentScenarioProfileUpdater: ImeUserSessionCompleted,
  SelfDeployingDeadlineConfirmed and WhiteGloveSealingConfirmed no longer
  overwrite an established DevicePreparation profile (seeded at Medium from
  the registry-deterministic v2 fact).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.0.1398
2026-08-17 11:55:19 +02:00
Oliver Kieselbach 2de8f5c4e6 Merge branch 'main' of https://github.com/okieselbach/Autopilot-Monitor 2026-08-17 09:35:54 +02:00
Oliver Kieselbach fe07f5cef0 command update 2026-08-17 09:35:07 +02:00
Oliver Kieselbach 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>
2026-08-17 09:34:03 +02:00
Claude 91b702e239 Fix squeezed Active Users header on mobile
The presence page header was a non-wrapping `flex justify-between`, so on a
phone viewport the title column was compressed to a few characters wide and
"Active Users" plus its description broke one or two words per line next to
the Window/Refresh controls.

Stack the title and controls vertically below the `sm` breakpoint and keep
the existing side-by-side row from `sm` up. The controls also wrap among
themselves on very narrow screens.

The user table had the same problem: its card used `overflow-hidden`, which
clipped the Tenant and Last seen columns off-screen with no way to reach
them. Wrap the table in the repo's usual `overflow-x-auto` container so it
scrolls inside its card, keep cells on one line, and trim cell padding to
`px-4` on mobile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwjVj2PvpVRk7QUjQf3i8C
2026-08-16 13:19:10 +00:00
Oliver Kieselbach e12da0c99b geo metrics: median API latency as display statistic
A single corrupt session average (request spanning sleep/hibernate counts
wall-clock hours) inflated the request-weighted location mean by orders of
magnitude (Amersfoort: 73286 ms from one 1.38M-ms session). Add
MedianApiLatencyMs per location + global (upper median over per-session
averages, duration convention), base ApiLatencyVsGlobalPct on medians, and
switch web display/sort to the median with the weighted avg in the tooltip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 14:59:42 +02:00
Oliver Kieselbach 3ac647a680 mcp tests: exact hostname match in Entra fetch stub (CodeQL alert #76)
The test-only fetch stub matched the Entra authority via substring
(includes), which CodeQL flags as incomplete URL sanitization. Parse the
URL and compare the hostname exactly instead — also strictly more precise
for what the stub intends (intercept only the Entra host).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 11:06:28 +02:00
Oliver Kieselbach ecf6e8c5a8 clickable TipOfTheDay 2026-08-15 10:59:54 +02:00
Oliver Kieselbach f26721c0f7 web: progress-portal serial-knowledge model — lookup client, serial proof on events/join, reconnect re-proof, visible join denials
- useProgressSearch calls the server-side lookup; the client-side filter
  over the tenant list is gone with the list itself.
- useProgressEvents passes the serial on every events read and refreshes
  the session summary via the lookup (guarded by sessionId so a newer
  re-enrollment of the same serial cannot swap the selected session).
- SignalRContext.joinGroup takes {serialNumber, onDenied}; the serial is
  remembered per group and re-presented on the automatic post-reconnect
  rejoin — without that, a roleless viewer's live updates would die on
  the first network blip. A refused join is now visible (console.warn +
  progress-page notification) instead of a silently frozen page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:42:36 +02:00
Oliver Kieselbach 789926fcad backend: progress-portal serial-knowledge model — server-side lookup, serial proof on events + session-group join
Security finding: GET progress/sessions shipped the tenant's latest 100
sessions to any authenticated (roleless) user with the serial filter
applied only in the browser; events and SignalR session-group joins were
open to any known sessionId.

Model now: knowing the device's serial number is the authorization proof.
- progress/sessions/lookup (AuthenticatedUserWithRole + QueryParam)
  resolves at most ONE session server-side: roleless callers need the
  exact serial/device name, members/GA keep substring search. The
  tenant-wide list route is gone (catalog fail-closes it).
- progress events require ?serial= re-proof on every read; missing
  session and serial mismatch answer the same 404 (no existence oracle).
- Session-group joins by roleless same-tenant callers require the serial
  (SerialKnowledgeProof, shared with the events route); member/GA/
  delegated joins are unchanged. MemberRead on progress routes stays
  deliberately rejected (regression history c4dabeee).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:42:24 +02:00
Oliver Kieselbach 01e76a6ff7 mcp: OAuth-proxy hardening — Entra token timeout, https boot guards, replay note
- /oauth/token: AbortSignal.timeout(15s) on the outbound Entra fetch;
  TimeoutError maps to 504 token_exchange_timeout (502 stays for hard
  failures). Undici defaults previously allowed ~300s per stalled request.
- Boot guards (fail-fast, matching config.ts posture): AUTOPILOT_ENTRA_AUTHORITY
  must be https (sovereign clouds allowed, host not pinned); MCP_PUBLIC_URL pin
  must be https (http tolerated for loopback dev only), trailing slashes
  stripped; production log hint when AUTOPILOT_ENTRA_CLIENT_SECRET is unset
  (public-client vs AADSTS7000218 misconfig).
- State one-time-use: documented as accepted residual risk (a nonce store would
  reintroduce the shared state the stateless multi-replica design removed;
  impact bounded by single-use PKCE-bound Entra codes + callback re-validation).
- Tests: oauth-boot-guards.test.ts (validators) + token-timeout route tests
  with Entra-host-scoped fetch stub. Suite: 559 passed, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 09:31:45 +02:00
Oliver Kieselbach daa6df6bab rules: evaluate 30 device-phase built-ins at the WhiteGlove seal, 6 also on_event
Every built-in whose evidence exists by the end of pre-provisioning Part 1 now
carries evaluateOn [enrollment_end, whiteglove_sealed] — app failures, low disk,
memory pressure, unsupported build, WU failures, clock skew, ESP device
subcategories, mid-ESP reboots, retry-recovery, cert store, local admins,
critical vulnerabilities, firmware CA2023, Secure Boot, AutoLogon, PPKG scans,
Office install and hybrid/TPM/MDM known-error codes. Six rules additionally
evaluate when their (rare, acute) trigger event arrives:
on_event:app_install_failed → APP-002/004/007 + CORR-003,
on_event:windows_update_failed → DEV-004,
on_event:modern_deployment_error → ENRL-003.

Deliberately enrollment-end only: APP-010/ESP-004/ENRL-001 (require
enrollment_complete/failed), ENRL-002 (session_timeout), DEV-001 (Hello =
user phase), APP-015 (user phase, terminal-precondition design), ID-004
(keeps its own hybrid_login_pending trigger). Minor version bump on all 36
changed rules (seed contract); rule JSONs normalized to 2-space formatting;
semantic equality vs HEAD verified (only evaluateOn/version differ).

Backend 4157 tests green (APP-013 version pin updated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:29:45 +02:00
Oliver Kieselbach 98fd173fb8 rules+backend+mcp: rule-verify sweep fixes (dead factor, {{token}} evidence, stats cap, list stringify)
Full verify pass over all 54 built-in rules (validate_rule sweep, 30d/7d
get_rule_stats, test_analyze_rule spot checks). Fixes:

- ANALYZE-APP-007 v1.1.0: declare enrollment_failed as a non-required
  condition — the exists-factor referenced an undeclared signal and could
  never apply.
- ANALYZE-APP-014 v1.1.0: add non-required event_data conditions for
  espCategory/espErrorCode/topCandidatePackage/candidateCount/confidence
  so all explanation {{tokens}} resolve instead of rendering literally.
- ANALYZE-ENRL-002 v1.2.0: capture session_timeout.timeoutHours so
  {{timeoutHours}} interpolates (verified via dry-run: renders "5").
- RuleEngine TryFlatLookup/ResolveDotPath: flatten IEnumerable data
  values to a comma-joined string — List fields (e.g. app_tracking_summary
  .pendingNames) stringified to "System.Collections.Generic.List`1[…]",
  breaking contains-matching and {{pendingNames}} interpolation.
- GetRuleStatsAsync: raise the 500-row cap to 10000 and log when hit.
  The scan walks dates ascending, so the old cap silently dropped the
  NEWEST dates of a 30d window — newly added rules (e.g. ANALYZE-SEC-008)
  were entirely missing from global stats.
- MCP validate_rule lint: accept event_data_array itemField as a {{token}}
  resolver — {{identity}} in ANALYZE-SEC-005/006 was a false positive.

Backend 4157 tests green, MCP 548 green + tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:50:29 +02:00
Oliver Kieselbach 06626a4095 web: evaluateOn in the analyze-rule editor + interim-trigger badge
The evaluateOn feature was API/MCP-only — portal users could not see or set
evaluation triggers. The rule editor gains an Evaluation Triggers section
(enrollment end / WhiteGlove seal checkboxes + on_event type list) with
client-side grammar + hard-block validation sourced from the baked guardrails
mirror (combine.js now emits BLOCKED_INTERIM_TRIGGER_EVENT_TYPES for the web);
rule cards show an 'Interim: …' badge for interim-enabled rules (incl. the
built-in ANALYZE-ID-004). Create/edit payloads round-trip evaluateOn; the
template-copy path already deep-clones it server-side.

Suites: web 762 + tsc + lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:34:56 +02:00
Oliver Kieselbach 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>
2026-08-14 16:07:39 +02:00
Oliver Kieselbach 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>
2026-08-14 15:53:02 +02:00
Oliver Kieselbach 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>
2026-08-14 15:03:25 +02:00
Oliver Kieselbach 6c6014940e rules: hybrid user-affinity + never-settled-app detection, dsregcmd on_event snapshot, evidence interpolation fixes
ANALYZE-ID-004 (hybrid sign-in never establishes Entra user affinity),
ANALYZE-APP-015 (required app pending forever, ESP completion deferred),
ANALYZE-ENRL-002 v1.1 (desktop-reached + apps-done evidence), GATHER-DEVICE-006
(dsregcmd /status triggered by hybrid_login_pending). Engine: list evidence
values now comma-joined for UI/{{token}} interpolation; timeoutHours added to
the evidence whitelist so ANALYZE-ENRL-002's explanation interpolates.

Motivated by esa-logistics sessions 3ac8cb0e/95f71149 (user-phase stuck,
GlobalProtect never settling, placeholder identity persisting across reboots).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:39:16 +02:00
Oliver Kieselbach c60c0bbede settings: admit Operators and the GlobalReader read-only (user decision)
The /settings layout now admits any own-tenant role or platform scope via
hasOwnTenantOrPlatformRole — the same set the sidebar's Configuration
visibility uses, closing the "sidebar shows it, guard bounces it" mismatch
the nav matrix test had ratcheted. Read-only stays enforced by the data
layer: MemberRead serves non-admins redacted config, canEditConfig is
false, and every write path (PUT + field PATCH) is TenantAdminOrGA.
This also realigns the code with the customer docs, which already
documented Operators/Viewers as read-only settings viewers.

The guard-consistency ratchet is now empty and may only stay empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 00:04:13 +02:00
Oliver Kieselbach 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>
2026-08-13 23:17:48 +02:00
Oliver Kieselbach 6dc2506fc5 web: latch server-side session search to once per query
Live verification after the P5/P6 deploy caught a request storm: the dashboard
auto-sweep effect refires whenever the loading flags toggle, and a q= sweep
whose matches were all already loaded changes nothing the effect watches — so
the same /api/search/sessions request looped indefinitely (~6 req/s observed).
The old loadAll() sweep terminated the effect via hasMore=false; searchAll()
deliberately leaves pagination state alone and therefore needs its own latch.

searchAll now runs at most once per trimmed query (latched before the first
await, also on failure); refetch/refetchWith/global-mode-toggle clear the latch
so a new backend scope may search again, and the explicit "Search all sessions"
button bypasses it via force. Ref declared with the sibling refs BEFORE the
mutating callbacks — the React compiler only accepts ref mutations when the
useRef declaration precedes the closure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 20:24:05 +02:00
Oliver Kieselbach 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>
2026-08-13 19:29:27 +02:00
Oliver Kieselbach f4d99b43f4 chore: remove accidentally committed verification screenshots
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:04:40 +02:00
Oliver Kieselbach 6c2b431aec sessions: viewer-aware empty state for analysis results
The empty state no longer tells read-only viewers to click a button
they do not have.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:04:16 +02:00
Oliver Kieselbach 383d0694fe rbac: wire Viewer as a real read-everything/change-nothing tier
Four-tier model: Member (Progress Portal only) < Viewer (sees everything,
changes nothing) < Operator < Admin. Backend policies already treated
Viewer correctly (MemberRead admits it, every write tier excludes it) —
this change makes the tier reachable and safe:

- role assignment now validates against Constants.TenantRoles
  (case-insensitive, canonicalized, 400 on unknown) — previously ANY string
  was persisted; also fixes the case-sensitive last-admin guard
- Viewer is assignable in all four role dropdowns; About Roles describes
  the four tiers
- shared hasTenantReadScope() replaces four hand-rolled predicates
  (AuthGate, dashboard x2, useDashboardSessions); Viewers land on the
  dashboard and the full navbar/sidebar ("adminOrOperator" visibility
  renamed to "tenantMember"); Settings admits Viewer read-only
- "Analyze now" stays Admin/GA/Operator (recompute trigger, not a view)
- deleted dead IsTenantMemberAsync (meant Admin-or-Operator — the inverse
  of RequestContext.IsTenantMemberRole)
- 16 endpoint tests for role validation, 15 tests for the scope helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:51:59 +02:00
Oliver Kieselbach 792b7d3b0e agent: enforce DecisionState.SchemaVersion at recovery (branch a2)
A snapshot written by a different state-schema generation (e.g. the
pre-self-update binary) is no longer trusted: it is quarantined like a
corrupt cache and the state is rebuilt from the authoritative SignalLog,
whose signals carry their own KindSchemaVersion — replaying them through
the current reducer is the defined upgrade path. Unreadable log escalates
to the existing total-loss quarantine. Closes the silent-Newtonsoft-default
window where a reshaped field could cause mid-session amnesia across a
self-update. Two adversarial recovery tests pin both paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:51:58 +02:00
Oliver Kieselbach c48f95b88c tests: cross-language parity manifest + agent safety nets (audit P3/P4)
P3 — shared manifest infrastructure: SharedManifestParityTests reflects the
wire-relevant catalogs out of AutopilotMonitor.Shared into a committed JSON
manifest (regenerate: AM_WRITE_SHARED_MANIFESTS=1 dotnet test); a web codegen
lifts it into literal types; sharedManifestChecks.ts enforces
AdminConfiguration field parity at COMPILE time and 12 vitest tests pin the
value catalogs (sources/operators, lanes/verdicts, severities incl. the
missing Trace=-1, phases both directions, terminal statuses, event-type
typo guard). KnownSources/KnownOperators made internal for the export.

P4 — agent safety nets: DecisionState builder/serializer completeness
(4-site sync tripwire), signal-kind fixture coverage with a shrink-only debt
set (found and fixed realmjoin-timeout-v1.json, which never deserialized),
RemoteConfigMerger completeness tripwire, DefaultComponentFactory ordering
invariants as real construction tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:18:18 +02:00
Oliver Kieselbach 4c413337ca annotations: serve writableLanes from the backend write matrix (audit P2)
The GET now computes writableLanes with the SAME function the PUT re-gates
with (IsLaneWritableByCaller, own-tenant binding included) and the card
renders exactly that list — the hand-mirrored TS matrix is deleted. Fixes a
latent UX authz bug (a GA without an own-tenant admin role was offered the
tenantadmin lane and would have hit a 403). Older backends without the field
fail closed to read-only, so web can ship independently; save affordances
appear once the backend is deployed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:18:05 +02:00
Oliver Kieselbach 8277d70915 fix: close live catalog/type drift across web, backend and agent (audit P1)
- adminConfig.ts: drop retired V1 latestAgent*/customSettings ghost fields,
  add the 8 backend fields the interface was missing
- ops alerts: register the Sla category (BreachNotification/ConsecutiveFailures/
  EvaluationCompleted) + OrphanEventsCleaned so operators can route them
- analyze rules: expose app_install_duration in the builder SOURCES;
  fix stale XML docs (event_data_array, count_per_group_gte/in/not_in)
- UploadIntervalSeconds: collapse 5 diverged defaults (10 vs 30) onto
  Constants.DefaultUploadIntervalSeconds = 10 (the served value)
- combine-rules workflow: commit ALL three combine.js outputs and include
  rules/guardrails.json in the path filters
- api.ts apps sessions builders: move trailing optional strings into an
  options object (adjacent optional strings swallowed a forgotten tenantId)
- SummaryDialog: canonical phase display names ("Apps (Device)")
- mcp-server.bicep: fix pre-EU-cutover hostname in deploy docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:17:51 +02:00
Oliver Kieselbach e1b3ec25fd web: wrap analysis-card context notes on mobile; fix NaN critical path
The confidence bar and the two fleet-context notes on an analysis finding
shared a non-wrapping flex row, so on phones they squeezed into
word-per-line columns — the row now wraps, each note taking its own line.

Also visible in the same screenshot: "critical path NaNh NaNm" in the
ESP-blocking apps block. The backend omits null fields (WhenWritingNull),
so espAppsOccupancySeconds arrives as undefined and the strict !== null
guard let it through to formatDuration. Loose != null check now hides the
label when the value is absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:33:35 +02:00
Oliver Kieselbach 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>
2026-08-13 13:39:03 +02:00
Oliver Kieselbach 842db1603a insights: fleet context on analysis cards + rule-filtered session deep link
Each fired analyze rule on the session detail page now reads "Fired in N
enrollment(s) in the last 14 days" (summed client-side from the existing
rule-stats trend — enrollments, never devices; RuleStats carries no serial)
with a "View sessions" deep link. The dashboard accepts ?ruleId=, fetches
the rule's hit-session ids once from the new GET /api/metrics/rule-hit-sessions
endpoint (MemberRead + QueryParam scoping, capped 2000 with a truncated flag)
and filters the loaded list client-side behind a clearable chip.

Side fix: the section's stats fetch now follows the session's tenant for
GA/delegated viewers (global route + tenantId) instead of showing the
viewer's own-tenant numbers next to a foreign session. Dashboard links in
fleet/fleet-health now go through the new canonical dashboardUrl() builder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 13:38:52 +02:00
Oliver Kieselbach c770bbffcf build: keep NuGet audit advisories as warnings in the warning freeze
A CVE published against an already-referenced package must not block an
unrelated hotfix deploy; advisories are handled by bumping the package.
WarningsNotAsErrors NU1901-NU1904 - everything else stays fatal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.0.1397
2026-08-13 09:49:27 +02:00
Oliver Kieselbach 3fbdc8ef62 build: freeze .NET builds at zero warnings (warnings-as-errors)
Repo-wide Directory.Build.props: TreatWarningsAsErrors +
MSBuildTreatWarningsAsErrors. Applies to compiler, MSBuild and NuGet
restore warnings including audit advisories (NU1901-NU1904) - a newly
published CVE fails the build until addressed (fail-closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:44:40 +02:00
Oliver Kieselbach 35cea4fe64 build: clear all remaining .NET build warnings to zero
- CmTraceLogParser: annotate TryParseLine (string? line, [NotNullWhen(true)]
  out entry), init string properties (CS8618/CS8625/CS8604)
- Shared: internal NotNullWhen polyfill for netstandard2.0; multi-target
  netstandard2.0;net10.0 so net10 consumers use the BCL attribute and the
  polyfill cannot collide via InternalsVisibleTo (CS0433)
- AgentEndpointMigrationRules.TryNormalizeTarget: now carries [NotNullWhen(true)]
- Functions.csproj: drop DataProtection/Cryptography.Xml security pins made
  redundant by .NET 10 framework-package pruning (NU1510)
- Functions.Tests.csproj: add Microsoft.AspNetCore.App FrameworkReference so
  pruning applies there too and the pins stay unnecessary (NU1903/NU1904)
- Agent tests: nullable test-helper param, Assert.NotNull before dereference

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:38:41 +02:00
Oliver Kieselbach 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>
2026-08-13 09:27:31 +02:00
Oliver Kieselbach 83da522dd7 ci: freeze the web lint warning cap at zero (batch 5)
The 99-warning legacy backlog from the React 19 / eslint-plugin-react-hooks
v6 upgrade is fully burned down — CI now fails on any new warning. The
compiler rules stay demoted to "warn" so local dev output remains readable;
the eslint config comments now point to the established fix patterns
instead of the retired ratchet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 19:23:44 +02:00
Oliver Kieselbach c15394df87 web: refactor context/sidebar/theme effect-state sync, last lint sites to zero (batch 3)
- AuthContext: delete the SSR isLoading effect — effects never run on the
  server, the guard was unreachable dead code.
- TenantContext: mirror user.tenantId via adjust-during-render; the value
  is still retained while auth resolves and cleared on settled sign-out.
- Global/Tenant notification contexts: scope-masked empty list derived in
  the provider value instead of effect-clearing; loading flag moved into
  the canonical async wrapper.
- New shared hooks/useMediaQuery (useSyncExternalStore, configurable
  server snapshot) — consumed by GlobalSidebar (isDesktop) and
  McpTerminalDemo (prefers-reduced-motion, finished transcript derived).
- GlobalSidebar: route-mode active section derived from pathname;
  active-group and pathname auto-expansion as adjust-during-render, state
  now holds user toggles only.
- ThemeContext: theme read via useSyncExternalStore backed by
  lib/themeStore (manual preference beats OS preference in the snapshot;
  same-tab writes notify through the store's listener list).
- TipOfTheDay: random tip via useSyncExternalStore with module-memoized
  pick (build-time Math.random would freeze one tip and mismatch on
  hydration); re-rolls per mount via cleanup.
- Navbar: previewMode block removed entirely — nothing in the codebase
  consumes the flag anymore, it only persisted itself.
- activation page: ?demo flag via useSyncExternalStore, tri-state null
  sentinel dropped.

Lint is at zero react-hooks warnings repo-wide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 19:17:55 +02:00