Build GE-Enforce manifest-store plugin (P0/P1): model, importer, parity gate
All checks were successful
CI / backend (push) Successful in 1m36s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s

First execution phases of docs/proposals/ge-enforce-plugin.md. The GE-Enforce
manifest becomes shopdb data.

P0 scaffold: new geenforce plugin (api_prefix /api/geenforce, default_enabled
false, core_version >=0.7.0). Registered in PLUGIN_TABLE_OWNERS (ADR-008); its
0001 baseline really creates the tables.

P1a model: one wide manifestentries table + entrytype discriminator (not STI,
not JSON blob), manifestscopes (UNIQUE scopename+phase), the three multi-value
filter child tables, inusechecks + processes, immutable manifestpublishedversions
(frozen rendered JSON), manifestpayloads (inline, capped), pctypealiases
(mirror of the engine lib's alias graph). regvalue stored as its raw JSON
literal so DWord typing survives.

P1c importer + exporter: parse common + gea-shopfloor-* + preinstall.json into
draft rows and rebuild the JSON verbatim from rows in sortorder.

P1d parity harness (GATE A): filters.py mirrors the engine's four filter
functions + alias graph; parity.py proves import+export is behaviorally lossless
(field-identical + same-entries-fire across 18 machine-profile fixtures) WITHOUT
byte-diffing. Verified PASS against all 11 real reference manifests (64 entries)
and a synthetic site-neutral fixture covering every type/filter (the CI gate).

First slice (gea-shopfloor-cmm shape): service layer (import/publish/rollback/
export-to-share), CLI (parity, import-share, publish, export-share), and the
client endpoint GET /api/geenforce/manifest serving the current published
snapshot (never the draft) with ETag/304. Split permissions
geenforce.manage/publish/fetch. Tests prove import->publish->serve, draft edits
never change served bytes, publish+rollback, and auth (401 unauth/wrong-scope).

Contract 0.11.0: added service_token_authorized(scope) to shopdb.api so plugin
service endpoints authorize a scoped managed token without importing core token
internals. Documented in PLUGIN-HOOKS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 16:53:18 -04:00
parent cf2f9c308e
commit d85b33bd68
24 changed files with 1830 additions and 3 deletions

View File

@@ -0,0 +1,152 @@
"""GE-Enforce first slice: import a scope, publish it, serve it to a client.
Exercises the vertical the plan calls the first slice (via a small synthetic
scope rather than the real gea-shopfloor-cmm): draft import -> publish ->
GET /api/geenforce/manifest with a geenforce.fetch service token; ETag/304;
draft edits never change the served bytes; publish + rollback change them.
"""
import json
import pytest
from plugins.geenforce.models import ManifestScope
from plugins.geenforce import service
SCOPE = {
'Version': '2.6',
'Applications': [
{'Name': 'Alpha', 'Type': 'MSI', 'Installer': 'apps/alpha.msi',
'DetectionMethod': 'Registry', 'DetectionPath': 'HKLM:\\SOFTWARE\\Alpha'},
{'Name': 'Beta', 'Type': 'PS1', 'Script': 'scripts/beta.ps1',
'DetectionMethod': 'Always'},
],
}
def _seed_and_publish(app, scopename='gea-shopfloor-cmm', manifest=None):
with app.app_context():
service.replace_scope_draft(scopename, 'runtime', manifest or SCOPE)
service.publish_scope(scopename, 'runtime', notes='initial')
service.db.session.commit()
def _mint_fetch_token(client, auth_headers):
resp = client.post('/api/apitokens',
json={'name': 'geenforce svc', 'scopes': ['geenforce.fetch']},
headers=auth_headers)
assert resp.status_code == 201, resp.get_json()
return resp.get_json()['data']['secret']
def test_import_publish_serve(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _mint_fetch_token(client, auth_headers)
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret})
assert resp.status_code == 200, resp.data
body = json.loads(resp.data)
assert [e['Name'] for e in body['Applications']] == ['Alpha', 'Beta']
assert resp.headers.get('ETag')
assert resp.headers.get('X-Manifest-Version') == '1'
def test_etag_304(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _mint_fetch_token(client, auth_headers)
first = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret})
etag = first.headers['ETag']
again = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret, 'If-None-Match': etag})
assert again.status_code == 304
def test_draft_edit_does_not_change_served_bytes(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _mint_fetch_token(client, auth_headers)
before = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret}).data
# Edit the DRAFT (add an entry) but do NOT publish.
changed = {'Version': '2.6', 'Applications': SCOPE['Applications'] + [
{'Name': 'Gamma', 'Type': 'PS1', 'Script': 'scripts/gamma.ps1'}]}
with app.app_context():
service.replace_scope_draft('gea-shopfloor-cmm', 'runtime', changed)
service.db.session.commit()
after = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret}).data
assert before == after # served bytes come from the published snapshot only
def test_publish_then_rollback(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _mint_fetch_token(client, auth_headers)
v1 = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret}).data
changed = {'Version': '2.6', 'Applications': SCOPE['Applications'] + [
{'Name': 'Gamma', 'Type': 'PS1', 'Script': 'scripts/gamma.ps1'}]}
with app.app_context():
service.replace_scope_draft('gea-shopfloor-cmm', 'runtime', changed)
service.publish_scope('gea-shopfloor-cmm', 'runtime', notes='add gamma')
service.db.session.commit()
v2 = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret})
assert v2.headers['X-Manifest-Version'] == '2'
assert b'Gamma' in v2.data
with app.app_context():
service.rollback_scope('gea-shopfloor-cmm', 'runtime', 1)
service.db.session.commit()
rolled = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret})
assert rolled.data == v1 # byte-identical: published snapshots are frozen text
def test_unauthenticated_rejected(client, db, app):
_seed_and_publish(app)
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
assert resp.status_code == 401
def test_wrong_scope_rejected(client, db, app, auth_headers):
_seed_and_publish(app)
resp = client.post('/api/apitokens',
json={'name': 'wrong', 'scopes': ['collector.ingest']},
headers=auth_headers)
secret = resp.get_json()['data']['secret']
manifest = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
headers={'X-API-Key': secret})
assert manifest.status_code == 401
def test_unpublished_scope_404(client, db, app, auth_headers):
with app.app_context():
service.replace_scope_draft('gea-shopfloor-keyence', 'runtime', SCOPE)
service.db.session.commit() # draft only, never published
secret = _mint_fetch_token(client, auth_headers)
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-keyence',
headers={'X-API-Key': secret})
assert resp.status_code == 404
def test_admin_list_and_preview(client, db, app, auth_headers):
_seed_and_publish(app)
listing = client.get('/api/geenforce/scopes', headers=auth_headers)
assert listing.status_code == 200
scopes = listing.get_json()['data']
cmm = next(s for s in scopes if s['scopename'] == 'gea-shopfloor-cmm')
assert cmm['entrycount'] == 2
assert cmm['publishedversion'] == 1
preview = client.get(f"/api/geenforce/scopes/{cmm['scopeid']}/preview",
headers=auth_headers)
assert preview.status_code == 200
assert [e['Name'] for e in preview.get_json()['data']['manifest']['Applications']] \
== ['Alpha', 'Beta']

View File

@@ -0,0 +1,142 @@
"""GE-Enforce P1 parity gate (Gate A): import + export is behaviorally lossless.
Runs the DB-free parity harness (parse -> in-memory rows -> render -> compare)
over a synthetic, site-neutral manifest that exercises every entry type,
detection method, targeting filter, RegValue typing, InUseCheck, and preinstall
flag. The synthetic fixture is what CI gates on (no real site data is vendored
into the framework repo). If the real GE-Enforce reference share is present
(dev), it is also checked.
"""
import os
import pytest
from plugins.geenforce.parity import run_parity, load_fixtures
from plugins.geenforce.importer import discover_share, load_manifest_file
# A site-neutral manifest covering the whole schema surface.
SYNTHETIC = {
'Version': '2.6',
'_comment': 'Synthetic parity fixture (not a real site manifest).',
'Applications': [
{
'_comment': 'MSI with Registry detection, version gate, InUseCheck.',
'Name': 'Sample MSI', 'Type': 'MSI',
'Installer': 'apps/sample.msi',
'InstallArgs': '/qn /norestart ALLUSERS=1',
'_CmmVersion': '2019',
'DetectionMethod': 'Registry',
'DetectionPath': 'HKLM:\\SOFTWARE\\Sample',
'DetectionName': 'DisplayVersion', 'DetectionValue': '1.2.3.4',
'InUseCheck': {
'Behavior': 'CloseAndReopen',
'Processes': [
{'Name': 'sample', 'ExePath': 'C:\\sample.exe',
'GracefulCloseTimeoutSec': 15},
{'Name': 'other'},
],
},
},
{
'Name': 'Sample EXE', 'Type': 'EXE', 'Installer': 'apps/sample.exe',
'InstallArgs': '/quiet', 'WaitTimeoutSec': 60,
'DetectionMethod': 'FileVersion',
'DetectionPath': 'C:\\sample.exe', 'DetectionValue': '1.0.0.0',
},
{
'Name': 'Sample PS1', 'Type': 'PS1', 'Script': 'scripts/run.ps1',
'Args': '-Force', 'DetectionMethod': 'Always',
},
{
'Name': 'Sample File', 'Type': 'File', 'Source': 'configs/app.json',
'Destination': 'C:\\ProgramData\\app.json',
'DetectionMethod': 'Hash', 'DetectionPath': 'C:\\ProgramData\\app.json',
'DetectionValue': 'a' * 64,
'PCTypes': ['gea-shopfloor-collections', 'gea-shopfloor-cmm'],
},
{
'Name': 'Sample Registry DWord', 'Type': 'Registry',
'RegPath': 'HKLM:\\SOFTWARE\\Sample', 'RegName': 'Enabled',
'RegValue': 1, 'RegType': 'DWord',
'DetectionMethod': 'ValueMatches',
'DetectionPath': 'HKLM:\\SOFTWARE\\Sample', 'DetectionName': 'Enabled',
},
{
'Name': 'Sample Registry String', 'Type': 'Registry',
'RegPath': 'HKLM:\\SOFTWARE\\Sample', 'RegName': 'Mode',
'RegValue': 'on', 'RegType': 'String',
},
{
'Name': 'Sample INF', 'Type': 'INF', 'Installer': 'configs/driver.inf',
'DetectionMethod': 'pnputil', 'DetectionPattern': 'SampleDriver',
},
{
'Name': 'Sample bay-gated', 'Type': 'CMD', 'Installer': 'scripts/bay.cmd',
'TargetMachineNumbers': ['3201', '3202'],
},
{
'Name': 'Sample host-gated', 'Type': 'BAT', 'Installer': 'scripts/host.bat',
'TargetHostnames': ['WJS-*', 'WJPC0615'],
},
{
'Name': 'Sample always-installs (no detection)', 'Type': 'PS1',
'Script': 'scripts/every.ps1',
},
{
'Name': 'Sample preinstall-flagged', 'Type': 'EXE',
'Installer': 'apps/pre.exe', 'PreEnrollment': True,
'PCTypesStrict': True, 'KillAfterDetection': True,
'PCTypes': ['gea-shopfloor-nocollections'],
},
],
}
REFERENCE_SHARE = '/home/camp/pxe-images/tsgwp00525-v2/shared/dt/shopfloor'
REFERENCE_PREINSTALL = '/home/camp/projects/pxe/playbook/preinstall/preinstall.json'
def test_synthetic_manifest_round_trips_losslessly():
"""The synthetic manifest imports + exports with full behavioral parity."""
results, ok = run_parity([('synthetic', 'runtime', SYNTHETIC)])
result = results[0]
assert result['entries_identical'] == result['entries_total'], result['firstdiff']
assert result['profiles_same'] == result['profiles_total'], result['firstdiff']
assert ok
def test_regvalue_numeric_type_preserved():
"""A DWord RegValue of 1 round-trips as int 1, not the string '1'."""
from plugins.geenforce.importer import build_scope
from plugins.geenforce.serializer import scope_to_manifest
rebuilt = scope_to_manifest(build_scope('synthetic', 'runtime', SYNTHETIC))
dword = next(e for e in rebuilt['Applications']
if e['Name'] == 'Sample Registry DWord')
assert dword['RegValue'] == 1
assert isinstance(dword['RegValue'], int)
@pytest.mark.skipif(not os.path.isdir(REFERENCE_SHARE),
reason='GE-Enforce reference share not present')
def test_reference_share_round_trips_losslessly():
"""Every real on-share manifest round-trips (the live Gate A)."""
manifests = list(discover_share(REFERENCE_SHARE))
if os.path.isfile(REFERENCE_PREINSTALL):
manifests.append(('preinstall', 'preinstall',
load_manifest_file(REFERENCE_PREINSTALL)))
results, ok = run_parity(manifests)
failed = [r for r in results if not r['passed']]
assert ok, f"parity failures: {[(r['scopename'], r['firstdiff']) for r in failed]}"
def test_fixtures_cover_every_pctype():
"""The machine-profile fixtures include one of each imaging pctype."""
labels = {f['pctype'] for f in load_fixtures()}
for pctype in ('gea-shopfloor-collections', 'gea-shopfloor-nocollections',
'gea-shopfloor-common', 'gea-shopfloor-cmm',
'gea-shopfloor-genspect', 'gea-shopfloor-heattreat',
'gea-shopfloor-waxtrace', 'gea-shopfloor-partmarker',
'gea-shopfloor-display'):
assert pctype in labels