feat(api): 預填修復候選 PlayBook 草案
This commit is contained in:
@@ -350,7 +350,13 @@ class RepairCandidateService:
|
||||
namespace=namespace,
|
||||
)
|
||||
metadata["playbook_draft_required"] = True
|
||||
metadata["repair_candidate_next_step"] = draft_package["next_step"]
|
||||
metadata["repair_candidate_prefilled_draft_summary"] = self._draft_summary_for_operator(
|
||||
draft_package.get("playbook_draft_template") or {}
|
||||
)
|
||||
metadata["repair_candidate_next_step"] = self._next_step_with_draft_summary(
|
||||
str(draft_package["next_step"]),
|
||||
str(metadata["repair_candidate_prefilled_draft_summary"]),
|
||||
)
|
||||
metadata["repair_candidate_draft_package"] = draft_package
|
||||
metadata["fallback_action"] = fallback_action
|
||||
return RepairCandidateResult(
|
||||
@@ -487,6 +493,16 @@ class RepairCandidateService:
|
||||
evidence=evidence,
|
||||
playbook=playbook,
|
||||
)
|
||||
playbook_draft_template = self._build_playbook_draft_template(
|
||||
coverage_gap=coverage_gap,
|
||||
blockers=blockers,
|
||||
lane=lane,
|
||||
alertname=alertname,
|
||||
target_resource=target_resource,
|
||||
namespace=namespace,
|
||||
evidence=evidence,
|
||||
playbook=playbook,
|
||||
)
|
||||
incident_id = getattr(incident, "incident_id", "") if incident else ""
|
||||
project_id = str(getattr(incident, "project_id", "awoooi") or "awoooi")
|
||||
work_item: dict[str, Any] | None = None
|
||||
@@ -517,6 +533,7 @@ class RepairCandidateService:
|
||||
"next_step": next_step,
|
||||
"required_fields": required_fields,
|
||||
"coverage_gap": coverage_gap,
|
||||
"playbook_draft_template": playbook_draft_template,
|
||||
"blocked_operations": blocked_operations,
|
||||
"target_href": f"/awooop/runs?{run_query}",
|
||||
"work_item_href": f"/awooop/work-items?{work_item_query}",
|
||||
@@ -542,12 +559,128 @@ class RepairCandidateService:
|
||||
"evidence_snapshot_id": evidence_ref,
|
||||
"required_fields": required_fields,
|
||||
"coverage_gap": coverage_gap,
|
||||
"playbook_draft_template": playbook_draft_template,
|
||||
"blocked_operations": blocked_operations,
|
||||
}
|
||||
if work_item:
|
||||
package["awooop_work_item"] = work_item
|
||||
return package
|
||||
|
||||
def _draft_summary_for_operator(self, template: dict[str, Any]) -> str:
|
||||
route = str(template.get("suggested_route") or "--")
|
||||
repair = str(template.get("repair_command_template") or "--")
|
||||
verifier = template.get("verifier_plan_template") or []
|
||||
verifier_head = "--"
|
||||
if isinstance(verifier, list) and verifier:
|
||||
verifier_head = str(verifier[0])
|
||||
return f"route={route}; repair_template={repair}; verifier={verifier_head}"
|
||||
|
||||
def _next_step_with_draft_summary(self, next_step: str, draft_summary: str) -> str:
|
||||
if not draft_summary or draft_summary == "route=--; repair_template=--; verifier=--":
|
||||
return next_step
|
||||
return f"{next_step} 草案已預填:{draft_summary}"
|
||||
|
||||
def _build_playbook_draft_template(
|
||||
self,
|
||||
*,
|
||||
coverage_gap: dict[str, Any],
|
||||
blockers: list[str],
|
||||
lane: str,
|
||||
alertname: str,
|
||||
target_resource: str,
|
||||
namespace: str,
|
||||
evidence: EvidenceSnapshot | None,
|
||||
playbook: Playbook | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Prefill a service-specific PlayBook draft without authorizing execution."""
|
||||
|
||||
target_kind = str(coverage_gap.get("target_kind") or "unknown")
|
||||
target = (target_resource or "").strip()
|
||||
ns = (namespace or "").strip()
|
||||
command_template = "owner_supplied_service_specific_command_after_evidence_review"
|
||||
rollback_template = "owner_supplied_rollback_or_safe_stop_after_evidence_review"
|
||||
route = "owner_review_required"
|
||||
verifier_plan = [
|
||||
"rerun_mcp_evidence_collection",
|
||||
"confirm_fingerprint_recurrence_stops_or_decreases",
|
||||
"write_execution_result_and_verifier_outcome",
|
||||
"update_km_and_playbook_trust_after_owner_review",
|
||||
]
|
||||
|
||||
if target_kind == "k8s_workload":
|
||||
workload = target.replace("deployment/", "").replace("pod/", "").replace("svc/", "")
|
||||
workload_ref = workload or "<deployment>"
|
||||
namespace_ref = ns or "<namespace>"
|
||||
command_template = (
|
||||
f"kubectl rollout restart deployment/{workload_ref} -n {namespace_ref}"
|
||||
)
|
||||
rollback_template = (
|
||||
f"kubectl rollout undo deployment/{workload_ref} -n {namespace_ref}"
|
||||
)
|
||||
route = "k8s_rollout_after_owner_review"
|
||||
verifier_plan.extend([
|
||||
f"kubectl rollout status deployment/{workload_ref} -n {namespace_ref}",
|
||||
f"kubectl get events -n {namespace_ref} --field-selector involvedObject.name={workload_ref}",
|
||||
])
|
||||
elif target_kind == "host_service":
|
||||
service_ref = target or "<service>"
|
||||
command_template = f"systemctl restart {service_ref}"
|
||||
rollback_template = f"systemctl status {service_ref}; journalctl -u {service_ref} -n 120"
|
||||
route = "host_service_route_after_owner_review"
|
||||
verifier_plan.extend([
|
||||
f"systemctl is-active {service_ref}",
|
||||
f"journalctl -u {service_ref} -n 120 --no-pager",
|
||||
])
|
||||
elif target_kind == "service":
|
||||
service_ref = target or "<service>"
|
||||
command_template = f"service-specific repair for {service_ref}"
|
||||
rollback_template = f"service-specific rollback for {service_ref}"
|
||||
route = "service_specific_route_after_owner_review"
|
||||
verifier_plan.append(f"confirm service health for {service_ref}")
|
||||
|
||||
owner_review_checklist = [
|
||||
"確認 alertname 與 target selector 沒有誤配",
|
||||
"確認 MCP evidence refs 足以支持修復假設",
|
||||
"確認 repair command 不是純診斷命令或通用兜底命令",
|
||||
"確認 rollback command 能在失敗時安全收斂",
|
||||
"確認 verifier plan 能判斷成功、失敗與是否升級人工",
|
||||
"確認 blast radius、maintenance window 與 owner 已填寫",
|
||||
"確認 runtime_execution_authorized 仍為 false,直到正式 approval gate 通過",
|
||||
]
|
||||
|
||||
return {
|
||||
"schema_version": "service_specific_playbook_draft_template_v1",
|
||||
"status": "prefilled_owner_review_required",
|
||||
"lane": lane,
|
||||
"coverage_key": coverage_gap.get("coverage_key"),
|
||||
"blocking_stage": coverage_gap.get("blocking_stage"),
|
||||
"blockers": list(dict.fromkeys(blockers)),
|
||||
"alert_selector": {
|
||||
"alertname": alertname or None,
|
||||
"namespace": ns or None,
|
||||
"target_resource": target or None,
|
||||
"target_kind": target_kind,
|
||||
},
|
||||
"mcp_evidence_refs": coverage_gap.get("required_mcp_evidence_refs") or [],
|
||||
"matched_playbook": {
|
||||
"playbook_id": playbook.playbook_id if playbook else None,
|
||||
"name": playbook.name if playbook else None,
|
||||
"status": playbook.status.value if playbook else None,
|
||||
"trust_score": float(playbook.trust_score) if playbook else None,
|
||||
},
|
||||
"suggested_route": route,
|
||||
"repair_command_template": command_template,
|
||||
"rollback_command_template": rollback_template,
|
||||
"verifier_plan_template": list(dict.fromkeys(verifier_plan)),
|
||||
"owner_review_checklist": owner_review_checklist,
|
||||
"evidence_snapshot_id": evidence.snapshot_id if evidence else None,
|
||||
"template_is_executable": False,
|
||||
"approval_required_before_execution": True,
|
||||
"runtime_execution_authorized": False,
|
||||
"writes_runtime_state": False,
|
||||
"telegram_send_authorized": False,
|
||||
}
|
||||
|
||||
def _build_coverage_gap(
|
||||
self,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user