diff --git a/apps/api/src/api/v1/webhooks.py b/apps/api/src/api/v1/webhooks.py index 187021310..bc5268995 100644 --- a/apps/api/src/api/v1/webhooks.py +++ b/apps/api/src/api/v1/webhooks.py @@ -228,6 +228,16 @@ def _controlled_ai_policy_allows(risk_level: RiskLevel | str) -> bool: return value.strip().lower() in {"low", "medium", "high"} +def _controlled_alert_execution_priority(risk_level: RiskLevel | str) -> int: + """Prioritize live alerts ahead of unranked historical candidates.""" + + value = risk_level.value if isinstance(risk_level, RiskLevel) else str(risk_level) + return {"high": 10, "medium": 20, "low": 30}.get( + value.strip().lower(), + 100, + ) + + async def _try_auto_repair_background( incident_id: str, approval_id: str, @@ -246,6 +256,7 @@ async def _try_auto_repair_background( ) op_log = get_alert_operation_log_repository() + execution_priority = _controlled_alert_execution_priority(risk_level) incident = await get_incident_service().get_from_working_memory(incident_id) if incident is None: handoff = { @@ -266,9 +277,11 @@ async def _try_auto_repair_background( "alert_type": alert_type, "namespace": namespace, "approval_id": approval_id, + "execution_priority": execution_priority, }, project_id=str(getattr(incident, "project_id", None) or "awoooi"), ) + handoff["execution_priority"] = execution_priority queued = handoff.get("queued") is True await op_log.append( @@ -292,6 +305,7 @@ async def _try_auto_repair_background( "status": handoff.get("status"), "automation_run_id": handoff.get("automation_run_id"), "single_writer_executor": handoff.get("single_writer_executor"), + "execution_priority": execution_priority, "side_effect_performed": False, "safe_next_action": ( "awooop_ansible_check_mode_worker_claims_candidate" 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 15c90d848..0267ca114 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -3918,6 +3918,17 @@ async def claim_pending_check_modes( CASE WHEN candidate.input ->> 'work_item_id' LIKE 'P0-%' THEN 0 + WHEN candidate.input ->> 'proposal_source' + = 'alert_webhook_controlled_router' + THEN LEAST( + CASE + WHEN candidate.input ->> 'execution_priority' + ~ '^[0-9]{1,3}$' + THEN (candidate.input ->> 'execution_priority')::int + ELSE 30 + END, + 30 + ) WHEN candidate.input ->> 'execution_priority' ~ '^[0-9]{1,3}$' THEN LEAST( diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index 1b063f0c9..577fbc60b 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -2832,6 +2832,8 @@ def test_ansible_claim_query_honors_bounded_execution_priority_before_fifo() -> source = inspect.getsource(claim_pending_check_modes) assert "candidate.input ->> 'work_item_id' LIKE 'P0-%'" in source + assert "alert_webhook_controlled_router" in source + assert "ELSE 30" in source assert "candidate.input ->> 'execution_priority'" in source assert "LEAST(" in source assert "100" in source diff --git a/apps/api/tests/test_cs1_auto_execute.py b/apps/api/tests/test_cs1_auto_execute.py index e1c7762db..f74a4bb62 100644 --- a/apps/api/tests/test_cs1_auto_execute.py +++ b/apps/api/tests/test_cs1_auto_execute.py @@ -117,7 +117,10 @@ async def test_webhook_router_writes_queue_receipt_without_side_effect( ) append = AsyncMock() - async def queue(**_kwargs): + queue_kwargs: dict = {} + + async def queue(**kwargs): + queue_kwargs.update(kwargs) return { "schema_version": "ai_decision_controlled_executor_handoff_v1", "status": "controlled_check_mode_queued", @@ -149,8 +152,11 @@ async def test_webhook_router_writes_queue_receipt_without_side_effect( assert handoff["queued"] is True assert handoff["side_effect_performed"] is False + assert handoff["execution_priority"] == 30 + assert queue_kwargs["proposal_data"]["execution_priority"] == 30 append.assert_awaited_once() assert append.await_args.kwargs["context"]["side_effect_performed"] is False + assert append.await_args.kwargs["context"]["execution_priority"] == 30 @pytest.mark.asyncio diff --git a/apps/api/tests/test_signal_worker_ansible_executor_binding.py b/apps/api/tests/test_signal_worker_ansible_executor_binding.py index b0d61513a..3e43ca083 100644 --- a/apps/api/tests/test_signal_worker_ansible_executor_binding.py +++ b/apps/api/tests/test_signal_worker_ansible_executor_binding.py @@ -62,6 +62,12 @@ def test_execution_broker_is_only_workload_with_ssh_transport() -> None: assert "mountPath: /etc/ssh-mcp/known_hosts" in manifest assert "secretName: ssh-mcp-key" in manifest deployment = yaml.safe_load(manifest) + broker_env = { + item["name"]: item.get("value") + for item in deployment["spec"]["template"]["spec"]["containers"][0]["env"] + } + assert broker_env["AWOOOP_ANSIBLE_CHECK_MODE_INTERVAL_SECONDS"] == "60" + assert broker_env["AWOOOP_ANSIBLE_CHECK_MODE_BATCH_LIMIT"] == "3" ssh_volume = next( volume for volume in deployment["spec"]["template"]["spec"]["volumes"] diff --git a/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml b/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml index 99b0cbdff..59873c89f 100644 --- a/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml +++ b/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml @@ -76,9 +76,9 @@ spec: - name: AWOOOP_ANSIBLE_CONTROLLED_APPLY_ALLOWED_RISK_LEVELS value: "low,medium,high" - name: AWOOOP_ANSIBLE_CHECK_MODE_INTERVAL_SECONDS - value: "300" + value: "60" - name: AWOOOP_ANSIBLE_CHECK_MODE_BATCH_LIMIT - value: "1" + value: "3" - name: AWOOOP_ANSIBLE_CHECK_MODE_TIMEOUT_SECONDS value: "180" - name: AWOOOP_ANSIBLE_CHECK_MODE_STARTUP_SLEEP_SECONDS