diff --git a/apps/api/src/services/awooop_ansible_audit_service.py b/apps/api/src/services/awooop_ansible_audit_service.py index 8e26f955b..d7a4cc202 100644 --- a/apps/api/src/services/awooop_ansible_audit_service.py +++ b/apps/api/src/services/awooop_ansible_audit_service.py @@ -62,6 +62,7 @@ _CATALOG: tuple[dict[str, Any], ...] = ( { "catalog_id": "ansible:188-ai-web", "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", + "check_mode_playbook_path": "infra/ansible/playbooks/188-ai-web-readonly.yml", "inventory_hosts": ["host_188"], "domains": ["docker", "momo_backup", "signoz", "minio", "litellm", "n8n", "open_webui", "nginx"], "keywords": [ @@ -233,6 +234,7 @@ def _catalog_hints(incident: dict[str, Any] | None, drift: dict[str, Any] | None "inventory_hosts", "domains", "supports_check_mode", + "check_mode_playbook_path", "auto_apply_enabled", "approval_required", "risk_level", @@ -347,6 +349,7 @@ def build_ansible_decision_audit_payload( { "catalog_id": row["catalog_id"], "playbook_path": row["playbook_path"], + "check_mode_playbook_path": row.get("check_mode_playbook_path"), "inventory_hosts": row["inventory_hosts"], "supports_check_mode": row["supports_check_mode"], "auto_apply_enabled": row["auto_apply_enabled"], diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 0577c2130..306546fdc 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -161,8 +161,14 @@ def _safe_candidate(input_payload: dict[str, Any]) -> dict[str, Any]: continue if catalog_item.get("auto_apply_enabled") is True: continue - playbook_path = str(candidate.get("playbook_path") or catalog_item.get("playbook_path") or "") - if playbook_path != str(catalog_item.get("playbook_path") or ""): + catalog_playbook_path = str(catalog_item.get("playbook_path") or "") + candidate_playbook_path = str(candidate.get("playbook_path") or catalog_playbook_path) + check_mode_playbook_path = str( + candidate.get("check_mode_playbook_path") + or catalog_item.get("check_mode_playbook_path") + or catalog_playbook_path + ) + if candidate_playbook_path not in {catalog_playbook_path, check_mode_playbook_path}: continue inventory_hosts = candidate.get("inventory_hosts") or catalog_item.get("inventory_hosts") or [] if ( @@ -172,7 +178,10 @@ def _safe_candidate(input_payload: dict[str, Any]) -> dict[str, Any]: ): return { "catalog_id": catalog_id, - "playbook_path": playbook_path, + "playbook_path": check_mode_playbook_path, + "catalog_playbook_path": catalog_playbook_path, + "source_candidate_playbook_path": candidate_playbook_path, + "check_mode_playbook_path": check_mode_playbook_path, "inventory_hosts": tuple(inventory_hosts), "risk_level": str(candidate.get("risk_level") or catalog_item.get("risk_level") or ""), } @@ -199,6 +208,9 @@ def build_ansible_check_mode_claim_input( "source_candidate_op_id": source_candidate_op_id, "catalog_id": safe["catalog_id"], "playbook_path": safe["playbook_path"], + "catalog_playbook_path": safe["catalog_playbook_path"], + "source_candidate_playbook_path": safe["source_candidate_playbook_path"], + "check_mode_playbook_path": safe["check_mode_playbook_path"], "inventory_hosts": list(safe["inventory_hosts"]), "risk_level": safe["risk_level"], } diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index f5599a510..a4079cdaf 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -884,6 +884,10 @@ def test_ansible_truth_surfaces_audited_check_mode_record() -> None: assert truth["candidate_catalog"]["decision_effect"] == "none" assert truth["candidate_catalog"]["candidates"][0]["catalog_id"] == "ansible:188-ai-web" assert truth["candidate_catalog"]["candidates"][0]["auto_apply_enabled"] is False + assert ( + truth["candidate_catalog"]["candidates"][0]["check_mode_playbook_path"] + == "infra/ansible/playbooks/188-ai-web-readonly.yml" + ) def test_ansible_truth_keeps_catalog_hint_separate_from_runtime_use() -> None: @@ -964,6 +968,8 @@ def test_ansible_decision_audit_payload_exposes_check_mode_safety_flags() -> Non candidate = payload["input"]["executor_candidates"][0] assert candidate["catalog_id"] == "ansible:188-ai-web" assert candidate["supports_check_mode"] is True + assert candidate["playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml" + assert candidate["check_mode_playbook_path"] == "infra/ansible/playbooks/188-ai-web-readonly.yml" assert candidate["auto_apply_enabled"] is False assert candidate["approval_required"] is True @@ -993,7 +999,10 @@ def test_ansible_check_mode_claim_input_keeps_apply_locked() -> None: assert claim["apply_enabled"] is False assert claim["transport_profile"] assert claim["approval_required_before_apply"] is True - assert claim["playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml" + assert claim["catalog_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml" + assert claim["source_candidate_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml" + assert claim["check_mode_playbook_path"] == "infra/ansible/playbooks/188-ai-web-readonly.yml" + assert claim["playbook_path"] == "infra/ansible/playbooks/188-ai-web-readonly.yml" def test_ansible_check_mode_claim_rejects_non_check_mode_catalog() -> None: diff --git a/infra/ansible/playbooks/188-ai-web-readonly.yml b/infra/ansible/playbooks/188-ai-web-readonly.yml new file mode 100644 index 000000000..19151299f --- /dev/null +++ b/infra/ansible/playbooks/188-ai-web-readonly.yml @@ -0,0 +1,86 @@ +--- +# AWOOOI Ansible - 192.168.0.188 read-only check-mode evidence. +# This playbook intentionally avoids become/sudo and performs no mutations. + +- name: "192.168.0.188 AI+Web read-only evidence" + hosts: host_188 + gather_facts: false + become: false + + vars: + expected_188_containers: + - openclaw + - tsenyang-website + - momo-pro-system + - momo-db + - signoz + - minio + - litellm + - n8n + - open-webui + - docker-registry + + tasks: + - name: "Preflight | collect remote user" + ansible.builtin.command: + cmd: "id -un" + register: remote_identity + changed_when: false + check_mode: false + + - name: "Docker | collect running container names" + ansible.builtin.command: + cmd: "docker ps --format '{{ '{{' }}.Names{{ '}}' }}'" + register: running_containers + changed_when: false + check_mode: false + + - name: "Docker | collect restarting containers" + ansible.builtin.command: + cmd: "docker ps --filter status=restarting --format '{{ '{{' }}.Names{{ '}}' }}'" + register: restarting_containers + changed_when: false + check_mode: false + + - name: "Docker | calculate missing expected containers" + ansible.builtin.set_fact: + missing_expected_containers: >- + {{ expected_188_containers | difference(running_containers.stdout_lines | default([])) }} + changed_when: false + + - name: "Backup | inspect momo PostgreSQL backup script" + ansible.builtin.stat: + path: /home/ollama/momo-pro/scripts/pg_backup.sh + register: momo_pg_backup_script + + - name: "Backup | inspect AwoooP ops notification helper" + ansible.builtin.stat: + path: /home/ollama/momo-pro/scripts/notify-awoooi-ops.sh + register: momo_notify_helper + + - name: "Backup | inspect momo backup directory" + ansible.builtin.stat: + path: /home/ollama/momo_backups + register: momo_backup_dir + + - name: "Backup | collect ollama crontab" + ansible.builtin.command: + cmd: "crontab -l" + register: ollama_crontab + changed_when: false + failed_when: ollama_crontab.rc not in [0, 1] + check_mode: false + + - name: "Evidence | emit read-only summary" + ansible.builtin.debug: + msg: + - "remote_user={{ remote_identity.stdout | default('unknown') }}" + - "running_containers={{ running_containers.stdout_lines | default([]) | join(',') }}" + - "restarting_containers={{ restarting_containers.stdout_lines | default([]) | join(',') }}" + - "missing_expected_containers={{ missing_expected_containers | default([]) | join(',') }}" + - "pg_backup_exists={{ momo_pg_backup_script.stat.exists | default(false) }}" + - "pg_backup_executable={{ momo_pg_backup_script.stat.executable | default(false) }}" + - "notify_helper_exists={{ momo_notify_helper.stat.exists | default(false) }}" + - "notify_helper_executable={{ momo_notify_helper.stat.executable | default(false) }}" + - "backup_dir_exists={{ momo_backup_dir.stat.exists | default(false) }}" + - "cron_has_pg_backup={{ '/home/ollama/momo-pro/scripts/pg_backup.sh' in (ollama_crontab.stdout | default('')) }}"