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>
124 lines
6.9 KiB
Markdown
124 lines
6.9 KiB
Markdown
# 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).
|