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,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def maybe_dump_guard_evidence(target_dir: str | Path, payload: dict[str, Any]) -> None:
|
|
10
|
+
"""Dump a small JSON blob of guard decision inputs when INVARLOCK_EVIDENCE_DEBUG=1.
|
|
11
|
+
|
|
12
|
+
Keeps payload tiny; callers should pre-filter arrays and redact large fields.
|
|
13
|
+
"""
|
|
14
|
+
if os.getenv("INVARLOCK_EVIDENCE_DEBUG", "0") != "1":
|
|
15
|
+
return
|
|
16
|
+
try:
|
|
17
|
+
path = Path(target_dir)
|
|
18
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
out = path / "guards_evidence.json"
|
|
20
|
+
out.write_text(
|
|
21
|
+
json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8"
|
|
22
|
+
)
|
|
23
|
+
except Exception:
|
|
24
|
+
# Never raise in evidence hook
|
|
25
|
+
pass
|
invarlock/cli/_json.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, is_dataclass
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from invarlock.core.exceptions import InvarlockError
|
|
12
|
+
except Exception: # pragma: no cover - optional in minimal environments
|
|
13
|
+
InvarlockError = None # type: ignore[assignment]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _ts() -> str:
|
|
17
|
+
return datetime.now(UTC).isoformat()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def emit(payload: Any, exit_code: int) -> None:
|
|
21
|
+
"""Emit a JSON payload with a stable envelope and exit.
|
|
22
|
+
|
|
23
|
+
- Adds `ts` (UTC ISO) and `component=cli` if absent
|
|
24
|
+
- Accepts dicts or dataclasses
|
|
25
|
+
- Exits with provided code via Typer
|
|
26
|
+
"""
|
|
27
|
+
if is_dataclass(payload):
|
|
28
|
+
payload = asdict(payload) # type: ignore[assignment]
|
|
29
|
+
if isinstance(payload, dict):
|
|
30
|
+
payload.setdefault("ts", _ts())
|
|
31
|
+
payload.setdefault("component", "cli")
|
|
32
|
+
typer.echo(json.dumps(payload, sort_keys=True))
|
|
33
|
+
raise typer.Exit(exit_code)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def encode_error(exc: Exception) -> dict[str, Any]:
|
|
37
|
+
"""Encode an exception as a structured error object for JSON envelopes.
|
|
38
|
+
|
|
39
|
+
Fields:
|
|
40
|
+
- code: error code if available (InvarlockError), else a generic tag
|
|
41
|
+
- category: exception class name
|
|
42
|
+
- recoverable: bool when available, else False
|
|
43
|
+
- context: attached details when available, else {}
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
category = type(exc).__name__
|
|
47
|
+
except Exception:
|
|
48
|
+
category = "Exception"
|
|
49
|
+
|
|
50
|
+
# Default shape
|
|
51
|
+
out: dict[str, Any] = {
|
|
52
|
+
"code": "E_GENERIC",
|
|
53
|
+
"category": category,
|
|
54
|
+
"recoverable": False,
|
|
55
|
+
"context": {},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# InvarlockError dataclass provides code/details/recoverable
|
|
59
|
+
try:
|
|
60
|
+
if InvarlockError is not None and isinstance(exc, InvarlockError):
|
|
61
|
+
out["code"] = getattr(exc, "code", out["code"]) or out["code"]
|
|
62
|
+
out["recoverable"] = bool(getattr(exc, "recoverable", False))
|
|
63
|
+
details = getattr(exc, "details", None)
|
|
64
|
+
if isinstance(details, dict):
|
|
65
|
+
out["context"] = details
|
|
66
|
+
return out
|
|
67
|
+
except Exception:
|
|
68
|
+
# Fall back to generic encoding when anything unexpected happens
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Heuristic: common schema/validation categories → generic schema code
|
|
72
|
+
if category in {"ValidationError", "ConfigError", "DataError"}:
|
|
73
|
+
out["code"] = "E_SCHEMA"
|
|
74
|
+
|
|
75
|
+
return out
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto adapter resolution utilities.
|
|
3
|
+
|
|
4
|
+
These helpers map a model identifier (HF directory or Hub ID) to a
|
|
5
|
+
concrete built-in adapter name (hf_gpt2, hf_llama, hf_bert) without
|
|
6
|
+
adding a hard dependency on Transformers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_local_hf_config(model_id: str | os.PathLike[str]) -> dict[str, Any] | None:
|
|
18
|
+
"""Read config.json from a local HF directory if present."""
|
|
19
|
+
try:
|
|
20
|
+
p = Path(model_id)
|
|
21
|
+
except Exception:
|
|
22
|
+
return None
|
|
23
|
+
cfg_path = p / "config.json"
|
|
24
|
+
if not cfg_path.exists():
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
with cfg_path.open("r", encoding="utf-8") as fh:
|
|
28
|
+
data = json.load(fh)
|
|
29
|
+
if isinstance(data, dict):
|
|
30
|
+
return data
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detect_quant_family_from_cfg(cfg: dict[str, Any]) -> str | None:
|
|
37
|
+
"""Detect quantization family from a HF config dict.
|
|
38
|
+
|
|
39
|
+
Returns one of: 'hf_gptq', 'hf_awq', 'hf_bnb' or None if not detected.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
q = cfg.get("quantization_config") or {}
|
|
43
|
+
if isinstance(q, dict):
|
|
44
|
+
method = str(q.get("quant_method", q.get("quant_method_full", ""))).lower()
|
|
45
|
+
if any(tok in method for tok in ("gptq",)):
|
|
46
|
+
return "hf_gptq"
|
|
47
|
+
if any(tok in method for tok in ("awq",)):
|
|
48
|
+
return "hf_awq"
|
|
49
|
+
# BitsAndBytes style
|
|
50
|
+
if any(
|
|
51
|
+
str(q.get(k, "")).lower() in {"true", "1"}
|
|
52
|
+
for k in ("load_in_4bit", "load_in_8bit")
|
|
53
|
+
) or any("bitsandbytes" in str(v).lower() for v in q.values()):
|
|
54
|
+
return "hf_bnb"
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_auto_adapter(
|
|
61
|
+
model_id: str | os.PathLike[str], default: str = "hf_gpt2"
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Resolve an appropriate built-in adapter name for a model.
|
|
64
|
+
|
|
65
|
+
Heuristics:
|
|
66
|
+
- Prefer local config.json (no network). Inspect `model_type` and
|
|
67
|
+
`architectures` to classify LLaMA/Mistral vs BERT vs GPT-like.
|
|
68
|
+
- Fallback to simple name heuristics on the model_id string.
|
|
69
|
+
- Default to `hf_gpt2` when unsure.
|
|
70
|
+
"""
|
|
71
|
+
cfg = _read_local_hf_config(model_id)
|
|
72
|
+
model_id_str = str(model_id)
|
|
73
|
+
|
|
74
|
+
def _from_cfg(c: dict[str, Any]) -> str | None:
|
|
75
|
+
# Prefer explicit quantization families first
|
|
76
|
+
fam = _detect_quant_family_from_cfg(c)
|
|
77
|
+
if fam:
|
|
78
|
+
return fam
|
|
79
|
+
mt = str(c.get("model_type", "")).lower()
|
|
80
|
+
archs = [str(a) for a in c.get("architectures", []) if isinstance(a, str)]
|
|
81
|
+
arch_blob = " ".join(archs)
|
|
82
|
+
if (
|
|
83
|
+
mt in {"llama", "mistral", "qwen", "yi"}
|
|
84
|
+
or "Llama" in arch_blob
|
|
85
|
+
or "Mistral" in arch_blob
|
|
86
|
+
):
|
|
87
|
+
return "hf_llama"
|
|
88
|
+
# Treat masked-LM families as BERT-like
|
|
89
|
+
if (
|
|
90
|
+
mt in {"bert", "roberta", "distilbert", "albert", "deberta", "deberta-v2"}
|
|
91
|
+
or "MaskedLM" in arch_blob
|
|
92
|
+
):
|
|
93
|
+
return "hf_bert"
|
|
94
|
+
# Generic causal LM
|
|
95
|
+
if "CausalLM" in arch_blob or mt in {
|
|
96
|
+
"gpt2",
|
|
97
|
+
"gpt_neox",
|
|
98
|
+
"opt",
|
|
99
|
+
"gptj",
|
|
100
|
+
"gptj8bit",
|
|
101
|
+
}:
|
|
102
|
+
return "hf_gpt2"
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# If local directory contains ONNX model files, prefer hf_onnx
|
|
106
|
+
try:
|
|
107
|
+
p = Path(model_id)
|
|
108
|
+
if p.exists() and p.is_dir():
|
|
109
|
+
# Common Optimum export names
|
|
110
|
+
onnx_files = [
|
|
111
|
+
"model.onnx",
|
|
112
|
+
"decoder_model.onnx",
|
|
113
|
+
"decoder_with_past_model.onnx",
|
|
114
|
+
"encoder_model.onnx",
|
|
115
|
+
]
|
|
116
|
+
if any((p / fname).exists() for fname in onnx_files):
|
|
117
|
+
return "hf_onnx"
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
if isinstance(cfg, dict):
|
|
122
|
+
resolved = _from_cfg(cfg)
|
|
123
|
+
if resolved:
|
|
124
|
+
return resolved
|
|
125
|
+
|
|
126
|
+
# String heuristics as last resort
|
|
127
|
+
lower_id = model_id_str.lower()
|
|
128
|
+
# Quantized repo heuristics
|
|
129
|
+
if any(k in lower_id for k in ["gptq", "-gptq", "_gptq"]):
|
|
130
|
+
return "hf_gptq"
|
|
131
|
+
if any(k in lower_id for k in ["awq", "-awq", "_awq"]):
|
|
132
|
+
return "hf_awq"
|
|
133
|
+
if any(
|
|
134
|
+
k in lower_id for k in ["bnb", "bitsandbytes", "-4bit", "-8bit", "4bit", "8bit"]
|
|
135
|
+
):
|
|
136
|
+
return "hf_bnb"
|
|
137
|
+
if any(k in lower_id for k in ["llama", "mistral", "qwen", "yi"]):
|
|
138
|
+
return "hf_llama"
|
|
139
|
+
if any(k in lower_id for k in ["bert", "roberta", "albert", "deberta"]):
|
|
140
|
+
return "hf_bert"
|
|
141
|
+
return default
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def apply_auto_adapter_if_needed(cfg: Any) -> Any:
|
|
145
|
+
"""Mutate/clone a InvarLockConfig to resolve adapter:auto → concrete adapter.
|
|
146
|
+
|
|
147
|
+
Returns the same config object if no change is needed.
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
adapter = str(getattr(cfg.model, "adapter", ""))
|
|
151
|
+
if adapter.strip().lower() not in {"auto", "hf_auto", "auto_hf"}:
|
|
152
|
+
return cfg
|
|
153
|
+
model_id = str(getattr(cfg.model, "id", ""))
|
|
154
|
+
resolved = resolve_auto_adapter(model_id)
|
|
155
|
+
data = cfg.model_dump()
|
|
156
|
+
data.setdefault("model", {})["adapter"] = resolved
|
|
157
|
+
return cfg.__class__(data) # re-wrap as InvarLockConfig
|
|
158
|
+
except Exception:
|
|
159
|
+
return cfg
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
__all__ = ["resolve_auto_adapter", "apply_auto_adapter_if_needed"]
|
invarlock/cli/app.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
InvarLock CLI Main Entry Point (unified namespace)
|
|
3
|
+
=============================================
|
|
4
|
+
|
|
5
|
+
Modern CLI with clean command interface using modular command structure.
|
|
6
|
+
|
|
7
|
+
Import guard: set `INVARLOCK_LIGHT_IMPORT=1` to avoid heavy plugin discovery and
|
|
8
|
+
third‑party imports during docs/tests. This keeps `import invarlock.cli.app` safe in
|
|
9
|
+
minimal environments.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from typer.core import TyperGroup
|
|
19
|
+
|
|
20
|
+
from invarlock.security import enforce_default_security
|
|
21
|
+
|
|
22
|
+
# Lightweight import mode disables heavy side effects in some modules, but we no
|
|
23
|
+
# longer force plugin discovery off globally here; individual commands may gate
|
|
24
|
+
# discovery based on their own flags.
|
|
25
|
+
LIGHT_IMPORT = os.getenv("INVARLOCK_LIGHT_IMPORT", "").strip().lower() in {
|
|
26
|
+
"1",
|
|
27
|
+
"true",
|
|
28
|
+
"yes",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Deterministic help ordering
|
|
33
|
+
class OrderedGroup(TyperGroup):
|
|
34
|
+
def list_commands(self, ctx): # type: ignore[override]
|
|
35
|
+
return [
|
|
36
|
+
"certify",
|
|
37
|
+
"report",
|
|
38
|
+
"verify",
|
|
39
|
+
"run",
|
|
40
|
+
"plugins",
|
|
41
|
+
"doctor",
|
|
42
|
+
"version",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Initialize CLI app
|
|
47
|
+
app = typer.Typer(
|
|
48
|
+
name="invarlock",
|
|
49
|
+
help=(
|
|
50
|
+
"InvarLock — certify model changes with deterministic pairing and safety gates.\n"
|
|
51
|
+
"Quick path: invarlock certify --baseline <MODEL> --subject <MODEL>\n"
|
|
52
|
+
"Hint: use --edit-config to run the built-in quant_rtn demo.\n"
|
|
53
|
+
"Tip: enable downloads with INVARLOCK_ALLOW_NETWORK=1 when fetching.\n"
|
|
54
|
+
"Exit codes:\n"
|
|
55
|
+
" 0=success\n"
|
|
56
|
+
" 1=generic failure\n"
|
|
57
|
+
" 2=schema invalid\n"
|
|
58
|
+
" 3=hard abort ([INVARLOCK:EXXX])."
|
|
59
|
+
),
|
|
60
|
+
no_args_is_help=True,
|
|
61
|
+
cls=OrderedGroup,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
console = Console()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def version():
|
|
69
|
+
"""Show InvarLock version."""
|
|
70
|
+
# Prefer package metadata when available so CLI reflects wheel truth
|
|
71
|
+
try:
|
|
72
|
+
from importlib.metadata import version as _pkg_version
|
|
73
|
+
|
|
74
|
+
schema = None
|
|
75
|
+
try:
|
|
76
|
+
from invarlock.reporting.certificate import (
|
|
77
|
+
CERTIFICATE_SCHEMA_VERSION as _SCHEMA,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
schema = _SCHEMA
|
|
81
|
+
except Exception:
|
|
82
|
+
schema = None
|
|
83
|
+
msg = f"InvarLock {_pkg_version('invarlock')}"
|
|
84
|
+
if schema:
|
|
85
|
+
msg += f" · schema={schema}"
|
|
86
|
+
console.print(msg)
|
|
87
|
+
return
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
try:
|
|
91
|
+
from invarlock import __version__
|
|
92
|
+
|
|
93
|
+
console.print(f"InvarLock {__version__}")
|
|
94
|
+
except Exception:
|
|
95
|
+
console.print("InvarLock version unknown")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
"""Register command modules and groups in the desired help order.
|
|
99
|
+
|
|
100
|
+
Order: certify → report → run → plugins → doctor → version
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command(
|
|
105
|
+
name="certify",
|
|
106
|
+
help=(
|
|
107
|
+
"Certify a subject model against a baseline and generate a safety certificate. "
|
|
108
|
+
"Use when you have two model snapshots and want pass/fail gating."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
def _certify_lazy(
|
|
112
|
+
source: str = typer.Option(
|
|
113
|
+
..., "--source", "--baseline", help="Baseline model dir or Hub ID"
|
|
114
|
+
),
|
|
115
|
+
edited: str = typer.Option(
|
|
116
|
+
..., "--edited", "--subject", help="Subject model dir or Hub ID"
|
|
117
|
+
),
|
|
118
|
+
adapter: str = typer.Option(
|
|
119
|
+
"auto", "--adapter", help="Adapter name or 'auto' to resolve"
|
|
120
|
+
),
|
|
121
|
+
device: str | None = typer.Option(
|
|
122
|
+
None, "--device", help="Device override for runs (auto|cuda|mps|cpu)"
|
|
123
|
+
),
|
|
124
|
+
profile: str = typer.Option("ci", "--profile", help="Profile (ci|release)"),
|
|
125
|
+
tier: str = typer.Option("balanced", "--tier", help="Tier label for context"),
|
|
126
|
+
preset: str | None = typer.Option(
|
|
127
|
+
None,
|
|
128
|
+
"--preset",
|
|
129
|
+
help=(
|
|
130
|
+
"Universal preset path to use (defaults to causal or masked preset "
|
|
131
|
+
"based on adapter)"
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
out: str = typer.Option("runs", "--out", help="Base output directory"),
|
|
135
|
+
cert_out: str = typer.Option(
|
|
136
|
+
"reports/cert", "--cert-out", help="Certificate output directory"
|
|
137
|
+
),
|
|
138
|
+
edit_config: str | None = typer.Option(
|
|
139
|
+
None, "--edit-config", help="Edit preset to apply a demo edit (quant_rtn)"
|
|
140
|
+
),
|
|
141
|
+
):
|
|
142
|
+
from .commands.certify import certify_command as _cert
|
|
143
|
+
|
|
144
|
+
return _cert(
|
|
145
|
+
source=source,
|
|
146
|
+
edited=edited,
|
|
147
|
+
adapter=adapter,
|
|
148
|
+
device=device,
|
|
149
|
+
profile=profile,
|
|
150
|
+
tier=tier,
|
|
151
|
+
preset=preset,
|
|
152
|
+
out=out,
|
|
153
|
+
cert_out=cert_out,
|
|
154
|
+
edit_config=edit_config,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _register_subapps() -> None:
|
|
159
|
+
# Import sub-apps lazily to keep module import light and satisfy E402
|
|
160
|
+
from .commands.doctor import doctor_command as _doctor_cmd
|
|
161
|
+
from .commands.plugins import plugins_app as _plugins_app
|
|
162
|
+
from .commands.report import report_app as _report_app
|
|
163
|
+
|
|
164
|
+
app.add_typer(_report_app, name="report")
|
|
165
|
+
app.add_typer(_plugins_app, name="plugins")
|
|
166
|
+
app.command(name="doctor")(_doctor_cmd)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command(
|
|
170
|
+
name="verify",
|
|
171
|
+
help=(
|
|
172
|
+
"Verify certificate JSON(s) against schema, pairing math, and gates. "
|
|
173
|
+
"Use --json for a single-line machine-readable envelope."
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
def _verify_typed(
|
|
177
|
+
certificates: list[str] = typer.Argument(
|
|
178
|
+
..., help="One or more certificate JSON files to verify."
|
|
179
|
+
),
|
|
180
|
+
baseline: str | None = typer.Option(
|
|
181
|
+
None,
|
|
182
|
+
"--baseline",
|
|
183
|
+
help="Optional baseline certificate/report JSON to enforce provider parity.",
|
|
184
|
+
),
|
|
185
|
+
tolerance: float = typer.Option(
|
|
186
|
+
1e-9, "--tolerance", help="Tolerance for analysis-basis comparisons."
|
|
187
|
+
),
|
|
188
|
+
profile: str | None = typer.Option(
|
|
189
|
+
"dev",
|
|
190
|
+
"--profile",
|
|
191
|
+
help="Execution profile affecting parity enforcement and exit codes (dev|ci|release).",
|
|
192
|
+
),
|
|
193
|
+
json_out: bool = typer.Option(
|
|
194
|
+
False,
|
|
195
|
+
"--json",
|
|
196
|
+
help="Emit machine-readable JSON (suppresses human-readable output)",
|
|
197
|
+
),
|
|
198
|
+
):
|
|
199
|
+
from pathlib import Path as _Path
|
|
200
|
+
|
|
201
|
+
from .commands.verify import verify_command as _verify
|
|
202
|
+
|
|
203
|
+
cert_paths = [_Path(c) for c in certificates]
|
|
204
|
+
baseline_path = _Path(baseline) if isinstance(baseline, str) else None
|
|
205
|
+
return _verify(
|
|
206
|
+
certificates=cert_paths,
|
|
207
|
+
baseline=baseline_path,
|
|
208
|
+
tolerance=tolerance,
|
|
209
|
+
profile=profile,
|
|
210
|
+
json_out=json_out,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@app.command(
|
|
215
|
+
name="run",
|
|
216
|
+
help=(
|
|
217
|
+
"Execute an end-to-end run from a YAML config (edit + guards + reports). "
|
|
218
|
+
"Writes run artifacts and optionally a safety certificate."
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
def _run_typed(
|
|
222
|
+
config: str = typer.Option(
|
|
223
|
+
..., "--config", "-c", help="Path to YAML configuration file"
|
|
224
|
+
),
|
|
225
|
+
device: str | None = typer.Option(
|
|
226
|
+
None, "--device", help="Device override (auto|cuda|mps|cpu)"
|
|
227
|
+
),
|
|
228
|
+
profile: str | None = typer.Option(
|
|
229
|
+
None, "--profile", help="Profile to apply (ci|release)"
|
|
230
|
+
),
|
|
231
|
+
out: str | None = typer.Option(None, "--out", help="Output directory override"),
|
|
232
|
+
edit: str | None = typer.Option(None, "--edit", help="Edit kind (quant|mixed)"),
|
|
233
|
+
tier: str | None = typer.Option(
|
|
234
|
+
None,
|
|
235
|
+
"--tier",
|
|
236
|
+
help="Auto-tuning tier override (conservative|balanced|aggressive)",
|
|
237
|
+
),
|
|
238
|
+
probes: int | None = typer.Option(
|
|
239
|
+
None, "--probes", help="Number of micro-probes (0=deterministic, >0=adaptive)"
|
|
240
|
+
),
|
|
241
|
+
until_pass: bool = typer.Option(
|
|
242
|
+
False, "--until-pass", help="Retry until certificate passes (max 3 attempts)"
|
|
243
|
+
),
|
|
244
|
+
max_attempts: int = typer.Option(
|
|
245
|
+
3, "--max-attempts", help="Maximum retry attempts for --until-pass mode"
|
|
246
|
+
),
|
|
247
|
+
timeout: int | None = typer.Option(
|
|
248
|
+
None, "--timeout", help="Timeout in seconds for --until-pass mode"
|
|
249
|
+
),
|
|
250
|
+
baseline: str | None = typer.Option(
|
|
251
|
+
None,
|
|
252
|
+
"--baseline",
|
|
253
|
+
help="Path to baseline report.json for certificate validation",
|
|
254
|
+
),
|
|
255
|
+
no_cleanup: bool = typer.Option(
|
|
256
|
+
False, "--no-cleanup", help="Skip cleanup of temporary artifacts"
|
|
257
|
+
),
|
|
258
|
+
):
|
|
259
|
+
from .commands.run import run_command as _run
|
|
260
|
+
|
|
261
|
+
return _run(
|
|
262
|
+
config=config,
|
|
263
|
+
device=device,
|
|
264
|
+
profile=profile,
|
|
265
|
+
out=out,
|
|
266
|
+
edit=edit,
|
|
267
|
+
tier=tier,
|
|
268
|
+
probes=probes,
|
|
269
|
+
until_pass=until_pass,
|
|
270
|
+
max_attempts=max_attempts,
|
|
271
|
+
timeout=timeout,
|
|
272
|
+
baseline=baseline,
|
|
273
|
+
no_cleanup=no_cleanup,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
_register_subapps()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def main() -> None:
|
|
281
|
+
"""Main entry point for the InvarLock CLI."""
|
|
282
|
+
enforce_default_security()
|
|
283
|
+
app()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""CLI command package.
|
|
2
|
+
|
|
3
|
+
Lightweight namespace re-exports for programmatic access in tests and tooling.
|
|
4
|
+
Import-time work is minimal; subcommands themselves may perform heavier imports
|
|
5
|
+
only when invoked.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .certify import certify_command
|
|
9
|
+
from .doctor import doctor_command
|
|
10
|
+
from .explain_gates import explain_gates_command
|
|
11
|
+
from .export_html import export_html_command
|
|
12
|
+
from .plugins import plugins_command
|
|
13
|
+
from .report import report_command
|
|
14
|
+
from .run import run_command
|
|
15
|
+
from .verify import verify_command
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"certify_command",
|
|
19
|
+
"doctor_command",
|
|
20
|
+
"explain_gates_command",
|
|
21
|
+
"export_html_command",
|
|
22
|
+
"plugins_command",
|
|
23
|
+
"run_command",
|
|
24
|
+
"verify_command",
|
|
25
|
+
"report_command",
|
|
26
|
+
]
|