redeploy 0.2.70__tar.gz → 0.2.71__tar.gz

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 (256) hide show
  1. {redeploy-0.2.70 → redeploy-0.2.71}/PKG-INFO +6 -6
  2. {redeploy-0.2.70 → redeploy-0.2.71}/README.md +5 -5
  3. {redeploy-0.2.70 → redeploy-0.2.71}/pyproject.toml +1 -1
  4. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/__init__.py +1 -1
  5. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/handlers.py +8 -14
  6. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/exec_.py +4 -11
  7. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/handlers/display.py +30 -4
  8. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/probes.py +7 -1
  9. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/runner.py +19 -2
  10. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/__init__.py +2 -0
  11. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/parser.py +30 -0
  12. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/mcp_server.py +42 -0
  13. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_heal.py +56 -0
  14. redeploy-0.2.71/redeploy/tests/test_security_hardening.py +93 -0
  15. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/PKG-INFO +6 -6
  16. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/SOURCES.txt +1 -0
  17. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/__init__.py +0 -0
  18. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/checkers/__init__.py +0 -0
  19. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/preflight_schema.py +0 -0
  20. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/spec_analyzer.py +0 -0
  21. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/__init__.py +0 -0
  22. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/exceptions.py +0 -0
  23. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/executor.py +0 -0
  24. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/progress.py +0 -0
  25. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/rollback.py +0 -0
  26. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/state.py +0 -0
  27. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/state_apply.py +0 -0
  28. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/utils/__init__.py +0 -0
  29. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/utils/run_container_build.py +0 -0
  30. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/audit.py +0 -0
  31. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/__init__.py +0 -0
  32. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/extractor.py +0 -0
  33. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/generators/__init__.py +0 -0
  34. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/generators/docker_compose.py +0 -0
  35. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/generators/migration.py +0 -0
  36. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/__init__.py +0 -0
  37. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/compose.py +0 -0
  38. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/hardware.py +0 -0
  39. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/infra.py +0 -0
  40. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/migration.py +0 -0
  41. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/__init__.py +0 -0
  42. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/__init__.py +0 -0
  43. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/audit.py +0 -0
  44. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/blueprint.py +0 -0
  45. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/bump_fix.py +0 -0
  46. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/detect.py +0 -0
  47. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/device_map.py +0 -0
  48. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/device_map_renderers.py +0 -0
  49. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/devices.py +0 -0
  50. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/diagnose.py +0 -0
  51. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/diff.py +0 -0
  52. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/export.py +0 -0
  53. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/gh_workflow.py +0 -0
  54. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/hardware.py +0 -0
  55. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/import_.py +0 -0
  56. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/init.py +0 -0
  57. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/inspect.py +0 -0
  58. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/lint.py +0 -0
  59. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/mcp_cmd.py +0 -0
  60. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/patterns.py +0 -0
  61. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/plan_apply.py +0 -0
  62. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/plugin.py +0 -0
  63. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/probe.py +0 -0
  64. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/prompt_cmd.py +0 -0
  65. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/push.py +0 -0
  66. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/state.py +0 -0
  67. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/status.py +0 -0
  68. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/target.py +0 -0
  69. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/__init__.py +0 -0
  70. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/commands.py +0 -0
  71. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/helpers.py +0 -0
  72. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/monorepo.py +0 -0
  73. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/release.py +0 -0
  74. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/scanner.py +0 -0
  75. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/__init__.py +0 -0
  76. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
  77. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/git_config.py +0 -0
  78. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/workflow.py +0 -0
  79. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/core.py +0 -0
  80. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/display.py +0 -0
  81. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/query.py +0 -0
  82. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli.py +0 -0
  83. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/__init__.py +0 -0
  84. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/applier.py +0 -0
  85. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/handlers/__init__.py +0 -0
  86. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/loader.py +0 -0
  87. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/data_sync.py +0 -0
  88. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/__init__.py +0 -0
  89. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/builtin/__init__.py +0 -0
  90. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/detector.py +0 -0
  91. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/hardware.py +0 -0
  92. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/hardware_rules.py +0 -0
  93. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/remote.py +0 -0
  94. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/templates.py +0 -0
  95. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/workflow.py +0 -0
  96. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/discovery.py +0 -0
  97. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl/__init__.py +0 -0
  98. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl/loader.py +0 -0
  99. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl/parser.py +0 -0
  100. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/__init__.py +0 -0
  101. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/context.py +0 -0
  102. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/decorators.py +0 -0
  103. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/docker_steps.py +0 -0
  104. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/exceptions.py +0 -0
  105. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/runner.py +0 -0
  106. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/steps.py +0 -0
  107. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/fleet.py +0 -0
  108. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/__init__.py +0 -0
  109. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/config_txt.py +0 -0
  110. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/__init__.py +0 -0
  111. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/hyperpixel.py +0 -0
  112. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/official.py +0 -0
  113. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/waveshare.py +0 -0
  114. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/fixes.py +0 -0
  115. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/__init__.py +0 -0
  116. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/autostart.py +0 -0
  117. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/browsers.py +0 -0
  118. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/compositors.py +0 -0
  119. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/output_profiles.py +0 -0
  120. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/panels.py +0 -0
  121. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/raspi_config.py +0 -0
  122. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/__init__.py +0 -0
  123. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/decider.py +0 -0
  124. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/hint_provider.py +0 -0
  125. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/log_writer.py +0 -0
  126. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/loop_detector.py +0 -0
  127. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal.py +0 -0
  128. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/__init__.py +0 -0
  129. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/base.py +0 -0
  130. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/config_hints.py +0 -0
  131. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/docker_compose.py +0 -0
  132. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/parsers/__init__.py +0 -0
  133. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/parsers/compose.py +0 -0
  134. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/registry.py +0 -0
  135. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/integrations/__init__.py +0 -0
  136. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/integrations/op3_bridge.py +0 -0
  137. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/compiler.py +0 -0
  138. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/models.py +0 -0
  139. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/__init__.py +0 -0
  140. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/blueprint.py +0 -0
  141. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/devices.py +0 -0
  142. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/enums.py +0 -0
  143. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/hardware.py +0 -0
  144. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/infra.py +0 -0
  145. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/manifest.py +0 -0
  146. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/persisted.py +0 -0
  147. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/pipeline.py +0 -0
  148. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/plan.py +0 -0
  149. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/spec.py +0 -0
  150. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/observe.py +0 -0
  151. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/parse.py +0 -0
  152. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/patterns.py +0 -0
  153. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plan/__init__.py +0 -0
  154. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plan/planner.py +0 -0
  155. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/__init__.py +0 -0
  156. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/__init__.py +0 -0
  157. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/browser_reload.py +0 -0
  158. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
  159. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/notify.py +0 -0
  160. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/process_control.py +0 -0
  161. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/systemd_reload.py +0 -0
  162. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/schema.py +0 -0
  163. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/spec_loader.py +0 -0
  164. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/ssh.py +0 -0
  165. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/__init__.py +0 -0
  166. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/docker.py +0 -0
  167. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/generic.py +0 -0
  168. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/hardware.py +0 -0
  169. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/k3s.py +0 -0
  170. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/kiosk.py +0 -0
  171. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/podman.py +0 -0
  172. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/process.py +0 -0
  173. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/scm.py +0 -0
  174. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/transfer.py +0 -0
  175. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/__init__.py +0 -0
  176. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_analyze.py +0 -0
  177. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_audit.py +0 -0
  178. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_cli.py +0 -0
  179. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_data_sync.py +0 -0
  180. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_discovery.py +0 -0
  181. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_dsl.py +0 -0
  182. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_examples.py +0 -0
  183. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_executor.py +0 -0
  184. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_fleet.py +0 -0
  185. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_git_integration.py +0 -0
  186. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_heal_decider.py +0 -0
  187. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_hooks.py +0 -0
  188. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_iac.py +0 -0
  189. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_iac_compose_contract.py +0 -0
  190. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
  191. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_manifest.py +0 -0
  192. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_markpact_compiler.py +0 -0
  193. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_markpact_parser.py +0 -0
  194. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_models.py +0 -0
  195. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_observe.py +0 -0
  196. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_parse.py +0 -0
  197. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_patterns.py +0 -0
  198. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_planner_edge.py +0 -0
  199. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_plugins.py +0 -0
  200. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_probes.py +0 -0
  201. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_public_api.py +0 -0
  202. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_resume.py +0 -0
  203. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_spec_loader.py +0 -0
  204. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_ssh.py +0 -0
  205. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_steps_registry.py +0 -0
  206. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_templates.py +0 -0
  207. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_verify.py +0 -0
  208. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version.py +0 -0
  209. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_changelog.py +0 -0
  210. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_cli.py +0 -0
  211. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_manifest.py +0 -0
  212. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_sources.py +0 -0
  213. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_workflow.py +0 -0
  214. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/verify.py +0 -0
  215. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/__init__.py +0 -0
  216. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/bump.py +0 -0
  217. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/changelog.py +0 -0
  218. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/commits.py +0 -0
  219. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/diff.py +0 -0
  220. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/git_integration.py +0 -0
  221. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/git_transaction.py +0 -0
  222. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/manifest.py +0 -0
  223. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/__init__.py +0 -0
  224. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/base.py +0 -0
  225. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/json_.py +0 -0
  226. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/plain.py +0 -0
  227. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/regex.py +0 -0
  228. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/toml_.py +0 -0
  229. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/yaml_.py +0 -0
  230. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/transaction.py +0 -0
  231. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version.py +0 -0
  232. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/dependency_links.txt +0 -0
  233. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/entry_points.txt +0 -0
  234. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/requires.txt +0 -0
  235. {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/top_level.txt +0 -0
  236. {redeploy-0.2.70 → redeploy-0.2.71}/setup.cfg +0 -0
  237. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_blueprint_sources.py +0 -0
  238. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_blueprint_sources_compose.py +0 -0
  239. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_cli_gh_workflow.py +0 -0
  240. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_cli_query.py +0 -0
  241. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_example_argocd_flux_plugin.py +0 -0
  242. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_example_gitops_ci_plugin.py +0 -0
  243. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_example_helm_kustomize_plugin.py +0 -0
  244. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_hardware_config.py +0 -0
  245. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_hardware_kiosk.py +0 -0
  246. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_hardware_rules.py +0 -0
  247. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_heal_decider.py +0 -0
  248. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_heal_loop_detector.py +0 -0
  249. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_iac.py +0 -0
  250. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_iac_config_hints.py +0 -0
  251. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_iac_plugin_loading.py +0 -0
  252. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_import_plugin_template.py +0 -0
  253. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_parse.py +0 -0
  254. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_prompt_llm.py +0 -0
  255. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_redeploy.py +0 -0
  256. {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_steps_kiosk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.70
3
+ Version: 0.2.71
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
32
32
 
33
33
  ## AI Cost Tracking
34
34
 
35
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-36.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-37.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
37
37
 
38
- - 🤖 **LLM usage:** $7.5000 (91 commits)
39
- - 👤 **Human dev:** ~$3684 (36.8h @ $100/h, 30min dedup)
38
+ - 🤖 **LLM usage:** $7.5000 (92 commits)
39
+ - 👤 **Human dev:** ~$3734 (37.3h @ $100/h, 30min dedup)
40
40
 
41
41
  Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
42
 
43
43
  ---
44
44
 
45
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
46
 
47
47
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
48
48
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-36.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-37.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (91 commits)
10
- - 👤 **Human dev:** ~$3684 (36.8h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (92 commits)
10
+ - 👤 **Human dev:** ~$3734 (37.3h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
19
19
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redeploy"
7
- version = "0.2.70"
7
+ version = "0.2.71"
8
8
  description = "Infrastructure migration toolkit: detect → plan → apply"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
7
7
 
8
8
  Everything not listed in ``__all__`` is internal and may change without notice.
9
9
  """
10
- __version__ = "0.2.70"
10
+ __version__ = "0.2.71"
11
11
 
12
12
  # ── Core models ───────────────────────────────────────────────────────────────
13
13
  from .models import ( # noqa: F401
@@ -350,7 +350,7 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
350
350
  - "#section-id" - script from section in current spec file
351
351
  - "#kiosk-browser-configuration-script" - markpact:ref block
352
352
  """
353
- from ..markpact.parser import extract_script_from_markdown, extract_script_by_ref
353
+ from ..markpact import resolve_script_ref
354
354
 
355
355
  # Parse command_ref
356
356
  if "#" in command_ref:
@@ -367,21 +367,15 @@ def _resolve_command_ref(command_ref: str, step: MigrationStep, plan: MigrationP
367
367
  if not file_path.exists():
368
368
  raise StepError(step, f"Command ref file not found: {file_path}")
369
369
 
370
- # Extract script from markdown - try both methods:
371
- # 1. First try markpact:ref codeblock (new format)
372
- # 2. Then try section heading (old format)
373
370
  markdown_content = file_path.read_text(encoding="utf-8")
371
+ result = resolve_script_ref(markdown_content, section_id, language="bash")
374
372
 
375
- # Try markpact:ref format first
376
- script = extract_script_by_ref(markdown_content, section_id, language="bash")
377
-
378
- # Fallback to section heading format
379
- if script is None:
380
- script = extract_script_from_markdown(markdown_content, section_id, language="bash")
381
-
382
- if script is None:
383
- raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
384
-
373
+ if result is None:
374
+ raise StepError(
375
+ step,
376
+ f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)"
377
+ )
378
+ script, _ = result
385
379
  return script
386
380
 
387
381
 
@@ -202,19 +202,12 @@ def _extract_all_scripts(md_path, ref_list, console) -> list[tuple[str, str, str
202
202
 
203
203
  def _extract_script_for_ref(md_content, ref_id) -> tuple[str, str] | None:
204
204
  """Extract script for a single ref."""
205
- from ...markpact.parser import extract_script_from_markdown, extract_script_by_ref
206
-
207
- # Try markpact:ref first
208
- script = extract_script_by_ref(md_content, ref_id, language="bash")
209
- lookup_method = "markpact:ref"
210
-
211
- if script is None and ref_id.startswith("#"):
212
- script = extract_script_from_markdown(md_content, ref_id[1:], language="bash")
213
- lookup_method = "section"
205
+ from ...markpact import resolve_script_ref
214
206
 
215
- if script is None:
207
+ result = resolve_script_ref(md_content, ref_id, language="bash")
208
+ if result is None:
216
209
  return None
217
- return script, lookup_method
210
+ return result
218
211
 
219
212
 
220
213
  def _execute_single_script(ref_id, script, host, timeout) -> bool:
@@ -2,12 +2,34 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import re
5
+ import shlex
5
6
  import sys
6
7
 
7
8
  from rich.console import Console
8
9
 
9
10
  from ...detect.remote import RemoteProbe
10
- from ...hardware.kiosk.compositors import DSI_OUTPUT_NAME
11
+
12
+
13
+ _SAFE_OUTPUT_RE = re.compile(r"^[A-Za-z0-9_.:-]+$")
14
+ _ALLOWED_TRANSFORMS = {
15
+ "normal",
16
+ "90",
17
+ "180",
18
+ "270",
19
+ "flipped",
20
+ "flipped-90",
21
+ "flipped-180",
22
+ "flipped-270",
23
+ }
24
+
25
+
26
+ def _validate_display_inputs(output_name: str, transform: str) -> None:
27
+ if not _SAFE_OUTPUT_RE.match(output_name):
28
+ raise ValueError(f"Unsafe display output name: {output_name!r}")
29
+ if transform not in _ALLOWED_TRANSFORMS:
30
+ raise ValueError(
31
+ f"Unsupported transform: {transform!r}. Allowed: {sorted(_ALLOWED_TRANSFORMS)}"
32
+ )
11
33
 
12
34
 
13
35
  def apply_display_transform(
@@ -30,12 +52,15 @@ def apply_display_transform(
30
52
  One of the wlr-randr transforms (``normal``, ``90``, ``180``, ``270``,
31
53
  ``flipped``, …).
32
54
  """
55
+ _validate_display_inputs(output_name, transform)
56
+
33
57
  console.print(f"[cyan]→ Setting transform {transform} on {output_name}[/cyan]")
34
58
 
35
59
  # 1. Apply immediately via wlr-randr
36
60
  wlr_cmd = (
37
61
  f"WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/$(id -u) "
38
- f"wlr-randr --output {output_name} --transform {transform} 2>&1"
62
+ f"wlr-randr --output {shlex.quote(output_name)} "
63
+ f"--transform {shlex.quote(transform)} 2>&1"
39
64
  )
40
65
  r = probe.run(wlr_cmd)
41
66
  if r.ok:
@@ -48,7 +73,8 @@ def apply_display_transform(
48
73
 
49
74
  # 2. Persist in kanshi config — update or create transform line
50
75
  kanshi_cfg_path = "~/.config/kanshi/config"
51
- read_r = probe.run(f"cat {kanshi_cfg_path} 2>/dev/null")
76
+ quoted_cfg_path = shlex.quote(kanshi_cfg_path)
77
+ read_r = probe.run(f"cat {quoted_cfg_path} 2>/dev/null")
52
78
 
53
79
  if read_r.ok and read_r.out.strip():
54
80
  current = read_r.out
@@ -77,7 +103,7 @@ def apply_display_transform(
77
103
  # Write updated config
78
104
  escaped = new_cfg.replace("'", "'\\''")
79
105
  write_r = probe.run(
80
- f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {kanshi_cfg_path}"
106
+ f"mkdir -p ~/.config/kanshi && printf '%s' '{escaped}' > {quoted_cfg_path}"
81
107
  )
82
108
  if write_r.ok:
83
109
  console.print(f"[green] ✓ kanshi config updated ({kanshi_cfg_path})[/green]")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  import re
6
+ import shlex
6
7
  from typing import Optional
7
8
 
8
9
  from ..models import (
@@ -12,6 +13,9 @@ from ..models import (
12
13
  from .remote import RemoteProbe
13
14
 
14
15
 
16
+ _SAFE_K8S_NS_RE = re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
17
+
18
+
15
19
  def probe_runtime(p: RemoteProbe) -> RuntimeInfo:
16
20
  """Detect installed runtimes: docker, k3s, podman, systemd."""
17
21
  def ver(cmd: str) -> Optional[str]:
@@ -126,8 +130,10 @@ def probe_k3s_services(p: RemoteProbe, namespaces: list[str]) -> list[ServiceInf
126
130
  for ns in namespaces:
127
131
  if ns in ("kube-system", "kube-public", "kube-node-lease", "cert-manager"):
128
132
  continue
133
+ if not _SAFE_K8S_NS_RE.match(ns):
134
+ continue
129
135
  r = p.run(
130
- f"k3s kubectl get pods -n {ns} --no-headers 2>/dev/null"
136
+ f"k3s kubectl get pods -n {shlex.quote(ns)} --no-headers 2>/dev/null"
131
137
  " | awk '{print $1\"|\"$2\"|\"$3}'"
132
138
  )
133
139
  if not r.ok:
@@ -228,8 +228,25 @@ class HealRunner:
228
228
  if decision.action is Action.ABORT:
229
229
  break
230
230
  if decision.action is Action.SKIP:
231
- # TODO: mark step as skipped in executor state and continue
232
- break
231
+ if not failed_step or executor.state is None:
232
+ self.console.print(
233
+ " [yellow]cannot skip without checkpointed state; aborting heal loop[/yellow]"
234
+ )
235
+ break
236
+
237
+ executor.state.mark_done(failed_step)
238
+ self.console.print(
239
+ f" [yellow]marking step as skipped in checkpoint:[/yellow] `{failed_step}`"
240
+ )
241
+
242
+ executor = self._make_executor(resume=True)
243
+ ok = executor.run()
244
+ self.console.print(f"\n{executor.summary()}")
245
+ if ok:
246
+ self._loop_detector.reset_all()
247
+ write_repair_log(self.spec_path, self.version, self.repairs)
248
+ return True
249
+ continue
233
250
 
234
251
  # Retry
235
252
  executor = self._make_executor(resume=True)
@@ -7,6 +7,7 @@ from .parser import (
7
7
  parse_markpact_file,
8
8
  parse_markpact_file_with_refs,
9
9
  parse_markpact_text,
10
+ resolve_script_ref,
10
11
  )
11
12
 
12
13
  __all__ = [
@@ -21,4 +22,5 @@ __all__ = [
21
22
  "parse_markpact_file",
22
23
  "parse_markpact_file_with_refs",
23
24
  "parse_markpact_text",
25
+ "resolve_script_ref",
24
26
  ]
@@ -278,3 +278,33 @@ def _handle_fence_line(
278
278
  return None, script_lines, True, "\n".join(script_lines)
279
279
  return None, script_lines, False, None
280
280
  return code_block_lang, script_lines, False, None
281
+
282
+
283
+ def resolve_script_ref(
284
+ md_content: str,
285
+ ref_id: str,
286
+ language: str = "bash",
287
+ ) -> tuple[str, str] | None:
288
+ """Resolve a script reference in markdown, trying markpact:ref then section heading.
289
+
290
+ Returns ``(script_text, lookup_method)`` or ``None`` if not found.
291
+ """
292
+ script = extract_script_by_ref(md_content, ref_id, language=language)
293
+ if script is not None:
294
+ return script, "markpact:ref"
295
+
296
+ # Try section heading fallback (ref may already have # stripped)
297
+ section_name = ref_id[1:] if ref_id.startswith("#") else ref_id
298
+ script = extract_script_from_markdown(md_content, section_name, language=language)
299
+ if script is not None:
300
+ return script, "section"
301
+
302
+ # Kebab-case fallback: "test-script-section" -> "Test Script Section"
303
+ if "-" in section_name:
304
+ script = extract_script_from_markdown(
305
+ md_content, section_name.replace("-", " "), language=language
306
+ )
307
+ if script is not None:
308
+ return script, "section"
309
+
310
+ return None
@@ -24,6 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import io
26
26
  import os
27
+ import re
27
28
  import subprocess
28
29
  import sys
29
30
  from pathlib import Path
@@ -45,6 +46,20 @@ mcp = FastMCP(
45
46
  # Helpers
46
47
  # ---------------------------------------------------------------------------
47
48
 
49
+ _SAFE_HOST_RE = re.compile(r"^[A-Za-z0-9_.@:-]+$")
50
+ _UNSAFE_SSH_TOKENS = (
51
+ "&&",
52
+ "||",
53
+ ";",
54
+ "|",
55
+ "`",
56
+ "$(",
57
+ ">",
58
+ "<",
59
+ "\n",
60
+ "\r",
61
+ )
62
+
48
63
  def _redeploy_bin() -> str:
49
64
  """Resolve the redeploy binary path (same venv as this process)."""
50
65
  import shutil
@@ -80,6 +95,24 @@ def _run(*args: str, cwd: str | None = None, timeout: int = 120) -> dict:
80
95
  return {"returncode": -1, "stdout": "", "stderr": str(exc), "success": False}
81
96
 
82
97
 
98
+ def _validate_exec_ssh_inputs(host: str, command: str) -> str | None:
99
+ """Return validation error message for unsafe exec_ssh inputs, or None."""
100
+ if not _SAFE_HOST_RE.match(host):
101
+ return f"Unsafe host format: {host!r}"
102
+
103
+ # Allow opting out explicitly for trusted environments.
104
+ if os.getenv("REDEPLOY_MCP_ALLOW_UNSAFE_SSH", "").lower() in {"1", "true", "yes"}:
105
+ return None
106
+
107
+ for token in _UNSAFE_SSH_TOKENS:
108
+ if token in command:
109
+ return (
110
+ "Unsafe command token detected "
111
+ f"({token!r}). Set REDEPLOY_MCP_ALLOW_UNSAFE_SSH=1 to bypass explicitly."
112
+ )
113
+ return None
114
+
115
+
83
116
  # ---------------------------------------------------------------------------
84
117
  # Tools
85
118
  # ---------------------------------------------------------------------------
@@ -218,6 +251,15 @@ def exec_ssh(
218
251
  Returns stdout, stderr and return code.
219
252
  CAUTION: this executes arbitrary commands — confirm with user before use.
220
253
  """
254
+ validation_error = _validate_exec_ssh_inputs(host, command)
255
+ if validation_error:
256
+ return {
257
+ "returncode": -1,
258
+ "stdout": "",
259
+ "stderr": validation_error,
260
+ "success": False,
261
+ }
262
+
221
263
  try:
222
264
  proc = subprocess.run(
223
265
  ["ssh", "-o", "ConnectTimeout=10", "-o", "BatchMode=yes", host, command],
@@ -1,4 +1,5 @@
1
1
  from redeploy.heal import HealLoopDetector
2
+ from redeploy.heal.decider import Action, Decision
2
3
 
3
4
 
4
5
  def test_loop_detector_triggers_on_identical_hint_streak():
@@ -124,3 +125,58 @@ def test_heal_runner_reverts_invalid_llm_patch_without_crash(monkeypatch, tmp_pa
124
125
 
125
126
  assert runner.run() is False
126
127
  assert spec_path.read_text() == original
128
+
129
+
130
+ def test_heal_runner_skip_marks_state_and_resumes(monkeypatch, tmp_path):
131
+ from redeploy.heal import HealRunner
132
+
133
+ spec_path = tmp_path / "spec.md"
134
+ spec_path.write_text("steps:\n - id: sync_env\n")
135
+
136
+ class _State:
137
+ def __init__(self):
138
+ self.marked: list[str] = []
139
+
140
+ def mark_done(self, step_id: str):
141
+ self.marked.append(step_id)
142
+
143
+ class _FakeExecutor:
144
+ def __init__(self, ok: bool, state):
145
+ self._ok = ok
146
+ self.state = state
147
+
148
+ def run(self):
149
+ return self._ok
150
+
151
+ def summary(self):
152
+ return "Step failed: [sync_env] exit=1: boom"
153
+
154
+ state = _State()
155
+ make_calls: list[bool] = []
156
+
157
+ def _fake_make_executor(self, resume=False):
158
+ make_calls.append(resume)
159
+ if not resume:
160
+ return _FakeExecutor(False, state)
161
+ return _FakeExecutor(True, state)
162
+
163
+ monkeypatch.setattr(HealRunner, "_make_executor", _fake_make_executor)
164
+ monkeypatch.setattr(
165
+ HealRunner,
166
+ "_heal_step",
167
+ lambda *_a, **_k: (Decision(Action.SKIP, "skip"), "sync_env", "hint"),
168
+ )
169
+ monkeypatch.setattr("redeploy.heal.runner.write_repair_log", lambda *_a, **_k: None)
170
+
171
+ runner = HealRunner(
172
+ migration=object(),
173
+ spec_path=spec_path,
174
+ host="pi@test",
175
+ max_retries=2,
176
+ dry_run=True,
177
+ version="0.0.0",
178
+ )
179
+
180
+ assert runner.run() is True
181
+ assert state.marked == ["sync_env"]
182
+ assert make_calls == [False, True]
@@ -0,0 +1,93 @@
1
+ """Security hardening tests for command construction and MCP guards."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+ from rich.console import Console
9
+
10
+ from redeploy.config_apply.handlers.display import apply_display_transform
11
+ from redeploy.detect.probes import probe_k3s_services
12
+
13
+
14
+ class _Result:
15
+ def __init__(self, ok: bool = True, out: str = ""):
16
+ self.ok = ok
17
+ self.out = out
18
+
19
+
20
+ class _Probe:
21
+ def __init__(self):
22
+ self.commands: list[str] = []
23
+
24
+ def run(self, cmd: str):
25
+ self.commands.append(cmd)
26
+ if cmd.startswith("cat "):
27
+ return _Result(ok=False, out="")
28
+ return _Result(ok=True, out="")
29
+
30
+
31
+ def test_apply_display_transform_rejects_unsafe_output_name():
32
+ probe = _Probe()
33
+ console = Console(file=io.StringIO(), force_terminal=False)
34
+
35
+ with pytest.raises(ValueError, match="Unsafe display output name"):
36
+ apply_display_transform(
37
+ console,
38
+ probe,
39
+ "DSI-2; rm -rf /",
40
+ "270",
41
+ )
42
+
43
+
44
+ def test_apply_display_transform_rejects_unsupported_transform():
45
+ probe = _Probe()
46
+ console = Console(file=io.StringIO(), force_terminal=False)
47
+
48
+ with pytest.raises(ValueError, match="Unsupported transform"):
49
+ apply_display_transform(console, probe, "DSI-2", "drop-table")
50
+
51
+
52
+ def test_probe_k3s_services_skips_unsafe_namespace():
53
+ commands: list[str] = []
54
+
55
+ class Probe:
56
+ def run(self, cmd: str):
57
+ commands.append(cmd)
58
+ return _Result(ok=True, out="pod-a|1/1|Running")
59
+
60
+ services = probe_k3s_services(Probe(), ["default; rm -rf /", "c2004"])
61
+
62
+ assert len(services) == 1
63
+ assert services[0].name == "pod-a"
64
+ assert all("default; rm -rf /" not in c for c in commands)
65
+
66
+
67
+ def test_exec_ssh_rejects_unsafe_command_tokens(monkeypatch):
68
+ pytest.importorskip("mcp.server.fastmcp")
69
+ from redeploy import mcp_server
70
+
71
+ with patch("redeploy.mcp_server.subprocess.run") as run:
72
+ result = mcp_server.exec_ssh("pi@192.168.1.10", "echo ok && whoami")
73
+
74
+ assert result["success"] is False
75
+ assert "Unsafe command token" in result["stderr"]
76
+ run.assert_not_called()
77
+
78
+
79
+ def test_exec_ssh_allows_override_for_trusted_environments(monkeypatch):
80
+ pytest.importorskip("mcp.server.fastmcp")
81
+ from redeploy import mcp_server
82
+
83
+ monkeypatch.setenv("REDEPLOY_MCP_ALLOW_UNSAFE_SSH", "1")
84
+
85
+ class Proc:
86
+ returncode = 0
87
+ stdout = "ok"
88
+ stderr = ""
89
+
90
+ with patch("redeploy.mcp_server.subprocess.run", return_value=Proc()):
91
+ result = mcp_server.exec_ssh("pi@192.168.1.10", "echo ok && whoami")
92
+
93
+ assert result["success"] is True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redeploy
3
- Version: 0.2.70
3
+ Version: 0.2.71
4
4
  Summary: Infrastructure migration toolkit: detect → plan → apply
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -32,17 +32,17 @@ Requires-Dist: mcp>=1.0; extra == "mcp"
32
32
 
33
33
  ## AI Cost Tracking
34
34
 
35
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-36.8h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
35
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
36
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-37.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
37
37
 
38
- - 🤖 **LLM usage:** $7.5000 (91 commits)
39
- - 👤 **Human dev:** ~$3684 (36.8h @ $100/h, 30min dedup)
38
+ - 🤖 **LLM usage:** $7.5000 (92 commits)
39
+ - 👤 **Human dev:** ~$3734 (37.3h @ $100/h, 30min dedup)
40
40
 
41
41
  Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
42
 
43
43
  ---
44
44
 
45
- ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.70-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
+ ![PyPI](https://img.shields.io/badge/pypi-redeploy-blue) ![Version](https://img.shields.io/badge/version-0.2.71-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
46
46
 
47
47
  Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
48
48
 
@@ -204,6 +204,7 @@ redeploy/tests/test_plugins.py
204
204
  redeploy/tests/test_probes.py
205
205
  redeploy/tests/test_public_api.py
206
206
  redeploy/tests/test_resume.py
207
+ redeploy/tests/test_security_hardening.py
207
208
  redeploy/tests/test_spec_loader.py
208
209
  redeploy/tests/test_ssh.py
209
210
  redeploy/tests/test_steps_registry.py
File without changes