fix(api): connect approval execution truth chain
All checks were successful
CD Pipeline / tests (push) Successful in 1m27s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 4m24s
CD Pipeline / post-deploy-checks (push) Successful in 1m29s

This commit is contained in:
Your Name
2026-06-11 13:03:54 +08:00
parent 57d11390d5
commit 32e4beca06
9 changed files with 409 additions and 14 deletions

View File

@@ -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",