From 11d23b0b7f7b0cde554676f2bd0204ceb70359f1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 26 Jun 2026 12:49:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(awooop):=20=E9=A0=90=E5=A1=AB=20owner=20re?= =?UTF-8?q?lease=20=E8=8D=89=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/platform_operator_service.py | 96 ++++++++++++--- .../test_awooop_operator_timeline_labels.py | 30 ++++- apps/web/messages/en.json | 9 ++ apps/web/messages/zh-TW.json | 9 ++ .../src/components/awooop/status-chain.tsx | 111 ++++++++++++++++++ 5 files changed, 236 insertions(+), 19 deletions(-) diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index 206579f72..cd961fa60 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -4445,6 +4445,60 @@ def _apply_gate_execution_release_contract( route_id = f"ansible-allowlisted-apply:{catalog_id}" mcp_evidence_ready = _safe_int(facts.get("mcp_gateway_total")) > 0 + owner_release_draft = { + "schema_version": "awooop_owner_release_draft_v1", + "status": "ai_prefilled_needs_owner_decision", + "source_id": source_ref, + "work_item_id": f"owner-release-draft:awoooi:{safe_source_ref}", + "ai_prefilled_count": 6, + "human_decision_count": 3, + "runtime_execution_authorized": False, + "runtime_write_allowed": False, + "human_only_fields": [ + "owner_approval_receipt", + "maintenance_window_final_approval", + "rollback_owner_confirmation", + ], + "draft_fields": [ + { + "key": "maintenance_window", + "status": "ai_suggested_owner_review_required", + "value": f"maintenance-window-review:awoooi:{safe_source_ref}", + "reason": "AI can prepare the maintenance packet, but final timing still needs owner approval.", + }, + { + "key": "rollback_owner", + "status": "ai_suggested_owner_review_required", + "value": "sre_oncall_or_service_owner_required", + "reason": "AI can nominate the required role; owner must confirm the named responder.", + }, + { + "key": "blast_radius", + "status": "ai_prefilled_owner_review_required", + "value": f"target={safe_source_ref}; route={route_id}; write_scope=single_allowlisted_apply", + "reason": "Blast radius is limited to the selected allowlisted route until owner release.", + }, + { + "key": "post_apply_verifier", + "status": "ai_prefilled_owner_review_required", + "value": f"verifier-plan:{safe_source_ref}", + "reason": "Verifier plan is prepared from the same source evidence and still needs owner review.", + }, + { + "key": "km_writeback_owner", + "status": "ai_prefilled_after_verified_execution", + "value": "hermes", + "reason": "Hermes prepares KM writeback only after verified execution.", + }, + { + "key": "playbook_trust_owner", + "status": "ai_prefilled_after_verified_execution", + "value": "openclaw_nemotron_review", + "reason": "Trust writeback waits for verifier output and reviewer scoring.", + }, + ], + "next_action": "owner_reviews_ai_prefill_then_release_or_reject", + } field_rows = [ { "key": "incident_ref", @@ -4483,48 +4537,55 @@ def _apply_gate_execution_release_contract( }, { "key": "maintenance_window", - "status": "blocked_missing_maintenance_window", - "value": "--", + "status": "ai_suggested_owner_review_required", + "value": f"maintenance-window-review:awoooi:{safe_source_ref}", "required": True, - "next_step": "collect_maintenance_window", + "next_step": "owner_review_ai_suggested_maintenance_window", }, { "key": "rollback_owner", - "status": "blocked_missing_rollback_owner", - "value": "--", + "status": "ai_suggested_owner_review_required", + "value": "sre_oncall_or_service_owner_required", "required": True, - "next_step": "confirm_rollback_owner_and_disable_plan", + "next_step": "owner_confirm_rollback_owner", }, { "key": "blast_radius", - "status": "blocked_missing_blast_radius", - "value": "--", + "status": "ai_prefilled_owner_review_required", + "value": f"target={safe_source_ref}; route={route_id}; write_scope=single_allowlisted_apply", "required": True, - "next_step": "define_blast_radius_before_apply", + "next_step": "owner_review_ai_prefilled_blast_radius", }, { "key": "post_apply_verifier", - "status": "blocked_missing_post_apply_verifier", + "status": "ai_prefilled_owner_review_required", "value": f"verifier-plan:{safe_source_ref}", "required": True, - "next_step": "approve_post_apply_verifier_plan", + "next_step": "owner_review_post_apply_verifier_plan", }, { "key": "km_writeback_owner", - "status": "blocked_until_verified_execution", - "value": "--", + "status": "ai_prefilled_after_verified_execution", + "value": "hermes", "required": True, "next_step": "assign_km_writeback_owner_after_verified_execution", }, { "key": "playbook_trust_owner", - "status": "blocked_until_verified_execution", - "value": "--", + "status": "ai_prefilled_after_verified_execution", + "value": "openclaw_nemotron_review", "required": True, "next_step": "assign_playbook_trust_owner_after_verified_execution", }, ] - ready_statuses = {"prefilled", "passed", "candidate_ready_no_runtime_authority"} + ready_statuses = { + "prefilled", + "passed", + "candidate_ready_no_runtime_authority", + "ai_suggested_owner_review_required", + "ai_prefilled_owner_review_required", + "ai_prefilled_after_verified_execution", + } ready_count = sum(1 for row in field_rows if row["status"] in ready_statuses) blocked_count = len(field_rows) - ready_count return { @@ -4545,12 +4606,13 @@ def _apply_gate_execution_release_contract( "work_item_id" ), "fields": field_rows, + "owner_release_draft": owner_release_draft, "next_steps": [ { "key": "owner_release", "owner_agent": "openclaw", "work_item_id": f"owner-release-review:awoooi:{safe_source_ref}", - "summary": "collect owner approval receipt and redacted evidence refs", + "summary": "review AI prefilled owner release draft and record approval or rejection", "runtime_write_allowed": False, }, { diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index f1a7cb6d9..68e862826 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -1940,9 +1940,9 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No assert release_contract["route_id"] == ( "ansible-allowlisted-apply:ansible:188-ai-web" ) - assert release_contract["ready_count"] == 4 + assert release_contract["ready_count"] == 10 assert release_contract["total_count"] == 11 - assert release_contract["blocked_count"] == 7 + assert release_contract["blocked_count"] == 1 assert [field["key"] for field in release_contract["fields"]] == [ "incident_ref", "route_id", @@ -1958,7 +1958,33 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No ] assert release_contract["fields"][0]["value"] == "INC-20260625-977E5F" assert release_contract["fields"][4]["status"] == "blocked_missing_owner_release" + assert release_contract["fields"][5]["status"] == ( + "ai_suggested_owner_review_required" + ) + assert release_contract["fields"][7]["status"] == ( + "ai_prefilled_owner_review_required" + ) assert release_contract["fields"][8]["value"] == "verifier-plan:INC-20260625-977E5F" + owner_release_draft = release_contract["owner_release_draft"] + assert owner_release_draft["schema_version"] == "awooop_owner_release_draft_v1" + assert owner_release_draft["status"] == "ai_prefilled_needs_owner_decision" + assert owner_release_draft["ai_prefilled_count"] == 6 + assert owner_release_draft["human_decision_count"] == 3 + assert owner_release_draft["runtime_execution_authorized"] is False + assert owner_release_draft["runtime_write_allowed"] is False + assert owner_release_draft["human_only_fields"] == [ + "owner_approval_receipt", + "maintenance_window_final_approval", + "rollback_owner_confirmation", + ] + assert [field["key"] for field in owner_release_draft["draft_fields"]] == [ + "maintenance_window", + "rollback_owner", + "blast_radius", + "post_apply_verifier", + "km_writeback_owner", + "playbook_trust_owner", + ] assert {step["runtime_write_allowed"] for step in release_contract["next_steps"]} == { False } diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index b48bb0fa3..2b502cc90 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -10555,6 +10555,11 @@ "releaseContractReady": "已預填欄位", "releaseContractBlocked": "待補欄位", "releaseContractNextStepsTitle": "放行合約下一步", + "ownerReleaseDraftTitle": "AI 預填 Owner release 草案", + "ownerReleaseDraftAiPrefilled": "AI 已預填", + "ownerReleaseDraftHumanDecision": "人工決策", + "ownerReleaseDraftHumanOnly": "人工必審欄位", + "ownerReleaseDraftStillHuman": "仍需人工批准", "checklistTitle": "Owner 審查清單", "forbiddenTitle": "禁止動作", "gates": { @@ -10633,6 +10638,10 @@ "blockedMissingPostApplyVerifier": "缺套用後 Verifier", "blockedUntilVerifiedExecution": "驗證執行前受阻", "draftPrefilledNeedsOwnerRelease": "草稿已預填,待 Owner 放行", + "aiPrefilledNeedsOwnerDecision": "AI 已預填,待 Owner 決策", + "aiSuggestedOwnerReviewRequired": "AI 建議,需 Owner 審查", + "aiPrefilledOwnerReviewRequired": "AI 已預填,需 Owner 審查", + "aiPrefilledAfterVerifiedExecution": "AI 已預填,驗證後回寫", "prefilled": "已預填", "blockedMissingMcpEvidence": "缺 MCP 證據", "blockedMissingBlastRadius": "缺 Blast radius" diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index b48bb0fa3..2b502cc90 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -10555,6 +10555,11 @@ "releaseContractReady": "已預填欄位", "releaseContractBlocked": "待補欄位", "releaseContractNextStepsTitle": "放行合約下一步", + "ownerReleaseDraftTitle": "AI 預填 Owner release 草案", + "ownerReleaseDraftAiPrefilled": "AI 已預填", + "ownerReleaseDraftHumanDecision": "人工決策", + "ownerReleaseDraftHumanOnly": "人工必審欄位", + "ownerReleaseDraftStillHuman": "仍需人工批准", "checklistTitle": "Owner 審查清單", "forbiddenTitle": "禁止動作", "gates": { @@ -10633,6 +10638,10 @@ "blockedMissingPostApplyVerifier": "缺套用後 Verifier", "blockedUntilVerifiedExecution": "驗證執行前受阻", "draftPrefilledNeedsOwnerRelease": "草稿已預填,待 Owner 放行", + "aiPrefilledNeedsOwnerDecision": "AI 已預填,待 Owner 決策", + "aiSuggestedOwnerReviewRequired": "AI 建議,需 Owner 審查", + "aiPrefilledOwnerReviewRequired": "AI 已預填,需 Owner 審查", + "aiPrefilledAfterVerifiedExecution": "AI 已預填,驗證後回寫", "prefilled": "已預填", "blockedMissingMcpEvidence": "缺 MCP 證據", "blockedMissingBlastRadius": "缺 Blast radius" diff --git a/apps/web/src/components/awooop/status-chain.tsx b/apps/web/src/components/awooop/status-chain.tsx index dbd289848..5e767a9d0 100644 --- a/apps/web/src/components/awooop/status-chain.tsx +++ b/apps/web/src/components/awooop/status-chain.tsx @@ -289,6 +289,24 @@ export interface AwoooPStatusChain { required?: boolean | null; next_step?: string | null; }>; + owner_release_draft?: { + schema_version?: string | null; + status?: string | null; + source_id?: string | null; + work_item_id?: string | null; + ai_prefilled_count?: number | null; + human_decision_count?: number | null; + runtime_execution_authorized?: boolean | null; + runtime_write_allowed?: boolean | null; + human_only_fields?: string[]; + draft_fields?: Array<{ + key?: string | null; + status?: string | null; + value?: string | null; + reason?: string | null; + }>; + next_action?: string | null; + }; next_steps?: Array<{ key?: string | null; owner_agent?: string | null; @@ -670,6 +688,9 @@ export function AwoooPStatusChainPanel({ const executionReleaseContract = closureReadiness?.execution_release_contract; const executionReleaseFields = executionReleaseContract?.fields ?? []; const executionReleaseNextSteps = executionReleaseContract?.next_steps ?? []; + const ownerReleaseDraft = executionReleaseContract?.owner_release_draft; + const ownerReleaseDraftFields = ownerReleaseDraft?.draft_fields ?? []; + const ownerReleaseHumanFields = ownerReleaseDraft?.human_only_fields ?? []; const ownerReviewChecklist = automationHandoff?.owner_review_checklist ?? []; const forbiddenActions = automationHandoff?.forbidden_actions ?? []; const sourceToolchainTone: SourceFlowTone = sourceCorrelation @@ -824,6 +845,10 @@ export function AwoooPStatusChainPanel({ blocked_missing_post_apply_verifier: t("applyGate.closureStatuses.blockedMissingPostApplyVerifier"), blocked_until_verified_execution: t("applyGate.closureStatuses.blockedUntilVerifiedExecution"), draft_prefilled_needs_owner_release: t("applyGate.closureStatuses.draftPrefilledNeedsOwnerRelease"), + ai_prefilled_needs_owner_decision: t("applyGate.closureStatuses.aiPrefilledNeedsOwnerDecision"), + ai_suggested_owner_review_required: t("applyGate.closureStatuses.aiSuggestedOwnerReviewRequired"), + ai_prefilled_owner_review_required: t("applyGate.closureStatuses.aiPrefilledOwnerReviewRequired"), + ai_prefilled_after_verified_execution: t("applyGate.closureStatuses.aiPrefilledAfterVerifiedExecution"), prefilled: t("applyGate.closureStatuses.prefilled"), blocked_missing_mcp_evidence: t("applyGate.closureStatuses.blockedMissingMcpEvidence"), blocked_missing_blast_radius: t("applyGate.closureStatuses.blockedMissingBlastRadius"), @@ -1451,6 +1476,92 @@ export function AwoooPStatusChainPanel({ ))} + {ownerReleaseDraft ? ( + <> +
+
+
+

{t("applyGate.ownerReleaseDraftTitle")}

+

+ {valueOrEmpty(ownerReleaseDraft.work_item_id, emptyLabel)} +

+
+ + {closureStatusLabel(ownerReleaseDraft.status)} + +
+
+
+ {[ + { + key: "aiPrefilled", + label: t("applyGate.ownerReleaseDraftAiPrefilled"), + value: ownerReleaseDraft.ai_prefilled_count ?? ownerReleaseDraftFields.length, + }, + { + key: "humanDecision", + label: t("applyGate.ownerReleaseDraftHumanDecision"), + value: ownerReleaseDraft.human_decision_count ?? ownerReleaseHumanFields.length, + }, + { + key: "humanOnly", + label: t("applyGate.ownerReleaseDraftHumanOnly"), + value: ownerReleaseHumanFields.length, + }, + { + key: "runtime", + label: t("applyGate.runtimeWrite", { + value: boolValue(ownerReleaseDraft.runtime_write_allowed, emptyLabel), + }), + value: boolValue(ownerReleaseDraft.runtime_execution_authorized, emptyLabel), + }, + ].map((item) => ( +
+

{item.label}

+

+ {item.value} +

+
+ ))} +
+
+ {(ownerReleaseDraftFields.length ? ownerReleaseDraftFields : [{ key: emptyLabel, status: emptyLabel, value: emptyLabel, reason: emptyLabel }]).map((field, index) => ( +
+
+ + +
+

{executionReleaseFieldLabel(field.key)}

+

+ {closureStatusLabel(field.status)} +

+
+
+

+ {valueOrEmpty(field.value, emptyLabel)} +

+

+ {valueOrEmpty(field.reason, emptyLabel)} +

+
+ ))} +
+
+
+ + {t("applyGate.ownerReleaseDraftStillHuman")} + +

+ {ownerReleaseHumanFields.length ? ownerReleaseHumanFields.join(" / ") : emptyLabel} +

+
+
+ + ) : null}
{(executionReleaseFields.length ? executionReleaseFields : [{ key: emptyLabel, status: emptyLabel, value: emptyLabel, next_step: emptyLabel, required: true }]).map((field, index) => (