diff --git a/docs/CANONICAL_SERVICES.md b/docs/CANONICAL_SERVICES.md index f245639e..9adc376e 100644 --- a/docs/CANONICAL_SERVICES.md +++ b/docs/CANONICAL_SERVICES.md @@ -78,6 +78,27 @@ Long-running autonomous processes managed by launchd or tmux supervisors. Status --- + +## Ad-hoc recovery mechanisms — superseded by Ansible IaC (#442) + +The following ad-hoc recovery/drift/healing scripts have been superseded by the +canonical Ansible deployment. They remain in the repo for historical reference +only. Do not use. New recovery must go through Ansible. + +| Ad-hoc script | Superseded by | Reason | +|----------------|---------------|--------| +| `scripts/config_drift.py` | `ansible/roles/golden_state` | Drift detection is unnecessary — golden_state enforces convergence | +| `scripts/config_drift_detector.py` | `ansible/roles/golden_state` | Duplicate — use golden_state playbook | +| `scripts/self_healing.py` | `ansible/roles/deadman_switch` | Self-healing is the deadman switch ACTION | +| `scripts/reset_pipeline_state.py` | `ansible/playbooks/agent_startup.yml` | Wizard resurrection is agent_startup | +| `scripts/provision_wizard.py` | `ansible/roles/wizard_base` | Provisioning is wizard_base + site.yml | +| `scripts/cron_audit.py` / `cron-audit-662.py` | `ansible/roles/cron_manager` | Cron jobs are declared in Ansible, not audited ad-hoc | + +**DEPRECATION NOTICE** (2026-04-29, STEP35): All scripts above now contain +inline DEPRECATION headers pointing to their Ansible replacements. They will +be removed after fleet verification. + + ## Change log | Date | Change | By | diff --git a/scripts/config_drift.py b/scripts/config_drift.py index 0104c731..702bdaf2 100644 --- a/scripts/config_drift.py +++ b/scripts/config_drift.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python3 +""" +config_drift.py — DEPRECATED + +Config drift detection and sync is handled by Ansible golden_state role; ad-hoc manual sync bypasses PR review. + +Superseded by: ansible/playbooks/golden_state.yml + ansible/roles/golden_state + +DO NOT USE. Use the Ansible canonical playbook instead: + ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/golden_state.yml + +This script remains only for historical reference. It will be removed after +fleet-wide migration is verified (issue timmy-config #442). + +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +Deprecated: 2026-04-29 (STEP35 FREE BURN) +""" + #!/usr/bin/env python3 """ config_drift.py — Detect configuration drift across fleet nodes. diff --git a/scripts/config_drift_detector.py b/scripts/config_drift_detector.py index 75268f82..a1791dc3 100755 --- a/scripts/config_drift_detector.py +++ b/scripts/config_drift_detector.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python3 +""" +config_drift_detector.py — DEPRECATED + +Config drift detection is superseded by Ansible golden_state convergence. + +Superseded by: ansible/playbooks/golden_state.yml + ansible/roles/golden_state + +DO NOT USE. Use the Ansible canonical playbook instead: + ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/golden_state.yml + +This script remains only for historical reference. It will be removed after +fleet-wide migration is verified (issue timmy-config #442). + +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +Deprecated: 2026-04-29 (STEP35 FREE BURN) +""" + #!/usr/bin/env python3 """ config_drift_detector.py — Detect config drift across fleet nodes. diff --git a/scripts/cron-audit-662.py b/scripts/cron-audit-662.py index 2778224c..d45c8e8f 100644 --- a/scripts/cron-audit-662.py +++ b/scripts/cron-audit-662.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python3 +""" +cron-audit-662.py — DEPRECATED + +Cron job auditing is superseded by source-controlled Ansible cron_manager. + +Superseded by: ansible/playbooks/cron_schedule.yml + ansible/roles/cron_manager + +DO NOT USE. Use the Ansible canonical playbook instead: + ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/cron_schedule.yml + +This script remains only for historical reference. It will be removed after +fleet-wide migration is verified (issue timmy-config #442). + +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +Deprecated: 2026-04-29 (STEP35 FREE BURN) +""" + #!/usr/bin/env python3 """ Cron Fleet Audit Script — #662 diff --git a/scripts/cron_audit.py b/scripts/cron_audit.py index d347ad4a..c4552983 100644 --- a/scripts/cron_audit.py +++ b/scripts/cron_audit.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python3 +""" +cron_audit.py — DEPRECATED + +Cron job auditing is superseded by source-controlled Ansible cron_manager; cron jobs are declared, not audited. + +Superseded by: ansible/playbooks/cron_schedule.yml + ansible/roles/cron_manager + +DO NOT USE. Use the Ansible canonical playbook instead: + ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/cron_schedule.yml + +This script remains only for historical reference. It will be removed after +fleet-wide migration is verified (issue timmy-config #442). + +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +Deprecated: 2026-04-29 (STEP35 FREE BURN) +""" + #!/usr/bin/env python3 """ Cron Job Audit — Identify erroring jobs, categorize health, recommend actions. diff --git a/scripts/provision_wizard.py b/scripts/provision_wizard.py index 2a0902f3..acd5cbd9 100644 --- a/scripts/provision_wizard.py +++ b/scripts/provision_wizard.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python3 +""" +provision_wizard.py — DEPRECATED + +Wizard provisioning is handled by Ansible wizard_base role; never ad-hoc. + +Superseded by: ansible/playbooks/site.yml + ansible/roles/wizard_base + +DO NOT USE. Use the Ansible canonical playbook instead: + ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/site.yml + +This script remains only for historical reference. It will be removed after +fleet-wide migration is verified (issue timmy-config #442). + +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +Deprecated: 2026-04-29 (STEP35 FREE BURN) +""" + #!/usr/bin/env python3 """ [OPS] Automated VPS Provisioning System (Von Neumann as Code) diff --git a/scripts/reset_pipeline_state.py b/scripts/reset_pipeline_state.py index db8f8901..13628990 100644 --- a/scripts/reset_pipeline_state.py +++ b/scripts/reset_pipeline_state.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python3 +""" +reset_pipeline_state.py — DEPRECATED + +Pipeline state reset/wizard resurrection is handled by Ansible agent_startup playbook. + +Superseded by: ansible/playbooks/agent_startup.yml + +DO NOT USE. Use the Ansible canonical playbook instead: + ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/agent_startup.yml + +This script remains only for historical reference. It will be removed after +fleet-wide migration is verified (issue timmy-config #442). + +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +Deprecated: 2026-04-29 (STEP35 FREE BURN) +""" + #!/usr/bin/env python3 """ reset_pipeline_state.py — Daily reset for pipeline_state.json diff --git a/scripts/self_healing.py b/scripts/self_healing.py index b81f4d25..0a181693 100644 --- a/scripts/self_healing.py +++ b/scripts/self_healing.py @@ -1,3 +1,21 @@ +#!/usr/bin/env python3 +""" +self_healing.py — DEPRECATED + +Self-healing recovery is handled by the Ansible-deployed deadman switch service. + +Superseded by: ansible/playbooks/deadman_switch.yml + ansible/roles/deadman_switch + +DO NOT USE. Use the Ansible canonical playbook instead: + ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/deadman_switch.yml + +This script remains only for historical reference. It will be removed after +fleet-wide migration is verified (issue timmy-config #442). + +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +Deprecated: 2026-04-29 (STEP35 FREE BURN) +""" + #!/usr/bin/env python3 """ [OPS] Self-Healing Infrastructure diff --git a/tests/test_ansible_structure.py b/tests/test_ansible_structure.py new file mode 100644 index 00000000..ee33a34d --- /dev/null +++ b/tests/test_ansible_structure.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +""" +Smoke test — verify ansible/ directory has required canonical structure. +Issue: timmy-config #442 — [P2] Ansible IaC — Canonical Playbook for Fleet Management +""" + +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).parent.parent +ANSIBLE_DIR = REPO_ROOT / "ansible" + +def test_ansible_readme_exists(): + """README.md exists and is the single source of truth declaration.""" + assert (ANSIBLE_DIR / "README.md").exists() + +def test_ansible_cfg_exists(): + """ansible.cfg exists with inventory configured.""" + cfg_path = ANSIBLE_DIR / "ansible.cfg" + assert cfg_path.exists() + content = cfg_path.read_text() + assert "inventory = inventory/hosts.yml" in content + +def test_banned_providers_exists(): + """BANNED_PROVIDERS.yml exists to enforce the Anthropic ban.""" + assert (ANSIBLE_DIR / "BANNED_PROVIDERS.yml").exists() + +def test_inventory_hosts_exists_and_lists_all_wizards(): + """inventory/hosts.yml lists all fleet wizards.""" + hosts_file = ANSIBLE_DIR / "inventory/hosts.yml" + assert hosts_file.exists() + content = hosts_file.read_text() + for wizard in ["timmy", "allegro", "bezalel", "ezra"]: + assert wizard in content, f"missing wizard: {wizard}" + +def test_inventory_group_vars_exists(): + """inventory/group_vars/wizards.yml with deadman/cron config.""" + gv = ANSIBLE_DIR / "inventory/group_vars/wizards.yml" + assert gv.exists() + content = gv.read_text() + # Should define deadman settings and cron_jobs (golden_state_providers is in hosts.yml) + assert "deadman_enabled" in content + assert "cron_jobs:" in content + +def test_playbooks_all_exist(): + """All required playbooks are present.""" + required = [ + "site.yml", + "deadman_switch.yml", + "golden_state.yml", + "agent_startup.yml", + "cron_schedule.yml", + "request_log.yml", + ] + for pb in required: + assert (ANSIBLE_DIR / "playbooks" / pb).exists(), f"missing playbook: {pb}" + +def test_site_yml_orchestrates_all_roles(): + """site.yml orchestrates wizard_base, golden_state, deadman_switch, request_log, cron_manager.""" + site = (ANSIBLE_DIR / "playbooks/site.yml").read_text() + for role in ["wizard_base", "golden_state", "deadman_switch", "request_log", "cron_manager"]: + assert f"role: {role}" in site, f"site.yml missing role: {role}" + +def test_golden_state_role_scans_for_banned_providers(): + """golden_state role includes banned provider detection.""" + tasks = (ANSIBLE_DIR / "roles/golden_state/tasks/main.yml").read_text() + # Should grep for anthropic/claude patterns + assert "grep" in tasks.lower() and "anthropic" in tasks.lower() + +def test_deadman_switch_role_has_templates(): + """deadman_switch role includes systemd timer and service templates.""" + role_dir = ANSIBLE_DIR / "roles/deadman_switch/templates" + assert (role_dir / "deadman_switch.service.j2").exists() + assert (role_dir / "deadman_switch.timer.j2").exists() + assert (role_dir / "deadman_action.sh.j2").exists() + +def test_cron_manager_role_exists(): + """cron_manager role exists and deploys managed cron jobs.""" + assert (ANSIBLE_DIR / "roles/cron_manager/tasks/main.yml").exists() + tasks = (ANSIBLE_DIR / "roles/cron_manager/tasks/main.yml").read_text() + assert "cron:" in tasks + +def test_request_log_role_has_schema(): + """request_log role includes SQLite schema.""" + assert (ANSIBLE_DIR / "roles/request_log/files/request_log_schema.sql").exists() + +def test_webhook_deploy_script_exists(): + """ansible/scripts/deploy_on_webhook.sh exists for automated deployment.""" + script = ANSIBLE_DIR / "scripts" / "deploy_on_webhook.sh" + assert script.exists() + content = script.read_text() + assert "ansible-pull" in content + +def test_config_validator_exists(): + """ansible/scripts/validate_config.py validates configs against golden state.""" + validator = ANSIBLE_DIR / "scripts" / "validate_config.py" + assert validator.exists() + content = validator.read_text() + assert "BANNED_PROVIDERS" in content or "banned_providers" in content + +def test_readme_declares_canonical_status_and_webhook(): + """README declares ansible/ as single source of truth and documents webhook.""" + readme = (ANSIBLE_DIR / "README.md").read_text() + assert "single source of truth" in readme.lower() + assert "webhook" in readme.lower() or "deploy_on_webhook" in readme.lower() + +def test_readme_documents_quick_start(): + """README includes Quick Start usage examples.""" + readme = (ANSIBLE_DIR / "README.md").read_text() + assert "ansible-playbook" in readme.lower() + +def test_golden_state_enforces_banned_providers_in_playbook(): + """golden_state.yml playbook or role enforces banned provider policy.""" + gs_content = (ANSIBLE_DIR / "playbooks" / "golden_state.yml").read_text() + # The playbook either has the check directly or includes the role that does + # We know the role does, so this passes if role is included + assert "role: golden_state" in gs_content + +def test_agent_startup_sequence_is_serial(): + """agent_startup.yml uses serial execution to avoid cascading.""" + startup = (ANSIBLE_DIR / "playbooks/agent_startup.yml").read_text() + assert "serial:" in startup + +def test_golden_state_providers_defined_in_hosts(): + """Golden state provider chain is defined in inventory.""" + hosts = (ANSIBLE_DIR / "inventory/hosts.yml").read_text() + assert "golden_state_providers:" in hosts + assert "kimi" in hosts.lower() + assert "openrouter" in hosts.lower() or "anthropic" not in hosts.lower() + +def test_ad_hoc_recovery_scripts_have_deprecation_headers(): + """Overlapping ad-hoc recovery scripts are marked DEPRECATED with Ansible alternatives.""" + scripts_dir = REPO_ROOT / "scripts" + deprecation_targets = [ + "config_drift.py", + "config_drift_detector.py", + "self_healing.py", + "reset_pipeline_state.py", + "provision_wizard.py", + "cron_audit.py", + "cron-audit-662.py", + ] + for script in deprecation_targets: + path = scripts_dir / script + assert path.exists(), f"missing script: {script}" + content = path.read_text() + assert "DEPRECATED" in content[:500], f"{script} missing DEPRECATED header" + cl = content.lower() + assert "superseded" in cl and "ansible" in cl, f"{script} missing ansible supersession" + assert "timmy-config #442" in content, f"{script} missing issue reference" +