# GE-Enforce: concepts, the shopdb plugin, and imaging-time integration

This guide explains how GE-Enforce works, how the shopdb `geenforce` plugin
manages it, and when GE-Enforce installs and takes over during the imaging
process. It is written for site IT.

It pairs with two companion docs:
- `docs/GE-ENFORCE-CLIENT.md` - the client fetch/report contract + the reference
  PowerShell kit (`plugins/geenforce/client/`).
- `docs/proposals/ge-enforce-plugin.md` - the design/plan and the staged cutover.

The ground truth for behavior is the engine itself
(`Install-FromManifest.ps1`) and the on-share manifests; this guide describes
what they do, it does not replace them.

---

## 1. What GE-Enforce is

GE-Enforce is a **desired-state enforcement** system for shopfloor PCs. Instead
of a one-time install during imaging, it continuously makes each PC match a
declared list of what should be installed - and RE-installs anything that drifts
(uninstalled, corrupted, or overwritten). It is the shopfloor equivalent of a
lightweight, air-gapped-friendly configuration-management agent.

Two things make up the system:

1. **The engine + dispatcher on each PC** - PowerShell that reads a manifest and
   enforces it every logon and periodically.
2. **The manifests** - JSON files that declare, per imaging PC type, what to
   install / copy / write and how to detect whether it is already correct.

The shopdb `geenforce` plugin adds a third piece: it lets you **author, publish,
and version those manifests in shopdb** (instead of hand-editing JSON on a file
share) and **see what every PC actually did** (fleet compliance reporting).

---

## 2. How GE-Enforce works (the framework)

### 2.1 Two phases

Every shopfloor PC is governed in two distinct phases:

| Phase | When | Runs what | Purpose |
|---|---|---|---|
| **Preinstall** | ONCE, at imaging | `preinstall.json` (via the imaging `00-PreInstall` step) | Day-zero foundation: PowerShell 7, the VC++ redistributable matrix, Oracle Client, Adobe Reader, HostExplorer, serial drivers, etc. "Install once at imaging, no drift correction." |
| **Runtime** | EVERY logon + periodically | `common/manifest.json`, then `gea-shopfloor-<type>/manifest.json`, then an optional `<type>-<subtype>` manifest | Ongoing enforcement + self-heal: app versions, config-file drift, registry drift, per-cycle scripts (asset report, VNC firewall, EventSaver), version-gated installs. |

The two phases share the same entry SHAPE (field names) but are run by different
runners with different capabilities. Preinstall is a one-shot at imaging that
implements only `Type=MSI` and `Type=EXE`, with only `Registry` / `File`
detection (other types/detections are skipped). Runtime is the continuous
enforcement loop and implements the full Type + DetectionMethod matrix below.

### 2.2 The runtime loop, step by step

On each cycle (`GE-Enforce.ps1` on the PC):

1. Read the PC's identity from `C:\Enrollment\` (see 2.4).
2. Look up the SFLD share credential in the registry and **mount the share**
   (SYSTEM cannot reach the share as its computer account, so it mounts as the
   provisioned SFLD user - `net use W: ...`). If no credential yet, exit 0 and
   retry next cycle (Azure DSC has not provisioned it).
3. Run the engine (`Install-FromManifest.ps1`) against `common/manifest.json`,
   then `gea-shopfloor-<pctype>/manifest.json`, then a `<pctype>-<subtype>`
   manifest if one exists. Common runs first so shared prerequisites (e.g.
   Oracle Client) land before type-specific apps that depend on them.
4. Write a status file back to the share (and, in the shopdb model, POST a
   report - see 4.3).

Every failure is non-fatal (exit 0) so a network blip or a not-yet-provisioned
credential never blocks or breaks a PC.

### 2.3 The manifest: scopes and entries

A manifest is `{ "Version", "_comment", "Applications": [ entry, ... ] }`. Each
imaging PC type is a **scope** with its own manifest, plus the fleet-wide
`common` scope:

- `common` - runs on EVERY PC type; entries use a `PCTypes` filter to target
  subsets (e.g. "EventSaver on collections + heattreat, but not CMM").
- `gea-shopfloor-collections`, `-nocollections`, `-cmm`, `-keyence`, `-common`
  (lab/timeclock), `-genspect`, `-heattreat`, `-partmarker`, `-waxtrace` - each
  runs only on PCs of that type, so its entries usually do NOT set `PCTypes`
  (the manifest already only runs there). Keyence is the exception: it uses
  `PCTypes` for hardware SUBTYPE targeting (`keyence-vr6000` vs `keyence-vr3000`).

Each **entry** declares one action. Its `Type` picks the action:

| Type | Action |
|---|---|
| MSI / EXE / CMD / BAT | run an installer with `InstallArgs` |
| PS1 | run a script from the share |
| INF | install a driver via `pnputil` |
| File | copy `Source` -> `Destination` |
| Registry | write a value |

### 2.4 Self-heal via detection

Every entry has a `DetectionMethod` that decides whether the action fires:

| Method | Means "already correct" when... |
|---|---|
| Registry | the key/value exists (optionally equals a value) |
| File | the file exists |
| FileVersion | the file's version string matches exactly (fleet convention is a 4-part string like 6.4.5.0; the engine does a raw string compare, it does not enforce 4 parts) |
| Hash | the file's SHA256 matches (case-insensitive) |
| MarkerFile | a marker file exists (the engine writes it after a clean install) |
| ValueMatches | a registry value equals the entry's target |
| pnputil | a driver matching a pattern is present |
| Always / (none) | fires EVERY cycle (used for per-cycle scripts) |

If detection says "not correct," the action runs. That is the self-heal: delete
`DncMain.exe` and next cycle re-installs eDNC; corrupt a config file whose Hash
no longer matches and next cycle re-copies it. **Entry order is execution
order** - config-restore entries sit AFTER their installer so a mid-cycle vendor
overwrite is healed on the same cycle.

### 2.5 Targeting gates (all ANDed)

An entry can be narrowed by any combination of:

- `PCTypes` - which PC types (alias-aware: old names like `Standard` map to
  `collections`/`nocollections`/`common`). Fleet-wide `common` uses this heavily.
- `TargetHostnames` - specific hostnames (supports `*` wildcards).
- `TargetMachineNumbers` - specific bay machine numbers (e.g. Okuma bays).
- `_CmmVersion` - CMM PCs only: a tagged entry applies when it equals the bay's
  resolved PC-DMIS version (`C:\Enrollment\cmm\version.txt`). IMPORTANT: if no
  version is resolved (file missing/empty - a pre-picker bay), ALL tagged
  entries apply (deliberate legacy "install-all" behavior), so such a bay gets
  every PC-DMIS version, not none. Requires engine lib >= 2.6.
- `PCTypesStrict` - disables alias expansion (PREINSTALL runner only; the runtime
  engine ignores it).

Different PC types have different niche gates: CMM uses a version gate, Keyence a
model subtype, Collections per-bay machine numbers. The shopdb editor shows only
the gates a given scope actually uses (see 4.1).

### 2.6 What the PC needs to know about itself (enrollment)

The runtime engine reads the PC's identity from `C:\Enrollment\`:

- `pc-type.txt` - the imaging PC type (which scope to run). `pc-subtype.txt` is
  LEGACY (no longer written at imaging since the 2026-05-04 rename reorg; the
  dispatcher still honors it if present on older fleet PCs).
- `machine-number.txt` - the bay number FALLBACK; the eDNC/DNC registry
  `MachineNo` value wins if present. `9999` is the imaging placeholder by
  convention - the enforcement engine does NOT special-case it; it is simply a
  value that won't match a real bay number in a `TargetMachineNumbers` gate.
  (The 9999-skip you may see is only in the status write-back, not enforcement.)
- `cmm/version.txt` - CMM bays only: the resolved PC-DMIS version for `_CmmVersion`.
- `site-config.json` - the share root and site settings.
- SFLD credentials at `HKLM:\SOFTWARE\GE\SFLD\Credentials` - provisioned by
  Azure DSC after enrollment (this is what gates the runtime phase starting).

Note: all of the identity files above (pc-type, machine-number, cmm version,
site-config) are written in WinPE at the PXE menu, BEFORE the image boots - the
preinstall phase already reads them. What happens post-imaging is only Intune
enrollment + the Azure DSC credential (see the timeline below).

---

## 3. When GE-Enforce installs / takes over (the imaging timeline)

This is the "when to implement it during imaging" question. The order is:

```
[0] WinPE / PXE menu (BEFORE the image boots)
      - identity written to C:\Enrollment: pc-type.txt, machine-number.txt,
        cmm/version.txt, site-config.json (startnet.cmd). The PC already knows
        what it is before Windows starts.
      |
      v
    PXE image applied, Windows boots
      |
      v
[1] PREINSTALL  (00-PreInstall runner runs preinstall.json ONCE)
      - reads the step-0 identity files, then installs the foundation:
        PowerShell 7, VC++ redists, Oracle Client, Adobe Reader, HostExplorer,
        serial drivers, Display kiosk app, ... (things later runtime apps need)
      - preinstall implements MSI/EXE + Registry/File detection only
      |
      v
[2] GE-ENFORCE ITSELF is laid down during imaging
      - the dispatcher (GE-Enforce.ps1), the engine lib (Install-FromManifest.ps1),
        and a scheduled task (at-logon + every ~5 min + shift windows) are
        registered as part of the image / shopfloor setup
      |
      v
[3] ENROLLMENT (post-imaging)
      - Intune / GCCH enrollment, THEN Azure DSC provisions the SFLD share
        credential into HKLM:\SOFTWARE\GE\SFLD\Credentials
      - (the identity files already exist from step 0 - enrollment adds only the
        credential, which is what unblocks runtime)
      |
      v
[4] FIRST LOGON  -> RUNTIME ENFORCEMENT BEGINS
      - the scheduled task runs GE-Enforce.ps1: mount share, run common + the
        PC-type (+ subtype) manifests, install/self-heal, report
      - repeats every logon + periodically forever after
```

Key points on timing:

- **Preinstall (step 1) is the imaging-time install.** Put anything that must
  exist before first logon, or that never needs drift correction, here (runtimes,
  redistributables, drivers). It runs once and is done.
- **Runtime enforcement (step 4) does not start until enrollment (step 3)
  provisions the SFLD credential.** Before that, GE-Enforce exits 0 each cycle
  and waits. So a freshly imaged PC that is not yet enrolled is inert, by design.
- **The engine lib version matters.** `_CmmVersion` gating needs lib >= 2.6 on
  the PC; deploy the lib before a manifest that uses it.
- Some apps appear in BOTH phases: preinstalled at imaging for day-zero, then
  carried by a runtime entry so drift is corrected later (Oracle, UDC, Adobe,
  HostExplorer, Defect Tracker).

Rule of thumb: **imaging-time (preinstall) = foundation that must be there or
never drifts; runtime = everything that needs to stay correct over the PC's
life.**

---

## 4. How the shopdb plugin manages this

The `geenforce` plugin turns the manifest from hand-edited JSON on a share into
shopdb data you author, version, publish, and monitor. It lives under the
top-level **GE-Enforce** section (Manifests | Enforcement Reports), not Settings,
because it is a full management surface.

### 4.1 Manifests - authoring (GE-Enforce > Manifests)

- **PC Types (scopes):** each imaging PC type is a row; add/edit/delete. (The
  scope's `computertypeid` field is now the imaging-pc-type -> ComputerType
  mapping mechanism, per ADR-012. The old Settings > Collector PC Types page is
  retired; there is nothing to configure there anymore.)
- **Entries:** an ordered list (Up/Down = the execution-order contract). Add/Edit
  opens a typed form: the payload fields switch on `Type` (MSI shows Installer +
  InstallArgs, PS1 shows Script + Args, File shows Source + Destination, Registry
  shows the Reg* fields), a detection block, an InUseCheck editor, and a
  **Targeting** section that shows only the gates the scope uses (CMM shows the
  version gate; the common/preinstall scopes show PC types; a scope whose entries
  use machine numbers shows those) with a "Show all targeting options" escape
  hatch.
- **Simulate ("what would a PC get?"):** enter a PC profile (type, subtype,
  hostname, machine number, CMM version) and see which entries apply and why the
  rest are filtered - without reading a PowerShell log.
- **Publish / Versions / Roll Back:** editing changes a DRAFT only. Publish
  freezes an immutable version; PCs are only ever served the published version;
  Roll Back restores an earlier one. History (date, author, note) per version.

### 4.2 Milestone 1 - export to the share (engine unchanged)

Today the enforcement engine still reads manifests from the SFLD share. The
plugin's **Export to Share** button writes the current published manifest to
`<shareroot>/<scope>/manifest.json` (backing up the old file to `_meta/history`
first). So the workflow is:

**author + publish in shopdb -> Export to Share -> the unchanged engine picks it
up next cycle.**

Nothing about the engine, the share layout, or the PCs changes. Rollback is
restoring the `_meta/history` backup (or re-publishing an older version and
re-exporting). This is the safe first milestone: all the authoring benefit, zero
client risk.

Configure the share root once at the top of the Manifests page.

### 4.3 Enforcement Reports - fleet compliance (GE-Enforce > Enforcement Reports)

Each PC reports its enforcement result back to shopdb (see the client kit). The
Reports page shows, per PC:

- **Received** - did the PC apply the latest published version? (applied vs
  latest). "behind" means it has not picked up your newest publish yet.
- **Status** - `ok` (nothing needed), `selfhealed` (drift corrected), `failed`.
- **Counts** - installed / skipped / failed, plus per-entry detail (action,
  self-heal flag, exit code, message) in the row's Detail view.

This is the observed-state half of the loop: the manifest is what SHOULD be
installed; the report is what each PC ACTUALLY did.

### 4.4 The client side (per PC)

The engine sources the manifest and reports results using the reference kit in
`plugins/geenforce/client/` (`ShopdbEnforceClient.psm1` +
`Invoke-ShopdbEnforce.ps1`), configured from `HKLM:\SOFTWARE\GE\ShopDB`
(BaseUrl + a `geenforce.fetch`/`geenforce.report` service token). See
`docs/GE-ENFORCE-CLIENT.md` for the fetch/report contract, the last-known-good
cache, shadow mode, and the staged cutover from share-sourced to shopdb-sourced
manifests.

The cutover from share-sourced to shopdb-sourced manifests is per PC type. The
**displays/kiosks cohort has cut over**: share-less display PCs fetch their
manifest and payloads entirely over HTTPS (see `docs/geenforce-api-cutover.md`
and `docs/GE-ENFORCE-DISPLAY.md`). All other fleet PC types (cmm, collections,
keyence, genspect, heattreat, partmarker, nocollections, common) still enforce
from the SFLD SMB share via Export to Share (4.2) and only REPORT to shopdb. The
playbook for moving the next PC type is `geenforce-api-cutover.md` section 11.

---

## 5. Day-to-day: common tasks

All in GE-Enforce > Manifests. No PowerShell, no editing JSON on the share.

- **Add an app to a PC type:** open the PC type, Add Entry, pick the Type (the
  form adapts), fill the installer + detection + any targeting, place it in order
  with Up/Down (config restores go BELOW their installer), Preview, Publish, then
  Export to Share.
- **Bump an app version:** drop the new installer in the scope's `apps/` folder
  on the share, open the entry, update the Installer filename + the Detection
  value (the new version), Publish, Export to Share. PCs self-heal next cycle.
- **Roll back a bad publish:** the PC type's Versions list -> Roll Back to the
  last good version -> Export to Share.
- **Canary a risky change:** add the one test PC under Target hostnames (via
  "Show all targeting options"), Publish; when happy, remove the filter and
  Publish again.
- **Check "did PC Y get app X":** use Simulate with that PC's type / machine
  number / CMM version; and check Enforcement Reports for what it actually did.

---

## 6. Reference

- Engine (behavior ground truth): `Install-FromManifest.ps1` (lib >= 2.6).
- Dispatcher: `GE-Enforce.ps1` (mount + run common then type scope).
- Preinstall runner: `00-PreInstall-*` over `preinstall.json` (imaging-time).
- shopdb model + API: `plugins/geenforce/` (models, importer/serializer,
  filters mirror, service, routes).
- Behavioral parity gate (proves the shopdb model round-trips the real
  manifests): `plugins/geenforce/parity.py` + `flask geenforce parity`.
- Client kit + contract: `plugins/geenforce/client/`, `docs/GE-ENFORCE-CLIENT.md`.
- Agent deployment (per PC, any imaging path): `docs/GE-ENFORCE-DEPLOY.md` +
  `plugins/geenforce/client/Install-GEEnforce.ps1`.
- Design + cutover plan: `docs/proposals/ge-enforce-plugin.md`.
