Files
shopdb-flask/docs/proposals/ge-enforce-plugin.md
cproudlock 1672e349e5
All checks were successful
CI / backend (push) Successful in 1m25s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
Collector auto-links measuring tools for metrology PCs; settings rail cleanup
Metrology PCs (CMM, Keyence, Genspect, wax-and-trace imaging pc-types) drive
an attached measuring instrument. The PC itself stays a shopfloor PC, but the
collector now models the instrument:

- New METROLOGY_TOOL_MAP (pctypemap.py) maps those pc-types to a
  MeasuringToolType (CMM, Vision System, Genspect, Form Tracer).
- ComputersPlugin._sync_measuringtool_link creates the MeasuringTool asset
  once and a directional PC->tool "controls" relationship, tagged
  collector:measuringtool. Idempotent (re-push reuses, no duplicate asset) and
  self-archiving (a PC re-imaged to a non-metrology type deactivates the link
  but keeps the asset and any calibration history). Mirrors the printer-link
  pattern. The MeasuringToolType is created on demand if not seeded.
- 4 tests: create+link, idempotent re-push, non-metrology skip, repurpose
  archives. Non-metrology PCs never warn about a missing controls type.

Settings rail cleanup:
- Collapsible groups so the 13-group rail fits without scrolling (1511px ->
  488px). The group containing the current page expands; the rest collapse.
  CSS-drawn caret (ASCII source, no Unicode). Empty groups never render, in
  both the rail and the landing page.
- Measuring Tools group placed with the other asset groups (right after
  Machines) instead of appended last; empty placeholder positions the
  plugin-contributed cards.
- Operating Systems moved from PCs to General Reference: OS is cross-asset
  (PCs, machines, measuring tools, network devices all run one).

Plus docs/proposals/ge-enforce-plugin.md: a planning doc for refactoring
GE-Enforce/DSC into a shopdb plugin (manifest as shopdb data, payloads on
SMB/HTTP/inline), grounded in the real manifest schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 15:29:08 -04:00

17 KiB

Proposal: GE-Enforce as a shopdb plugin

Status: DRAFT / planning only. Not accepted, not built. Author: planning session 2026-07-12.

1. What this is

Today GE-Enforce is a PowerShell manifest engine that reads per-PC-type manifest.json files off an SMB share (\\tsgwp00525.wjs.geaerospace.net\ shared\dt\shopfloor\). Each logon, a scheduled task running as SYSTEM mounts the share, reads the manifest for the machine's PC type, and installs or self-heals apps, files, drivers, registry values, and scripts. A parallel preinstall.json runs the same schema once at imaging.

This proposal turns the manifest into shopdb data: the authoritative manifest lives in the shopdb database, is edited through the shopdb UI (an expansion of /settings/pctypemapping), and is served to clients over HTTP as JSON. The payloads (MSI/EXE/PS1/config bytes) stay on SMB, on HTTP, or both, referenced by URL/path from the manifest rows. GE-Enforce.ps1 changes from "read a file on W:" to "GET a manifest from shopdb, then fetch each payload from wherever the row says."

The result: managing imaging PC types, their apps, scripts, files, registry rules, and version gates becomes a first-class shopdb feature instead of hand- edited JSON on a file share.

2. Why it fits shopdb

  • shopdb already models the fleet (the collector ingests every PC's hostname, pctype, installed software, versions). Making shopdb also own what SHOULD be installed closes the loop: desired-state (manifest) and observed-state (collector) live in one system and can be diffed.
  • /settings/pctypemapping already maps gea-shopfloor-* PC types to ComputerType. That page becomes the entry point for full imaging-PC-type management.
  • The plugin contract (per-plugin models, migrations, API prefix, settings cards, collector hooks) is exactly the shape this needs.
  • ADR-004 (per-site instances) matches: each site's shopdb owns each site's manifest. No multi-tenant complication.

3. Grounding: the real manifest schema

Source of truth for these field names (do not invent others):

  • Schema: pxe-images/tsgwp00525-v2/shared/dt/shopfloor/_meta/manifest-schema.json
  • Engine: pxe-images/common/lib/Install-FromManifest.ps1
  • Dispatcher: .../shopfloor/common/GE-Enforce.ps1
  • Architecture: pxe/docs/ge-enforce-v2-architecture.md

A manifest is { "Version": str, "_comment": str, "Applications": [entry, ...] }. Only Name and Type are required per entry.

Per-entry fields (complete set)

Identity / action:

  • Name (required, unique, also the status-key <scope>/<Name>)
  • Type (required): one of MSI EXE CMD BAT PS1 INF File Registry
  • _comment (documentation, heavily used in practice)

Type-specific payload references (sparse; depends on Type):

  • MSI/EXE/CMD/BAT/INF: Installer (relative path) + InstallArgs
  • PS1: Script (relative path, falls back to Installer) + Args
  • File: Source (relative) + Destination (absolute on-PC path)
  • Registry: RegPath + RegName + RegValue + RegType (RegType in String DWord QWord MultiString ExpandString Binary)
  • Optional LogFile, WaitTimeoutSec (EXE hang kill), InUseCheck

Detection (decides whether the action fires / self-heals):

  • DetectionMethod: one of Registry File FileVersion Hash MarkerFile ValueMatches pnputil Always
  • DetectionPath, DetectionName, DetectionValue, DetectionPattern
  • Note: DetectionValue is method-dependent - SHA256 for Hash, a 4-part version for FileVersion, a registry value for Registry, ignored for Always/File. Same column, different meaning per method.
  • No DetectionMethod = always installs.

Targeting filters (all ANDed; each is multi-value):

  • PCTypes (array; "*" = all; alias graph expands old<->new names)
  • PCSubTypes / subtype via <pctype>-<subtype> values
  • TargetHostnames (array; exact + -like WJS-* wildcards)
  • TargetMachineNumbers (array; per-bay)
  • _CmmVersion (scalar; per-entry PC-DMIS version gate, needs lib >= 2.6)

Nested:

  • InUseCheck: { Behavior, Processes: [{Name, ExePath, GracefulCloseTimeoutSec}] } Behavior in Defer CloseAndReopen ForceClose ScheduleForReboot

Parsed-but-inert today (model them, mark inert):

  • ApplyMode (Nightly Immediate ImmediateReboot), UpdateWindow (HH:MM-HH:MM)

Preinstall-only extras (phase discriminator):

  • PreEnrollment, KillAfterDetection, PCTypesStrict, _pcTypesNote

Load-bearing behaviors the model must preserve

  1. Array order IS execution order. Config-restore entries are deliberately placed AFTER their vendor installer so a mid-cycle overwrite heals the same cycle (eMxInfo.txt after eDNC; udc_webserver_settings after UDC). We MUST store an explicit per-scope sortorder, not a set.
  2. PCTypes alias graph is many-to-many old<->new names resolved by set intersection, with a PCTypesStrict escape hatch. Not a simple FK.
  3. Polymorphic entry by Type - sparse column set per type. Model as single-table with nullable columns, or a typed-payload child. STI is simpler.
  4. Two manifest phases - runtime (self-heal, per logon) and preinstall (once at imaging) share the schema. One table with a phase discriminator.

4. Data model (new geenforce plugin)

Per-plugin Alembic chain (ADR-008). Tables (lowercase concatenated per naming convention):

  • manifestscopes - one row per imaging PC type / scope.

    • scopeid PK
    • scopename (e.g. gea-shopfloor-cmm; unique)
    • computertypeid FK -> computertypes (this REPLACES the thin pctypemap_<pxetype> setting; the mapping becomes a column here)
    • phase enum (runtime | preinstall)
    • manifestversion (string, mirrors manifest Version)
    • description, isactive
    • iscommon bool (the common/ fleet-wide scope)
  • manifestentries - one row per Applications[] entry.

    • entryid PK, scopeid FK
    • sortorder int (preserves array order; the ordering contract)
    • name, entrytype (MSI/EXE/.../Registry), comment
    • payload columns (nullable, per type): installer, installargs, scriptpath, scriptargs, sourcepath, destination, regpath, regname, regvalue, regtype
    • payloadsource enum (smb | http | inline) + payloadref (see section 5)
    • detection columns: detectionmethod, detectionpath, detectionname, detectionvalue, detectionpattern
    • gates: cmmversion, plus child tables for the multi-value filters
    • control: logfile, waittimeoutsec, applymode, updatewindow
    • preinstall flags: preenrollment, killafterdetection, pctypesstrict
    • isactive
  • manifestentrypctypes, manifestentryhostnames, manifestentrymachinenumbers

    • child rows for the ANDed multi-value filters (one value per row, wildcards stored verbatim as patterns)
  • manifestinusechecks + manifestinusecheckprocesses

    • the nested InUseCheck object and its Processes[] child list
  • pctypealiases - the old<->new name alias graph (seed from Install-FromManifest.ps1:463-475), so the server can resolve/validate PCTypes the same way the engine does.

The JSON the client receives is REBUILT from these rows in exact array order. It must byte-for-byte match what the current engine expects (round-trip tested against the live manifests).

5. Payloads: SMB and/or HTTP (both supported)

The user asked whether payloads can be SMB and/or HTTP. Yes - per entry:

  • payloadsource = smb: payloadref is the current relative path (apps/eDNC_6-4-5.msi); the client resolves it against the mounted scope root exactly as today. Zero client behavior change for these rows. This is the default and the migration target for large binaries (MSIs are hundreds of MB; SMB streaming beats HTTP for those).
  • payloadsource = http: payloadref is a URL (absolute, or relative to a configured payload base). The client downloads to a local temp dir, verifies the Hash/FileVersion detection value, then runs it. Good for small config/script payloads and for sites with no SMB share.
  • payloadsource = inline: for small text payloads (a .ps1, a config file, a registry value), the bytes live in shopdb itself and are served in-band. No external store at all. Best for scripts and File-type config drops.

Manifest generation emits, per entry, whatever the client needs to fetch the bytes. The engine's existing "stage network EXE to local temp first" logic (SYSTEM access-denied workaround) generalizes cleanly to HTTP download.

Hashing stays the integrity mechanism regardless of transport: DetectionValue carries the SHA256, the client verifies after fetch. This is the security- critical bit - shopdb serves a manifest that says "install X with hash H"; a tampered payload from any transport fails the hash and does not run.

6. API surface (/api/geenforce/...)

Admin CRUD (gated by a new geenforce.manage permission via the plugin's get_permissions() hook):

  • GET/POST /scopes, GET/PUT/DELETE /scopes/<id> - imaging PC types
  • GET/POST /scopes/<id>/entries, PUT/DELETE /entries/<id> - manifest entries
  • PUT /scopes/<id>/entries/reorder - the ordering contract, drag-to-reorder
  • POST /entries/<id>/payload - upload an inline/http payload (multipart), compute + store its SHA256 into detectionvalue
  • GET /scopes/<id>/preview - the exact JSON a client would receive (for review before publish)

Client-facing (gated by a collector-style service token, geenforce.fetch scope, reusing the PAT + X-API-Key machinery already built for the collector):

  • GET /manifest?pctype=<scope>&subtype=<s>&hostname=<h>&machinenumber=<n> Returns the assembled manifest JSON for that machine. The server can pre-apply the PCTypes/hostname/machinenumber/cmmversion filters (thin client) OR return the full scope and let the engine filter (fat client, matches today). Start fat: return the scope manifest unchanged so the engine logic is untouched.
  • Payload fetch for http/inline rows: GET /payload/<entryid> streaming the bytes with the right content type.

7. Frontend: expand /settings/pctypemapping

The current page (PCTypeMappingSettings.vue, "Collector PC Types") is a read- only-ish table of pxetype -> ComputerType dropdowns. It grows into the imaging- PC-type manager:

  • Scopes list: add/rename/delete imaging PC types; each still carries its ComputerType mapping (that column moves from a setting into manifestscopes). A phase toggle (runtime vs preinstall). Common scope flagged.
  • Scope detail / manifest editor: an ordered, drag-reorderable list of entries (the ordering contract made visible). Each entry is a typed form - the visible fields switch on entrytype (MSI shows Installer+InstallArgs; PS1 shows Script+Args; File shows Source+Destination; Registry shows the Reg* quartet). Detection block with a method dropdown that reveals only the relevant Detection* fields. Filter chips for PCTypes/hostnames/machine numbers. InUseCheck sub-editor. Payload source selector (smb/http/inline) with upload for the latter two.
  • Preview + publish: show the generated JSON; "publish" bumps manifestversion and writes a history snapshot (mirrors the current _meta/history/<date>-<scope>.json backups).
  • Desired vs observed: because the collector already reports installedversions, the scope page can show, per entry, how many fleet PCs match the expected detection value. This is the payoff of unifying the two.

This is an ADR-010 settings card contributed by the geenforce plugin, so it only appears when the plugin is enabled.

8. Client change (minimal, staged)

GE-Enforce.ps1 today: mount W:, read <scope>\manifest.json, hand to Install-FromManifest. New path: GET the manifest from shopdb, write it to the same local location the engine reads, then run the engine unchanged. That is the smallest possible client delta - the engine, detection logic, self-heal, and SMB payload resolution all stay identical. Only the source of the JSON moves from file to HTTP.

Payloads: smb rows need no client change. http/inline rows need a small fetch-and-verify helper (download to temp, check SHA256, then the existing installer action runs against the local copy). The engine already stages network EXEs to temp, so this is an extension, not a rewrite.

Auth: the client already has SFLD credentials in HKLM:\SOFTWARE\GE\SFLD\Credentials. Add a shopdb service token (a geenforce.fetch PAT) provisioned the same way (Azure DSC writes it to registry), sent as X-API-Key. If shopdb is unreachable, the client falls back to the last-known-good manifest cached locally (fail-safe: never leave a PC unmanaged because the web app is down). This mirrors today's "creds missing = exit 0, retry next cycle" resilience.

9. Cutover strategy

The manifest is desired-state that runs as SYSTEM and installs software fleet- wide. A bad cutover = a fleet-wide mis-install. Stage it:

  1. Import + parity. Write a one-shot importer that reads the current on-share manifests (common + every gea-shopfloor-* + preinstall.json) into the new tables. Then generate JSON back out and diff against the originals until byte-identical for every scope. This proves the model is lossless before anything depends on it. (This is the same discipline as the ADR-001 data migration.)
  2. Shadow mode. shopdb serves the manifest at a new endpoint; a canary PC fetches from shopdb but ALSO reads the share, and logs any diff. No install behavior changes. Run across one of each PC type for a few cycles.
  3. Read cutover, payloads still SMB. Flip GE-Enforce to source the JSON from shopdb (payloads stay smb). The blast radius is only "where the JSON comes from"; the bytes and engine are unchanged. Keep the share manifests as the rollback (revert the dispatcher one-liner).
  4. Payload migration (optional, per entry). Move small scripts/configs to inline/http opportunistically. Leave big MSIs on SMB indefinitely - SMB is the right transport for them.
  5. Author in shopdb. Once read-cutover is stable, new manifest edits happen in the shopdb UI and the on-share JSON is retired (or auto-exported as a backup for break-glass).

Rollback at every stage is a one-line dispatcher revert, because the engine and payload layout never stop working from the share.

10. Risks / open questions

  • The engine is the contract. Any drift between shopdb's generated JSON and what Install-FromManifest.ps1 expects is a fleet-wide install bug. The byte-identical round-trip test (step 1) is non-negotiable, and the plugin must pin which engine lib version it targets (>= 2.6 for _CmmVersion).
  • PCTypes alias graph must be seeded and kept in sync with Install-FromManifest.ps1:463-475. If the engine's alias map changes, ours must too. Consider having the engine fetch the alias map from shopdb as well, so there is one source.
  • Availability coupling. GE-Enforce currently depends only on SMB. Adding an HTTP dependency on shopdb means shopdb downtime could stall enforcement - hence the last-known-good local cache in section 8. Must be built in from day one, not bolted on.
  • Secrets in payloads. Some config drops (site-config, credentials) may contain secrets. inline payloads live in the shopdb DB - those must respect the existing "secrets stay in .env, not the settings table" rule. Likely keep any secret-bearing payload on SMB with ACLs, never inline.
  • Preinstall runner is a separate consumer (00-PreInstall-* at imaging, before enrollment). It may not have a shopdb token yet at that point in the imaging sequence. Preinstall may need to stay share-sourced longer than runtime, or fetch a bootstrap manifest anonymously over HTTP.
  • This is a big build. Realistically phased: (P1) model + importer + parity test; (P2) admin API + CRUD; (P3) frontend editor on /settings/pctypemapping; (P4) client fetch + shadow mode; (P5) read cutover; (P6) payload migration. P1 is the gating de-risk - if the round-trip is not lossless, stop.

11. Relationship to existing work

  • Replaces plugins/computers/pctypemap.py (the thin pctypemap_<pxetype> settings) - the pctype -> ComputerType mapping becomes the computertypeid column on manifestscopes. Migrate those settings in, then retire them.
  • Also folds in the metrology mapping now living in pctypemap.py (METROLOGY_TOOL_MAP). The collector already auto-creates a MeasuringTool asset and a directional PC->tool controls relationship when it sees a metrology pctype (CMM / Keyence / Genspect / wax-and-trace); the PC stays a shopfloor PC. A metrology scope in the manifest model should carry the attached-measuring-tool type alongside its ComputerType so imaging and collector agree on what device the scope implies.
  • Reuses the collector's token machinery (PAT + X-API-Key + scopes) for the client-facing endpoints.
  • Reuses get_permissions() (contract 0.10.0) for geenforce.manage / geenforce.fetch.
  • Pairs with the collector: desired-state (this plugin) + observed-state (collector) enable a fleet compliance view.

12. Recommendation

Feasible and a strong architectural fit, but it is a multi-phase build with a fleet-wide blast radius. The single most important gate is P1: import the real manifests and prove byte-identical round-trip. Do not build the UI or touch a client until that parity test is green. If and when we proceed, this warrants a new ADR (ADR-012: GE-Enforce manifest ownership) capturing the desired-state model, the SMB/HTTP/inline payload contract, and the fail-safe cache.