From the full multi-agent review (0 high, 7 medium, 17 low findings). Applies the mechanical, low-risk items; design/policy findings left for a decision. Docs accuracy: CLAUDE.md contract 0.10.0 -> 0.11.0 and both stale Alembic head citations -> 7d24_customfield_searchable / 31 migrations; Dockerfile bundled- plugin comment fixed (drop nonexistent "equipment", add machines + measuringtools, count eleven). Style/naming (LOCKED rules): remove a CSS-escaped pushpin emoji before location search results (no-emoji policy); rename ManifestEditor shareRoot -> shareroot (variable mirrors the API field verbatim). Dead code: remove confirmed-unused imports across ~20 modules (require_role/ require_permission scaffold residue, stray db/Vendor/Model/current_user/Optional/ error_response); drop unused build_scope import + a stale GEENFORCE_API_KEY docstring clause in geenforce. Migration files left untouched. Correctness: geenforce ingest robustness - record_enforcement_report now 400s on a non-dict counts / non-list results instead of 500; _apply_app_link ignores a non-numeric appid per its docstring instead of 500. Regression tests added. Backend query.get sweep finished: auth.py refresh -> db.session.get (last one). 910 backend tests pass; pyflakes clean; naming green; frontend build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
195 lines
7.8 KiB
Python
195 lines
7.8 KiB
Python
"""GE-Enforce manifest-store plugin.
|
|
|
|
Owns the imaging-PC-type scopes and their install manifests as shopdb data
|
|
(see docs/proposals/ge-enforce-plugin.md). This is the P0/P1 foundation: models,
|
|
the alias-graph seed, and the client-facing manifest endpoint. Authoring UI and
|
|
client cutover come in later phases.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import List, Dict, Optional, Type
|
|
|
|
import click
|
|
from flask import Flask, Blueprint
|
|
|
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
|
from shopdb.api import db
|
|
|
|
from .api import geenforce_bp
|
|
from .models import (
|
|
ManifestScope, ManifestEntry, ManifestEntryPcType, ManifestEntryHostname,
|
|
ManifestEntryMachineNumber, ManifestInUseCheck, ManifestInUseCheckProcess,
|
|
ManifestPublishedVersion, ManifestPayload, ManifestEnforcementReport,
|
|
ManifestEnforcementResult, PcTypeAlias,
|
|
)
|
|
from .filters import ALIAS_GROUPS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GeEnforcePlugin(BasePlugin):
|
|
"""Desired-state manifest store for the GE-Enforce client."""
|
|
|
|
def __init__(self):
|
|
self._manifest = self._load_manifest()
|
|
|
|
def _load_manifest(self) -> Dict:
|
|
manifest_path = Path(__file__).parent / 'manifest.json'
|
|
if manifest_path.exists():
|
|
with open(manifest_path, 'r') as handle:
|
|
return json.load(handle)
|
|
return {}
|
|
|
|
@property
|
|
def meta(self) -> PluginMeta:
|
|
return PluginMeta(
|
|
name=self._manifest.get('name', 'geenforce'),
|
|
version=self._manifest.get('version', '0.1.0'),
|
|
description=self._manifest.get('description', 'GE-Enforce manifest store'),
|
|
author=self._manifest.get('author', 'ShopDB Team'),
|
|
dependencies=self._manifest.get('dependencies', []),
|
|
core_version=self._manifest.get('core_version', '>=0.7.0,<1.0.0'),
|
|
api_prefix=self._manifest.get('api_prefix', '/api/geenforce'),
|
|
)
|
|
|
|
def get_blueprint(self) -> Optional[Blueprint]:
|
|
return geenforce_bp
|
|
|
|
def get_models(self) -> List[Type]:
|
|
return [
|
|
ManifestScope, ManifestEntry, ManifestEntryPcType,
|
|
ManifestEntryHostname, ManifestEntryMachineNumber,
|
|
ManifestInUseCheck, ManifestInUseCheckProcess,
|
|
ManifestPublishedVersion, ManifestPayload,
|
|
ManifestEnforcementReport, ManifestEnforcementResult, PcTypeAlias,
|
|
]
|
|
|
|
def get_permissions(self) -> List:
|
|
"""RBAC permissions this plugin owns (edit vs ship are split)."""
|
|
return [
|
|
('geenforce.manage', 'Edit imaging PC types and manifest drafts',
|
|
'geenforce'),
|
|
('geenforce.publish', 'Publish, roll back, and export manifests',
|
|
'geenforce'),
|
|
('geenforce.fetch', 'Fetch published manifests (client service token)',
|
|
'geenforce'),
|
|
('geenforce.report', 'Report enforcement results (client service token)',
|
|
'geenforce'),
|
|
]
|
|
|
|
def get_navigation_items(self) -> List[Dict]:
|
|
"""Top-level sidebar section (GE-Enforce is a large operational surface,
|
|
not a mere setting). The tabbed shell hosts Manifests + Reports."""
|
|
return [
|
|
{
|
|
'name': 'GE-Enforce',
|
|
'icon': 'shield',
|
|
'route': '/geenforce/manifests',
|
|
'position': 46,
|
|
},
|
|
]
|
|
|
|
def init_app(self, app: Flask, db_instance) -> None:
|
|
logger.info(f"GE-Enforce plugin initialized (v{self.meta.version})")
|
|
|
|
def on_install(self, app: Flask) -> None:
|
|
with app.app_context():
|
|
self._seed_aliases()
|
|
db.session.commit()
|
|
logger.info("GE-Enforce plugin installed")
|
|
|
|
def _seed_aliases(self) -> None:
|
|
"""Seed pctypealiases from the engine lib's alias graph (idempotent)."""
|
|
for group_index, group in enumerate(ALIAS_GROUPS):
|
|
for aliasname in group:
|
|
exists = PcTypeAlias.query.filter_by(
|
|
aliasgroup=group_index, aliasname=aliasname).first()
|
|
if not exists:
|
|
db.session.add(PcTypeAlias(
|
|
aliasgroup=group_index, aliasname=aliasname))
|
|
|
|
def get_cli_commands(self) -> List:
|
|
"""CLI: parity (Gate A), import-share, export-share, publish."""
|
|
|
|
@click.group('geenforce')
|
|
def geenforce_cli():
|
|
"""GE-Enforce manifest-store commands."""
|
|
|
|
@geenforce_cli.command('parity')
|
|
@click.option('--shareroot', required=True,
|
|
help='GE-Enforce share root (contains common/, '
|
|
'gea-shopfloor-*/).')
|
|
@click.option('--preinstall', default=None,
|
|
help='Optional path to a preinstall.json to include.')
|
|
def parity_cmd(shareroot, preinstall):
|
|
"""Prove import+export is behaviorally lossless (Gate A). DB-free."""
|
|
from .importer import discover_share, load_manifest_file
|
|
from .parity import run_parity, format_result
|
|
|
|
manifests = list(discover_share(shareroot))
|
|
if preinstall:
|
|
manifests.append(
|
|
('preinstall', 'preinstall', load_manifest_file(preinstall)))
|
|
results, ok = run_parity(manifests)
|
|
for result in results:
|
|
click.echo(format_result(result))
|
|
click.echo(f"RESULT: {'PASS' if ok else 'FAIL'} "
|
|
f"({len(results)} scopes)")
|
|
raise SystemExit(0 if ok else 1)
|
|
|
|
@geenforce_cli.command('import-share')
|
|
@click.option('--shareroot', required=True)
|
|
@click.option('--preinstall', default=None)
|
|
@click.option('--scope', default=None,
|
|
help='Import only this scope name (else all).')
|
|
def import_share_cmd(shareroot, preinstall, scope):
|
|
"""Import on-share manifests into draft rows (idempotent rebuild)."""
|
|
from flask import current_app
|
|
from .importer import discover_share, load_manifest_file
|
|
from .service import replace_scope_draft
|
|
|
|
with current_app.app_context():
|
|
sources = list(discover_share(shareroot))
|
|
if preinstall:
|
|
sources.append(('preinstall', 'preinstall',
|
|
load_manifest_file(preinstall)))
|
|
count = 0
|
|
for name, phase, manifest in sources:
|
|
if scope and name != scope:
|
|
continue
|
|
replace_scope_draft(name, phase, manifest)
|
|
count += 1
|
|
db.session.commit()
|
|
click.echo(f"Imported {count} scope(s).")
|
|
|
|
@geenforce_cli.command('publish')
|
|
@click.argument('scopename')
|
|
@click.option('--phase', default='runtime')
|
|
@click.option('--notes', default=None)
|
|
def publish_cmd(scopename, phase, notes):
|
|
"""Freeze the current draft of a scope into a published snapshot."""
|
|
from flask import current_app
|
|
from .service import publish_scope
|
|
|
|
with current_app.app_context():
|
|
version = publish_scope(scopename, phase, notes=notes)
|
|
db.session.commit()
|
|
click.echo(f"Published {scopename}/{phase} as v{version}.")
|
|
|
|
@geenforce_cli.command('export-share')
|
|
@click.argument('scopename')
|
|
@click.option('--shareroot', required=True)
|
|
@click.option('--phase', default='runtime')
|
|
def export_share_cmd(scopename, shareroot, phase):
|
|
"""Write a scope's published JSON to the share (with history backup)."""
|
|
from flask import current_app
|
|
from .service import export_scope_to_share
|
|
|
|
with current_app.app_context():
|
|
path = export_scope_to_share(scopename, phase, shareroot)
|
|
click.echo(f"Exported to {path}.")
|
|
|
|
return [geenforce_cli]
|