Proposal: fold in what a day of fixing the current pipeline taught

New section 9a - five design rules, each from a bug fixed on 2026-08-06 and each
cheap to honour in a new design but expensive to retrofit:

  Absent and empty are different. Four scripts tested config as
  "not null AND Count > 0", so an explicitly empty list fell through to a
  hardcoded default - a kiosk configured with "desktopApps": [] was given the
  full machine-tool set. Configuring "none" produced "everything".

  Deny by default. preinstall.json had 16 of 21 entries at PCTypes ['*'] and the
  SMB manifest 18 of 25 unfiltered. Both filters worked perfectly; they just were
  not applied.

  Gate at the point of action. Call-site gates on PC type were bypassed by the
  finalization phase, silently.

  The image carries configuration. WJ Shopfloor.lnk is inside the WIM, so no
  pipeline change removes it.

  The ppkg is a hard boundary. Chrome, RealVNC, Tanium, CyberArk and the rest come
  from the package - thinning the image does not thin them.

Also corrects a premise in section 4.2: a first-boot selection UI cannot assume
the API is reachable, because ShopDB needs the AESFMA wifi SSID and the imaging
LAN has no route to it. Points at Install-ShopdbKiosk-WhenOnline.ps1 as the
reusable shape - arm a task at imaging, do nothing until the API answers, act
once, verify, self-remove.

Plus two risks, the diagnostics collector in the reference map, and a gotchas
section: HardwareDriver.json defeats ConvertFrom-Json on every PowerShell
version, Compress-Archive caps at 2 GB against an 8 GB package, and PowerShell
Core normalises backslashes on Linux so bay scripts can be tested here.
This commit is contained in:
cproudlock
2026-08-06 17:36:17 -04:00
parent 0e1b80b903
commit 6d5fee786c

View File

@@ -114,6 +114,20 @@ Shopfloor -> Collections | No Collections | Common | Heattreat | Part Marker
The tool selection is the important half: it drives PC-DMIS version, FormTracePak The tool selection is the important half: it drives PC-DMIS version, FormTracePak
version, DODA flag, NTLARS/eDNC registry, UDC settings, machine number. version, DODA flag, NTLARS/eDNC registry, UDC settings, machine number.
**The network constraint that shapes this.** ShopDB is only reachable once the
bay has joined the **AESFMA wifi SSID** - not on the imaging LAN, and not
necessarily at first boot. So a first-boot selection UI cannot assume the API is
there. It must either work offline and sync later, or wait. Confirmed on
579C144: the bay held `172.16.9.81` and `172.24.19.142`, neither in the
production ranges, and `sync_intune` sat retrying every 30 seconds.
**The pattern that solves it, already built and worth reusing.**
`Install-ShopdbKiosk-WhenOnline.ps1` is a SYSTEM task armed at imaging that does
nothing until ShopDB answers, then acts once, verifies, and unregisters itself.
Any step in the new architecture that needs the API - selection sync, config
pull, enrollment completion - can use the same shape rather than assuming
connectivity.
**Design points:** **Design points:**
- **Server is the source of truth.** The selection POSTs to ShopDB keyed on - **Server is the source of truth.** The selection POSTs to ShopDB keyed on
@@ -401,6 +415,78 @@ silently. A clean slate is the moment to fix that. See `docs/OWNERSHIP.md`.
--- ---
## 9a. Design rules learned the hard way (2026-08-06)
A day of fixing the current pipeline produced five rules. They are cheap to
honour in a new design and expensive to retrofit.
### Absent and empty are different
Four separate scripts tested config as
`$null -ne $cfg -and $cfg.Count -gt 0`, so an explicitly empty list fell through
to a hardcoded default. A Display kiosk configured with
```json
"desktopApps": [], "startupItems": [], "taskbarPins": []
```
was given UDC, eDNC, NTLARS, WJ Shopfloor, Defect Tracker and Plant Apps -
**configuring "none" produced "everything"**. Plant Apps launched
`msedge --new-window` on a screen with no keyboard.
Measured, not assumed: an empty JSON array round-trips as `Object[]` with
`Count = 0`, and an absent key as `$null`. They are distinguishable; the code
simply conflated them.
*Rule: in the API model, "no items" and "not configured" must be different
values on the wire, and the default for an unrecognised or missing scope is
NOTHING, not a fallback set.*
### Deny by default, always
`preinstall.json` had 16 of 21 entries at `PCTypes: ["*"]`, so a kiosk installed
Adobe, OpenText, Defect Tracker, the serial drivers and twelve legacy VC++
redistributables. The SMB manifest had 18 of 25 entries unfiltered. In both
cases the filter worked perfectly - it simply was not applied.
*Rule: every payload declares which PC types it is for. An undeclared payload
installs nowhere and fails validation, rather than installing everywhere.*
### Gate at the point of action, not the call site
`Run-ShopfloorSetup.ps1` gated both the S: mapper and the machine-number prompt
on PC type. Both registered anyway on a Display, with no "Skipping" line in the
log - something in the finalization phase reaches those registrars past the
call-site gate. The fix was to gate inside each registrar.
*Rule: authority checks belong with the thing being done. A caller-side check is
a convenience, never the control.*
### The image itself carries configuration
`WJ Shopfloor.lnk`, dated April, sits in the all-users Startup folder of every
bay - **inside the WIM**. No script change removes it, and no amount of thinning
the pipeline touches it.
*Rule: "thin image" must include auditing what is baked into the WIM, not just
what the pipeline adds afterwards.*
### The provisioning package is a hard boundary
These are installed by the ppkg, not by anything PXE controls:
```
Google Chrome (+2 updater tasks) RealVNC Tanium
CyberArk EPM .NET 3.5 Report IP Sysinternals Autologon DSC
PowerShell 7 x64 AND x86
```
*Rule: thinning the image does not thin the package. Anything that must not be
on a kiosk and comes from the ppkg needs a package change or an explicit
post-install removal - it cannot be solved by scoping our own payloads.*
---
## 10. Phased delivery ## 10. Phased delivery
Each phase is useful standing alone. No phase requires the next. Each phase is useful standing alone. No phase requires the next.
@@ -454,6 +540,8 @@ Each phase is useful standing alone. No phase requires the next.
| Re-implementing PESetup | ongoing MCL format tracking + a likely compliance conversation | scope it against "one click per build" - zero-touch is the only thing that needs it | | Re-implementing PESetup | ongoing MCL format tracking + a likely compliance conversation | scope it against "one click per build" - zero-touch is the only thing that needs it |
| Big payloads over the corporate network | PC-DMIS, FormTracePak ISOs (~2 GB each), Keyence | GE-Enforce supports `smb` / `http` / `inline` per entry - decide per payload | | Big payloads over the corporate network | PC-DMIS, FormTracePak ISOs (~2 GB each), Keyence | GE-Enforce supports `smb` / `http` / `inline` per entry - decide per payload |
| Three images drift apart | drift caused the 2026-08-06 outage | prefer one image; `share-drift.py` guards what remains | | Three images drift apart | drift caused the 2026-08-06 outage | prefer one image; `share-drift.py` guards what remains |
| API unreachable at first boot | ShopDB needs AESFMA; the imaging LAN cannot reach it | wait-for-network task pattern (§4.2); never assume connectivity |
| ppkg-delivered apps on a kiosk | Chrome, RealVNC, Tanium etc. are outside PXE control | package change or explicit post-install removal - scoping our payloads cannot fix it |
--- ---
@@ -490,6 +578,7 @@ Each phase is useful standing alone. No phase requires the next.
| `scripts/lint-unattend.py` | schema limits: Path 259, CommandLine 1024, Description 256 | | `scripts/lint-unattend.py` | schema limits: Path 259, CommandLine 1024, Description 256 |
| `scripts/lint-driver-catalogue.py` | reimplements `GetDriverByModel` | | `scripts/lint-driver-catalogue.py` | reimplements `GetDriverByModel` |
| `scripts/share-drift.py` | repo vs live share, git-owned vs unreconciled | | `scripts/share-drift.py` | repo vs live share, git-owned vs unreconciled |
| `playbook/shopfloor-setup/Collect-ImagingDiagnostics.ps1` | one-pass bay diagnostics - identity, enrollment, installed apps, all four autostart surfaces, GE-Enforce reachability |
| `scripts/build-pctype-media.py` | per-PCTYPE media views (retired by Phase 6) | | `scripts/build-pctype-media.py` | per-PCTYPE media views (retired by Phase 6) |
### 13.2 PXE server (`pxe@172.16.9.1`) ### 13.2 PXE server (`pxe@172.16.9.1`)
@@ -549,6 +638,20 @@ C:\Windows\Panther\ setupact.log, setuperr.log, unattend.xml, PESetu
C:\Windows\Panther\UnattendGC\ oobeSystem pass C:\Windows\Panther\UnattendGC\ oobeSystem pass
``` ```
### 13.5a Gotchas any new tooling will hit
- **`HardwareDriver.json` cannot be read by `ConvertFrom-Json`.** It carries both
casings of the same fields (`fileName`/`FileName`, `destinationDir`/
`DestinationDir`) and PowerShell rejects the document - on 5.1 *and* 7.
`-AsHashtable` is PS6+ and WinPE runs 5.1. Extract fields by regex, or parse in
Python where duplicate keys are tolerated.
- **`Compress-Archive` caps at 2 GB**, and `run-enrollment` harvests the 8 GB
provisioning package into `C:\Logs\PPKG`, so any log collection must exclude
`*.ppkg`.
- **Backslash-vs-forward-slash**: PowerShell Core normalises `\` to `/` on Linux,
so `Join-Path` with Windows separators works cross-platform - handy for testing
bay scripts on the dev box.
### 13.6 How to re-derive things ### 13.6 How to re-derive things
```bash ```bash