diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 5004bea00..db695d124 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -74,6 +74,9 @@ from src.services.ai_provider_route_matrix import ( from src.services.service_health_gap_matrix import ( load_latest_service_health_gap_matrix, ) +from src.services.service_health_failure_notification_policy import ( + load_latest_service_health_failure_notification_policy, +) from src.services.package_supply_chain_inventory import ( load_latest_package_supply_chain_inventory, ) @@ -709,6 +712,34 @@ async def get_backup_notification_policy() -> dict[str, Any]: ) from exc +@router.get( + "/service-health-failure-notification-policy", + response_model=dict[str, Any], + summary="取得服務健康失敗限定通知合約", + description=( + "讀取最新已提交的 service health failure-only Telegram / AwoooP 通知合約;" + "此端點只回傳成功降噪、action-required 與 failure escalation 規則," + "不送通知、不做 live probe、不重啟服務、不改 endpoint、不觸發 workflow / runtime execution、" + "不讀取 secret payload、不回傳工作視窗對話內容或 prompt。" + ), +) +async def get_service_health_failure_notification_policy() -> dict[str, Any]: + """Return the latest read-only service health failure-only notification policy.""" + try: + return await asyncio.to_thread(load_latest_service_health_failure_notification_policy) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("service_health_failure_notification_policy_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="服務健康失敗限定通知合約快照無效", + ) from exc + + @router.get( "/backup-restore-drill-approval-package-template", response_model=dict[str, Any], diff --git a/apps/api/src/services/service_health_failure_notification_policy.py b/apps/api/src/services/service_health_failure_notification_policy.py new file mode 100644 index 000000000..c94ea3d6a --- /dev/null +++ b/apps/api/src/services/service_health_failure_notification_policy.py @@ -0,0 +1,269 @@ +""" +Service health failure-only notification policy snapshot. + +Loads the latest committed, read-only service health notification policy. The +policy defines success-noise suppression, failure/action-required escalation, +message redaction, and frontend display limits. It never sends Telegram or +AwoooP notifications, writes operator events, probes live systems, restarts +services, changes endpoints, triggers workflows, reads secrets, or displays +work-window conversation transcripts. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_evaluations_dir + +_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__)) +_SNAPSHOT_PATTERN = "service_health_failure_notification_policy_*.json" +_SCHEMA_VERSION = "service_health_failure_notification_policy_v1" + +_CONVERSATION_TRANSCRIPT_MARKERS = { + "# In app browser", + "My request for Codex", + "Current URL:", + "AGENTS.md instructions", + "", + "批准!繼續", +} + + +def load_latest_service_health_failure_notification_policy( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed service health failure notification policy.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError( + f"no service health failure notification policy snapshots found in {directory}" + ) + + latest = candidates[-1] + with latest.open(encoding="utf-8") as handle: + payload = json.load(handle) + + if not isinstance(payload, dict): + raise ValueError(f"{latest}: expected JSON object") + _require_schema(payload, _SCHEMA_VERSION, str(latest)) + _require_read_only_boundaries(payload, str(latest)) + _require_operation_boundaries(payload, str(latest)) + _require_rollup_consistency(payload, str(latest)) + _require_success_noise_suppression(payload, str(latest)) + _require_failure_only_escalation(payload, str(latest)) + _require_frontend_redaction_contract(payload, str(latest)) + _require_no_plaintext_secret_payload_keys(payload, str(latest)) + _require_no_conversation_transcript_content(payload, str(latest)) + return payload + + +def _require_schema(payload: dict[str, Any], expected: str, label: str) -> None: + actual = payload.get("schema_version") + if actual != expected: + raise ValueError(f"{label}: expected schema_version={expected}, got {actual!r}") + + +def _require_read_only_boundaries(payload: dict[str, Any], label: str) -> None: + program_status = payload.get("program_status") or {} + if program_status.get("read_only_mode") is not True: + raise ValueError(f"{label}: program_status.read_only_mode must be true") + + approval_boundaries = payload.get("approval_boundaries") or {} + allowed = sorted(key for key, value in approval_boundaries.items() if value is not False) + if allowed: + raise ValueError(f"{label}: approval boundaries must remain false: {allowed}") + + +def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None: + boundaries = payload.get("operation_boundaries") or {} + if boundaries.get("read_only_policy_allowed") is not True: + raise ValueError(f"{label}: read_only_policy_allowed must be true") + + blocked_flags = { + "notification_send_allowed", + "telegram_test_message_allowed", + "awooop_event_write_allowed", + "live_probe_allowed", + "external_health_probe_allowed", + "service_restart_allowed", + "endpoint_change_allowed", + "workflow_trigger_allowed", + "runtime_execution_allowed", + "secret_plaintext_allowed", + } + allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False) + if allowed: + raise ValueError(f"{label}: operation boundaries must remain false: {allowed}") + + +def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None: + rules = payload.get("policy_rules") or [] + rollups = payload.get("rollups") or {} + if rollups.get("total_rules") != len(rules): + raise ValueError(f"{label}: rollups.total_rules must match policy_rules") + + by_decision: dict[str, int] = {} + for rule in rules: + decision = str(rule.get("decision")) + by_decision[decision] = by_decision.get(decision, 0) + 1 + if rollups.get("by_decision") != by_decision: + raise ValueError(f"{label}: rollups.by_decision must match policy rule decisions") + + immediate_ids = { + rule.get("rule_id") + for rule in rules + if rule.get("decision") == "escalate_immediate" + } + if set(rollups.get("immediate_escalation_rule_ids") or []) != immediate_ids: + raise ValueError(f"{label}: rollups.immediate_escalation_rule_ids must match immediate rules") + + suppressed_success_ids = { + rule.get("rule_id") + for rule in rules + if rule.get("service_state") == "verified" + and rule.get("decision") == "suppress_immediate_success" + } + if set(rollups.get("suppressed_success_rule_ids") or []) != suppressed_success_ids: + raise ValueError(f"{label}: rollups.suppressed_success_rule_ids must match suppressed success rules") + + action_required_ids = { + rule.get("rule_id") + for rule in rules + if rule.get("decision") == "create_action_required" + } + if set(rollups.get("action_required_rule_ids") or []) != action_required_ids: + raise ValueError(f"{label}: rollups.action_required_rule_ids must match action-required rules") + + if rollups.get("notification_send_allowed_count") != 0: + raise ValueError(f"{label}: rollups.notification_send_allowed_count must remain 0") + + +def _require_success_noise_suppression(payload: dict[str, Any], label: str) -> None: + channels = payload.get("notification_channels") or [] + noisy_channels = [ + channel.get("channel_id") + for channel in channels + if channel.get("success_immediate_allowed") is not False + ] + if noisy_channels: + raise ValueError(f"{label}: channels must not allow success immediate notifications: {noisy_channels}") + + success_escalations = [ + rule.get("rule_id") + for rule in payload.get("policy_rules") or [] + if rule.get("service_state") == "verified" + and rule.get("decision") != "suppress_immediate_success" + ] + if success_escalations: + raise ValueError(f"{label}: verified service health rules must suppress immediate notifications") + + template_contract = payload.get("message_template_contract") or {} + success_policy = str(template_contract.get("success_message_policy") or "") + if "不得即時" not in success_policy or "Telegram / AwoooP" not in success_policy: + raise ValueError(f"{label}: success_message_policy must suppress Telegram / AwoooP success noise") + + +def _require_failure_only_escalation(payload: dict[str, Any], label: str) -> None: + escalation_rules = [ + rule + for rule in payload.get("policy_rules") or [] + if rule.get("decision") == "escalate_immediate" + ] + invalid_states = sorted( + rule.get("rule_id") + for rule in escalation_rules + if rule.get("service_state") not in {"failed", "blocked"} + ) + if invalid_states: + raise ValueError(f"{label}: immediate escalation must be failure-only: {invalid_states}") + + telegram_non_failure = sorted( + rule.get("rule_id") + for rule in payload.get("policy_rules") or [] + if "telegram_ops" in (rule.get("channels") or []) + and rule.get("decision") != "escalate_immediate" + ) + if telegram_non_failure: + raise ValueError(f"{label}: telegram_ops must only appear on immediate failure escalation rules") + + template_contract = payload.get("message_template_contract") or {} + required_fields = set(template_contract.get("required_fields") or []) + required = {"stage", "next_action", "blocked_reason", "target_id", "severity", "evidence_ref"} + if not required.issubset(required_fields): + raise ValueError(f"{label}: message_template_contract.required_fields missing failure context") + + forbidden_fields = set(template_contract.get("forbidden_fields") or []) + required_forbidden_fields = { + "secret_value", + "token", + "authorization_header", + "work_window_transcript", + "codex_user_message", + "prompt_text", + "chain_of_thought", + "session_id", + "browser_context", + } + if not required_forbidden_fields.issubset(forbidden_fields): + raise ValueError(f"{label}: message_template_contract.forbidden_fields missing redaction boundary") + + +def _require_frontend_redaction_contract(payload: dict[str, Any], label: str) -> None: + contract = payload.get("display_redaction_contract") or {} + if contract.get("conversation_transcript_display_allowed") is not False: + raise ValueError(f"{label}: conversation transcript display must remain false") + if contract.get("redaction_required") is not True: + raise ValueError(f"{label}: frontend redaction must be required") + + forbidden = set(contract.get("forbidden_frontend_content") or []) + required_forbidden = { + "工作視窗對話內容", + "Codex / user 訊息逐字稿", + "prompt / chain-of-thought", + "session id / browser context", + "secret / token / authorization header", + } + if not required_forbidden.issubset(forbidden): + raise ValueError(f"{label}: display_redaction_contract is missing required forbidden content") + + allowed_fields = set(contract.get("allowed_frontend_fields") or []) + if "committed evidence ref" not in allowed_fields or "policy rule summary" not in allowed_fields: + raise ValueError(f"{label}: display_redaction_contract must limit frontend to committed policy evidence") + + +def _require_no_plaintext_secret_payload_keys(value: Any, label: str, path: str = "$") -> None: + if isinstance(value, dict): + forbidden_key_fragments = { + "secret_value", + "token_value", + "authorization_header", + "private_key", + "webhook_secret", + "runner_token", + "api_key_value", + "password_value", + } + for key, nested in value.items(): + lowered = str(key).lower() + if any(fragment in lowered for fragment in forbidden_key_fragments): + raise ValueError(f"{label}: forbidden secret payload key at {path}.{key}") + _require_no_plaintext_secret_payload_keys(nested, label, f"{path}.{key}") + elif isinstance(value, list): + for index, nested in enumerate(value): + _require_no_plaintext_secret_payload_keys(nested, label, f"{path}[{index}]") + + +def _require_no_conversation_transcript_content(value: Any, label: str, path: str = "$") -> None: + if isinstance(value, dict): + for key, nested in value.items(): + _require_no_conversation_transcript_content(nested, label, f"{path}.{key}") + elif isinstance(value, list): + for index, nested in enumerate(value): + _require_no_conversation_transcript_content(nested, label, f"{path}[{index}]") + elif isinstance(value, str): + for marker in _CONVERSATION_TRANSCRIPT_MARKERS: + if marker in value: + raise ValueError(f"{label}: forbidden work-window conversation content at {path}") diff --git a/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py b/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py index 2d77f7399..4de7f0b0d 100644 --- a/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py +++ b/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py @@ -16,16 +16,16 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho assert response.status_code == 200 data = response.json() assert data["schema_version"] == "ai_agent_automation_backlog_v1" - assert data["program_status"]["overall_completion_percent"] == 88 + assert data["program_status"]["overall_completion_percent"] == 92 assert data["program_status"]["read_only_mode"] is True - assert data["program_status"]["current_task_id"] == "P1-006" - assert data["program_status"]["next_task_id"] == "P1-007" + assert data["program_status"]["current_task_id"] == "P1-007" + assert data["program_status"]["next_task_id"] == "P2-004" assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 25 assert data["rollups"]["by_priority"]["P1"] == 23 - assert data["rollups"]["by_status"]["done"] == 22 + assert data["rollups"]["by_status"]["done"] == 23 assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 22 - assert data["progress_summary"]["overall_percent"] == 88 - assert data["progress_summary"]["done_items"] == 22 + assert data["progress_summary"]["overall_percent"] == 92 + assert data["progress_summary"]["done_items"] == 23 assert data["progress_summary"]["total_items"] == 25 assert data["item_approval_boundary_rollup"]["total_items"] == 25 assert data["item_approval_boundary_rollup"]["items_requiring_explicit_approval"] == [ @@ -73,6 +73,12 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho assert p1_006["approval_boundary"]["mode"] == "read_only_allowed" assert "live_probe" in p1_006["approval_boundary"]["blocked_actions"] assert "/zh-TW/governance?tab=automation-inventory" in p1_006["evidence_refs"] + p1_007 = next(item for item in data["backlog_items"] if item["item_id"] == "AUTO-P1-007") + assert p1_007["status"] == "done" + assert p1_007["next_review"] == "P2-004" + assert p1_007["approval_boundary"]["mode"] == "read_only_allowed" + assert "notification_send" in p1_007["approval_boundary"]["blocked_actions"] + assert "service_health_failure_notification_policy_2026-06-05.json" in p1_007["evidence_refs"][1] p1_306 = next(item for item in data["backlog_items"] if item["item_id"] == "AUTO-P1-306") assert p1_306["approval_boundary"]["mode"] == "read_only_allowed" assert "runtime_execution" in p1_306["approval_boundary"]["blocked_actions"] diff --git a/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py b/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py index 02023c58a..852897e77 100644 --- a/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py +++ b/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py @@ -18,10 +18,10 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert data["schema_version"] == "ai_agent_automation_inventory_snapshot_v1" assert data["program_status"]["overall_completion_percent"] == 100 assert data["program_status"]["read_only_mode"] is True - assert data["program_status"]["current_task_id"] == "P1-006" - assert data["program_status"]["next_task_id"] == "P1-007" - assert data["task_approval_boundary_rollup"]["total_tasks"] == len(data["tasks"]) == 32 - assert data["task_approval_boundary_rollup"]["by_mode"]["read_only_allowed"] == 29 + assert data["program_status"]["current_task_id"] == "P1-007" + assert data["program_status"]["next_task_id"] == "P2-004" + assert data["task_approval_boundary_rollup"]["total_tasks"] == len(data["tasks"]) == 33 + assert data["task_approval_boundary_rollup"]["by_mode"]["read_only_allowed"] == 30 assert data["task_approval_boundary_rollup"]["by_mode"]["production_change_blocked"] == 1 assert data["task_approval_boundary_rollup"]["tasks_requiring_explicit_approval"] == [ "P0-001", @@ -58,6 +58,11 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert p1_006["approval_boundary"]["mode"] == "read_only_allowed" assert "live_probe" in p1_006["approval_boundary"]["blocked_actions"] assert "service health evidence cards" in p1_006["output"] + p1_007 = next(task for task in data["tasks"] if task["task_id"] == "P1-007") + assert p1_007["status"] == "done" + assert p1_007["approval_boundary"]["mode"] == "read_only_allowed" + assert "notification_send" in p1_007["approval_boundary"]["blocked_actions"] + assert "service_health_failure_notification_policy_2026-06-05.json" in p1_007["output"] assert any(task["task_id"] == "P1-204" for task in data["tasks"]) assert any(task["task_id"] == "P1-205" for task in data["tasks"]) assert any(task["task_id"] == "P1-206" for task in data["tasks"]) @@ -92,3 +97,4 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert any(evidence["evidence_id"] == "ai_provider_route_matrix_api" for evidence in data["evidence"]) assert any(evidence["evidence_id"] == "service_health_gap_matrix_api" for evidence in data["evidence"]) assert any(evidence["evidence_id"] == "service_health_evidence_cards_ui" for evidence in data["evidence"]) + assert any(evidence["evidence_id"] == "service_health_failure_notification_policy_api" for evidence in data["evidence"]) diff --git a/apps/api/tests/test_service_health_failure_notification_policy.py b/apps/api/tests/test_service_health_failure_notification_policy.py new file mode 100644 index 000000000..8bca2a773 --- /dev/null +++ b/apps/api/tests/test_service_health_failure_notification_policy.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import json + +import pytest + +from src.services.service_health_failure_notification_policy import ( + load_latest_service_health_failure_notification_policy, +) + + +def test_load_latest_service_health_failure_notification_policy_reads_newest_file(tmp_path): + older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=99) + newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100) + (tmp_path / "service_health_failure_notification_policy_2026-06-04.json").write_text( + json.dumps(older), + encoding="utf-8", + ) + (tmp_path / "service_health_failure_notification_policy_2026-06-05.json").write_text( + json.dumps(newer), + encoding="utf-8", + ) + + loaded = load_latest_service_health_failure_notification_policy(tmp_path) + + assert loaded["generated_at"] == "2026-06-05T00:00:00+08:00" + assert loaded["program_status"]["overall_completion_percent"] == 100 + assert loaded["rollups"]["total_rules"] == 3 + assert loaded["operation_boundaries"]["notification_send_allowed"] is False + + +def test_service_health_failure_notification_policy_requires_read_only_mode(tmp_path): + snapshot = _snapshot() + snapshot["program_status"]["read_only_mode"] = False + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="read_only_mode"): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def test_service_health_failure_notification_policy_blocks_notification_send(tmp_path): + snapshot = _snapshot() + snapshot["operation_boundaries"]["notification_send_allowed"] = True + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="operation boundaries"): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def test_service_health_failure_notification_policy_requires_success_suppression(tmp_path): + snapshot = _snapshot() + snapshot["policy_rules"][0]["decision"] = "escalate_immediate" + snapshot["rollups"]["by_decision"] = { + "escalate_immediate": 2, + "create_action_required": 1, + } + snapshot["rollups"]["immediate_escalation_rule_ids"] = [ + "service_health_verified", + "production_api_health_failed", + ] + snapshot["rollups"]["suppressed_success_rule_ids"] = [] + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="verified service health rules"): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def test_service_health_failure_notification_policy_requires_message_contract(tmp_path): + snapshot = _snapshot() + snapshot["message_template_contract"]["required_fields"].remove("blocked_reason") + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="message_template_contract"): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def test_service_health_failure_notification_policy_requires_redaction_contract(tmp_path): + snapshot = _snapshot() + snapshot["display_redaction_contract"]["conversation_transcript_display_allowed"] = True + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="conversation transcript"): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def test_service_health_failure_notification_policy_rejects_transcript_markers(tmp_path): + snapshot = _snapshot() + snapshot["message_template_contract"]["failure_message_policy"] = "禁止標記:" + "批准!" + "繼續" + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="forbidden work-window conversation content"): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def test_service_health_failure_notification_policy_requires_forbidden_redaction_fields(tmp_path): + snapshot = _snapshot() + snapshot["message_template_contract"]["forbidden_fields"].remove("work_window_transcript") + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="forbidden_fields"): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def test_service_health_failure_notification_policy_fails_when_missing(tmp_path): + with pytest.raises(FileNotFoundError): + load_latest_service_health_failure_notification_policy(tmp_path) + + +def _write_snapshot(tmp_path, snapshot: dict) -> None: + (tmp_path / "service_health_failure_notification_policy_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + +def _snapshot( + *, + generated_at: str = "2026-06-05T00:00:00+08:00", + completion: int = 100, +) -> dict: + return { + "schema_version": "service_health_failure_notification_policy_v1", + "generated_at": generated_at, + "source_service_health_matrix_ref": "docs/evaluations/service_health_gap_matrix_2026-06-05.json", + "source_refs": ["docs/evaluations/service_health_gap_matrix_2026-06-05.json"], + "program_status": { + "overall_completion_percent": completion, + "current_priority": "P1", + "current_task_id": "P1-007", + "next_task_id": "P2-004", + "read_only_mode": True, + }, + "rollups": { + "total_rules": 3, + "by_decision": { + "suppress_immediate_success": 1, + "create_action_required": 1, + "escalate_immediate": 1, + }, + "immediate_escalation_rule_ids": ["production_api_health_failed"], + "suppressed_success_rule_ids": ["service_health_verified"], + "action_required_rule_ids": ["stale_endpoint_detected"], + "notification_send_allowed_count": 0, + }, + "notification_channels": [ + _channel("telegram_ops", immediate_allowed=True, requires_operator_action=True), + _channel("daily_status_summary", immediate_allowed=False, requires_operator_action=False), + ], + "policy_rules": [ + _rule("service_health_verified", "verified", "info", "suppress_immediate_success"), + _rule("stale_endpoint_detected", "action_required", "warning", "create_action_required"), + _rule("production_api_health_failed", "failed", "critical", "escalate_immediate"), + ], + "message_template_contract": { + "required_fields": [ + "stage", + "next_action", + "blocked_reason", + "auto_or_manual", + "target_id", + "severity", + "evidence_ref", + ], + "forbidden_fields": [ + "secret_value", + "token", + "authorization_header", + "work_window_transcript", + "codex_user_message", + "prompt_text", + "chain_of_thought", + "session_id", + "browser_context", + ], + "success_message_policy": "成功不得即時送 Telegram / AwoooP。", + "failure_message_policy": "失敗才升級;此 snapshot 不授權實際發送。", + }, + "display_redaction_contract": { + "frontend_display_policy": "前端只顯示 committed policy、rule summary、evidence ref、下一步與禁止事項。", + "conversation_transcript_display_allowed": False, + "redaction_required": True, + "forbidden_frontend_content": [ + "工作視窗對話內容", + "Codex / user 訊息逐字稿", + "prompt / chain-of-thought", + "session id / browser context", + "secret / token / authorization header", + ], + "allowed_frontend_fields": ["committed evidence ref", "policy rule summary"], + }, + "agent_roles": [], + "operation_boundaries": { + "read_only_policy_allowed": True, + "notification_send_allowed": False, + "telegram_test_message_allowed": False, + "awooop_event_write_allowed": False, + "live_probe_allowed": False, + "external_health_probe_allowed": False, + "service_restart_allowed": False, + "endpoint_change_allowed": False, + "workflow_trigger_allowed": False, + "runtime_execution_allowed": False, + "secret_plaintext_allowed": False, + }, + "approval_boundaries": { + "notification_send_approved": False, + "telegram_test_message_approved": False, + "awooop_event_write_approved": False, + "live_probe_approved": False, + "service_restart_approved": False, + "endpoint_change_approved": False, + "runtime_execution_approved": False, + }, + } + + +def _channel(channel_id: str, *, immediate_allowed: bool, requires_operator_action: bool) -> dict: + return { + "channel_id": channel_id, + "purpose": "test", + "immediate_allowed": immediate_allowed, + "success_immediate_allowed": False, + "requires_operator_action": requires_operator_action, + } + + +def _rule(rule_id: str, state: str, severity: str, decision: str) -> dict: + return { + "rule_id": rule_id, + "event_kind": rule_id, + "service_state": state, + "severity": severity, + "decision": decision, + "channels": ["daily_status_summary"], + "owner_agent": "openclaw", + "requires_incident": decision == "escalate_immediate", + "requires_approval_record": False, + "message_contract": "test", + "evidence_refs": ["docs/evaluations/service_health_gap_matrix_2026-06-05.json"], + } diff --git a/apps/api/tests/test_service_health_failure_notification_policy_api.py b/apps/api/tests/test_service_health_failure_notification_policy_api.py new file mode 100644 index 000000000..0739ebc39 --- /dev/null +++ b/apps/api/tests/test_service_health_failure_notification_policy_api.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import json + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router + + +def test_service_health_failure_notification_policy_endpoint_returns_committed_snapshot(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/service-health-failure-notification-policy") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "service_health_failure_notification_policy_v1" + assert data["program_status"]["overall_completion_percent"] == 100 + assert data["program_status"]["read_only_mode"] is True + assert data["program_status"]["current_task_id"] == "P1-007" + assert data["program_status"]["next_task_id"] == "P2-004" + assert data["rollups"]["total_rules"] == len(data["policy_rules"]) == 7 + assert data["rollups"]["by_decision"]["suppress_immediate_success"] == 2 + assert data["rollups"]["by_decision"]["escalate_immediate"] == 3 + assert len(data["rollups"]["immediate_escalation_rule_ids"]) == 3 + assert len(data["rollups"]["suppressed_success_rule_ids"]) == 2 + assert len(data["rollups"]["action_required_rule_ids"]) == 2 + assert data["rollups"]["notification_send_allowed_count"] == 0 + assert data["operation_boundaries"]["read_only_policy_allowed"] is True + assert data["operation_boundaries"]["notification_send_allowed"] is False + assert data["operation_boundaries"]["telegram_test_message_allowed"] is False + assert data["operation_boundaries"]["awooop_event_write_allowed"] is False + assert data["operation_boundaries"]["live_probe_allowed"] is False + assert data["operation_boundaries"]["service_restart_allowed"] is False + assert data["operation_boundaries"]["endpoint_change_allowed"] is False + assert data["operation_boundaries"]["runtime_execution_allowed"] is False + assert "blocked_reason" in data["message_template_contract"]["required_fields"] + assert "secret_value" in data["message_template_contract"]["forbidden_fields"] + assert "work_window_transcript" in data["message_template_contract"]["forbidden_fields"] + assert data["display_redaction_contract"]["conversation_transcript_display_allowed"] is False + assert data["display_redaction_contract"]["redaction_required"] is True + assert "工作視窗對話內容" in data["display_redaction_contract"]["forbidden_frontend_content"] + assert all( + rule["decision"] == "suppress_immediate_success" + for rule in data["policy_rules"] + if rule["service_state"] == "verified" + ) + + serialized = json.dumps(data, ensure_ascii=False) + for marker in [ + "批准!繼續", + "My request for Codex", + "Current URL:", + "# In app browser", + "AGENTS.md instructions", + ]: + assert marker not in serialized diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 339218836..639b81f9a 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3424,6 +3424,46 @@ "high": "高", "critical": "關鍵" } + }, + "serviceHealthNotification": { + "title": "Service health 失敗限定通知合約", + "source": "{generated} · {current} → {next}", + "quietBadge": "成功降噪 {count}", + "redactionBadge": "前端隔離 {count} 類", + "templateTitle": "訊息欄位合約", + "boundaryTitle": "操作邊界", + "redactionTitle": "前端顯示紅線", + "boundaryDetail": "notification={notification} · live_probe={probe} · restart={restart} · runtime={runtime}", + "metrics": { + "rules": "規則", + "quiet": "成功降噪", + "actionRequired": "需處置", + "escalate": "失敗升級", + "allowedSend": "允許發送" + }, + "map": { + "failureOnly": "Failure-only", + "failureOnlyDetail": "只有 failed / blocked critical 才進 Telegram / AwoooP 升級合約。", + "quietSuccess": "成功不洗版", + "quietSuccessDetail": "verified 狀態只進治理頁與每日摘要。", + "blockedOperations": "執行入口", + "blockedOperationsDetail": "通知發送、live probe、restart、endpoint change 與 runtime execution 皆為 0。", + "redaction": "顯示隔離", + "redactionDetail": "前端只顯示 committed evidence 與政策摘要,{count} 類內容不得顯示。" + }, + "labels": { + "transcriptAllowed": "conversation_display={value}", + "redactionRequired": "redaction_required={value}" + }, + "decisions": { + "suppress_immediate_success": "成功抑制", + "create_action_required": "建立處置", + "escalate_immediate": "立即升級" + }, + "values": { + "locked": "已鎖定", + "review_required": "需複核" + } } } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 339218836..639b81f9a 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3424,6 +3424,46 @@ "high": "高", "critical": "關鍵" } + }, + "serviceHealthNotification": { + "title": "Service health 失敗限定通知合約", + "source": "{generated} · {current} → {next}", + "quietBadge": "成功降噪 {count}", + "redactionBadge": "前端隔離 {count} 類", + "templateTitle": "訊息欄位合約", + "boundaryTitle": "操作邊界", + "redactionTitle": "前端顯示紅線", + "boundaryDetail": "notification={notification} · live_probe={probe} · restart={restart} · runtime={runtime}", + "metrics": { + "rules": "規則", + "quiet": "成功降噪", + "actionRequired": "需處置", + "escalate": "失敗升級", + "allowedSend": "允許發送" + }, + "map": { + "failureOnly": "Failure-only", + "failureOnlyDetail": "只有 failed / blocked critical 才進 Telegram / AwoooP 升級合約。", + "quietSuccess": "成功不洗版", + "quietSuccessDetail": "verified 狀態只進治理頁與每日摘要。", + "blockedOperations": "執行入口", + "blockedOperationsDetail": "通知發送、live probe、restart、endpoint change 與 runtime execution 皆為 0。", + "redaction": "顯示隔離", + "redactionDetail": "前端只顯示 committed evidence 與政策摘要,{count} 類內容不得顯示。" + }, + "labels": { + "transcriptAllowed": "conversation_display={value}", + "redactionRequired": "redaction_required={value}" + }, + "decisions": { + "suppress_immediate_success": "成功抑制", + "create_action_required": "建立處置", + "escalate_immediate": "立即升級" + }, + "values": { + "locked": "已鎖定", + "review_required": "需複核" + } } } }, diff --git a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx index 5a23a5d0e..2bdb3de0a 100644 --- a/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx +++ b/apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx @@ -26,6 +26,7 @@ import { RefreshCw, Route, Server, + ShieldAlert, ShieldCheck, Target, } from 'lucide-react' @@ -44,6 +45,7 @@ import { type ObservabilityContractMatrixSnapshot, type OffsiteEscrowReadinessStatusSnapshot, type RuntimeSurfaceInventorySnapshot, + type ServiceHealthFailureNotificationPolicySnapshot, type ServiceHealthGapMatrixSnapshot, } from '@/lib/api-client' @@ -310,6 +312,7 @@ export function AutomationInventoryTab() { const [observabilityMatrix, setObservabilityMatrix] = useState(null) const [providerRouteMatrix, setProviderRouteMatrix] = useState(null) const [serviceHealthGapMatrix, setServiceHealthGapMatrix] = useState(null) + const [serviceHealthNotificationPolicy, setServiceHealthNotificationPolicy] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) @@ -327,6 +330,7 @@ export function AutomationInventoryTab() { apiClient.getObservabilityContractMatrix(), apiClient.getAiProviderRouteMatrix(), apiClient.getServiceHealthGapMatrix(), + apiClient.getServiceHealthFailureNotificationPolicy(), ] as const Promise.allSettled(requests) @@ -343,6 +347,7 @@ export function AutomationInventoryTab() { observabilityMatrixResult, providerRouteMatrixResult, serviceHealthGapMatrixResult, + serviceHealthNotificationPolicyResult, ] = results setSnapshot(inventoryResult.status === 'fulfilled' ? inventoryResult.value : null) @@ -356,6 +361,7 @@ export function AutomationInventoryTab() { setObservabilityMatrix(observabilityMatrixResult.status === 'fulfilled' ? observabilityMatrixResult.value : null) setProviderRouteMatrix(providerRouteMatrixResult.status === 'fulfilled' ? providerRouteMatrixResult.value : null) setServiceHealthGapMatrix(serviceHealthGapMatrixResult.status === 'fulfilled' ? serviceHealthGapMatrixResult.value : null) + setServiceHealthNotificationPolicy(serviceHealthNotificationPolicyResult.status === 'fulfilled' ? serviceHealthNotificationPolicyResult.value : null) setError([ inventoryResult, backlogResult, @@ -367,6 +373,7 @@ export function AutomationInventoryTab() { observabilityMatrixResult, providerRouteMatrixResult, serviceHealthGapMatrixResult, + serviceHealthNotificationPolicyResult, ].some(result => result.status === 'rejected')) }) .catch(() => setError(true)) @@ -545,7 +552,7 @@ export function AutomationInventoryTab() { ) } - if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !serviceHealthGapMatrix) { + if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) { return (
@@ -622,6 +629,15 @@ export function AutomationInventoryTab() { + serviceHealthGapMatrix.rollups.notification_send_allowed_count + serviceHealthGapMatrix.rollups.runtime_execution_allowed_count ) + const serviceHealthImmediateEscalations = serviceHealthNotificationPolicy.rollups.immediate_escalation_rule_ids.length + const serviceHealthSuppressedSuccess = serviceHealthNotificationPolicy.rollups.suppressed_success_rule_ids.length + const serviceHealthActionRequiredRules = serviceHealthNotificationPolicy.rollups.action_required_rule_ids.length + const serviceHealthNotificationAllowedCount = serviceHealthNotificationPolicy.rollups.notification_send_allowed_count + const serviceHealthRedactionCategoryCount = serviceHealthNotificationPolicy.display_redaction_contract.forbidden_frontend_content.length + const serviceHealthRedactionLocked = ( + serviceHealthNotificationPolicy.display_redaction_contract.conversation_transcript_display_allowed === false + && serviceHealthNotificationPolicy.display_redaction_contract.redaction_required === true + ) const backlogProgressPercent = backlog.progress_summary.overall_percent const explicitApprovalItemCount = backlog.item_approval_boundary_rollup.items_requiring_explicit_approval.length const taskBoundaryCount = snapshot.task_approval_boundary_rollup.total_tasks @@ -762,6 +778,14 @@ export function AutomationInventoryTab() { } } + const serviceHealthNotificationDecisionLabel = (value: string) => { + try { + return t(`serviceHealthNotification.decisions.${value}` as never) + } catch { + return value + } + } + return (
@@ -2116,6 +2140,133 @@ export function AutomationInventoryTab() {
+ +
+
+
+
+ + + {t('serviceHealthNotification.title')} + +
+ + {t('serviceHealthNotification.source', { + generated: formatDateTime(serviceHealthNotificationPolicy.generated_at), + current: serviceHealthNotificationPolicy.program_status.current_task_id, + next: serviceHealthNotificationPolicy.program_status.next_task_id, + })} + +
+
+ + +
+
+ +
+ } /> + } /> + } /> + } /> + } /> +
+ +
+ } + /> + } + /> + } + /> + } + /> +
+ +
+
+ {serviceHealthNotificationPolicy.policy_rules.map(rule => ( +
+
+ + {rule.rule_id} + + +
+
+ + +
+
+ {rule.message_contract} +
+
+ ))} +
+ +
+
+ {t('serviceHealthNotification.templateTitle')} +
+ {serviceHealthNotificationPolicy.message_template_contract.required_fields.map(field => ( + + ))} +
+
+ {serviceHealthNotificationPolicy.message_template_contract.success_message_policy} +
+
+
+ {t('serviceHealthNotification.redactionTitle')} +
+ {serviceHealthNotificationPolicy.display_redaction_contract.frontend_display_policy} +
+
+ + +
+
+
+ {t('serviceHealthNotification.boundaryTitle')} +
+ {t('serviceHealthNotification.boundaryDetail', { + notification: String(serviceHealthNotificationPolicy.operation_boundaries.notification_send_allowed), + probe: String(serviceHealthNotificationPolicy.operation_boundaries.live_probe_allowed), + restart: String(serviceHealthNotificationPolicy.operation_boundaries.service_restart_allowed), + runtime: String(serviceHealthNotificationPolicy.operation_boundaries.runtime_execution_allowed), + })} +
+
+ {serviceHealthNotificationPolicy.message_template_contract.forbidden_fields.map(field => ( + + ))} +
+
+
+
+
+
+
@@ -2241,8 +2392,15 @@ export function AutomationInventoryTab() { .automation-inventory-service-health-kpi-grid, .automation-inventory-service-health-map-grid, .automation-inventory-service-health-evidence-grid, + .automation-inventory-service-health-notification-kpi-grid, + .automation-inventory-service-health-notification-grid, + .automation-inventory-service-health-notification-rule-grid, .automation-inventory-service-health-grid, .automation-inventory-service-health-target-grid, + .automation-inventory-service-health-notification-kpi-grid, + .automation-inventory-service-health-notification-map-grid, + .automation-inventory-service-health-notification-grid, + .automation-inventory-service-health-notification-rule-grid, .automation-inventory-bottom-grid, .automation-inventory-task-grid { grid-template-columns: 1fr !important; diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index d8ae88293..0d2a8bbe7 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -287,6 +287,11 @@ export const apiClient = { return handleResponse(res) }, + async getServiceHealthFailureNotificationPolicy() { + const res = await fetch(`${API_BASE_URL}/agents/service-health-failure-notification-policy`) + return handleResponse(res) + }, + async getBackupDrTargetInventory() { const res = await fetch(`${API_BASE_URL}/agents/backup-dr-target-inventory`) return handleResponse(res) @@ -1187,6 +1192,63 @@ export interface ServiceHealthGapMatrixSnapshot { approval_boundaries: Record } +export interface ServiceHealthFailureNotificationPolicySnapshot { + schema_version: 'service_health_failure_notification_policy_v1' + generated_at: string + source_service_health_matrix_ref: string + source_refs: string[] + program_status: { + overall_completion_percent: number + current_priority: 'P0' | 'P1' | 'P2' | 'P3' + current_task_id: string + next_task_id: string + read_only_mode: true + } + rollups: { + total_rules: number + by_decision: Record + immediate_escalation_rule_ids: string[] + suppressed_success_rule_ids: string[] + action_required_rule_ids: string[] + notification_send_allowed_count: number + } + notification_channels: Array<{ + channel_id: string + purpose: string + immediate_allowed: boolean + success_immediate_allowed: boolean + requires_operator_action: boolean + }> + policy_rules: Array<{ + rule_id: string + event_kind: string + service_state: string + severity: string + decision: string + channels: string[] + owner_agent: string + requires_incident: boolean + requires_approval_record: boolean + message_contract: string + evidence_refs: string[] + }> + message_template_contract: { + required_fields: string[] + forbidden_fields: string[] + success_message_policy: string + failure_message_policy: string + } + display_redaction_contract: { + frontend_display_policy: string + allowed_frontend_fields: string[] + forbidden_frontend_content: string[] + conversation_transcript_display_allowed: false + redaction_required: true + } + operation_boundaries: Record + approval_boundaries: Record +} + export interface BackupDrTargetInventorySnapshot { schema_version: 'backup_dr_target_inventory_v1' generated_at: string diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 309886676..762203eba 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,70 @@ +## 2026-06-05|P1-007 服務健康失敗限定通知合約本地完成 + +**背景**:接續 P1-006 服務健康證據卡正式驗證後推進 P1-007。本段只建立 service health failure-only Telegram / AwoooP 通知合約、只讀 API 與治理頁顯示;不發送 Telegram / AwoooP、不做 live probe / external probe、不重啟 service / pod / host、不修改 endpoint / ConfigMap、不讀 Secret / Redis / DB payload、不觸發 workflow / deploy / reload / runtime execution。 + +**本輪完成**: +- 新增 `service_health_failure_notification_policy_v1` schema 與 `docs/evaluations/service_health_failure_notification_policy_2026-06-05.json`。 +- 新增 `GET /api/v1/agents/service-health-failure-notification-policy` 只讀 API 與 service guard,強制驗證成功通知壓制、failure-only escalation、message template 欄位、frontend redaction contract 與全部執行邊界。 +- 治理頁 `/zh-TW/governance?tab=automation-inventory` 新增「服務健康失敗限定通知合約」區塊,以 KPI、規則卡、訊息欄位合約與不可誤讀合約呈現。 +- 同步 automation backlog / inventory snapshot:current `P1-007`、next `P2-004`、backlog overall `92%`、P1 `100%`、done `23/25`、inventory tasks `33`。 + +**目前數字**: +- Notification policy rules:`7`。 +- 成功靜默規則:`2`。 +- Action-required 規則:`2`。 +- Failure escalation 規則:`3`。 +- Notification send / Telegram test / AwoooP event write / live probe / service restart / endpoint change / runtime execution allowed counts 全部 `0` 或 false。 + +**本地驗證**: +- JSON parse 通過:service health failure notification policy snapshot / schema、automation backlog / inventory snapshots、zh-TW / en messages。 +- 目標測試通過:service health failure notification policy service / API、automation inventory / backlog snapshot API 共 `12 passed`。 +- zh-TW / en i18n key 差異 `0`;web typecheck 通過。 +- 本段未執行本機 Next production build:同 worktree 仍有既有 `3121` dev server,為避免再次互踩 `.next` 產物,正式 build 與頁面可見性以 Gitea CD 與 production smoke 驗證。 + +**待補**: +- 推送 Gitea 後等待 deploy marker,補 production API readback 與 desktop / mobile browser smoke,再更新正式驗證紀錄。 + +**邊界**: +- P1-007 API/UI 可見只代表 committed notification policy 可讀,不代表任何 Telegram / AwoooP 訊息已送出,也不代表 runtime gate、S4.9 owner response gate 或 security acceptance gate 已提高。 +- 成功狀態不得即時通知;failed / blocked / high action-required 只是合約中的升級條件,本段仍未授權實際發送。 +- Live probe、external health probe、service / pod / host restart、rollout restart、endpoint / ConfigMap 修改、provider switch、paid API call、Secret payload read、通知發送、workflow / deploy / reload / runtime execution 全部仍未批准。 + +**下一步**: +1. 完成本輪 Gitea push、正式 deploy marker 追蹤與 production smoke。 +2. P2-004:配置優化主線需保持 read-only / approval gate,不得直接改 runtime。 + +## 2026-06-05|P1-007 Service health 失敗限定通知合約 + +**背景**:接續 P1-006 服務健康證據卡正式上線,依工作清單推進 `P1-007`。本段只建立 committed failure-only 通知合約、只讀 API、治理頁顯示與 redaction 合約;不發 Telegram / AwoooP 通知、不寫 AwoooP event、不做 live probe / external probe、不重啟 service / pod / host、不修改 endpoint / ConfigMap、不讀 Secret / Redis / DB payload、不觸發 workflow / deploy / reload / runtime execution。 + +**本輪完成**: +- 新增 `service_health_failure_notification_policy_v1` schema 與 `docs/evaluations/service_health_failure_notification_policy_2026-06-05.json`。 +- 新增 `GET /api/v1/agents/service-health-failure-notification-policy` 與 service guard,強制驗證 read-only mode、failure-only escalation、成功降噪、`notification_send_allowed_count=0`、訊息欄位 redaction 與前端顯示紅線。 +- 治理頁 `/zh-TW/governance?tab=automation-inventory` 新增「Service health 失敗限定通知合約」區塊,只顯示 committed policy、rule summary、evidence ref、下一步、禁止事項與 redaction 狀態;不得顯示工作視窗對話內容、Codex/user 訊息逐字稿、prompt、session id 或 browser context。 +- 同步 automation backlog / inventory snapshot:current `P1-007`、next `P2-004`、backlog overall `92%`、P1 `100%`、done `23/25`、inventory tasks `33`。 + +**目前數字**: +- 通知規則:`7`。 +- 成功降噪規則:`2`。 +- Action-required 規則:`2`。 +- 立即升級規則:`3`。 +- `notification_send_allowed_count=0`。 +- `conversation_transcript_display_allowed=false`、`redaction_required=true`。 + +**本地驗證(目前進度)**: +- P1-007 後端目標測試通過:`python3 -m pytest apps/api/tests/test_service_health_failure_notification_policy.py apps/api/tests/test_service_health_failure_notification_policy_api.py` → `10 passed`。 +- JSON parse 通過:`service_health_failure_notification_policy_2026-06-05.json`、automation backlog / inventory snapshots。 +- Snapshot / API / frontend 文字掃描未出現工作視窗 transcript 標記。 + +**邊界**: +- Telegram / AwoooP 通知發送、測試通知、AwoooP event 寫入、live probe、external probe、service / pod / host restart、endpoint / ConfigMap 修改、Secret payload read、workflow / deploy / reload / runtime execution 全部仍未批准。 +- 前端 redaction 合約只允許顯示 committed evidence 與政策摘要;任何工作視窗逐字稿、prompt、session 或 browser context 顯示需求皆視為阻擋。 + +**下一步**: +1. 完成本地 typecheck / build / browser smoke。 +2. 提交、推 Gitea main 並做正式環境 API / UI 驗證。 +3. 驗證完成後進 `P2-004` 依賴 / 供應鏈漂移監控。 + ## 2026-06-05|P1-006 服務健康證據卡正式上線 **背景**:接續 P1-005 服務健康缺口與過期端點正式驗證,依工作清單推進 `P1-006`。本段只把 committed service health evidence refs 顯示成更細緻的 UI 證據卡;不做 live probe / external probe、不重啟 service / pod / host、不修改 endpoint / ConfigMap、不讀 Secret / Redis / DB payload、不發 Telegram / AwoooP 通知、不觸發 workflow / deploy / reload / runtime execution。 diff --git a/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md b/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md index c7e753c23..8bb06a79a 100644 --- a/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md +++ b/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md @@ -10,10 +10,10 @@ |---|---:|---|---| | Agent 市場治理 | 72% | 進行中 | `agent_market_governance_snapshot_v1`、API、UI 分頁、每週觀察流程 | | Nemotron 實際整合應用 | 30% | 完整回放前仍被關卡擋下 | `blocked_needs_evidence`,下一關是 `refresh_source_evidence_then_5_record_smoke_only` | -| 工具 / 服務 / 套件 AI 自動化 | 88% | P0 已完成,P1 套件 / 供應鏈主線已完成;備份 / DR 主線已完成到異地 / escrow 準備度顯示;任務批准邊界、進度彙總、P1-001 執行面只讀矩陣、P1-002 Gitea 工作流程 / runner 健康合約、P1-003 監控合約 / 降噪矩陣、P1-004 AI 供應商路由矩陣、P1-005 服務健康缺口矩陣與 P1-006 service health 證據卡已完成,下一主線是 P1-007 失敗限定通知合約 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Backup / DR 證據 UI、復原演練批准包模板、異地 / escrow 準備度狀態、任務批准邊界、確定性進度彙總、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板、runtime_surface_inventory_v1 schema / snapshot / API / UI、gitea_workflow_runner_health_v1 schema / snapshot / API / UI、observability_contract_matrix_v1 schema / snapshot / API / UI、ai_provider_route_matrix_v1 schema / snapshot / API / UI、service_health_gap_matrix_v1 schema / snapshot / API / UI、service health evidence cards UI 已完成 | +| 工具 / 服務 / 套件 AI 自動化 | 92% | P0 已完成;P1 服務 / runtime / 監控 / provider / service health / 備份 / DR / 套件與供應鏈只讀基線已完成;P1-007 失敗限定通知合約與前端 redaction 合約已完成;下一主線是 P2-004 依賴 / 供應鏈漂移監控 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Backup / DR 證據 UI、復原演練批准包模板、異地 / escrow 準備度狀態、任務批准邊界、確定性進度彙總、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板、runtime_surface_inventory_v1 schema / snapshot / API / UI、gitea_workflow_runner_health_v1 schema / snapshot / API / UI、observability_contract_matrix_v1 schema / snapshot / API / UI、ai_provider_route_matrix_v1 schema / snapshot / API / UI、service_health_gap_matrix_v1 schema / snapshot / API / UI、service health evidence cards UI、service_health_failure_notification_policy_v1 schema / snapshot / API / UI 已完成 | | 本工作清單與分析報告 | 100% | 已完成 | 本 MD 文件 | -AI Agent 自動化工作包目前完成度:**88%**。本工作清單文件本身完成度:**100%**。 +AI Agent 自動化工作包目前完成度:**92%**。本工作清單文件本身完成度:**100%**。 完成度計算模型: @@ -872,7 +872,7 @@ UI: | P1-004 | 完成 | 100 | OpenClaw | 盤點 AI Router / Ollama / Nemotron / Gemini provider 路徑 | `ai_provider_route_matrix_v1` / `GET /api/v1/agents/ai-provider-route-matrix` / AI 供應商路由矩陣 UI | 只讀;不切 provider、不呼叫付費 API、不改 fallback order、不進 shadow/canary | | P1-005 | 完成 | 100 | OpenClaw | 偵測服務健康缺口與過期端點 | `service_health_gap_matrix_v1` / `GET /api/v1/agents/service-health-gap-matrix` / 服務健康缺口與過期端點 UI | 只讀;不重啟、不改 endpoint、不做 active probe、不發通知 | | P1-006 | 完成 | 100 | Hermes | 在 UI 顯示 service health 證據卡 | 治理頁 service health evidence cards:主要 evidence ref、狀態、新鮮度、風險與下一步 | 只讀;不 live probe、不重啟、不改 endpoint、不發通知 | -| P1-007 | 待辦 | 0 | OpenClaw | 建立 service health 失敗限定 Telegram / AwoooP 對應 | 通知合約 | 不發成功洗版 | +| P1-007 | 完成 | 100 | OpenClaw | 建立 service health 失敗限定 Telegram / AwoooP 對應 | `service_health_failure_notification_policy_v1` / `GET /api/v1/agents/service-health-failure-notification-policy` / 治理頁 failure-only 通知合約與 redaction 狀態 | 成功不洗版;不發通知;前端不顯示工作視窗對話內容、prompt、session 或 browser context | ### P1 - 備份與 DR 自動化 @@ -1102,22 +1102,36 @@ UI: 本次同步: ```text -進度:88%。 +進度:92%。 目前優先級:P1。 -目前任務:P1-006 在 UI 顯示 service health 證據卡。 +目前任務:P1-007 建立 service health 失敗限定 Telegram / AwoooP 對應。 狀態變更:待辦 -> 完成。 -證據:service_health_gap_matrix_v1 snapshot / API;治理頁 service health evidence cards;automation backlog 88%;inventory tasks 32。 -目前數字:service health targets 10;證據卡 10;需處置 targets 5;stale endpoints 3;health gaps 5;service restart / endpoint change / active probe / notification send / runtime execution allowed counts 全部 0;backlog done 22/25;overall 88%;P1 96%;WS3 監控自動化 100%。 +證據:service_health_failure_notification_policy_v1 schema / snapshot;GET /api/v1/agents/service-health-failure-notification-policy;治理頁失敗限定通知合約;automation backlog 92%;inventory tasks 33。 +目前數字:notification policy rules 7;成功靜默 2;action-required 2;failure escalation 3;notification send / Telegram test / AwoooP event write / live probe / service restart / endpoint change / runtime execution allowed counts 全部 0 或 false;backlog done 23/25;overall 92%;P1 100%;WS7 安全執行關卡 100%。 驗證:JSON parse 通過;service health gap matrix service / API、automation inventory / backlog snapshot service / API、AI provider route matrix service / API 目標測試 `35 passed`;Python py_compile 通過;zh-TW / en i18n key 差異 `0`;web typecheck 通過;乾淨重建 `.next` 後 Next production build 通過,治理頁 First Load JS `387 kB`,standalone server.js 正常產生;source-control-owner-response guard、security-mirror-progress guard、`git diff --check` 通過。本地 API readback 回 backlog current `P1-006`、next `P1-007`、done `22/25`;inventory tasks `32` 且存在 `service_health_evidence_cards_ui`;service health matrix targets `10`、需處置 `5`、stale endpoints `3`、health gaps `5`。本地 desktop `1440x1000` 與 mobile `390x844` browser smoke 通過,11 個 agents API 皆 `200`,主要證據卡數 `10`,`服務健康證據卡`、`主要證據`、`下一步`、`P1-006`、`P1-007`、`88%`、`Ollama 三層健康合約`、`scripts/health_check_session.sh`、`apps/api/src/api/v1/health.py`、`允許入口` 可見,blocking console error `0`、blocking HTTP failed response `0`、`horizontalOverflow=0`、overflowing elements `0`、危險互動入口 `0`;本地 mini API 的 dashboard SSE close 為 known local noise。 -正式驗證:code commit `7d62cad6`;deploy marker `f42afd9b chore(cd): deploy 7d62cad [skip ci]`;Gitea code-review `#2611` 成功、CD `#2610` 成功。Production health `healthy/prod/mock_mode=false`;backlog API 回 current `P1-006`、next `P1-007`、overall `88%`、done `22/25`;inventory API 回 current `P1-006`、next `P1-007`、tasks `32`、read-only allowed `29`;service health API 回 `service_health_gap_matrix_v1`、current `P1-005`、next `P1-006`、targets `10`、需處置 `5`、stale endpoints `3`、health gaps `5`、service restart / endpoint change / active probe / notification send / runtime execution allowed counts 全部 `0`。Production desktop `1440x1000` 與 mobile `390x844` smoke 通過,`服務健康證據卡`、`主要證據`、`下一步`、`服務健康缺口與過期端點`、`P1-006`、`P1-007`、`88%`、`Production API Health`、`Ollama 三層健康合約`、`不可誤讀合約` 可見;`horizontalOverflow=0`、overflowing elements `0`、內容危險操作入口 `0`、錯誤文字 `0`。補充 recheck:docs-only `e95f5e60` 推上 Gitea 後,production desktop / mobile 再驗皆通過,主要證據卡數 `10`、console error `0`、HTTP failed response `0`、`horizontalOverflow=0`、overflowing elements `0`、危險互動入口 `0`、錯誤文字 `0`。 +正式驗證:待 code commit 推送 Gitea、取得 deploy marker 後補 production API readback 與 desktop / mobile smoke。補充 recheck:docs-only `e95f5e60` 推上 Gitea 後,production desktop / mobile 再驗皆通過,主要證據卡數 `10`、console error `0`、HTTP failed response `0`、`horizontalOverflow=0`、overflowing elements `0`、危險互動入口 `0`、錯誤文字 `0`。 阻擋:live probe、external health probe、service / pod / host restart、rollout restart、endpoint / ConfigMap 修改、provider switch、paid API call、Secret payload read、通知發送、workflow/deploy/reload/runtime execution 仍全部禁止。 -下一步:P1-007 建立 service health 失敗限定 Telegram / AwoooP 對應。 +下一步:P2-004 配置優化主線需保持 read-only / approval gate。 +``` + +本次同步: + +```text +進度:92%。 +目前優先級:P1。 +目前任務:P1-007 建立 service health 失敗限定 Telegram / AwoooP 對應。 +狀態變更:待辦 -> 完成。 +證據:service_health_failure_notification_policy_v1 schema / snapshot / API;治理頁 failure-only 通知合約與 redaction 狀態;automation backlog 92%;inventory tasks 33。 +目前數字:通知規則 7;成功降噪 2;需處置 2;立即升級 3;notification_send_allowed_count 0;conversation_transcript_display_allowed=false;redaction_required=true;backlog done 23/25;P1 100%。 +驗證:P1-007 後端目標測試 `10 passed`;JSON parse 通過;snapshot / API / frontend 文字掃描未出現工作視窗 transcript 標記;前端只顯示 committed policy、rule summary、evidence ref、下一步、禁止事項與 redaction 狀態。 +阻擋:Telegram / AwoooP 通知發送、測試通知、AwoooP event 寫入、live probe、external probe、service restart、endpoint / ConfigMap 修改、Secret payload read、workflow/deploy/reload/runtime execution、工作視窗對話內容 / prompt / session / browser context 前端顯示仍全部禁止。 +下一步:P2-004 依賴 / 供應鏈漂移監控;P3-001 仍需證據與費用 / shadow / canary 關卡。 ``` ## 13. 立即執行順序 -1. P1-007:建立 service health 失敗限定 Telegram / AwoooP 對應。 -2. P2 / P3 必須等 P1 服務、監控、provider route 與 service health evidence cards 可見且關卡穩定後再做。 +1. P2-004:依賴 / 供應鏈漂移監控,保持只讀觀察與批准包邊界。 +2. P3-001:外部 Agent / SDK / API 相關能力仍需證據、費用批准與 shadow / canary 關卡。 ## 14. 目前風險 diff --git a/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json b/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json index bb0906810..8f1edf9c7 100644 --- a/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json +++ b/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json @@ -1,12 +1,12 @@ { "schema_version": "ai_agent_automation_backlog_v1", - "generated_at": "2026-06-05T14:55:00+08:00", + "generated_at": "2026-06-05T16:20:00+08:00", "source_inventory_snapshot_ref": "docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json", "program_status": { - "overall_completion_percent": 88, + "overall_completion_percent": 92, "current_priority": "P1", - "current_task_id": "P1-006", - "next_task_id": "P1-007", + "current_task_id": "P1-007", + "next_task_id": "P2-004", "read_only_mode": true }, "rollups": { @@ -17,19 +17,19 @@ "P3": 1 }, "by_status": { - "done": 22, - "planned": 3 + "done": 23, + "planned": 2 }, "by_gate_status": { - "blocked_by_evidence": 1, - "cost_approval_required": 1, + "read_only_allowed": 22, "production_change_blocked": 1, - "read_only_allowed": 22 + "cost_approval_required": 1, + "blocked_by_evidence": 1 }, "by_owner_agent": { "hermes": 13, - "nemotron": 1, - "openclaw": 11 + "openclaw": 11, + "nemotron": 1 } }, "backlog_items": [ @@ -534,32 +534,43 @@ { "item_id": "AUTO-P1-007", "priority": "P1", - "status": "planned", + "status": "done", "workstream_id": "WS7", - "source_asset_id": "telegram_chain", + "source_asset_id": "service_health_failure_notification_policy", "source_signal_kind": "approval_boundary", "title": "建立 service health failure-only Telegram / AwoooP 對應", "owner_agent": "openclaw", - "recommended_action": "定義 action-required 與 failure-only 通知 contract,不發成功洗版訊息。", - "action_class": "prepare_approval_package", + "recommended_action": "已定義 service health failure-only Telegram / AwoooP 通知合約;成功不即時通知,failed / blocked / high action-required 才升級,但本段不發送通知。", + "action_class": "notification_policy", "gate_status": "read_only_allowed", "risk_level": "critical", "evidence_refs": [ - "docs/HARD_RULES.md", - "apps/api/tests/test_telegram_message_templates.py" + "docs/schemas/service_health_failure_notification_policy_v1.schema.json", + "docs/evaluations/service_health_failure_notification_policy_2026-06-05.json", + "GET /api/v1/agents/service-health-failure-notification-policy", + "/zh-TW/governance?tab=automation-inventory", + "apps/api/src/services/service_health_failure_notification_policy.py", + "apps/api/tests/test_service_health_failure_notification_policy.py", + "apps/api/tests/test_service_health_failure_notification_policy_api.py", + "apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx" ], "acceptance_criteria": [ "不得發送測試通知到正式群組", "成功不通知的預設政策被保留", - "action-required 必須可追蹤 incident / approval / evidence" + "action-required 必須可追蹤 incident / approval / evidence", + "message template 必須包含 stage、next action、blocked reason、auto/manual、target、severity、evidence ref", + "不得新增 live probe / restart / endpoint change / notification send 操作入口", + "前端只允許顯示 committed policy、rule summary、evidence ref、下一步與禁止事項", + "前端不得顯示工作視窗對話內容、Codex/user 訊息逐字稿、prompt、session id 或 browser context", + "API snapshot 必須維持 conversation_transcript_display_allowed=false 與 redaction_required=true" ], - "next_review": "P1-007", + "next_review": "P2-004", "approval_boundary": { "mode": "read_only_allowed", - "display_summary": "只允許只讀盤點、顯示與批准包準備;不得直接執行寫入、部署、通知或外部呼叫。", + "display_summary": "只允許定義 service health failure-only 通知合約與 UI 顯示;不得直接發送 Telegram / AwoooP、live probe、重啟、改 endpoint 或 runtime execution。", "allowed_actions": [ "讀取 committed snapshot", - "整理只讀證據", + "整理 failure-only 通知政策", "顯示治理 UI" ], "blocked_actions": [ @@ -567,12 +578,25 @@ "runtime_execution", "destructive_operation", "secret_plaintext_collection", - "unapproved_deploy", - "unapproved_external_call" + "notification_send", + "telegram_test_message", + "awooop_event_write", + "live_probe", + "service_restart", + "endpoint_change", + "work_window_transcript_display", + "prompt_display", + "session_context_display" ], "requires_operator_approval_for": [ - "任何非只讀操作", - "任何部署、排程、通知或外部呼叫變更" + "Telegram / AwoooP 通知發送", + "測試通知", + "AwoooP event 寫入", + "live probe", + "服務重啟", + "endpoint / ConfigMap 修改", + "runtime execution", + "任何工作視窗逐字稿、prompt、session 或 browser context 顯示需求" ] } }, @@ -1254,10 +1278,10 @@ "item_approval_boundary_rollup": { "total_items": 25, "by_mode": { - "blocked_by_evidence": 1, - "cost_approval_required": 1, + "read_only_allowed": 22, "production_change_blocked": 1, - "read_only_allowed": 22 + "cost_approval_required": 1, + "blocked_by_evidence": 1 }, "items_requiring_explicit_approval": [ "AUTO-P1-004", @@ -1290,19 +1314,43 @@ "AUTO-P1-306", "AUTO-P2-004", "AUTO-P3-001" + ], + "read_only_item_ids": [ + "AUTO-P1-303", + "AUTO-P1-304", + "AUTO-P1-305", + "AUTO-P1-306", + "AUTO-P1-001", + "AUTO-P1-002", + "AUTO-P1-003", + "AUTO-P1-005", + "AUTO-P1-006", + "AUTO-P1-007", + "AUTO-P1-101", + "AUTO-P1-102", + "AUTO-P1-103", + "AUTO-P1-104", + "AUTO-P1-105", + "AUTO-P1-106", + "AUTO-P1-201", + "AUTO-P1-202", + "AUTO-P1-203", + "AUTO-P1-204", + "AUTO-P1-205", + "AUTO-P1-206" ] }, "progress_summary": { - "overall_percent": 88, - "done_items": 22, - "planned_items": 3, + "overall_percent": 92, + "done_items": 23, + "planned_items": 2, "total_items": 25, "formula": "round(done_items / total_items * 100),只有 status=done 計入完成;planned/in_progress/blocked/deferred/rejected 不計入。", "by_priority": [ { "priority": "P1", - "completion_percent": 96, - "done_items": 22, + "completion_percent": 100, + "done_items": 23, "total_items": 23 }, { @@ -1346,10 +1394,10 @@ { "workstream_id": "WS7", "display_name": "安全執行關卡", - "completion_percent": 0, - "done_items": 0, + "completion_percent": 100, + "done_items": 1, "total_items": 1, - "next_task_id": "P1-007" + "next_task_id": "complete" }, { "workstream_id": "WS4", diff --git a/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json b/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json index 9727b5420..824e96906 100644 --- a/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json +++ b/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json @@ -1,11 +1,11 @@ { "schema_version": "ai_agent_automation_inventory_snapshot_v1", - "generated_at": "2026-06-05T14:55:00+08:00", + "generated_at": "2026-06-05T16:20:00+08:00", "program_status": { "overall_completion_percent": 100, "current_priority": "P1", - "current_task_id": "P1-006", - "next_task_id": "P1-007", + "current_task_id": "P1-007", + "next_task_id": "P2-004", "read_only_mode": true }, "status_taxonomy": { @@ -337,15 +337,15 @@ "domain_id": "security", "display_name": "Telegram 告警與批准鏈路", "asset_type": "external_service", - "status": "planned", - "gate_status": "approval_required", + "status": "done", + "gate_status": "read_only_allowed", "owner_agent": "openclaw", "risk_level": "critical", "evidence_refs": [ - "docs/HARD_RULES.md", - "apps/api/tests/test_telegram_message_templates.py" + "docs/evaluations/service_health_failure_notification_policy_2026-06-05.json", + "GET /api/v1/agents/service-health-failure-notification-policy" ], - "next_action": "P1-007 對齊 failure-only 通知與 action-required 映射。" + "next_action": "P1-007 已完成 failure-only 通知合約;不得直接發送通知。" }, { "asset_id": "backup_gitea", @@ -481,6 +481,21 @@ "automation_scope": "read_only_visualization", "recommended_next_action": "以 production desktop / mobile smoke 驗證證據卡可見與無水平溢出。", "gate_status": "read_only_allowed" + }, + { + "asset_id": "service_health_failure_notification_policy", + "name": "Service health failure-only notification policy", + "kind": "notification_policy", + "status": "done", + "risk_level": "critical", + "owner_agent": "openclaw", + "source_refs": [ + "docs/evaluations/service_health_failure_notification_policy_2026-06-05.json", + "docs/schemas/service_health_failure_notification_policy_v1.schema.json" + ], + "automation_scope": "read_only_notification_policy", + "recommended_next_action": "Production API / UI smoke 後進 P2-004;不得發送測試通知或顯示工作視窗對話內容。", + "gate_status": "read_only_allowed" } ], "workstreams": [ @@ -1130,6 +1145,50 @@ ] } }, + { + "task_id": "P1-007", + "priority": "P1", + "status": "done", + "owner_agent": "OpenClaw", + "title": "建立 service health 失敗限定 Telegram / AwoooP 對應", + "output": "docs/evaluations/service_health_failure_notification_policy_2026-06-05.json + GET /api/v1/agents/service-health-failure-notification-policy + 前端 redaction 合約 UI", + "gate_status": "read_only_allowed", + "next_action": "P2-004 依賴 / 供應鏈漂移監控;P1-007 不批准通知發送或工作視窗內容顯示。", + "approval_boundary": { + "mode": "read_only_allowed", + "display_summary": "只允許整理 failure-only notification policy;不得發送 Telegram / AwoooP、live probe、重啟、改 endpoint 或 runtime execution。", + "allowed_actions": [ + "讀取 committed snapshot", + "整理通知合約", + "顯示治理 UI" + ], + "blocked_actions": [ + "production_write", + "runtime_execution", + "destructive_operation", + "secret_plaintext_collection", + "notification_send", + "telegram_test_message", + "awooop_event_write", + "live_probe", + "service_restart", + "endpoint_change", + "work_window_transcript_display", + "prompt_display", + "session_context_display" + ], + "requires_operator_approval_for": [ + "Telegram / AwoooP 通知發送", + "測試通知", + "AwoooP event 寫入", + "live probe", + "服務重啟", + "endpoint / ConfigMap 修改", + "runtime execution", + "任何工作視窗逐字稿、prompt、session 或 browser context 顯示需求" + ] + } + }, { "task_id": "P1-101", "priority": "P1", @@ -1941,6 +2000,30 @@ "source": "apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx", "status": "committed", "summary": "治理頁顯示 service health evidence cards;只讀 committed evidence refs,不提供執行入口。" + }, + { + "evidence_id": "service_health_failure_notification_policy_schema", + "kind": "schema", + "ref": "docs/schemas/service_health_failure_notification_policy_v1.schema.json", + "result": "Service health failure-only notification policy schema 已建立,明確禁止通知發送、live probe、重啟、endpoint 修改、workflow / runtime execution 與 secret 明文。" + }, + { + "evidence_id": "service_health_failure_notification_policy_snapshot", + "kind": "snapshot", + "ref": "docs/evaluations/service_health_failure_notification_policy_2026-06-05.json", + "result": "P1-007 committed failure-only 通知合約已建立;成功不即時通知,failed / blocked / high action-required 才升級;redaction contract 禁止前端顯示工作視窗對話內容。" + }, + { + "evidence_id": "service_health_failure_notification_policy_api", + "kind": "read_only_api", + "ref": "GET /api/v1/agents/service-health-failure-notification-policy", + "result": "只讀 API 回傳 service_health_failure_notification_policy_v1;不送 Telegram / AwoooP、不做 live probe、不重啟、不改 endpoint、不回傳工作視窗對話內容或 prompt。" + }, + { + "evidence_id": "service_health_failure_notification_policy_tests", + "kind": "test", + "ref": "python3 -m pytest apps/api/tests/test_service_health_failure_notification_policy.py apps/api/tests/test_service_health_failure_notification_policy_api.py", + "result": "P1-007 目標測試 10 passed;涵蓋 read-only boundaries、failure-only escalation、notification_send_allowed_count=0、redaction contract 與 transcript marker 阻擋。" } ], "approval_boundaries": { @@ -1951,12 +2034,12 @@ "destructive_operation_allowed": false }, "task_approval_boundary_rollup": { - "total_tasks": 32, + "total_tasks": 33, "by_mode": { + "ready_for_operator_review": 1, + "read_only_allowed": 30, "approval_required": 1, - "production_change_blocked": 1, - "read_only_allowed": 29, - "ready_for_operator_review": 1 + "production_change_blocked": 1 }, "tasks_requiring_explicit_approval": [ "P0-001", @@ -1978,6 +2061,7 @@ "P1-004", "P1-005", "P1-006", + "P1-007", "P1-101", "P1-102", "P1-103", @@ -1996,6 +2080,38 @@ "P1-304", "P1-305", "P1-306" + ], + "read_only_task_ids": [ + "P0-002", + "P0-003", + "P0-005", + "P0-006", + "P0-007", + "P0-008", + "P1-001", + "P1-002", + "P1-003", + "P1-301", + "P1-302", + "P1-303", + "P1-304", + "P1-305", + "P1-306", + "P1-006", + "P1-007", + "P1-101", + "P1-102", + "P1-103", + "P1-104", + "P1-105", + "P1-106", + "P1-201", + "P1-202", + "P1-203", + "P1-204", + "P1-205", + "P1-206", + "P1-005" ] } } diff --git a/docs/evaluations/service_health_failure_notification_policy_2026-06-05.json b/docs/evaluations/service_health_failure_notification_policy_2026-06-05.json new file mode 100644 index 000000000..86707e520 --- /dev/null +++ b/docs/evaluations/service_health_failure_notification_policy_2026-06-05.json @@ -0,0 +1,303 @@ +{ + "schema_version": "service_health_failure_notification_policy_v1", + "generated_at": "2026-06-05T16:20:00+08:00", + "source_service_health_matrix_ref": "docs/evaluations/service_health_gap_matrix_2026-06-05.json", + "source_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json", + "docs/schemas/service_health_failure_notification_policy_v1.schema.json", + "docs/HARD_RULES.md", + "docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md", + "apps/api/tests/test_telegram_message_templates.py" + ], + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P1", + "current_task_id": "P1-007", + "next_task_id": "P2-004", + "read_only_mode": true + }, + "rollups": { + "total_rules": 7, + "by_decision": { + "suppress_immediate_success": 2, + "create_action_required": 2, + "escalate_immediate": 3 + }, + "immediate_escalation_rule_ids": [ + "production_api_health_failed", + "ollama_provider_health_action_required", + "security_scanner_health_blocked" + ], + "suppressed_success_rule_ids": [ + "service_health_verified", + "endpoint_freshness_verified" + ], + "action_required_rule_ids": [ + "stale_endpoint_detected", + "health_gap_requires_owner_review" + ], + "notification_send_allowed_count": 0 + }, + "notification_channels": [ + { + "channel_id": "awooop_operator_event", + "purpose": "承載需人工處理、incident 或 approval evidence 的 operator-visible event。", + "immediate_allowed": true, + "success_immediate_allowed": false, + "requires_operator_action": true + }, + { + "channel_id": "telegram_ops", + "purpose": "只承載 failure、blocked、critical action-required 即時升級;成功與一般 verified 狀態不得即時送出。", + "immediate_allowed": true, + "success_immediate_allowed": false, + "requires_operator_action": true + }, + { + "channel_id": "governance_readonly_ui", + "purpose": "顯示 committed policy、evidence refs、next action 與禁止事項;不發送通知。", + "immediate_allowed": false, + "success_immediate_allowed": false, + "requires_operator_action": false + }, + { + "channel_id": "daily_status_summary", + "purpose": "成功與非緊急狀態只進每日摘要,避免 Telegram / AwoooP 成功洗版。", + "immediate_allowed": false, + "success_immediate_allowed": false, + "requires_operator_action": false + } + ], + "policy_rules": [ + { + "rule_id": "service_health_verified", + "event_kind": "service_health_target_verified", + "service_state": "verified", + "severity": "info", + "decision": "suppress_immediate_success", + "channels": [ + "governance_readonly_ui", + "daily_status_summary" + ], + "owner_agent": "hermes", + "requires_incident": false, + "requires_approval_record": false, + "message_contract": "健康目標已驗證時只更新治理頁與每日摘要;不得送 Telegram / AwoooP 即時成功訊息。", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json" + ] + }, + { + "rule_id": "endpoint_freshness_verified", + "event_kind": "endpoint_freshness_verified", + "service_state": "verified", + "severity": "info", + "decision": "suppress_immediate_success", + "channels": [ + "governance_readonly_ui", + "daily_status_summary" + ], + "owner_agent": "hermes", + "requires_incident": false, + "requires_approval_record": false, + "message_contract": "端點 freshness 已對齊時只更新 committed evidence 與每日摘要;不得即時洗版。", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json" + ] + }, + { + "rule_id": "stale_endpoint_detected", + "event_kind": "service_health_stale_endpoint", + "service_state": "action_required", + "severity": "warning", + "decision": "create_action_required", + "channels": [ + "awooop_operator_event", + "governance_readonly_ui", + "daily_status_summary" + ], + "owner_agent": "openclaw", + "requires_incident": false, + "requires_approval_record": false, + "message_contract": "過期端點先建立 action-required,必須帶 stale ref、current truth、evidence ref 與下一步;不得直接改 endpoint。", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json" + ] + }, + { + "rule_id": "health_gap_requires_owner_review", + "event_kind": "service_health_gap_requires_owner_review", + "service_state": "action_required", + "severity": "high", + "decision": "create_action_required", + "channels": [ + "awooop_operator_event", + "governance_readonly_ui", + "daily_status_summary" + ], + "owner_agent": "openclaw", + "requires_incident": false, + "requires_approval_record": true, + "message_contract": "健康缺口需 owner review 時建立 action-required 與 approval evidence;不得 live probe 或重啟。", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json" + ] + }, + { + "rule_id": "production_api_health_failed", + "event_kind": "production_api_health_failed", + "service_state": "failed", + "severity": "critical", + "decision": "escalate_immediate", + "channels": [ + "awooop_operator_event", + "telegram_ops", + "governance_readonly_ui" + ], + "owner_agent": "openclaw", + "requires_incident": true, + "requires_approval_record": false, + "message_contract": "正式 API health 失敗才可升級;訊息必須包含 target、last committed evidence、stage、next action 與 blocked reason,不得夾帶 secret。", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json" + ] + }, + { + "rule_id": "ollama_provider_health_action_required", + "event_kind": "ai_provider_health_action_required", + "service_state": "failed", + "severity": "critical", + "decision": "escalate_immediate", + "channels": [ + "awooop_operator_event", + "telegram_ops", + "governance_readonly_ui" + ], + "owner_agent": "openclaw", + "requires_incident": true, + "requires_approval_record": true, + "message_contract": "AI provider 健康關鍵失敗才升級;必須顯示 provider lane、fallback impact、evidence ref 與人工批准需求,不得切 provider。", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json" + ] + }, + { + "rule_id": "security_scanner_health_blocked", + "event_kind": "security_scanner_health_blocked", + "service_state": "blocked", + "severity": "critical", + "decision": "escalate_immediate", + "channels": [ + "awooop_operator_event", + "telegram_ops", + "governance_readonly_ui" + ], + "owner_agent": "openclaw", + "requires_incident": true, + "requires_approval_record": true, + "message_contract": "安全掃描健康阻擋才升級;必須保留 Kali / scanner 只讀邊界,不得觸發 active scan 或 /execute。", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json" + ] + } + ], + "message_template_contract": { + "required_fields": [ + "stage", + "next_action", + "blocked_reason", + "auto_or_manual", + "target_id", + "severity", + "evidence_ref" + ], + "forbidden_fields": [ + "secret_value", + "token", + "authorization_header", + "private_key", + "database_password", + "work_window_transcript", + "codex_user_message", + "prompt_text", + "chain_of_thought", + "session_id", + "browser_context" + ], + "success_message_policy": "成功與 verified 狀態不得即時送 Telegram / AwoooP,只能進治理頁與每日摘要。", + "failure_message_policy": "critical failed / blocked 或 high action-required 才可升級,但本 snapshot 不授權實際發送。" + }, + "agent_roles": [ + { + "agent_id": "openclaw", + "role": "判斷 service health failure / action-required 是否需要 incident、approval 與 operator event。", + "allowed_actions": [ + "只讀仲裁嚴重度", + "整理 message contract", + "拒絕成功即時洗版" + ], + "blocked_actions": [ + "送出 Telegram / AwoooP 訊息", + "live probe", + "重啟或修改 endpoint" + ] + }, + { + "agent_id": "hermes", + "role": "整理每日摘要、治理頁文字與 evidence refs。", + "allowed_actions": [ + "只讀整理證據", + "標示摘要欄位", + "更新 committed policy" + ], + "blocked_actions": [ + "發送通知", + "讀取 secret payload", + "改排程或 workflow" + ] + } + ], + "operation_boundaries": { + "read_only_policy_allowed": true, + "notification_send_allowed": false, + "telegram_test_message_allowed": false, + "awooop_event_write_allowed": false, + "live_probe_allowed": false, + "external_health_probe_allowed": false, + "service_restart_allowed": false, + "endpoint_change_allowed": false, + "workflow_trigger_allowed": false, + "runtime_execution_allowed": false, + "secret_plaintext_allowed": false + }, + "approval_boundaries": { + "notification_send_approved": false, + "telegram_test_message_approved": false, + "awooop_event_write_approved": false, + "live_probe_approved": false, + "service_restart_approved": false, + "endpoint_change_approved": false, + "runtime_execution_approved": false + }, + "display_redaction_contract": { + "frontend_display_policy": "前端只顯示 committed policy evidence、規則摘要與 sanitized message contract;不得顯示工作視窗對話、prompt、session 或 browser context。", + "allowed_frontend_fields": [ + "committed evidence ref", + "policy rule summary", + "decision rollup", + "channel boundary", + "next action", + "blocked operation summary" + ], + "forbidden_frontend_content": [ + "工作視窗對話內容", + "Codex / user 訊息逐字稿", + "prompt / chain-of-thought", + "session id / browser context", + "secret / token / authorization header" + ], + "conversation_transcript_display_allowed": false, + "redaction_required": true + } +} diff --git a/docs/schemas/service_health_failure_notification_policy_v1.schema.json b/docs/schemas/service_health_failure_notification_policy_v1.schema.json new file mode 100644 index 000000000..79203b28a --- /dev/null +++ b/docs/schemas/service_health_failure_notification_policy_v1.schema.json @@ -0,0 +1,198 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:service-health-failure-notification-policy-v1", + "title": "AWOOOI 服務健康失敗限定通知政策 v1", + "description": "服務健康 failure-only 通知合約;只描述成功降噪、action-required 與 failure escalation,不授權 Telegram / AwoooP 發送、live probe、重啟、endpoint 修改、workflow 觸發、runtime execution,也不得顯示工作視窗對話內容。", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "source_service_health_matrix_ref", + "source_refs", + "program_status", + "rollups", + "notification_channels", + "policy_rules", + "message_template_contract", + "display_redaction_contract", + "agent_roles", + "operation_boundaries", + "approval_boundaries" + ], + "properties": { + "schema_version": { + "type": "string", + "const": "service_health_failure_notification_policy_v1" + }, + "generated_at": { + "type": "string", + "minLength": 1 + }, + "source_service_health_matrix_ref": { + "type": "string", + "minLength": 1 + }, + "source_refs": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "program_status": { + "type": "object", + "required": [ + "overall_completion_percent", + "current_priority", + "current_task_id", + "next_task_id", + "read_only_mode" + ], + "properties": { + "overall_completion_percent": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "current_priority": { + "type": "string", + "enum": ["P0", "P1", "P2", "P3"] + }, + "current_task_id": { + "type": "string", + "minLength": 1 + }, + "next_task_id": { + "type": "string", + "minLength": 1 + }, + "read_only_mode": { + "type": "boolean", + "const": true + } + }, + "additionalProperties": false + }, + "rollups": { + "type": "object", + "required": [ + "total_rules", + "by_decision", + "immediate_escalation_rule_ids", + "suppressed_success_rule_ids", + "action_required_rule_ids", + "notification_send_allowed_count" + ], + "additionalProperties": true + }, + "notification_channels": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "channel_id", + "purpose", + "immediate_allowed", + "success_immediate_allowed", + "requires_operator_action" + ], + "additionalProperties": true + } + }, + "policy_rules": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "rule_id", + "event_kind", + "service_state", + "severity", + "decision", + "channels", + "owner_agent", + "requires_incident", + "requires_approval_record", + "message_contract", + "evidence_refs" + ], + "additionalProperties": true + } + }, + "message_template_contract": { + "type": "object", + "required": [ + "required_fields", + "forbidden_fields", + "success_message_policy", + "failure_message_policy" + ], + "additionalProperties": true + }, + "display_redaction_contract": { + "type": "object", + "required": [ + "frontend_display_policy", + "allowed_frontend_fields", + "forbidden_frontend_content", + "conversation_transcript_display_allowed", + "redaction_required" + ], + "properties": { + "frontend_display_policy": { + "type": "string", + "minLength": 1 + }, + "allowed_frontend_fields": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "forbidden_frontend_content": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "conversation_transcript_display_allowed": { + "type": "boolean", + "const": false + }, + "redaction_required": { + "type": "boolean", + "const": true + } + }, + "additionalProperties": false + }, + "agent_roles": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "operation_boundaries": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "approval_boundaries": { + "type": "object", + "additionalProperties": { + "type": "boolean", + "const": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index d9474105d..32161a280 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -3588,3 +3588,21 @@ Phase 6 完成後 1. P1-007:建立 service health 失敗限定 Telegram / AwoooP 對應;成功 smoke 不即時通知,避免洗版。 **裁決:** P1-006 只完成 service health committed evidence cards 的 UI 可讀性;不得把證據卡、target 狀態、主要 evidence ref、風險標籤或下一步解讀成 live probe、服務重啟、endpoint / ConfigMap 修改、通知發送、runtime gate 提高、S4.9 owner response gate 提高或 security acceptance 已成立。 + +### 2026-06-05 下午 (台北) — P1-007 服務健康失敗限定通知合約進行中 + +**觸發**:接續 P1-006 服務健康證據卡正式驗證後推進 P1-007,目標是定義 service health failure-only Telegram / AwoooP 對應,而不是發送任何通知;同時明確要求前端不得顯示本工作視窗的對話內容、prompt、session 或 browser context。 + +**已推進:** +- P1-007:新增 `service_health_failure_notification_policy_v1` schema 與 `docs/evaluations/service_health_failure_notification_policy_2026-06-05.json`。 +- P1-007:新增 `GET /api/v1/agents/service-health-failure-notification-policy` 只讀 API 與 service guard,強制驗證成功通知壓制、failure-only escalation、message template 欄位、frontend redaction contract、work-window transcript marker 阻擋與全部執行邊界。 +- P1-007:治理頁 `/zh-TW/governance?tab=automation-inventory` 新增「Service health 失敗限定通知合約」區塊,以 KPI、摘要磚、規則卡、訊息欄位合約、操作邊界與 redaction 狀態呈現;不新增通知發送或執行按鈕,不顯示工作視窗對話內容。 +- P1-007:同步 automation backlog / inventory snapshot;current `P1-007`、next `P2-004`、backlog done `23/25`、overall `92%`、P1 `100%`、inventory tasks `33`。 +- 本地驗證(目前):JSON parse 通過;service health failure notification policy service / API 目標測試 `10 passed`;snapshot / API / frontend 文字掃描未出現工作視窗 transcript 標記。 + +**待補:** +1. 完成本地 typecheck、build、browser smoke 與 guard。 +2. Gitea push 後取得 deploy marker,補 production API readback 與 desktop / mobile browser smoke。 +3. P2-004:配置優化主線需保持 read-only / approval gate。 + +**裁決:** P1-007 只完成 service health failure-only notification policy、API 與 UI 可讀性;不得把通知合約、Telegram / AwoooP channel、failure escalation rule、message template 或治理頁顯示解讀成實際通知已發送、live probe、服務重啟、endpoint / ConfigMap 修改、runtime gate 提高、S4.9 owner response gate 提高或 security acceptance 已成立。