From 810687953f5fdaf4d840c6c61393af8825774c45 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Mon, 13 Jul 2026 06:37:40 -0400 Subject: [PATCH] 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 --- CLAUDE.md | 1 + .../ADR-012-geenforce-manifest-ownership.md | 123 ++++++++++++++++++ docs/adr/README.md | 1 + 3 files changed, 125 insertions(+) create mode 100644 docs/adr/ADR-012-geenforce-manifest-ownership.md diff --git a/CLAUDE.md b/CLAUDE.md index a81ba55..d62f158 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,7 @@ Architecture decisions live in `docs/adr/`. Read those before making schema or c - ADR-009: Frontend plugin route gating - ACCEPTED - ADR-010: Frontend plugin hook contract - ACCEPTED - ADR-011: Machines rename + modeltypes retyping - ACCEPTED +- ADR-012: GE-Enforce manifest ownership in shopdb - ACCEPTED ## Coding convention diff --git a/docs/adr/ADR-012-geenforce-manifest-ownership.md b/docs/adr/ADR-012-geenforce-manifest-ownership.md new file mode 100644 index 0000000..169b5c7 --- /dev/null +++ b/docs/adr/ADR-012-geenforce-manifest-ownership.md @@ -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). diff --git a/docs/adr/README.md b/docs/adr/README.md index 7b20fbe..453eac8 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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