mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 21:19:38 +00:00
6a9203905df78db0ef10a1861cadc19573be057f
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
0ee68c6192 |
feat(agent-v2): M4.5.b runtime entry + DefaultComponentFactory
Wires the V2 agent into a runnable state: - TenantIdResolver: HKLM\SOFTWARE\Microsoft\Enrollments → AADTenantID (EnrollmentType=6) - SessionIdPersistence: atomic session.id via File.Replace - IComponentFactory signature: +ISignalIngressSink +IClock (bridges the sink/clock access into factory-built adapters) - DefaultComponentFactory: 5 kernel hosts (EspAndHelloTracker coordinator, DesktopArrivalDetector, AadJoinWatcher, ImeLogTracker+ImeProcessWatcher, StallProbeCollector) + 2 peripheral hosts (Performance, AgentSelfMetrics) conditional on CollectorConfiguration toggles - Program.cs: full runtime entry (args → Logger → TenantId/SessionId → AwaitEnrollment → BackendApiClient+RemoteConfigService → MtlsHttpClientFactory → BackendTelemetryUploader → DefaultComponentFactory → EnrollmentOrchestrator → Ctrl+C wait → Stop → exit 0) Production host choice: EspAndHelloTracker as single coordinator entry (instead of 4 sub-tracker hosts in parallel) — avoids adapter signal duplication documented in M4.3 tech-debt. StallProbeHost drives its own 60s idle-tick timer (Legacy MonitoringService.IdleCheckCallback is gone). Tests: +8 SessionIdPersistenceTests (atomic-write, round-trip, corrupt- recovery, regeneration, multi-instance resume). V2.Core 287→295, total 1424→1432 green. Known peripheral gaps (tracked for follow-up M4.6): install mode, SelfUpdater, ServerActionDispatcher, CleanupService, Geo/NTP/Timezone wiring, Emergency/DistressReporter wiring, AnalyzerManager, SummaryDialog, DiagnosticsPackageService, gather-rules/ime-matching modes, watchdog timers, multi-instance guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
710504549b |
refactor(agent): V2-Agent-Split skeleton + infrastructure copy (M2)
Three new projects as a clean-slate V2 fork of the Legacy Agent:
AutopilotMonitor.Agent.V2 — net48 exe, minimal Program.cs,
buildcounter 2.0.N, version-v2.json
AutopilotMonitor.Agent.V2.Core — net48 library, 94 files copied from
Agent.Core via bulk sed namespace
rewrite; same folder layout
AutopilotMonitor.DecisionCore.Tests — xUnit net8.0 with ReplayHarness
+ 13 smoke/harness tests
V2 isolation is build-time-enforced via V2Isolation.targets imported from
both V2 csproj files — any Legacy ProjectReference fails the build with a
clear error. Grep-verified at commit time: zero `using
AutopilotMonitor.Agent.Core` statements and zero Legacy ProjectReferences
in the V2 tree.
Per plan §2.11, kernel items (CompletionStateMachine, CompletionGuards,
EnrollmentTracker*, Flows/*, WhiteGloveClassifier, WhiteGloveSignals,
EnrollmentStatePersistence) are transitionally retained so the V2 tree
compiles; M4 removes them when SignalAdapters + V2Orchestrator take over.
Per user direction (feedback_v2_kernel_only_refactor): the V2 refactor
replaces only the decision kernel. All peripheral capabilities — gather
rules, analyzers, diagnostics upload, timezone, server-driven actions,
summary dialog launch, env-var resolution, version checks, registry
watchers, network/DO/performance telemetry, self-update, bootstrap→cert
handover, self-destruct — are preserved verbatim via the bulk copy.
Supporting M3 bring-up platform:
- ReplayHarness (empty-stream deterministic, SHA256 hash over terminal
state fields) in DecisionCore.Tests/Harness/
- Fixture scaffold under tests/fixtures/: committed anonymized directory
with commit-policy README, plus gitignored enrollment-sessions-local/
for raw exports
- tools/export-session-fixture.mjs with --help / --dry-run / --validate
modes; real MCP wiring marked TODO(v2-m3)
V2 bootstrap scripts scripts/Bootstrap/Install-AutopilotMonitor-v2(.|-dev).ps1
(committed, pure ASCII verified per feedback_ps1_ascii_only). V2 deployment
scripts under scripts/Deployment/V2/ are gitignored (analog to Legacy).
Test totals at M2 gate: 13 DecisionCore + 507 Agent.Core (unchanged) +
557 Functions (unchanged) = 1077 green.
Plan: plans/REFACTOR_AGENT_V2.md §4 M2 + §2.16 V2-Agent-Split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|