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.
Files changed (91) hide show
  1. pybinaryguard/__init__.py +78 -0
  2. pybinaryguard/__main__.py +7 -0
  3. pybinaryguard/_compat/__init__.py +0 -0
  4. pybinaryguard/agent/__init__.py +63 -0
  5. pybinaryguard/agent/guard.py +232 -0
  6. pybinaryguard/agent/recommender.py +283 -0
  7. pybinaryguard/agent/schema.py +200 -0
  8. pybinaryguard/agent/simulator.py +430 -0
  9. pybinaryguard/agent/tool_interface.py +474 -0
  10. pybinaryguard/analyzers/__init__.py +209 -0
  11. pybinaryguard/analyzers/base.py +40 -0
  12. pybinaryguard/analyzers/dependency_analyzer.py +336 -0
  13. pybinaryguard/analyzers/elf_analyzer.py +754 -0
  14. pybinaryguard/analyzers/symbol_analyzer.py +280 -0
  15. pybinaryguard/analyzers/wheel_analyzer.py +308 -0
  16. pybinaryguard/cli/__init__.py +5 -0
  17. pybinaryguard/cli/commands.py +414 -0
  18. pybinaryguard/cli/formatters.py +720 -0
  19. pybinaryguard/cli/main.py +250 -0
  20. pybinaryguard/diagnostics/__init__.py +13 -0
  21. pybinaryguard/diagnostics/explainer.py +356 -0
  22. pybinaryguard/diagnostics/findings.py +146 -0
  23. pybinaryguard/diagnostics/suggestions.py +508 -0
  24. pybinaryguard/frameworks/__init__.py +20 -0
  25. pybinaryguard/frameworks/onnxruntime.py +214 -0
  26. pybinaryguard/frameworks/pytorch.py +223 -0
  27. pybinaryguard/frameworks/tensorflow.py +266 -0
  28. pybinaryguard/frameworks/tensorrt.py +189 -0
  29. pybinaryguard/models/__init__.py +19 -0
  30. pybinaryguard/models/enums.py +102 -0
  31. pybinaryguard/models/finding.py +109 -0
  32. pybinaryguard/models/package.py +121 -0
  33. pybinaryguard/models/system.py +118 -0
  34. pybinaryguard/plugins/__init__.py +19 -0
  35. pybinaryguard/plugins/contrib/__init__.py +7 -0
  36. pybinaryguard/plugins/contrib/gstreamer.py +109 -0
  37. pybinaryguard/plugins/contrib/jetson.py +385 -0
  38. pybinaryguard/plugins/contrib/opencv.py +306 -0
  39. pybinaryguard/plugins/contrib/tensorrt.py +426 -0
  40. pybinaryguard/plugins/hooks.py +273 -0
  41. pybinaryguard/plugins/loader.py +190 -0
  42. pybinaryguard/predictor/__init__.py +23 -0
  43. pybinaryguard/predictor/dependency_graph.py +226 -0
  44. pybinaryguard/predictor/linker_simulator.py +161 -0
  45. pybinaryguard/predictor/predictor.py +144 -0
  46. pybinaryguard/predictor/resolver.py +282 -0
  47. pybinaryguard/probes/__init__.py +73 -0
  48. pybinaryguard/probes/base.py +45 -0
  49. pybinaryguard/probes/board_probe.py +248 -0
  50. pybinaryguard/probes/cpu_probe.py +176 -0
  51. pybinaryguard/probes/glibc_probe.py +215 -0
  52. pybinaryguard/probes/gpu_probe.py +430 -0
  53. pybinaryguard/probes/library_probe.py +132 -0
  54. pybinaryguard/probes/os_probe.py +227 -0
  55. pybinaryguard/probes/python_probe.py +135 -0
  56. pybinaryguard/probes/toolchain_probe.py +89 -0
  57. pybinaryguard/probes/venv_probe.py +111 -0
  58. pybinaryguard/profiles/__init__.py +12 -0
  59. pybinaryguard/profiles/engine.py +343 -0
  60. pybinaryguard/rules/__init__.py +24 -0
  61. pybinaryguard/rules/base.py +57 -0
  62. pybinaryguard/rules/builtin/__init__.py +136 -0
  63. pybinaryguard/rules/builtin/arch_rules.py +86 -0
  64. pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
  65. pybinaryguard/rules/builtin/container_rules.py +170 -0
  66. pybinaryguard/rules/builtin/cpu_rules.py +204 -0
  67. pybinaryguard/rules/builtin/cuda_rules.py +760 -0
  68. pybinaryguard/rules/builtin/dependency_rules.py +278 -0
  69. pybinaryguard/rules/builtin/framework_rules.py +252 -0
  70. pybinaryguard/rules/builtin/glibc_rules.py +320 -0
  71. pybinaryguard/rules/builtin/numpy_rules.py +110 -0
  72. pybinaryguard/rules/builtin/predictive_rules.py +165 -0
  73. pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
  74. pybinaryguard/rules/builtin/source_build_rules.py +150 -0
  75. pybinaryguard/rules/builtin/venv_rules.py +159 -0
  76. pybinaryguard/rules/engine.py +123 -0
  77. pybinaryguard/scanner.py +904 -0
  78. pybinaryguard/scoring/__init__.py +19 -0
  79. pybinaryguard/scoring/engine.py +416 -0
  80. pybinaryguard/snapshot/__init__.py +20 -0
  81. pybinaryguard/snapshot/generator.py +168 -0
  82. pybinaryguard/snapshot/lockfile.py +152 -0
  83. pybinaryguard/snapshot/verifier.py +315 -0
  84. pybinaryguard/validators/__init__.py +7 -0
  85. pybinaryguard/validators/import_validator.py +231 -0
  86. pybinaryguard-1.0.0.dist-info/METADATA +876 -0
  87. pybinaryguard-1.0.0.dist-info/RECORD +91 -0
  88. pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
  89. pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
  90. pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
  91. 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)
@@ -0,0 +1,7 @@
1
+ """Support for ``python -m pybinaryguard``."""
2
+
3
+ import sys
4
+
5
+ from pybinaryguard.cli.main import main
6
+
7
+ sys.exit(main())
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)