invarlock 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invarlock/__init__.py +33 -0
- invarlock/__main__.py +10 -0
- invarlock/_data/runtime/profiles/ci_cpu.yaml +15 -0
- invarlock/_data/runtime/profiles/release.yaml +23 -0
- invarlock/_data/runtime/tiers.yaml +76 -0
- invarlock/adapters/__init__.py +102 -0
- invarlock/adapters/_capabilities.py +45 -0
- invarlock/adapters/auto.py +99 -0
- invarlock/adapters/base.py +530 -0
- invarlock/adapters/base_types.py +85 -0
- invarlock/adapters/hf_bert.py +852 -0
- invarlock/adapters/hf_gpt2.py +403 -0
- invarlock/adapters/hf_llama.py +485 -0
- invarlock/adapters/hf_mixin.py +383 -0
- invarlock/adapters/hf_onnx.py +112 -0
- invarlock/adapters/hf_t5.py +137 -0
- invarlock/adapters/py.typed +1 -0
- invarlock/assurance/__init__.py +43 -0
- invarlock/cli/__init__.py +8 -0
- invarlock/cli/__main__.py +8 -0
- invarlock/cli/_evidence.py +25 -0
- invarlock/cli/_json.py +75 -0
- invarlock/cli/adapter_auto.py +162 -0
- invarlock/cli/app.py +287 -0
- invarlock/cli/commands/__init__.py +26 -0
- invarlock/cli/commands/certify.py +403 -0
- invarlock/cli/commands/doctor.py +1358 -0
- invarlock/cli/commands/explain_gates.py +151 -0
- invarlock/cli/commands/export_html.py +100 -0
- invarlock/cli/commands/plugins.py +1331 -0
- invarlock/cli/commands/report.py +354 -0
- invarlock/cli/commands/run.py +4146 -0
- invarlock/cli/commands/verify.py +1040 -0
- invarlock/cli/config.py +396 -0
- invarlock/cli/constants.py +68 -0
- invarlock/cli/device.py +92 -0
- invarlock/cli/doctor_helpers.py +74 -0
- invarlock/cli/errors.py +6 -0
- invarlock/cli/overhead_utils.py +60 -0
- invarlock/cli/provenance.py +66 -0
- invarlock/cli/utils.py +41 -0
- invarlock/config.py +56 -0
- invarlock/core/__init__.py +62 -0
- invarlock/core/abi.py +15 -0
- invarlock/core/api.py +274 -0
- invarlock/core/auto_tuning.py +317 -0
- invarlock/core/bootstrap.py +226 -0
- invarlock/core/checkpoint.py +221 -0
- invarlock/core/contracts.py +73 -0
- invarlock/core/error_utils.py +64 -0
- invarlock/core/events.py +298 -0
- invarlock/core/exceptions.py +95 -0
- invarlock/core/registry.py +481 -0
- invarlock/core/retry.py +146 -0
- invarlock/core/runner.py +2041 -0
- invarlock/core/types.py +154 -0
- invarlock/edits/__init__.py +12 -0
- invarlock/edits/_edit_utils.py +249 -0
- invarlock/edits/_external_utils.py +268 -0
- invarlock/edits/noop.py +47 -0
- invarlock/edits/py.typed +1 -0
- invarlock/edits/quant_rtn.py +801 -0
- invarlock/edits/registry.py +166 -0
- invarlock/eval/__init__.py +23 -0
- invarlock/eval/bench.py +1207 -0
- invarlock/eval/bootstrap.py +50 -0
- invarlock/eval/data.py +2052 -0
- invarlock/eval/metrics.py +2167 -0
- invarlock/eval/primary_metric.py +767 -0
- invarlock/eval/probes/__init__.py +24 -0
- invarlock/eval/probes/fft.py +139 -0
- invarlock/eval/probes/mi.py +213 -0
- invarlock/eval/probes/post_attention.py +323 -0
- invarlock/eval/providers/base.py +67 -0
- invarlock/eval/providers/seq2seq.py +111 -0
- invarlock/eval/providers/text_lm.py +113 -0
- invarlock/eval/providers/vision_text.py +93 -0
- invarlock/eval/py.typed +1 -0
- invarlock/guards/__init__.py +18 -0
- invarlock/guards/_contracts.py +9 -0
- invarlock/guards/invariants.py +640 -0
- invarlock/guards/policies.py +805 -0
- invarlock/guards/py.typed +1 -0
- invarlock/guards/rmt.py +2097 -0
- invarlock/guards/spectral.py +1419 -0
- invarlock/guards/tier_config.py +354 -0
- invarlock/guards/variance.py +3298 -0
- invarlock/guards_ref/__init__.py +15 -0
- invarlock/guards_ref/rmt_ref.py +40 -0
- invarlock/guards_ref/spectral_ref.py +135 -0
- invarlock/guards_ref/variance_ref.py +60 -0
- invarlock/model_profile.py +353 -0
- invarlock/model_utils.py +221 -0
- invarlock/observability/__init__.py +10 -0
- invarlock/observability/alerting.py +535 -0
- invarlock/observability/core.py +546 -0
- invarlock/observability/exporters.py +565 -0
- invarlock/observability/health.py +588 -0
- invarlock/observability/metrics.py +457 -0
- invarlock/observability/py.typed +1 -0
- invarlock/observability/utils.py +553 -0
- invarlock/plugins/__init__.py +12 -0
- invarlock/plugins/hello_guard.py +33 -0
- invarlock/plugins/hf_awq_adapter.py +82 -0
- invarlock/plugins/hf_bnb_adapter.py +79 -0
- invarlock/plugins/hf_gptq_adapter.py +78 -0
- invarlock/plugins/py.typed +1 -0
- invarlock/py.typed +1 -0
- invarlock/reporting/__init__.py +7 -0
- invarlock/reporting/certificate.py +3221 -0
- invarlock/reporting/certificate_schema.py +244 -0
- invarlock/reporting/dataset_hashing.py +215 -0
- invarlock/reporting/guards_analysis.py +948 -0
- invarlock/reporting/html.py +32 -0
- invarlock/reporting/normalizer.py +235 -0
- invarlock/reporting/policy_utils.py +517 -0
- invarlock/reporting/primary_metric_utils.py +265 -0
- invarlock/reporting/render.py +1442 -0
- invarlock/reporting/report.py +903 -0
- invarlock/reporting/report_types.py +278 -0
- invarlock/reporting/utils.py +175 -0
- invarlock/reporting/validate.py +631 -0
- invarlock/security.py +176 -0
- invarlock/sparsity_utils.py +323 -0
- invarlock/utils/__init__.py +150 -0
- invarlock/utils/digest.py +45 -0
- invarlock-0.2.0.dist-info/METADATA +586 -0
- invarlock-0.2.0.dist-info/RECORD +132 -0
- invarlock-0.2.0.dist-info/WHEEL +5 -0
- invarlock-0.2.0.dist-info/entry_points.txt +20 -0
- invarlock-0.2.0.dist-info/licenses/LICENSE +201 -0
- invarlock-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class InvarlockError(Exception):
|
|
9
|
+
code: str
|
|
10
|
+
message: str
|
|
11
|
+
details: dict[str, Any] | None = None
|
|
12
|
+
recoverable: bool = False
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
15
|
+
return f"[INVARLOCK:{self.code}] {self.message}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigError(InvarlockError):
|
|
19
|
+
"""Configuration parsing/validation errors."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidationError(InvarlockError):
|
|
23
|
+
"""Domain validation errors for inputs/parameters."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DependencyError(InvarlockError):
|
|
27
|
+
"""Missing/invalid external dependency (package, binary, model file)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ResourceError(InvarlockError):
|
|
31
|
+
"""Insufficient resources (CPU/GPU/RAM/Disk)."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TimeoutError(InvarlockError):
|
|
35
|
+
"""Operation timed out."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DataError(InvarlockError):
|
|
39
|
+
"""Dataset/provider errors (shape, availability, corruption)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MetricsError(InvarlockError):
|
|
43
|
+
"""Metric computation errors (non-finite, mismatch)."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ModelLoadError(InvarlockError):
|
|
47
|
+
"""Model/weights loading failures."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AdapterError(InvarlockError):
|
|
51
|
+
"""Adapter-specific errors (resolution, device mapping)."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EditError(InvarlockError):
|
|
55
|
+
"""Model edit/transform failures."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GuardError(InvarlockError):
|
|
59
|
+
"""Guard setup/execution failures."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PolicyViolationError(InvarlockError):
|
|
63
|
+
"""Guard or policy violation (hard gate)."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PluginError(InvarlockError):
|
|
67
|
+
"""Plugin resolution/entry-point/import errors."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ObservabilityError(InvarlockError):
|
|
71
|
+
"""Observability/metrics/export issues."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class VersionError(InvarlockError):
|
|
75
|
+
"""Version/ABI compatibility issues."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"InvarlockError",
|
|
80
|
+
"ConfigError",
|
|
81
|
+
"ValidationError",
|
|
82
|
+
"DependencyError",
|
|
83
|
+
"ResourceError",
|
|
84
|
+
"TimeoutError",
|
|
85
|
+
"DataError",
|
|
86
|
+
"MetricsError",
|
|
87
|
+
"ModelLoadError",
|
|
88
|
+
"AdapterError",
|
|
89
|
+
"EditError",
|
|
90
|
+
"GuardError",
|
|
91
|
+
"PolicyViolationError",
|
|
92
|
+
"PluginError",
|
|
93
|
+
"ObservabilityError",
|
|
94
|
+
"VersionError",
|
|
95
|
+
]
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
InvarLock Core Registry
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
Unified plugin registry using entry point discovery.
|
|
6
|
+
Provides centralized access to adapters, edits, and guards.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import warnings
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from importlib.metadata import (
|
|
16
|
+
EntryPoint,
|
|
17
|
+
PackageNotFoundError,
|
|
18
|
+
entry_points,
|
|
19
|
+
)
|
|
20
|
+
from importlib.metadata import (
|
|
21
|
+
version as metadata_version,
|
|
22
|
+
)
|
|
23
|
+
from typing import Any, cast
|
|
24
|
+
|
|
25
|
+
from invarlock import __version__ as INVARLOCK_VERSION
|
|
26
|
+
|
|
27
|
+
from .abi import INVARLOCK_CORE_ABI
|
|
28
|
+
from .api import Guard, ModelAdapter, ModelEdit
|
|
29
|
+
from .exceptions import DependencyError, PluginError
|
|
30
|
+
|
|
31
|
+
__all__ = ["PluginInfo", "CoreRegistry", "get_registry"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class PluginInfo:
|
|
36
|
+
"""Plugin information from entry points."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
module: str
|
|
40
|
+
class_name: str
|
|
41
|
+
available: bool
|
|
42
|
+
status: str
|
|
43
|
+
package: str | None = None
|
|
44
|
+
version: str | None = None
|
|
45
|
+
entry_point: Any | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _select_entry_points(eps: Any, group: str) -> list[EntryPoint]:
|
|
49
|
+
"""Return entry points for a given group across importlib versions."""
|
|
50
|
+
|
|
51
|
+
selected: Iterable[EntryPoint]
|
|
52
|
+
if hasattr(eps, "select"):
|
|
53
|
+
selected = cast("Iterable[EntryPoint]", eps.select(group=group))
|
|
54
|
+
else:
|
|
55
|
+
selected = cast("Iterable[EntryPoint]", eps.get(group, []))
|
|
56
|
+
return list(selected)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CoreRegistry:
|
|
60
|
+
"""
|
|
61
|
+
Central registry for InvarLock plugins using entry point discovery.
|
|
62
|
+
|
|
63
|
+
Discovers and manages adapters, edits, and guards through
|
|
64
|
+
setuptools entry points without requiring imports.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self):
|
|
68
|
+
self._adapters: dict[str, PluginInfo] = {}
|
|
69
|
+
self._edits: dict[str, PluginInfo] = {}
|
|
70
|
+
self._guards: dict[str, PluginInfo] = {}
|
|
71
|
+
self._initialized = False
|
|
72
|
+
|
|
73
|
+
def _ensure_initialized(self) -> None:
|
|
74
|
+
"""Lazy initialization of plugin discovery."""
|
|
75
|
+
if self._initialized:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
self._discover_plugins()
|
|
79
|
+
self._initialized = True
|
|
80
|
+
|
|
81
|
+
def _discover_plugins(self) -> None:
|
|
82
|
+
"""Discover all plugins through entry points with fallback registration."""
|
|
83
|
+
# Try entry points first
|
|
84
|
+
try:
|
|
85
|
+
eps = entry_points()
|
|
86
|
+
|
|
87
|
+
# Discover adapters
|
|
88
|
+
adapter_eps = _select_entry_points(eps, "invarlock.adapters")
|
|
89
|
+
for ep in adapter_eps:
|
|
90
|
+
info = self._create_plugin_info(ep, "adapters")
|
|
91
|
+
self._adapters[ep.name] = info
|
|
92
|
+
|
|
93
|
+
# Discover edits
|
|
94
|
+
edit_eps = _select_entry_points(eps, "invarlock.edits")
|
|
95
|
+
for ep in edit_eps:
|
|
96
|
+
info = self._create_plugin_info(ep, "edits")
|
|
97
|
+
self._edits[ep.name] = info
|
|
98
|
+
|
|
99
|
+
# Discover guards
|
|
100
|
+
guard_eps = _select_entry_points(eps, "invarlock.guards")
|
|
101
|
+
for ep in guard_eps:
|
|
102
|
+
info = self._create_plugin_info(ep, "guards")
|
|
103
|
+
self._guards[ep.name] = info
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
warnings.warn(f"Plugin discovery failed: {e}", stacklevel=2)
|
|
107
|
+
|
|
108
|
+
# Fallback registration for development
|
|
109
|
+
self._register_fallback_plugins()
|
|
110
|
+
|
|
111
|
+
def _register_fallback_plugins(self) -> None:
|
|
112
|
+
"""Register fallback plugins when entry points are not available."""
|
|
113
|
+
|
|
114
|
+
def _fallback(
|
|
115
|
+
registry: dict[str, PluginInfo],
|
|
116
|
+
name: str,
|
|
117
|
+
module: str,
|
|
118
|
+
class_name: str,
|
|
119
|
+
status: str = "Available (fallback)",
|
|
120
|
+
) -> None:
|
|
121
|
+
if name not in registry:
|
|
122
|
+
registry[name] = PluginInfo(
|
|
123
|
+
name=name,
|
|
124
|
+
module=module,
|
|
125
|
+
class_name=class_name,
|
|
126
|
+
available=True,
|
|
127
|
+
status=status,
|
|
128
|
+
package="invarlock",
|
|
129
|
+
version=INVARLOCK_VERSION,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Register built-in adapters
|
|
133
|
+
_fallback(self._adapters, "hf_gpt2", "invarlock.adapters", "HF_GPT2_Adapter")
|
|
134
|
+
_fallback(self._adapters, "hf_bert", "invarlock.adapters", "HF_BERT_Adapter")
|
|
135
|
+
_fallback(self._adapters, "hf_llama", "invarlock.adapters", "HF_LLaMA_Adapter")
|
|
136
|
+
_fallback(self._adapters, "hf_t5", "invarlock.adapters", "HF_T5_Adapter")
|
|
137
|
+
_fallback(
|
|
138
|
+
self._adapters, "hf_onnx", "invarlock.adapters", "HF_ORT_CausalLM_Adapter"
|
|
139
|
+
)
|
|
140
|
+
# Convenience auto adapters (delegate to built-ins)
|
|
141
|
+
_fallback(
|
|
142
|
+
self._adapters,
|
|
143
|
+
"hf_causal_auto",
|
|
144
|
+
"invarlock.adapters",
|
|
145
|
+
"HF_Causal_Auto_Adapter",
|
|
146
|
+
)
|
|
147
|
+
_fallback(
|
|
148
|
+
self._adapters, "hf_mlm_auto", "invarlock.adapters", "HF_MLM_Auto_Adapter"
|
|
149
|
+
)
|
|
150
|
+
# Optional plugin adapters (available when modules present)
|
|
151
|
+
_fallback(
|
|
152
|
+
self._adapters,
|
|
153
|
+
"hf_gptq",
|
|
154
|
+
"invarlock.plugins.hf_gptq_adapter",
|
|
155
|
+
"HF_GPTQ_Adapter",
|
|
156
|
+
status="Available (fallback plugin)",
|
|
157
|
+
)
|
|
158
|
+
_fallback(
|
|
159
|
+
self._adapters,
|
|
160
|
+
"hf_awq",
|
|
161
|
+
"invarlock.plugins.hf_awq_adapter",
|
|
162
|
+
"HF_AWQ_Adapter",
|
|
163
|
+
status="Available (fallback plugin)",
|
|
164
|
+
)
|
|
165
|
+
_fallback(
|
|
166
|
+
self._adapters,
|
|
167
|
+
"hf_bnb",
|
|
168
|
+
"invarlock.plugins.hf_bnb_adapter",
|
|
169
|
+
"HF_BNB_Adapter",
|
|
170
|
+
status="Available (fallback plugin)",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Register built-in edits (quant-only core) and internal no-op
|
|
174
|
+
_fallback(self._edits, "quant_rtn", "invarlock.edits", "RTNQuantEdit")
|
|
175
|
+
_fallback(self._edits, "noop", "invarlock.edits.noop", "NoopEdit")
|
|
176
|
+
|
|
177
|
+
# Register built-in guards
|
|
178
|
+
_fallback(self._guards, "invariants", "invarlock.guards", "InvariantsGuard")
|
|
179
|
+
_fallback(self._guards, "spectral", "invarlock.guards", "SpectralGuard")
|
|
180
|
+
_fallback(self._guards, "variance", "invarlock.guards", "VarianceGuard")
|
|
181
|
+
_fallback(self._guards, "rmt", "invarlock.guards", "RMTGuard")
|
|
182
|
+
_fallback(self._guards, "hello_guard", "invarlock.plugins", "HelloGuard")
|
|
183
|
+
|
|
184
|
+
def _create_plugin_info(
|
|
185
|
+
self, entry_point: EntryPoint, plugin_type: str
|
|
186
|
+
) -> PluginInfo:
|
|
187
|
+
"""Create plugin info from entry point."""
|
|
188
|
+
try:
|
|
189
|
+
# Parse module and class from entry point value
|
|
190
|
+
module_path, class_name = entry_point.value.split(":")
|
|
191
|
+
|
|
192
|
+
# Determine package/version metadata
|
|
193
|
+
package_name: str | None = None
|
|
194
|
+
version: str | None = None
|
|
195
|
+
|
|
196
|
+
dist = getattr(entry_point, "dist", None)
|
|
197
|
+
if dist is not None:
|
|
198
|
+
package_name = getattr(dist, "metadata", {}).get("Name") or getattr(
|
|
199
|
+
dist, "name", None
|
|
200
|
+
)
|
|
201
|
+
version = getattr(dist, "version", None)
|
|
202
|
+
|
|
203
|
+
if not package_name:
|
|
204
|
+
package_name = module_path.split(".")[0]
|
|
205
|
+
try:
|
|
206
|
+
version = metadata_version(package_name)
|
|
207
|
+
except PackageNotFoundError:
|
|
208
|
+
version = None
|
|
209
|
+
|
|
210
|
+
# Defer import to instantiation time to avoid heavy imports here
|
|
211
|
+
available = True
|
|
212
|
+
status = "Deferred load"
|
|
213
|
+
|
|
214
|
+
return PluginInfo(
|
|
215
|
+
name=entry_point.name,
|
|
216
|
+
module=module_path,
|
|
217
|
+
class_name=class_name,
|
|
218
|
+
available=available,
|
|
219
|
+
status=status,
|
|
220
|
+
package=package_name,
|
|
221
|
+
version=version,
|
|
222
|
+
entry_point=entry_point,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return PluginInfo(
|
|
227
|
+
name=entry_point.name,
|
|
228
|
+
module="unknown",
|
|
229
|
+
class_name="unknown",
|
|
230
|
+
available=False,
|
|
231
|
+
status=f"Parse error: {e}",
|
|
232
|
+
entry_point=entry_point,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def list_adapters(self) -> list[str]:
|
|
236
|
+
"""List all registered adapter names."""
|
|
237
|
+
self._ensure_initialized()
|
|
238
|
+
return list(self._adapters.keys())
|
|
239
|
+
|
|
240
|
+
def list_edits(self) -> list[str]:
|
|
241
|
+
"""List all registered edit names."""
|
|
242
|
+
self._ensure_initialized()
|
|
243
|
+
return list(self._edits.keys())
|
|
244
|
+
|
|
245
|
+
def list_guards(self) -> list[str]:
|
|
246
|
+
"""List all registered guard names."""
|
|
247
|
+
self._ensure_initialized()
|
|
248
|
+
return list(self._guards.keys())
|
|
249
|
+
|
|
250
|
+
def get_adapter(self, name: str) -> ModelAdapter:
|
|
251
|
+
"""Get an adapter instance by name."""
|
|
252
|
+
self._ensure_initialized()
|
|
253
|
+
|
|
254
|
+
if name not in self._adapters:
|
|
255
|
+
available = list(self._adapters.keys())
|
|
256
|
+
raise KeyError(f"Unknown adapter '{name}'. Available: {available}")
|
|
257
|
+
|
|
258
|
+
info = self._adapters[name]
|
|
259
|
+
if not info.available:
|
|
260
|
+
raise ImportError(f"Adapter '{name}' unavailable: {info.status}")
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
if info.entry_point:
|
|
264
|
+
cls = info.entry_point.load()
|
|
265
|
+
else:
|
|
266
|
+
# Fallback loading
|
|
267
|
+
module = importlib.import_module(info.module)
|
|
268
|
+
cls = getattr(module, info.class_name)
|
|
269
|
+
# ABI compatibility check on the providing module
|
|
270
|
+
try: # pragma: no cover - simple guard
|
|
271
|
+
provider_mod = importlib.import_module(cls.__module__)
|
|
272
|
+
plugin_abi = getattr(provider_mod, "INVARLOCK_CORE_ABI", None)
|
|
273
|
+
if plugin_abi is not None and str(plugin_abi) != INVARLOCK_CORE_ABI:
|
|
274
|
+
raise ImportError(
|
|
275
|
+
f"ABI mismatch: plugin={plugin_abi} != core={INVARLOCK_CORE_ABI}"
|
|
276
|
+
)
|
|
277
|
+
except Exception as abi_exc:
|
|
278
|
+
raise ImportError(str(abi_exc)) from abi_exc
|
|
279
|
+
instance = cls()
|
|
280
|
+
if not isinstance(instance, ModelAdapter):
|
|
281
|
+
raise TypeError(f"Expected ModelAdapter, got {type(instance)}")
|
|
282
|
+
return instance
|
|
283
|
+
except Exception as e:
|
|
284
|
+
raise ImportError(f"Failed to load adapter '{name}': {e}") from e
|
|
285
|
+
|
|
286
|
+
def get_edit(self, name: str) -> ModelEdit:
|
|
287
|
+
"""Get an edit instance by name."""
|
|
288
|
+
self._ensure_initialized()
|
|
289
|
+
|
|
290
|
+
if name not in self._edits:
|
|
291
|
+
available = list(self._edits.keys())
|
|
292
|
+
raise KeyError(f"Unknown edit '{name}'. Available: {available}")
|
|
293
|
+
|
|
294
|
+
info = self._edits[name]
|
|
295
|
+
if not info.available:
|
|
296
|
+
raise ImportError(f"Edit '{name}' unavailable: {info.status}")
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
if info.entry_point:
|
|
300
|
+
cls = info.entry_point.load()
|
|
301
|
+
else:
|
|
302
|
+
# Fallback loading
|
|
303
|
+
module = importlib.import_module(info.module)
|
|
304
|
+
cls = getattr(module, info.class_name)
|
|
305
|
+
try: # ABI check
|
|
306
|
+
provider_mod = importlib.import_module(cls.__module__)
|
|
307
|
+
plugin_abi = getattr(provider_mod, "INVARLOCK_CORE_ABI", None)
|
|
308
|
+
if plugin_abi is not None and str(plugin_abi) != INVARLOCK_CORE_ABI:
|
|
309
|
+
raise ImportError(
|
|
310
|
+
f"ABI mismatch: plugin={plugin_abi} != core={INVARLOCK_CORE_ABI}"
|
|
311
|
+
)
|
|
312
|
+
except Exception as abi_exc:
|
|
313
|
+
raise ImportError(str(abi_exc)) from abi_exc
|
|
314
|
+
instance = cls()
|
|
315
|
+
if not isinstance(instance, ModelEdit):
|
|
316
|
+
raise TypeError(f"Expected ModelEdit, got {type(instance)}")
|
|
317
|
+
return instance
|
|
318
|
+
except Exception as e:
|
|
319
|
+
raise ImportError(f"Failed to load edit '{name}': {e}") from e
|
|
320
|
+
|
|
321
|
+
def get_guard(self, name: str) -> Guard:
|
|
322
|
+
"""Get a guard instance by name."""
|
|
323
|
+
self._ensure_initialized()
|
|
324
|
+
|
|
325
|
+
if name not in self._guards:
|
|
326
|
+
available = list(self._guards.keys())
|
|
327
|
+
raise KeyError(f"Unknown guard '{name}'. Available: {available}")
|
|
328
|
+
|
|
329
|
+
info = self._guards[name]
|
|
330
|
+
if not info.available:
|
|
331
|
+
raise ImportError(f"Guard '{name}' unavailable: {info.status}")
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
if info.entry_point:
|
|
335
|
+
cls = info.entry_point.load()
|
|
336
|
+
else:
|
|
337
|
+
# Fallback loading
|
|
338
|
+
module = importlib.import_module(info.module)
|
|
339
|
+
cls = getattr(module, info.class_name)
|
|
340
|
+
try: # ABI check
|
|
341
|
+
provider_mod = importlib.import_module(cls.__module__)
|
|
342
|
+
plugin_abi = getattr(provider_mod, "INVARLOCK_CORE_ABI", None)
|
|
343
|
+
if plugin_abi is not None and str(plugin_abi) != INVARLOCK_CORE_ABI:
|
|
344
|
+
raise ImportError(
|
|
345
|
+
f"ABI mismatch: plugin={plugin_abi} != core={INVARLOCK_CORE_ABI}"
|
|
346
|
+
)
|
|
347
|
+
except Exception as abi_exc:
|
|
348
|
+
raise ImportError(str(abi_exc)) from abi_exc
|
|
349
|
+
instance = cls()
|
|
350
|
+
if not isinstance(instance, Guard):
|
|
351
|
+
raise TypeError(f"Expected Guard, got {type(instance)}")
|
|
352
|
+
return instance
|
|
353
|
+
except Exception as e:
|
|
354
|
+
raise ImportError(f"Failed to load guard '{name}': {e}") from e
|
|
355
|
+
|
|
356
|
+
def get_plugin_info(self, name: str, plugin_type: str) -> dict[str, Any]:
|
|
357
|
+
"""Get plugin information without instantiation."""
|
|
358
|
+
self._ensure_initialized()
|
|
359
|
+
|
|
360
|
+
if plugin_type == "adapters":
|
|
361
|
+
registry = self._adapters
|
|
362
|
+
entry_group = "invarlock.adapters"
|
|
363
|
+
elif plugin_type == "edits":
|
|
364
|
+
registry = self._edits
|
|
365
|
+
entry_group = "invarlock.edits"
|
|
366
|
+
elif plugin_type == "guards":
|
|
367
|
+
registry = self._guards
|
|
368
|
+
entry_group = "invarlock.guards"
|
|
369
|
+
else:
|
|
370
|
+
raise ValueError(f"Unknown plugin type: {plugin_type}")
|
|
371
|
+
|
|
372
|
+
if name not in registry:
|
|
373
|
+
return {"available": False, "status": "Not found", "module": "unknown"}
|
|
374
|
+
|
|
375
|
+
info = registry[name]
|
|
376
|
+
return {
|
|
377
|
+
"available": info.available,
|
|
378
|
+
"status": info.status,
|
|
379
|
+
"module": info.module,
|
|
380
|
+
"package": info.package,
|
|
381
|
+
"version": info.version,
|
|
382
|
+
"entry_point": info.entry_point.name if info.entry_point else None,
|
|
383
|
+
"entry_point_group": entry_group if info.entry_point else None,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
def get_plugin_metadata(self, name: str, plugin_type: str) -> dict[str, Any]:
|
|
387
|
+
"""Return comprehensive metadata for a plugin."""
|
|
388
|
+
metadata = self.get_plugin_info(name, plugin_type)
|
|
389
|
+
if metadata.get("module") == "unknown":
|
|
390
|
+
raise KeyError(f"Unknown {plugin_type.rstrip('s')} plugin '{name}'")
|
|
391
|
+
|
|
392
|
+
metadata.update(
|
|
393
|
+
{
|
|
394
|
+
"name": name,
|
|
395
|
+
"type": plugin_type,
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
return metadata
|
|
399
|
+
|
|
400
|
+
# Typed-error wrappers that preserve existing behavior for legacy methods
|
|
401
|
+
def get_adapter_typed(self, name: str) -> ModelAdapter:
|
|
402
|
+
try:
|
|
403
|
+
return self.get_adapter(name)
|
|
404
|
+
except Exception as e: # pragma: no cover - exercised in tests
|
|
405
|
+
details = {"name": name, "kind": "adapter", "reason": type(e).__name__}
|
|
406
|
+
if isinstance(e, ImportError | ModuleNotFoundError):
|
|
407
|
+
raise DependencyError(
|
|
408
|
+
code="E702", message="PLUGIN-DEPENDENCY-MISSING", details=details
|
|
409
|
+
) from e
|
|
410
|
+
raise PluginError(
|
|
411
|
+
code="E701", message="PLUGIN-LOAD-FAILED", details=details
|
|
412
|
+
) from e
|
|
413
|
+
|
|
414
|
+
def get_edit_typed(self, name: str) -> ModelEdit:
|
|
415
|
+
try:
|
|
416
|
+
return self.get_edit(name)
|
|
417
|
+
except Exception as e: # pragma: no cover - exercised in tests
|
|
418
|
+
details = {"name": name, "kind": "edit", "reason": type(e).__name__}
|
|
419
|
+
if isinstance(e, ImportError | ModuleNotFoundError):
|
|
420
|
+
raise DependencyError(
|
|
421
|
+
code="E702", message="PLUGIN-DEPENDENCY-MISSING", details=details
|
|
422
|
+
) from e
|
|
423
|
+
raise PluginError(
|
|
424
|
+
code="E701", message="PLUGIN-LOAD-FAILED", details=details
|
|
425
|
+
) from e
|
|
426
|
+
|
|
427
|
+
def get_guard_typed(self, name: str) -> Guard:
|
|
428
|
+
try:
|
|
429
|
+
return self.get_guard(name)
|
|
430
|
+
except Exception as e: # pragma: no cover - exercised in tests
|
|
431
|
+
details = {"name": name, "kind": "guard", "reason": type(e).__name__}
|
|
432
|
+
if isinstance(e, ImportError | ModuleNotFoundError):
|
|
433
|
+
raise DependencyError(
|
|
434
|
+
code="E702", message="PLUGIN-DEPENDENCY-MISSING", details=details
|
|
435
|
+
) from e
|
|
436
|
+
raise PluginError(
|
|
437
|
+
code="E701", message="PLUGIN-LOAD-FAILED", details=details
|
|
438
|
+
) from e
|
|
439
|
+
|
|
440
|
+
def validate_configuration(
|
|
441
|
+
self, adapter_name: str, edit_name: str, guard_names: list[str]
|
|
442
|
+
) -> tuple[bool, str]:
|
|
443
|
+
"""Validate that a configuration is available."""
|
|
444
|
+
self._ensure_initialized()
|
|
445
|
+
|
|
446
|
+
issues = []
|
|
447
|
+
|
|
448
|
+
# Check adapter
|
|
449
|
+
if adapter_name not in self._adapters:
|
|
450
|
+
issues.append(f"Unknown adapter: {adapter_name}")
|
|
451
|
+
elif not self._adapters[adapter_name].available:
|
|
452
|
+
issues.append(f"Adapter unavailable: {adapter_name}")
|
|
453
|
+
|
|
454
|
+
# Check edit
|
|
455
|
+
if edit_name not in self._edits:
|
|
456
|
+
issues.append(f"Unknown edit: {edit_name}")
|
|
457
|
+
elif not self._edits[edit_name].available:
|
|
458
|
+
issues.append(f"Edit unavailable: {edit_name}")
|
|
459
|
+
|
|
460
|
+
# Check guards
|
|
461
|
+
for guard_name in guard_names:
|
|
462
|
+
if guard_name == "noop":
|
|
463
|
+
continue # noop is always available
|
|
464
|
+
if guard_name not in self._guards:
|
|
465
|
+
issues.append(f"Unknown guard: {guard_name}")
|
|
466
|
+
elif not self._guards[guard_name].available:
|
|
467
|
+
issues.append(f"Guard unavailable: {guard_name}")
|
|
468
|
+
|
|
469
|
+
if issues:
|
|
470
|
+
return False, "; ".join(issues)
|
|
471
|
+
|
|
472
|
+
return True, "Configuration is valid"
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# Global registry instance
|
|
476
|
+
_global_registry = CoreRegistry()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_registry() -> CoreRegistry:
|
|
480
|
+
"""Get the global plugin registry instance."""
|
|
481
|
+
return _global_registry
|