feat(drift): record remediation evidence
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m10s
CD Pipeline / build-and-deploy (push) Successful in 3m42s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s

This commit is contained in:
Your Name
2026-05-19 09:13:58 +08:00
parent 5bf49f81be
commit 64b34828a7
7 changed files with 641 additions and 6 deletions

View File

@@ -57,6 +57,28 @@ class DriftFingerprintHandoffRequest(BaseModel):
note: str | None = Field(default=None, max_length=500)
class DriftFingerprintRemediationRequest(BaseModel):
"""Record-only remediation request for a stable drift fingerprint."""
report_id: str | None = Field(default=None, min_length=1)
namespace: str | None = Field(default="awoooi-prod", min_length=1)
remediation_kind: Literal[
"live_env_rollback",
"git_adopted",
"git_rollback",
"zero_diff_pr_cleanup",
"manual_noop",
] = "live_env_rollback"
remediation_status: Literal[
"executed_unverified",
"verified_no_drift",
"verification_failed",
] | None = None
verification_report_id: str | None = Field(default=None, min_length=1)
note: str | None = Field(default=None, max_length=1000)
commands_summary: list[str] = Field(default_factory=list, max_length=12)
@router.post("/scan", response_model=DriftScanResponse, summary="觸發漂移掃描")
async def trigger_drift_scan(
request: DriftScanRequest,
@@ -160,6 +182,31 @@ async def record_drift_fingerprint_handoff(
raise HTTPException(status_code=404, detail="drift_report_not_found") from exc
@router.post("/fingerprints/remediation", summary="記錄 Config Drift fingerprint 修復")
async def record_drift_fingerprint_remediation(
request: DriftFingerprintRemediationRequest,
) -> dict:
"""
記錄 stable fingerprint 已完成的修復 / 驗證證據。
安全邊界:只寫 alert_operation_log / timeline_events不修改 drift 狀態、
incident 狀態、自動修復結果,不建立外部 ticket也不執行 kubectl。
"""
svc = get_drift_fingerprint_state_service()
try:
return await svc.record_remediation(
report_id=request.report_id,
namespace=request.namespace,
remediation_kind=request.remediation_kind,
remediation_status=request.remediation_status,
verification_report_id=request.verification_report_id,
note=request.note,
commands_summary=request.commands_summary,
)
except DriftFingerprintStateNotFoundError as exc:
raise HTTPException(status_code=404, detail="drift_report_not_found") from exc
@router.post("/reports/{report_id}/rollback", summary="覆蓋回 Git 狀態")
async def rollback_drift(report_id: str, _csrf_token: CSRFToken) -> dict: # Phase 20: CSRF Protection (驗證用,不需要使用值)
"""

View File

@@ -2,9 +2,9 @@
The drift scanner creates a new report_id on every scan, so the operator-facing
state must be keyed by the stable drift fingerprint instead of the volatile
report id. This service is intentionally conservative: it can record a human
handoff breadcrumb, but it does not adopt, merge, roll back, or mutate incident
state.
report id. This service is intentionally conservative: it can record human
handoff and remediation breadcrumbs, but it does not adopt, merge, roll back,
or mutate incident state.
"""
from __future__ import annotations
@@ -27,6 +27,7 @@ logger = structlog.get_logger(__name__)
SCHEMA_VERSION = "drift_fingerprint_state_v1"
HANDOFF_SCHEMA_VERSION = "drift_fingerprint_handoff_history_v1"
REMEDIATION_SCHEMA_VERSION = "drift_fingerprint_remediation_history_v1"
SERVICE_ACTOR = "drift_fingerprint_state_service"
DEFAULT_NAMESPACE = "awoooi-prod"
@@ -36,6 +37,20 @@ DriftFingerprintHandoffKind = Literal[
"zero_diff_pr_cleanup",
]
DriftFingerprintRemediationKind = Literal[
"live_env_rollback",
"git_adopted",
"git_rollback",
"zero_diff_pr_cleanup",
"manual_noop",
]
DriftFingerprintRemediationStatus = Literal[
"executed_unverified",
"verified_no_drift",
"verification_failed",
]
class DriftFingerprintStateNotFoundError(Exception):
"""Raised when no drift report can be found for the requested selector."""
@@ -87,8 +102,19 @@ def _derive_fsm_state(
repeat_state: dict[str, Any],
open_pr: dict[str, Any] | None,
latest_handoff: dict[str, Any] | None,
latest_remediation: dict[str, Any] | None,
) -> str:
status = str(_enum_value(report.status))
remediation_status = (latest_remediation or {}).get("remediation_status")
if remediation_status == "verified_no_drift":
if report.high_count == 0 and report.medium_count == 0 and report.info_count == 0:
return "no_drift_verified"
return "remediated_verified"
if remediation_status == "executed_unverified":
return "remediation_executed_unverified"
if remediation_status == "verification_failed":
return "remediation_verification_failed"
if status == "adopted":
return "adopted_unverified"
if status == "rolled_back":
@@ -113,6 +139,12 @@ def _derive_fsm_state(
def _next_step_for_state(state: str, open_pr: dict[str, Any] | None) -> str:
if state in {"no_drift_verified", "remediated_verified"}:
return "monitor_for_recurrence"
if state == "remediation_executed_unverified":
return "run_verification_scan_then_record_result"
if state == "remediation_verification_failed":
return "open_manual_investigation_with_failed_verification"
if state == "pr_open_zero_diff":
return "close_zero_diff_pr_and_prepare_real_yaml_patch"
if state == "pr_open_waiting_review":
@@ -138,6 +170,7 @@ def build_drift_fingerprint_state(
*,
open_pr: dict[str, Any] | None = None,
latest_handoff: dict[str, Any] | None = None,
latest_remediation: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the operator-facing Config Drift fingerprint state payload."""
@@ -145,7 +178,13 @@ def build_drift_fingerprint_state(
report.namespace,
report.items,
)
fsm_state = _derive_fsm_state(report, repeat_state, open_pr, latest_handoff)
fsm_state = _derive_fsm_state(
report,
repeat_state,
open_pr,
latest_handoff,
latest_remediation,
)
next_step = _next_step_for_state(fsm_state, open_pr)
return {
@@ -169,6 +208,7 @@ def build_drift_fingerprint_state(
"next_step": next_step,
"open_pr": open_pr,
"latest_handoff": latest_handoff,
"latest_remediation": latest_remediation,
"strict_fingerprint": repeat_state.get("strict_fingerprint"),
"p0_escalation": {
"suppresses_repeated_p0": True,
@@ -188,6 +228,7 @@ def build_drift_fingerprint_state(
"writes_auto_repair_result": False,
"writes_drift_status": False,
"writes_ticket": False,
"writes_remediation_record": False,
"creates_external_ticket": False,
}
@@ -230,6 +271,71 @@ def _handoff_description(context: dict[str, Any]) -> str:
)[:500]
def _is_no_drift_report(report: DriftReport | None) -> bool:
if report is None:
return False
return report.high_count == 0 and report.medium_count == 0 and report.info_count == 0
def _remediation_context(
state: dict[str, Any],
*,
remediation_kind: DriftFingerprintRemediationKind,
remediation_status: DriftFingerprintRemediationStatus,
verification_report: DriftReport | None,
note: str | None,
commands_summary: list[str] | None,
) -> dict[str, Any]:
verification_summary = None
if verification_report is not None:
verification_summary = {
"report_id": verification_report.report_id,
"status": str(_enum_value(verification_report.status)),
"summary": _report_summary(verification_report),
"high_count": verification_report.high_count,
"medium_count": verification_report.medium_count,
"info_count": verification_report.info_count,
"scanned_at": _iso(verification_report.scanned_at),
"is_no_drift": _is_no_drift_report(verification_report),
}
return {
"schema_version": REMEDIATION_SCHEMA_VERSION,
"fingerprint": state.get("fingerprint"),
"namespace": state.get("namespace"),
"remediated_report_id": state.get("latest_report_id"),
"verification_report_id": getattr(verification_report, "report_id", None),
"verification_summary": verification_summary,
"remediation_kind": remediation_kind,
"remediation_status": remediation_status,
"note": note,
"commands_summary": [
str(command)[:180] for command in (commands_summary or [])[:12]
],
"fsm_state_before": state.get("fsm_state"),
"next_step_before": state.get("next_step"),
"writes_incident_state": False,
"writes_auto_repair_result": False,
"writes_drift_status": False,
"writes_ticket": False,
"writes_remediation_record": True,
"creates_external_ticket": False,
"recorded_at": now_taipei().isoformat(),
}
def _remediation_description(context: dict[str, Any]) -> str:
verification = context.get("verification_summary") or {}
return (
f"fingerprint={context.get('fingerprint')} "
f"kind={context.get('remediation_kind')} "
f"status={context.get('remediation_status')} "
f"remediated_report={context.get('remediated_report_id')} "
f"verification_report={context.get('verification_report_id') or '--'} "
f"verification={verification.get('summary') or '--'}"
)[:500]
class DriftFingerprintStateService:
"""Read and record the state of a stable Config Drift fingerprint."""
@@ -249,11 +355,22 @@ class DriftFingerprintStateService:
alternate_fingerprints=_repeat_state_fingerprint_aliases(repeat_state),
report_ids=_repeat_state_report_ids(repeat_state),
)
latest_remediation = await self._fetch_latest_remediation(
fingerprint,
alternate_fingerprints=_repeat_state_fingerprint_aliases(repeat_state),
report_ids={
report.report_id,
*_repeat_state_report_ids(repeat_state),
},
namespace=report.namespace,
allow_namespace_fallback=_is_no_drift_report(report),
)
return build_drift_fingerprint_state(
report,
repeat_state,
open_pr=open_pr,
latest_handoff=latest_handoff,
latest_remediation=latest_remediation,
)
async def record_handoff(
@@ -357,6 +474,132 @@ class DriftFingerprintStateService:
"history": history,
}
async def record_remediation(
self,
*,
report_id: str | None = None,
namespace: str | None = None,
remediation_kind: DriftFingerprintRemediationKind = "live_env_rollback",
remediation_status: DriftFingerprintRemediationStatus | None = None,
verification_report_id: str | None = None,
note: str | None = None,
commands_summary: list[str] | None = None,
) -> dict[str, Any]:
state = await self.get_state(report_id=report_id, namespace=namespace)
verification_report = await self._load_verification_report(
report_id=verification_report_id,
namespace=state.get("namespace") or namespace,
)
resolved_status: DriftFingerprintRemediationStatus = (
remediation_status
or (
"verified_no_drift"
if _is_no_drift_report(verification_report)
else "executed_unverified"
)
)
context = _remediation_context(
state,
remediation_kind=remediation_kind,
remediation_status=resolved_status,
verification_report=verification_report,
note=note,
commands_summary=commands_summary,
)
history = {
"recorded": False,
"alert_operation_id": None,
"timeline_event_id": None,
"reason": None,
}
incident_id = str(state.get("latest_report_id") or "")
success = resolved_status != "verification_failed"
try:
from src.repositories.alert_operation_log_repository import (
get_alert_operation_log_repository,
)
record = await get_alert_operation_log_repository().append(
"CHANGE_APPLIED",
incident_id=incident_id or None,
actor=SERVICE_ACTOR,
action_detail=(
f"drift_fingerprint_remediation:{remediation_kind}"
)[:200],
success=success,
context=context,
)
if record is not None:
history["alert_operation_id"] = getattr(record, "id", None)
except Exception as exc:
logger.warning(
"drift_fingerprint_remediation_aol_failed",
report_id=incident_id,
error=str(exc),
)
try:
from src.services.approval_db import get_timeline_service
event = await get_timeline_service().add_event(
event_type="exec",
status="success" if success else "error",
title="AwoooP drift remediation verified",
description=_remediation_description(context),
actor=SERVICE_ACTOR,
actor_role=remediation_kind,
incident_id=incident_id or None,
)
if event:
history["timeline_event_id"] = event.get("id")
except Exception as exc:
logger.warning(
"drift_fingerprint_remediation_timeline_failed",
report_id=incident_id,
error=str(exc),
)
history["recorded"] = bool(
history.get("alert_operation_id") or history.get("timeline_event_id")
)
if not history["recorded"]:
history["reason"] = "history_sink_unavailable"
latest_remediation = {
"remediation_kind": remediation_kind,
"remediation_status": (
resolved_status if history["recorded"] else "record_failed"
),
"verification_report_id": context.get("verification_report_id"),
"verification_summary": context.get("verification_summary"),
"note": note,
"commands_summary": context.get("commands_summary"),
"created_at": context.get("recorded_at"),
"source": "current_request",
}
updated_state = build_drift_fingerprint_state(
await self._load_report(
report_id=str(state.get("latest_report_id") or ""),
namespace=state.get("namespace"),
),
{
**(state.get("repeat_state") or {}),
"fingerprint": state.get("fingerprint"),
},
open_pr=state.get("open_pr"),
latest_handoff=state.get("latest_handoff"),
latest_remediation=latest_remediation,
)
return {
**updated_state,
"remediation_kind": remediation_kind,
"remediation_status": latest_remediation["remediation_status"],
"history": history,
}
async def _load_report(
self,
*,
@@ -376,6 +619,22 @@ class DriftFingerprintStateService:
return report
raise DriftFingerprintStateNotFoundError(desired_namespace)
async def _load_verification_report(
self,
*,
report_id: str | None,
namespace: str | None,
) -> DriftReport | None:
repo = get_drift_repository()
if report_id:
return await repo.get(report_id)
desired_namespace = namespace or DEFAULT_NAMESPACE
for report in await repo.list_recent(limit=50):
if report.namespace == desired_namespace and _is_no_drift_report(report):
return report
return None
async def _fetch_latest_handoff(
self,
fingerprint: str,
@@ -439,6 +698,86 @@ class DriftFingerprintStateService:
"note": context.get("note"),
}
async def _fetch_latest_remediation(
self,
fingerprint: str,
*,
alternate_fingerprints: set[str] | None = None,
report_ids: set[str] | None = None,
namespace: str | None = None,
allow_namespace_fallback: bool = False,
) -> dict[str, Any] | None:
fingerprints = {fingerprint, *(alternate_fingerprints or set())}
fingerprints = {value for value in fingerprints if value}
report_ids = {value for value in (report_ids or set()) if value}
try:
async with get_db_context() as db:
result = await db.execute(
text(
"""
SELECT id, action_detail, success, context, created_at
FROM alert_operation_log
WHERE actor = :actor
AND action_detail LIKE 'drift_fingerprint_remediation:%'
ORDER BY created_at DESC
LIMIT 100
"""
),
{"actor": SERVICE_ACTOR},
)
rows = result.mappings().all()
except Exception as exc:
logger.warning(
"drift_fingerprint_remediation_lookup_failed",
fingerprint=fingerprint,
error=str(exc),
)
return {
"lookup_error": str(exc)[:160],
"remediation_status": "lookup_failed",
}
namespace_fallback = None
row = None
for candidate in rows:
context = candidate.get("context") or {}
if not isinstance(context, dict):
continue
if context.get("fingerprint") in fingerprints:
row = candidate
break
if context.get("remediated_report_id") in report_ids:
row = candidate
break
if context.get("verification_report_id") in report_ids:
row = candidate
break
if (
allow_namespace_fallback
and namespace
and context.get("namespace") == namespace
and namespace_fallback is None
):
namespace_fallback = candidate
row = row or namespace_fallback
if row is None:
return None
context = row.get("context") or {}
return {
"alert_operation_id": row.get("id"),
"action_detail": row.get("action_detail"),
"success": row.get("success"),
"created_at": _iso(row.get("created_at")),
"remediation_kind": context.get("remediation_kind"),
"remediation_status": context.get("remediation_status") or "recorded",
"remediated_report_id": context.get("remediated_report_id"),
"verification_report_id": context.get("verification_report_id"),
"verification_summary": context.get("verification_summary"),
"note": context.get("note"),
"commands_summary": context.get("commands_summary"),
}
async def _lookup_open_pr(self, report: DriftReport) -> dict[str, Any] | None:
settings = get_settings()
api_url = settings.GITEA_API_URL.rstrip("/")

View File

@@ -27,6 +27,18 @@ def _report(report_id: str = "drift-1", status: DriftStatus = DriftStatus.PENDIN
)
def _no_drift_report(report_id: str = "no-drift-1") -> DriftReport:
return DriftReport(
report_id=report_id,
namespace="awoooi-prod",
high_count=0,
medium_count=0,
info_count=0,
status=DriftStatus.IGNORED,
items=[],
)
def test_build_state_marks_repeated_pending_human() -> None:
current = _report("drift-3")
repeat = build_drift_repeat_state(
@@ -81,3 +93,38 @@ def test_build_state_marks_handoff_recorded() -> None:
assert state["fsm_state"] == "handoff_recorded"
assert state["next_step"] == "operator_review_handoff_and_execute_manual_plan"
def test_build_state_marks_verified_no_drift_remediation() -> None:
report = _no_drift_report("no-drift-2")
repeat = build_drift_repeat_state(report, [])
state = build_drift_fingerprint_state(
report,
repeat,
latest_remediation={
"remediation_kind": "live_env_rollback",
"remediation_status": "verified_no_drift",
"verification_report_id": "no-drift-2",
},
)
assert state["fsm_state"] == "no_drift_verified"
assert state["next_step"] == "monitor_for_recurrence"
assert state["latest_remediation"]["remediation_status"] == "verified_no_drift"
assert state["writes_remediation_record"] is False
def test_build_state_marks_remediation_executed_unverified() -> None:
report = _report("drift-6")
repeat = build_drift_repeat_state(report, [])
state = build_drift_fingerprint_state(
report,
repeat,
latest_remediation={
"remediation_kind": "live_env_rollback",
"remediation_status": "executed_unverified",
},
)
assert state["fsm_state"] == "remediation_executed_unverified"
assert state["next_step"] == "run_verification_scan_then_record_result"

View File

@@ -1069,7 +1069,11 @@
"next": "Next: {step}",
"writes": "Writes: drift={drift}; incident={incident}; repair={repair}; ticket={ticket}",
"pr": "PR: {pr}; zeroDiff={zeroDiff}",
"p0Dedup": "P0 dedupe {hours}h"
"p0Dedup": "P0 dedupe {hours}h",
"remediation": "Remediation: {status}; verification report: {report}",
"remediationKind": "Remediation kind: {kind}",
"remediationVerification": "Verification: {summary}",
"remediationNote": "Note: {note}"
}
},
"neuralCommand": {
@@ -1814,6 +1818,7 @@
"driftFingerprintId": "Fingerprint: {fingerprint}; Report: {report}",
"driftFingerprintPr": "PR: {pr}; zeroDiff={zeroDiff}",
"driftFingerprintNext": "Next: {step}",
"driftFingerprintRemediation": "Remediation: {kind} / {status}; verification report: {report}",
"driftFingerprintEmpty": "No Config Drift fingerprint state yet",
"remediationQueue": "Remediation work: {total}; AI-ready: {ready}; human: {human}",
"telegramCallbacks": "Telegram callback lookup and history summary are being repaired",
@@ -1866,6 +1871,10 @@
"pr_open_waiting_review": "PR waiting review",
"pr_merged_unverified": "PR merged, unverified",
"handoff_recorded": "Handoff recorded",
"no_drift_verified": "No drift, verified",
"remediated_verified": "Remediated, verified",
"remediation_executed_unverified": "Remediated, unverified",
"remediation_verification_failed": "Remediation verification failed",
"adopted_unverified": "Adopted, unverified",
"rolled_back": "Rolled back",
"acknowledged": "Acknowledged",
@@ -1877,6 +1886,8 @@
"review_pr_then_merge_or_reject": "Review PR, then merge or reject",
"verify_git_baseline_then_mark_adopted": "Verify Git baseline, then mark adopted",
"operator_review_handoff_and_execute_manual_plan": "Operator reviews handoff and executes manual plan",
"run_verification_scan_then_record_result": "Run verification scan, then record the result",
"open_manual_investigation_with_failed_verification": "Open manual investigation with the failed verification",
"verify_k8s_matches_git_baseline": "Verify K8s matches Git baseline",
"confirm_no_repeat_after_rollback": "Confirm no repeat after rollback",
"monitor_for_recurrence": "Monitor for recurrence",
@@ -1893,6 +1904,28 @@
"handoff": {
"latest": "Latest handoff: {status}"
},
"remediation": {
"title": "Remediation / Verification",
"latest": "Latest remediation: {kind} / {status}",
"verification": "Verification report: {report}; {summary}",
"note": "Note: {note}"
},
"remediationKinds": {
"live_env_rollback": "Live env rollback",
"git_adopted": "Git adopted",
"git_rollback": "Git rollback",
"zero_diff_pr_cleanup": "Zero-diff PR cleanup",
"manual_noop": "Manual no-op",
"unknown": "Unknown"
},
"remediationStatuses": {
"executed_unverified": "Executed, unverified",
"verified_no_drift": "Verified no drift",
"verification_failed": "Verification failed",
"record_failed": "Record failed",
"lookup_failed": "Lookup failed",
"unknown": "No record yet"
},
"actions": {
"record": "Record handoff",
"recording": "Recording",

View File

@@ -1070,7 +1070,11 @@
"next": "下一步:{step}",
"writes": "寫入drift={drift}incident={incident}repair={repair}ticket={ticket}",
"pr": "PR{pr}zeroDiff={zeroDiff}",
"p0Dedup": "P0 去重 {hours}h"
"p0Dedup": "P0 去重 {hours}h",
"remediation": "修復:{status};驗證 Report{report}",
"remediationKind": "修復方式:{kind}",
"remediationVerification": "驗證結果:{summary}",
"remediationNote": "備註:{note}"
}
},
"neuralCommand": {
@@ -1815,6 +1819,7 @@
"driftFingerprintId": "Fingerprint{fingerprint}Report{report}",
"driftFingerprintPr": "PR{pr}zeroDiff={zeroDiff}",
"driftFingerprintNext": "下一步:{step}",
"driftFingerprintRemediation": "修復:{kind} / {status};驗證 Report{report}",
"driftFingerprintEmpty": "尚無 Config Drift fingerprint 狀態",
"remediationQueue": "補救工作:{total}AI 可接手:{ready};人工:{human}",
"telegramCallbacks": "目前修補 Telegram callback 查詢鏈與歷史摘要",
@@ -1867,6 +1872,10 @@
"pr_open_waiting_review": "PR 等待 review",
"pr_merged_unverified": "PR 已 merge 待驗證",
"handoff_recorded": "交接已記錄",
"no_drift_verified": "無漂移且已驗證",
"remediated_verified": "已修復且已驗證",
"remediation_executed_unverified": "已修復待驗證",
"remediation_verification_failed": "修復驗證失敗",
"adopted_unverified": "已採納待驗證",
"rolled_back": "已回滾",
"acknowledged": "已知悉",
@@ -1878,6 +1887,8 @@
"review_pr_then_merge_or_reject": "review PR 後 merge 或 reject",
"verify_git_baseline_then_mark_adopted": "驗證 Git baseline 後標記採納",
"operator_review_handoff_and_execute_manual_plan": "Operator review 交接並執行人工方案",
"run_verification_scan_then_record_result": "執行驗證掃描並記錄結果",
"open_manual_investigation_with_failed_verification": "建立人工調查並附上失敗驗證",
"verify_k8s_matches_git_baseline": "驗證 K8s 與 Git baseline 一致",
"confirm_no_repeat_after_rollback": "確認回滾後不再重複",
"monitor_for_recurrence": "監控是否復發",
@@ -1894,6 +1905,28 @@
"handoff": {
"latest": "最近交接:{status}"
},
"remediation": {
"title": "修復 / 驗證",
"latest": "最近修復:{kind} / {status}",
"verification": "驗證 Report{report}{summary}",
"note": "備註:{note}"
},
"remediationKinds": {
"live_env_rollback": "線上 env 回滾",
"git_adopted": "Git 採納",
"git_rollback": "Git 回滾",
"zero_diff_pr_cleanup": "零 diff PR 清理",
"manual_noop": "人工確認無需動作",
"unknown": "未知"
},
"remediationStatuses": {
"executed_unverified": "已執行待驗證",
"verified_no_drift": "已驗證無漂移",
"verification_failed": "驗證失敗",
"record_failed": "入庫失敗",
"lookup_failed": "查詢失敗",
"unknown": "尚無記錄"
},
"actions": {
"record": "記錄交接",
"recording": "記錄中",

View File

@@ -229,6 +229,24 @@ type DriftFingerprintState = {
created_at?: string | null;
lookup_error?: string | null;
} | null;
latest_remediation?: {
remediation_kind?: string | null;
remediation_status?: string | null;
remediated_report_id?: string | null;
verification_report_id?: string | null;
verification_summary?: {
report_id?: string | null;
status?: string | null;
summary?: string | null;
high_count?: number | null;
medium_count?: number | null;
info_count?: number | null;
is_no_drift?: boolean | null;
} | null;
note?: string | null;
created_at?: string | null;
lookup_error?: string | null;
} | null;
p0_escalation?: {
suppresses_repeated_p0?: boolean | null;
dedup_window_hours?: number | null;
@@ -422,6 +440,10 @@ function driftFsmStateKey(state?: string | null) {
state === "pr_open_waiting_review" ||
state === "pr_merged_unverified" ||
state === "handoff_recorded" ||
state === "no_drift_verified" ||
state === "remediated_verified" ||
state === "remediation_executed_unverified" ||
state === "remediation_verification_failed" ||
state === "adopted_unverified" ||
state === "rolled_back" ||
state === "acknowledged" ||
@@ -438,6 +460,8 @@ function driftNextStepKey(step?: string | null) {
step === "review_pr_then_merge_or_reject" ||
step === "verify_git_baseline_then_mark_adopted" ||
step === "operator_review_handoff_and_execute_manual_plan" ||
step === "run_verification_scan_then_record_result" ||
step === "open_manual_investigation_with_failed_verification" ||
step === "verify_k8s_matches_git_baseline" ||
step === "confirm_no_repeat_after_rollback" ||
step === "monitor_for_recurrence" ||
@@ -449,9 +473,38 @@ function driftNextStepKey(step?: string | null) {
return "unknown";
}
function driftRemediationKindKey(kind?: string | null) {
if (
kind === "live_env_rollback" ||
kind === "git_adopted" ||
kind === "git_rollback" ||
kind === "zero_diff_pr_cleanup" ||
kind === "manual_noop"
) {
return kind;
}
return "unknown";
}
function driftRemediationStatusKey(status?: string | null) {
if (
status === "executed_unverified" ||
status === "verified_no_drift" ||
status === "verification_failed" ||
status === "record_failed" ||
status === "lookup_failed"
) {
return status;
}
return "unknown";
}
function driftStatusForWorkItem(state: DriftFingerprintState | null): WorkStatus {
const fsm = state?.fsm_state;
if (!state) return "blocked";
if (fsm === "no_drift_verified" || fsm === "remediated_verified") return "live";
if (fsm === "remediation_executed_unverified") return "in_progress";
if (fsm === "remediation_verification_failed") return "blocked";
if (fsm === "adopted_unverified" || fsm === "pr_merged_unverified") return "in_progress";
if (fsm === "rolled_back" || fsm === "acknowledged" || fsm === "ignored") return "watching";
if (fsm === "pr_open_waiting_review" || fsm === "handoff_recorded") return "in_progress";
@@ -482,6 +535,12 @@ function buildWorkItems(
const driftState = telemetry.driftFingerprintState;
const driftFsmKey = driftFsmStateKey(driftState?.fsm_state);
const driftNextKey = driftNextStepKey(driftState?.next_step);
const driftRemediationKind = driftRemediationKindKey(
driftState?.latest_remediation?.remediation_kind
);
const driftRemediationStatus = driftRemediationStatusKey(
driftState?.latest_remediation?.remediation_status
);
const governanceEventsUnavailable = telemetry.governanceEvents === null;
const governanceQueueMissing = telemetry.governanceQueue?.table_pending === true;
const governanceDispatchBlocked =
@@ -575,6 +634,13 @@ function buildWorkItems(
t("evidence.driftFingerprintNext", {
step: t(`driftFingerprint.nextSteps.${driftNextKey}` as never),
}),
t("evidence.driftFingerprintRemediation", {
kind: t(`driftFingerprint.remediationKinds.${driftRemediationKind}` as never),
status: t(
`driftFingerprint.remediationStatuses.${driftRemediationStatus}` as never
),
report: driftState.latest_remediation?.verification_report_id ?? "--",
}),
]
: [t("evidence.driftFingerprintEmpty")],
href: "/drift",
@@ -1104,6 +1170,12 @@ function DriftFingerprintPanel({
});
const fsmKey = driftFsmStateKey(state?.fsm_state);
const nextKey = driftNextStepKey(state?.next_step);
const remediationKindKey = driftRemediationKindKey(
state?.latest_remediation?.remediation_kind
);
const remediationStatusKey = driftRemediationStatusKey(
state?.latest_remediation?.remediation_status
);
const handoffKind = state?.open_pr?.is_zero_diff
? "zero_diff_pr_cleanup"
: "open_pr_review";
@@ -1222,6 +1294,34 @@ function DriftFingerprintPanel({
})}
</p>
</div>
<div className="mt-4 border border-[#d8d3c7] bg-[#faf9f3] px-3 py-2">
<div className="flex items-center gap-2">
<ShieldCheck className="h-4 w-4 text-[#17602a]" aria-hidden="true" />
<h4 className="text-sm font-semibold text-[#141413]">
{t("remediation.title")}
</h4>
</div>
<div className="mt-2 grid gap-1 text-xs leading-5 text-[#5f5b52]">
<p>
{t("remediation.latest", {
kind: t(`remediationKinds.${remediationKindKey}` as never),
status: t(`remediationStatuses.${remediationStatusKey}` as never),
})}
</p>
<p>
{t("remediation.verification", {
report: state.latest_remediation?.verification_report_id ?? "--",
summary:
state.latest_remediation?.verification_summary?.summary ?? "--",
})}
</p>
<p>
{t("remediation.note", {
note: state.latest_remediation?.note ?? "--",
})}
</p>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"

View File

@@ -78,6 +78,17 @@ interface DriftFingerprintState {
latest_handoff?: {
handoff_status?: string | null
} | null
latest_remediation?: {
remediation_kind?: string | null
remediation_status?: string | null
verification_report_id?: string | null
verification_summary?: {
summary?: string | null
is_no_drift?: boolean | null
} | null
note?: string | null
created_at?: string | null
} | null
p0_escalation?: {
suppresses_repeated_p0?: boolean | null
dedup_window_hours?: number | null
@@ -231,7 +242,32 @@ function DriftFingerprintStateCard({
hours: state.p0_escalation?.dedup_window_hours ?? 24,
})}
</span>
<span className="border border-status-healthy/20 bg-status-healthy/10 px-2 py-0.5 text-status-healthy">
{t('fingerprintState.remediation', {
status: state.latest_remediation?.remediation_status ?? '--',
report: state.latest_remediation?.verification_report_id ?? '--',
})}
</span>
</div>
{state.latest_remediation ? (
<div className="mt-3 border border-neutral-200 bg-white px-3 py-2 text-[11px] leading-5 text-neutral-500">
<p>
{t('fingerprintState.remediationKind', {
kind: state.latest_remediation.remediation_kind ?? '--',
})}
</p>
<p>
{t('fingerprintState.remediationVerification', {
summary: state.latest_remediation.verification_summary?.summary ?? '--',
})}
</p>
<p>
{t('fingerprintState.remediationNote', {
note: state.latest_remediation.note ?? '--',
})}
</p>
</div>
) : null}
</div>
)
}