# GE-Enforce client integration (shopdb manifest source + reporting)

This is the client-side contract for the GE-Enforce manifest-store plugin: how a
PC sources its install manifest from shopdb instead of a share file, and how it
reports its enforcement result back. It pairs with the plugin proposal in
`docs/proposals/ge-enforce-plugin.md`.

The reference kit lives in `plugins/geenforce/client/`:

- `ShopdbEnforceClient.psm1` - fetch (with ETag + last-known-good cache),
  shadow compare, and report helpers.
- `Invoke-ShopdbEnforce.ps1` - a reference orchestrator that fetches a manifest,
  runs the UNCHANGED engine against it, and reports the result.

These are site-neutral references, not the live dispatcher. A site adapts them
into its GE-Enforce.ps1 flow. The engine (`Install-FromManifest.ps1`),
detection, self-heal, and SMB payload resolution are untouched - only the source
of the manifest JSON moves, plus a result report.

## What does NOT change

- The engine and its four filters, all detection methods, self-heal, marker
  files, and SMB payload staging.
- Payload transport for `smb` rows: the client still mounts the share and
  resolves `apps/...` paths exactly as today. Only the manifest JSON source moves.
- The fail-safe posture: any error exits 0. A PC is never blocked or broken
  because shopdb is unreachable.

## Configuration

Registry (provisioned by Azure DSC, same channel as the SFLD credentials):

```
HKLM:\SOFTWARE\GE\ShopDB
  BaseUrl   https://shopdb.<site>.geaerospace.net
  ApiToken  <a geenforce.fetch (+ geenforce.report) managed service token>
```

Mint the token in shopdb: Settings > API Tokens, scopes `geenforce.fetch` and
`geenforce.report`. It is a service token (owner must hold those permissions).

## Fetch contract

```
GET /api/geenforce/manifest?pctype=<scope>[&phase=runtime]
    X-API-Key: <token>
    If-None-Match: <cached ETag>     (optional)
```

- `200` - body is the full published manifest JSON for the scope (fat client:
  the engine filters locally, exactly as today). Response headers carry `ETag`
  and `X-Manifest-Version`. Cache the body + ETag + version.
- `304` - your cached copy is current; use it.
- `404` - no such scope, or the scope has no published version yet.
- Network failure - enforce from the last-known-good cached manifest (the kit
  does this automatically) and log a warning.

The served manifest is always the current PUBLISHED snapshot, never a live draft
being edited in shopdb, so a half-finished edit can never reach a PC.

## Report contract

Each enforcement cycle, POST the result (best-effort; a failed report never
fails the cycle):

`appliedversion` is the published version the client actually ran, not the
latest one available.

```
POST /api/geenforce/report
    X-API-Key: <token>
    Content-Type: application/json
{
  "hostname": "CMMPC01",
  "scopename": "gea-shopfloor-cmm",
  "appliedversion": 3,
  "enforcerversion": "2.6",
  "counts":  { "installed": 1, "skipped": 3, "failed": 0, "filtered": 2 },
  "results": [
    { "name": "PC-DMIS 2019 R2", "action": "installed", "selfhealed": true },
    { "name": "Protect Viewer",  "action": "skipped" },
    { "name": "eDNC",            "action": "failed", "exitcode": 1603,
      "message": "MSI 1603" }
  ]
}
```

- `appliedversion` lets shopdb show which PCs received the latest manifest
  (`receivedlatest` in the fleet view).
- `action` per entry: `installed` (fired - a self-heal when it should already be
  present), `skipped` (detected present), `failed`, `filtered`. `selfhealed`
  marks a drift correction.
- shopdb keeps the latest report per (hostname, scope, phase) plus history, and
  surfaces it under GE-Enforce > Enforcement Reports.

The engine already computes these counts (`installed/skipped/failed/pcFiltered`
at the end of its main loop) and knows each entry's action; shape them into the
`results` list at the call site (`New-ShopdbReport` in the kit takes a summary
with `Installed/Skipped/Failed/Filtered` + a `Results` list).

The engine emits per-entry outcomes in PascalCase (`Name/Action/SelfHealed/
ExitCode/Message`); `New-ShopdbReport` maps every per-entry key down to the
lowercase names above (`name/action/selfhealed/exitcode/message`) before POST,
so the entire wire contract shopdb reads is lowercase. `ConvertTo-ShopdbSummary`
first normalizes whatever the engine returns (a well-formed summary, a bare
return code, `$null`, or several emitted objects) into the count/results shape
`New-ShopdbReport` expects, so a not-yet-compliant engine still produces a valid
report.

## Common-scope inheritance (opt-in, OFF by default)

By default a PC enforces its `-Scope` ALONE. Pass `-IncludeCommon` to also fetch
the fleet-wide `common` scope and merge it on top, mirroring the real
GE-Enforce.ps1 (which applies `common\manifest.json` first, then the pctype's).
When enabled, `Invoke-ShopdbEnforce.ps1` fetches `common` in addition to
`-Scope` and merges it via `Merge-ShopdbManifests`:

- entries are keyed by `Name` (case-insensitive);
- common's unique entries come first, then all pctype entries (common enforces
  ahead of the pctype, as on the share);
- on a `Name` conflict the pctype entry wins (its override replaces common's).

Common is fetched over the same fail-safe path (ETag + last-known-good cache).
`-CommonScope <name>` inherits a different fleet scope; a run whose `-Scope`
already is the common scope does not merge itself.

Displays do NOT use this: the `gea-shopfloor-display` scope is self-sufficient,
so the display scheduled task omits `-IncludeCommon`. Common-merge exists for a
future share-less non-display PC that genuinely needs the fleet-wide entries
(which would first require repackaging common's SMB payloads as http/inline).
The three display subtypes (Dashboard, Lobby, 3D Print Room), selected by
`C:\Enrollment\display-type.txt`, carry their shared policy inside the display
scope itself, not via common.

## Fail-safe is observable, not silent

Any error still exits 0 - a bad web app never blocks or breaks a PC. But a fresh
display with an EMPTY cache (first boot, shopdb unreachable or the token
rejected with 401 / a TLS-trust failure) would otherwise enforce nothing
*silently*. When no manifest and no cache are available, the kit:

- writes a Windows Application event-log entry (source `ShopdbEnforce`, event id
  1001, type Error) naming the scope and the reason (HTTP status or transport
  error), and
- fires a best-effort report ping (counts `failed: 1`, a single
  `(manifest-fetch)` result carrying the reason) so the miss surfaces under
  GE-Enforce > Enforcement Reports.

The cycle still exits 0; the signal just makes the no-enforcement state visible.

## Cutover (safe, staged)

1. **Configure** the registry values on a canary PC; mint the token.
2. **Shadow mode**: run `Invoke-ShopdbEnforce.ps1 -ShadowMode -ShareManifestPath
   <current share manifest>`. It installs from the SHARE (no behavior change),
   fetches the shopdb manifest, logs any diff, and reports. Watch for zero diffs
   across one PC of every pctype for ~20 cycles.
3. **Read cutover**: drop `-ShadowMode`. The engine now runs against the
   shopdb-sourced manifest; payloads still come from the share. Rollback is a
   one-line revert to the share-sourced call. Keep exporting manifests from
   shopdb to the share (GE-Enforce > Manifests > Export to Share) so the
   share stays a break-glass copy.
4. **Payload migration** (optional, later): move small scripts/configs to
   `http`/`inline` payloads, verified by `payloadsha256`. Big MSIs stay on SMB.

Do not cut a fleet over before the shadow diffs are clean. Preinstall
(`phase=preinstall`) stays share-sourced until its own cutover is planned - it
runs before enrollment provisions a token.

## Security notes

- The client runs as SYSTEM, so shopdb's TLS certificate must be in the machine
  trust store (air-gapped/self-signed sites provision the CA via the same DSC
  step as the token).
- `http`/`inline` payloads are verified against `payloadsha256` before running,
  independent of how the entry detects install state. This is the real integrity
  guarantee and holds even over plain HTTP inside a trusted segment.
- The token is a scoped service token: it can fetch manifests and report, and
  nothing else.
