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.
- {redeploy-0.2.70 → redeploy-0.2.71}/PKG-INFO +6 -6
- {redeploy-0.2.70 → redeploy-0.2.71}/README.md +5 -5
- {redeploy-0.2.70 → redeploy-0.2.71}/pyproject.toml +1 -1
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/__init__.py +1 -1
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/handlers.py +8 -14
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/exec_.py +4 -11
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/handlers/display.py +30 -4
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/probes.py +7 -1
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/runner.py +19 -2
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/__init__.py +2 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/parser.py +30 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/mcp_server.py +42 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_heal.py +56 -0
- redeploy-0.2.71/redeploy/tests/test_security_hardening.py +93 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/PKG-INFO +6 -6
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/SOURCES.txt +1 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/checkers/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/preflight_schema.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/analyze/spec_analyzer.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/audit.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/gh_workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/lint.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/prompt_cmd.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/version/utils/git_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/cli.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/handlers/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/discovery.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/fleet.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/decider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/hint_provider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/log_writer.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal/loop_detector.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/heal.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/blueprint.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/devices.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/enums.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/infra.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/persisted.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/pipeline.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/plan.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/models/spec.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/observe.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/parse.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/patterns.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/schema.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/ssh.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/docker.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/generic.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/k3s.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/podman.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/process.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/scm.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/steps/transfer.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_analyze.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_hooks.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_iac_compose_contract.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_steps_registry.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/verify.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy/version.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/setup.cfg +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_cli_gh_workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_example_argocd_flux_plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_example_gitops_ci_plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_example_helm_kustomize_plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_heal_loop_detector.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_iac.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_iac_plugin_loading.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_import_plugin_template.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_parse.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_prompt_llm.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.71}/tests/test_redeploy.py +0 -0
- {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.
|
|
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
|
-
     
|
|
36
|
+
  
|
|
37
37
|
|
|
38
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
39
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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
|
-
|
|
207
|
+
result = resolve_script_ref(md_content, ref_id, language="bash")
|
|
208
|
+
if result is None:
|
|
216
209
|
return None
|
|
217
|
-
return
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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}' > {
|
|
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
|
-
|
|
232
|
-
|
|
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.
|
|
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
|
-
     
|
|
36
|
+
  
|
|
37
37
|
|
|
38
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
39
|
-
- 👤 **Human dev:** ~$
|
|
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
|
-
    
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|