From 1e08440cd03f26df6ae2387ae8c5ca0f7ce32e60 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 11 Jun 2026 19:47:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20=E8=A3=9C=E4=BF=AE=E5=BE=A9?= =?UTF-8?q?=E5=80=99=E9=81=B8=20coverage=20gap=20=E5=A5=91=E7=B4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/repair_candidate_service.py | 108 ++++++++++++++++++ .../tests/test_repair_candidate_service.py | 49 ++++++++ docs/LOGBOOK.md | 10 ++ ...026-06-04-iwooos-security-governance-p0.md | 12 +- 4 files changed, 173 insertions(+), 6 deletions(-) diff --git a/apps/api/src/services/repair_candidate_service.py b/apps/api/src/services/repair_candidate_service.py index a89c7da15..317348616 100644 --- a/apps/api/src/services/repair_candidate_service.py +++ b/apps/api/src/services/repair_candidate_service.py @@ -478,6 +478,15 @@ class RepairCandidateService: "approve_no_action_as_repair", "generic_fallback_repair", ] + coverage_gap = self._build_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 @@ -507,6 +516,7 @@ class RepairCandidateService: "reason": ",".join(blockers), "next_step": next_step, "required_fields": required_fields, + "coverage_gap": coverage_gap, "blocked_operations": blocked_operations, "target_href": f"/awooop/runs?{run_query}", "work_item_href": f"/awooop/work-items?{work_item_query}", @@ -531,12 +541,110 @@ class RepairCandidateService: "matched_playbook_name": playbook.name if playbook else None, "evidence_snapshot_id": evidence_ref, "required_fields": required_fields, + "coverage_gap": coverage_gap, "blocked_operations": blocked_operations, } if work_item: package["awooop_work_item"] = work_item return package + def _build_coverage_gap( + self, + *, + blockers: list[str], + lane: str, + alertname: str, + target_resource: str, + namespace: str, + evidence: EvidenceSnapshot | None, + playbook: Playbook | None, + ) -> dict[str, Any]: + """Describe why this alert is not yet covered by an executable PlayBook.""" + + blocker_set = set(blockers) + coverage_key = self._coverage_key(alertname=alertname, target_resource=target_resource) + target_kind = self._infer_target_kind(target_resource=target_resource, namespace=namespace) + if "mcp_evidence_missing" in blocker_set: + blocking_stage = "mcp_evidence" + elif { + "playbook_not_matched", + "playbook_not_found", + "playbook_generic_fallback_not_repair", + "playbook_observe_only", + "playbook_has_no_repair_steps", + "playbook_has_no_executable_step", + } & blocker_set: + blocking_stage = "service_playbook_coverage" + elif {"playbook_not_approved", "playbook_trust_below_gate"} & blocker_set: + blocking_stage = "playbook_trust_gate" + elif "playbook_command_not_safely_routable" in blocker_set: + blocking_stage = "safe_execution_route" + else: + blocking_stage = "owner_review" + + evidence_refs = [ + "alertmanager_event", + "incident_timeline", + "mcp_health_snapshot", + "k8s_or_host_status", + "metrics_or_logs_window", + "recurrence_fingerprint", + ] + if target_kind == "k8s_workload": + evidence_refs.extend(["k8s_events", "rollout_status"]) + elif target_kind == "host_service": + evidence_refs.extend(["systemd_or_container_status", "host_resource_window"]) + + return { + "schema_version": "repair_candidate_coverage_gap_v1", + "coverage_key": coverage_key, + "target_kind": target_kind, + "blocking_stage": blocking_stage, + "next_owner_lane": lane, + "alertname": alertname or None, + "namespace": namespace or None, + "target_resource": target_resource or None, + "matched_playbook_id": playbook.playbook_id if playbook else None, + "evidence_snapshot_id": evidence.snapshot_id if evidence else None, + "mcp_evidence_ready": bool(evidence and evidence.sensors_succeeded > 0), + "required_mcp_evidence_refs": list(dict.fromkeys(evidence_refs)), + "playbook_template_required": True, + "playbook_template_fields": [ + "symptom_pattern.alert_names", + "symptom_pattern.affected_services", + "mcp_evidence_refs", + "repair_steps.command_or_ansible_ref", + "repair_steps.rollback_command", + "verifier_plan", + "owner_review_record", + "trust_score_update_policy", + ], + "blocked_operations": [ + "auto_execute", + "mark_repaired_without_execution", + "approve_no_action_as_repair", + ], + "runtime_execution_authorized": False, + "writes_runtime_state": False, + } + + def _coverage_key(self, *, alertname: str, target_resource: str) -> str: + raw_alert = (alertname or "unknown-alert").strip().lower() + raw_target = (target_resource or "unknown-target").strip().lower() + safe_alert = "-".join(raw_alert.replace("_", "-").split()) + safe_target = "-".join(raw_target.replace("_", "-").split()) + return f"{safe_alert}:{safe_target}" + + def _infer_target_kind(self, *, target_resource: str, namespace: str) -> str: + target = (target_resource or "").lower() + if any(marker in target for marker in ("node-exporter", "host", "188", "111", "168")): + return "host_service" + if namespace or any(marker in target for marker in ("deployment/", "pod/", "svc/", "api")): + return "k8s_workload" + if target: + return "service" + return "unknown" + def _build_description( self, *, diff --git a/apps/api/tests/test_repair_candidate_service.py b/apps/api/tests/test_repair_candidate_service.py index 5a1cab5a2..48ea11c53 100644 --- a/apps/api/tests/test_repair_candidate_service.py +++ b/apps/api/tests/test_repair_candidate_service.py @@ -229,6 +229,15 @@ async def test_candidate_blocked_when_playbook_is_generic_fallback() -> None: ) assert "建立專屬 PlayBook 草案" in result.metadata["repair_candidate_next_step"] assert "repair_command" in result.metadata["repair_candidate_draft_package"]["required_fields"] + coverage_gap = result.metadata["repair_candidate_draft_package"]["coverage_gap"] + assert coverage_gap["schema_version"] == "repair_candidate_coverage_gap_v1" + assert coverage_gap["coverage_key"] == "unknownalert:awoooi-api" + assert coverage_gap["blocking_stage"] == "service_playbook_coverage" + assert coverage_gap["next_owner_lane"] == "create_service_specific_repair_playbook" + assert coverage_gap["mcp_evidence_ready"] is True + assert coverage_gap["runtime_execution_authorized"] is False + assert "recurrence_fingerprint" in coverage_gap["required_mcp_evidence_refs"] + assert "repair_steps.command_or_ansible_ref" in coverage_gap["playbook_template_fields"] work_item = result.metadata["repair_candidate_draft_package"]["awooop_work_item"] assert work_item["schema_version"] == "awooop_repair_candidate_draft_work_item_v1" assert work_item["work_item_id"].startswith( @@ -238,6 +247,7 @@ async def test_candidate_blocked_when_playbook_is_generic_fallback() -> None: assert work_item["needs_human"] is True assert work_item["decision_effect"] == "none" assert work_item["writes_runtime_state"] is False + assert work_item["coverage_gap"]["coverage_key"] == "unknownalert:awoooi-api" assert "/awooop/work-items?" in work_item["work_item_href"] assert "https://awoooi.wooo.work/zh-TW/awooop/work-items?" in work_item["work_item_url"] @@ -278,11 +288,50 @@ async def test_candidate_blocked_observe_only_prompts_repair_playbook_draft() -> assert result.metadata["repair_candidate_draft_package"]["lane"] == ( "promote_diagnostic_to_repair_playbook" ) + coverage_gap = result.metadata["repair_candidate_draft_package"]["coverage_gap"] + assert coverage_gap["coverage_key"] == "nodeexporterdown:node-exporter-188" + assert coverage_gap["target_kind"] == "host_service" + assert coverage_gap["blocking_stage"] == "service_playbook_coverage" + assert coverage_gap["matched_playbook_id"] == "PB-REPAIR-001" + assert "systemd_or_container_status" in coverage_gap["required_mcp_evidence_refs"] assert "診斷命令保留為 MCP evidence collector" in result.metadata["repair_candidate_next_step"] work_item = result.metadata["repair_candidate_draft_package"]["awooop_work_item"] assert work_item["target_resource"] == "node-exporter-188" assert work_item["lane"] == "promote_diagnostic_to_repair_playbook" assert work_item["safety_level"] == "read_only_work_item_projection" + assert work_item["coverage_gap"]["next_owner_lane"] == "promote_diagnostic_to_repair_playbook" + + +@pytest.mark.asyncio +async def test_missing_mcp_evidence_records_collectable_coverage_gap() -> None: + incident = _incident() + service = RepairCandidateService( + incident_service=FakeIncidentService(), + investigator=FakeInvestigator(_evidence(incident.incident_id, sensors_succeeded=0)), + playbook_repository=FakePlaybookRepository( + _playbook("kubectl rollout restart deployment/awoooi-api -n awoooi-prod") + ), + auto_repair_service=FakeAutoRepairService(), + ) + + result = await service.build_from_incident( + incident=incident, + alertname="NodeExporterDown", + target_resource="awoooi-api", + namespace="awoooi-prod", + message="node exporter is down", + fallback_action="NO_ACTION - REPAIR_CANDIDATE_MISSING", + matched_playbook_id="PB-REPAIR-001", + severity="medium", + ) + + coverage_gap = result.metadata["repair_candidate_draft_package"]["coverage_gap"] + assert coverage_gap["coverage_key"] == "nodeexporterdown:awoooi-api" + assert coverage_gap["blocking_stage"] == "mcp_evidence" + assert coverage_gap["mcp_evidence_ready"] is False + assert coverage_gap["target_kind"] == "k8s_workload" + assert "mcp_health_snapshot" in coverage_gap["required_mcp_evidence_refs"] + assert coverage_gap["runtime_execution_authorized"] is False def test_approval_record_data_uses_preallocated_id_without_leaking_metadata() -> None: diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index c1c872c71..912023bf9 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,13 @@ +## 2026-06-11|P0 MCP evidence / PlayBook 修復候選 D5 + +**背景**:D4 已讓 AwoooP Work Items 能顯示 PlayBook 草案處置板,但後端 blocked result 仍只提供草案欄位與 lane,缺少「是哪個 alert / target 沒有服務專屬 PlayBook coverage、卡在哪一階段、下一步要收哪些 MCP evidence」的結構化契約。這會讓 Telegram 批准後看起來仍像 `REPAIR_CANDIDATE_MISSING` 斷線,而不是可持續推進的 PlayBook 補洞流程。 + +**完成**:`repair_candidate_draft_package_v1` 與 `awooop_repair_candidate_draft_work_item_v1` 新增 `repair_candidate_coverage_gap_v1`,包含 `coverage_key`、`target_kind`、`blocking_stage`、`next_owner_lane`、matched PlayBook、evidence snapshot、MCP evidence readiness、必收證據 refs、PlayBook template fields、blocked operations 與 0 / false runtime 邊界。`node-exporter-188` 類 target 會歸類為 `host_service`,補 `systemd_or_container_status` / `host_resource_window`;K8s workload 會補 `k8s_events` / `rollout_status`。 + +**驗證**:`DATABASE_URL=sqlite+aiosqlite:///tmp/awoooi-test.db pytest apps/api/tests/test_repair_candidate_service.py -q`:`7 passed`,覆蓋通用兜底、診斷型 PlayBook、MCP evidence 缺失與 coverage gap metadata;`python3 -m py_compile apps/api/src/services/repair_candidate_service.py apps/api/src/services/awooop_deeplinks.py` 通過;`git diff --check`、owner response guard、security mirror progress guard 通過。 + +**完成度與邊界**:P0-9 MCP evidence / PlayBook 修復候選:D5 `88%`;Telegram 監控治理長期項 `96% -> 97%`。D5 讓缺候選路徑產出可追蹤的服務 coverage gap,但仍不代表 runtime execution、PlayBook acceptance、verifier、KM 回寫或 live-fire 自動修復已完成。active runtime gate 仍 `0`;不 SSH、不 active scan、不讀 secret payload、不重啟服務 / 主機、不發 Telegram 測試通知、不直接執行修復。 + ## 2026-06-11|P0 MCP evidence / PlayBook 修復候選 D4 **背景**:D3 已把 Telegram 的「修復候選草案」連到 AwoooP Work Items,但 operator 點進工作項後仍缺明確處置板,無法直接看到 PlayBook 草案必填欄位、禁止誤讀原因、下一步與 Runs / 審批關聯。本段補 Work Items 詳細呈現,回應「批准後沒有自動化,也沒有人工操作選項」的實際痛點;這仍是 read-only 接手面,不是 runtime execution。 diff --git a/docs/workplans/2026-06-04-iwooos-security-governance-p0.md b/docs/workplans/2026-06-04-iwooos-security-governance-p0.md index 75484613a..d3853eb2c 100644 --- a/docs/workplans/2026-06-04-iwooos-security-governance-p0.md +++ b/docs/workplans/2026-06-04-iwooos-security-governance-p0.md @@ -7,12 +7,12 @@ | 項目 | 目前值 | |------|--------| | 工作視窗 | IwoooS / AWOOOI 資安治理 P0 | -| 本次乾淨 worktree | `/private/tmp/awoooi-manual-repair-p0-20260611` | -| 本次分支 | `codex/repair-candidate-workitem-ui-20260611` | -| 最新觀察到的 `gitea/main` | `bfb2d028 chore(cd): deploy e8e15fa [skip ci]` | +| 本次乾淨 worktree | `/private/tmp/awoooi-repair-playbook-coverage-20260611` | +| 本次分支 | `codex/repair-playbook-coverage-20260611` | +| 最新觀察到的 `gitea/main` | `8f3ec9f4 fix(i18n): 移除內部工作用語顯示` | | 最新 P0 Telegram 告警 / 批准執行真相鏈基準 | code `32e4beca`、deploy marker `717b5870`、code-review `2658`、CD `2657`;no-action approval 不再觸發 executor,可執行修復 approval 會寫入 `auto_repair_executions`、KM 與 verifier | | 最新 P0 Telegram no-action 人工處置包基準 | code `cd928852`、deploy marker `9181cc0e`、code-review `2666`;正式部署 tree 已包含 no-action 人工處置包、`處置包 / 重診 / 歷史 / 靜默 / 真相鏈 / Runs` 鍵盤、production pod render / keyboard smoke | -| 最新 P0 MCP evidence / PlayBook 修復候選基準 | code `cc614023`、D1 blocker clarity `47d677ac`、D2 manual draft package `febe9ecf`、D3 draft work item `e8d5eafb`、D4 work item detail panel `e8a5bac5`、deploy marker `985a2cfe`;正式部署 tree 經 production pod smoke 與 Work Items browser smoke 確認可由 MCP evidence + approved PlayBook trust 產生 medium approval candidate、綁定預配置 approval id、不外露 preallocated metadata,且通用兜底 / 診斷型 PlayBook 不會被誤當修復命令;若缺安全修復候選,Telegram 人工處置包會顯示阻擋原因、下一步、PlayBook 草案欄位與 AwoooP 修復候選草案工作項,工作項頁會顯示 PlayBook 草案處置板、必填欄位、阻擋原因、下一步與 Runs / 審批連結 | +| 最新 P0 MCP evidence / PlayBook 修復候選基準 | code `cc614023`、D1 blocker clarity `47d677ac`、D2 manual draft package `febe9ecf`、D3 draft work item `e8d5eafb`、D4 work item detail panel `e8a5bac5`、D5 coverage gap contract 本地完成;目前 production deploy marker 仍為 `985a2cfe` 的 D4 Work Items detail panel,D5 尚待推送 / 部署驗證。正式部署 tree 經 production pod smoke 與 Work Items browser smoke 確認可由 MCP evidence + approved PlayBook trust 產生 medium approval candidate、綁定預配置 approval id、不外露 preallocated metadata,且通用兜底 / 診斷型 PlayBook 不會被誤當修復命令;若缺安全修復候選,Telegram 人工處置包會顯示阻擋原因、下一步、PlayBook 草案欄位與 AwoooP 修復候選草案工作項,工作項頁會顯示 PlayBook 草案處置板、必填欄位、阻擋原因、下一步與 Runs / 審批連結;D5 讓 blocked result 進一步輸出服務 coverage gap、blocking stage、必收 MCP evidence refs 與 PlayBook template fields | | 最新 P2-D0 繁中文案基準 | code `cd2275a2`、deploy marker `1920bd08`、code-review `2565`、CD `2564` | | 最新 P2-D1 本地掃描基準 | `VISIBLE_LITERAL_TARGET_SCAN_OK files=221`;全站 TS / TSX 中文 literal 盤點 `35` 檔 / `752` 行;註解語氣 backlog `32` 筆 | | 最新 P2-D1 正式部署基準 | code `f9bf8a28`、deploy marker `879b0a36`、CD `2578`、code-review `2579` | @@ -50,7 +50,7 @@ | Frontend design system / visual grammar | 維持 `60%` | 否 | 本輪只推進 IwoooS 頁面資訊架構與圖表排序,不宣稱全站設計系統完成 | | P2 全站繁中治理 D0+D1+D2 註解語氣 / AIOps sample / Code Review 候選分類 / AwoooP Runs fallback | `53%` | 只限高風險可見 literal、盤點、註解語氣、AIOps 範例資料命名、Code Review route i18n 與 AwoooP Runs fallback readability | D0 已清 messages 高風險殘留;D1 已掃 `apps/web/src` 字串 literal 並修正高風險可見 / bundle 文案;D2 已清 `apps/web/src` 內部稱謂註解、AIOps sample/mock 命名、Code Review 可見文案搬進 `codeReview` namespace,並清理 Runs / Callback / Source Flow fallback 可見殘留。全站 hardcoded TSX 可見 literal 仍需後續分批搬遷 | | AI Agent automation inventory UX / readability | 本地 `100%`;正式站 `100%` | 否 | 治理頁已把 P1-002 後資料重排為決策指揮摘要、決策支援六因子覆蓋率與 Gitea / Runner 缺口矩陣;這是可讀性與決策支援 slice,不調高 backlog `78%`、IwoooS `64%`、S4.9 gate 或 active runtime gate | -| Telegram 監控告警 / 批准執行真相鏈 | outbound 主鏈路 `100%`;批准後執行止血 `100%`;no-action 人工處置包 D0 `100%`;MCP / PlayBook 修復候選 D4 `84%`;治理長期項 `96%` | 是,僅限候選產生、阻擋原因、人工草案包、AwoooP 工作項可追蹤性與 Work Items 詳細接手板 | 已修復 Alertmanager tenant context、既有 approval 收斂告警 recurrence、AI 分析中重複告警 recurrence、no-action approval 誤導執行、可執行修復 execution / KM / verifier 紀錄、no-action 人工處置包、MCP evidence / PlayBook trust 候選產生、通用兜底 / 診斷型 PlayBook 阻擋理由,以及缺候選時的 PlayBook 草案欄位 / 下一步 / AwoooP work item 入口與詳細處置板;但完整自動修復飛輪仍需用真實告警驗證 approval -> execution -> verifier -> KM / PlayBook trust 全鏈,不調高 runtime gate | +| Telegram 監控告警 / 批准執行真相鏈 | outbound 主鏈路 `100%`;批准後執行止血 `100%`;no-action 人工處置包 D0 `100%`;MCP / PlayBook 修復候選 D5 `88%`;治理長期項 `97%` | 是,僅限候選產生、阻擋原因、人工草案包、AwoooP 工作項可追蹤性、Work Items 詳細接手板與 coverage gap metadata | 已修復 Alertmanager tenant context、既有 approval 收斂告警 recurrence、AI 分析中重複告警 recurrence、no-action approval 誤導執行、可執行修復 execution / KM / verifier 紀錄、no-action 人工處置包、MCP evidence / PlayBook trust 候選產生、通用兜底 / 診斷型 PlayBook 阻擋理由、缺候選時的 PlayBook 草案欄位 / 下一步 / AwoooP work item 入口與詳細處置板,以及 blocked result 的服務 coverage gap / blocking stage / required MCP evidence refs;但完整自動修復飛輪仍需用真實告警驗證 approval -> execution -> verifier -> KM / PlayBook trust 全鏈,不調高 runtime gate | ## 2. P0 工作拆解與優先順序 @@ -72,7 +72,7 @@ | P0-6 | Telegram 監控告警靜音修復 | outbound 主鏈路 100%;靜音 / recurrence slice 88%;整體治理見總表 90% | Alertmanager 缺 project context、既有 approval 收斂告警靜音、AI 分析中重複告警靜音皆已修復並正式 smoke;下一步處理 `polling_active=false` 與非 Alertmanager route project context warning | API health、Telegram health、API pod Alertmanager smoke、production logs `converged_alert_recurrence_sent` | | P0-7 | Telegram 批准後執行真相鏈止血 | 100% | no-action approval 不再顯示批准 / 執行中;可執行修復 approval 會寫入 `auto_repair_executions`、KM 與 verifier;下一步補 MCP evidence / PlayBook trust 產生真正修復候選 | 目標 pytest `125 passed`、py_compile、guard、production health、API / worker rollout、production pod classifier readback | | P0-8 | Telegram no-action 人工處置包與操作入口 | 100% | no-action 卡片已新增人工處置包、證據補齊清單、AwoooP 修復候選建立步驟、verifier / KM / PlayBook 回寫提醒,並改成 `處置包`、`重診`、`歷史`、`靜默`、`真相鏈`、`Runs` 鍵盤;舊訊息不 retroactive 改寫 | 目標 pytest `64 passed + 44 passed`、py_compile、guard、production health、API / worker rollout、production pod render / keyboard smoke | -| P0-9 | MCP evidence -> PlayBook 修復候選產生 | D4 `84%` | 已補 webhook fallback 先建立 incident,再收 MCP evidence、查 approved PlayBook、檢查 trust / command safety、產生 medium approval candidate 與 verifier plan;D1 追加通用兜底 PlayBook / 診斷型命令不可誤當修復、阻擋理由繁中化;D2 在缺候選時產生 `repair_candidate_draft_package_v1`、`playbook_draft_required`、下一步與必填欄位;D3 新增 `awooop_repair_candidate_draft_work_item_v1` read-only projection 與 Telegram `工作項目` deeplink;D4 讓 AwoooP Work Items 詳細呈現 PlayBook 草案處置板、必填欄位、阻擋原因、下一步、Runs / 審批連結;下一步要補服務專屬 PlayBook coverage、MCP tool call/result 詳細證據面,並用真實告警跑完 approval -> execution -> verifier -> KM / PlayBook 回寫 | 目標 pytest `132 passed`、py_compile、ruff F/E9、guard、production health、API / Web / Worker / canary rollout、production pod work item render smoke、Work Items desktop / mobile production smoke;status-chain 後續仍必須看到 tool call、PlayBook id、risk gate、repair candidate、verifier plan | +| P0-9 | MCP evidence -> PlayBook 修復候選產生 | D5 `88%` | 已補 webhook fallback 先建立 incident,再收 MCP evidence、查 approved PlayBook、檢查 trust / command safety、產生 medium approval candidate 與 verifier plan;D1 追加通用兜底 PlayBook / 診斷型命令不可誤當修復、阻擋理由繁中化;D2 在缺候選時產生 `repair_candidate_draft_package_v1`、`playbook_draft_required`、下一步與必填欄位;D3 新增 `awooop_repair_candidate_draft_work_item_v1` read-only projection 與 Telegram `工作項目` deeplink;D4 讓 AwoooP Work Items 詳細呈現 PlayBook 草案處置板、必填欄位、阻擋原因、下一步、Runs / 審批連結;D5 新增 `repair_candidate_coverage_gap_v1`,讓 blocked result 帶出 coverage key、target kind、blocking stage、必收 MCP evidence refs、PlayBook template fields 與 runtime 0 / false 邊界;下一步要補 MCP tool call/result 詳細證據面與真實告警 approval -> execution -> verifier -> KM / PlayBook 回寫 | 目標 pytest `7 passed`、py_compile、guard、diff check;後續部署後需補 production health、API / worker rollout 與 production pod metadata render smoke;status-chain 後續仍必須看到 tool call、PlayBook id、risk gate、repair candidate、verifier plan | ## 3. S4.9 Owner Response Gate 規範