redeploy 0.2.70__tar.gz → 0.2.72__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.72}/PKG-INFO +7 -7
- {redeploy-0.2.70 → redeploy-0.2.72}/README.md +6 -6
- {redeploy-0.2.70 → redeploy-0.2.72}/pyproject.toml +1 -1
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/__init__.py +1 -1
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/analyze/preflight_schema.py +9 -15
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/analyze/spec_analyzer.py +2 -5
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/handlers.py +8 -14
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/exec_.py +7 -18
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/config_apply/handlers/display.py +30 -4
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/probes.py +7 -1
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/heal/runner.py +19 -2
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/markpact/__init__.py +2 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/markpact/parser.py +30 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/mcp_server.py +42 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_heal.py +56 -0
- redeploy-0.2.72/redeploy/tests/test_security_hardening.py +93 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy.egg-info/PKG-INFO +7 -7
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy.egg-info/SOURCES.txt +1 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/analyze/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/analyze/checkers/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/exceptions.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/executor.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/progress.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/rollback.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/state_apply.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/utils/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/apply/utils/run_container_build.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/audit.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/extractor.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/generators/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/generators/docker_compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/generators/migration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/sources/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/sources/compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/sources/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/sources/infra.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/blueprint/sources/migration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/audit.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/blueprint.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/bump_fix.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/detect.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/device_map.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/device_map_renderers.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/devices.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/diagnose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/diff.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/export.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/gh_workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/import_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/init.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/inspect.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/lint.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/mcp_cmd.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/patterns.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/plan_apply.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/probe.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/prompt_cmd.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/push.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/state.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/status.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/target.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/commands.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/helpers.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/monorepo.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/release.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/scanner.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/utils/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/utils/changelog_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/version/utils/git_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/commands/workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/core.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/display.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli/query.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/cli.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/config_apply/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/config_apply/applier.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/config_apply/handlers/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/config_apply/loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/builtin/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/hardware_rules.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/discovery.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/fleet.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/config_txt.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/data/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/data/hyperpixel.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/data/official.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/data/waveshare.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/fixes.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/kiosk/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/kiosk/autostart.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/kiosk/browsers.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/kiosk/compositors.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/kiosk/output_profiles.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/panels.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/hardware/raspi_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/heal/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/heal/decider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/heal/hint_provider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/heal/log_writer.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/heal/loop_detector.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/heal.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/iac/config_hints.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/integrations/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/integrations/op3_bridge.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/markpact/compiler.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/markpact/models.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/blueprint.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/devices.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/enums.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/infra.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/persisted.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/pipeline.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/plan.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/models/spec.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/observe.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/parse.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/patterns.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plugins/builtin/hardware_diagnostic.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plugins/builtin/process_control.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/schema.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/ssh.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/docker.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/generic.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/hardware.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/k3s.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/kiosk.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/podman.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/process.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/scm.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/steps/transfer.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_analyze.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_executor.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_hooks.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_iac_compose_contract.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_iac_docker_compose_contract.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_markpact_parser.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_resume.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_steps_registry.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/verify.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy/version.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/setup.cfg +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_blueprint_sources.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_blueprint_sources_compose.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_cli_gh_workflow.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_cli_query.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_example_argocd_flux_plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_example_gitops_ci_plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_example_helm_kustomize_plugin.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_hardware_config.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_hardware_kiosk.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_hardware_rules.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_heal_decider.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_heal_loop_detector.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_iac.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_iac_config_hints.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_iac_plugin_loading.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_import_plugin_template.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_parse.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_prompt_llm.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/tests/test_redeploy.py +0 -0
- {redeploy-0.2.70 → redeploy-0.2.72}/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.72
|
|
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 (93 commits)
|
|
39
|
+
- 👤 **Human dev:** ~$3848 (38.5h @ $100/h, 30min dedup)
|
|
40
40
|
|
|
41
|
-
Generated on 2026-04-
|
|
41
|
+
Generated on 2026-04-24 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 (93 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$3848 (38.5h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-04-
|
|
12
|
+
Generated on 2026-04-24 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.72"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -124,7 +124,7 @@ def save_preflight_schema(schema: dict[str, Any], output_path: Path) -> None:
|
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
def _resolve_command_refs(spec, spec_path: Path, blockers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
127
|
-
from ..markpact.parser import
|
|
127
|
+
from ..markpact.parser import resolve_script_ref
|
|
128
128
|
|
|
129
129
|
out: list[dict[str, Any]] = []
|
|
130
130
|
for step in spec.extra_steps:
|
|
@@ -156,14 +156,11 @@ def _resolve_command_refs(spec, spec_path: Path, blockers: list[dict[str, Any]])
|
|
|
156
156
|
})
|
|
157
157
|
continue
|
|
158
158
|
text = ref_file.read_text(encoding="utf-8", errors="replace")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if by_ref is not None:
|
|
159
|
+
result = resolve_script_ref(text, ref_id, language="bash")
|
|
160
|
+
if result is not None:
|
|
162
161
|
entry["resolved"] = True
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
entry["resolved"] = True
|
|
166
|
-
entry["mode"] = "heading"
|
|
162
|
+
_, lookup_method = result
|
|
163
|
+
entry["mode"] = "markpact_ref" if lookup_method == "markpact:ref" else "heading"
|
|
167
164
|
else:
|
|
168
165
|
blockers.append({
|
|
169
166
|
"type": "missing_command_ref",
|
|
@@ -178,14 +175,11 @@ def _resolve_command_refs(spec, spec_path: Path, blockers: list[dict[str, Any]])
|
|
|
178
175
|
entry["file"] = str(ref_file)
|
|
179
176
|
entry["ref"] = cref
|
|
180
177
|
text = ref_file.read_text(encoding="utf-8", errors="replace") if ref_file.exists() else ""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if by_ref is not None:
|
|
184
|
-
entry["resolved"] = True
|
|
185
|
-
entry["mode"] = "markpact_ref"
|
|
186
|
-
elif by_heading is not None:
|
|
178
|
+
result = resolve_script_ref(text, cref, language="bash")
|
|
179
|
+
if result is not None:
|
|
187
180
|
entry["resolved"] = True
|
|
188
|
-
|
|
181
|
+
_, lookup_method = result
|
|
182
|
+
entry["mode"] = "markpact_ref" if lookup_method == "markpact:ref" else "heading"
|
|
189
183
|
else:
|
|
190
184
|
blockers.append({
|
|
191
185
|
"type": "missing_command_ref",
|
|
@@ -523,7 +523,7 @@ class _CommandRefChecker(_Checker):
|
|
|
523
523
|
"""Validate command_ref references for nested markdown/script dependencies."""
|
|
524
524
|
|
|
525
525
|
def check(self, spec, document, base_dir, result):
|
|
526
|
-
from ..markpact.parser import
|
|
526
|
+
from ..markpact.parser import resolve_script_ref
|
|
527
527
|
|
|
528
528
|
in_doc_refs: set[str] = set()
|
|
529
529
|
if document:
|
|
@@ -551,10 +551,7 @@ class _CommandRefChecker(_Checker):
|
|
|
551
551
|
)
|
|
552
552
|
continue
|
|
553
553
|
text = file_path.read_text(encoding="utf-8", errors="replace")
|
|
554
|
-
if
|
|
555
|
-
extract_script_by_ref(text, ref_id, language="bash")
|
|
556
|
-
or extract_script_from_markdown(text, ref_id, language="bash")
|
|
557
|
-
):
|
|
554
|
+
if resolve_script_ref(text, ref_id, language="bash") is None:
|
|
558
555
|
result.add(
|
|
559
556
|
IssueSeverity.ERROR,
|
|
560
557
|
"command_ref",
|
|
@@ -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
|
|
|
@@ -34,7 +34,6 @@ def exec_cmd(ctx, ref, host, markdown_file, dry_run, timeout):
|
|
|
34
34
|
redeploy exec '#install-deps' --host root@server.com --file deploy.md --dry-run
|
|
35
35
|
redeploy exec 'kiosk-browser-configuration-script' --host pi@192.168.188.108 --file migration.md
|
|
36
36
|
"""
|
|
37
|
-
from ...markpact.parser import extract_script_by_ref
|
|
38
37
|
from ...apply.executor import Executor, MigrationPlan, MigrationStep, StepAction
|
|
39
38
|
|
|
40
39
|
console = Console()
|
|
@@ -65,15 +64,14 @@ def exec_cmd(ctx, ref, host, markdown_file, dry_run, timeout):
|
|
|
65
64
|
console.print(f"[dim]Reading {md_path}...[/dim]")
|
|
66
65
|
md_content = md_path.read_text(encoding="utf-8")
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if script is None:
|
|
67
|
+
result = _extract_script_for_ref(md_content, ref_id)
|
|
68
|
+
if result is None:
|
|
72
69
|
console.print(f"[red]✗ Could not find bash script with ref '#{ref_id}'[/red]")
|
|
73
70
|
console.print("[dim] Make sure there's either:[/dim]")
|
|
74
71
|
console.print("[dim] - A codeblock: ```bash markpact:ref {ref_id}[/dim]")
|
|
75
72
|
console.print("[dim] - Or a ## section heading and ```bash codeblock[/dim]")
|
|
76
73
|
sys.exit(1)
|
|
74
|
+
script, lookup_method = result
|
|
77
75
|
|
|
78
76
|
console.print(f"[green]✓[/green] Found script ({len(script)} chars) via {lookup_method} '#{ref_id}'")
|
|
79
77
|
|
|
@@ -183,8 +181,6 @@ def exec_multi_cmd(ctx, refs, host, markdown_file, dry_run, timeout, parallel):
|
|
|
183
181
|
|
|
184
182
|
def _extract_all_scripts(md_path, ref_list, console) -> list[tuple[str, str, str]]:
|
|
185
183
|
"""Extract all scripts from markdown by ref list."""
|
|
186
|
-
from ...markpact.parser import extract_script_from_markdown, extract_script_by_ref
|
|
187
|
-
|
|
188
184
|
md_content = md_path.read_text(encoding="utf-8")
|
|
189
185
|
scripts: list[tuple[str, str, str]] = []
|
|
190
186
|
|
|
@@ -202,19 +198,12 @@ def _extract_all_scripts(md_path, ref_list, console) -> list[tuple[str, str, str
|
|
|
202
198
|
|
|
203
199
|
def _extract_script_for_ref(md_content, ref_id) -> tuple[str, str] | None:
|
|
204
200
|
"""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"
|
|
201
|
+
from ...markpact import resolve_script_ref
|
|
214
202
|
|
|
215
|
-
|
|
203
|
+
result = resolve_script_ref(md_content, ref_id, language="bash")
|
|
204
|
+
if result is None:
|
|
216
205
|
return None
|
|
217
|
-
return
|
|
206
|
+
return result
|
|
218
207
|
|
|
219
208
|
|
|
220
209
|
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
|