Captures the full picture of how the manifest engine works, why scripts don't need self-heal entries (run from share), credential context (SYSTEM = computer account, requires Mount-SFLDShare for file-level reads), C:\Enrollment vs SFLD share copy distinction, and update workflows. Written in response to a session that wasted time adding redundant manifest entries because this wasn't documented. Companion to scripts/diagnostics/Capture-LockdownState.ps1 and the auditing script in pxe-images/Audit-SFLDShare.ps1.
15 KiB
GE-Enforce v2 architecture
Ongoing fleet enforcement layer for GE Aerospace shopfloor PCs. Replaces the
v1 Machine-Enforce.ps1 + main\machineapps\machineapps-manifest.json
arrangement that DSC-based deployment retired.
What it does
On every user logon (and on a periodic schedule), every shopfloor PC mounts
the SFLD share with credentials provisioned by Azure DSC, reads the
manifest(s) for its PC type, and runs Install-FromManifest.ps1 to install,
update, or repair anything whose detection rule fails. The detection-driven
design means a freshly-imaged PC in a known-good state no-ops; an out-of-
date or tampered PC self-heals on the next cycle.
The same entry-points handle:
- Initial shopfloor app install on first logon after PXE imaging.
- Drift correction (e.g. user uninstalled UDC, vendor patch tampered with a config file, manifest version bumped fleet-wide).
- One-shot per-bay actions like UDC data restore from a swap-source PC.
Components
| Piece | Lives at | Role |
|---|---|---|
GE-Enforce.ps1 |
playbook/shopfloor-setup/common/GE-Enforce.ps1 |
Logon dispatcher. Mounts share, finds manifests, calls Install-FromManifest. |
Register-GEEnforce.ps1 |
playbook/shopfloor-setup/common/Register-GEEnforce.ps1 |
Registers the scheduled task (GE Shopfloor Machine Apps Enforce) at imaging time. |
Install-FromManifest.ps1 |
playbook/shopfloor-setup/common/lib/Install-FromManifest.ps1 |
Manifest interpreter. Iterates entries, runs detection, fires installers. |
Mount-SFLDShare |
playbook/shopfloor-setup/Shopfloor/lib/Restore-EDncReg.ps1 |
Reads creds from HKLM:\SOFTWARE\GE\SFLD\Credentials\* and net uses the share with the SFLD user identity. |
| Manifests | SFLD share <scope>\manifest.json |
Declarative list of entries to enforce. |
| Site config | playbook/shopfloor-setup/site-config.json (deployed to C:\Enrollment\site-config.json) |
Per-PC-type share path overrides + startup/desktop/taskbar layout. |
Lifecycle
Imaging time (PXE)
startnet.cmd xcopies shopfloor-setup -> W:\Enrollment\
Run-ShopfloorSetup.ps1 runs the imaging-phase scripts
Register-GEEnforce.ps1 registers the scheduled task
First user logon (post-imaging, post-SFLD-DSC creds delivered)
Scheduled task fires GE-Enforce.ps1
GE-Enforce reads HKLM:\SOFTWARE\GE\SFLD\Credentials -> Mount-SFLDShare W:
Reads pc-type from C:\Enrollment\pc-type.txt
For each scope (common, then <pctype>):
Reads <scope>\manifest.json
Calls Install-FromManifest.ps1 -ManifestPath ... -InstallerRoot W:\<scope>
Install-FromManifest iterates each Application entry:
Apply PCTypes filter (skip if entry doesn't apply to this PC)
Apply TargetMachineNumbers filter (skip if applicable)
Run Detection - if installed at expected version/hash/state, skip
Else run installer per Type (MSI/EXE/CMD/PS1/File/INF/Registry)
Log result to C:\Logs\Shopfloor\enforce-YYYYMMDD.log
Unmounts W:
Every subsequent logon
Same dispatcher fires. No-ops on already-correct state. Self-heals drift.
SFLD share layout (v2)
\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\
_meta\
README.md layout doc
manifest-schema.json JSON schema for entries
history\ timestamped backup of each manifest on push
_outputs\
ntlars-backups\ eDNC INI backups per machine number
logs\ client-uploaded diagnostics
common\ applies to every PC type
manifest.json
apps\ MSI / EXE / CMD installers
configs\ data files (XML, fonts, txt)
scripts\ PS1 / BAT / CMD helpers
standard-machine\ Standard PC type, subtype Machine
manifest.json
apps\ configs\ scripts\
cmm\ display\ genspect\ keyence\ lab\ waxandtrace\
per-PC-type
_meta, _outputs, and per-PC-type dirs each have apps/, configs/,
scripts/ siblings to manifest.json, referenced by relative paths in manifest
entries (Source: scripts/Foo.ps1 => <scope>\scripts\Foo.ps1).
v1 vs v2
v1 used \shared\dt\shopfloor\main\machineapps\machineapps-manifest.json as
the single Standard-Machine manifest. v2 reorganized to per-PC-type
manifests so CMM/Keyence/Display PCs get their own scope without filtering
the whole fleet manifest by PCTypes. The v1 path may still exist on the
share for backwards compat; v2 GE-Enforce ignores it. Do not edit the v1
manifest for v2-deployed sites.
Manifest entry schema
{
"Version": "2.0",
"_comment": "...",
"Applications": [
{
"_comment": "Per-entry context: install path, version pin reasons, rotation procedure",
"Name": "<unique label, also shown in log>",
"Type": "<MSI|EXE|CMD|PS1|File|INF|Registry>",
"Installer": "<path-relative-to-scope>",
"Source": "<for Type=File: path-relative-to-scope>",
"Destination": "<for Type=File: absolute disk path>",
"Script": "<for Type=PS1: path-relative-to-scope>",
"Args": "<optional: extra args>",
"InstallArgs": "<optional: msiexec/exe args>",
"DetectionMethod": "<File|Registry|FileVersion|Hash|MarkerFile|ValueMatches|Always>",
"DetectionPath": "<absolute disk path, or HKLM:\\... for Registry>",
"DetectionName": "<for Registry: value name>",
"DetectionValue": "<expected value or hash>",
"PCTypes": ["Standard", "CMM", "..."],
"PCSubTypes": ["Machine"],
"TargetMachineNumbers": ["1234", "5678"]
}
]
}
Type semantics (the part that's easy to get wrong)
| Type | What it does | Local artifact? | Self-heal pattern |
|---|---|---|---|
MSI |
msiexec /i <share path> + InstallArgs |
Vendor footprint (uninstall reg key, files) | Detection on uninstall reg DisplayVersion or product File |
EXE |
Direct exec of installer EXE | Vendor footprint | Same |
CMD |
cmd /c <share path> (a wrapper script) |
Whatever the wrapper deploys | Same |
PS1 |
powershell.exe -File <share path> - executes from share, no local copy |
None - runs in place | NOT NEEDED. Update share file = next logon runs new code. No Hash entry required because there's nothing on disk to drift. |
File |
Copy-Item -Source <share> -Destination <disk> |
The file at Destination | Hash detection IS needed. Drift = re-copy. |
INF |
pnputil /add-driver |
Driver staged | Detection via PnP |
Registry |
Detection-only (no install side; entry is purely a no-op trigger) | Whatever wrote the key | n/a |
Type=PS1 is for scripts that need to RUN every cycle, not be DEPLOYED.
Common pattern: DetectionMethod: Always (always fires). Used for one-shot
cleanups (e.g. Restore-UDCData.ps1 consumes a swap backup and self-no-ops
when none is waiting).
If you want a PS1 to ALSO be deployed to disk somewhere, that's a separate
Type=File entry copying it - very rarely useful since GE-Enforce already
runs it from the share.
Detection methods
File-Test-PathonDetectionPath. OptionalDetectionNamefor property.Registry-Test-Pathon registry key. WithDetectionName+DetectionValue, must match.FileVersion- PE FileVersion ofDetectionPathmatchesDetectionValueexactly.Hash- SHA256 ofDetectionPathmatchesDetectionValue. Case-insensitive.MarkerFile-Test-Pathon a marker file (engine creates it after a successful PS1 run). For PS1 entries that should NOT re-fire after one success.ValueMatches- Registry value exact match.Always- Never matches; entry fires every cycle. For per-cycle cleanup/check scripts.
Filters
PCTypes- applies only ifC:\Enrollment\pc-type.txtmatches one of the listed values. Skips otherwise.PCSubTypes- same, againstC:\Enrollment\pc-subtype.txt.TargetMachineNumbers- applies only if localudc_settings.jsonMachineNumber is in the list. Used for variant-specific (Fanuc/Okuma/Makino) entries.
Credential context (the trap that bit us 2026-05-01)
GE-Enforce runs as NT AUTHORITY\SYSTEM (scheduled task at startup +
logon). SYSTEM authenticates to remote SMB as the computer account
(DOMAIN\HOSTNAME$), not as a user.
The SFLD share's ACL grants top-level enumeration to authenticated computers
but file-level reads only to a specific SFLD user. So Test-Path on a UNC
path from SYSTEM:
- Top-level dir like
\\tsgwp00525...\shared\dt\shopfloor\backup\udc\-> True - Bay-level subdir like
...\backup\udc\3207\-> True (depending on ACL) - File-level like
...\backup\udc\3207\CurrentData.json->$false, indistinguishable from "file not found"
Symptom: scripts that Test-Path raw UNC paths from SYSTEM context silently
log "absent" / "no work" while the files actually exist. Real cause is
access denied returning False. Failure mode is invisible.
Always use Mount-SFLDShare (in Shopfloor\lib\Restore-EDncReg.ps1)
before file access. It reads creds from HKLM:\SOFTWARE\GE\SFLD\Credentials\*
and net use W: \\... /user:<sflduser> <pw> /persistent:no. Subsequent
file operations on W:\... succeed.
If creds are missing in the registry (e.g. SFLD-DSC bootstrap didn't run),
Mount-SFLDShare returns $false. Scripts should fail-fast with a clear
ERROR rather than continue with raw-UNC access.
C:\Enrollment\shopfloor-setup\ vs SFLD share
Two separate copies of overlapping content with different roles:
| Path | Source | Used by | Updated when |
|---|---|---|---|
C:\Enrollment\shopfloor-setup\ |
PXE imaging copy from \\10.9.100.1\enrollment\shopfloor-setup\ |
Imaging-flow scripts: Run-ShopfloorSetup.ps1, Stage-Dispatcher.ps1, Set-MachineNumber.ps1 -> Update-MachineNumber.ps1 |
Re-image only |
SFLD share \<scope>\ |
Direct upload | GE-Enforce.ps1 / Install-FromManifest.ps1 (every logon) | Direct file upload to share |
Implication for hot-fixing scripts: a fix to Restore-UDCData.ps1 needs to
land on the SFLD share (standard-machine/scripts/Restore-UDCData.ps1) for
the manifest engine to pick it up. Updating only the PXE enrollment share
helps NEW imaging but not already-imaged PCs.
site-config.json paths
Defines per-PC-type share roots used by Update-MachineNumber.ps1 and Backup-UDCData.ps1 logic outside the manifest engine:
Field (under pcProfiles.Standard-Machine) |
Used for |
|---|---|
machineappsSharePath |
v1 legacy machineapps share (mostly unused in v2) |
ntlarsBackupSharePath |
per-machine .reg backups (eDNC) |
udcBackupSharePath |
per-bay live UDC data backup (CurrentData.json + ArchivedData/) |
udcSettingsSharePath |
per-bay udc_settings_<n>.json (UDC settings overrides) |
These paths are read by Update-MachineNumber.ps1 placeholder->real transition logic to mount the right share + restore the right data when a tech sets a real machine number on a previously-9999 PC.
Update workflow
Bumping a vendor app version
- Drop new
.msi/.exein<scope>\apps\on the share. - Update the matching manifest entry:
Installerfilename +DetectionValue. - Save
<scope>\manifest.json. - Optional: copy current manifest to
_meta\history\<date>-<scope>.json. - Every PC of that scope picks up the change next logon.
Rotating a deployed config (eMxInfo.txt, udc_webserver_settings.json)
- Overwrite the file in
<scope>\configs\on the share. - Recompute SHA256:
(Get-FileHash .\udc_webserver_settings.json -Algorithm SHA256).Hash - Paste into the manifest entry's
DetectionValue. - Save manifest.
Hot-fixing a script (Restore-UDCData.ps1, etc.)
- Patch script in repo.
- Copy to
<scope>\scripts\<name>.ps1on the SFLD share. - (No manifest edit needed -
Type=PS1runs from share.) - Every PC of that scope runs the new version next logon.
Adding a new entry
- Decide scope: cross-PC-type goes in
common/, PC-type-specific in<pctype>/. Verify the entry isn't already present in either. - Pick
Typebased on what the entry is: vendor installer, config file deploy, or per-cycle script run. - For
Filetype: include source on the share; pickDetectionMethod=Hashwith a precomputed SHA256 so drift triggers re-deploy. - For
PS1type: place script in<scope>\scripts\; useDetectionMethod=Alwaysfor per-cycle, orMarkerFilefor one-shot. - For installer types: provide
Installerpath,Type,InstallArgs, detection rule that distinguishes "installed at this version" from "missing or wrong version". - Add a
_commentfield documenting WHY this entry exists, where the target installs, and how to rotate the version. - Save + push.
Rolling back
History snapshots live at _meta\history\<date>-<scope>.json. Manual revert:
copy old snapshot back over <scope>\manifest.json. Engine picks it up next
cycle.
Logs
Per-PC log file: C:\Logs\Shopfloor\enforce-YYYYMMDD.log
Format:
[YYYY-MM-DD HH:MM:SS] [INFO] Mounted \\... as W:
[YYYY-MM-DD HH:MM:SS] [INFO] standard\manifest.json not on share - no type-specific apps
[YYYY-MM-DD HH:MM:SS] [INFO] ---- Processing scope: common ----
[YYYY-MM-DD HH:MM:SS] [INFO] Manifest lists N app(s)
[YYYY-MM-DD HH:MM:SS] [INFO] ==> Adobe Acrobat Reader DC
[YYYY-MM-DD HH:MM:SS] [INFO] Already installed at expected version - skipping
[YYYY-MM-DD HH:MM:SS] [INFO] ==> WJF Defect Tracker
[YYYY-MM-DD HH:MM:SS] [INFO] msiexec: W:\common\apps\WJF_Defect_Tracker.msi
[YYYY-MM-DD HH:MM:SS] [INFO] verbose log: C:\Logs\Shopfloor\msi-WJF_Defect_Tracker.log
[YYYY-MM-DD HH:MM:SS] [INFO] Exit 0 - SUCCESS
Per-installer logs go alongside (msi-*.log, per-script transcripts).
PS1 entry stdout/stderr surfaces in the main enforce log.
If detection succeeds first time on a freshly-imaged PC, every entry logs "Already installed at expected version - skipping" - the no-op path proves the manifest is in sync with the image.
Common audit failures and their cause
| Symptom | Likely cause |
|---|---|
<vendor> MSI redeploys every cycle |
Detection rule's path or DetectionValue doesn't match the actual install footprint. Verify PE FileVersion is 4-part vs DisplayVersion is 3-part. Verify 32-bit installers go under WOW6432Node. |
| Script logs "absent" or "no work" repeatedly while file is visible interactively | Running as SYSTEM with raw-UNC access. Switch to Mount-SFLDShare. |
| New script change doesn't take effect | Edited the wrong copy. C:\Enrollment\ is imaging-time; SFLD share is runtime. |
| Manifest entry skipped silently | PCTypes / PCSubTypes filter excludes this PC. Check C:\Enrollment\pc-type.txt. |
| Two PCs racing for the same per-bay backup | Both have the same udc_settings.json MachineNumber. Find + decommission the second one. |
See also
playbook/shopfloor-setup/_meta/README.md(on the SFLD share local mirror) - canonical share layout docpxe-images/Audit-SFLDShare.ps1- audit script that walks the share and validates layout + canonical hashes + detection-rule sanityplaybook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1- imaging-time progress monitor that watches for the upstream signals (SFLD reg key, DSCDeployment.log, Consume Credentials task) before GE-Enforce can even start