The publishability gate caught internal tooling names and developer paths but nothing site-specific, so roughly sixty leaks reached the wiki: the site name in ten documents, real fleet hostnames in the collector and GE-Enforce examples, an internal database name through the whole import guide, imaging-share paths, and a maintainer's username as the Deciders line of every ADR and inside a generated curl example. None of it is a security matter on an air-gapped fleet. It matters because these pages are read by engineers at other plants, and a document that names one site throughout reads as that site's notes rather than a product's documentation - which is exactly what it then gets treated as. Examples now use neutral hostnames, the site is "the reference site" where the distinction carries meaning, and ADRs are decided by "ShopDB maintainers". The gate carries all of these patterns, so the next one fails a build. Two documents leave docs/ because they were never written for an outside reader. PROJECT-REVIEW.md is an internal health memo pinned to a commit from July, whose headline finding (an untracked playbook) has since been fixed - it is history, and git holds it. PILOT-DEPLOY.md is one site's own cutover runbook, complete with a "re-measure before publishing" placeholder; it moves next to the loader it belongs to, in scripts/site_imports/wjf/. ADR-015 is AMENDED rather than rewritten. Its enforcement section still said report-only and its backlog still listed hardcodes that are now cleared, which left the record contradicting itself. The amendment says what changed and why the report-only period ended; the original text stays, because what the decision looked like when it was taken is the part worth keeping. Also corrects llms.txt's response envelope, which had errors at the top level and pagination at meta.total. Both are nested one deeper, so anything written against that description read undefined on every error it tried to handle.
8.1 KiB
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
smbrows: the client still mounts the share and resolvesapps/...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 carryETagandX-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):
POST /api/geenforce/report
X-API-Key: <token>
Content-Type: application/json
{
"hostname": "CMMPC01",
"scopename": "gea-shopfloor-cmm",
"appliedversion": 3, // the published version you actually ran
"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" }
]
}
appliedversionlets shopdb show which PCs received the latest manifest (receivedlatestin the fleet view).actionper entry:installed(fired - a self-heal when it should already be present),skipped(detected present),failed,filtered.selfhealedmarks 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
Nameconflict 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)
- Configure the registry values on a canary PC; mint the token.
- 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. - 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. - Payload migration (optional, later): move small scripts/configs to
http/inlinepayloads, verified bypayloadsha256. 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/inlinepayloads are verified againstpayloadsha256before 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.