ADR-012: GE-Enforce manifest ownership in shopdb (ACCEPTED)
Formalizes the design built this session: manifest as shopdb data (wide entries table + entrytype discriminator, per-plugin Alembic chain), immutable published snapshots + rollback, behavioral-parity gate, engine-as-source-of-truth filter mirror, payload integrity separate from detection, observed-state reporting, service-token auth (contract 0.11.0), client kit + provisioning-agnostic Install-GEEnforce bootstrap (engine referenced not vendored), Milestone-1 export-to-share + staged cutover, and NO application auto-seeding (curated linking instead). Indexed in ADR README + CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
123
docs/adr/ADR-012-geenforce-manifest-ownership.md
Normal file
123
docs/adr/ADR-012-geenforce-manifest-ownership.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# ADR-012: GE-Enforce manifest ownership in shopdb
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-07-13
|
||||
- **Deciders:** cproudlock
|
||||
- **Relates to:** ADR-002 (plugin contract versioning), ADR-004 (per-site
|
||||
deployment), ADR-006 (collector contract), ADR-008 (per-plugin Alembic chains)
|
||||
|
||||
## Context
|
||||
|
||||
GE-Enforce is a desired-state enforcement system for shopfloor PCs: a PowerShell
|
||||
engine (`Install-FromManifest.ps1`) reads per-PC-type `manifest.json` files off
|
||||
an SMB share every logon and installs / self-heals what they declare. Authoring
|
||||
those manifests today means hand-editing JSON on a file share, and there is no
|
||||
central view of what each PC actually did.
|
||||
|
||||
We want shopdb to own the manifests as data (author, version, publish, roll
|
||||
back) and to observe fleet compliance, while NOT taking on the GE-Enforce engine
|
||||
itself (which is the GE-Enforce framework's, maintained separately) and NOT
|
||||
dictating any site's imaging path (per ADR-004, each site is single-tenant with
|
||||
its own provisioning - PXE at West Jefferson, OOBE provisioning packages at
|
||||
others).
|
||||
|
||||
The manifests are an enforcement PROGRAM, not an application inventory: entry
|
||||
`Type` is not an app/config discriminator and entry `Name` is a manifest label,
|
||||
not a Windows ARP DisplayName. Any design that treats them as an app catalog is
|
||||
wrong.
|
||||
|
||||
## Decision
|
||||
|
||||
Build a bundled `geenforce` plugin that owns the manifest as shopdb data, with a
|
||||
client kit and a deployment bootstrap. Specifically:
|
||||
|
||||
1. **Data model.** One wide `manifestentries` table with an `entrytype`
|
||||
discriminator and nullable per-type columns (not SQLAlchemy STI, not a JSON
|
||||
blob - the fleet is ~64 entries, so sparse columns are free and stay
|
||||
queryable). Scopes are `manifestscopes`, unique on `(scopename, phase)`;
|
||||
runtime is per-pctype scopes, preinstall is one flat scope. Multi-value gates
|
||||
(PCTypes / hostnames / machine numbers) and the nested InUseCheck are child
|
||||
tables. `sortorder` is the execution-order contract. RegValue is stored as a
|
||||
raw JSON literal so DWord-vs-string typing survives. Per-plugin Alembic chain
|
||||
(ADR-008).
|
||||
|
||||
2. **Published snapshots.** Editing touches a DRAFT only. Publish freezes the
|
||||
rendered JSON document into an immutable `manifestpublishedversions` row; the
|
||||
client is ALWAYS served the current published snapshot, never the draft;
|
||||
rollback flips `iscurrent` to an older version. Freezing the document (not
|
||||
row-mirroring) makes immutability structural.
|
||||
|
||||
3. **Behavioral-parity gate, not byte-identity.** A DB-free harness
|
||||
(`parity.py`) imports each real manifest and renders it back, then proves
|
||||
BEHAVIORAL equivalence (same ordered entries with identical detection /
|
||||
targeting, and the same entries fire across machine-profile fixtures) - never
|
||||
byte equality, which re-serialization would never satisfy. This gates any
|
||||
build that touches the model.
|
||||
|
||||
4. **Filter mirror; engine is the single source of truth.** `filters.py`
|
||||
mirrors the engine's four gate functions and alias graph for the "what would
|
||||
this PC get" simulator and parity. The engine lib stays authoritative;
|
||||
shopdb mirrors it (never the reverse). PCTypesStrict is honored only for the
|
||||
preinstall phase, matching the runners.
|
||||
|
||||
5. **Payload integrity is separate from detection.** For `http`/`inline`
|
||||
payloads a dedicated `payloadsha256` is verified before running - independent
|
||||
of `DetectionMethod` (DetectionValue is a hash only for `Hash` detection).
|
||||
`smb` payloads keep the share ACL as their trust boundary. Large binaries
|
||||
stay on SMB; small config/scripts may move to http/inline later.
|
||||
|
||||
6. **Observed-state reporting.** Each PC POSTs its enforcement result;
|
||||
`manifestenforcementreports` (+ results) records the applied version
|
||||
(received-latest) and per-entry self-heal / failure. Status derives from
|
||||
explicit self-heal flags only, never the raw installed count (Always/no-
|
||||
detection scripts install every cycle without being drift corrections).
|
||||
|
||||
7. **Service-token auth.** Client endpoints authorize via managed service
|
||||
tokens scoped `geenforce.fetch` / `geenforce.report`, through a new
|
||||
`service_token_authorized(scope)` on the `shopdb.api` contract surface
|
||||
(contract 0.11.0). Admin CRUD uses `geenforce.manage` / `geenforce.publish`.
|
||||
|
||||
8. **Client + deployment, engine referenced not vendored.** shopdb ships the
|
||||
fetch/report kit (`plugins/geenforce/client/`) and a site-neutral bootstrap
|
||||
(`Install-GEEnforce.ps1`) that provisions a PC's identity
|
||||
(`C:\Enrollment\pc-type.txt` etc. - what determines the PC type; there is no
|
||||
auto-detection, the provisioner supplies it), the shopdb registry config, and
|
||||
the scheduled task. The GE-Enforce ENGINE is referenced (`-EngineSource`),
|
||||
not carried by shopdb. Deployment is provisioning-path independent (PXE step,
|
||||
OOBE ppkg, Intune, manual); the runtime task is fail-safe.
|
||||
|
||||
9. **Milestone 1 = export to share; staged cutover.** Until a site cuts its
|
||||
client over to shopdb-sourced manifests, the plugin publishes and EXPORTS the
|
||||
manifest to the share (with a `_meta/history` backup, atomic write); the
|
||||
unchanged engine picks it up. Cutover is staged: shadow mode (fetch from
|
||||
shopdb AND read the share, log diffs, install from share) then read cutover.
|
||||
|
||||
10. **No application auto-seeding.** The core Applications catalog already
|
||||
tracks these apps (from the classic-shopdb migration) with version
|
||||
histories; auto-creating Applications from manifest labels produced
|
||||
duplicates and misclassified config drops. Application linkage, if wanted, is
|
||||
a curated manifest-entry -> existing-Application link, not label scraping.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive.** Manifests become validated, versioned, publishable data with
|
||||
one-click rollback and a fleet-compliance view; desired-state and observed-
|
||||
state live in one system. The parity gate + published snapshots + separate
|
||||
payload hash make a fleet-wide-SYSTEM system safe to author. The plugin is
|
||||
provisioning-agnostic, so any GE Aerospace site can adopt it regardless of
|
||||
imaging path. Validated end to end: parity green against the real manifests,
|
||||
and the client kit + installer proven on a Windows VM (PS 5.1) and Linux
|
||||
pwsh 7.
|
||||
- **Boundaries / risks.** The engine remains the GE-Enforce framework's, so
|
||||
shopdb's parity mirror must be kept in sync with the lib (guarded by the parity
|
||||
fixtures; the plugin pins lib >= 2.6 for `_CmmVersion`). Provisioning writes
|
||||
the PC identity - shopdb cannot set a PC's type at imaging (a PC is unknown
|
||||
until it enrolls and reports). Manifest-label vs ARP-name mismatch means the
|
||||
catalog link, when built, needs a curated alias layer.
|
||||
- **Deferred.** Desired-vs-observed per-entry compliance (needs a collector
|
||||
installedVersions field); curated manifest-entry -> Application linking; the
|
||||
live client cutover (a site operational decision); inline payload upload.
|
||||
|
||||
See `docs/proposals/ge-enforce-plugin.md` (design + cutover), `docs/GE-ENFORCE.md`
|
||||
(concepts + imaging timeline), `docs/GE-ENFORCE-CLIENT.md` (fetch/report
|
||||
contract), and `docs/GE-ENFORCE-DEPLOY.md` (agent deployment).
|
||||
@@ -24,6 +24,7 @@ Each ADR captures a single architectural decision: the context, the decision its
|
||||
| [009](ADR-009-frontend-plugin-gating.md) | Frontend plugin route gating | ACCEPTED |
|
||||
| [010](ADR-010-frontend-plugin-hooks.md) | Frontend plugin hook contract | ACCEPTED |
|
||||
| [011](ADR-011-machines-rename.md) | Machines rename + modeltypes retyping | ACCEPTED |
|
||||
| [012](ADR-012-geenforce-manifest-ownership.md) | GE-Enforce manifest ownership in shopdb | ACCEPTED |
|
||||
|
||||
## Authoring
|
||||
|
||||
|
||||
Reference in New Issue
Block a user