fix(api): connect approval execution truth chain
This commit is contained in:
@@ -571,6 +571,16 @@ class ApprovalExecutionService:
|
||||
repair_executed=repair_executed,
|
||||
repair_attempted=repair_attempted,
|
||||
)
|
||||
if repair_attempted:
|
||||
await self._record_approved_repair_execution(
|
||||
approval=approval,
|
||||
success=result.success,
|
||||
error_message=None if result.success else result.error,
|
||||
operation_type=operation_type,
|
||||
resource_name=resource_name,
|
||||
namespace=namespace,
|
||||
duration_ms=result.duration_ms,
|
||||
)
|
||||
|
||||
# Update approval status based on result
|
||||
total_attempts = attempt # attempt 在重試迴圈後為最終嘗試次數
|
||||
@@ -631,12 +641,33 @@ class ApprovalExecutionService:
|
||||
approval_id=str(approval.id),
|
||||
timeout_sec=30.0,
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self.write_execution_result_to_km(
|
||||
approval=approval,
|
||||
success=True,
|
||||
error_message=None,
|
||||
),
|
||||
timeout=15.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
"execution_km_write_timeout",
|
||||
approval_id=str(approval.id),
|
||||
timeout_sec=15.0,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"execution_km_write_failed",
|
||||
approval_id=str(approval.id),
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
# ADR-081 Phase 1 + ADR-090 修復 (2026-04-19 ogt + Claude Opus 4.7):
|
||||
# PostExecutionVerifier 改 await + 60s timeout,確保 verification_result 必寫入。
|
||||
# 之前 fire-and-forget 在 Pod recycle 時 task 被殺,導致 1212 筆 evidence 全 NULL.
|
||||
from src.core.feature_flags import aiops_flags
|
||||
if aiops_flags.is_sub_flag_enabled("AIOPS_P1_POST_EXECUTION_VERIFIER"):
|
||||
if repair_executed or aiops_flags.is_sub_flag_enabled("AIOPS_P1_POST_EXECUTION_VERIFIER"):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self._run_post_execution_verify(
|
||||
@@ -771,6 +802,28 @@ class ApprovalExecutionService:
|
||||
approval_id=str(approval.id),
|
||||
timeout_sec=30.0,
|
||||
)
|
||||
if repair_attempted:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self.write_execution_result_to_km(
|
||||
approval=approval,
|
||||
success=False,
|
||||
error_message=result.error,
|
||||
),
|
||||
timeout=15.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
"execution_km_write_timeout",
|
||||
approval_id=str(approval.id),
|
||||
timeout_sec=15.0,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"execution_km_write_failed",
|
||||
approval_id=str(approval.id),
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
# ADR-090 修復 (2026-04-19 ogt + Claude Opus 4.7):
|
||||
# 失敗時也跑 verifier,把 verification_result='failed' 回寫 evidence。
|
||||
@@ -1477,6 +1530,93 @@ class ApprovalExecutionService:
|
||||
return None
|
||||
return getattr(risk_level, "value", str(risk_level))
|
||||
|
||||
async def _record_approved_repair_execution(
|
||||
self,
|
||||
*,
|
||||
approval: "ApprovalRequest",
|
||||
success: bool,
|
||||
error_message: str | None,
|
||||
operation_type: OperationType | None,
|
||||
resource_name: str | None,
|
||||
namespace: str | None,
|
||||
duration_ms: int | None = None,
|
||||
) -> None:
|
||||
"""Persist the repair evidence for an approved executable action."""
|
||||
incident_id = getattr(approval, "incident_id", None)
|
||||
if not incident_id:
|
||||
logger.info(
|
||||
"approved_repair_execution_record_skipped_no_incident",
|
||||
approval_id=str(getattr(approval, "id", "")),
|
||||
action=str(getattr(approval, "action", ""))[:160],
|
||||
)
|
||||
return
|
||||
if self._is_observation_only_action(getattr(approval, "action", None)):
|
||||
return
|
||||
|
||||
operation_label = (
|
||||
operation_type.value
|
||||
if operation_type is not None and hasattr(operation_type, "value")
|
||||
else str(operation_type or "unknown")
|
||||
)
|
||||
target = resource_name or "unknown"
|
||||
playbook_id = str(getattr(approval, "matched_playbook_id", None) or approval.id)[:36]
|
||||
requested_by = str(getattr(approval, "requested_by", None) or "telegram_approval")
|
||||
triggered_by = (
|
||||
requested_by[:50]
|
||||
if self._is_auto_approved_request(approval)
|
||||
else "human_approved"
|
||||
)
|
||||
playbook_name = f"approval_execute:{operation_label}:{target}"[:200]
|
||||
step = str(getattr(approval, "action", "") or "")
|
||||
|
||||
try:
|
||||
from src.repositories.audit_log_repository import get_auto_repair_execution_repository
|
||||
|
||||
repo = get_auto_repair_execution_repository()
|
||||
existing = await repo.list_by_incident(incident_id)
|
||||
already_recorded = any(
|
||||
str(getattr(row, "playbook_id", "")) == playbook_id
|
||||
and getattr(row, "triggered_by", "") == triggered_by
|
||||
and step in list(getattr(row, "executed_steps", []) or [])
|
||||
for row in existing
|
||||
)
|
||||
if already_recorded:
|
||||
logger.info(
|
||||
"approved_repair_execution_record_already_exists",
|
||||
approval_id=str(approval.id),
|
||||
incident_id=incident_id,
|
||||
playbook_id=playbook_id,
|
||||
)
|
||||
return
|
||||
|
||||
await repo.create(
|
||||
incident_id=incident_id,
|
||||
playbook_id=playbook_id,
|
||||
playbook_name=playbook_name,
|
||||
success=success,
|
||||
executed_steps=[step],
|
||||
error_message=error_message,
|
||||
triggered_by=triggered_by,
|
||||
risk_level=self._approval_risk_value(approval),
|
||||
execution_time_ms=duration_ms,
|
||||
)
|
||||
logger.info(
|
||||
"approved_repair_execution_recorded",
|
||||
approval_id=str(approval.id),
|
||||
incident_id=incident_id,
|
||||
operation_type=operation_label,
|
||||
target=target,
|
||||
namespace=namespace,
|
||||
success=success,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"approved_repair_execution_record_failed",
|
||||
approval_id=str(getattr(approval, "id", "")),
|
||||
incident_id=incident_id,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
async def finalize_auto_approved_execution(
|
||||
self,
|
||||
approval: "ApprovalRequest",
|
||||
|
||||
Reference in New Issue
Block a user