tnfr 4.5.2__py3-none-any.whl → 8.5.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.

Potentially problematic release.


This version of tnfr might be problematic. Click here for more details.

Files changed (365) hide show
  1. tnfr/__init__.py +334 -50
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +214 -37
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +149 -556
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +51 -16
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +344 -32
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +676 -50
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +18 -3
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/{constants/core.py → config/defaults_core.py} +59 -6
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +51 -133
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +3 -1
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +9 -15
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +213 -633
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +2699 -398
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +496 -102
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +10 -5
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +77 -55
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +29 -50
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +66 -53
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +144 -57
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +70 -30
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +22 -16
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +5 -241
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.pyi +7 -0
  139. tnfr/mathematics/__init__.py +79 -0
  140. tnfr/mathematics/backend.py +453 -0
  141. tnfr/mathematics/backend.pyi +99 -0
  142. tnfr/mathematics/dynamics.py +408 -0
  143. tnfr/mathematics/dynamics.pyi +90 -0
  144. tnfr/mathematics/epi.py +391 -0
  145. tnfr/mathematics/epi.pyi +65 -0
  146. tnfr/mathematics/generators.py +242 -0
  147. tnfr/mathematics/generators.pyi +29 -0
  148. tnfr/mathematics/metrics.py +119 -0
  149. tnfr/mathematics/metrics.pyi +16 -0
  150. tnfr/mathematics/operators.py +239 -0
  151. tnfr/mathematics/operators.pyi +59 -0
  152. tnfr/mathematics/operators_factory.py +124 -0
  153. tnfr/mathematics/operators_factory.pyi +11 -0
  154. tnfr/mathematics/projection.py +87 -0
  155. tnfr/mathematics/projection.pyi +33 -0
  156. tnfr/mathematics/runtime.py +182 -0
  157. tnfr/mathematics/runtime.pyi +64 -0
  158. tnfr/mathematics/spaces.py +256 -0
  159. tnfr/mathematics/spaces.pyi +83 -0
  160. tnfr/mathematics/transforms.py +305 -0
  161. tnfr/mathematics/transforms.pyi +62 -0
  162. tnfr/metrics/__init__.py +47 -9
  163. tnfr/metrics/__init__.pyi +20 -0
  164. tnfr/metrics/buffer_cache.py +163 -0
  165. tnfr/metrics/buffer_cache.pyi +24 -0
  166. tnfr/metrics/cache_utils.py +214 -0
  167. tnfr/metrics/coherence.py +1510 -330
  168. tnfr/metrics/coherence.pyi +129 -0
  169. tnfr/metrics/common.py +23 -16
  170. tnfr/metrics/common.pyi +35 -0
  171. tnfr/metrics/core.py +251 -36
  172. tnfr/metrics/core.pyi +13 -0
  173. tnfr/metrics/diagnosis.py +709 -110
  174. tnfr/metrics/diagnosis.pyi +86 -0
  175. tnfr/metrics/emergence.py +245 -0
  176. tnfr/metrics/export.py +60 -18
  177. tnfr/metrics/export.pyi +7 -0
  178. tnfr/metrics/glyph_timing.py +233 -43
  179. tnfr/metrics/glyph_timing.pyi +81 -0
  180. tnfr/metrics/learning_metrics.py +280 -0
  181. tnfr/metrics/learning_metrics.pyi +21 -0
  182. tnfr/metrics/phase_coherence.py +351 -0
  183. tnfr/metrics/phase_compatibility.py +349 -0
  184. tnfr/metrics/reporting.py +63 -28
  185. tnfr/metrics/reporting.pyi +25 -0
  186. tnfr/metrics/sense_index.py +1126 -43
  187. tnfr/metrics/sense_index.pyi +9 -0
  188. tnfr/metrics/trig.py +215 -23
  189. tnfr/metrics/trig.pyi +13 -0
  190. tnfr/metrics/trig_cache.py +148 -24
  191. tnfr/metrics/trig_cache.pyi +10 -0
  192. tnfr/multiscale/__init__.py +32 -0
  193. tnfr/multiscale/hierarchical.py +517 -0
  194. tnfr/node.py +646 -140
  195. tnfr/node.pyi +139 -0
  196. tnfr/observers.py +160 -45
  197. tnfr/observers.pyi +31 -0
  198. tnfr/ontosim.py +23 -19
  199. tnfr/ontosim.pyi +28 -0
  200. tnfr/operators/__init__.py +1358 -106
  201. tnfr/operators/__init__.pyi +31 -0
  202. tnfr/operators/algebra.py +277 -0
  203. tnfr/operators/canonical_patterns.py +420 -0
  204. tnfr/operators/cascade.py +267 -0
  205. tnfr/operators/cycle_detection.py +358 -0
  206. tnfr/operators/definitions.py +4108 -0
  207. tnfr/operators/definitions.pyi +78 -0
  208. tnfr/operators/grammar.py +1164 -0
  209. tnfr/operators/grammar.pyi +140 -0
  210. tnfr/operators/hamiltonian.py +710 -0
  211. tnfr/operators/health_analyzer.py +809 -0
  212. tnfr/operators/jitter.py +107 -38
  213. tnfr/operators/jitter.pyi +11 -0
  214. tnfr/operators/lifecycle.py +314 -0
  215. tnfr/operators/metabolism.py +618 -0
  216. tnfr/operators/metrics.py +2138 -0
  217. tnfr/operators/network_analysis/__init__.py +27 -0
  218. tnfr/operators/network_analysis/source_detection.py +186 -0
  219. tnfr/operators/nodal_equation.py +395 -0
  220. tnfr/operators/pattern_detection.py +660 -0
  221. tnfr/operators/patterns.py +669 -0
  222. tnfr/operators/postconditions/__init__.py +38 -0
  223. tnfr/operators/postconditions/mutation.py +236 -0
  224. tnfr/operators/preconditions/__init__.py +1226 -0
  225. tnfr/operators/preconditions/coherence.py +305 -0
  226. tnfr/operators/preconditions/dissonance.py +236 -0
  227. tnfr/operators/preconditions/emission.py +128 -0
  228. tnfr/operators/preconditions/mutation.py +580 -0
  229. tnfr/operators/preconditions/reception.py +125 -0
  230. tnfr/operators/preconditions/resonance.py +364 -0
  231. tnfr/operators/registry.py +74 -0
  232. tnfr/operators/registry.pyi +9 -0
  233. tnfr/operators/remesh.py +1415 -91
  234. tnfr/operators/remesh.pyi +26 -0
  235. tnfr/operators/structural_units.py +268 -0
  236. tnfr/operators/unified_grammar.py +105 -0
  237. tnfr/parallel/__init__.py +54 -0
  238. tnfr/parallel/auto_scaler.py +234 -0
  239. tnfr/parallel/distributed.py +384 -0
  240. tnfr/parallel/engine.py +238 -0
  241. tnfr/parallel/gpu_engine.py +420 -0
  242. tnfr/parallel/monitoring.py +248 -0
  243. tnfr/parallel/partitioner.py +459 -0
  244. tnfr/py.typed +0 -0
  245. tnfr/recipes/__init__.py +22 -0
  246. tnfr/recipes/cookbook.py +743 -0
  247. tnfr/rng.py +75 -151
  248. tnfr/rng.pyi +26 -0
  249. tnfr/schemas/__init__.py +8 -0
  250. tnfr/schemas/grammar.json +94 -0
  251. tnfr/sdk/__init__.py +107 -0
  252. tnfr/sdk/__init__.pyi +19 -0
  253. tnfr/sdk/adaptive_system.py +173 -0
  254. tnfr/sdk/adaptive_system.pyi +21 -0
  255. tnfr/sdk/builders.py +370 -0
  256. tnfr/sdk/builders.pyi +51 -0
  257. tnfr/sdk/fluent.py +1121 -0
  258. tnfr/sdk/fluent.pyi +74 -0
  259. tnfr/sdk/templates.py +342 -0
  260. tnfr/sdk/templates.pyi +41 -0
  261. tnfr/sdk/utils.py +341 -0
  262. tnfr/secure_config.py +46 -0
  263. tnfr/security/__init__.py +70 -0
  264. tnfr/security/database.py +514 -0
  265. tnfr/security/subprocess.py +503 -0
  266. tnfr/security/validation.py +290 -0
  267. tnfr/selector.py +59 -22
  268. tnfr/selector.pyi +19 -0
  269. tnfr/sense.py +92 -67
  270. tnfr/sense.pyi +23 -0
  271. tnfr/services/__init__.py +17 -0
  272. tnfr/services/orchestrator.py +325 -0
  273. tnfr/sparse/__init__.py +39 -0
  274. tnfr/sparse/representations.py +492 -0
  275. tnfr/structural.py +639 -263
  276. tnfr/structural.pyi +83 -0
  277. tnfr/telemetry/__init__.py +35 -0
  278. tnfr/telemetry/cache_metrics.py +226 -0
  279. tnfr/telemetry/cache_metrics.pyi +64 -0
  280. tnfr/telemetry/nu_f.py +422 -0
  281. tnfr/telemetry/nu_f.pyi +108 -0
  282. tnfr/telemetry/verbosity.py +36 -0
  283. tnfr/telemetry/verbosity.pyi +15 -0
  284. tnfr/tokens.py +2 -4
  285. tnfr/tokens.pyi +36 -0
  286. tnfr/tools/__init__.py +20 -0
  287. tnfr/tools/domain_templates.py +478 -0
  288. tnfr/tools/sequence_generator.py +846 -0
  289. tnfr/topology/__init__.py +13 -0
  290. tnfr/topology/asymmetry.py +151 -0
  291. tnfr/trace.py +300 -126
  292. tnfr/trace.pyi +42 -0
  293. tnfr/tutorials/__init__.py +38 -0
  294. tnfr/tutorials/autonomous_evolution.py +285 -0
  295. tnfr/tutorials/interactive.py +1576 -0
  296. tnfr/tutorials/structural_metabolism.py +238 -0
  297. tnfr/types.py +743 -12
  298. tnfr/types.pyi +357 -0
  299. tnfr/units.py +68 -0
  300. tnfr/units.pyi +13 -0
  301. tnfr/utils/__init__.py +282 -0
  302. tnfr/utils/__init__.pyi +215 -0
  303. tnfr/utils/cache.py +4223 -0
  304. tnfr/utils/cache.pyi +470 -0
  305. tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
  306. tnfr/utils/callbacks.pyi +49 -0
  307. tnfr/utils/chunks.py +108 -0
  308. tnfr/utils/chunks.pyi +22 -0
  309. tnfr/utils/data.py +428 -0
  310. tnfr/utils/data.pyi +74 -0
  311. tnfr/utils/graph.py +85 -0
  312. tnfr/utils/graph.pyi +10 -0
  313. tnfr/utils/init.py +821 -0
  314. tnfr/utils/init.pyi +80 -0
  315. tnfr/utils/io.py +559 -0
  316. tnfr/utils/io.pyi +66 -0
  317. tnfr/{helpers → utils}/numeric.py +51 -24
  318. tnfr/utils/numeric.pyi +21 -0
  319. tnfr/validation/__init__.py +257 -0
  320. tnfr/validation/__init__.pyi +85 -0
  321. tnfr/validation/compatibility.py +460 -0
  322. tnfr/validation/compatibility.pyi +6 -0
  323. tnfr/validation/config.py +73 -0
  324. tnfr/validation/graph.py +139 -0
  325. tnfr/validation/graph.pyi +18 -0
  326. tnfr/validation/input_validation.py +755 -0
  327. tnfr/validation/invariants.py +712 -0
  328. tnfr/validation/rules.py +253 -0
  329. tnfr/validation/rules.pyi +44 -0
  330. tnfr/validation/runtime.py +279 -0
  331. tnfr/validation/runtime.pyi +28 -0
  332. tnfr/validation/sequence_validator.py +162 -0
  333. tnfr/validation/soft_filters.py +170 -0
  334. tnfr/validation/soft_filters.pyi +32 -0
  335. tnfr/validation/spectral.py +164 -0
  336. tnfr/validation/spectral.pyi +42 -0
  337. tnfr/validation/validator.py +1266 -0
  338. tnfr/validation/window.py +39 -0
  339. tnfr/validation/window.pyi +1 -0
  340. tnfr/visualization/__init__.py +98 -0
  341. tnfr/visualization/cascade_viz.py +256 -0
  342. tnfr/visualization/hierarchy.py +284 -0
  343. tnfr/visualization/sequence_plotter.py +784 -0
  344. tnfr/viz/__init__.py +60 -0
  345. tnfr/viz/matplotlib.py +278 -0
  346. tnfr/viz/matplotlib.pyi +35 -0
  347. tnfr-8.5.0.dist-info/METADATA +573 -0
  348. tnfr-8.5.0.dist-info/RECORD +353 -0
  349. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
  350. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
  351. tnfr/collections_utils.py +0 -300
  352. tnfr/config.py +0 -32
  353. tnfr/grammar.py +0 -344
  354. tnfr/graph_utils.py +0 -84
  355. tnfr/helpers/__init__.py +0 -71
  356. tnfr/import_utils.py +0 -228
  357. tnfr/json_utils.py +0 -162
  358. tnfr/logging_utils.py +0 -116
  359. tnfr/presets.py +0 -60
  360. tnfr/validators.py +0 -84
  361. tnfr/value_utils.py +0 -59
  362. tnfr-4.5.2.dist-info/METADATA +0 -379
  363. tnfr-4.5.2.dist-info/RECORD +0 -67
  364. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  365. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,503 @@
1
+ """Command execution security utilities for TNFR.
2
+
3
+ This module provides secure wrappers for subprocess execution and input validation
4
+ to prevent command injection attacks while maintaining TNFR structural coherence.
5
+
6
+ TNFR Context
7
+ ------------
8
+ These utilities ensure that external process execution maintains the integrity of
9
+ the TNFR computational environment without introducing security vulnerabilities.
10
+ They act as a coherence boundary between user input and system command execution.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import re
17
+ import subprocess
18
+ from pathlib import Path
19
+ from typing import Any, Sequence
20
+
21
+ __all__ = [
22
+ "validate_git_ref",
23
+ "validate_path_safe",
24
+ "validate_file_path",
25
+ "resolve_safe_path",
26
+ "validate_version_string",
27
+ "run_command_safely",
28
+ "CommandValidationError",
29
+ "PathTraversalError",
30
+ ]
31
+
32
+
33
+ class CommandValidationError(ValueError):
34
+ """Raised when command input validation fails."""
35
+
36
+
37
+ class PathTraversalError(ValueError):
38
+ """Raised when path traversal attempt is detected."""
39
+
40
+
41
+ # Allowlisted commands that are safe to execute
42
+ ALLOWED_COMMANDS = frozenset(
43
+ {
44
+ "git",
45
+ "python",
46
+ "python3",
47
+ "stubgen",
48
+ "gh",
49
+ "pip",
50
+ "twine",
51
+ }
52
+ )
53
+
54
+ # Pattern for valid git refs (branches, tags, commit SHAs)
55
+ GIT_REF_PATTERN = re.compile(r"^[a-zA-Z0-9/_\-\.]+$")
56
+
57
+ # Pattern for semantic version strings
58
+ VERSION_PATTERN = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(-[a-zA-Z0-9\-\.]+)?$")
59
+
60
+ # Pattern for safe path components (no path traversal)
61
+ SAFE_PATH_PATTERN = re.compile(r"^[a-zA-Z0-9/_\-\.]+$")
62
+
63
+
64
+ def validate_git_ref(ref: str) -> str:
65
+ """Validate a git reference (branch, tag, or SHA).
66
+
67
+ Parameters
68
+ ----------
69
+ ref : str
70
+ The git reference to validate.
71
+
72
+ Returns
73
+ -------
74
+ str
75
+ The validated reference.
76
+
77
+ Raises
78
+ ------
79
+ CommandValidationError
80
+ If the reference contains invalid characters.
81
+
82
+ Examples
83
+ --------
84
+ >>> validate_git_ref("main")
85
+ 'main'
86
+ >>> validate_git_ref("feature/new-operator")
87
+ 'feature/new-operator'
88
+ >>> validate_git_ref("v1.0.0")
89
+ 'v1.0.0'
90
+ >>> validate_git_ref("abc123def")
91
+ 'abc123def'
92
+ """
93
+ if not ref:
94
+ raise CommandValidationError("Git reference cannot be empty")
95
+
96
+ if not GIT_REF_PATTERN.match(ref):
97
+ raise CommandValidationError(
98
+ f"Invalid git reference: {ref!r}. "
99
+ "References must contain only alphanumeric characters, "
100
+ "hyphens, underscores, slashes, and dots."
101
+ )
102
+
103
+ # Additional security: prevent path traversal patterns
104
+ if ".." in ref or ref.startswith("/") or ref.startswith("~"):
105
+ raise CommandValidationError(
106
+ f"Invalid git reference: {ref!r}. "
107
+ "References cannot contain path traversal patterns."
108
+ )
109
+
110
+ return ref
111
+
112
+
113
+ def validate_version_string(version: str) -> str:
114
+ """Validate a semantic version string.
115
+
116
+ Parameters
117
+ ----------
118
+ version : str
119
+ The version string to validate.
120
+
121
+ Returns
122
+ -------
123
+ str
124
+ The validated version string.
125
+
126
+ Raises
127
+ ------
128
+ CommandValidationError
129
+ If the version string is invalid.
130
+
131
+ Examples
132
+ --------
133
+ >>> validate_version_string("1.0.0")
134
+ '1.0.0'
135
+ >>> validate_version_string("v16.2.3")
136
+ 'v16.2.3'
137
+ >>> validate_version_string("2.0.0-beta.1")
138
+ '2.0.0-beta.1'
139
+ """
140
+ if not version:
141
+ raise CommandValidationError("Version string cannot be empty")
142
+
143
+ if not VERSION_PATTERN.match(version):
144
+ raise CommandValidationError(
145
+ f"Invalid version string: {version!r}. "
146
+ "Version must follow semantic versioning (e.g., '1.0.0' or 'v1.0.0')."
147
+ )
148
+
149
+ return version
150
+
151
+
152
+ def validate_path_safe(path: str | Path) -> Path:
153
+ """Validate that a path is safe (no path traversal attacks).
154
+
155
+ .. deprecated:: 0.2
156
+ Use :func:`validate_file_path` instead for more comprehensive validation.
157
+
158
+ Parameters
159
+ ----------
160
+ path : str | Path
161
+ The path to validate.
162
+
163
+ Returns
164
+ -------
165
+ Path
166
+ The validated path as a Path object.
167
+
168
+ Raises
169
+ ------
170
+ CommandValidationError
171
+ If the path contains unsafe patterns.
172
+
173
+ Examples
174
+ --------
175
+ >>> validate_path_safe("src/tnfr/core.py")
176
+ PosixPath('src/tnfr/core.py')
177
+ >>> validate_path_safe(Path("tests/unit"))
178
+ PosixPath('tests/unit')
179
+ """
180
+ path_obj = Path(path)
181
+ path_str = str(path_obj)
182
+
183
+ # Check for absolute paths in untrusted input
184
+ if path_obj.is_absolute():
185
+ raise CommandValidationError(
186
+ f"Absolute paths not allowed in user input: {path_str!r}"
187
+ )
188
+
189
+ # Check for path traversal
190
+ if ".." in path_obj.parts:
191
+ raise CommandValidationError(f"Path traversal not allowed: {path_str!r}")
192
+
193
+ # Check for special characters that could be exploited
194
+ if not SAFE_PATH_PATTERN.match(path_str):
195
+ raise CommandValidationError(f"Path contains invalid characters: {path_str!r}")
196
+
197
+ return path_obj
198
+
199
+
200
+ def validate_file_path(
201
+ path: str | Path,
202
+ *,
203
+ allow_absolute: bool = False,
204
+ allowed_extensions: Sequence[str] | None = None,
205
+ ) -> Path:
206
+ """Validate file path to prevent path traversal and unauthorized access.
207
+
208
+ This function provides comprehensive path validation to prevent:
209
+ - Path traversal attacks (../../../etc/passwd)
210
+ - Unauthorized file access
211
+ - Special character exploits
212
+ - Symlink attacks
213
+
214
+ TNFR Context
215
+ ------------
216
+ Maintains structural coherence by ensuring file operations preserve:
217
+ - Configuration integrity (EPI structure preservation)
218
+ - Data export authenticity (coherence metrics validity)
219
+ - Model persistence safety (NFR state protection)
220
+
221
+ Parameters
222
+ ----------
223
+ path : str | Path
224
+ The file path to validate.
225
+ allow_absolute : bool, default=False
226
+ Whether to allow absolute paths. Default is False for user input.
227
+ allowed_extensions : Sequence[str] | None, default=None
228
+ List of allowed file extensions (e.g., ['.json', '.yaml', '.toml']).
229
+ If None, any extension is allowed.
230
+
231
+ Returns
232
+ -------
233
+ Path
234
+ The validated path as a Path object.
235
+
236
+ Raises
237
+ ------
238
+ PathTraversalError
239
+ If path traversal patterns are detected.
240
+ ValueError
241
+ If the path is invalid or contains unsafe patterns.
242
+
243
+ Examples
244
+ --------
245
+ >>> validate_file_path("config.json", allowed_extensions=['.json', '.yaml'])
246
+ PosixPath('config.json')
247
+
248
+ >>> validate_file_path("data/export.csv")
249
+ PosixPath('data/export.csv')
250
+
251
+ >>> validate_file_path("../../../etc/passwd") # doctest: +IGNORE_EXCEPTION_DETAIL
252
+ Traceback (most recent call last):
253
+ ...
254
+ PathTraversalError: Path traversal detected
255
+ """
256
+ if not path:
257
+ raise ValueError("Path cannot be empty")
258
+
259
+ # Convert to Path object
260
+ path_obj = Path(path)
261
+ path_str = str(path)
262
+ path_parts = Path(path).parts
263
+
264
+ # Check for null bytes (common in exploit attempts) - do this before resolve()
265
+ if "\x00" in path_str:
266
+ raise ValueError(f"Null byte detected in path: {path!r}")
267
+
268
+ # Check for path traversal attempts in the original path first
269
+ if ".." in path_parts:
270
+ raise PathTraversalError(
271
+ f"Path traversal detected in {path!r}. "
272
+ "Relative parent directory references (..) are not allowed."
273
+ )
274
+
275
+ # Normalize the path to resolve any . or .. components
276
+ try:
277
+ # Use resolve() with strict=False to normalize without checking existence
278
+ normalized = path_obj.resolve()
279
+ except (OSError, RuntimeError, ValueError) as e:
280
+ # Catch embedded null byte errors from resolve()
281
+ error_msg = str(e)
282
+ if "null byte" in error_msg.lower():
283
+ raise ValueError(f"Null byte detected in path: {path!r}") from e
284
+ raise ValueError(f"Invalid path: {path}") from e
285
+
286
+ # Check for absolute paths if not allowed
287
+ if not allow_absolute and normalized.is_absolute():
288
+ # For relative paths, ensure they don't escape to absolute paths
289
+ if not Path(path).is_absolute():
290
+ # This is a relative path that was resolved to absolute
291
+ # We need to check if it contains .. components
292
+ pass
293
+ else:
294
+ raise ValueError(
295
+ f"Absolute paths not allowed: {path}. "
296
+ "Use allow_absolute=True if this is intentional."
297
+ )
298
+
299
+ # Check for other dangerous patterns
300
+ dangerous_patterns = [
301
+ ("~", "Home directory expansion"),
302
+ ("\n", "Newline character"),
303
+ ("\r", "Carriage return"),
304
+ ]
305
+
306
+ for pattern, desc in dangerous_patterns:
307
+ if pattern in path_str:
308
+ raise ValueError(f"{desc} not allowed in path: {path!r}")
309
+
310
+ # Validate file extension if restrictions are specified
311
+ if allowed_extensions is not None:
312
+ suffix = path_obj.suffix.lower()
313
+ allowed_lower = [ext.lower() for ext in allowed_extensions]
314
+ if suffix not in allowed_lower:
315
+ raise ValueError(
316
+ f"File extension {suffix!r} not allowed. "
317
+ f"Allowed extensions: {allowed_extensions}"
318
+ )
319
+
320
+ return path_obj
321
+
322
+
323
+ def resolve_safe_path(
324
+ path: str | Path,
325
+ base_dir: str | Path,
326
+ *,
327
+ must_exist: bool = False,
328
+ allowed_extensions: Sequence[str] | None = None,
329
+ ) -> Path:
330
+ """Resolve a path safely within a base directory.
331
+
332
+ This function ensures that the resolved path stays within the specified
333
+ base directory, preventing path traversal attacks while allowing normal
334
+ subdirectory navigation.
335
+
336
+ TNFR Context
337
+ ------------
338
+ Ensures configuration and data files maintain operational fractality by
339
+ restricting file access to designated structural boundaries (base directories).
340
+
341
+ Parameters
342
+ ----------
343
+ path : str | Path
344
+ The path to resolve (can be relative or absolute).
345
+ base_dir : str | Path
346
+ The base directory that the path must stay within.
347
+ must_exist : bool, default=False
348
+ If True, raise ValueError if the resolved path doesn't exist.
349
+ allowed_extensions : Sequence[str] | None, default=None
350
+ List of allowed file extensions.
351
+
352
+ Returns
353
+ -------
354
+ Path
355
+ The validated, resolved absolute path.
356
+
357
+ Raises
358
+ ------
359
+ PathTraversalError
360
+ If the resolved path escapes the base directory.
361
+ ValueError
362
+ If the path is invalid or doesn't meet requirements.
363
+
364
+ Examples
365
+ --------
366
+ >>> base = Path("/home/user/tnfr")
367
+ >>> resolve_safe_path("config/settings.json", base) # doctest: +SKIP
368
+ PosixPath('/home/user/tnfr/config/settings.json')
369
+
370
+ >>> resolve_safe_path("../../../etc/passwd", base) # doctest: +SKIP +IGNORE_EXCEPTION_DETAIL
371
+ Traceback (most recent call last):
372
+ ...
373
+ PathTraversalError: Path escapes base directory
374
+ """
375
+ if not path:
376
+ raise ValueError("Path cannot be empty")
377
+ if not base_dir:
378
+ raise ValueError("Base directory cannot be empty")
379
+
380
+ # First validate the path itself
381
+ path_obj = validate_file_path(
382
+ path,
383
+ allow_absolute=True,
384
+ allowed_extensions=allowed_extensions,
385
+ )
386
+
387
+ # Resolve base directory to absolute path
388
+ base_path = Path(base_dir).resolve()
389
+
390
+ # Resolve the target path
391
+ # If path is relative, resolve it relative to base_dir
392
+ if not path_obj.is_absolute():
393
+ resolved = (base_path / path_obj).resolve()
394
+ else:
395
+ resolved = path_obj.resolve()
396
+
397
+ # Security check: ensure resolved path is within base directory
398
+ try:
399
+ resolved.relative_to(base_path)
400
+ except ValueError as e:
401
+ raise PathTraversalError(
402
+ f"Path {path!r} escapes base directory {base_dir!r}. "
403
+ f"Resolved path: {resolved}"
404
+ ) from e
405
+
406
+ # Check existence if required
407
+ if must_exist and not resolved.exists():
408
+ raise ValueError(f"Path does not exist: {resolved}")
409
+
410
+ return resolved
411
+
412
+
413
+ def run_command_safely(
414
+ command: Sequence[str],
415
+ *,
416
+ check: bool = True,
417
+ capture_output: bool = True,
418
+ text: bool = True,
419
+ timeout: int | None = None,
420
+ cwd: str | Path | None = None,
421
+ env: dict[str, str] | None = None,
422
+ ) -> subprocess.CompletedProcess[Any]:
423
+ """Execute a command safely with validation.
424
+
425
+ This function provides a secure wrapper around subprocess.run that:
426
+ 1. Never uses shell=True
427
+ 2. Validates the command is in the allowlist
428
+ 3. Ensures all arguments are strings
429
+ 4. Provides timeout protection
430
+
431
+ Parameters
432
+ ----------
433
+ command : Sequence[str]
434
+ Command and arguments as a list of strings.
435
+ check : bool, optional
436
+ If True, raise CalledProcessError on non-zero exit. Default is True.
437
+ capture_output : bool, optional
438
+ If True, capture stdout and stderr. Default is True.
439
+ text : bool, optional
440
+ If True, decode output as text. Default is True.
441
+ timeout : int | None, optional
442
+ Maximum time in seconds to wait for command completion.
443
+ cwd : str | Path | None, optional
444
+ Working directory for command execution.
445
+ env : dict[str, str] | None, optional
446
+ Environment variables for the subprocess.
447
+
448
+ Returns
449
+ -------
450
+ subprocess.CompletedProcess
451
+ The result of the command execution.
452
+
453
+ Raises
454
+ ------
455
+ CommandValidationError
456
+ If the command is not in the allowlist or arguments are invalid.
457
+ subprocess.CalledProcessError
458
+ If check=True and the command returns non-zero exit code.
459
+ subprocess.TimeoutExpired
460
+ If timeout is exceeded.
461
+
462
+ Examples
463
+ --------
464
+ >>> result = run_command_safely(["git", "status"])
465
+ >>> result.returncode
466
+ 0
467
+ >>> result = run_command_safely(["git", "log", "-1", "--oneline"])
468
+ """
469
+ if not command:
470
+ raise CommandValidationError("Command cannot be empty")
471
+
472
+ # Validate all arguments are strings
473
+ if not all(isinstance(arg, str) for arg in command):
474
+ raise CommandValidationError(
475
+ "All command arguments must be strings. "
476
+ f"Got: {[type(arg).__name__ for arg in command]}"
477
+ )
478
+
479
+ # Extract base command (handle paths like /usr/bin/python)
480
+ base_cmd = Path(command[0]).name
481
+
482
+ # Validate command is in allowlist
483
+ if base_cmd not in ALLOWED_COMMANDS:
484
+ raise CommandValidationError(
485
+ f"Command not in allowlist: {base_cmd!r}. "
486
+ f"Allowed commands: {sorted(ALLOWED_COMMANDS)}"
487
+ )
488
+
489
+ # Validate cwd if provided
490
+ if cwd is not None:
491
+ cwd = str(cwd)
492
+
493
+ # Execute with shell=False (explicit for clarity)
494
+ return subprocess.run(
495
+ list(command),
496
+ check=check,
497
+ capture_output=capture_output,
498
+ text=text,
499
+ timeout=timeout,
500
+ cwd=cwd,
501
+ env=env,
502
+ shell=False, # CRITICAL: Never use shell=True
503
+ )