mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
fc778b4154ee9a059e7f43ce8aee5dea3f4eeaf0
1194 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fc778b4154 |
feat(agent-v2): V1-parity remediation — lifecycle events, auth/binary watchdogs, remote config
Closes the main V1/V2 divergences identified by the parity audit (tasks/plan_v2_parity.md). PR #47 — RemoteConfigMerger V1-parity - Tenant remote wins unconditionally for every 1:1-mapped knob (V1 parity with MonitoringService.ApplyRuntimeSettingsFromRemoteConfig). CLI flags only seed the initial AgentConfiguration. - Map EnableImeMatchLog → ImeMatchLogPath (Constants.ImeMatchLogPath default), with a safeguard for --ime-match-log dev overrides. - UploadIntervalSeconds, NtpServer, DiagnosticsUploadMode, DiagnosticsUploadEnabled, MaxAuthFailures, AuthFailureTimeoutMinutes all flow through. - Merge returns RemoteConfigMergeResult so the caller can audit UnrestrictedMode and LogLevel transitions. Program.cs emits `agent_unrestricted_mode_changed` after orchestrator.Start when the guardrail flips (V1 AuditUnrestrictedModeChange). PR #48 — BinaryIntegrityVerifier runtime self-update - Use Process.GetCurrentProcess().MainModule.FileName (V1 parity; Assembly.Location is unreliable under ngen / shadow copy). - Match → Info log; mismatch → Error log with V1-format message. - New `runtimeSelfUpdateTrigger` delegate fired with an Interlocked single-shot guard. Program.cs wires it to SelfUpdater.CheckAndApplyUpdateAsync(forceUpdate=true, triggerReason="runtime_hash_mismatch") so the agent auto-heals on SHA drift. PR #49 — AuthFailureTracker V1-parity - Tracker is now the single dispatch point for auth-failure distress. First 401/403 → AuthCertificateRejected / DeviceNotRegistered distress (V1 HandleAuthFailure). Duplicate ConfigFetchDenied send removed from RemoteConfigService. - Threshold log uses the V1 === AGENT SHUTDOWN === banner. - Program.cs emits `agent_shutdown` (reason=auth_failure) with the full V1 data schema before tripping the shutdown signal. PR #50 — Lifecycle events + SaveWhiteGloveComplete + standalone reboot - SessionIdPersistence gains SaveWhiteGloveComplete / ClearWhiteGloveComplete so WhiteGlove Part-2 resume detection finally works (IsWhiteGloveResume was unreachable before because no path wrote the marker). - EnrollmentTerminationHandler emits the V1 lifecycle events: `enrollment_summary_shown`, `diagnostics_collecting`, `diagnostics_uploaded` / `diagnostics_upload_failed`, `whiteglove_part1_complete`, `reboot_triggered`. - Standalone-reboot flow: when SelfDestructOnComplete=false and RebootOnComplete=true the handler drains the spool briefly and invokes shutdown.exe /r /t <delay>. - Program.cs emits `agent_started` (with 11+ data fields), `whiteglove_resumed`, and `system_reboot_detected` (previousExitType=reboot_kill) after orchestrator.Start. PR #52 — Collector parity - EnrollmentOrchestrator's max-lifetime watchdog now emits a V1-format `enrollment_failed` event (failureType=agent_timeout, failureSource=max_lifetime_timer, Phase=Complete) in addition to raising Terminated(MaxLifetimeExceeded). - GatherRuleExecutorHost propagates AgentConfiguration.UnrestrictedMode into the runtime executor so gather rules can bypass AllowList checks when the tenant admin elevates the guardrail (V1 CollectorCoordinator.StartGatherRuleExecutor). Tests - 476/476 V2 tests pass (was 475 — one timing-sensitive mismatch test bumped to 10s). - 507/507 V1 Agent tests + 708/708 Backend tests unaffected. - New tests cover: Remote-wins-always, UnrestrictedMode audit, EnableImeMatchLog, runtime self-update trigger single-shot, distress type per status code, diagnostics event emission, standalone reboot, WhiteGlove marker write. Deferred - PR #51 (register-session 5-retry) is out of scope — V2 does not call /api/agent/register-session in the main flow today. Porting that is a feature addition, not a V1 parity fix. - PR #53 (emergency-break, RunStartupAnalyzers) already landed in main via earlier PRs #44–#46; verified against plan §3.11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ddacaf29ce |
Merge pull request #46 from okieselbach/v2-feat-auth-tracker-binary-integrity
feat(agent): implement MaxAuthFailures watchdog + post-config binary integrity check |
||
|
|
6a9203905d |
feat(agent): implement MaxAuthFailures watchdog + post-config binary integrity check
Closes three DTO-only fields on AgentConfigResponse that have been dead since the config schema was defined — neither V1 nor V2 ever consumed them. V2 now honours them end-to-end. 1. MaxAuthFailures + AuthFailureTimeoutMinutes — AuthFailureTracker New Security/AuthFailureTracker: thread-safe counter + time-window observer that fires a single-shot ThresholdExceeded event when either ceiling is crossed. Wired into: - RemoteConfigService.FetchConfigAsync: RecordSuccess on 2xx, RecordFailure on BackendAuthException (401/403 from /api/agent/config). - BackendTelemetryUploader.MapResponseAsync: RecordSuccess on 2xx, RecordFailure on 401/403 (moved from static to instance method). - Program.cs: tracker constructed before FetchConfig with CLI defaults; UpdateLimits called after RemoteConfigMerger.Merge so tenant policy wins; ThresholdExceeded handler sets the main shutdown signal so the agent exits cleanly instead of retrying a permanently-revoked certificate forever. Defaults: MaxAuthFailures=5, AuthFailureTimeoutMinutes=0 (time-window disabled). Both honour 0 = "feature off", and UpdateLimits can tighten or relax the ceilings mid-run when the tenant admin changes policy. 2. LatestAgentExeSha256 — BinaryIntegrityVerifier New Security/BinaryIntegrityVerifier: pure-static helper that computes SHA-256 over the running agent EXE and compares against the backend- advertised hash. Case-insensitive hex compare. Three outcomes: - Skipped: no hash provided / local path not resolvable → no-op. - Match: silent success path (debug-log only). - Mismatch: emits AgentErrorType.IntegrityCheckFailed via EmergencyReporter (the enum slot was already reserved — just never used). Does NOT terminate or auto-update; SelfUpdater's own hash-based trigger handles the "wrong EXE" case at next startup, and auto-terminating the agent on every mismatch would risk boot loops during rollouts. Wired in Program.cs immediately after RemoteConfigMerger.Merge. Context These are three of the four "DTO-only" fields I flagged in PR #45's body. User feedback: do not leave dead config schema entries. V1 never implemented any of these either (V1 has a ZIP-hash check in SelfUpdater via LatestAgentSha256, but no EXE-hash check and no auth-failure shutdown threshold) — so this is net-new functionality that makes V2 strictly better than V1 in these three dimensions, not a regression closure. Test coverage - AuthFailureTrackerTests (12 tests): count-based threshold, time-window threshold, RecordSuccess resets both, single-shot idempotence, handler-exception isolation, disabled-limits never fire, UpdateLimits retroactively tightens or relaxes, thread-safety under 8 concurrent callers, ctor null-guards. - BinaryIntegrityVerifierTests (6 tests): skipped on null/empty/missing file, match path, case-insensitive compare, mismatch path, and a known-vector test against SHA-256("abc") to pin the hex formatter. 464 / 464 V2.Core tests green (vs. 446 before). Not mapped / remaining dead-config EnableImeMatchLog stays DTO-only — it duplicates the CLI flag --ime-match-log with no semantic overlap to a backend switch, and mapping it would just overwrite a dev-only debug toggle. Left as documented non-feature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7e5f7d5964 |
Merge pull request #45 from okieselbach/v2-hotfix-config-mapping-and-init-order
fix(agent): V2 hotfix #2 — wire remote config + reorder Transport subscription |
||
|
|
a46ecb7ebd |
fix(agent): wire remote config onto AgentConfiguration + reorder Transport subscription
Two V2 agent bugs surfaced in the VM smoke-test after the first-run ghost-detect hotfix (PR #44) let the agent reach the backend fetch phase: 1. INIT-ORDER CRASH — Program.WireTelemetryServerResponse touched orchestrator.Transport before orchestrator.Start() was called, and the Transport getter throws InvalidOperationException pre-Start because _transport is only constructed inside Start() at EnrollmentOrchestrator.cs:311. Every VM crashed at this exact line. Fix: move the wire call to immediately after orchestrator.Start() inside the same try block. No behavioural loss — the first ServerResponseReceived cannot fire until a telemetry upload completes, which happens after Start(). 2. REMOTE-CONFIG WIRING GAP — RemoteConfigService fetched AgentConfigResponse from the backend but only Collectors, ImeLogPatterns, GatherRules, WhiteGloveSealingPatternIds and LatestAgentSha256 were consumed downstream. ~20 tenant-controlled fields were bound to AgentConfiguration defaults from CLI / bootstrap-config and never got projected from the remote DTO — SelfDestructOnComplete, KeepLogFile, RebootOnComplete, NtpServer, EnableTimezoneAutoSet, EnableGeoLocation, DiagnosticsUploadMode, DiagnosticsLogPaths, DiagnosticsUploadEnabled, ShowEnrollmentSummary + EnrollmentSummary* (4 fields), LogLevel, AgentMaxLifetimeMinutes, HelloWaitTimeoutSeconds, UploadIntervalSeconds, MaxBatchSize, SendTraceEvents, UnrestrictedMode, RebootDelaySeconds. Every consumer site (CleanupService, SummaryDialogLauncher, StartupEnvironmentProbes, DiagnosticsPackageService, EnrollmentTerminationHandler, AgentLogger, EnrollmentOrchestrator, GatherRuleGuards) reads agentConfig.XXX — so tenant admin settings were effectively ignored. A tenant with SelfDestructOnComplete=false still saw its agent wipe ProgramData on enrollment complete, because the CLI-default !noCleanup=true always won. Fix: new RemoteConfigMerger.Merge(agentConfig, remoteConfig, args) pure static helper, called immediately after FetchConfigAsync. Projects all ~20 tenant-controlled fields onto AgentConfiguration. CLI flags remain authoritative: flags explicitly passed (--no-cleanup, --keep-logfile, --reboot-on-complete, --disable-geolocation, --log-level) skip the remote overwrite so local dev runs keep their ad-hoc overrides. Partial-remote-config tolerance: empty strings, null lists and non-positive ints keep the AgentConfiguration default. Non-V1-parity fields (MaxAuthFailures, AuthFailureTimeoutMinutes, LatestAgentExeSha256) are deliberately skipped — they are DTO-only in V1 too and have no consumer in either version, no regression. Test coverage added: - ProgramGuardsTests: Transport pre-Start now explicitly asserted to throw (regression guard); Transport_is_available_after_start confirms the post- Start happy path. - RemoteConfigMergerTests (20 tests): happy path (all fields), 5 CLI override tests (no-cleanup, keep-logfile, reboot, disable-geolocation, log-level), log-level case-insensitivity, log-level parse failure preserves default, null remote-config + null args + null nested Collectors tolerated, empty- string NtpServer + null DiagnosticsUploadMode keep defaults, null DiagnosticsLogPaths becomes empty list (not null), non-positive int clamping (UploadIntervalSeconds, MaxBatchSize, HelloWaitTimeoutSeconds), zero AgentMaxLifetimeMinutes allowed (explicit "disabled"), null-agent-config argument guard, and a regression test for the exact VM scenario (tenant SelfDestructOnComplete=false + agent with CLI-default true). 446 / 446 V2.Core tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
abd232ea90 |
Merge pull request #44 from okieselbach/v2-hotfix-first-run-ghost-detection
fix(agent): V2 hotfix — first-run ghost-detect triggers self-destruct on fresh install |
||
|
|
bf090c436e |
fix(agent): create SessionId before startup guards to prevent first-run ghost-detect
The V2 agent was deterministically self-destructing on every first run after --install on a fresh VM. CheckEnrollmentCompleteMarker reads HKLM\SOFTWARE\AutopilotMonitor\Deployed (written by --install) and treats "marker present + session.id absent" as a ghost restart, triggering ExecuteSelfDestruct and wiping %ProgramData%\AutopilotMonitor before FetchConfigAsync can run. Remote config's SelfDestructOnComplete=false was never honored because the config fetch never happened. Legacy V1 agent creates the session in LoadConfiguration (Program.Configuration.cs:131) before the guard runs (Program.cs:169). V2 reversed this ordering with a comment claiming it prevents "dead session resurrection" — but the guards key on file-system state (SessionPersistence.SessionExists()), not the runtime-loaded session object, so the reordering was functionally unnecessary and introduced this regression. Fix: move SessionIdPersistence.GetOrCreate + --new-session handling back before the guards, restoring Legacy boot order. Ghost-restart detection still triggers in its intended case (session.id file deleted by incomplete self-destruct, but Scheduled Task + registry marker survive). Test coverage: adds 3 missing tests for CheckEnrollmentCompleteMarker in ProgramGuardsTests.cs (first-run with session, marker without selfdestruct, marker with selfdestruct retry). The M4.6.alpha guard suite had no coverage for this path, which is how the regression shipped. 425 V2.Core tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
09899a90ac |
ci(web): allow workflow_dispatch to run the deploy job
The build_and_deploy_job's if-guard accepted push + non-closed PR
events only, so manually triggering the Static Web Apps workflow via
`gh workflow run` ran but skipped both jobs (conclusion=skipped).
After the V2-refactor merge to main (commit
|
||
|
|
62c0c990bc |
Merge pull request #43 — V2 Agent refactor + M5 backend + Codex review fixes
Merges branch Agent-v2.0 onto main: complete V1 to V2 agent kernel refactor (M1 to M5), admin UI for EnableIndexDualWrite flag, plus the three Codex review fixes and the SessionTraceOrdinal recovery-seed follow-up. 60 sub-milestone commits preserved via merge-commit strategy for future git-bisect traceability. |
||
|
|
1a1573ec45 |
fix(agent): seed SessionTraceOrdinalProvider from persisted max on recovery (Codex follow-up M4.4.5.f)
Closes the pre-existing TODO Codex flagged in EnrollmentOrchestrator.cs
(doc line 39 + init line 354) and removes the recovery-time duplicate-
trace-ordinal hazard it would have left in place.
Before: the provider was constructed with no seed, so on agent restart
its counter began at -1 and the first post-recovery signal landed with
SessionTraceOrdinal=0 — guaranteeing a collision with whatever trace
ordinal the previous session had already persisted. The collision
surfaces at the Inspector (cross-type correlation fields) and in any
downstream replay that joins on SessionTraceOrdinal.
Interface additions (ISignalLogWriter + IJournalWriter):
- long LastTraceOrdinal { get; }
max SessionTraceOrdinal observed across persisted entries; -1 on an
empty log. Distinct from LastOrdinal / LastStepIndex which track the
kind-specific monotone counters; SessionTraceOrdinal is the
session-wide correlation counter shared across Event + Signal +
Transition (DecisionSignal §2.2).
Implementations (SignalLogWriter + JournalWriter):
- Renamed ScanHighestOrdinal / ScanHighestStepIndex into a shared
ScanRecoveryState() that populates both fields in a single JSONL pass
on construction. Same stop-at-corrupt-tail semantic as before
(recovery §2.7). The Append path also tracks the max inline so
subsequent queries after a writer is in use stay O(1).
EnrollmentOrchestrator.Start (step 9):
var traceSeed = Math.Max(
Math.Max(_signalLog.LastTraceOrdinal, _journal.LastTraceOrdinal),
_spool.LastAssignedItemId);
_traceCounter = new SessionTraceOrdinalProvider(seedLastAssigned: traceSeed);
All three sources are consulted because each can hold a trace ordinal
the other two missed:
- SignalLog may contain signals whose spool-enqueue later failed (the
emit path swallows transport exceptions per SignalIngress §2.7c).
- Journal likewise for transitions with failed spool-enqueue.
- Spool.LastAssignedItemId is the single-source-of-truth for everything
that reached the transport — including Events that bypass the provider
entirely but still share the TraceOrdinal space.
Test harness:
- FakeJournalWriter gains a LastTraceOrdinal property (computes max
from its captured Appended list) so processor-level tests keep
compiling.
Tests (+6 in V2.Core.Tests):
- SignalLogWriter: empty → -1; max across out-of-order trace ordinals
(not just the last appended); restart rescans on-disk file to
recover max.
- JournalWriter: same three cases.
Both empty-log tests exist so the orchestrator seed falls through
cleanly on first-ever startup (seedLastAssigned=-1 → Next()=0 matches
the fresh-session invariant).
Suites: V2.Core 422 green (prev 416 + 6 new). Full solution build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
51e9966e0a |
fix(agent): forward AdminAction outcome to termination handler (Codex Finding 2)
Portal Mark-Succeeded traffic was silently landing as Failed locally. The
M4.6.ε ServerAction synthesis in Program.cs already copied the adminOutcome
value into the terminate_session ServerAction's Params bag, but the
terminate_session callback that fed the TerminationHandler hard-coded
EnrollmentTerminationOutcome.Failed regardless of what adminOutcome said.
Downstream consequences of the hard-coded Failed:
- FinalStatusBuilder.MapOutcome rendered "Failed" in the SummaryDialog
even when the operator had explicitly marked the session Succeeded.
- EnrollmentTerminationHandler's DiagnosticsUploadMode.OnFailure gate
fired a diagnostics package for every admin-override, regardless of
actual outcome.
Fix:
- New Program.ServerActionSynthesis.cs partial (public static class
Program, matches existing declaration) carrying a pure static helper
MapAdminOutcome(IReadOnlyDictionary<string,string>? params) →
EnrollmentTerminationOutcome. Case-insensitive "Succeeded" → Succeeded;
missing/null/empty/any-other-value → Failed (default-failure-safe for
kill-signal-driven synthesis that never sets adminOutcome).
- The terminate_session callback in Program.cs now calls the helper and
threads the mapped outcome into EnrollmentTerminatedEventArgs. Log
message extended to include the mapped outcome so ops can correlate
the server-side override with the local termination result.
Reason stays DecisionTerminalStage. A distinct AdminAction reason would
need a DecisionCore enum extension — out of scope for this fix. What
Codex flagged concretely (Mark-Succeeded → visible as Failed) is now
addressed.
Tests (+10 in V2.Core.Tests): AdminActionOutcomeMappingTests
- "Succeeded" → Succeeded
- Case-insensitive variants (succeeded / SUCCEEDED / mixed case)
- "Failed" → Failed
- Unknown values (TimedOut / weird-future / empty) → Failed
- Missing adminOutcome param (kill-signal case) → Failed
- Null params dict → Failed
Accesses the helper through InternalsVisibleTo("AutopilotMonitor.Agent.V2.Core.Tests")
that was already set on the V2 agent assembly.
Suites: V2.Core 416 green (prev 406 + 10 new). Full solution build green.
This closes the last of the three Codex findings from the PR-43 review.
Remaining review comment on cross-layer test coverage is already
satisfied by the TelemetryWireRoundTripTests added in commit
|
||
|
|
b6ea13f122 |
fix(agent): upload V2 Signals + DecisionTransitions to backend (Codex Finding 1)
The V2 agent was never projecting DecisionSignals or DecisionTransitions
onto the telemetry transport. SignalIngress.Append wrote the signal to
the local SignalLog and handed it to the reducer; DecisionStepProcessor
journaled each transition locally. Neither called into ITelemetryTransport,
so the entire M5 backend ingest pipeline for these two telemetry kinds
(Signals/DecisionTransitions primary tables, Inspector reads,
ReducerVerifier, the M5.d index fan-out + timer) had zero agent traffic.
Adds the missing emitters — analogous to TelemetryEventEmitter — and
wires them through EnrollmentOrchestrator:
- TelemetrySignalEmitter (V2.Core/Telemetry/Signals/): serializes a
DecisionSignal with Newtonsoft + StringEnumConverter and builds a
TelemetryItemDraft with Kind=Signal, PK={tenant}_{session},
RK=SessionSignalOrdinal.ToString("D19") — matches
TableSignalRepository.BuildRowKey for idempotent UpsertReplace.
- TelemetryTransitionEmitter (V2.Core/Telemetry/Transitions/): same
contract for DecisionTransition; RK=StepIndex.ToString("D10").
Wiring:
- SignalIngress accepts an optional TelemetrySignalEmitter. After the
local SignalLog.Append succeeds — before the reducer runs — the
ingress enqueues the signal on the transport. Enqueue failures are
swallowed: the local SignalLog is authoritative per §2.7c / L.1, and
the reducer must still see the signal even if the upload path is
broken.
- DecisionStepProcessor accepts an optional TelemetryTransitionEmitter.
After the Journal.Append try-block succeeds (and before the effect
runner), it enqueues the transition. Same swallow semantics. Journal
failures still rethrow as before, so a failed-journal step can never
leak a ghost row to the backend.
- EnrollmentOrchestrator constructs both emitters alongside the existing
TelemetryEventEmitter (step 4 of the startup sequence) and passes them
into steps 8 + 10. Both emitter constructor params are optional to
keep pre-M5 test constructors compilable without touch-up.
Tests (+26 across two assemblies):
V2.Core.Tests (+21):
- TelemetrySignalEmitterTests (6): correct keys + kind, StringEnumConverter
for DecisionSignalKind, PascalCase field names the backend parser
reads, D19 padding survives long.MaxValue, null-rejection, ctor
argument validation.
- TelemetryTransitionEmitterTests (7): same surface + dead-end case
preserves DeadEndReason + Taken=false, nested ClassifierVerdict
emits ClassifierId/Level that the backend parser can read.
- SignalIngressUploadPathTests (4): emit happens per signal, monotonic
RowKey padding, null emitter preserves pre-M5 behaviour, transport
throw is swallowed (reducer keeps running).
- DecisionStepProcessorUploadPathTests (4): emit happens per successful
journal, null emitter preserves pre-M5 behaviour, journal failure
prevents emit (no ghost rows), transport throw is swallowed (effects
still run + CurrentState advances).
Functions.Tests (+5) — the Codex-requested cross-layer gate:
- TelemetryWireRoundTripTests: a DecisionSignal / DecisionTransition
serialized with the exact JsonSerializerSettings the agent emitter
uses (StringEnumConverter + NullValueHandling.Ignore) is parsed by
TelemetryPayloadParser.ParseSignal / ParseTransition back into a
SignalRecord / DecisionTransitionRecord with every field preserved.
Includes explicit-zero edge cases (ordinal=0, stepIndex=0) so the
Finding-3 required-field guards don't trip on legitimate agent traffic.
The backend project can't reference the net48 agent assembly directly,
so the test replicates the wire contract via shared DecisionCore types
+ the agreed serializer settings — any drift between agent and backend
breaks this test immediately.
Suites: V2.Core 406 green (prev 385 + 21 new). Functions 708 green
(prev 703 + 5 new). Full solution build green.
Does not address Codex Finding 2 (AdminAction=Succeeded mis-mapped to
Failed locally) — lands in the next commit on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ee07059bc3 |
fix(backend): harden /api/agent/telemetry batch validation (Codex Finding 3)
The ingest endpoint trusted the first item's PartitionKey for the whole
batch and silently defaulted RowKey-driver fields to 0 when the agent
envelope omitted them. Both paths can misroute data on the storage layer.
1. Cross-session guard: every TelemetryItemDto in a batch must share the
same PartitionKey as items[0]. Without this, a mixed-session batch
would be stamped with session-A's identity inside PersistItemsAsync
and silently relocate session-B's signals/transitions into session-A's
primary rows. Mismatch now returns 400 BadRequest with the offending
index logged. Extracted as pure helper FindMismatchingPartitionKey so
the guard is unit-testable without an HTTP harness.
2. RowKey-driver enforcement in TelemetryPayloadParser:
- ParseSignal now rejects envelopes where SessionSignalOrdinal is
absent or null (returns null → caller drops the item). A missing
value would default to 0 and clobber a legitimate ordinal-0 row
on UpsertReplace. Explicit ordinal=0 stays a valid value.
- ParseTransition applies the same defense to StepIndex.
- Distinguishes "key missing" from "key present with value 0" via
JObject.TryGetValue + JTokenType check.
Tests (+11 in Functions):
- IngestTelemetryFunctionTests: empty batch, single-item batch, uniform
batch, mismatch in middle, case-sensitive mismatch.
- TelemetryPayloadParserTests: missing/null SessionSignalOrdinal on
Signal → null; explicit ordinal 0 accepted; missing/null StepIndex on
DecisionTransition → null; explicit StepIndex 0 accepted.
Full Functions suite: 703 green (prev 692 + 11 new).
Does not address Codex Findings 1 (V2 agent has no Signal/Transition
upload path) or 2 (AdminAction=Succeeded mis-mapped to Failed locally)
— those land in follow-up commits on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
37fff23943 |
chore: gitignore entire tasks/ folder, untrack per-session notes
Replaces the targeted `tasks/todo.md` ignore with a blanket `tasks/` so the whole folder stays local-only. Content there is per-session planning + lesson notes (todo.md, lessons.md, m2-copy-list.md); it doesn't belong in version control alongside the shipped code — it's operator workspace, not repo state. Untracks the two files that were still being versioned (tasks/lessons.md, tasks/m2-copy-list.md) via `git rm --cached` so they stay on disk but leave the index. Historical versions remain in git history for reference; new edits won't be tracked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
65085ff7ce |
feat(web): expose EnableIndexDualWrite toggle in admin Global Settings
Closes the operator-facing gap left by M5.d.2: the feature flag was plumbed server-side (AdminConfiguration model + TableConfigRepository Store/Map) but the UI had no way to flip it. Without this change the entire M5.d pipeline was unreachable in production. Wire-up mirrors the existing allowAgentDowngrade toggle (closest analog — also a developer/ops-level bool with caveat copy): - types/adminConfig.ts: add optional enableIndexDualWrite field with a doc comment pointing at Plan §M5.d so future maintainers understand what toggling it does. - AdminConfigContext.tsx: add state + setter, hydrate from GET response (default false), include in save-spread, include in reset fallback, expose via context value + save-useCallback deps. - AdminConfigSettingsSection.tsx: add prop pair and a checkbox-style toggle below the ModernDeployment harmless-IDs input. Copy explains the 5 index tables + the 2h safety-net timer + warns that it should only be flipped after release-gate verification. - SectionGlobalSettings.tsx: destructure + forward the new prop pair. Round-trip is safe: GET (System.Text.Json, camelCase) emits enableIndexDualWrite, UI spreads it back into PUT body, Newtonsoft case-insensitive deserialization binds to EnableIndexDualWrite on the model — so an operator save no longer silently resets the flag. Verification: npx tsc --noEmit clean, vitest suite 168/168 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fd7bf0e7ac |
feat(backend): M5.d.4 index-reconcile timer + release-gate — M5 complete
Closes the M5.d dual-write loop with a 2h safety-net timer and the
per-index-table PK/RK consistency release-gate tests required by
Plan §M5-Pflicht.
Components:
- ISignalRepository.QueryByTimestampAtOrAfterAsync +
IDecisionTransitionRepository.QueryByTimestampAtOrAfterAsync: new
cross-partition scans filtered on the Azure Tables system-managed
`Timestamp` property. Bounded to maxResults=50000 with a warn-log
on cap-reached so silent truncation can't masquerade as success.
- IndexReconcileTimer: TimerTrigger on "0 0 */2 * * *" (same cadence
as MaintenanceFunction). Scans the last 4h (window deliberately 2×
the cadence so every primary row gets re-checked at least twice
before falling out of range), builds envelopes via the existing
IndexReconcileEnvelopeFactory, and hands them to the producer —
reusing the M5.d.2 flag-gate + queue-failure-swallow semantics
instead of duplicating them.
- Testable core: RunReconcileAsync(cutoffUtc, CT) split out from the
TimerTrigger entry so unit tests can drive with deterministic
cutoffs without an Azure Functions host.
- AdminConfigurationService.GetConfigurationAsync is now virtual —
enables Moq-based mocking in timer flag-gate tests. Low-risk prod
change (no existing caller relies on final).
Release gate (Plan §M5-Pflicht line 1395): IndexDualWriteConsistencyTests
verifies per-index-table that the captured index row's back-ref
fields (TenantId, SessionId, StepIndex / SessionSignalOrdinal) plug
directly into the primary repos' key builders to reconstruct the
primary (PartitionKey, RowKey). The resubmit-idempotency test simulates
a timer-triggered replay of the same primaries and asserts all
duplicated rows collapse onto identical Index (PK, RK) pairs — which
is exactly what the real TableIndexRepository's UpsertReplace does
at the storage layer.
Left deferred (not blocking M5 close):
- SessionsByStage stale-row pruning: stage-X→Y transitions still leave
an orphan row in {tenantId}_X. Either query-side dedupe (per
SessionId, max-Ticks RK) or a follow-up pruner handles it.
- Actual EnableIndexDualWrite=true flip: stays default-false until
post-deploy verification runs. Producer + timer both no-op in the
meantime, so the change is zero-risk on merge.
- Cross-partition-scan perf: fine for current primary-table sizes;
revisit if prod metrics after pilot show degradation.
Tests (+10 in Functions):
- 4 IndexReconcileTimerTests: flag-off short-circuits queries, flag-on
with primary rows enqueues all envelopes, empty-window skips enqueue,
ReconcileWindow=4h invariant.
- 6 IndexDualWriteConsistencyTests: 5 per-index-table PK/RK back-ref
(SignalsByKind + all 4 transition-derived indices) + 1 resubmit
idempotency ensuring all duplicated rows collapse onto the same
index keys.
M5 status: M5.a + M5.b (+ 3 sub-commits) + M5.c (+ 3 sub-commits) +
M5.d (+ 4 sub-commits) all complete. M5.d loop:
Primary commit (M5.a) → Envelope enqueue (M5.d.2, flag-gated)
→ Queue → Handler (M5.d.3) → Index-Tables (M5.d.1)
+ 2h safety-net Timer (M5.d.4) reconciles missed envelopes.
Full Functions suite: 692 green (prev 682 + 10 new). Full solution
build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8e39299da5 |
feat(backend): M5.d.3 queue-triggered index reconcile consumer
Consumes the telemetry-index-reconcile queue (M5.d.2) and fans each
envelope out into 0–3 index-table rows via IIndexTableRepository
(M5.d.1). Closes the producer→consumer loop; once the
EnableIndexDualWrite flag is flipped, primary commits drive
eventual-consistent index upserts asynchronously.
Components:
- Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues 5.5.4
package for the isolated-worker QueueTrigger binding.
- IndexReconcileHandler (plain class — not interface-abstracted per
Plan §M5.d design: DAL abstraction lives at IIndexTableRepository,
not here). Routing:
Signal → SignalsByKind (always, 1 row).
DecisionTransition:
IsTerminal=true → SessionsByTerminal
Taken=true → SessionsByStage
Taken=false + reason → DeadEndsByReason
ClassifierVerdictId!=null → ClassifierVerdictsByIdLevel
Up to 3 rows per DecisionTransition envelope (a Taken+terminal+
classifier transition fans to SessionsByTerminal + SessionsByStage
+ ClassifierVerdictsByIdLevel).
- IndexReconcileFunction thin QueueTrigger wrapper
(Connection = AzureTableStorageConnectionString). Malformed JSON
logged + dropped to avoid poison-queue retry loops; transient
repo errors propagate so the platform retries.
- DI: handler registered as Singleton in Program.cs.
Failure semantics:
- Required-field-missing envelopes (Signal without SessionSignalOrdinal
or SignalKind; DecisionTransition without StepIndex) → log warn,
drop. These won't become valid on retry — avoiding the retry loop.
- Transient Table Storage errors → rethrow. QueueTrigger platform
retries up to 5 times by default, then moves to
telemetry-index-reconcile-poison for manual investigation.
Deferred to M5.d.4:
- SessionsByStage stale-row pruning: when a session moves from
Stage-X→Stage-Y, the old Stage-X row stays. Either query-side
dedupe (per SessionId, max-Ticks RK) or reconcile-timer pruning
handles this. Deliberately not solved in the handler to avoid
a query+delete round-trip per Taken transition.
- End-to-end dual-write verification tests (Azurite harness) are
the M5.d.4 release gate before the flag is flipped.
Tests (+10 in Functions) via capture-only FakeIndexTableRepository:
- Signal happy path (1 SignalsByKind row).
- Signal missing SessionSignalOrdinal / SignalKind → 0 rows (x2).
- DecisionTransition Terminal+Taken+Classifier → 3 rows.
- DecisionTransition Taken non-terminal no-classifier → 1 row
(SessionsByStage only).
- DecisionTransition dead-end with classifier → 2 rows
(DeadEndsByReason + ClassifierVerdictsByIdLevel).
- DecisionTransition dead-end without reason → 0 rows (PK-collision
defense).
- DecisionTransition missing StepIndex → 0 rows (all paths gated).
- Unknown SourceKind → 0 rows.
- Null envelope → 0 rows.
Full Functions suite: 682 green (prev 672 + 10 new). Full solution
build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c3ee0cad55 |
feat(backend): M5.d.2 index-reconcile queue producer + feature flag
Adds the async fan-out producer path from Plan §2.8 / §M5.d so primary Signals + DecisionTransitions rows can be projected onto their index tables without blocking the hot ingest loop. Components: - Constants.QueueNames.TelemetryIndexReconcile (azure-queue-name-safe, all lowercase + hyphens). - IndexReconcileEnvelope (Shared DTO, flat shape with nullable fields per SourceKind="Signal"/"DecisionTransition"). Avoids polymorphic Newtonsoft deserialization — see M5.c.3 finding. - IndexReconcileEnvelopeFactory pure static helper (FromSignal / FromDecisionTransition / BuildBatch) — state-less projection. - IIndexReconcileProducer (Shared interface) contract: no-op when the feature flag is off; never rethrows underlying-queue failures. - AzureQueueIndexReconcileProducer (Functions) — Base64-encoded JSON messages, lazy CreateIfNotExistsAsync guarded by Interlocked flag, Managed-Identity-or-connection-string construction mirroring BlobStorageService / TableStorageService. - IngestTelemetryFunction ctor gains IIndexReconcileProducer; after primary Signals + DecisionTransitions commit it builds envelopes and enqueues them. Event path is untouched (events don't feed the index tables per §2.8). - Program.cs registers the producer as a singleton. Feature gate (AdminConfiguration.EnableIndexDualWrite, default false): - New bool field in AdminConfiguration. - TableConfigRepository Store + Map extension (per feedback_table_storage_serialization). - Producer checks config each call via AdminConfigurationService's 5-min memory cache. Flag off → primary behaviour is bit-exact. Fault tolerance: queue-side exceptions (network, throttling, missing queue before first CreateIfNotExists) are logged per envelope and swallowed. The primary row is the source of truth; the 2h reconcile timer (M5.d.4) is the safety-net recovery path. Tests (+11 in Functions): - 5 IndexReconcileEnvelopeFactoryTests: Signal/DecisionTransition projection, dead-end preservation, batch ordering, null/empty. - 3 IndexReconcileEnvelopeSerializationTests: Newtonsoft round-trip (Signal, full DecisionTransition, minimal-envelope tolerance). - 3 AdminConfigurationEnableIndexDualWriteTests: default-false on new config + CreateDefault, persists when set. Deferred to later sub-milestones: - M5.d.3: queue-triggered consumer that maps envelopes to 0–3 index rows using M5.d.1 TableIndexRepository. - M5.d.4: 2h reconcile timer + release-gate dual-write verification tests, then flip EnableIndexDualWrite=true in prod admin config. Full Functions suite: 672 green (prev 661 + 11 new). Full solution build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5eb09176b9 |
feat(backend): M5.d.1 index-table scaffolding for Plan §2.8 query matrix
Registers 5 secondary-index tables (SessionsByTerminal, SessionsByStage, DeadEndsByReason, ClassifierVerdictsByIdLevel, SignalsByKind) in Constants.TableNames.All[] so TableStorageService.InitializeTablesAsync auto-creates them at startup. Adds: - 5 sealed DTO records in Shared/Models/IndexRecords.cs carrying only discriminator columns + back-refs (StepIndex / SessionSignalOrdinal) to the primary row — no PayloadJson chunking needed. - IIndexTableRepository with 5 Store*Async methods (single-interface shape keeps boilerplate low; Cosmos swap remains trivial since each Store* is independent). - IndexRowKeys pure static helper for all PK/RK formats per Plan §2.8: D6 StepIndex suffix, D10 SessionSignalOrdinal suffix, D19 Ticks padding for SessionsByStage (matches lex ordering to numeric time). Defensive Sanitize() for forbidden PK/RK chars (/\#?). - TableIndexRepository with a generic group-by-PK + 100-op-chunk UpsertReplace write path mirroring TableSignalRepository. - DI wiring in DataAccessServiceExtensions. Scope: write-only. Query methods deferred until MCP/Web concretely need them. No queue producer / reconcile handler yet — that lands in M5.d.2+. Tests (+25 in Functions): 15 PK/RK format assertions in IndexRowKeysTests + 10 entity-projection round-trips (incl. null-tolerance) in TableIndexRepositoryMappingTests. Full Functions suite: 661 green (prev 636 + 25 new). Full solution build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
44f0e44b72 |
feat(backend): M5.c.3 GET /api/sessions/{id}/reducer-verification — structural audit
Third and final V2 Inspector read endpoint. Admin-only (not tenant-exposed) structural health check over a session's persisted SignalLog + DecisionTransitions journal. Closes the M5 read-endpoint trio. - Shared DTO ReducerVerificationReport + VerificationIssue: counts, ordinal / step-index contiguity, ReducerVersion drift, orphaned-reference count, human-readable issue stream for the Inspector's verification panel. - ReducerVerifier.Verify — pure function, zero IO. Five detectors: empty-session (Info), signal ordinal gaps (Warning, capped at 20 issues + overflow summary to keep reports bounded), step-index gaps (Warning, same cap), orphaned transitions whose SignalOrdinalRef falls outside the loaded signal set (Warning, capped), and ReducerVersion drift (Info — known condition per plan §2.10, not an error). - GetSessionReducerVerificationFunction — reads signals + transitions for the session, loads the live DecisionEngine.ReducerVersion, runs the verifier, returns the report. Global-Admin cross-tenant fallback via SessionsIndex when the effective tenant has no data. Truncation flag when either the 5000-signal or 5000-transition cap is hit. - Endpoint gated by EndpointPolicy.GlobalAdminOnly — the plan explicitly scopes this to adminMode/ops access. The fail-closed catalog policy completeness test enforces the registration. Scope boundary explicitly documented on the function: full engine replay (DecisionEngine.Reduce over reconstructed DecisionSignals with per-step diff) is NOT implemented in this commit. It would require polymorphic deserialisation of SignalRecord.PayloadJson back into DecisionSignal with its Evidence abstract type, which is a non-trivial refactor. The structural checks land today catch the realistic release-gate failure modes (corruption, drift, orphan refs) and give ops a usable tool. Full replay is a dedicated follow-up once the Evidence deserialisation contract is pinned down. 9 new tests cover all five detectors plus happy path, shuffled input, and the issue-cap overflow message. 636/636 backend green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
04270f1fb0 |
feat(backend): M5.c.2 GET /api/sessions/{id}/decision-graph — Inspector DAG
Second V2 Inspector read endpoint. Server-side projection from the persisted
DecisionTransitions journal into a renderable DAG so the UI consumes one
structured shape instead of rebuilding the graph on every page open, and so
"what counts as terminal" stays authoritative next to the backend copy of
that list.
- Shared DTO DecisionGraphProjection (Nodes + Edges + ReducerVersion).
Nodes are de-duplicated stages with IsTerminal + a coarse TerminalOutcome
label ("Succeeded" / "Failed" / "PausedForPart2"). Edges are one per
transition, preserving dead-ends (Taken=false) for the UI to render in a
distinct style. VisitCount counts *taken* transitions only — dead-ends
don't advance the heat-map.
- IDecisionTransitionRepository.QueryBySessionAsync added alongside
StoreBatchAsync. Mirror of the M5.c.1 Signals pattern: PartitionKey filter,
explicit StepIndex sort as a redundancy guard (the D10 RowKey padding
already matches numeric order).
- TableDecisionTransitionRepository.FromEntity — reverse mapping (entity →
record) including TableStorageChunking.ReassembleProperty for oversized
PayloadJson. Round-trip tested end-to-end.
- DecisionGraphBuilder.Build — pure projection function, no IO, no service
deps. Unit-testable against canned transition lists; the test suite covers
empty input, node de-duplication, terminal classification for all 4 stages,
dead-end preservation, StepIndex ordering under shuffled input, and
VisitCount semantics.
- GetSessionDecisionGraphFunction — same shape as GetSessionSignalsFunction
(RequestContext resolution, Global-Admin cross-tenant fallback), plus a
truncation flag when the 5000-step cap is hit.
- Endpoint registered in EndpointAccessPolicyCatalog under MemberRead +
TenantScoping.QueryParam (sibling of sessions/{id}/events).
Scope boundary — the plan calls for terminal-node decoration with the richer
TerminationReason + TerminationOutcome from the M4.6.β enrollment_terminated
event. That data lives in the Events table, not in the transition journal;
joining it here would expand scope into cross-table reads. The current
projection exposes the coarse TerminalOutcome label (derived from the
transition's ToStage) which is enough for the M6 Inspector's initial DAG
render. Richer decoration is a dedicated follow-up.
Three sources of truth now carry the terminal-stage list: agent-side
DecisionCore.SessionStageExtensions.IsTerminal, backend-ingest
TelemetryPayloadParser.IsTerminalStage, and now DecisionGraphBuilder's copy.
Adding a new terminal stage = three places to update; flagged in the builder's
docstring so it doesn't get missed.
627/627 backend tests green (+13: 11 builder + 2 transition round-trip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f1e89146cb |
feat(backend): M5.c.1 GET /api/sessions/{id}/signals — Inspector SignalLog read
First of three V2 Inspector read endpoints (Plan §M5). Returns the SignalLog
for a session — ordered by SessionSignalOrdinal, bounded by a caller-supplied
maxResults with a hard 5000 cap.
- ISignalRepository.QueryBySessionAsync added alongside the existing
StoreBatchAsync. Uses Azure Tables filter on PartitionKey = {tenant}_{session};
the D19(ordinal) RowKey padding means lex order already matches numeric
order, so the explicit sort on return is a redundancy guard — cheap
invariant check, not a real cost.
- TableSignalRepository.FromEntity — reverse mapping (entity → SignalRecord)
that mirrors ToEntity, including TableStorageChunking.ReassembleProperty
for oversized PayloadJson. Round-trip tested end-to-end.
- GetSessionSignalsFunction.Run — follows the same shape as
GetSessionEventsFunction: RequestContext.TargetTenantId resolved by
PolicyEnforcementMiddleware, Global-Admin cross-tenant fallback via
SessionsIndex when the effective-tenant query comes up empty.
- ParseMaxResults extracted as a pure helper taking the raw query string
(not HttpRequestData) so it's unit-testable without mocking the abstract
Worker HttpRequestData class.
- Endpoint registered in EndpointAccessPolicyCatalog under MemberRead +
TenantScoping.QueryParam (same policy as sessions/{id}/events). Without
this the fail-closed catalog would reject the route with 403 — verified
by the EndpointPolicyCatalogCompletenessTests.
9 new tests (614/614 backend green): ParseMaxResults defaults / garbage /
in-range / cap / negative / null query string (6), plus FromEntity
round-trip (simple payload + oversized-chunked payload).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
260d9afd8d |
feat(backend): wire WhiteGloveSealingPatternIds through /api/agent/config
The V2-agent AgentConfigResponse schema has carried four fields the backend was expected to populate (Plan §M5): LatestAgentSha256, LatestAgentExeSha256, AllowAgentDowngrade, and WhiteGloveSealingPatternIds. The first three were already wired through adminConfig; the last one was declared on the DTO but never populated from storage — V2 agents always received an empty list, effectively leaving the sealing-pattern emission path dead. Adds the missing plumbing: - AdminConfiguration.WhiteGloveSealingPatternIdsJson (string, JSON-serialised List<string>) + GetWhiteGloveSealingPatternIds() helper. Defaults to empty for null/whitespace/invalid JSON so the feature stays off by default (M3-compatible, no regression on tenants that haven't opted in). Global-only, no per-tenant override (plan §M5 M4.4.5.e decision). - TableConfigRepository Store + Map entries for the new column (feedback_table_storage_serialization — new model properties MUST round-trip through the table mapping or the field silently drops on save/load). - GetAgentConfigFunction populates WhiteGloveSealingPatternIds from adminConfig.GetWhiteGloveSealingPatternIds(). ConfigVersion bumped 25 → 26 as a semantic-change marker. 5 new tests mirror the HarmlessEventIds pattern (null / whitespace / invalid JSON / valid round-trip / explicit empty array). 605/605 backend tests green. Agent-side (M4.4.4 / M4.4.5.e): the V2 agent's OnPatternMatched hook already reads WhiteGloveSealingPatternIds from the cached config and re-emits matching IME pattern hits as WhiteGloveSealingPatternDetected DecisionSignals. With this commit the data path is end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0936132a00 |
feat(backend): M5.b.3 payload size-cap guard for /api/agent/telemetry
Defense-in-depth: reject oversized request bodies before buffering them in memory + deserialising. Closes the documented gap from M5.b — the V2 endpoint previously accepted unbounded bodies while legacy /api/agent/ingest has capped NDJSON payloads since day one. - Reuse existing TenantConfiguration.MaxNdjsonPayloadSizeMB (table-only, admin-set, default 5 MB). The property name is historical; scope is shape-agnostic — both NDJSON and JSON-array paths now share the knob. Doc comment updated to reflect that. No new config surface. - New internal static helper IngestTelemetryFunction.ReadBodyWithSizeCapAsync reads the already-gzip-decompressed request stream with a strict byte cap. Short-circuits + returns (exceeded=true, body="") the instant the cap would be exceeded, so a malicious sender can't force a 100 MB allocation. Semantics match legacy NdjsonParser: strict greater-than, equal-to accepted. - Cap exceeded → HTTP 413 Payload Too Large with IngestEventsResponse body (Success=false, Message describes limit). Agent's UploadResult parser classifies 413 as Permanent (not retried), which is correct — the same batch will fail again. The agent handles this by dropping the batch from the spool after the permanent-failure log. 4 new tests: under cap, exactly at cap, over cap (asserting the stream is not fully drained — memory bound), empty stream. 600/600 backend tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
31eabbefef |
refactor(backend): split EventIngestProcessor into thematic partials
The single 1060-LoC file was hard to navigate; partial-class split mirrors the legacy TableStorageService/IngestEventsFunction pattern and keeps each file focused on one concern. - EventIngestProcessor.cs (491) — orchestrator + ctor + DI + ProcessEventsAsync + EventIngestResult DTO - EventIngestProcessor.Classification.cs (212) — ClassifyEvents, IsPeriodicOrStallEvent, UpdateSessionStatusAsync - EventIngestProcessor.Notifications.cs (168) — SendWebhookNotificationsAsync, BuildSignalRMessages - EventIngestProcessor.AppInstall.cs (171) — AggregateAppInstallEvent - EventIngestProcessor.RuleStats.cs (86) — RecordGatherRuleStatsAsync, RecordAnalyzeRuleStatsAsync Pure mechanical refactor — code is verbatim, only file boundaries moved. All 596 backend tests green. No changes to behaviour or to legacy /api/agent/ingest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d218a6ca25 |
feat(backend): M5.b.2 EventIngestProcessor — V2 endpoint event-pipeline parity
Extract the post-parse event-processing tail of IngestEventsFunction into a standalone EventIngestProcessor service so /api/agent/telemetry runs the same downstream logic as /api/agent/ingest. Deliberate copy-duplication, not a shared refactor: - Legacy /api/agent/ingest is production-hot — it serves live traffic from every deployed V1 agent and is NOT touched by this commit. Zero regression risk on the existing path. - EventIngestProcessor is a near-verbatim copy of the legacy pipeline's tail (~1000 LoC). Duplication is accepted because legacy /api/agent/ingest is scheduled for decommission post-v11 GA (see tasks/todo.md → Follow-Ups → Legacy-Agent-Ausmusterung). Until then: bugs fixed in legacy must be ported here manually. Copied pipeline steps (all 1:1 with legacy ProcessIngestAsync lines 150-566): StampServerFields → SanitizeEventTimestamps → StoreEventsBatchAsync → Index updates fire-and-forget → IME agent version history → ClassifyEvents → App install summaries → Geo-location update → UpdateSessionStatusAsync (all 7 status branches) → IncrementSessionEventCountAsync → Rule engine analysis fire-and-forget + SignalR + stats → Vulnerability correlation with WhiteGlove Part-2 merge → IncrementPlatformStatAsync TotalEvents + SuccessfulEnrollments → RecordGatherRuleStatsAsync → Diagnostics blob update → GetSession for updatedSession → Session age 4h warning → WhiteGlove IsPreProvisioned error log → SendWebhookNotificationsAsync (all 3 alert paths) → SLA breach evaluation → Admin override detection → ServerAction fetch-and-clear with TrackEvent telemetry → BuildSignalRMessages. Shared (not duplicated, to avoid drift in pure helpers): IngestEventsFunction.StampServerFields, SanitizeEventTimestamps, RecalculateAppDurations (all internal static, state-less, stable). The internal DTOs EventClassification + AppInstallAggregationState are also reused — they carry no behaviour, only fields. IngestTelemetryFunction wiring: - Return type HttpResponseData → IngestEventsOutput so the V2 endpoint can emit SignalR messages for the real-time UI push (matches legacy binding). - PersistItemsAsync partitions the batch: Events go through the processor (full pipeline + AdminAction + pending ServerActions + SignalR); Signal + Transition batches skip the processor and read control signals directly via a lean ReadControlSignalsAsync helper on ISessionRepository — no point firing the full pipeline for a batch that carries no events. - EventIngestProcessor registered in Program.cs DI. Known gap (explicitly NOT in this commit): the V2 endpoint lacks a payload size guard (legacy reads MaxNdjsonPayloadSizeMB from tenant config in ParseNdjsonRequest). Different payload shape (JSON array vs NDJSON), deferred to a dedicated follow-up. All 596 backend tests green (unchanged count — copy path has no new tests yet; the V2 endpoint's behaviour is validated by the legacy test coverage through the shared-helper surface + the TelemetryPayloadParser tests from M5.b.1). Integration coverage lands with M7 test-VM smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f0fc3696f6 |
feat(backend): M5.b IngestTelemetry endpoint — POST /api/agent/telemetry
V2 ingest endpoint consuming a heterogeneous batch of TelemetryItemDtos (Event + Signal + DecisionTransition in one JSON array). gzip request bodies are decoded transparently by the existing UseRequestDecompression middleware — new code only has to read a plain stream. - TelemetryItemDto (Shared): wire-format DTO mirroring the agent's TelemetryItem JSON shape so the backend can deserialise without linking agent assemblies. - IngestTelemetryFunction: security check (cert / rate / hardware whitelist) → device & version kill-switch short-circuits with DeviceBlocked=true body → body parse → tenant-header/body match → partition by Kind → persist via the appropriate repository → 200 with AdminAction + Actions control-signal fields populated from ISessionRepository (matching the /api/agent/ingest parity needed by the M4.6.ε agent-side UploadResult parser). - TelemetryPayloadParser: pure defensive parsing layer (wire → storage record). Malformed PayloadJson on a single item returns null; one poisonous item can't poison the batch. Projects index discriminators (IsTerminal, ClassifierVerdictId, ClassifierHypothesisLevel) off the DecisionTransition payload so the later queue-driven index fan-out reads them straight off the primary row. - EndpointAccessPolicyCatalog: POST agent/telemetry registered under DeviceOrBootstrapAuth (same policy as agent/ingest). Without this the fail-closed catalog rejects the route with 403. Scope boundary — Events get stored via StoreEventsBatchAsync only; the rule engine, app-install aggregation, SignalR notification, and vulnerability correlation remain on the legacy /api/agent/ingest path. Sharing that pipeline is tracked as M5.b.2 (extract EventIngestProcessor so both functions run the same downstream steps) — M7 rollout gates on completing it. Documented inline in PersistItemsAsync. 28 new tests: defensive null-handling per kind, typed-column extraction for Signal (with envelope fallback for missing SessionTraceOrdinal), DecisionTransition dead-end + classifier-verdict nesting, IsTerminal authoritative-list guard (Completed / Failed / WhiteGloveSealed / WhiteGloveCompletedPart2 — mirrors DecisionCore SessionStageExtensions), PartitionKey parser including malformed-shape rejection. The EndpointPolicyCatalogCompletenessTests guard the route registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eeaa0643a2 |
feat(backend): M5.a primary tables — Signals + DecisionTransitions repositories
Groundwork for the V2 ingest path (/api/agent/telemetry). Adds the two primary Decision-Engine tables projected from the agent-side SignalLog and Journal so the Inspector + Reducer-Verifier can replay sessions. - Constants.TableNames: Signals, DecisionTransitions (+ All[] init). - Shared DTOs SignalRecord / DecisionTransitionRecord carry a flat projection plus the agent-serialised PayloadJson for full fidelity. DecisionTransitionRecord eagerly carries index discriminators (IsTerminal, ClassifierVerdictId, ClassifierHypothesisLevel) so the later index fan-out can read them straight off the primary row. - ISignalRepository / IDecisionTransitionRepository: lean interfaces (StoreBatchAsync only — queries land with the read endpoints in M5.c). - TableSignalRepository / TableDecisionTransitionRepository: idempotent UpsertReplace via entity-group-transactions, grouped per (tenant, session), chunked at the 100-op Azure limit. PayloadJson goes through TableStorageChunking so the 32K-char property ceiling can't bite on large Evidence + Payload blobs. - Registered in DataAccessServiceExtensions alongside the existing 13 Table repositories — Cosmos swap remains a single-file change. - 11 pure mapping tests cover key builders (PK tenant_session, RK D19(ordinal) / D10(stepIndex), lex-ordering invariant), full entity projection, chunked PayloadJson handling, dead-end paths, and null-payload tolerance. Index tables (SessionsByTerminal, SessionsByStage, DeadEndsByReason, ClassifierVerdictsByIdLevel, SignalsByKind) land with the queue-driven fan-out in M5.b; the primary schemas are stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
941e03cf32 |
refactor(agent-v2): rename telemetry endpoint to /api/agent/telemetry
Place the new V2 ingest route under the existing /api/agent/* namespace (where all other agent-writes live: register-session, ingest, config, upload-url) instead of the orphan /api/telemetry/* bucket. Agent writes vs. admin/web reads is the thematic split used throughout the repo — /api/sessions/* is reserved for read-side and admin action routes. Constant renamed TelemetryBatch → IngestTelemetry to mirror the legacy IngestEvents pattern; semantic successor, heterogeneous TelemetryItem[] payload replacing per-type ingest once Legacy is decommissioned. No wire-format or handler changes — URL string only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a0d0178353 |
feat(agent-v2): gzip request body in BackendTelemetryUploader
Legacy IngestEventsAsync compressed the NDJSON body with gzip (~70% reduction) — low-bandwidth OOBE links depend on it. The V2 uploader only benefitted from response-side gzip (AutomaticDecompression on the mTLS handler) and shipped plain-JSON outbound. Compress the serialised JSON-array batch with GZipStream, swap StringContent for ByteArrayContent, advertise Content-Encoding: gzip. Wire format stays JSON-array — M5 backend contract is unchanged, only the transport is compressed. RecordingHttpMessageHandler transparently gunzips captured bodies so existing parse assertions keep working, and now exposes RawBody for the new gzip magic-byte / size test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9615b4c902 |
feat(agent-v2): M4.6.ε backend kill-switch + device-block + admin-action plumbing
Closes the last peripheral-parity gap flagged by the user: Legacy's admin
kill-switch / device-block / admin-mark-failed signals flow through the
batch-upload response and drive the same termination / pause paths. Without
this the V2 agent had no way to be remotely stopped or quarantined — it
could only be self-destructed by its own decision engine.
UploadResult extended
- New optional fields on success: DeviceBlocked, UnblockAt, DeviceKillSignal,
AdminAction, Actions. Factory OkWithSignals(...) for success-with-control.
- Legacy-parity: same semantic as IngestEventsResponse's quarantine / kill /
admin surface, just riding on the new /api/telemetry/batch response body.
BackendTelemetryUploader.TryReadControlSignalsAsync
- Parses the 2xx response body as JSON (defensive — empty body or malformed
JSON degrades cleanly to plain Ok). Accepts both camelCase and PascalCase
field names so an M5 backend can choose either without breaking V2
agents in the wild.
- Recognises deviceBlocked (bool) + unblockAt (ISO-8601 UTC) + deviceKillSignal
(bool) + adminAction (string) + actions (ServerAction[]).
TelemetryUploadOrchestrator
- New public event ServerResponseReceived fires on successful uploads that
carry any control signal. Handlers run after the cursor has moved, so a
handler exception does not abort or re-run the batch.
- New public IsPaused property — DeviceBlocked sets an internal
_pausedUntilUtc; subsequent DrainAllAsync calls short-circuit with a
"device blocked" LastErrorReason until UnblockAt elapses (or indefinitely
when UnblockAt is null). UnblockAt=null maps to DateTime.MaxValue so the
comparison stays deterministic without a null branch per tick.
EnrollmentOrchestrator.Transport property
- Exposes the live TelemetryUploadOrchestrator so Program.cs can subscribe
to ServerResponseReceived without leaking transport internals through
public orchestrator API.
Program.cs WireTelemetryServerResponse
- Routes ServerResponseReceived through the existing ServerActionDispatcher:
• DeviceKillSignal → synthesises terminate_session with forceSelfDestruct=
true + gracePeriodSeconds=0 (kill overrides local SelfDestructOnComplete
config — Legacy parity, the operator's decision wins)
• AdminAction → synthesises soft terminate_session respecting local config
• Actions[] → dispatched directly
• DeviceBlocked → logged (the transport already paused itself)
- onTerminateRequested extended: reads params["forceSelfDestruct"] and
flips agentConfig.SelfDestructOnComplete at runtime when set, so the
cleanup service actually runs.
Tests (+15 in V2.Core.Tests → 384)
- BackendTelemetryUploaderControlSignalsTests: empty body, malformed JSON,
DeviceBlocked + UnblockAt, PascalCase accepted, DeviceKillSignal,
AdminAction, Actions array round-trip, non-2xx does not parse signals,
body with only unknown fields.
- TelemetryUploadOrchestratorServerResponseTests: plain Ok is silent,
kill-signal raises event once, admin-action forwards field, actions
forward list, DeviceBlocked pauses → unblock lifts pause → next drain
uploads the queued item, handler exceptions do not break cursor.
All 4 suites green: 88 DecisionCore + 384 V2.Core + 507 Agent.Core + 557 Functions
= 1536 tests green.
With this commit M4.6 closes all known Legacy-vs-V2 peripheral-parity gaps:
agent_kill (DeviceKillSignal → synthesized terminate_session), agent_blocked
(DeviceBlocked → paused drain), portal Mark-Failed/Mark-Succeeded (AdminAction
→ synthesized terminate_session), plus the generic server-queued Actions[].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
50c3bf0516 |
feat(agent-v2): M4.6.δ diagnostic modes + analyzer manager + gather-rules runtime
Closes the final M4.6 batch of peripheral-parity gaps. After this commit the
V2 agent has full feature parity with the Legacy agent around every
capability NOT inside the DecisionEngine itself.
Diagnostic CLI modes (Program.*.cs partials)
- --run-gather-rules: ported 1:1 from Legacy. Resolves TenantId via V2
TenantIdResolver, creates a fresh ephemeral SessionId, registers it,
fetches remote config, runs GatherRuleExecutor against all Trigger=startup
rules (120s cap), uploads the collected events in-memory via
BackendApiClient.IngestEventsAsync. Never touches the live agent's spool.
- --run-ime-matching <path> [--patterns <file>]: ported 1:1 from Legacy.
Single-file or directory scan through the same IME log-file patterns the
live tracker watches, loads patterns from GitHub (fallback: cached
remote-config.json), compiles regexes with {GUID} substitution + 1s
timeout, scans + writes ime_pattern_matching.log with per-pattern counts.
- Both modes fork early in Main before orchestrator startup — no side
effects on the Scheduled Task, no process conflict with the live agent.
AgentAnalyzerManager (V2.Core/Runtime/)
- Ported from Legacy AnalyzerManager. Reads AnalyzerConfiguration from
AgentConfigResponse.Analyzers directly (no RemoteConfigService
indirection — the cache is already resolved at call site).
- Initialize() registers LocalAdminAnalyzer / SoftwareInventoryAnalyzer /
IntegrityBypassAnalyzer gated by Enable* flags; idempotent across
repeated calls.
- RunStartup() fires on a background task, swallows per-analyzer
exceptions. Called from Program.cs right after orchestrator.Start so
analyzers emit through the live TelemetryEventEmitter.
- RunShutdown(int? whiteGlovePart) runs synchronously before the
diagnostics upload, routing SoftwareInventoryAnalyzer to its overload
that distinguishes WhiteGlove part 1 baseline from part 2 delta.
EnrollmentTerminationHandler integration
- New optional `analyzerManager` ctor param. RunShutdownAnalyzers runs
FIRST in the termination sequence (before FinalStatus + diagnostics
upload) so the analyzers' delta events make it into the final
diagnostics ZIP. Passes whiteGlovePart=1 on WhiteGloveSealed terminal.
GatherRuleExecutorHost (DefaultComponentFactory nested)
- Runtime gather-rules execution via a new ICollectorHost that wraps
GatherRuleExecutor. Only registered when remoteConfig.GatherRules has
entries. Picks up ImeLogPathOverride for file-collector paths.
- Legacy parity: gather rules run as part of the active enrollment session
alongside the collector hosts, not only via the standalone --run-
gather-rules diagnostic mode.
Program.cs mode fork (Main)
- --install / --run-gather-rules / --run-ime-matching handled in order
after --help / --version, before multi-instance guard and self-update
(those only apply to the normal monitoring mode).
Tests (+8 in V2.Core.Tests → 369)
- AgentAnalyzerManagerTests: toggle-matrix, null-config default behaviour,
idempotent Initialize, argument-null guards, fake-analyzer injection for
RunShutdown success/exception/empty-list paths. The three real analyzers
already have their own parity-tested implementations in V2.Core shared
with Legacy.
All 4 suites green: 88 DecisionCore + 369 V2.Core + 507 Agent.Core + 557 Functions
= 1521 tests green.
M4.6 is now complete. The V2 agent matches Legacy feature parity for:
install mode, self-update, multi-instance guard, clean-exit marker,
previous-exit classification, enrollment-complete ghost-restart detection,
session-age emergency break, AgentMaxLifetime watchdog, BootstrapConfigCleanup,
terminal-stage detection with typed Terminated event, FinalStatus.json +
SummaryDialog launch via UserSessionProcessLauncher, CleanupService self-
destruct (with WhiteGlove-part-1 pause), DiagnosticsPackageService upload,
ServerActionDispatcher (terminate_session / request_diagnostics / rotate_
config callbacks ready for M5 response-path plumbing), Geo/NTP/Timezone
probes, Emergency + Distress reporters, DeliveryOptimizationCollector,
3 analyzers, --run-gather-rules + --run-ime-matching diagnostic modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7e199019ce |
feat(agent-v2): M4.6.γ diagnostic probes, reporters, DO collector wired
Closes the third batch of peripheral-parity gaps — Legacy's fire-and-forget
startup probes, the two cert-less + authenticated error-reporting channels,
and Delivery-Optimization telemetry all run in V2.
StartupEnvironmentProbes (V2.Core/Runtime/)
- New helper that replicates the Legacy MonitoringService startup diagnostic
trio: device_location / timezone_auto_set / ntp_time_check.
- Sequential RunAsync(config, logger, emitter): geo lookup → (optional)
tzutil auto-set → NTP offset check. Each step is best-effort and feeds
through the orchestrator's TelemetryEventEmitter.
- Geo probe respects EnableGeoLocation; timezone probe respects
EnableTimezoneAutoSet AND requires an IANA tz from geo. NTP probe always
runs (diagnostic-only; Warning on offset >60s or failure).
- Program.cs wires this via Task.Run right after orchestrator.Start so a
slow network never delays the critical path.
EmergencyReporter + DistressReporter (live in Program.cs)
- Previously instantiated nowhere — now built once at startup with the
shared hardware info. RemoteConfigService ctor takes both so config-fetch
failures flow to the right channel (auth → distress, network → emergency).
- Initial AuthCertificateMissing distress fires when UseClientCertAuth=true
but BackendApiClient.ClientCertificate is null — surfaces pre-MDM-
enrollment dead-ends via the cert-less channel (Legacy parity).
- BackendTelemetryUploader uses the same hardware info (dedup'd the second
HardwareInfo.GetHardwareInfo call that was duplicated since M4.5.b).
DeliveryOptimizationHost (DefaultComponentFactory nested)
- New ICollectorHost wrapping DeliveryOptimizationCollector with:
• getPackageStates → ImeLogHost.PackageStates (existing accessor)
• onDoTelemetryReceived → ImeLogHost.Tracker.OnDoTelemetryReceived pass-
through (new internal accessor; keeps encapsulation)
• OnAppStateChanged chained into ImeLogTracker so WakeUp() fires when
any app enters Downloading or Installing — before any fire we preserve
the existing handler (the adapter's signal-ingest path must keep
running unchanged).
- Gated by CollectorConfiguration.EnableDeliveryOptimizationCollector.
Tests (+8 in V2.Core.Tests → 353 → 361)
- StartupEnvironmentProbesTests: event-shape for all three probes + geo
success/failure/null attempt, timezone success/failure severity, NTP
small/large offset + failure-with-detail.
All 4 suites green: 88 DecisionCore + 361 V2.Core + 507 Agent.Core + 557 Functions
= 1513 tests green.
Note: the "RegistryWatcher standalone admin-action-detector" from the
original M4.6 gap list was a Subagent fabrication — verified no such usage
exists in either Legacy or V2 (the class is only consumed transitively
inside AadJoinWatcher + ProvisioningStatusTracker, both already wired via
DefaultComponentFactory since M4.5.b). Removed from the M4.6.γ scope.
Remaining: M4.6.δ (--run-gather-rules + --run-ime-matching modes,
AnalyzerManager + 3 Analyzers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
17063be600 |
feat(agent-v2): M4.6.β terminal dispatch — FinalStatus, SummaryDialog, cleanup, server actions
Closes the second batch of peripheral-parity gaps so the V2 agent actually
terminates like Legacy does: write FinalStatus.json, launch the WPF summary
dialog in the user session, upload diagnostics (respecting mode/outcome),
write the enrollment-complete marker, run self-destruct, and signal shutdown.
All of this is driven off a single typed kernel event so nothing leaks back
into the DecisionEngine.
Terminal-stage detection (kernel hook)
- DecisionCore/State/SessionStageExtensions.IsTerminal() + IsPauseBeforePart2()
— Completed, Failed, WhiteGloveSealed, WhiteGloveCompletedPart2 are terminal;
WhiteGloveSealed is the "pause before Part 2" special case (session resumes
on reboot, cleanup MUST be suppressed).
- DecisionStepProcessor ctor gains Action<DecisionState>? onTerminalStageReached;
fires at most once per processor lifetime, swallows handler exceptions,
suppressed when initial state was already terminal (crash-recovery race).
- EnrollmentOrchestrator.OnDecisionTerminalStage translates the hook into the
public Terminated event with Reason=DecisionTerminalStage + Outcome mapped
from stage (Completed/Part2/Sealed→Succeeded, Failed→Failed, else TimedOut).
MaxLifetime watchdog is cancelled on the real terminal so the two paths can't
race each other.
Peripheral termination (V2.Core/Termination/*)
- FinalStatus DTO — wire-format mirror of SummaryDialog's Models/FinalStatus
(same JSON property names). Lives in V2.Core because the dialog is a
separate net48 WPF exe we can't ProjectReference.
- FinalStatusBuilder — pure function: DecisionState + AppPackageStateList +
TerminatedArgs + agent start time → FinalStatus. Maps outcome string
("succeeded" | "failed" | "timed_out" | "whiteglove_part1" | "whiteglove_part2"),
derives signalsSeen from DecisionState facts, counts apps by AppTargeted
phase (Device/User), tallies device vs user errors.
- SummaryDialogLauncher — writes final-status.json, copies dialog exe + deps
+ status file to a per-launch temp dir (%TEMP%\AutopilotMonitor-Summary\<guid>),
composes the Legacy-compatible CLI (--status-file, --timeout, --cleanup,
optional --branding-url), invokes UserSessionProcessLauncher.LaunchInUserSession
with the configured retry window. Fire-and-forget, swallows all errors.
- EnrollmentTerminationHandler — the Terminated-event handler that sequences:
build FinalStatus → write JSON + launch dialog → upload diagnostics
(respects DiagnosticsUploadMode: Off/Always/OnFailure) → write
enrollment-complete.marker (suppressed on WhiteGloveSealed) → run
CleanupService.ExecuteSelfDestruct (suppressed on WhiteGloveSealed OR when
SelfDestructOnComplete=false) → signal shutdown MRE. Idempotent; each step
is best-effort and logs rather than throws — nothing is allowed to prevent
agent shutdown.
DefaultComponentFactory
- Stores the ImeLogHost it creates and exposes its PackageStates to the
termination pipeline (factory.ImePackageStates) so FinalStatusBuilder has
the live package list without breaking host encapsulation.
Program.cs wiring
- Instantiates DiagnosticsPackageService, EnrollmentTerminationHandler, and
ServerActionDispatcher with all callbacks live:
• rotate_config → RemoteConfigService.FetchConfigAsync()
• request_diagnostics → DiagnosticsPackageService.CreateAndUploadAsync()
• terminate_session → EnrollmentTerminationHandler.Handle() synthesising a
DecisionTerminalStage/Failed termination
- Terminated event routed to the handler; the handler signals the shutdown
MRE so the main thread cleanly calls orchestrator.Stop().
- ServerActionDispatcher is alive but has no ingest-response consumer yet —
V2 batch endpoint is M5 scope, we plumb the API contract here so M5 can
drop in the response parser.
Tests (+47 total)
- DecisionCore.Tests: +15 SessionStageExtensionsTests (IsTerminal + IsPauseBeforePart2)
- V2.Core.Tests: +32 across terminal-stage hook, FinalStatusBuilder, handler
• DecisionStepProcessorTerminalStageTests: fires once per stage, skips
non-terminal, respects initial-terminal-state (recovery), swallows
handler exceptions, works with null callback
• FinalStatusBuilderTests: outcome mapping per stage, uptime, HelloOutcome
fact path, signalsSeen composition, empty package list, device/user
error tallying, JSON property-name contract
• EnrollmentTerminationHandlerTests: writes FinalStatus.json + marker,
runs/skips cleanup per config + WhiteGlove rule, idempotent shutdown
signal, diagnostics mode matrix (Off/Always/OnFailure × Succeeded/Failed),
suffix ("success" | "failure") per outcome
All 4 suites green: 88 DecisionCore + 353 V2.Core + 507 Agent.Core + 557 Functions
= 1505 tests green.
Remaining M4.6 scope: γ (Geo/NTP/Timezone/Emergency/Distress services wiring,
RegistryWatcher admin-action detector, DeliveryOptimizationCollector in
DefaultComponentFactory) + δ (--run-gather-rules + --run-ime-matching modes,
AnalyzerManager + 3 analyzers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
02bbb5490c |
feat(agent-v2): M4.6.α peripheral parity — lifecycle, install, self-update, watchdog
Closes the first batch of peripheral-parity gaps between Legacy and V2 so the
V2 agent is actually deployable/runnable in production. M4 kernel is unchanged
— only wiring and startup helpers.
Deployment + install
- Program.InstallMode.cs: --install mode ported 1:1 (payload copy, schtasks
Create/Run, bootstrap-config.json + await-enrollment.json persistence,
HKLM\SOFTWARE\AutopilotMonitor\Deployed marker)
- Configuration/BootstrapConfigFile.cs + AwaitEnrollmentConfigFile.cs
- Scheduled Task target is AutopilotMonitor.Agent.V2.exe (same task name
as Legacy — V2 replaces Legacy on the device)
Startup guards (Program.Guards.cs)
- Multi-instance guard (IsAnotherAgentInstanceRunning)
- clean-exit.marker writer registered in AppDomain.ProcessExit
- DetectPreviousExit: reads marker + crash_*.log + session files + event log
→ classifies {first_run, clean, exception_crash, hard_kill, reboot_kill}
- CheckEnrollmentCompleteMarker: ghost-restart detection via HKLM Deployed
key + enrollment-complete.marker → cleanup-retry + exit
- CheckSessionAgeEmergencyBreak: AbsoluteMaxSessionHours watchdog across
restarts (respects whiteglove.complete pause)
- WriteCrashLog: last-resort crash_YYYYMMDD_HHMMSS.log
- TryRead/Delete bootstrap-config.json + await-enrollment.json helpers
Self-update (SelfUpdater wired for the first time in V2 startup)
- CleanupPreviousUpdate + CheckAndApplyUpdateAsync called BEFORE logger init
- Cached remote-config.json read → BackendExpectedSha256 + AllowAgentDowngrade
- VersionCheckEventBuilder wired: emits agent_version_check event via
orchestrator.EventEmitter right after Start (when startup markers exist)
Agent max-lifetime watchdog (EnrollmentOrchestrator)
- New ctor param agentMaxLifetime: TimeSpan?
- New typed API: event EventHandler<EnrollmentTerminatedEventArgs> Terminated
with EnrollmentTerminationReason (MaxLifetimeExceeded | DecisionTerminalStage)
+ EnrollmentTerminationOutcome (Succeeded | Failed | TimedOut)
- M4.6.α fires MaxLifetimeExceeded only; DecisionTerminalStage is the M4.6.β
hook for CleanupService self-destruct + SummaryDialog launch
- Program.cs wires the event to the shutdown ManualResetEventSlim
Bootstrap-config cleanup (H-2 mitigation)
- BootstrapConfigCleanup.TryDeleteIfCertReadyAsync now actually called after
RemoteConfigService.FetchConfigAsync (cert probe confirms mTLS works)
SessionIdPersistence extensions
- New session.created companion file (UTC roundtrip) for the emergency break
- whiteglove.complete resume marker awareness
- SessionExists / LoadSessionCreatedAt / SaveSessionCreatedAt / IsWhiteGloveResume
- Delete now clears session.id + session.created + whiteglove.complete
Tests (+26 in V2.Core.Tests → 295 → 321)
- SessionIdPersistenceTests: +8 tests covering the new methods
- EnrollmentOrchestratorMaxLifetimeTests: watchdog fires once, respects null,
rejects non-positive, is cancelled by Stop (under SerialThreading collection)
- ProgramGuardsTests: 13 tests covering DetectPreviousExit state machine,
CheckSessionAgeEmergencyBreak (pass/trip/whiteglove-resume/recover-missing-
session.created paths), TryRead/Delete bootstrap + await-enrollment configs,
WriteCrashLog format
- V2.Core.Tests gains ProjectReference to V2 exe + V2 exe exposes
InternalsVisibleTo so the Program.Guards internal statics are testable
without duplicating code into V2.Core
All 4 suites green: 73 DecisionCore + 321 V2.Core + 507 Agent.Core + 557 Functions
= 1458 tests green.
Still open for M4.6.β: CleanupService terminal-event wiring, ServerActionDispatcher,
DiagnosticsPackageService, SummaryDialog launch + FinalStatusBuilder,
UserSessionProcessLauncher / UserProfileResolver wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9f45b5b7a6 |
test(v2core): M4.5.c stabilize SignalIngress + DeadlineScheduler threading flakes
Four tests were flaky under full-suite parallelism even though they passed
in isolation (SpinWait/ManualResetEventSlim timeouts expire when the
ThreadPool is contended by unrelated test classes):
- SignalIngressTests.Clock_advance_past_throttle_window_re_arms_emission
- SignalIngressTests.Null_observer_is_supported_and_back_pressure_is_silent
- DeadlineSchedulerTests.Reschedule_same_name_replaces_old_timer
- DeadlineSchedulerTests.RehydrateFromSnapshot_reschedules_each_deadline_past_due
Fix: [Collection("SerialThreading")] on both classes (xUnit runs them
sequentially against each other) + wait-helper/DefaultWaitMs bumped
2000ms→5000ms for threadpool-contention cushion.
Verified: 5× full-suite runs green (287/287 pre-M4.5.b, 295/295 post).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0ee68c6192 |
feat(agent-v2): M4.5.b runtime entry + DefaultComponentFactory
Wires the V2 agent into a runnable state: - TenantIdResolver: HKLM\SOFTWARE\Microsoft\Enrollments → AADTenantID (EnrollmentType=6) - SessionIdPersistence: atomic session.id via File.Replace - IComponentFactory signature: +ISignalIngressSink +IClock (bridges the sink/clock access into factory-built adapters) - DefaultComponentFactory: 5 kernel hosts (EspAndHelloTracker coordinator, DesktopArrivalDetector, AadJoinWatcher, ImeLogTracker+ImeProcessWatcher, StallProbeCollector) + 2 peripheral hosts (Performance, AgentSelfMetrics) conditional on CollectorConfiguration toggles - Program.cs: full runtime entry (args → Logger → TenantId/SessionId → AwaitEnrollment → BackendApiClient+RemoteConfigService → MtlsHttpClientFactory → BackendTelemetryUploader → DefaultComponentFactory → EnrollmentOrchestrator → Ctrl+C wait → Stop → exit 0) Production host choice: EspAndHelloTracker as single coordinator entry (instead of 4 sub-tracker hosts in parallel) — avoids adapter signal duplication documented in M4.3 tech-debt. StallProbeHost drives its own 60s idle-tick timer (Legacy MonitoringService.IdleCheckCallback is gone). Tests: +8 SessionIdPersistenceTests (atomic-write, round-trip, corrupt- recovery, regeneration, multi-instance resume). V2.Core 287→295, total 1424→1432 green. Known peripheral gaps (tracked for follow-up M4.6): install mode, SelfUpdater, ServerActionDispatcher, CleanupService, Geo/NTP/Timezone wiring, Emergency/DistressReporter wiring, AnalyzerManager, SummaryDialog, DiagnosticsPackageService, gather-rules/ime-matching modes, watchdog timers, multi-instance guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1e77305857 |
docs: lesson — generalize no-assumption-without-evidence to all decisions
Meta-Regel top-of-Rules: grep/path-name/plan-doc/agent-report sind Triage, nicht Evidenz. Vor Delete/Refactor/Classify/Behauptung muss die relevante Stelle gelesen worden sein. Im Zweifel ganze Datei lesen. Link auf feedback_no_assumption_without_evidence.md, das jetzt die erweiterte Meta-Version enthaelt (nicht mehr nur session-classification- spezifisch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2ea959b93b |
docs: lesson — peripheral capability classification by function, not folder
Nach M4.5.a-Incident: ImeTrackerStatePersistence wurde als "enrollment-legacy" geloescht weil Pfad "Monitoring/Enrollment/Ime/" enthaelt. Tatsaechlich persistierte die Klasse collector-internal IME-Tracker-State (File-Positions, SeenAppIds, AppPackageStates mit DO-Telemetry + Install-Progress) — peripheral capability per feedback_v2_kernel_only_refactor. Rule: vor jedem Delete in V2.Core den File oeffnen + nach FUNKTION klassifizieren (DecisionEngine vs Collector-Concern), nicht nach Pfad-Location. Plan-Doc + Explore-Agent-Output sind Startpunkte, nicht Autoritaet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
925fc707cc |
fix(v2core): M4.5.a restore ImeTrackerStatePersistence (peripheral capability, not dead code)
M4.5.a hat ImeTrackerStatePersistence faelschlicherweise als Legacy
geloescht. Korrektur: die Klasse persistiert den IME-Collector-internal
State, nicht DecisionEngine-State:
- LogFilePositionTracker: Byte-Offset pro IME-Logfile (ohne das
re-parst der Tracker nach Restart von Byte 0 → Event-Spam)
- SeenAppIds: Phase-Isolation (Device-Phase-Apps duerfen nicht in
AccountSetup-Phase bleeden)
- IgnoreList: App-IDs aus abgeschlossenen Phasen, die weiterhin in
Logs auftauchen koennen
- Packages (AppPackageStateList): vollstaendiger Install-Progress,
DO-Telemetry, ErrorPatterns, AppType, AttemptNumber — essentiell
fuer App-Download-Monitor, Install-Monitor, App-Summaries
- Phase-Tracking (CurrentPhaseOrder, LastEspPhaseDetected, etc.)
SignalLog-Replay deckt das NICHT ab — der SignalLog persistiert
DecisionSignals (SessionStarted, EspPhaseChanged, HelloResolved, …),
nicht per-App-Install-Progress oder Log-File-Positions.
Restore:
- ImeTrackerStatePersistence.cs wieder aus git history
- ImeLogTracker.cs: _statePersistence-Feld + Ctor-Init +
LoadState/SaveState/DeleteState-Bodies aus 8ca845c^ restored
(nur die beiden dead using-Imports auf Flows/Completion bleiben
entfernt, da deren Namespaces tatsaechlich geloescht sind)
Build + Tests gruen (286/287, known pre-existing Flake bleibt fuer
M4.5.c).
Danke an User fuer den Catch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8ca845c9cb |
refactor(v2core): M4.5.a legacy dead-code cleanup (~9360 LoC removed)
Scope: alle Enrollment/Transport/Runtime-Glue-Files aus dem Legacy-M2-
Import, die durch DecisionEngine+Reducer (M3) und Transport/Telemetry
(M4.1) abgeloest sind und keine aktiven V2-Konsumenten mehr haben
(Program.cs ist noch M2-Skeleton, null Test-Refs auf Legacy).
Delete:
- Monitoring/Runtime/{MonitoringService, SessionPersistence,
CollectorCoordinator, PeriodicCollectorManager, AnalyzerManager}
- Monitoring/Enrollment/{EnrollmentTracker.*cs (4 partials),
EnrollmentStatePersistence}
- Monitoring/Enrollment/Flows/ (4 files, Ordner weg)
- Monitoring/Enrollment/Completion/ (6 files, Ordner weg)
- Monitoring/Enrollment/Ime/{ImeTrackerStatePersistence, LogReplayService}
- Monitoring/Transport/{EventSpool, EventUploadOrchestrator}
Fix peripherals (Import-Pruning inline):
- ImeLogTracker: ImeTrackerStatePersistence-Referenzen entfernt, Load/
Save/DeleteState sind no-op stubs. V2 resumed ueber SignalLog-Replay
+ SignalAdapter fire-once dedup.
- AgentSelfMetricsCollector: EventSpool-Param + spool_queue_depth-
Metric entfernt. V2-Equivalent via TelemetrySpool kommt in M4.5.b.
- 10 Files: dead using-Imports auf Enrollment.Flows / .Completion
entfernt (sed-Batch).
Keep (aktive V2-Konsumenten):
- BackendApiClient, EmergencyReporter, DistressReporter
→ RemoteConfigService.FetchConfigAsync() bleibt auf GetAgentConfig
bis M5 oder V2-Slim-Client
- NetworkMetrics (Utility), ImeLogTracker + Ime/-Support (aktiver
V2-Collector)
- Peripheral capabilities (per feedback_v2_kernel_only_refactor):
DiagnosticsPackageService, CleanupService, PerformanceCollector,
DeliveryOptimizationCollector, SelfUpdater, ServerActionDispatcher,
RegistryWatcher, UserProfileResolver, UserSessionProcessLauncher,
VersionCheckEventBuilder, NetworkChangeDetector, Analyzers
Inventory-Greps alle 0 (nur noch README.md + Kommentar-Refs):
EnrollmentTracker class, CompletionStateMachine, IEnrollmentFlowHandler,
EventSpool, EventUploadOrchestrator, MonitoringService (Code),
CollectorCoordinator, PeriodicCollectorManager, AnalyzerManager.
V2-Isolation-Grep (using AutopilotMonitor.Agent.Core): 0.
Build gruen, Tests: 286/287 (pre-existing Flake
SignalIngressTests.Null_observer_is_supported_and_back_pressure_is_silent,
M4.5.c-Scope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
aca66a79c0 |
refactor(v2core): M4.4.5.g integration tests (14 M3 scenarios as real V2 runtime tests)
Release-Gate M4.4. Alle 14 M3-Szenarien laufen durch den echten V2- Agent-Runtime (DecisionEngine + DecisionStepProcessor + SignalIngress + EffectRunner + Persistence + TelemetryTransport + production-Classifier), nicht als M3-Harness-Replay. EnrollmentOrchestratorFixture lockstep-synchronisiert den Fixture- Replay ueber processor.CurrentState.StepIndex + ingress.Queue-Length: StepIndex wird erst NACH DecisionStepProcessor.ApplyStep inkrementiert (inkl. EffectRunner.RunAsync, das synthetische ClassifierVerdictIssued- Signale postet) — Wait auf signalLog.LastOrdinal allein war race-prone, weil LastOrdinal in SignalIngress.ProcessItem VOR EffectRunner updated. Szenario-Coverage (alle gruen): 01 UserDriven-Happy, 02 UserDriven-HelloTimeout, 03 SelfDeploying-Happy, 04 SelfDeploying-EspExitUnknown, 05 WhiteGlove-Inline, 06 WhiteGlove-SignalCorrelated, 07 WhiteGlove-AntiLoop, 08 WhiteGlove-FalsePositive-LateAADJ, 09 WhiteGlovePart2-Happy, 10 WhiteGlovePart2-Stuck, 11 Hybrid-Reboot, 12 ESP-Terminal-Failure, 13 Late-AADJ-Standalone, + Telemetry-Pipeline-Smoke. DevPrep-v2-Varianten nicht drin: keine M3-Fixture vorhanden (follow-up M5/M7). FixtureLoader als Copy aus DecisionCore.Tests (Plan §2.16 Copy-Not-Share fuer V2-Isolation). Fakes an System-Boundaries: FakeBackendTelemetry Uploader, VirtualClock, TempDirectory. Real: Persistence, Ingress, EffectRunner, Classifiers. +14 Integration-Tests (273 → 287 gruen). Gesamte Test-Suite: ~287 V2.Core + 73 DecisionCore + 557 Functions = ~917 Tests plus Legacy Agent.Core 507. Pre-existing Threading-Flakiness in SignalIngressTests/DeadlineSchedulerTests bleibt (unveraendert, tracked als known-flake in Orchestration-Testclass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a6f515643f |
refactor(v2core): M4.4.5.f recovery paths (WG part-1 resume + segment quarantine + transport resume)
Plan §2.7 Sonderfaelle 1-3 umgesetzt:
Sonderfall 1 — WG Part-1 → Reboot → Part-2:
Orchestrator erkennt state.Stage == WhiteGloveSealed beim snapshot.Load
und setzt IsWhiteGlovePart2Resume-Flag. Nach ingress.Start (vor
Collectors/Adapters) wird ein SessionRecovered-Signal in den Ingress
gepostet — der Reducer bridged ueber HandleWhiteGlovePart1To2Bridge zu
WhiteGloveAwaitingUserSignIn. Self-Destruct-Disable bleibt Caller-Sache
(Info via IsWhiteGlovePart2Resume exponiert).
Sonderfall 2 — Beschaedigtes Segment:
Snapshot-File existiert aber Load liefert null (Checksum-Mismatch oder
Deserialize-Failure) → SnapshotPersistence.Quarantine + neuer
SegmentQuarantine-Helper bewegen Snapshot/SignalLog/Journal/
EventSequence nach .quarantine/{timestamp}/. Writer werden danach
reinstantiiert (stale LastOrdinal/LastStepIndex → reset auf -1). Fresh
start mit DecisionState.CreateInitial. Observability via
WasStartupQuarantine.
Sonderfall 3 — Transport-Resume:
Kein Code-Aenderung noetig: UploadCursorPersistence + TelemetrySpool
lesen persistierten Cursor im Ctor, Orchestrator resumed automatisch
ab LastUploadedItemId + 1. Wire-Format (PK, RK) garantiert Backend-
Idempotenz. Test verifiziert: seeded Spool mit 3 Items + Cursor@0 →
Resume liefert nur Items 1+2.
+5 RecoveryPathTests (268 → 273 gruen). Helper-Klasse
Persistence/SegmentQuarantine (parallel zu SnapshotPersistence.Quarantine)
als oeffentliche API fuer zukuenftige Recovery-Szenarien.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
81b3e053a3 |
refactor(v2core): M4.4.5.e whiteglove sealing pattern IDs injection
Neue AgentConfigResponse.WhiteGloveSealingPatternIds-Property traegt die Tenant-Config-Liste der Pattern-IDs, die vom V2-Agent als WhiteGloveSealingPatternDetected-DecisionSignal re-emittiert werden. Default leer = Feature off = M3-kompatibel. IComponentFactory.CreateCollectorHosts nimmt zusaetzlich whiteGloveSealingPatternIds entgegen. Orchestrator akzeptiert die Liste per Ctor-Param und reicht sie an die Factory weiter, die sie an den ImeLogTrackerAdapter-Ctor uebergibt (M4.4.4 hat den Hook vorbereitet). EnrollmentOrchestratorEventBridgeTests FakeFactory-Signatur nachgezogen. +3 WG-Pattern-Injection-Tests (265 → 268 gruen; null → empty coerce, verbatim passthrough, empty collection erhalten). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4300a01769 |
refactor(v2core): M4.4.5.d mTLS HttpClient factory + certificate resolver seam
MtlsHttpClientFactory baut HttpClient/HttpClientHandler mit Client-Cert attached fuer den BackendTelemetryUploader. Delegiert an ICertificateResolver-Seam (DefaultCertificateResolver in Prod nutzt CertificateHelper, Tests liefern Fake mit self-signed Cert). Factory wirft InvalidOperationException wenn kein Cert gefunden — mTLS ist Pflicht, Agent darf ohne Cert nicht starten. Decompression (GZip + Deflate) + 30s Default-Timeout analog Legacy BackendApiClient. CreateHandler separat exponiert, damit Tests direkt auf dem Handler asserten koennen (HttpClient._handler ist internal + reflection-fragil auf net48). +5 Agent.V2.Core Tests (260 → 265 gruen; self-signed-Cert via System.Security.Cryptography.CertificateRequest, kein Cert-Store-Touch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a72e716862 |
refactor(v2core): M4.4.5.c collector-host seam + event-bridge wiring
ICollectorHost + IComponentFactory als Seam fuer Collector-Produktion: Factory liefert fertig-konstruierte Hosts mit der telemetryEmitter.Emit- Callback bereits als onEnrollmentEvent-Sink gebunden. Orchestrator ruft Start/Stop/Dispose in korrekter Reihenfolge auf. Die 5 Collectors (HelloTracker, ShellCoreTracker, ProvisioningStatusTracker, StallProbeCollector, ModernDeploymentTracker) haben heavy Production-Deps (Event-Log-Watcher, WMI, Registry) — die Factory-Seam erlaubt Prod-Impl in Program.cs / Tests nutzen Fakes. Exception-Swallow-Wrapper im Orchestrator: ein Emitter-Fail (z.B. Transport disposed) darf den Collector-Thread nicht killen. ModernDeploymentTracker explizit als bridge-only dokumentiert: kein SignalAdapter, nur Event-Bridge — validiert durch dedizierten Test, der prueft dass Events im Transport landen aber SignalLog leer bleibt. Factory optional (default null): Orchestrator ohne Factory laeuft weiter ohne Collectors (backwards-compatible zu M4.4.5.b). +7 Agent.V2.Core Tests (253 → 260 gruen). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
51f9db6429 |
refactor(v2core): M4.4.5.b enrollment orchestrator skeleton + lifecycle
Top-Level-Lifecycle-Owner. Start() verdrahtet die Kern-Pipeline (SignalLog/Journal/Snapshot, TelemetrySpool+Uploader, EventSequence+ Emitter+Timeline+BackPressure, DeadlineScheduler+ClassifierRegistry, EffectRunner, DecisionStepProcessor, SessionTraceOrdinalProvider, SignalIngress) und verbindet Scheduler.Fired → ingress.Post-Bridge. Zirkel EffectRunner-ISignalIngressSink ↔ SignalIngress-Processor via LazyIngressSinkRelay gebrochen. Snapshot-Recovery-Vorstufe bei Start; volle Checksum/Replay-Logik kommt in M4.4.5.f. Shutdown idempotent + reverse-order: Scheduler-Unsubscribe → Scheduler dispose → Ingress.Stop → Drain-Loop cancel → Terminal-Drain → Final- Snapshot → Dispose. IQuarantineSink-Impl setzt Flag (Bewegung erst beim naechsten Start, nicht mid-run). Drain-Loop nutzt Task.Delay (Wall-Clock) statt IClock.Delay, damit VirtualClock-Tests nicht in Tight-Loop laufen. +14 Agent.V2.Core Tests (239 → 253 gruen). Collectors + SignalAdapters folgen in M4.4.5.c, mTLS-HttpClient-Factory in .d, Pattern-Injection in .e, Recovery-Pfade in .f. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ce62a6527d |
refactor(v2core): M4.4.5.a decision-step processor + quarantine escalation
Produktions-IDecisionStepProcessor als Applicator-Schicht zwischen SignalIngress und Journal/Effects/Snapshot. Sequenz: Journal-Append → EffectRunner (sync-over-async) → Snapshot (best-effort) → State-Forward. Quarantine-Eskalation: zaehlt aufeinanderfolgende Journal-Failures; ab Threshold (default 3) Fire-once Call an IQuarantineSink. SignalIngress schluckt das Throw weiter (Worker-Thread bleibt am Leben), aber der Orchestrator (M4.4.5.b/f) kennt den Alarm fuer die Quarantine-Bewegung beim naechsten Start. +11 Agent.V2.Core Tests (228 → 239 gruen). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8294f7a8a2 |
refactor(v2): M4.4.4 classifier-tick from session start + hello-outcome forward + pattern-match hook
Three related fixes Plan §4.x M4.4.4 calls out: 1. ClassifierTick armed at SessionStarted (DecisionCore.Shared) Previously the WhiteGlove classifier deadline was armed reactively on the first WG-relevant signal (AttachWhiteGloveClassifierEffects, WhiteGlove.cs). Non-WG sessions and late-WG sessions never got a periodic re-evaluation, which is the M3 re-trigger-lücke. Fix: HandleSessionStartedV1 now schedules the ClassifierTick deadline up-front and emits the matching ScheduleDeadline effect. The existing reactive `hasTick` dedup makes the WG-path arm a no-op once the tick is already present. Classifier-runner anti-loop (snapshot-hash) keeps the tick cheap for non-WG paths until the session reaches a terminal stage and HandleClassifierTickDeadlineFired stops re-arming. Dead-end branch (SessionStarted on active state) is unchanged — no deadline, no effect, state preserved. 2. HelloOutcome now forwards from coordinator (EspAndHelloTrackerAdapter) ReadHelloOutcome() previously returned hardcoded "unknown", so HelloResolved signals always carried that value regardless of what the inner HelloTracker actually observed. The coordinator already exposes a forwarded HelloOutcome property (line 78, since M2) — only the adapter was missing the wiring. One-line fix: `_coordinator.HelloOutcome ?? "unknown"`. 3. ImeLogTracker.OnPatternMatched hook + WhiteGloveSealingPatternDetected emission (ImeLogTracker + ImeLogTrackerAdapter, closing the M4.3.4 tech debt) Added `Action<string> OnPatternMatched` to ImeLogTracker, invoked by HandlePatternMatch after LastMatchedPatternId is set (subscribers can read the ID synchronously). Adapter gains an optional ctor arg `IReadOnlyCollection<string> whiteGloveSealingPatternIds` (default empty, backwards-compatible with pre-M4.4.4 M3 behavior — no emission). When configured and a matching pattern fires, adapter posts WhiteGloveSealingPatternDetected once per session (fire-once). Polling-on- LastMatchedPatternId (alternative A from plan) was rejected; the hook is a real event surface. Orchestrator in M4.4.5 will inject the tenant- configured sealing-pattern-ID list. Bonus: reduced EventSequenceCounterTests thread-safety stress from 6×200 to 4×50 to avoid parallel xUnit test contention on the Windows atomic-rename path (Next() persists to disk per call). Test still catches any counter race via the uniqueness assertion. SmokeTests.DecisionEngine_Reduce_SessionStarted_advancesStep updated to assert the new ClassifierTick effect/deadline instead of `Assert.Empty(step.Effects)`. +18 tests (1350 -> 1368 green, 57 DecisionCore + 218 V2.Core before vs. 73 + 228 after): ClassifierTickFromSessionStartTests (5) · EspAndHelloTrackerAdapter- HelloOutcomeTests (3) · ImeLogTrackerAdapterSealingPatternTests (7) · smoke + harness updates. Bestehende Scenario-Tests grün (Classic/WG1/WG2/ SelfDeploying/Edge) — ClassifierTick-at-start ist additiv, kein Scoring-Impact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fb98d43ae5 |
refactor(decisioncore): M4.4.3 reducer handlers for diagnostic signals
DeviceInfoCollected and AutopilotProfileRead previously fell through to HandleUnhandledSignal, writing unhandled_signal_kind:* DeadEnd rows into the journal on every emission — pure noise, no decision value, but distorting the Inspector timeline. Plan §4.x M4.4.3 fix. Both are diagnostic signals: they carry hardware inventory / Autopilot profile payload used only by the Inspector and backend analyzers. They do NOT drive stage or hypotheses. Fix is minimal: neutral Taken transitions via a new shared helper RecordDiagnosticObservation — bookkeeping advances, stage/hypotheses untouched, no effects, no DeadEndReason. Full payload stays in the SignalLog for Inspector rendering (Plan §2.7c — Snapshot/Journal are Cache, SignalLog is Master; no reason to duplicate data into DecisionState). Version-gap protection: dispatcher entries are keyed on (Kind, v1). A future v2 payload still dead-ends with unhandled_signal_kind:*:v2 — the L.6 forward-compat contract requires explicit handler upgrades, not silent recording under the old handler. AppInstallCompleted/AppInstallFailed intentionally stay unwired — no decision consumer in v11 scope, and the collector isn't emitting them yet. +11 tests (1339 -> 1350 green): Taken-without-DeadEnd, stage+hypotheses unchanged, bookkeeping advancement, no effects, existing deadlines preserved, v2-schema-version falls through to unhandled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |