pybinaryguard 1.0.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.
- pybinaryguard/__init__.py +78 -0
- pybinaryguard/__main__.py +7 -0
- pybinaryguard/_compat/__init__.py +0 -0
- pybinaryguard/agent/__init__.py +63 -0
- pybinaryguard/agent/guard.py +232 -0
- pybinaryguard/agent/recommender.py +283 -0
- pybinaryguard/agent/schema.py +200 -0
- pybinaryguard/agent/simulator.py +430 -0
- pybinaryguard/agent/tool_interface.py +474 -0
- pybinaryguard/analyzers/__init__.py +209 -0
- pybinaryguard/analyzers/base.py +40 -0
- pybinaryguard/analyzers/dependency_analyzer.py +336 -0
- pybinaryguard/analyzers/elf_analyzer.py +754 -0
- pybinaryguard/analyzers/symbol_analyzer.py +280 -0
- pybinaryguard/analyzers/wheel_analyzer.py +308 -0
- pybinaryguard/cli/__init__.py +5 -0
- pybinaryguard/cli/commands.py +414 -0
- pybinaryguard/cli/formatters.py +720 -0
- pybinaryguard/cli/main.py +250 -0
- pybinaryguard/diagnostics/__init__.py +13 -0
- pybinaryguard/diagnostics/explainer.py +356 -0
- pybinaryguard/diagnostics/findings.py +146 -0
- pybinaryguard/diagnostics/suggestions.py +508 -0
- pybinaryguard/frameworks/__init__.py +20 -0
- pybinaryguard/frameworks/onnxruntime.py +214 -0
- pybinaryguard/frameworks/pytorch.py +223 -0
- pybinaryguard/frameworks/tensorflow.py +266 -0
- pybinaryguard/frameworks/tensorrt.py +189 -0
- pybinaryguard/models/__init__.py +19 -0
- pybinaryguard/models/enums.py +102 -0
- pybinaryguard/models/finding.py +109 -0
- pybinaryguard/models/package.py +121 -0
- pybinaryguard/models/system.py +118 -0
- pybinaryguard/plugins/__init__.py +19 -0
- pybinaryguard/plugins/contrib/__init__.py +7 -0
- pybinaryguard/plugins/contrib/gstreamer.py +109 -0
- pybinaryguard/plugins/contrib/jetson.py +385 -0
- pybinaryguard/plugins/contrib/opencv.py +306 -0
- pybinaryguard/plugins/contrib/tensorrt.py +426 -0
- pybinaryguard/plugins/hooks.py +273 -0
- pybinaryguard/plugins/loader.py +190 -0
- pybinaryguard/predictor/__init__.py +23 -0
- pybinaryguard/predictor/dependency_graph.py +226 -0
- pybinaryguard/predictor/linker_simulator.py +161 -0
- pybinaryguard/predictor/predictor.py +144 -0
- pybinaryguard/predictor/resolver.py +282 -0
- pybinaryguard/probes/__init__.py +73 -0
- pybinaryguard/probes/base.py +45 -0
- pybinaryguard/probes/board_probe.py +248 -0
- pybinaryguard/probes/cpu_probe.py +176 -0
- pybinaryguard/probes/glibc_probe.py +215 -0
- pybinaryguard/probes/gpu_probe.py +430 -0
- pybinaryguard/probes/library_probe.py +132 -0
- pybinaryguard/probes/os_probe.py +227 -0
- pybinaryguard/probes/python_probe.py +135 -0
- pybinaryguard/probes/toolchain_probe.py +89 -0
- pybinaryguard/probes/venv_probe.py +111 -0
- pybinaryguard/profiles/__init__.py +12 -0
- pybinaryguard/profiles/engine.py +343 -0
- pybinaryguard/rules/__init__.py +24 -0
- pybinaryguard/rules/base.py +57 -0
- pybinaryguard/rules/builtin/__init__.py +136 -0
- pybinaryguard/rules/builtin/arch_rules.py +86 -0
- pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
- pybinaryguard/rules/builtin/container_rules.py +170 -0
- pybinaryguard/rules/builtin/cpu_rules.py +204 -0
- pybinaryguard/rules/builtin/cuda_rules.py +760 -0
- pybinaryguard/rules/builtin/dependency_rules.py +278 -0
- pybinaryguard/rules/builtin/framework_rules.py +252 -0
- pybinaryguard/rules/builtin/glibc_rules.py +320 -0
- pybinaryguard/rules/builtin/numpy_rules.py +110 -0
- pybinaryguard/rules/builtin/predictive_rules.py +165 -0
- pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
- pybinaryguard/rules/builtin/source_build_rules.py +150 -0
- pybinaryguard/rules/builtin/venv_rules.py +159 -0
- pybinaryguard/rules/engine.py +123 -0
- pybinaryguard/scanner.py +904 -0
- pybinaryguard/scoring/__init__.py +19 -0
- pybinaryguard/scoring/engine.py +416 -0
- pybinaryguard/snapshot/__init__.py +20 -0
- pybinaryguard/snapshot/generator.py +168 -0
- pybinaryguard/snapshot/lockfile.py +152 -0
- pybinaryguard/snapshot/verifier.py +315 -0
- pybinaryguard/validators/__init__.py +7 -0
- pybinaryguard/validators/import_validator.py +231 -0
- pybinaryguard-1.0.0.dist-info/METADATA +876 -0
- pybinaryguard-1.0.0.dist-info/RECORD +91 -0
- pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
- pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
- pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
- pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""PyBinaryGuard -- Binary Compatibility Intelligence for Python.
|
|
2
|
+
|
|
3
|
+
Detect binary incompatibilities *before* they crash your program.
|
|
4
|
+
|
|
5
|
+
Quick start::
|
|
6
|
+
|
|
7
|
+
import pybinaryguard
|
|
8
|
+
|
|
9
|
+
report = pybinaryguard.scan()
|
|
10
|
+
print(report.health_score)
|
|
11
|
+
|
|
12
|
+
findings = pybinaryguard.check("torch")
|
|
13
|
+
profile = pybinaryguard.profile()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
|
|
20
|
+
from pybinaryguard.models.enums import ScanMode, Severity
|
|
21
|
+
from pybinaryguard.models.finding import Finding, ScanReport
|
|
22
|
+
from pybinaryguard.models.system import SystemProfile
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Finding",
|
|
26
|
+
"ScanMode",
|
|
27
|
+
"ScanReport",
|
|
28
|
+
"Severity",
|
|
29
|
+
"SystemProfile",
|
|
30
|
+
"check",
|
|
31
|
+
"inspect",
|
|
32
|
+
"profile",
|
|
33
|
+
"scan",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def scan(**kwargs: object) -> ScanReport:
|
|
38
|
+
"""Full environment scan.
|
|
39
|
+
|
|
40
|
+
Returns a :class:`ScanReport` with all findings, health score, and
|
|
41
|
+
package counts. Accepts the same keyword arguments as
|
|
42
|
+
:class:`~pybinaryguard.scanner.Scanner`.
|
|
43
|
+
"""
|
|
44
|
+
from pybinaryguard.scanner import Scanner
|
|
45
|
+
|
|
46
|
+
scanner = Scanner(**kwargs) # type: ignore[arg-type]
|
|
47
|
+
return scanner.run()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check(package: str, **kwargs: object) -> list:
|
|
51
|
+
"""Check a specific installed package.
|
|
52
|
+
|
|
53
|
+
Returns a list of :class:`Finding` objects for the given package.
|
|
54
|
+
"""
|
|
55
|
+
from pybinaryguard.scanner import Scanner
|
|
56
|
+
|
|
57
|
+
scanner = Scanner(packages=[package], **kwargs) # type: ignore[arg-type]
|
|
58
|
+
report = scanner.run()
|
|
59
|
+
return report.findings
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def profile() -> SystemProfile:
|
|
63
|
+
"""Collect the system profile without running compatibility checks."""
|
|
64
|
+
from pybinaryguard.scanner import Scanner
|
|
65
|
+
|
|
66
|
+
scanner = Scanner()
|
|
67
|
+
return scanner.get_profile()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def inspect(file_path: str) -> list:
|
|
71
|
+
"""Inspect a ``.whl`` or ``.so`` file against the current system.
|
|
72
|
+
|
|
73
|
+
Returns a list of :class:`Finding` objects.
|
|
74
|
+
"""
|
|
75
|
+
from pybinaryguard.scanner import Scanner
|
|
76
|
+
|
|
77
|
+
scanner = Scanner()
|
|
78
|
+
return scanner.inspect_file(file_path)
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""PyBinaryGuard Agent SDK — agent-native binary compatibility intelligence.
|
|
2
|
+
|
|
3
|
+
Provides structured, machine-readable diagnostics that AI agents can call,
|
|
4
|
+
interpret, and act on without parsing human-formatted output.
|
|
5
|
+
|
|
6
|
+
Quick start for agents::
|
|
7
|
+
|
|
8
|
+
from pybinaryguard.agent import scan, check, simulate_install, doctor
|
|
9
|
+
|
|
10
|
+
# Full environment scan — returns structured ActionableReport
|
|
11
|
+
report = scan()
|
|
12
|
+
print(report.to_dict()) # JSON-serializable
|
|
13
|
+
print(report.safe_actions) # Auto-executable pip commands
|
|
14
|
+
|
|
15
|
+
# Check one package
|
|
16
|
+
result = check("torch")
|
|
17
|
+
|
|
18
|
+
# Predict compatibility BEFORE installing
|
|
19
|
+
sim = simulate_install("torch==2.4.0+cu124")
|
|
20
|
+
|
|
21
|
+
# Diagnose an error
|
|
22
|
+
dx = doctor("GLIBC_2.34 not found")
|
|
23
|
+
|
|
24
|
+
# Export tool schema for agent framework registration
|
|
25
|
+
from pybinaryguard.agent import export_tool_schema
|
|
26
|
+
schema = export_tool_schema(format="openai")
|
|
27
|
+
|
|
28
|
+
# Register as agent tool (one-liner)
|
|
29
|
+
tool = as_agent_tool()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from pybinaryguard.agent.tool_interface import (
|
|
35
|
+
ActionableReport,
|
|
36
|
+
AgentCheckResult,
|
|
37
|
+
AgentDoctorResult,
|
|
38
|
+
AgentSimulateResult,
|
|
39
|
+
RecommendedAction,
|
|
40
|
+
as_agent_tool,
|
|
41
|
+
check,
|
|
42
|
+
doctor,
|
|
43
|
+
scan,
|
|
44
|
+
simulate_install,
|
|
45
|
+
)
|
|
46
|
+
from pybinaryguard.agent.schema import export_tool_schema, get_tool_descriptors
|
|
47
|
+
from pybinaryguard.agent.recommender import ActionRecommender
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"ActionRecommender",
|
|
51
|
+
"ActionableReport",
|
|
52
|
+
"AgentCheckResult",
|
|
53
|
+
"AgentDoctorResult",
|
|
54
|
+
"AgentSimulateResult",
|
|
55
|
+
"RecommendedAction",
|
|
56
|
+
"as_agent_tool",
|
|
57
|
+
"check",
|
|
58
|
+
"doctor",
|
|
59
|
+
"export_tool_schema",
|
|
60
|
+
"get_tool_descriptors",
|
|
61
|
+
"scan",
|
|
62
|
+
"simulate_install",
|
|
63
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Runtime import guard — intercept import failures with structured diagnostics.
|
|
2
|
+
|
|
3
|
+
Activation::
|
|
4
|
+
|
|
5
|
+
# Method 1: Side-effect import (activates immediately)
|
|
6
|
+
import pybinaryguard.agent.guard
|
|
7
|
+
|
|
8
|
+
# Method 2: Explicit control
|
|
9
|
+
from pybinaryguard.agent.guard import enable_guard, disable_guard
|
|
10
|
+
enable_guard()
|
|
11
|
+
# ... do imports ...
|
|
12
|
+
disable_guard()
|
|
13
|
+
|
|
14
|
+
# Method 3: Context manager
|
|
15
|
+
from pybinaryguard.agent.guard import guarded_imports
|
|
16
|
+
with guarded_imports() as issues:
|
|
17
|
+
import torch
|
|
18
|
+
if issues:
|
|
19
|
+
print(issues) # List of structured diagnostics
|
|
20
|
+
|
|
21
|
+
When active, the guard installs a custom ``sys.excepthook`` that intercepts
|
|
22
|
+
``ImportError`` and ``OSError`` (missing .so) during imports, runs a fast
|
|
23
|
+
diagnostic, and stores structured results in ``guard.captured_issues``.
|
|
24
|
+
|
|
25
|
+
The guard does NOT suppress exceptions — it captures diagnostics alongside
|
|
26
|
+
the normal traceback.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import sys
|
|
32
|
+
import threading
|
|
33
|
+
from contextlib import contextmanager
|
|
34
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Thread-local storage for guard state
|
|
38
|
+
_local = threading.local()
|
|
39
|
+
|
|
40
|
+
# Module-level state
|
|
41
|
+
_original_excepthook: Optional[Callable[..., Any]] = None
|
|
42
|
+
_guard_active: bool = False
|
|
43
|
+
captured_issues: List[Dict[str, object]] = []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _diagnose_import_error(exc: ImportError) -> Dict[str, object]:
|
|
47
|
+
"""Produce a structured diagnostic from an ImportError."""
|
|
48
|
+
issue: Dict[str, object] = {
|
|
49
|
+
"type": "import_error",
|
|
50
|
+
"module": exc.name or str(exc),
|
|
51
|
+
"message": str(exc),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
msg = str(exc).lower()
|
|
55
|
+
|
|
56
|
+
# Detect GLIBC issues
|
|
57
|
+
if "glibc" in msg or "version" in msg and ".so" in msg:
|
|
58
|
+
issue["category"] = "glibc_mismatch"
|
|
59
|
+
issue["fix_hint"] = (
|
|
60
|
+
"The imported library requires a newer GLIBC than your system. "
|
|
61
|
+
"Try: pip install --force-reinstall <package>"
|
|
62
|
+
)
|
|
63
|
+
# Detect missing .so
|
|
64
|
+
elif "cannot open shared object" in msg or ".so" in msg:
|
|
65
|
+
issue["category"] = "missing_shared_library"
|
|
66
|
+
# Extract the library name
|
|
67
|
+
import re
|
|
68
|
+
lib_match = re.search(r"lib\w+\.so[\.\d]*", msg)
|
|
69
|
+
if lib_match:
|
|
70
|
+
issue["missing_library"] = lib_match.group()
|
|
71
|
+
issue["fix_hint"] = (
|
|
72
|
+
"A required shared library is missing. Install the system "
|
|
73
|
+
"dependency or reinstall the package."
|
|
74
|
+
)
|
|
75
|
+
# Detect CUDA errors
|
|
76
|
+
elif "cuda" in msg or "cudart" in msg or "nvrtc" in msg:
|
|
77
|
+
issue["category"] = "cuda_missing"
|
|
78
|
+
issue["fix_hint"] = (
|
|
79
|
+
"CUDA runtime not found. Install a CPU-only version or "
|
|
80
|
+
"set up the CUDA toolkit."
|
|
81
|
+
)
|
|
82
|
+
# Detect architecture issues
|
|
83
|
+
elif "wrong elf class" in msg or "exec format error" in msg:
|
|
84
|
+
issue["category"] = "architecture_mismatch"
|
|
85
|
+
issue["fix_hint"] = (
|
|
86
|
+
"Binary compiled for a different CPU architecture. "
|
|
87
|
+
"Reinstall with: pip install --no-binary :all: <package>"
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
issue["category"] = "unknown"
|
|
91
|
+
issue["fix_hint"] = (
|
|
92
|
+
"Run: pybinaryguard check <package> for detailed diagnostics."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return issue
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _diagnose_os_error(exc: OSError) -> Optional[Dict[str, object]]:
|
|
99
|
+
"""Produce a structured diagnostic from an OSError if it's import-related."""
|
|
100
|
+
msg = str(exc).lower()
|
|
101
|
+
|
|
102
|
+
# Only handle .so loading failures
|
|
103
|
+
if ".so" not in msg and "shared object" not in msg:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
issue: Dict[str, object] = {
|
|
107
|
+
"type": "os_error",
|
|
108
|
+
"message": str(exc),
|
|
109
|
+
"category": "shared_object_load_failure",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
import re
|
|
113
|
+
lib_match = re.search(r"(lib\w+\.so[\.\d]*)", msg)
|
|
114
|
+
if lib_match:
|
|
115
|
+
issue["missing_library"] = lib_match.group(1)
|
|
116
|
+
|
|
117
|
+
issue["fix_hint"] = (
|
|
118
|
+
"A shared library failed to load. Check that all system "
|
|
119
|
+
"dependencies are installed."
|
|
120
|
+
)
|
|
121
|
+
return issue
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _guard_excepthook(
|
|
125
|
+
exc_type: Type[BaseException],
|
|
126
|
+
exc_value: BaseException,
|
|
127
|
+
exc_tb: Any,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Custom excepthook that captures structured diagnostics."""
|
|
130
|
+
global captured_issues
|
|
131
|
+
|
|
132
|
+
issue = None
|
|
133
|
+
|
|
134
|
+
if isinstance(exc_value, ImportError):
|
|
135
|
+
issue = _diagnose_import_error(exc_value)
|
|
136
|
+
elif isinstance(exc_value, OSError):
|
|
137
|
+
issue = _diagnose_os_error(exc_value)
|
|
138
|
+
|
|
139
|
+
if issue is not None:
|
|
140
|
+
captured_issues.append(issue)
|
|
141
|
+
|
|
142
|
+
# Always call the original excepthook — we don't suppress errors
|
|
143
|
+
if _original_excepthook is not None:
|
|
144
|
+
_original_excepthook(exc_type, exc_value, exc_tb)
|
|
145
|
+
else:
|
|
146
|
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def enable_guard() -> None:
|
|
150
|
+
"""Activate the import guard.
|
|
151
|
+
|
|
152
|
+
Installs a custom ``sys.excepthook`` that captures structured
|
|
153
|
+
diagnostics for import-related failures.
|
|
154
|
+
"""
|
|
155
|
+
global _original_excepthook, _guard_active
|
|
156
|
+
|
|
157
|
+
if _guard_active:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
_original_excepthook = sys.excepthook
|
|
161
|
+
sys.excepthook = _guard_excepthook
|
|
162
|
+
_guard_active = True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def disable_guard() -> None:
|
|
166
|
+
"""Deactivate the import guard, restoring the original excepthook."""
|
|
167
|
+
global _original_excepthook, _guard_active
|
|
168
|
+
|
|
169
|
+
if not _guard_active:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if _original_excepthook is not None:
|
|
173
|
+
sys.excepthook = _original_excepthook
|
|
174
|
+
_original_excepthook = None
|
|
175
|
+
|
|
176
|
+
_guard_active = False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def is_active() -> bool:
|
|
180
|
+
"""Return whether the import guard is currently active."""
|
|
181
|
+
return _guard_active
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_captured_issues() -> List[Dict[str, object]]:
|
|
185
|
+
"""Return all captured issues since the guard was enabled."""
|
|
186
|
+
return list(captured_issues)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def clear_captured_issues() -> None:
|
|
190
|
+
"""Clear all captured issues."""
|
|
191
|
+
captured_issues.clear()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@contextmanager
|
|
195
|
+
def guarded_imports():
|
|
196
|
+
"""Context manager that captures import diagnostics.
|
|
197
|
+
|
|
198
|
+
Usage::
|
|
199
|
+
|
|
200
|
+
with guarded_imports() as issues:
|
|
201
|
+
import some_problematic_module
|
|
202
|
+
|
|
203
|
+
if issues:
|
|
204
|
+
for issue in issues:
|
|
205
|
+
print(issue)
|
|
206
|
+
|
|
207
|
+
Yields a list that will be populated with any import-related
|
|
208
|
+
diagnostics captured during the block.
|
|
209
|
+
"""
|
|
210
|
+
issues: List[Dict[str, object]] = []
|
|
211
|
+
was_active = _guard_active
|
|
212
|
+
prev_issues = list(captured_issues)
|
|
213
|
+
|
|
214
|
+
enable_guard()
|
|
215
|
+
clear_captured_issues()
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
yield issues
|
|
219
|
+
finally:
|
|
220
|
+
# Capture any issues that occurred
|
|
221
|
+
issues.extend(captured_issues)
|
|
222
|
+
|
|
223
|
+
# Restore previous state
|
|
224
|
+
clear_captured_issues()
|
|
225
|
+
captured_issues.extend(prev_issues)
|
|
226
|
+
|
|
227
|
+
if not was_active:
|
|
228
|
+
disable_guard()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# Auto-activate on import
|
|
232
|
+
enable_guard()
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Action recommendation engine for agent workflows.
|
|
2
|
+
|
|
3
|
+
Transforms raw diagnostic findings into structured, executable actions
|
|
4
|
+
classified by safety level so agents know what they can auto-execute
|
|
5
|
+
versus what requires human approval.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from pybinaryguard.models.enums import Severity
|
|
15
|
+
from pybinaryguard.models.finding import Finding
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class RecommendedAction:
|
|
20
|
+
"""A single recommended action an agent can take.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
action_type: Category — "reinstall", "install", "uninstall",
|
|
24
|
+
"downgrade", "upgrade", "configure", "ignore".
|
|
25
|
+
target: The package or system component to act on.
|
|
26
|
+
command: Ready-to-execute shell command (usually pip).
|
|
27
|
+
reason: One-sentence explanation of why this action is needed.
|
|
28
|
+
safety: "safe" (agent can auto-execute), "review" (agent should
|
|
29
|
+
confirm with user), or "dangerous" (requires human approval).
|
|
30
|
+
confidence: 0.0-1.0 confidence that this action will fix the issue.
|
|
31
|
+
finding_id: The rule_id of the finding that triggered this action.
|
|
32
|
+
priority: Lower number = higher priority. 1 = fix first.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
action_type: str
|
|
36
|
+
target: str
|
|
37
|
+
command: str
|
|
38
|
+
reason: str
|
|
39
|
+
safety: str # "safe", "review", "dangerous"
|
|
40
|
+
confidence: float = 0.8
|
|
41
|
+
finding_id: str = ""
|
|
42
|
+
priority: int = 1
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> Dict[str, object]:
|
|
45
|
+
return {
|
|
46
|
+
"action_type": self.action_type,
|
|
47
|
+
"target": self.target,
|
|
48
|
+
"command": self.command,
|
|
49
|
+
"reason": self.reason,
|
|
50
|
+
"safety": self.safety,
|
|
51
|
+
"confidence": round(self.confidence, 2),
|
|
52
|
+
"priority": self.priority,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Action templates keyed by rule_id patterns
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
_ACTION_TEMPLATES: List[Dict[str, object]] = [
|
|
61
|
+
# CUDA mismatches
|
|
62
|
+
{
|
|
63
|
+
"pattern": r"CUDA_RUNTIME_MISMATCH|PYTORCH_CUDA_ABI_MISMATCH",
|
|
64
|
+
"action_type": "reinstall",
|
|
65
|
+
"command_template": "pip install {package} --index-url https://download.pytorch.org/whl/cu{cuda_short}",
|
|
66
|
+
"reason": "Package built for wrong CUDA version",
|
|
67
|
+
"safety": "safe",
|
|
68
|
+
"confidence": 0.9,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"pattern": r"CUDA_DRIVER_TOO_OLD",
|
|
72
|
+
"action_type": "configure",
|
|
73
|
+
"command_template": "# Upgrade GPU driver or install package for older CUDA: pip install {package}+cu{driver_cuda}",
|
|
74
|
+
"reason": "GPU driver too old for installed CUDA runtime",
|
|
75
|
+
"safety": "dangerous",
|
|
76
|
+
"confidence": 0.7,
|
|
77
|
+
},
|
|
78
|
+
# GLIBC
|
|
79
|
+
{
|
|
80
|
+
"pattern": r"GLIBC_VERSION_MISMATCH",
|
|
81
|
+
"action_type": "downgrade",
|
|
82
|
+
"command_template": "pip install {package}<={safe_version}",
|
|
83
|
+
"reason": "Package requires newer GLIBC than system provides",
|
|
84
|
+
"safety": "safe",
|
|
85
|
+
"confidence": 0.8,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"pattern": r"MUSL_GLIBC_CONFLICT",
|
|
89
|
+
"action_type": "reinstall",
|
|
90
|
+
"command_template": "pip install --no-binary :all: {package}",
|
|
91
|
+
"reason": "Binary wheel is glibc-linked but system uses musl (Alpine)",
|
|
92
|
+
"safety": "review",
|
|
93
|
+
"confidence": 0.7,
|
|
94
|
+
},
|
|
95
|
+
# Architecture
|
|
96
|
+
{
|
|
97
|
+
"pattern": r"ARCH_MISMATCH|JETSON_X86_WHEEL",
|
|
98
|
+
"action_type": "reinstall",
|
|
99
|
+
"command_template": "pip install --no-binary :all: {package}",
|
|
100
|
+
"reason": "Binary compiled for wrong CPU architecture",
|
|
101
|
+
"safety": "review",
|
|
102
|
+
"confidence": 0.6,
|
|
103
|
+
},
|
|
104
|
+
# Python ABI
|
|
105
|
+
{
|
|
106
|
+
"pattern": r"PYTHON_ABI_MISMATCH|PYTHON_VERSION_MISMATCH",
|
|
107
|
+
"action_type": "reinstall",
|
|
108
|
+
"command_template": "pip install --force-reinstall {package}",
|
|
109
|
+
"reason": "Package built for different Python version/ABI",
|
|
110
|
+
"safety": "safe",
|
|
111
|
+
"confidence": 0.9,
|
|
112
|
+
},
|
|
113
|
+
# Missing libraries
|
|
114
|
+
{
|
|
115
|
+
"pattern": r"MISSING_SHARED_LIB|CUDA_LIB_MISSING",
|
|
116
|
+
"action_type": "install",
|
|
117
|
+
"command_template": "# Install missing system library or: pip install {package}",
|
|
118
|
+
"reason": "Required shared library not found on system",
|
|
119
|
+
"safety": "review",
|
|
120
|
+
"confidence": 0.6,
|
|
121
|
+
},
|
|
122
|
+
# NumPy ABI
|
|
123
|
+
{
|
|
124
|
+
"pattern": r"NUMPY_ABI_MISMATCH",
|
|
125
|
+
"action_type": "reinstall",
|
|
126
|
+
"command_template": "pip install --force-reinstall {package}",
|
|
127
|
+
"reason": "NumPy C API version mismatch",
|
|
128
|
+
"safety": "safe",
|
|
129
|
+
"confidence": 0.9,
|
|
130
|
+
},
|
|
131
|
+
# CPU instruction set
|
|
132
|
+
{
|
|
133
|
+
"pattern": r"AVX2_REQUIRED|ILLEGAL_INSTRUCTION_RISK",
|
|
134
|
+
"action_type": "downgrade",
|
|
135
|
+
"command_template": "pip install {package} --prefer-binary",
|
|
136
|
+
"reason": "Binary requires CPU instructions not available on this system",
|
|
137
|
+
"safety": "review",
|
|
138
|
+
"confidence": 0.5,
|
|
139
|
+
},
|
|
140
|
+
# Board/embedded
|
|
141
|
+
{
|
|
142
|
+
"pattern": r"KNOWN_BROKEN_WHEEL|BOARD_INCOMPATIBLE_PACKAGE",
|
|
143
|
+
"action_type": "uninstall",
|
|
144
|
+
"command_template": "pip uninstall -y {package}",
|
|
145
|
+
"reason": "Package known incompatible with detected board",
|
|
146
|
+
"safety": "review",
|
|
147
|
+
"confidence": 0.8,
|
|
148
|
+
},
|
|
149
|
+
# Predictive failures
|
|
150
|
+
{
|
|
151
|
+
"pattern": r"PREDICTED_IMPORT_ERROR",
|
|
152
|
+
"action_type": "reinstall",
|
|
153
|
+
"command_template": "pip install --force-reinstall {package}",
|
|
154
|
+
"reason": "Predicted import failure based on dependency analysis",
|
|
155
|
+
"safety": "safe",
|
|
156
|
+
"confidence": 0.7,
|
|
157
|
+
},
|
|
158
|
+
# Container
|
|
159
|
+
{
|
|
160
|
+
"pattern": r"CONTAINER_NO_GPU_MOUNT",
|
|
161
|
+
"action_type": "configure",
|
|
162
|
+
"command_template": "# Re-run container with: --gpus all",
|
|
163
|
+
"reason": "GPU devices not mounted in container",
|
|
164
|
+
"safety": "dangerous",
|
|
165
|
+
"confidence": 0.9,
|
|
166
|
+
},
|
|
167
|
+
# TensorRT
|
|
168
|
+
{
|
|
169
|
+
"pattern": r"TENSORRT_INCOMPATIBLE",
|
|
170
|
+
"action_type": "reinstall",
|
|
171
|
+
"command_template": "pip install tensorrt=={compatible_version}",
|
|
172
|
+
"reason": "TensorRT version incompatible with current GPU/CUDA",
|
|
173
|
+
"safety": "review",
|
|
174
|
+
"confidence": 0.7,
|
|
175
|
+
},
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ActionRecommender:
|
|
180
|
+
"""Transforms findings into executable recommended actions.
|
|
181
|
+
|
|
182
|
+
The recommender matches findings against known action templates and
|
|
183
|
+
produces structured actions that agents can auto-execute (safe),
|
|
184
|
+
present for review, or flag as dangerous.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def recommend(self, findings: List[Finding]) -> List[RecommendedAction]:
|
|
188
|
+
"""Generate recommended actions from findings.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
findings:
|
|
193
|
+
Diagnostic findings from a scan or check.
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
List[RecommendedAction]
|
|
198
|
+
Actions sorted by priority (highest priority first).
|
|
199
|
+
"""
|
|
200
|
+
actions: List[RecommendedAction] = []
|
|
201
|
+
seen_targets: set = set()
|
|
202
|
+
|
|
203
|
+
for finding in findings:
|
|
204
|
+
if finding.severity == Severity.PASSED:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
action = self._match_action(finding)
|
|
208
|
+
if action is None:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
# Deduplicate: one action per target package
|
|
212
|
+
target_key = (action.action_type, action.target)
|
|
213
|
+
if target_key in seen_targets:
|
|
214
|
+
continue
|
|
215
|
+
seen_targets.add(target_key)
|
|
216
|
+
|
|
217
|
+
actions.append(action)
|
|
218
|
+
|
|
219
|
+
# Sort by priority: critical first, then by confidence descending
|
|
220
|
+
actions.sort(key=lambda a: (a.priority, -a.confidence))
|
|
221
|
+
return actions
|
|
222
|
+
|
|
223
|
+
def _match_action(self, finding: Finding) -> Optional[RecommendedAction]:
|
|
224
|
+
"""Match a finding to an action template."""
|
|
225
|
+
for template in _ACTION_TEMPLATES:
|
|
226
|
+
pattern = template["pattern"]
|
|
227
|
+
if re.match(pattern, finding.rule_id):
|
|
228
|
+
return self._instantiate_action(template, finding)
|
|
229
|
+
|
|
230
|
+
# Fallback: use the finding's own suggestion if available
|
|
231
|
+
if finding.suggestion and finding.package:
|
|
232
|
+
return RecommendedAction(
|
|
233
|
+
action_type="fix",
|
|
234
|
+
target=finding.package,
|
|
235
|
+
command=finding.suggestion,
|
|
236
|
+
reason=finding.title,
|
|
237
|
+
safety="review",
|
|
238
|
+
confidence=finding.confidence * 0.6,
|
|
239
|
+
finding_id=finding.rule_id,
|
|
240
|
+
priority=self._severity_to_priority(finding.severity),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def _instantiate_action(
|
|
246
|
+
self, template: Dict[str, object], finding: Finding
|
|
247
|
+
) -> RecommendedAction:
|
|
248
|
+
"""Create a concrete action from a template and finding."""
|
|
249
|
+
package = finding.package or "unknown"
|
|
250
|
+
command_template = str(template["command_template"])
|
|
251
|
+
|
|
252
|
+
# Fill template variables
|
|
253
|
+
command = command_template.format(
|
|
254
|
+
package=package,
|
|
255
|
+
cuda_short="121", # sensible default
|
|
256
|
+
safe_version="",
|
|
257
|
+
driver_cuda="118",
|
|
258
|
+
compatible_version="",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# If the finding has a specific suggestion, prefer it
|
|
262
|
+
if finding.suggestion:
|
|
263
|
+
command = finding.suggestion
|
|
264
|
+
|
|
265
|
+
return RecommendedAction(
|
|
266
|
+
action_type=str(template["action_type"]),
|
|
267
|
+
target=package,
|
|
268
|
+
command=command,
|
|
269
|
+
reason=str(template["reason"]),
|
|
270
|
+
safety=str(template["safety"]),
|
|
271
|
+
confidence=float(template.get("confidence", 0.7)),
|
|
272
|
+
finding_id=finding.rule_id,
|
|
273
|
+
priority=self._severity_to_priority(finding.severity),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _severity_to_priority(severity: Severity) -> int:
|
|
278
|
+
return {
|
|
279
|
+
Severity.CRITICAL: 1,
|
|
280
|
+
Severity.WARNING: 2,
|
|
281
|
+
Severity.INFO: 3,
|
|
282
|
+
Severity.PASSED: 4,
|
|
283
|
+
}.get(severity, 3)
|