geenforce: display scope is self-sufficient, no common inheritance

Per decision: displays need none of the fleet-wide common scope's software, so
the gea-shopfloor-display scope carries everything it enforces and does not
inherit common. This avoids repackaging common's SMB-backed payloads for a
share-less display.

- Invert the client common-merge switch: -NoCommon (default-on) becomes
  -IncludeCommon (default OFF). A scope now enforces alone unless opted in.
  The capability stays for a future share-less non-display PC; displays omit it.
- Drop the common SMB-payload audit + inheritance sections from the display
  seed comments and docs (GE-ENFORCE-DISPLAY.md); document self-sufficiency.
- GE-ENFORCE-CLIENT.md: common-scope inheritance is now opt-in.
This commit is contained in:
cproudlock
2026-07-23 08:22:23 -04:00
parent 9d65ef103d
commit d0bf37ced7
5 changed files with 56 additions and 89 deletions

View File

@@ -104,12 +104,13 @@ 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 `New-ShopdbReport` expects, so a not-yet-compliant engine still produces a valid
report. report.
## Common-scope inheritance ## Common-scope inheritance (opt-in, OFF by default)
Every PC inherits the fleet-wide `common` scope on top of its own pctype scope, By default a PC enforces its `-Scope` ALONE. Pass `-IncludeCommon` to also fetch
mirroring the real GE-Enforce.ps1 (which applies `common\manifest.json` first, the fleet-wide `common` scope and merge it on top, mirroring the real
then the pctype's). `Invoke-ShopdbEnforce.ps1` fetches the `common` scope in GE-Enforce.ps1 (which applies `common\manifest.json` first, then the pctype's).
addition to `-Scope` and merges it in via `Merge-ShopdbManifests`: When enabled, `Invoke-ShopdbEnforce.ps1` fetches `common` in addition to
`-Scope` and merges it via `Merge-ShopdbManifests`:
- entries are keyed by `Name` (case-insensitive); - entries are keyed by `Name` (case-insensitive);
- common's unique entries come first, then all pctype entries (common enforces - common's unique entries come first, then all pctype entries (common enforces
@@ -117,11 +118,16 @@ addition to `-Scope` and merges it in via `Merge-ShopdbManifests`:
- on a `Name` conflict the pctype entry wins (its override replaces common's). - 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). Common is fetched over the same fail-safe path (ETag + last-known-good cache).
Pass `-NoCommon` to enforce the pctype alone, or `-CommonScope <name>` to inherit `-CommonScope <name>` inherits a different fleet scope; a run whose `-Scope`
a different fleet scope. A run whose `-Scope` already is the common scope does already is the common scope does not merge itself.
not merge itself. This is how the three display subtypes (Dashboard, Lobby, 3D
Print Room), selected by `C:\Enrollment\display-type.txt`, pick up shared policy Displays do NOT use this: the `gea-shopfloor-display` scope is self-sufficient,
without duplicating it per subtype. 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 ## Fail-safe is observable, not silent

View File

@@ -5,7 +5,8 @@ credential-less kiosk PCs that pull their manifest over HTTPS on port 443 and
authenticate with a read-only service PAT scoped `geenforce.fetch`, sent as authenticate with a read-only service PAT scoped `geenforce.fetch`, sent as
`X-API-Key`. They have no SMB share mount. The kiosk engine and the kiosk `X-API-Key`. They have no SMB share mount. The kiosk engine and the kiosk
browser are baked into the display image, not shipped over HTTPS, so the display browser are baked into the display image, not shipped over HTTPS, so the display
manifest heals POLICY / CONFIG drift and inherited common entries, never EXEs. manifest heals POLICY / CONFIG drift only, never EXEs. It is self-sufficient and
does not inherit the fleet-wide `common` scope (see below).
There are three display subtypes, selected by `C:\Enrollment\display-type.txt`: There are three display subtypes, selected by `C:\Enrollment\display-type.txt`:
`Dashboard`, `Lobby`, and `3DPrintRoom`. `Dashboard`, `Lobby`, and `3DPrintRoom`.
@@ -53,58 +54,18 @@ creates a new version).
| `Lobby` | `/tv` | slides plugin TV dashboard (surface `lobby`) | | `Lobby` | `/tv` | slides plugin TV dashboard (surface `lobby`) |
| `3DPrintRoom` | `/parts-kiosk` | **PLACEHOLDER, TODO-confirm** printedparts parts kiosk route; confirm the real 3D-print-room target with the floor team before publishing to production displays | | `3DPrintRoom` | `/parts-kiosk` | **PLACEHOLDER, TODO-confirm** printedparts parts kiosk route; confirm the real 3D-print-room target with the floor team before publishing to production displays |
## Inheritance: the client merges common ## Self-sufficient: displays do NOT inherit common
The manifest model has no inheritance column. The display scope is a plain The `gea-shopfloor-display` scope carries everything a display enforces. It does
(non-common) runtime scope carrying only display-specific entries. The CLIENT NOT inherit the fleet-wide `common` scope. Displays run the enforcer with
merges the fleet-wide `common` scope underneath the display scope at fetch time. common-merge off (the client default; common-merge is opt-in via
So `common` is where the fleet-wide policy/config/self-update entries live, and `Invoke-ShopdbEnforce.ps1 -IncludeCommon`), so `common`'s SMB-backed fleet
`gea-shopfloor-display` adds the kiosk-only entries on top. entries (Adobe, Oracle, OpenText, Defect Tracker, EventSaver, printer map,
self-update, asset-reporting, ...) never reach a share-less display.
## Blocker: common carries SMB payloads that break on share-less displays This was a deliberate decision: a display needs none of common's software, and
inheriting common would have forced repackaging every SMB `common` payload as
Before a share-less display can safely inherit `common`, every `common` entry `http`/`inline` for a share-less box. Keeping the display scope self-sufficient
that pulls a payload file from the SMB share must first be given an `http` or avoids all of that. If a future non-display share-less PC genuinely needs the
`inline` payload (with a `payloadsha256`). A display has no share mount, so any fleet-wide entries, that is what `-IncludeCommon` plus a per-entry SMB->http
inherited entry whose `Installer` / `Source` / `Script` resolves to a payload conversion would be for -- but displays do not use it.
share-relative path will fail its fetch.
Registry entries in `common` carry no payload (they write inline reg values) and
are safe to inherit as-is. The entries below reference a share file and must be
converted first. This audit is the authoritative to-convert list; the payload
bytes themselves are not converted here (that needs the real payload files).
Common entries that reference an SMB/share payload (as of the on-share
`common/manifest.json`, 22 entries, Version 2.0):
| # | Common entry | Type | Share payload | Field |
| --- | --- | --- | --- | --- |
| 1 | Adobe Acrobat Reader DC | CMD | `scripts/Install-AcroReader.cmd` | Installer |
| 2 | Migrate pc-type.txt to gea-shopfloor-* taxonomy | PS1 | `scripts/Migrate-PCType.ps1` | Script |
| 3 | WJF Defect Tracker | MSI | `apps/WJF_Defect_Tracker.msi` | Installer |
| 4 | 3OF9 barcode font | File | `configs/3OF9.ttf` | Source |
| 5 | Edge IE-Mode site list | File | `configs/enterprise-mode-site-list.xml` | Source |
| 6 | Ensure VNC firewall rule | PS1 | `scripts/ensure-vnc-firewall.ps1` | Script |
| 7 | FMS hosts pin (WJFMS3.AE.GE.COM) | PS1 | `scripts/Set-FmsHostsEntry.ps1` | Script |
| 8 | Oracle Client 11.2 | CMD | `scripts/Install-Oracle11r2.cmd` | Installer |
| 9 | PrinterInstallerMap (site-map printer installer) | File | `apps/PrinterInstallerMap.exe` | Source |
| 10 | OpenText HostExplorer ShopFloor | CMD | `scripts/Setup-OpenText.cmd` | Installer |
| 11 | GE-Enforce dispatcher (self-update) | File | `GE-Enforce.ps1` | Source |
| 12 | Install-FromManifest lib (self-update) | File | `lib/Install-FromManifest.ps1` | Source |
| 13 | Report asset (host + IP + machine number) to ShopDB | PS1 | `apps/Report-AssetToShopDB.ps1` | Script |
| 14 | EventSaver screensaver (binary) | File | `apps/EventSaver.scr` | Source |
| 15 | EventSaver screensaver (config) | File | `configs/EventSaver.ini` | Source |
| 16 | EventSaver enable (per-user screensaver) | PS1 | `scripts/Set-EventSaverScreensaver.ps1` | Script |
| 17 | EventSaver power (keep monitor awake) | PS1 | `scripts/Set-EventSaverPower.ps1` | Script |
| 18 | EventSaver disable (measuring-tool bays) | PS1 | `scripts/Set-EventSaverDisable.ps1` | Script |
| 19 | EventSaver disable (specific hostnames) | PS1 | `scripts/Set-EventSaverDisable.ps1` | Script |
Safe to inherit as-is (Registry entries, no share payload): `3OF9 barcode font
registry entry`, `Edge IE-Mode policy level`, `Edge IE-Mode policy site list
pointer`.
Not every entry above is relevant to a display (a display needs no Oracle
client, OpenText, or Defect Tracker), so a follow-up decision is which common
entries a display should actually run (via `PCTypes` targeting) versus which
must be repackaged as `http`/`inline`. But any that survive targeting must have
a non-SMB payload before displays inherit common.

View File

@@ -24,15 +24,16 @@
.PARAMETER ShadowMode .PARAMETER ShadowMode
Fetch + compare + report, but install from the share (no behavior change). Fetch + compare + report, but install from the share (no behavior change).
.PARAMETER CommonScope .PARAMETER IncludeCommon
The fleet-wide scope every PC inherits (default 'common'). Its manifest is Also fetch the fleet-wide CommonScope and merge it into -Scope, so the PC
fetched in addition to -Scope and merged in, so a display enforces its own enforces its own scope entries PLUS common's (on a Name conflict the -Scope
scope entries PLUS common's. On a Name conflict the -Scope (pctype) entry entry wins). OFF by default: a scope is enforced ALONE (self-sufficient).
wins. Set -NoCommon to disable, or point at a different common scope name. Displays are self-sufficient and do NOT set this. Only a share-less non-display
PC that genuinely needs the fleet-wide common entries over HTTPS turns it on.
.PARAMETER NoCommon .PARAMETER CommonScope
Do not fetch or merge the common scope; enforce -Scope alone (the original Name of the fleet-wide scope merged when -IncludeCommon is set (default
single-scope behavior). 'common'). Ignored without -IncludeCommon.
.NOTES .NOTES
Fail-safe: any error exits 0 so a bad web app never blocks or breaks a PC. Fail-safe: any error exits 0 so a bad web app never blocks or breaks a PC.
@@ -44,8 +45,8 @@ param(
[Parameter(Mandatory)] [string]$EnginePath, [Parameter(Mandatory)] [string]$EnginePath,
[string]$ShareManifestPath, [string]$ShareManifestPath,
[switch]$ShadowMode, [switch]$ShadowMode,
[switch]$IncludeCommon,
[string]$CommonScope = 'common', [string]$CommonScope = 'common',
[switch]$NoCommon,
[string]$BaseUrl, [string]$BaseUrl,
[string]$ApiToken, [string]$ApiToken,
[string]$LogFile = "C:\Logs\Shopfloor\shopdb-enforce-$(Get-Date -Format yyyyMMdd).log" [string]$LogFile = "C:\Logs\Shopfloor\shopdb-enforce-$(Get-Date -Format yyyyMMdd).log"
@@ -126,12 +127,13 @@ try {
# no common merge - the share already carries its own common scope). # no common merge - the share already carries its own common scope).
$manifestToRun = $ShareManifestPath $manifestToRun = $ShareManifestPath
} else { } else {
# Common-scope inheritance: a display enforces its own scope PLUS the # Optional common-scope inheritance (OFF by default; displays are
# fleet-wide common scope. Fetch common too (best-effort, same fail-safe # self-sufficient). Only when -IncludeCommon is set do we fetch the
# cache) and merge it in with the pctype winning on conflict. Skipped # fleet-wide common scope (best-effort, same fail-safe cache) and merge
# when -NoCommon, or when this run IS the common scope. # it in with the pctype winning on conflict. Skipped when this run IS
# the common scope.
$manifestToMerge = $sync.Path $manifestToMerge = $sync.Path
if (-not $NoCommon -and $CommonScope -and ($CommonScope -ine $Scope)) { if ($IncludeCommon -and $CommonScope -and ($CommonScope -ine $Scope)) {
$commonSync = Sync-ShopdbManifest -Scope $CommonScope -Config $config $commonSync = Sync-ShopdbManifest -Scope $CommonScope -Config $config
if ($commonSync.Path) { if ($commonSync.Path) {
$manifestToMerge = Merge-ShopdbManifests -PrimaryManifestPath $sync.Path -CommonManifestPath $commonSync.Path $manifestToMerge = Merge-ShopdbManifests -PrimaryManifestPath $sync.Path -CommonManifestPath $commonSync.Path

View File

@@ -15,12 +15,11 @@ What this scope contains:
launches the kiosk target for the subtype. The display-type -> target map is launches the kiosk target for the subtype. The display-type -> target map is
a data-driven table (DISPLAY_TYPE_TARGETS) so the targets are easy to edit. a data-driven table (DISPLAY_TYPE_TARGETS) so the targets are easy to edit.
Inheritance: the manifest model has no inheritance column. The display scope is Self-sufficient: the display scope carries EVERYTHING a display enforces and
a plain (non-common) runtime scope; the CLIENT merges the fleet-wide 'common' does NOT inherit the fleet-wide 'common' scope. Displays run the enforcer with
scope with the display scope at fetch time. So this scope carries display-only common-merge off (the client default), so common's SMB-backed fleet entries
entries and relies on the client to layer 'common' underneath. See the common never reach a share-less display. This keeps the display path simple and needs
SMB-payload audit in docs/GE-ENFORCE-DISPLAY.md before letting a share-less no common-payload repackaging.
display inherit common.
Authoring path mirrors how every other scope is created: build a manifest dict Authoring path mirrors how every other scope is created: build a manifest dict
and hand it to service.replace_scope_draft (the same call the import-share CLI and hand it to service.replace_scope_draft (the same call the import-share CLI
@@ -35,8 +34,6 @@ from . import service
SCOPE_NAME = 'gea-shopfloor-display' SCOPE_NAME = 'gea-shopfloor-display'
SCOPE_PHASE = 'runtime' SCOPE_PHASE = 'runtime'
# Match the fleet-wide 'common' manifest version so a merged display + common
# document stays internally consistent.
SCOPE_VERSION = '2.0' SCOPE_VERSION = '2.0'
# Edge kiosk relaunch policy key. Values below mirror 09-Setup-Display.ps1 # Edge kiosk relaunch policy key. Values below mirror 09-Setup-Display.ps1
@@ -222,8 +219,8 @@ def build_display_manifest():
'gea-shopfloor-display runtime scope. Heals Edge kiosk relaunch ' 'gea-shopfloor-display runtime scope. Heals Edge kiosk relaunch '
'policy drift and dispatches the kiosk to the subtype target. No ' 'policy drift and dispatches the kiosk to the subtype target. No '
'EXE payloads: kiosk engine and browser are baked into the display ' 'EXE payloads: kiosk engine and browser are baked into the display '
'image. The client merges the fleet-wide common scope underneath ' 'image. Self-sufficient: displays do NOT inherit the common '
'this one at fetch time.'), 'scope.'),
'Applications': applications, 'Applications': applications,
} }

View File

@@ -21,7 +21,8 @@ def test_seed_creates_display_scope(db):
scope = ManifestScope.query.filter_by( scope = ManifestScope.query.filter_by(
scopename=SCOPE_NAME, phase='runtime').first() scopename=SCOPE_NAME, phase='runtime').first()
assert scope is not None assert scope is not None
# Not the common scope: the client merges common underneath at fetch time. # A plain runtime scope, not the common scope. Displays are self-sufficient
# and do NOT inherit common.
assert scope.iscommon is False assert scope.iscommon is False
# Four Registry drift-heal entries + one PS1 dispatcher, in order. # Four Registry drift-heal entries + one PS1 dispatcher, in order.