From 876ba030d8c21bad895e7ae25868564d5b716c31 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 29 Jun 2026 17:26:55 +0800 Subject: [PATCH] fix(awooop): allow ansible learning receipt rows --- .gitea/workflows/cd.yaml | 7 ++++ ...ible_learning_writeback_operation_type.sql | 37 +++++++++++++++++++ ...learning_writeback_operation_type_down.sql | 36 ++++++++++++++++++ .../services/awooop_ansible_audit_service.py | 1 + .../tests/test_awooop_truth_chain_service.py | 25 +++++++++++-- .../test_cd_controlled_runtime_profile.py | 4 ++ 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 apps/api/migrations/adr090e_ansible_learning_writeback_operation_type.sql create mode 100644 apps/api/migrations/adr090e_ansible_learning_writeback_operation_type_down.sql diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index d06e737dc..035bded9b 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -242,8 +242,14 @@ jobs: ;; apps/api/src/services/ai_agent_autonomous_runtime_control.py) ;; + apps/api/src/services/awooop_ansible_audit_service.py) + ;; apps/api/src/services/awooop_ansible_check_mode_service.py) ;; + apps/api/migrations/adr090e_ansible_learning_writeback_operation_type.sql) + ;; + apps/api/migrations/adr090e_ansible_learning_writeback_operation_type_down.sql) + ;; apps/api/src/services/auto_approve.py) ;; apps/api/src/services/decision_fusion.py) @@ -474,6 +480,7 @@ jobs: src/services/awoooi_production_deploy_readback_blocker.py \ src/services/agent_replay_normalizer.py \ src/services/ai_agent_autonomous_runtime_control.py \ + src/services/awooop_ansible_audit_service.py \ src/services/awooop_ansible_check_mode_service.py \ src/services/auto_repair_service.py \ src/services/auto_approve.py \ diff --git a/apps/api/migrations/adr090e_ansible_learning_writeback_operation_type.sql b/apps/api/migrations/adr090e_ansible_learning_writeback_operation_type.sql new file mode 100644 index 000000000..7bf4c7e18 --- /dev/null +++ b/apps/api/migrations/adr090e_ansible_learning_writeback_operation_type.sql @@ -0,0 +1,37 @@ +-- ADR-090-E: automation_operation_log.operation_type adds Ansible learning writeback receipt +-- Created: 2026-06-29 Taipei +-- +-- Purpose: +-- P1-C autonomous learning loop closure. This operation type records that +-- post-apply verifier output was accepted by LearningService / PlayBook trust +-- writeback after an Ansible controlled apply. +-- +-- Safety: +-- This migration only expands the CHECK allowlist. It does not execute +-- Ansible, change incidents, read secrets, or alter retained data. + +ALTER TABLE automation_operation_log + DROP CONSTRAINT IF EXISTS automation_operation_log_type_valid; + +ALTER TABLE automation_operation_log + ADD CONSTRAINT automation_operation_log_type_valid CHECK (operation_type IN ( + 'monitor_configured','monitor_removed', + 'alert_fired','alert_suppressed','alert_routed', + 'rule_created','rule_updated','rule_matched','rule_rejected','rule_deprecated', + 'playbook_generated','playbook_updated','playbook_executed', + 'remediation_executed','remediation_verified','remediation_rolled_back', + 'self_correction_attempted', + 'km_created','km_updated','km_linked', + 'asset_discovered','coverage_recalculated', + 'capacity_recommendation','quota_enforced', + 'notification_formatted', + 'ansible_candidate_matched', + 'ansible_check_mode_executed', + 'ansible_apply_executed', + 'ansible_learning_writeback_recorded', + 'ansible_rollback_executed', + 'ansible_execution_skipped' + )); + +COMMENT ON CONSTRAINT automation_operation_log_type_valid ON automation_operation_log IS + 'ADR-090-E: allow Ansible learning writeback receipt rows for autonomous runtime closure.'; diff --git a/apps/api/migrations/adr090e_ansible_learning_writeback_operation_type_down.sql b/apps/api/migrations/adr090e_ansible_learning_writeback_operation_type_down.sql new file mode 100644 index 000000000..9ab32d65f --- /dev/null +++ b/apps/api/migrations/adr090e_ansible_learning_writeback_operation_type_down.sql @@ -0,0 +1,36 @@ +-- ADR-090-E rollback: remove Ansible learning writeback receipt from operation_type allowlist. +-- Only apply after confirming no automation_operation_log rows use the operation type. + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM automation_operation_log + WHERE operation_type = 'ansible_learning_writeback_recorded' + LIMIT 1 + ) THEN + RAISE EXCEPTION 'cannot remove ansible_learning_writeback_recorded while rows still exist'; + END IF; +END $$; + +ALTER TABLE automation_operation_log + DROP CONSTRAINT IF EXISTS automation_operation_log_type_valid; + +ALTER TABLE automation_operation_log + ADD CONSTRAINT automation_operation_log_type_valid CHECK (operation_type IN ( + 'monitor_configured','monitor_removed', + 'alert_fired','alert_suppressed','alert_routed', + 'rule_created','rule_updated','rule_matched','rule_rejected','rule_deprecated', + 'playbook_generated','playbook_updated','playbook_executed', + 'remediation_executed','remediation_verified','remediation_rolled_back', + 'self_correction_attempted', + 'km_created','km_updated','km_linked', + 'asset_discovered','coverage_recalculated', + 'capacity_recommendation','quota_enforced', + 'notification_formatted', + 'ansible_candidate_matched', + 'ansible_check_mode_executed', + 'ansible_apply_executed', + 'ansible_rollback_executed', + 'ansible_execution_skipped' + )); diff --git a/apps/api/src/services/awooop_ansible_audit_service.py b/apps/api/src/services/awooop_ansible_audit_service.py index 06617cc42..829cb6d89 100644 --- a/apps/api/src/services/awooop_ansible_audit_service.py +++ b/apps/api/src/services/awooop_ansible_audit_service.py @@ -28,6 +28,7 @@ ANSIBLE_OPERATION_TYPES = frozenset({ "ansible_candidate_matched", "ansible_check_mode_executed", "ansible_apply_executed", + "ansible_learning_writeback_recorded", "ansible_rollback_executed", "ansible_execution_skipped", }) diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index 10b927e89..11d3bf4eb 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -1590,19 +1590,19 @@ def test_ansible_apply_operation_row_can_backfill_auto_repair_receipt() -> None: assert result.duration_ms == 456 -def test_ansible_apply_operation_row_reconstructs_from_columns_when_input_is_sparse() -> None: +def test_ansible_apply_operation_row_reconstructs_from_input_without_physical_columns() -> None: reconstructed = _claim_from_apply_operation_row({ "op_id": "apply-op-2", "parent_op_id": "check-op-2", "incident_id": "INC-20260629-231F8E", "status": "success", - "catalog_id": "ansible:188-momo-backup-user", - "playbook_path": "infra/ansible/playbooks/188-momo-backup-user.yml", - "risk_level": "low", "input": { "incident_id": "INC-20260629-231F8E", + "catalog_id": "ansible:188-momo-backup-user", "source_candidate_op_id": "candidate-op-2", "check_mode_op_id": "check-op-2", + "playbook_path": "infra/ansible/playbooks/188-momo-backup-user.yml", + "risk_level": "low", }, "output": {"returncode": 0, "stdout_tail": "ok"}, "dry_run_result": {"apply_executed": True}, @@ -1624,6 +1624,9 @@ def test_ansible_apply_receipt_backfill_queries_existing_apply_rows() -> None: assert "operation_type = 'ansible_apply_executed'" in source assert "auto_repair_executions existing" in source assert "executed_steps::text LIKE" in source + assert "apply.catalog_id" not in source + assert "apply.playbook_path" not in source + assert "apply.risk_level" not in source def test_ansible_auto_repair_receipt_insert_casts_asyncpg_parameters() -> None: @@ -1669,6 +1672,20 @@ def test_ansible_post_apply_km_writeback_is_idempotent_for_learning_backfill() - assert "KnowledgeDBRepository" in source +def test_ansible_learning_writeback_operation_type_has_schema_migration() -> None: + migration = Path( + "apps/api/migrations/adr090e_ansible_learning_writeback_operation_type.sql" + ).read_text() + down = Path( + "apps/api/migrations/adr090e_ansible_learning_writeback_operation_type_down.sql" + ).read_text() + + assert "ansible_learning_writeback_recorded" in migration + assert "automation_operation_log_type_valid" in migration + assert "DROP CONSTRAINT IF EXISTS automation_operation_log_type_valid" in migration + assert "cannot remove ansible_learning_writeback_recorded" in down + + def test_ansible_live_controlled_apply_sends_telegram_receipt_but_backfill_does_not() -> None: live_source = inspect.getsource(run_controlled_apply_for_claim) backfill_source = inspect.getsource(backfill_missing_auto_repair_execution_receipts_once) diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py index c70838a3d..e3d45dfcf 100644 --- a/ops/runner/test_cd_controlled_runtime_profile.py +++ b/ops/runner/test_cd_controlled_runtime_profile.py @@ -88,7 +88,11 @@ def test_ai_autonomous_runtime_control_stays_on_controlled_runtime_profile() -> def test_awooop_ansible_check_mode_stays_on_controlled_runtime_profile() -> None: text = _workflow_text() expected_sources = [ + "apps/api/src/services/awooop_ansible_audit_service.py)", "apps/api/src/services/awooop_ansible_check_mode_service.py)", + "apps/api/migrations/adr090e_ansible_learning_writeback_operation_type.sql)", + "apps/api/migrations/adr090e_ansible_learning_writeback_operation_type_down.sql)", + "src/services/awooop_ansible_audit_service.py", "apps/api/tests/test_awooop_truth_chain_service.py)", "src/services/awooop_ansible_check_mode_service.py", "tests/test_awooop_truth_chain_service.py",