Skip to content

4.12 Moving the Legacy Farm Clients

Every farm PC running the agent of the farm MVP moves to the new farm sync client through the MVP's own update channel, silently and without losing a night of data (decided 2026-11-10). The MVP's agent receives one last Runtime, the bridge, which installs the new client from releases, hands it the farm's enrolment and source settings, waits until the new client has synced, and then retires itself. The MVP (moonitoring-legacy-farm_mvp) is otherwise frozen and is deleted once no farm depends on it.

What exists today

The facts about the MVP's updater come from its code on 2026-11-10, read for the security review of that day. Nothing on this page is built yet.

4.12.1 What the Legacy Channel Can Do

  • On every farm PC the Launcher starts each night, from a per-user scheduled task, and runs the Runtime named by a manifest it downloads from the MVP's API. The Runtime can be replaced; the Launcher, the scheduled task and the URLs built into the Launcher cannot: a new Launcher only arrives when the farmer runs a new Setup.
  • Everything runs as the farmer's Windows user, never elevated. Only AfiMilk farms have one task that runs as SYSTEM, installed by Setup with a UAC prompt.
  • The channel is trusted by TLS alone: the manifest carries a SHA-256 but no signature, and the executables are not code-signed. The bridge therefore trusts nothing it downloads through this channel except itself.

4.12.2 Requirements on the New Client

The move is silent only if the farm sync client meets these from its first release (moonitoring-app-farm_sync):

  1. It installs and runs per user, without elevation: files in %LOCALAPPDATA%\MooNitoring\SyncClient, a per-user scheduled task or log-on start; a Windows service only as an optional, elevated upgrade later.
  2. Its installer is published on a releases channel, so its manifests are covered by the channel's TUF trust chain (Signing). Executables are not code-signed for now, in prod too (decided 2026-11-10): Windows may show its SmartScreen warning at the first run; Authenticode signing is added when it is decided.
  3. It accepts a hand-over bundle at first start (below), redeems the enrolment code with a key it generates itself, and deletes the bundle.
  4. It reads the same export folders the legacy agent writes for each vendor, so the first raw upload needs no new set-up at the farm.

4.12.3 How a Farm Moves

sequenceDiagram participant Staff participant MVP as MVP API participant Bridge as Bridge (legacy Runtime) participant Releases as releases participant Client as New sync client participant Farm as farm Staff->>Farm: create the sync client of the farm's Organization Staff->>MVP: enable the move for this farm (wave) Bridge->>MVP: nightly: is there a move for this device? MVP->>Farm: issue an enrolment code (staff service account) MVP-->>Bridge: channel, enrolment code Bridge->>Releases: manifest of the client, verified against the pinned TUF root Bridge->>Client: install per user, hand-over bundle Client->>Farm: redeem the code with its own key Client->>Farm: first sync run, raw upload Bridge->>MVP: following nights: has the client synced? Bridge->>Bridge: after 7 nights of both: retire the legacy agent
  1. Before a farm moves, staff give it an Organization in access and a sync client in farm (the move of the MVP's farms and users into the platform is its own procedure), and enable the move for that farm in the MVP. Farms move in waves: one pilot farm, then a tenth of them, then the rest, each wave after the previous one has synced for a week.
  2. The bridge is a normal Runtime release of the MVP: on a farm not yet enabled it behaves exactly like the current Runtime, so publishing it changes nothing until staff enable a farm.
  3. On an enabled farm, the bridge asks the MVP's API, with the device's own secret, for its move ticket: the releases channel to install from and a one-time enrolment code that the MVP's API obtains from farm (POST /sync-clients/{client_id}/enrolment-codes) with a staff service account. The code lasts 24 hours; an unused one is replaced the next night.
  4. The bridge downloads the new client from releases and verifies it against the channel's TUF root, which is built into the bridge (and the Authenticode signature, once executables are signed). A failed check stops the move and is reported; the legacy agent goes on as before.
  5. It installs the client per user and writes the hand-over bundle: the enrolment code, the farm's vendor sources with their export folders, and the source credentials the legacy agent holds, encrypted with Windows DPAPI for the farmer's user and readable only by that user. The bundle is never passed on a command line or logged.
  6. The new client redeems the code, deletes the bundle, and starts syncing. Both agents now run each night, each to its own backend, so no night is lost while the new path is proved; staff compare the two.
  7. After seven nights in which the new client has completed a sync run, the bridge retires the legacy agent: it deletes its scheduled task, its device secret and its source credentials, keeps its logs for 30 days, and tells the MVP's API, which revokes the device. The Launcher's file stays until the logs go, since a running process cannot remove itself.
  8. If the new client has not synced within 14 nights, the move is reported and paused for that farm; the legacy agent keeps running until staff resolve it.

AfiMilk farms need one elevated step: the legacy SYSTEM task cannot be removed without administrator rights, and the new client's AfiMilk exporter needs them too. The new client asks for it once, with a UAC prompt, at a moment the farmer is at the PC; until then the old task stays.

4.12.4 Security of the Move

  • The bridge itself arrives through the unsigned legacy channel, as every Runtime does today. Before it is published, the MVP's release environment gets required reviewers and a branch rule (security finding MVP-H3), so only a reviewed build can become the bridge.
  • Everything the bridge installs is verified against keys the MVP never held: the TUF root of releases, built into the bridge (and the Authenticode certificate, once executables are signed).
  • The enrolment code reaches the bridge only over the device's authenticated session with the MVP's API, is single-use, and lasts 24 hours.
  • Secrets cross from the old agent to the new one only on the PC, encrypted for the same Windows user, and are deleted from the legacy agent once the move completes.

4.12.5 What Each Part Needs

Part Work
moonitoring-app-farm_sync The requirements of 4.12.2; reading the hand-over bundle
releases A channel for the client, its installer, and the public manifest path the bridge reads; the installer also installs for a signed-in person and writes the same bundle (4.12.6)
farm Nothing new: sync clients, enrolment codes and sync runs exist
The MVP The move ticket endpoint, the per-farm switch and waves, the bridge Runtime, the protected release environment
Staff tooling Creating the Organizations and sync clients of the MVP's farms; following each wave

4.12.6 The Hand-over Bundle

The bridge hands the farm to the new client in one file (decided 2026-11-10, read by the farm sync client): %LOCALAPPDATA%\MooNitoring\SyncClient\handover.bin, written after the bridge installed the client and before it starts it, at most 1 MiB.

The installer writes the same file when a person installs the client and chooses a farm (decided 2026-11-10): it signs the person in through the browser, registers the farm's sync client, issues its enrolment code with the person's token and puts that code in the bundle, with sources empty, so the client starts already connected and the person signs in only once (Installer and Client Library).

  • It is the output of CryptProtectData for the current user (never the machine scope), with the entropy MooNitoring.SyncClient.HandOver.v1, so only the same Windows user on the same PC can read it. It is never passed on a command line, in an environment variable or in a log.
  • Its content is UTF-8 JSON, and unknown fields are refused, so a bridge and a client of different formats never half-understand each other:
{
  "format": "moonitoring.farm_sync.handover/v1",
  "enrolment_code": "7K2QD-M9XBA-4RT6W",
  "sources": [
    {"vendor": "delpro", "export_folder": "C:\\DelPro\\Export",
     "source_feed": "<farm feed id>", "origin_version": "<origin version id>",
     "credential": {"user": "…", "password": "…"}}
  ]
}
Field Required What it is
format yes moonitoring.farm_sync.handover/v1
enrolment_code yes The one-time code of the move ticket
sources[].vendor yes The legacy agent's key for the vendor: letters, digits, -, _
sources[].export_folder yes The absolute folder of that vendor's exports
sources[].source_feed, origin_version no The feed of farm and the origin version, when the move ticket names them
sources[].credential no The fields of the source credential, by name

At its first start the client unprotects and checks the bundle (one of another user, entropy or format, or with an unknown field, is deleted and reported), redeems the code with a key it makes itself, keeps each credential in the operating system's credential store, assigns each folder to its feed, and deletes the bundle. If farm cannot be reached the bundle stays for the next start; if the code is refused the bundle is deleted.