mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
249e39bc687b00950476f2c7918d4e46c3e22f02
52 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6d607c9045 |
fix(agent): network-link gate before initial config fetch + post-registration recovery
Root cause (tenant aebdce78 audit 2026-08-24, session ba828038): on boot-time
relaunches the agent starts before the Wi-Fi link is up. The Phase 4 config fetch
had no network gate (
|
||
|
|
ff35270d5d |
agent: legacy bootstrap-config without TenantIdWaitSeconds must not disable the wait
A bootstrap-config.json written before the TenantIdWaitSeconds property existed deserialized the missing field to 0, which the runtime treated as an explicit opt-out and skipped the 600s TenantId wait entirely — exactly the wait Hybrid-Join devices need while their Entra registration lands. TenantIdWaitSeconds is now nullable: missing/null means 'not configured' and falls through to the agent default (600s); an explicit 0 remains the legacy fast-fail opt-out. The install merge carries null through instead of freezing the default into the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4ebd60fb1e |
feat: EnableGatherRuleDebugLog — per-tenant gather-rule debug trace (ConfigVersion 36)
Customers cannot tell why a gather rule produces nothing in the timeline (e.g. a logparser rule with emit_mode on_change that never emits). New per-tenant boolean EnableGatherRuleDebugLog (EnableImeMatchLog-style plumbing end to end): when enabled, the agent writes %ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log with one pipe-delimited line per evaluation decision across all previously silent "produced nothing" exits — disabled rules, interval without intervalSeconds, first-tick-after-one-full-interval, phase-scope skips incl. currentPhase=Unknown, per-(rule, phase) dedup, empty collector results (emitOnlyIfExists), on_change hash suppression (hash prefix + streak), guard blocks, logparser per-file outcomes (position skips, format=text hint when every line fails CMTrace parsing, maxLines and wildcard caps), and full exception stacks the agent log drops. - Backend: ConfigVersion 35 -> 36; TenantConfiguration/AgentConfigResponse field + TableConfigRepository Store+Map - Agent: GatherRuleDebugLog writer (10 MB cap, one .old generation, failures swallowed, optional console echo), GatherRuleContext.DebugLog (null-safe no-op), trace points in executor + collectors; never writes under _scopeLock (reasons captured inside, written after release); merger bool->path projection with --gather-debug-log CLI override tolerance; deliberately NOT in the offline-cache strip lists; --run-gather-rules always traces; zero-rules delivery leaves a one-line file instead of nothing - Side fix: GatherRuleExecutorHost now passes the logger into InformationalEventPost so gather-event emissions hit the DEBUG log - Web: tenant settings toggle (Settings -> Agent) + admin tenant-config report rows - Tests: merger on/off/override, writer format+rotation, executor debug lines, logparser debug lines (agent 2131, backend 3561, tsc green) - Docs: docs/agent/gather-rule-debug-log.md + index/log entries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b005c20598 |
feat(agent,backend,web): config-channel agent endpoint migration (MigrateToApiBaseUrl)
Re-home agents to a new API base URL through the agent/config response (backend move or per-tenant EU->US region move) - the lesson from the 2026-07-18 WEU->GWC cutover, where the compiled-in API hostname hard-cut every in-flight agent (no DNS indirection possible: mTLS breaks behind a proxy, Flex custom-domain TLS is broken). Backend (ConfigVersion 33): - AdminConfiguration.AgentMigrateApiBaseUrl (global) + AgentMigrateTenantOverridesJson (tenantId->URL; empty value pins a tenant against the global target; override wins) - GetAgentConfigFunction.ResolveMigrateTarget serves the validated target; AgentMigrateServed / AgentMigrateRejected warnings as delivery evidence - TableConfigRepository Store+Map; admin portal Global Settings round-trip Agent (kill-switch-mirrored hardening): - Shared AgentEndpointMigrationRules: https-only, *.azurewebsites.net on a label boundary, no path/port/userinfo - enforced on BOTH sides - Live fetch only: RemoteConfigService cache strips the field on write AND read; EndpointMigration.ResolveTarget re-checks the fetch outcome - AgentRuntimeConfig.Resolve returns before merge/integrity/bootstrap cleanup on a pending migration (no spurious IntegrityCheckFailed against a stale migration stub), Program.RunAgent rebuilds Phase 3+4 against the new URL - single hop, kill signal wins before and after the hop Tests: AgentEndpointMigrationTests (backend rules + resolution), EndpointMigrationTests + cache-hardening strip (agent). Full suites green (Functions 2992, Agent Core 1822, vitest 557, tsc silent). Doc: docs/agent/endpoint-migration.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
12e05f2ffb |
docs: move design docs out of docs/design; keep customer work-files git-ignored
Reorganize docs/ tree:
- docs/design/mcp-oauth-flow.md -> docs/mcp-oauth-flow.md (no customer data)
- enrollment-status-reclassification{,-plan}.md -> tasks/ (git-ignored work
files; they contained a customer domain that must not ship in the public repo)
- remove now-empty docs/design/ folder
Repoint the 22 source files whose comments referenced
docs/design/enrollment-status-reclassification.md to the new tasks/ path.
Comment/docstring text only; no functional code changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
824ef9a19c |
feat(agent): Part C — best-effort emergency-break report [net48, CI-verify]
Make the agent's silent 48h absolute session-age break visible: on break, before cleanup/exit, fire a SessionAgeEmergencyBreak AgentErrorReport over the resilient emergency channel; the backend (Part B) materializes it as an agent_emergency_break timeline event and the timeout classifier terminalizes the session instead of waiting out grace. Decoupled so the guard stays pure and its unit tests are untouched: - CheckSessionAgeEmergencyBreak gains optional Action onBreakFired = null, invoked once when the break is confirmed, before marker/cleanup, fully swallowed. - New EmergencyBreakReporter.TrySend builds a throwaway auth bundle (BackendClientFactory) and sends, bounded to a 5s wait. AgentBootstrap wires it; GetAgentVersion made internal. NOTE: agent targets net48 and was NOT compiled in this environment (only the .NET 8 SDK is available) — to be built/verified locally. The Shared AgentErrorType value is verified (compiled in the backend build). |
||
|
|
34582c0f0e |
feat(kill-switch): deliver kill on config channel + KillSignalDelivered ops event
Kill/Block rules were only served on the telemetry response, which cannot reach an agent whose upload loop is paused (indefinite block pins the drain until DateTime.MaxValue), whose spool is empty, or whose binary is kill-blind. Config is fetched at every process start (boot via Scheduled Task), making it the reliable control channel - no heartbeat added by design (48h emergency break + 6h max lifetime bound the remaining cases). Backend (effective on deploy): - KillSwitchEvaluator: shared device+version check for both channels; ingest wire response and check order unchanged (device first, short-circuits) - Every served Kill emits ops event KillSignalDelivered (Security/Warning), throttled 24h per tenant+serial+pattern; Blocks stay event-free - GET agent/config (incl. bootstrap wrapper) now returns DeviceBlocked/ DeviceKillSignal/UnblockAt alongside the full config; old binaries ignore the new fields - Web: KillSignalDelivered dual-registered in OPS_EVENT_TYPES Agent V2 (effective with next agent release): - CheckConfigKillSignal guard after the Phase-4 fetch: forced self-destruct (overrides SelfDestructOnComplete), enrollment-complete marker, session cleared, exit before mTLS/registration - Kill honoured ONLY from a live fetch: config disk cache strips the flags on write and read (UnrestrictedMode pattern), guard re-checks LastFetchOutcome Tests: 9 KillSwitchEvaluatorTests + 7 ProgramConfigKillSignalTests; Functions 2557/2557, Agent V2 1578/1578, web tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e0d4094a25 |
feat(agent-v2): capture mini-dumps + emit previous_crash_detected event
When the V2 agent process dies via AppDomain.UnhandledException or TaskScheduler.UnobservedTaskException, synchronously write a compact MiniDump (data-segs + handle + thread-info, ~5-20 MB) and a CrashRecord JSON sibling to %ProgramData%\AutopilotMonitor\Crashes\. Next agent start scans the directory, drains pending records into the timeline as previous_crash_detected events (Warning, ImmediateUpload), and applies retention (max 5 dumps, max 7 days). Dumps stay on disk so the existing customer-opt-in diagnostics-upload can collect them. Handlers register in Program.Main before SelfUpdater so any downstream crash is captured; AgentRuntimeHost re-registers once session/tenant are known so the CrashRecord carries those labels. 6 builder/retention tests; 1163/1163 V2.Core tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d51bde6017 |
refactor(release): parametric per-line agent build pipeline
Replaces V1/V2-hardcoded build + bootstrap with a single parametric
pipeline that scales to V3+ without rewrites. Full architecture in
.claude/plans/v2-cutover.md.
Stable namespace (Bootstrap-side, never renamed): generic Bootstrap
script, agent owns its --install defaults so it works for any line.
Versioned namespace (per-line, SelfUpdater-side): isolated *-v{N}.*
blobs; SelfUpdater never crosses lines or touches stable.
Backend GetAgentConfig dispatches per X-Agent-Version-Major to
AdminConfiguration.GetAgentLine(int) -> LatestAgentV{N}* fields.
Future V3 = add field set + switch arm. TableConfigRepository.Map
falls back to legacy unsuffixed columns for transparent migration.
V1 SelfUpdater now reads version-v1.json (would have been clobbered
by the V2 cutover otherwise). V2 agent owns its own 600s TenantId-wait
default so the Bootstrap calls --install plain.
Tests: 3289 across 6 suites, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d8e1eb9292 | Changed bootstrap script and V2 agent binary name (now without V2, preperation for V1 replacement) | ||
|
|
b86c294771 |
fix(agent): merge bootstrap-config.json on --install instead of clobbering
Three follow-ups from the TenantIdAwaiter PR review: * HIGH: A redeploy that passed only --tenant-id-wait (PS1's default invocation today) was overwriting bootstrap-config.json with BootstrapToken=null / TenantId=null, pushing devices to cert-auth before their MDM cert was usable. The install path now reads the existing file, merges only fields explicitly given on this invocation, and writes the merged result back. Field-given booleans drive the merge so that prior values survive an absent CLI flag. * MEDIUM: --tenant-id-wait 0 is now a real opt-out. Previously the persist block was gated on tenantIdWaitSeconds > 0, so an admin who flipped the PS1 default to 0 and redeployed kept the old non-zero wait persisted. The gate is now "any flag explicitly set" and the merge writes 0 over the prior value when 0 is what was given. * LOW: --tenant-id-wait <SEC> is now in --help under a new "TenantId resolution" section so the start-reliability switch is discoverable. The merge logic is extracted as Program.MergeBootstrapConfig — a pure function — and covered by 7 InstallModeMergeTests cases (wait-only preserve, explicit-zero override, tenant-only preserve, token-only preserve, first-install path, PS1 default invocation, legacy existing-config without the new field). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3016c15919 |
feat(agent): event-driven TenantId wait via RegistryWatcher
Bridges the OOBE / hybrid-AAD-join race window where the agent fires before AADTenantID / CloudDomainJoin\TenantInfo are written to the registry. Field evidence (GardnerMedia, 2026-04-30): the agent fired ~5 minutes before the AAD device cert landed; an event-driven wait of 10 minutes recovers the run instead of bailing and waiting for next reboot. Mechanism (V2 only — V1 keeps the fast-fail path): * New TenantIdAwaiter.WaitForTenantId(timeoutSec, logger, ct) sets up a CompositeRegistryChangeSignal over HKLM\SOFTWARE\Microsoft\Enrollments and HKLM\SYSTEM\...\CloudDomainJoin (Registry64 view, recursive, Name+LastSet+ThreadAgnostic filter), re-probes silently on every change, and returns the resolved TenantId or null on timeout. * 30 s belt-and-suspenders periodic re-probe protects against a missed RegNotifyChangeKeyValue notification. * 100 ms debounce after each signal so multi-value enrollment transactions land before the re-probe. Configuration: * Opt-in via --tenant-id-wait <sec>. Default 0 = no wait (legacy fast-fail). The PS1 bootstrap script owns the production default; the agent itself never assumes one. * CLI value persisted to bootstrap-config.json by --install so the Scheduled Task picks it up on each restart. * Install-AutopilotMonitor-v2.ps1 sets $tenantIdWaitSec = 600. Admins override per tenant by editing this single line and re-deploying. Testability: * IRegistryChangeSignal abstraction lets unit tests drive the loop with a fake signal and scripted probe — 8 deterministic cases cover timeout, signal hit, multi-signal, cancellation, periodic re-probe, whitespace/empty/null handling, zero-timeout short-circuit, and argument validation. * RegistryWatcherIntegrationTests exercise the real Win32 plumbing (RegNotifyChangeKeyValue → Changed event) against HKCU so no elevation is required and the test runs in standard CI. * Both classes opt out of xUnit parallelisation — the timing-sensitive Thread.Sleep handshakes are unreliable on a busy parallel pool. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ba0fb7ae0c |
feat(agent): V2 WhiteGlove Part 1 graceful-exit hardening
Three coordinated fixes so a WhiteGlove Part-1 reseal-reboot does not race the agent's termination path and leave the timeline missing agent_shutting_down / whiteglove_part1_complete events plus a stale previousExit=reboot_kill on the next run. - Real spool-empty drain: EnrollmentTerminationHandler.DrainSpool polls PendingItemCount + IngressPendingSignalCount instead of blind Task.Delay, so it returns the moment the backend actually ack'd the lifecycle events (or hits the bounded fallback timeout). - Early clean-exit.marker write: marker is written before _signalShutdown returns control, in addition to the existing AppDomain.ProcessExit fallback. Wins the race against admin-triggered shutdown.exe. - Off-worker termination dispatch: Terminated event from OnDecisionTerminalStage now goes through Task.Run so the SignalIngress worker is freed to process events the handler itself just posted. Adds PendingSignalCount with race-free posted-not-yet-completed semantics (Interlocked.Increment in Post BEFORE channel.Add, decrement in ProcessItem finally + add-throw paths). Two-phase drain (ingress queue -> spool) replaces the previous spool-only wait. - Fast-path inline classifier on WhiteGloveShellCoreSuccess: emits whiteglove_complete in a single reduce step with the full DecisionAuditTrailBuilder TypedPayload (decisionSource, trigger, signalsSeen, classifier verdict, classifierInputs, scenario), preserving byte-stable forensics across fast and slow paths. Tests: - DecisionCore: fast-path audit-trail completeness, fast-path single-step transition, slow-path fallback when AccountSetup activity excludes WG, scenario-replay run/skip counts. - V2 Core: Termination two-phase drain (immediate / poll-until-zero / timeout-fallback / blind-delay-fallback / accessor-throw safe), early clean-exit-marker ordering vs signalShutdown, off-worker Terminated-dispatch verified by Thread.Name + handler-posted-signal drain, SignalIngress PendingSignalCount idle/in-flight/post-stop and deterministic race-leak regression via internal test seam. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0e08af8f58 |
refactor(v2): extract AgentRuntimeHost + TerminationPipeline (PR7)
Final PR of the V2 Runtime-Host slice — Phase 7+8 of RunAgent (the EnrollmentOrchestrator-using block, the shutdown synchronisation primitives, the onIngressReady wiring, the post-startup decision-signal posts, the analyzer + probes startup, the shutdown.Wait + finally) moves into Runtime/AgentRuntimeHost.cs and Runtime/TerminationPipeline.cs. AgentRuntimeHost.Run takes the bundles produced by PR2-PR5 (BootstrapResult, BackendAuthBundle, RuntimeConfigBundle, TelemetryClientResult, SessionRegistrationOutcomeResult) plus the pre-computed paths and returns the agent's process exit code. The cadence-clamp helpers (Min/Max/Default Upload* constants + ClampUploadIntervalSeconds + ClampUploadBatchSize) move with it — they are an internal concern of the runtime host, not a Program-level configuration helper. TerminationPipeline.Run owns the finally-block contract: unsubscribe event handlers (Ctrl+C / ProcessExit / Terminated / ThresholdExceeded) to prevent re-entrant shutdown, run orchestrator.Stop, dispose the mTLS HttpClient and the BackendApiClient, then signal shutdownComplete LAST so any ingest-dispatcher thread parked in the ServerActionDispatcher terminate-callback only resumes once a fully-cleaned-up agent is visible. The Plan §6.2 synchronous-shutdown contract is preserved verbatim. RunAgent collapses to a linear ~75-line composer: Phase 1+2 → AgentBootstrap.Run, Phase 3 → BackendClientFactory.BuildAuthClients, Phase 4 → AgentRuntimeConfig.Resolve, Phase 5 → BackendClientFactory.BuildTelemetryClients, Phase 6 → BackendSessionRegistration.Register, Phase 7+8 → AgentRuntimeHost.Run. Every dead alias from the previous PRs is cleared; each phase is a single named call with a clear exit-code result. Program.cs: 1607 LOC (pre-PR1) → 441 LOC (after PR7). RunAgent body: ~770 LOC → ~75 LOC, fully linear. No behavioural change — every construction order, exit code, side effect and synchronisation contract is preserved exactly. Build clean (0 W / 0 E). Full V2.Core suite 766/767 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fb71fb1c9c |
refactor(v2): extract LifecycleEmitters + ServerControlPlane (PR6)
PR6 of the V2 Runtime-Host slice — the heaviest closure-capture block of RunAgent moves into two new factory classes plus the ServerActionSynthesis partial is folded into the new home. LifecycleEmitters (Runtime/LifecycleEmitters.cs): - EmitAgentStarted, EmitVersionCheckIfAny, EmitUnrestrictedModeAuditIfChanged - PostSessionStarted, PostAdminPreemption, PostSystemRebootObserved (decision-signal posters that previously lived as Program statics) - CreateMaxLifetimeEmitter / CreateAuthThresholdHandler — factory methods returning EventHandlers that close over a Func<Post> accessor so the watchdogs can be subscribed BEFORE the live InformationalEventPost is constructed in the orchestrator's onIngressReady hook (the trick that made the inline lambdas hard to read). ServerControlPlane (Runtime/ServerControlPlane.cs): - BuildDispatcher — constructs the live ServerActionDispatcher with its rotate_config / upload_diagnostics / terminate_session callbacks and the 60-second synchronous-shutdown wait - Wire — the EnrollmentOrchestrator.Transport.ServerResponseReceived handler that translates DeviceKillSignal / AdminAction / Actions[] into ServerActions - HandleAdminAction, MapAdminOutcome — moved from Program.cs and the Program.ServerActionSynthesis.cs partial (which is deleted) The terminate_session callback's synchronous-shutdown wait, the dispatcher's reentry short-circuit (Codex Finding 1) and the admin-outcome mapping (Codex Finding 2) are preserved verbatim with identical comments. RunAgent's onIngressReady block shrinks from ~135 lines to ~12 lines of factory invocations. Existing tests (AdminActionOutcomeMappingTests, HandleAdminActionTests, PostSessionStartedSignalTests) migrated to the new namespace (global::AutopilotMonitor.Agent.V2.Runtime.* instead of Program.*). New tests (LifecycleEmittersTests, 9 cases): max-lifetime suppress on null post, no-emit on non-max-lifetime reason, emit-on-max-lifetime, auth-threshold signalShutdown side-effect, agent_shutdown event emission, signalShutdown still fires when post is null, unrestricted-mode no-op gates (null/unchanged/changed). Build clean (0 W / 0 E). Full V2.Core suite 766/767 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
79622515f0 |
refactor(v2): extract BackendSessionRegistration from Program.RunAgent (PR5)
Phase 6 (POST /api/agent/register-session with retry, outcome-based exit-code mapping, on-failure mTLS+API-client disposal) moves into Runtime/BackendSessionRegistration.cs. SessionRegistrationOutcomeResult is the discriminated outcome — Continue with the live SessionRegistrationResult (consumed downstream by PostSessionStartedSignal, PostAdminPreemptionSignal, WireTelemetryServerResponse, HandleAdminAction) or Exit with the V1-parity codes (6 = AuthFailed, 7 = anything else; the diag skill keys on the distinction in Scheduled-Task history). Disposal of mtlsHttpClient + auth.BackendApiClient on registration failure is preserved inside the factory, matching the original inline behaviour. RunAgent's Phase 6 block shrinks from ~20 lines to 8 lines (factory call + result destructuring). Tests: 4 new BackendSessionRegistrationTests (Exit-6, Exit-7, Continue, null-arg validation). Existing SessionRegistrationHelperTests cover the underlying retry/outcome behaviour. Full V2.Core suite 757/758 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6b804d38d9 |
refactor(v2): extract AgentRuntimeConfig from Program.RunAgent (PR4)
Phase 4 (RemoteConfigService.FetchConfigAsync, RemoteConfigMerger.Merge, AuthFailureTracker ceiling refresh, logger SetLogLevel, SelfUpdater hash propagation, BinaryIntegrityVerifier check + IntegrityCheckFailed emergency-report dispatch, BootstrapConfigCleanup H-2 mitigation) moves into Runtime/AgentRuntimeConfig.cs. RuntimeConfigBundle exposes the live RemoteConfigService (still needed later by the rotate_config ServerAction callback), the fetched AgentConfigResponse and the RemoteConfigMergeResult diff (consumed by the EmitUnrestrictedModeAuditIfChanged lifecycle hook). VerifyBinaryIntegrity and TryCleanupBootstrapConfig stay private to the factory — they are pure side-effect helpers with no return value needed downstream. The runtime-self-update trigger closure no longer captures Program.GetAgentVersion via implicit access; it receives the agent version as a parameter instead. RunAgent's Phase 4 block shrinks from ~70 lines to 7 lines (factory call + bundle destructuring). No behavioural change: ordering of side effects (Merge before UpdateLimits before SetLogLevel before SelfUpdater hash before VerifyIntegrity before BootstrapCleanup) is preserved exactly. Tests: 4 new AgentRuntimeConfigTests (RuntimeConfigBundle property mapping + 3 null-arg validation cases). Full V2.Core suite 753/754 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1785854ac7 |
refactor(v2): extract BackendClientFactory from Program.RunAgent (PR3)
Phase 3 (BackendApiClient + DistressReporter + EmergencyReporter + AuthFailureTracker + AuthCertificateMissing distress emit) and Phase 5 (mTLS HttpClient + BackendTelemetryUploader) move into Runtime/BackendClientFactory.cs. BackendAuthBundle holds the four clients plus the cached manufacturer/model/serial from HardwareInfo so RunAgent + Phase 5 read the same hardware identity. TelemetryClientResult is the discriminated outcome for Phase 5 — Continue with mTLS+uploader, or Exit with the V1-parity codes (4 = mTLS construction failure, 5 = uploader construction failure). Phase 4 (RemoteConfig fetch + merge + binary-integrity + bootstrap- config cleanup) stays inline in RunAgent for PR4. RunAgent reads the Phase 3 clients via local aliases so Phase 4 wiring is unchanged. RunAgent's Phase 3+5 blocks shrink from ~80 lines to ~12 lines of factory invocations + result destructuring. No behavioural change: constructor argument lists, distress-emit guard, exit-code mapping and the disposal-on-failure semantics are preserved. Tests: 4 new BackendClientFactoryTests (auth-bundle smoke, no-cert-lookup-on-bootstrap-auth, TelemetryClientResult.Exit, TelemetryClientResult.Continue). V2.Core 747/750 without the 3 known EnrollmentOrchestrator threading flakes (pass solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9b9ad61315 |
refactor(v2): extract AgentBootstrap from Program.RunAgent (PR2)
Phase 1+2 of RunAgent (previous-exit classification, persisted-config load, TenantId resolution, AgentConfiguration build, SessionId persistence, completion-marker / emergency-break guards, optional --await-enrollment cert wait) move into Runtime/AgentBootstrap.cs. The static helpers (DetectPreviousExit, TryReadBootstrapConfig, TryReadAwaitEnrollmentConfig, DeleteAwaitEnrollmentConfig, CheckEnrollmentCompleteMarker, CheckSessionAgeEmergencyBreak, BuildAgentConfiguration) stay on Program (Program.Guards.cs) and are called via internal access from the new Runtime namespace. BootstrapResult is the discriminated outcome — ShouldExit + ExitCode for the V1-parity early-return paths (0 = guard handled, 2 = no TenantId, 3 = await timeout), or a Continue payload carrying AgentConfiguration, SessionIdPersistence, PreviousExitSummary, IsWhiteGloveResume and the cleanup-service factory. RunAgent's first ~95 lines collapse to a single AgentBootstrap.Run call + result destructuring. No behavioural change: every helper invocation, exit code and side effect (sessionPersistence.Delete on --new-session, IsWhiteGloveResume snapshot before GetOrCreate, DeleteAwaitEnrollmentConfig on cert success) is preserved. Tests: 4 new AgentBootstrapTests (marker-handled exit, age-emergency exit, continue-with-bootstrap-config, first_run classification); existing 18 ProgramGuardsTests still pass; full V2.Core suite 745/746 (1 known RecoveryPathTests threading flake). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a775f3288b |
fix(v2): apply remote telemetry cadence + spool perf/observability
P1 — UploadIntervalSeconds and MaxBatchSize were merged into agentConfig by RemoteConfigMerger but never reached the orchestrator: drainInterval fell back to a hardcoded 30s and TelemetryUploadOrchestrator to its default batchSize=100. Pass both from agentConfig at construction with clamped bounds (5..300s, 1..500 items) and log the applied values. P2 step 1 — TelemetrySpool.Enqueue used FileOptions.WriteThrough + Flush(true) per item. Make fsync conditional on RequiresImmediateFlush so periodic snapshot items go through the OS cache only; backend dedupes via (PartitionKey, RowKey), so the worst-case loss on crash is a few non-lifecycle items. P2 step 2 — Replace File.ReadAllLines on every Peek with an in-memory LinkedList tail cache populated once in the ctor and kept in sync by Enqueue/MarkUploaded. Disk JSONL stays the source of truth for crash recovery and diagnostics ZIP forensics; the cache makes drain O(batchSize) instead of O(N). Telemetry — surface spool stats (pendingItemCount, peakPendingItemCount, fileSizeBytes, totalEnqueuedCount) in agent_metrics_snapshot, plumbed via DefaultComponentFactory.SetTelemetrySpool. Emit a one-shot spool_pressure_detected event with ImmediateUpload=true when pending crosses 2000 items or file grows past 5 MB, so upload-stall conditions become queryable on the backend without waiting for the next snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1d42332c7b |
fix(v2): count BackendTelemetryUploader requests in NetworkMetrics
Insert a NetworkMetricsRecordingHandler at the head of the mTLS HttpClient pipeline so every outbound request through MtlsHttpClientFactory.Create (in particular BackendTelemetryUploader's POST /api/agent/telemetry, which dominates per-session traffic) is recorded on the shared NetworkMetrics instance. Previously only the legacy BackendApiClient calls (config + gather-rules + emergency) called RecordRequest directly, so agent_metrics_snapshot.net_total_requests undercounted by ~25-30x (observed: ~2/session in V2 vs ~58/session in V1). The handler is wired by passing backendApiClient.NetworkMetrics — the same instance later read by AgentSelfMetricsCollector — into the factory. No double-count: BackendApiClient builds its own HttpClient and is unaffected by the mTLS pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fdc0e3e8ce |
fix(v2): normalize timeline event Source values + stop periodic collectors before diag ZIP
Source attribution review on session df7abbc2 surfaced four inconsistencies in the Source field on the Events table — assembly leakage, V1-parity carry-overs, and a CamelCase / snake_case mix on the same emitter. Normalize so every Source is a CamelCase class-name or the stable lifecycle label "Agent": - Program.cs `agent_started` — "AutopilotMonitor.Agent.V2" -> "Agent" (V1 parity, drops assembly leak) - ImeLogTrackerAdapter `script_completed` — "IME" -> "ImeLogTracker" (consistent with the seven sibling events emitted from the same adapter) - StartupEnvironmentProbes (5 sites: device_location, agent_trace, timezone_auto_set, two ntp_time_check shapes) — "Network" -> "StartupEnvironmentProbes" (class-name consistency; eventType already encodes the sub-probe) - EventTimelineEmitter default — "decision_engine" -> "DecisionEngine" (CamelCase, matches the override path used by phase_transition; eliminates the per-event mix on the wire) Tests adjusted: ImeLogTrackerAdapterTests, StartupEnvironmentProbesTests, EventTimelineEmitterTests, InformationalEventPostTests. EnrollmentEvent.Source XML doc updated to reflect the new convention. Same session also showed `performance_snapshot` (seq 220) firing AFTER `diagnostics_collecting` (seq 219), because periodic collectors keep ticking through the termination handler. Fix: - New EnrollmentOrchestrator.StopCollectorHosts() — idempotent; the existing Stop() now delegates to it so we never double-stop. - EnrollmentTerminationHandler gains an optional `stopPeripheralCollectors` callback and invokes it best-effort right before RunUploadDiagnosticsWithEvents (after DelayLateEventGrace). The WhiteGlove Part 1 path returns earlier and is unaffected — the agent stays alive for Part 2. - Program.cs wires `() => orchestrator.StopCollectorHosts()` so all collector hosts (Performance, AgentSelfMetrics, DeliveryOptimization, ESP/Hello, IME log, …) are stopped before the diag ZIP is built. Late `performance_snapshot` / `agent_metrics_snapshot` no longer slip in after the `diagnostics_collecting` event. Tests: V2.Core 732/732, DecisionCore 232/232, Backend 736/736. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
07312ee2a3 |
fix(v2): 882fef64 PR3 — kernel-path observability (A2 + A3 + D1-D5)
Session 882fef64's 12-min agent log carried only `EnrollmentOrchestrator: started.`
and `decision terminal stage reached` between the first event and termination — no
stage-transition trail, no signal cadence, no effect failures. PR3 wires logging
into the kernel hot path so every Decision-Stage transition, every signal accept/
reject, every effect retry/critical-failure, and every IME app state change leaves
a trace at the right severity.
A2 — noise reduction:
- TelemetrySpool `enqueued itemId=...` DEBUG -> VERBOSE (600+ lines/session before)
- IME `impersonation` lines deduplicated by user with 60s rollup
A3 — strengthened existing weak lines:
- `Command line:` shows `(no args)` instead of empty
- `Process exit code: 0` -> `Process completed: <name> -> exit=N, durationMs=T`
- IME session change now carries sessionId+user
- HelloTracker 358/360 suppression includes eventTime+provider
- EnrollmentOrchestrator started/stopping carry sessionId/stage/hosts/signals
D1-D5 — observability into the silent kernel components:
- DecisionStepProcessor: per-step DEBUG (stage transitions + effects) + VERBOSE for no-ops
- SignalIngress: 5 strategic log lines (drop, append-fail, worker-fault, stop summary, processed)
- EffectRunner: 5 strategic log lines (retry, exhausted, critical, anti-loop, classifier throw)
- ImeLogTrackerAdapter: 7 emit-site log lines covering all 7 emission paths
- InformationalEventPost: per-emit DEBUG with eventType/source/immediate/severity/dataKeys
All log statements are null-safe (_logger?.Debug); legacy unit-test rigs without a
logger keep working. New ctor overloads on InformationalEventPost / SignalIngress /
EffectRunner / ImeLogTrackerAdapter are backwards-compatible (logger param defaults
null).
Side fix: BinaryIntegrityVerifierTests pinned to [Collection("SerialThreading")].
Process-global `_runtimeSelfUpdateFired` static raced with parallel mismatch tests;
PR3's logging shifted thread timing enough to surface the race consistently.
Tests: V2.Core 727, DecisionCore 231, Functions 736, vitest 179, solution build 0/0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b243706629 |
fix(v2): 882fef64 PR1 — Company Portal UI, diagnostics ZIP State/Spool, agent_shutting_down V1 parity
- DownloadProgress.tsx: exempt app_download_started from <1024B drop so WinGet/Store apps (Company Portal etc.) appear in the UI; filter logic extracted to pure helpers in lib/downloadProgressFilters.ts for vitest coverage. - DiagnosticsPackageService: ZIP now also packages AgentState/ (snapshot, journal, signal-log, ime-tracker-state, enrollment-complete.marker, final-status, whiteglove-backfill-state + .quarantine), AgentSpool/ (pending uploads + cursor), and AgentMarkers/ (top-level whiteglove.complete + clean-exit). BuildArchiveBytes extracted as internal seam so xUnit asserts ZIP contents without HTTP. - EnrollmentTerminationHandler: agent_shutting_down emission moved to top of Handle() so it fires unconditionally — including SelfDestruct=false dev VMs and WhiteGlove Part-1 exits. Data carries reason/outcome/stage/uptimeMinutes/agentVersion. Tests: V2.Core 711 (+12), DecisionCore 220, Functions 736, vitest 179 (+11), solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8b7a5f509e |
fix(v2): debrief 7dd4e593 followups + final-status schema 2 + V2 dialog renderer
F3 — SummaryDialog launch crash: switch temp dir from C:\WINDOWS\SystemTemp\ (unreadable for standard users when agent runs as SYSTEM) to flat %ProgramData%\AutopilotMonitor-Summary\ + wipe-on-launch (V1 parity), and grant BUILTIN\Users delete rights so the dialog's --cleanup self-removal works. F4 — Backend phase classification: classify `phase_transition` events (emitted by V2 DecisionEngine for AppsDevice/AppsUser/FinalizingSetup) as phase updates so CurrentPhase advances during App phases. Also force CurrentPhase=Complete on Status=Succeeded to materialise the terminal phase regardless of the carrier event's Phase value (V2 emits enrollment_ complete with Phase=Unknown per feedback_phase_strategy). F5 — App tracking summary covers all phases: ImeLogTracker now snapshots AppPackageState references (not serialized dicts) before _packageStates. Clear() at the AccountSetup transition, and exposes a deduped union GetAllKnownPackageStates() for the termination summary path. Without this, DeviceSetup apps were silently dropped from app_tracking_summary and the SummaryDialog after the phase transition. Schema 2 / V2 dialog — final-status.json now carries schemaVersion=2 plus failureReason, signalTimestamps, and per-app errorPatternId/errorDetail/ errorCode. SummaryDialog branches on schemaVersion: V1 path is the unchanged legacy renderer (deletable when V1 retires), V2 path renders 6 outcome states (Success / PreProvisioningComplete / TimedOut / Failure / Unknown / WhiteGlovePart2), shows a coloured banner with the agent- supplied failure reason, and per-app rows with duration on the right plus the IME-pattern error detail under the app name when an install failed. V1 agent + V1 dialog rendering remain bit-for-bit identical (no schema field → V1 render path). V1 Agent.Core.Tests 507/507, DecisionCore 220/220. Tests: V2.Core 702/702 (+12 schema-2 builder, signal-timestamps, failure- reason, per-app errors, phase-snapshot dedup, launcher path). Backend 736/736. New SummaryDialog.Tests 45/45 (OutcomeMapper + V1/V2 JSON roundtrip + FormatDuration). Build clean (0 warnings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fef5834ad0 |
v2 single-rail 13: remove dead Registry-guard in CheckEnrollmentCompleteMarker
The registry-based ghost-restart branch in CheckEnrollmentCompleteMarker (HKLM\SOFTWARE\AutopilotMonitor\Deployed present + !SessionExists) has never fired in production and was structurally unreachable: - session.id is created via SessionIdPersistence.GetOrCreate BEFORE the guard runs (both in V1 LoadConfiguration:131 and V2 Program.cs:216), so the !SessionExists() retry-cleanup arm is never reachable at runtime. - Even if the arm were reachable, the Deployed regkey cannot distinguish "fresh install" from "ghost restart": it is written by --install and intentionally preserved by CleanupService as a bootstrap-script reentry lock (read by scripts/Bootstrap/Install-AutopilotMonitor-v2.ps1 Guard 1 to prevent re-install on production devices). Both scenarios therefore present as "Deployed present + session.id absent". Remove the registry-guard block and its dead retry-cleanup path. Keep the file-based enrollment-complete.marker path intact — it is written by CheckSessionAgeEmergencyBreak before its own retry-cleanup attempt and serves the "cleanup-disabled emergency break" exit-cleanly case. Also drop the now-obsolete "Create SessionId BEFORE guards" justification comment in Program.cs and the matching boot-sequence doc wording — neither remaining guard (file-based marker, session-age watchdog) keys on session.id absence for ghost detection. CheckEnrollmentCompleteMarker loses the now-unused dataDirectory parameter; callsites updated. Tests: ProgramGuardsTests.cs keeps marker-absent / marker-without- selfdestruct / marker-with-selfdestruct-retry coverage; the first-run- after-install regression test is renamed to match its actual scope (returns_false_when_marker_file_is_absent). 630/630 V2.Core tests green. No behavior change — removed path was unreachable. TryWriteDeploymentMarker in Program.InstallMode.cs stays (writes the bootstrap-reentry-lock); CleanupService continues to intentionally leave the regkey in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8fe75a0f4e |
v2 single-rail 12: first-session-fixes PR 4 — data completeness + operability (Fixes 4+5)
Closes the "final-status.json shows only 3 apps", "every app ~30 s" and "agent log silent during uploads" gaps observed in session 30410cd7. Fix 4c — Per-app install-lifecycle timing. New AppInstallTiming model (StartedAtUtc / CompletedAtUtc / DurationSeconds). ImeLogTrackerAdapter tracks timing in a set-once dictionary (first Downloading/Installing/ InProgress stamps StartedAt; first terminal stamps CompletedAt) and exposes it via a new public AppTimings snapshot. app_install_* DataJson carries startedAt / completedAt / durationSeconds when known; omitted otherwise so the UI can distinguish "not tracked" from "stamp=epoch". Fix 4a — FinalStatusBuilder uses timings. Optional appTimings param on Build(); FinalStatusPackageInfo gets StartedAt / CompletedAt / DurationSeconds with NullValueHandling.Ignore. Existing per-targeted phase bucketing preserved — that was correct. Fix 4b — app_tracking_summary terminal event. EnrollmentTerminationHandler. EmitAppTrackingSummary runs once at termination before diagnostics upload with aggregate counts, per-phase breakdown, and per-app records (appId/name/phase/finalState/optional timing). ImmediateUpload=true, Phase=Unknown (feedback_phase_strategy). Skipped on WhiteGlove Part 1 (apps haven't installed yet). Fix 5 — Upload-cadence logging. BackendTelemetryUploader + TelemetrySpool gained optional AgentLogger params. Uploader logs INFO flushing/ingest OK, WARN ingest TRANSIENT (5xx/timeout/network), ERROR ingest PERMANENT (4xx), with items + durationMs. Spool logs DEBUG per-enqueue with kind + immediate flag + pending count. Empty-batch path stays silent (short-circuit). Plumbing: ImeLogHost.AppTimings → DefaultComponentFactory.ImeAppTimings → Program.cs wires appTimingsAccessor into EnrollmentTerminationHandler; logger flows into uploader (Program.cs) and spool (EnrollmentOrchestrator). All new ctor params are nullable-default → zero breakage for existing call sites and tests. Tests: +18 new (7 adapter timing, 2 builder timing, 5 handler summary, 4 uploader logging). DecisionCore 139, Agent.V2.Core 630, Functions 727, Web vitest 168 — all green. Web typecheck clean. Solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b64f98e63 |
v2 single-rail 9: soft admin-action semantics + shutdown-race fixes
Addresses two tasks/todo.md entries from 2026-04-23:
- Soft Admin-Action Semantik + SignalIngress-Race-Fix
- Backend AdminAction-Trigger fix
Backend — authoritative AdminMarkedAction field:
New Session.AdminMarkedAction property (null | "Succeeded" | "Failed")
replaces the fragile heuristic "Status is terminal AND current event is not
a completion marker", which fired falsely on every post-completion event
the agent sent (agent_shutting_down, diagnostics_uploaded,
enrollment_summary_shown). The backend's AdminAction response to /register
and /ingest now reads the explicit field. ISessionRepository +
TableSessionRepository + TableStorageService.Sessions +
Ingest{Events,Telemetry}Function + MarkSession{Succeeded,Failed}Function +
RegisterSessionFunction + EventIngestProcessor all updated to set / clear /
read the new field coherently.
Agent — Program.cs WireTelemetryServerResponse refactor:
Routes TelemetryUploadOrchestrator.ServerResponseReceived through three
distinct semantic paths instead of one terminate_session synthesis:
1. DeviceKillSignal -> terminate_session with forceSelfDestruct=true
(hard admin kill — overrides SelfDestructOnComplete).
2. AdminAction=Succeeded -> single admin_marked_session timeline entry
(informational; agent continues own path).
3. AdminAction=Failed -> EnrollmentTerminationHandler.Handle(outcome=Failed)
(soft shutdown; same analyzers/diagnostics/summary
as a locally-detected failure).
Plus a shutdown-race guard: if shutdownComplete is already set, short-circuit
instead of crashing deep in the dispatcher with an InvalidOperationException
from a disposed SignalIngress. Backed by new HandleAdminActionTests.
Agent — ServerActionDispatcher ingress-disposed race:
Added InvalidOperationException swallow around InformationalEventPost.Emit
for the narrow window between SignalIngress.Stop and
TelemetryUploadOrchestrator.DrainAllAsync completion. Dropped events are
acceptable during teardown; the alternative was a nested Emit failure that
corrupted the outer termination sequence. Backed by new
ServerActionDispatcherTests case.
Tests all green: DecisionCore 133, Agent.V2.Core 586, Functions 727.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
92d300d4c2 |
v2 single-rail 7: codex-review feedback — 4 findings fixed
Addresses all 4 findings from the Codex review on PR #48. 564 V2.Core + 111 DecisionCore tests green (up from 559 + 109; 7 new regression tests). Finding 1 (P1) — terminate_session drain deadlock TelemetryUploadOrchestrator raised ServerResponseReceived synchronously while holding _drainGuard. The terminate callback then invoked orchestrator.Stop() on the main thread, which tried a terminal DrainAllAsync and blocked on the same guard — timing out before the final agent_shutting_down event could be uploaded. Fix: buffer signal-bearing UploadResults during drain, release the guard, then raise events. Also added a defensive re-entry short-circuit in onTerminateRequested (skips if shutdown.IsSet) for the pathological case of a nested drain producing a different ActionId. Files: TelemetryUploadOrchestrator.cs, Program.cs Test: Handler_may_invoke_reentrant_drain_without_deadlock Finding 2 (P1) — structured Event.Data was degenerating to type-name strings InformationalEventPost.StringifyData fell through to kv.Value.ToString() for nested containers, so e.g. network_adapters.adapters became the literal string "System.Collections.Generic.List\`1[...]" on the wire. Fix (redesigned after review): typed-sidecar through the bus, mirroring the existing DecisionEffect.ClassifierSnapshot pattern. DecisionSignal.TypedPayload (object?) ← new DecisionEffect.TypedPayload (object?) ← new ISignalIngressSink.Post(..., typedPayload) EventTimelineEmitter.Emit(..., typedPayload) uses it as Data verbatim Live path does ZERO serialization for structured event data — the Dictionary<string,object> flows object-identity-preserving from collector to TelemetryEventEmitter, where JsonConvert.SerializeObject runs once at the wire boundary. Persistence writes TypedPayload to JSONL once at the disk boundary (SignalSerializer); replay restores it as JObject/JArray tokens that re-serialize identically. Files: DecisionSignal.cs, DecisionEffect.cs, DecisionEngine.Shared.cs, SignalSerializer.cs, ISignalIngressSink.cs, SignalIngress.cs, EnrollmentOrchestrator.cs (LazyIngressSinkRelay), IEventTimelineEmitter.cs, EventTimelineEmitter.cs, EffectRunner.cs, InformationalEventPost.cs, + FakeSignalIngressSink, FakeEventTimelineEmitter test doubles Tests: TypedPayload_dictionary_becomes_EnrollmentEvent_Data_verbatim_on_wire, TypedPayload_missing_falls_back_to_string_parameter_reconstruction, Emit_EnrollmentEvent_overload_forwards_Data_as_typed_payload_by_reference, Emit_EnrollmentEvent_overload_passes_DateTime_as_live_object_through_typed_payload, SignalSerializer_roundtrip_preserves_TypedPayload_structure, SignalSerializer_roundtrip_null_TypedPayload_stays_null Finding 3 (P2) — ImmediateUpload=false silently flipped to true Helper wrote the ImmediateUpload payload key only when true; the emitter defaulted "missing key → true". Net effect: a caller passing immediateUpload:false (e.g. performance_snapshot, agent_metrics_snapshot) had their intent silently overridden; every snapshot was flushed immediately. Fix: helper now writes the key both-ways ("true" or "false"). Emitter default stays true because reducer-synthesised terminal events (whiteglove_complete, enrollment_complete, enrollment_failed) construct effect parameters with just eventType and rely on that default for immediate flush — documented inline. Files: InformationalEventPost.cs, EventTimelineEmitter.cs (doc-only) Tests: ImmediateUpload_false_writes_explicit_false_key_to_payload, Emit_optional_payload_keys_are_set_only_when_caller_provides_them (updated) Finding 4 (P2) — PeriodicCollectorLifecycleHost saw only its own posts IdleActivityObservingIngressSink wrapped the ingress per-host, so _lastRealEventTimeUtc was only updated by signals routed through this host's own _post. Every other component (Program.cs lifecycle, IME, ESP, DeviceInfo, Analyzer, Gather, StallProbe, Network, AAD, Desktop, etc.) constructs its own InformationalEventPost against the raw shared ingress and bypassed the wrapper — and periodic events posted by the host itself were correctly filtered out. Result: the idle clock never advanced past session start; collectors stopped after idle timeout regardless of live enrollment activity. Fix: moved observation into SignalIngress itself as an event SignalPosted, fired after every successful enqueue (fast path + slow path). PeriodicCollectorLifecycleHost subscribes via the concrete SignalIngress type at Start, unsubscribes at Stop. The observer now sees ALL signal kinds (ESP, IME, Classifier, DeadlineFired, etc.), not just InformationalEvents. Deleted the obsolete wrapper class. Files: SignalIngress.cs, DefaultComponentFactory.cs Tests: SignalPosted_fires_for_every_successful_post_regardless_of_kind, SignalPosted_handler_exception_does_not_break_ingress, SignalPosted_is_raised_even_when_channel_took_slow_path Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
818fa53728 |
v2 single-rail 5.7: migrate Analyzer trio + AgentAnalyzerManager to Signal-Post (B24-B26)
Full migration — the 3 analyzer classes and their manager all move to
InformationalEventPost, no Action<EnrollmentEvent>-bypass remains in the
analyzer surface (no dual-use concerns; no standalone CLI mode touches
these types).
Analyzers (1 Emit-site each):
* LocalAdminAnalyzer (B24): local_admin_analysis.
* SoftwareInventoryAnalyzer (B25): software_inventory_analysis
(startup baseline + shutdown delta; WG-part discriminator unchanged).
* IntegrityBypassAnalyzer (B26): integrity_bypass_analysis.
Manager:
* AgentAnalyzerManager stores InformationalEventPost _post and passes
it to all 3 analyzer constructors in Initialize().
Program.cs wiring:
* AgentAnalyzerManager construction deferred into orchestrator.Start's
onIngressReady hook so lifecyclePost is non-null at construction time.
The outer-scope `AgentAnalyzerManager analyzerManager = null;`
declaration is captured by both the later RunStartup() call (after
Start returns) and the terminationHandler constructor (inside the
same hook).
Tests:
* AgentAnalyzerManagerTests helper NewPost() returns a throwaway
InformationalEventPost(new FakeSignalIngressSink(), VirtualClock)
for the 7 ctor sites including the null-arg test.
* 538 V2.Core + 108 DecisionCore = 646 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0bc04c3a9a |
v2 single-rail 5.3c: Program.cs terminate callback waits for shutdown completion
Plan §6.2 synchronous-shutdown — the ServerActionDispatcher terminate callback now blocks until the main-thread cleanup sequence (orchestrator stop, client disposal) has fully run, instead of returning Task.CompletedTask immediately after terminationHandler.Handle. Mechanism: a second ManualResetEventSlim `shutdownComplete` is Set() at the very end of the finally block (after orchestrator.Stop + HTTP-client disposal). The terminate callback waits on it for up to 60s after terminationHandler.Handle returns. If the bound expires we log and return rather than deadlock the ingest-dispatcher thread forever. Effect: no fresh ingest call can happen between terminationHandler.Handle returning and the process actually exiting, which would otherwise give the backend another window to re-deliver terminate_session. Combined with the PR #3a dedup this closes the 77×-storm loop from the root. Non-server-action termination paths (decision-engine enrollment_complete, max-lifetime, auth-failure, Ctrl+C) are unaffected — they already drive shutdown from the main thread itself. V2.Core: 538, DecisionCore: 108, all green. |
||
|
|
491c749688 |
v2 single-rail 5.3b: migrate EnrollmentTerminationHandler + agent_shutting_down event
Plan §5.3 + §6.2 — EnrollmentTerminationHandler now emits the six peripheral lifecycle events (enrollment_summary_shown, diagnostics_collecting/uploaded/upload_failed, reboot_triggered, whiteglove_part1_complete) through InformationalEventPost instead of the legacy Action<EnrollmentEvent> bypass. New acknowledgement: `agent_shutting_down` fires with ImmediateUpload=true just before CleanupService.ExecuteSelfDestruct. This gives the backend a definitive "agent accepted termination" signal so it can stop re-queuing terminate_session even when the cleanup script wins the race against the final telemetry drain. Program.cs: EnrollmentTerminationHandler construction moved into orchestrator.Start's onIngressReady hook (alongside ServerActionDispatcher) so the live lifecyclePost backs it. The Terminated-event subscription uses a null-checking dispatch wrapper so the registration can sit before Start returns without referencing the not-yet-constructed handler; by the time the decision loop processes its first signal, the hook has run synchronously and the handler is live. Constants.EventTypes.AgentShuttingDown = "agent_shutting_down" added. Tests: EnrollmentTerminationHandlerTests refactored — Rig now creates a real InformationalEventPost backed by FakeSignalIngressSink; assertions consult EmittedEventTypes / PayloadOf(eventType) helpers that read the posted signal payloads. Three new tests cover - agent_shutting_down emitted on self-destruct path with correct payload - skipped when SelfDestructOnComplete=false - skipped on WhiteGlove Part-1 exit (no self-destruct) V2.Core: 535 → 538, DecisionCore: 108, all green. Architecture baseline unchanged — EnrollmentTerminationHandler now takes InformationalEventPost, no structural reference to TelemetryEventEmitter. |
||
|
|
c2c55a191e |
v2 single-rail 5.3a: migrate ServerActionDispatcher to Signal-Post + ActionId dedup
Plan §5.3 + §6.2 — ServerActionDispatcher now emits server_action_received/executed/failed through InformationalEventPost (single-rail pipe), not the legacy Action<EnrollmentEvent> bypass. Adds dedup of at-least-once re-deliveries from the backend PendingActions queue: composite key (Type|RuleId|QueuedAt) maps every re-delivery of the same server-queued action to the same slot, squelching duplicates before any telemetry or handler side effect fires. Fixes the 77× terminate_session storm observed in the PR-motivating session without rejecting two genuinely distinct admin-issued terminates. Program.cs: ServerActionDispatcher construction moved inside orchestrator.Start's onIngressReady hook so the live lifecyclePost backs it; WireTelemetryServerResponse picks it up from the outer-scope local after Start returns. Tests: 8 new ServerActionDispatcherTests cover success / failure / unknown- type emission plus the dedup matrix (same key twice, same-batch dup, distinct QueuedAt, distinct RuleId, distinct Type with identical QueuedAt). V2.Core test suite: 527 → 535, all green. Architecture baseline unchanged — ServerActionDispatcher still holds no structural dependency on TelemetryEventEmitter. |
||
|
|
b02313b7ea |
v2 single-rail 5.2: migrate StartupEnvironmentProbes to Signal-Post (B7)
StartupEnvironmentProbes.RunAsync no longer takes a TelemetryEventEmitter
— it now routes device_location, timezone_auto_set, ntp_time_check, and
agent_trace (geo-failure) events through InformationalEventPost. The
existing Source labels ("Network") survive via the Emit(EnrollmentEvent)
overload's payload decomposition.
- Parameter rename: emitter → post throughout RunAsync / RunGeoAndTimezone /
RunNtpCheck / SafeEmit. Bodies unchanged apart from the dispatched Emit.
- Unused using removed (Telemetry.Events namespace no longer referenced
from this file — the probes live outside the emitter-internal zone).
- Program.cs passes lifecyclePost (the single-rail helper instance
established in PR #1) to the background Task.Run invocation, which runs
after orchestrator.Start has completed so the hook has already assigned
the variable.
- Architecture baseline test (PR #0 §4.4) now expects 3 dependents
instead of 4 — StartupEnvironmentProbes dropped out. Comment updated
to mirror the migration inventory so future PRs see the correct
removal sequence.
527 V2.Core tests + 108 DecisionCore tests still pass. No behavior
change on the wire: device_location / ntp / timezone_auto_set events
carry the same EventType / Source / DataJson as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8b413d59a8 |
v2 single-rail 5.1c: migrate Program.cs lifecycle events to Signal-Post
Shifts every direct orchestrator.EventEmitter.Emit(...) in Program.cs (B2-B6 in the plan inventory) onto InformationalEventPost. Fixes the Parity Issue #1 ordering regression: agent_started now lands on the signal log as Seq=1 because it is posted from inside the orchestrator's onIngressReady hook, before any collector-generated signal reaches the ingress. Call sites migrated: - agent_started, agent_version_check, agent_unrestricted_mode_changed: emitted inside the new orchestrator.Start(ingress => { ... }) hook, in the canonical boot order. - enrollment_failed (max_lifetime watchdog), agent_shutdown (auth_failure watchdog): emitted from their event-handler lambdas via a lifecyclePost variable captured by reference — the handlers are registered before Start but only fire later, by which time the callback has assigned the live helper. Three private helpers (EmitAgentStartedEvent, EmitVersionCheckEventIfAny, EmitUnrestrictedModeAuditIfChanged) switched from an EnrollmentOrchestrator parameter to an InformationalEventPost parameter; bodies unchanged apart from the final Emit call. Side fixes: - enrollment_failed (max_lifetime) no longer declares Phase=Complete (plan §1.4 phase invariant — only phase-declaration events may set a non-Unknown phase). The UI bucketing behavior is unchanged; the legacy setting produced a phantom phase in the timeline. - Null-guard on the lifecyclePost variable: if a watchdog fires before Start's hook has assigned the helper, emission is skipped with a warning rather than NRE. In practice impossible (handlers only fire after Terminated / ThresholdExceeded which come later), but defensive. Left for later PRs (plan §2 inventory): - line ~575 ServerActionDispatcher emitEvent callback → PR #3 (B28). - orchestrator.EventEmitter property itself → PR #10 enforcement step. Tests: 527 V2.Core + 108 DecisionCore pass. Full solution builds clean. Functional impact on a real session: the V2 parity audit's Parity Issue #1 (agent_started = Seq=13) is structurally fixed — verification will be via an E2E session after this lands on a ring device. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2e82dd2ba6 | fix folder structure v2 agent | ||
|
|
f149eef3fe |
feat(agent-v2): wire --replay-log-dir CLI flag to ImeLogTracker SimulationMode
V2 had the complete replay infrastructure in place but never wired up: ImeLogTracker.SimulationMode / SpeedFactor properties (ImeLogTracker.cs:66-67) and the ApplySimulationDelay compression loop (ImeLogTracker.Handlers.cs:529-546) were fully implemented, and AgentConfiguration had ReplayLogDir + ReplaySpeedFactor fields (L.128, L.134), but nothing set them from the CLI and nothing read them in the factory. This commit adds: - CLI args --replay-log-dir <path> and --replay-speed-factor <n> in Program.BuildAgentConfiguration (invariant-culture parse, zero/malformed rejected). - DefaultComponentFactory.ImeLogHost ctor accepts a simulation mode + speed factor and applies them to the inner tracker when ReplayLogDir is non-empty. - PrintUsage documents the flags under a new "Dev / Test" section. For the deterministic signal-level replay (V2 ReplayHarness + signal-log.jsonl fixtures) no wiring is needed - that is already the primary V2 test path. The IME- log replay activated here is the V1 compatibility dev-mode for support cases where only raw IME logs are available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5b7f424d19 |
refactor(agent-v2): route whiteglove_resumed / system_reboot_detected / admin preemption through reducer effects
V2 parity cleanup - the three lifecycle events were emitted from Program.cs
directly while the corresponding reducer handlers (HandleWhiteGlovePart1To2Bridge,
HandleSystemRebootObservedV1) returned Array.Empty<DecisionEffect>(). Admin
preemption synthesised an EnrollmentTerminatedEventArgs and invoked
terminationHandler.Handle directly, bypassing the reducer entirely.
This commit unifies all three flows: the reducer is the single state owner, the
telemetry event appears as an EmitEventTimelineEntry effect of the state
transition, and the orchestrator's Terminated event fires automatically when
stage goes terminal - no more synthetic bypass.
- B1: HandleWhiteGlovePart1To2Bridge emits whiteglove_resumed timeline entry.
- B2: HandleSystemRebootObservedV1 emits system_reboot_detected with payload
keys previousExitType/lastBootUtc.
- B3: new DecisionSignalKind.AdminPreemptionDetected + new SessionOutcome.AdminPreempted.
Reducer HandleAdminPreemptionDetectedV1 transitions to Completed/Failed based on
adminOutcome payload, emits enrollment_complete/_failed timeline entry with
adminAction+source data keys. Program.cs posts the signal; terminationHandler
still subscribes to orchestrator.Terminated (no direct Handle calls on the
startup-preemption path).
Fixtures: tests/fixtures/signal-kinds/{system-reboot-observed,admin-preemption-detected}-v1.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a63512d139 |
feat(agent-v2): post SessionStarted signal after register-session (V2 reducer anchor)
V2 parity lack: EnrollmentOrchestrator.Start never synthesised a SessionStarted signal, so HandleSessionStartedV1 (DecisionEngine.Shared.cs:36) was unreachable in production. DecisionState.Stage stayed at Unknown for the entire session lifetime while raw signals (ESP / Hello / etc.) landed on an uninitialised state. The reducer tolerated this silently, but it left us no session-anchor timestamp and no early stage-classification hook. Program.cs now posts SessionStarted directly after register-session success, with the tenant-registered metadata (EnrollmentType, IsHybridJoin, ValidatedBy, ...) as signal payload. Skipped on WhiteGlove Part-2 resume (orchestrator posts SessionRecovered instead) and on admin preemption (subsequent AdminPreemption signal drives straight to terminal; see next commit). Follow-up: the reducer handler currently only bumps StepIndex - a later patch should have it consume the enrollmentType/isHybridJoin payload for flow routing. Logged in tasks/todo.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bcd3c23168 |
feat(agent-v2): register-session handshake + periodic-collector idle mgmt + network-change + DO handler-chain fix
Closes the remaining critical V1 parity gaps uncovered by the audit. Without these, the V2 agent would accept events into /api/agent/telemetry but the backend Sessions table would never get a row (so every UpdateSessionStatusAsync / IncrementSession silently no-ops), PerformanceCollector + AgentSelfMetricsCollector would run for the entire session lifetime filling the spool, network transitions would be invisible, and the DO collector would overwrite newer handlers on shutdown. PR #51 — Session registration (V1 MonitoringService.RegisterSessionAsync) - New SessionRegistrationHelper with 5-retry + V1 exponential backoff (2s/4s/8s/16s). Auth failures (401/403) short-circuit via AuthFailureTracker.RecordFailure ("agent/register-session") — which already fires first-failure distress with AuthCertificateRejected / DeviceNotRegistered. Other exceptions on the last attempt trigger EmergencyReporter with AgentErrorType.RegisterSessionFailed. - New EnrollmentRegistryDetector — V1-parity registry probes for CloudAssignedDomainJoinMethod (hybrid join) and CloudAssignedDeviceRegistration / CloudAssignedEspEnabled (v1 vs v2 / WDP) so SessionRegistration carries the same EnrollmentType / IsHybridJoin values V1 sent. - Program.cs calls RegisterWithRetryAsync after BackendTelemetryUploader is ready and BEFORE orchestrator.Start. On non-Succeeded outcome, the agent exits cleanly (exit code 6 for auth, 7 for other) instead of spinning up collectors for an unregistered session — V1 parity: "=== SESSION REGISTRATION FAILED — collectors will NOT start ===" - BackendApiClient.RegisterSessionAsync is now `virtual` so test doubles can intercept the HTTP call. - Admin-override preemption: when the register response carries AdminAction ("Succeeded"/"Failed") we emit the matching enrollment_complete/_failed event and synthesise a Terminated so the termination handler runs cleanup immediately — V1 parity with MonitoringService.cs:388-413. Periodic-collector idle lifecycle (V1 PeriodicCollectorManager) - New PeriodicCollectorLifecycleHost owns PerformanceCollector + AgentSelfMetricsCollector under a single CollectorIdleTimeoutMinutes window (default 15). Stops both after N minutes of no real enrollment activity and restarts them on the next real event. Without this the V2 agent kept polling perf counters every 60s for the agent's full lifetime on dormant sessions. - Activity observation: the host wraps the onEnrollmentEvent sink passed to its internal collectors so events with "performance_snapshot" / "agent_metrics_snapshot" / "stall_probe_*" / "session_stalled" types do NOT count as activity — only real events reset the idle timer. - Emits performance_collector_stopped / agent_metrics_collector_stopped on idle-stop (V1 parity event types + data schema). Network-change wiring - NetworkChangeDetector existed in V2 but was never instantiated. New NetworkChangeHost wires it into DefaultComponentFactory so WiFi SSID / default-route / MDM-endpoint-reachability transitions are emitted as network_change events — V1 parity with CollectorCoordinator.StartOptionalCollectors:375-382. DeliveryOptimizationHost Stop() bug - V1 comparison `ReferenceEquals(x.OnAppStateChanged, x.OnAppStateChanged)` was a tautology (always true) — silently overwrote any newer handler that had taken over the slot. Fixed by capturing the chained handler reference and comparing against it instead. Tests - 481/481 V2 tests green (+5 new SessionRegistrationHelper tests covering the 5-retry happy path, retry-then-success, auth short-circuit, exhaustion after 5 exceptions, and registration payload correctness). - 708/708 Backend tests untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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>
|