Appearance
The Windows service, claim by claim
WIN-DLP-EPIC-002 names seven things: start, stop, restart, recover, report state, log to the Windows event log, and refuse to run on a build it does not support. Its reason is worth repeating because it is the right one: an agent that is not running is an agent that protects nothing, and the commonest reason a DLP agent is not running is that its service lifecycle was written casually.
So this is a cross-reference, not a description. Every claim names the test that holds it up, or says plainly what does not.
The implementation is agent/dlp-agent/src/service.rs; the console mode it shares a loop with is dlp-agent run.
No Windows CI job has passed yet. No GitHub-hosted job ran from 2026-09-15 to 2026-10-01, because the account's billing failed, and the Windows jobs were added in that window (
DLP-1215). Since then they run and have not yet passed (Windows CI status). Where this page nameswindows-latest,windows-e2eorwindows-agentas the proof, read it as the test written to prove it: none of those steps has executed. The Rust tests named here run on Linux, and the Windows arms are cross-compiled.
1. Start
| Claim | Proved by |
|---|---|
The SCM is handed a ServiceMain and the process blocks until the service ends | tests/windows_verification.ps1, service section, run by the windows-e2e job on windows-latest |
SERVICE_START_PENDING is reported with a rising checkpoint while start-up work runs, so the SCM does not kill a slow start | service::tests::the_stop_checkpoint_beats_the_wait_hint covers the checkpoint discipline; the PowerShell run covers the real start |
| Start-up does only local work, so an unreachable management server cannot fail the start | serve() - Agent::load then SERVICE_RUNNING, with the first check-in after it. Asserted end to end by the same script, which starts the service before the agent has talked to anything |
| A start that fails exits with a service-specific code, so the SCM runs the recovery action instead of recording an ordinary stop | ERROR_SERVICE_SPECIFIC_ERROR in run_service; observed by the recovery assertion below |
2. Stop, and the three ways Windows asks for one
SERVICE_CONTROL_STOP, SERVICE_CONTROL_SHUTDOWN and SERVICE_CONTROL_PRESHUTDOWN all mean the same thing to this agent: drain the queue, release the run marker, report SERVICE_STOPPED.
| Claim | Proved by |
|---|---|
| Every flavour of stop is treated as a stop | service::tests::every_flavour_of_stop_is_a_stop |
| Only a running service accepts controls | service::tests::only_a_running_service_accepts_controls |
| A stop releases the run marker, so it is not later reported as tampering | continuity::tests::a_clean_shutdown_leaves_the_next_run_nothing_to_report (in dlp-agent-core) |
| A killed service is reported on the next start | continuity::tests::a_run_that_was_killed_is_reported_on_the_next_start |
That pair is the point of the marker: an ordinary stop and a kill must not look alike, in either direction.
3. Restart and recover
| Claim | Proved by |
|---|---|
| Recovery actions are configured at install: restart after 5s, 30s, 120s, with a 24-hour reset window | The deferred MyDlpConfigureService action calls ChangeServiceConfig2W after InstallServices; the MSI Windows E2E reads the resulting registry flags and sc.exe qfailure output |
| The SCM actually brings the service back after its process is killed | The same script kills the process and waits for it. The windows-e2e job does not skip that section - a GitHub runner is elevated, so it can |
| Restart and stop work through the ordinary Windows verbs | Restart-Service / Stop-Service in the same script |
4. Report state
SetServiceStatus is called at every transition, with a checkpoint and a wait hint on the pending ones. The state machine is testable on any host because the SCM's vocabulary is declared unconditionally - which is why the three tests above run on Linux as well as Windows.
5. The event log
Seven ids, in three ranges that carry meaning: 1000 lifecycle, 2000 normal operation, 3000 something an operator should look at. A reader filtering on >= 3000 gets everything worth an alert without knowing any individual id.
They are published and pinned, because an administrator writes a SIEM rule against a number and a number that moves between releases breaks every rule built on it, silently, and the first anybody knows is that an alert stopped firing.
| Claim | Proved by |
|---|---|
| The ids are the published numbers | service::tests::the_event_ids_are_the_published_numbers |
| Each is listed once and explained | service::tests::every_id_is_listed_once_and_explained |
| Anything worth an alert is at or above 3000 | service::tests::anything_worth_an_alert_is_at_or_above_three_thousand |
| The catalogue is documented where an administrator would look | service::tests::the_catalogue_is_documented_where_an_administrator_would_look, which reads and fails if an id is missing from it |
| A failed start's reason reaches the log | windows-e2e dumps Get-EventLog -Source MyDLPAgent on failure, which is where the reason exists when a service will not start |
| Lifecycle entries render their message text, not a missing-description placeholder | windows-e2e reads Application events 1000 and 1001 after start/stop and checks the source's message table points at the installed agent |
The catalogue test is the one that earns its place: it means this document and that one cannot drift apart without the build going red.
6. "Refuse to run on a build it does not support" - done differently, on purpose
The service starts on every build, and says so when the build is not one the agent claims. That is a deliberate departure from the epic's wording, and the reasoning is the epic's own: an agent that is not running protects nothing.
Refusing to start on an unrecognised build would turn "we have not tested this" into "you have no DLP on this machine", which is a worse outcome than the untested one - and it would do it at the moment a fleet is most likely to hit it, which is a Windows version released after the agent shipped.
What the agent does instead:
| Claim | Proved by |
|---|---|
The build is read through RtlGetVersion, not the shimmed GetVersionExW that lies to unmanifested processes | agent/core/src/windows_build.rs, ten tests |
A build older than Windows 10 is Unsupported, and one below the minimum for its line is too | windows_build tests |
A build that cannot be read is Undetermined - no claim either way | windows_build tests |
| The verdict qualifies the whole capability matrix rather than sitting beside it | platform::tests::the_reported_build_support_matches_the_platform_it_is_reported_on |
| The verdict reaches the machine's own Application log at start-up, not only the server | Event id 3003 (DLP-116), written when the verdict is noteworthy and not when it is Supported - a warning on every start is one nobody reads |
Before 3003 existed the verdict reached the server's capability matrix and never the endpoint's own log, so an administrator standing at the machine had no way to find it. That was the gap this audit found.
7. What is not covered
- No reboot has been watched, and no restart either. The
windows-e2ejob is written to kill the process and see the SCM restart the service, and it has not yet passed onwindows-latest(Windows CI status); even then it would not restart the machine. Delayed automatic start, and whether the agent survives a real boot, are unobserved. - A runner is not a fleet. One image, no domain, no Group Policy, no other endpoint software, no antivirus with its own opinions about a service that watches files.
docs/KNOWN_LIMITATIONS.mdsays so. - CI has not yet passed these on Windows. They exist and are wired into
.github/workflows/ci.yml; no hosted job ran until 2026-10-01 (DLP-1215), and since then the job stops at the agent's failing tests (Windows CI status). That they are green is a claim nobody can make yet. - Nothing here covers an upgrade in place, which is
DLP-1122, or an unauthorised uninstall, which isDLP-154.