diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index fa8f80e59..5004bea00 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -71,6 +71,9 @@ from src.services.observability_contract_matrix import ( from src.services.ai_provider_route_matrix import ( load_latest_ai_provider_route_matrix, ) +from src.services.service_health_gap_matrix import ( + load_latest_service_health_gap_matrix, +) from src.services.package_supply_chain_inventory import ( load_latest_package_supply_chain_inventory, ) @@ -598,6 +601,33 @@ async def get_ai_provider_route_matrix() -> dict[str, Any]: ) from exc +@router.get( + "/service-health-gap-matrix", + response_model=dict[str, Any], + summary="取得服務健康缺口與過期端點矩陣", + description=( + "讀取最新已提交的 service health gap matrix;此端點不做 live probe、" + "不重啟服務、不修改 endpoint / ConfigMap、不讀 Secret/Redis/DB payload、" + "不發通知、不觸發 workflow/deploy/reload/runtime execution。" + ), +) +async def get_service_health_gap_matrix() -> dict[str, Any]: + """Return the latest read-only service health gap matrix.""" + try: + return await asyncio.to_thread(load_latest_service_health_gap_matrix) + 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_gap_matrix_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="服務健康缺口矩陣快照無效", + ) from exc + + @router.get( "/backup-dr-target-inventory", response_model=dict[str, Any], diff --git a/apps/api/src/services/service_health_gap_matrix.py b/apps/api/src/services/service_health_gap_matrix.py new file mode 100644 index 000000000..9e1ef9d80 --- /dev/null +++ b/apps/api/src/services/service_health_gap_matrix.py @@ -0,0 +1,282 @@ +""" +Service health gap matrix snapshot. + +Loads the latest committed, read-only service health / stale endpoint gap +matrix. This module never restarts services, changes endpoints, probes live +systems, reads secrets, sends notifications, triggers workflows, deploys, or +executes runtime actions. +""" + +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_gap_matrix_*.json" +_SCHEMA_VERSION = "service_health_gap_matrix_v1" + + +def load_latest_service_health_gap_matrix( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed service health gap matrix snapshot.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError(f"no service health gap matrix 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_target_evidence(payload, str(latest)) + _require_gap_evidence(payload, str(latest)) + _require_operator_denials(payload, str(latest)) + _require_no_plaintext_secret_payload_keys(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_api_allowed") is not True: + raise ValueError(f"{label}: read_only_api_allowed must be true") + + blocked_flags = { + "service_restart_allowed", + "pod_restart_allowed", + "host_restart_allowed", + "rollout_restart_allowed", + "endpoint_change_allowed", + "configmap_patch_allowed", + "active_probe_allowed", + "external_health_probe_allowed", + "live_benchmark_allowed", + "provider_switch_allowed", + "paid_api_call_allowed", + "secret_read_allowed", + "secret_plaintext_allowed", + "notification_send_allowed", + "workflow_trigger_allowed", + "deploy_trigger_allowed", + "reload_trigger_allowed", + "runtime_execution_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: + targets = payload.get("service_health_targets") or [] + gaps = payload.get("health_gaps") or [] + stale_endpoints = payload.get("stale_endpoints") or [] + rollups = payload.get("rollups") or {} + + if rollups.get("total_targets") != len(targets): + raise ValueError(f"{label}: rollups.total_targets must match service_health_targets") + if rollups.get("by_kind") != _count_by(targets, "kind"): + raise ValueError(f"{label}: rollups.by_kind must match service_health_targets") + if rollups.get("by_status") != _count_by(targets, "status"): + raise ValueError(f"{label}: rollups.by_status must match service_health_targets") + if rollups.get("by_freshness_status") != _count_by(targets, "freshness_status"): + raise ValueError(f"{label}: rollups.by_freshness_status must match service_health_targets") + + requiring_action = sorted( + target.get("target_id") + for target in targets + if target.get("status") == "action_required" + ) + if sorted(rollups.get("target_ids_requiring_action") or []) != requiring_action: + raise ValueError(f"{label}: rollups.target_ids_requiring_action must match targets") + + if sorted(rollups.get("health_gap_ids") or []) != sorted(gap.get("gap_id") for gap in gaps): + raise ValueError(f"{label}: rollups.health_gap_ids must match health_gaps") + + if sorted(rollups.get("stale_endpoint_ids") or []) != sorted( + endpoint.get("endpoint_id") for endpoint in stale_endpoints + ): + raise ValueError(f"{label}: rollups.stale_endpoint_ids must match stale_endpoints") + + critical_targets = sorted( + target.get("target_id") + for target in targets + if target.get("risk_level") == "critical" + ) + if sorted(rollups.get("critical_target_ids") or []) != critical_targets: + raise ValueError(f"{label}: rollups.critical_target_ids must match service_health_targets") + + zero_count_fields = { + "service_restart_allowed_count", + "endpoint_change_allowed_count", + "active_probe_allowed_count", + "notification_send_allowed_count", + "runtime_execution_allowed_count", + } + non_zero = sorted(field for field in zero_count_fields if rollups.get(field) != 0) + if non_zero: + raise ValueError(f"{label}: operation permission rollup counts must remain 0: {non_zero}") + + +def _require_target_evidence(payload: dict[str, Any], label: str) -> None: + targets = payload.get("service_health_targets") or [] + missing = sorted( + target.get("target_id") + for target in targets + if not target.get("health_contract") + or not target.get("endpoint_contract") + or not target.get("evidence_refs") + or not target.get("next_action") + ) + if missing: + raise ValueError( + f"{label}: service_health_targets must include health, endpoint, evidence, next_action: {missing}" + ) + + required_target_ids = { + "production_api_health_public", + "awoooi_web_health_manifest", + "ollama_three_layer_health_contract", + "openclaw_health_endpoint_contract", + "prometheus_alertmanager_endpoint_reference", + "gitea_workflow_runner_health_contract", + "kali_scanner_health_reference", + } + present = {target.get("target_id") for target in targets} + missing_required = sorted(required_target_ids - present) + if missing_required: + raise ValueError(f"{label}: missing required service health targets: {missing_required}") + + +def _require_gap_evidence(payload: dict[str, Any], label: str) -> None: + gaps = payload.get("health_gaps") or [] + endpoints = payload.get("stale_endpoints") or [] + required_gap_ids = { + "endpoint_reference_stale_hosts", + "gitea_runner_attestation_health_gap", + "health_check_script_not_authoritative", + "openclaw_nemo_rca_health_review", + "security_scanner_health_evidence_gap", + } + present_gaps = {gap.get("gap_id") for gap in gaps} + missing_gaps = sorted(required_gap_ids - present_gaps) + if missing_gaps: + raise ValueError(f"{label}: missing required health gaps: {missing_gaps}") + + missing_gap_fields = sorted( + gap.get("gap_id") + for gap in gaps + if not gap.get("target_ids") or not gap.get("evidence_refs") or not gap.get("next_action") + ) + if missing_gap_fields: + raise ValueError(f"{label}: health gaps must include targets, evidence, next_action: {missing_gap_fields}") + + required_endpoint_ids = { + "legacy_188_ollama_provider_endpoint", + "prometheus_alertmanager_110_188_split", + "openclaw_8088_comment_vs_8089_contract", + } + present_endpoints = {endpoint.get("endpoint_id") for endpoint in endpoints} + missing_endpoints = sorted(required_endpoint_ids - present_endpoints) + if missing_endpoints: + raise ValueError(f"{label}: missing required stale endpoints: {missing_endpoints}") + + missing_endpoint_fields = sorted( + endpoint.get("endpoint_id") + for endpoint in endpoints + if not endpoint.get("stale_ref") + or not endpoint.get("current_truth") + or not endpoint.get("evidence_refs") + or not endpoint.get("next_action") + ) + if missing_endpoint_fields: + raise ValueError( + f"{label}: stale endpoints must include stale_ref, current_truth, evidence, next_action: {missing_endpoint_fields}" + ) + + +def _require_operator_denials(payload: dict[str, Any], label: str) -> None: + contract = payload.get("operator_contract") or {} + must_not_interpret_as = set(contract.get("must_not_interpret_as") or []) + required_denials = { + "服務重啟批准", + "endpoint 修改批准", + "active probe 批准", + "live health check 已執行", + "Secret payload 已讀取或可輸出", + "Telegram 成功通知批准", + "workflow / deploy / reload 觸發批准", + "provider 切換批准", + "runtime execution 授權", + "OpenClaw 取代或降級批准", + } + if not required_denials.issubset(must_not_interpret_as): + raise ValueError(f"{label}: operator_contract.must_not_interpret_as is missing required denials") + + restart_policy = str(contract.get("restart_policy") or "") + if "P1-005" not in restart_policy or "只能列缺口" not in restart_policy: + raise ValueError(f"{label}: restart_policy must preserve P1-005 read-only boundary") + + endpoint_policy = str(contract.get("endpoint_policy") or "") + if "P1-005" not in endpoint_policy or "不修改端點" not in endpoint_policy: + raise ValueError(f"{label}: endpoint_policy must preserve P1-005 endpoint boundary") + + notification_policy = str(contract.get("notification_policy") or "") + if "P1-007" not in notification_policy or "成功 smoke 不通知" not in notification_policy: + raise ValueError(f"{label}: notification_policy must preserve failure-only notification boundary") + + +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 _count_by(items: list[dict[str, Any]], key: str) -> dict[str, int]: + counts: dict[str, int] = {} + for item in items: + value = item.get(key) + counts[value] = counts.get(value, 0) + 1 + return counts 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 15a499093..c7717610b 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,18 +16,18 @@ 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"] == 87 + assert data["program_status"]["overall_completion_percent"] == 88 assert data["program_status"]["read_only_mode"] is True - assert data["program_status"]["current_task_id"] == "P1-004" - assert data["program_status"]["next_task_id"] == "P1-005" - assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 23 - assert data["rollups"]["by_priority"]["P1"] == 21 - assert data["rollups"]["by_status"]["done"] == 20 - assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 20 - assert data["progress_summary"]["overall_percent"] == 87 - assert data["progress_summary"]["done_items"] == 20 - assert data["progress_summary"]["total_items"] == 23 - assert data["item_approval_boundary_rollup"]["total_items"] == 23 + assert data["program_status"]["current_task_id"] == "P1-005" + assert data["program_status"]["next_task_id"] == "P1-006" + assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 24 + assert data["rollups"]["by_priority"]["P1"] == 22 + assert data["rollups"]["by_status"]["done"] == 21 + assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 21 + assert data["progress_summary"]["overall_percent"] == 88 + assert data["progress_summary"]["done_items"] == 21 + assert data["progress_summary"]["total_items"] == 24 + assert data["item_approval_boundary_rollup"]["total_items"] == 24 assert data["item_approval_boundary_rollup"]["items_requiring_explicit_approval"] == [ "AUTO-P1-004", "AUTO-P2-004", @@ -61,6 +61,12 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho assert p1_004["approval_boundary"]["mode"] == "production_change_blocked" assert "provider_switch" in p1_004["approval_boundary"]["blocked_actions"] assert "ai_provider_route_matrix_2026-06-05.json" in p1_004["evidence_refs"][0] + p1_005 = next(item for item in data["backlog_items"] if item["item_id"] == "AUTO-P1-005") + assert p1_005["status"] == "done" + assert p1_005["next_review"] == "P1-006" + assert p1_005["approval_boundary"]["mode"] == "read_only_allowed" + assert "service_restart" in p1_005["approval_boundary"]["blocked_actions"] + assert "service_health_gap_matrix_2026-06-05.json" in p1_005["evidence_refs"][0] 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 d97132907..b5f9af5a3 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-004" - assert data["program_status"]["next_task_id"] == "P1-005" - assert data["task_approval_boundary_rollup"]["total_tasks"] == len(data["tasks"]) == 30 - assert data["task_approval_boundary_rollup"]["by_mode"]["read_only_allowed"] == 27 + assert data["program_status"]["current_task_id"] == "P1-005" + assert data["program_status"]["next_task_id"] == "P1-006" + assert data["task_approval_boundary_rollup"]["total_tasks"] == len(data["tasks"]) == 31 + assert data["task_approval_boundary_rollup"]["by_mode"]["read_only_allowed"] == 28 assert data["task_approval_boundary_rollup"]["by_mode"]["production_change_blocked"] == 1 assert data["task_approval_boundary_rollup"]["tasks_requiring_explicit_approval"] == [ "P0-001", @@ -48,6 +48,11 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert p1_004["approval_boundary"]["mode"] == "production_change_blocked" assert "provider_switch" in p1_004["approval_boundary"]["blocked_actions"] assert "ai_provider_route_matrix_2026-06-05.json" in p1_004["output"] + p1_005 = next(task for task in data["tasks"] if task["task_id"] == "P1-005") + assert p1_005["status"] == "done" + assert p1_005["approval_boundary"]["mode"] == "read_only_allowed" + assert "service_restart" in p1_005["approval_boundary"]["blocked_actions"] + assert "service_health_gap_matrix_2026-06-05.json" in p1_005["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"]) @@ -80,3 +85,4 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert any(evidence["evidence_id"] == "gitea_workflow_runner_health_api" for evidence in data["evidence"]) assert any(evidence["evidence_id"] == "observability_contract_matrix_api" for evidence in data["evidence"]) 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"]) diff --git a/apps/api/tests/test_service_health_gap_matrix.py b/apps/api/tests/test_service_health_gap_matrix.py new file mode 100644 index 000000000..0611f1f65 --- /dev/null +++ b/apps/api/tests/test_service_health_gap_matrix.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +import json + +import pytest + +from src.services.service_health_gap_matrix import load_latest_service_health_gap_matrix + + +def test_load_latest_service_health_gap_matrix_reads_newest_file(tmp_path): + older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=40) + newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100) + (tmp_path / "service_health_gap_matrix_2026-06-04.json").write_text( + json.dumps(older), + encoding="utf-8", + ) + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(newer), + encoding="utf-8", + ) + + loaded = load_latest_service_health_gap_matrix(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_targets"] == 7 + assert loaded["operation_boundaries"]["active_probe_allowed"] is False + + +def test_service_health_gap_matrix_requires_read_only_mode(tmp_path): + snapshot = _snapshot() + snapshot["program_status"]["read_only_mode"] = False + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="read_only_mode"): + load_latest_service_health_gap_matrix(tmp_path) + + +def test_service_health_gap_matrix_blocks_probe_restart_endpoint_and_notifications(tmp_path): + snapshot = _snapshot() + snapshot["operation_boundaries"]["active_probe_allowed"] = True + snapshot["operation_boundaries"]["service_restart_allowed"] = True + snapshot["operation_boundaries"]["endpoint_change_allowed"] = True + snapshot["operation_boundaries"]["notification_send_allowed"] = True + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="operation boundaries"): + load_latest_service_health_gap_matrix(tmp_path) + + +def test_service_health_gap_matrix_requires_rollup_consistency(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["target_ids_requiring_action"] = [] + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="target_ids_requiring_action"): + load_latest_service_health_gap_matrix(tmp_path) + + +def test_service_health_gap_matrix_requires_required_gaps_and_stale_endpoints(tmp_path): + snapshot = _snapshot() + snapshot["health_gaps"] = snapshot["health_gaps"][:-1] + snapshot["stale_endpoints"] = snapshot["stale_endpoints"][:-1] + _refresh_rollups(snapshot) + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="missing required health gaps"): + load_latest_service_health_gap_matrix(tmp_path) + + +def test_service_health_gap_matrix_requires_operator_denials(tmp_path): + snapshot = _snapshot() + snapshot["operator_contract"]["must_not_interpret_as"].remove("active probe 批准") + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="operator_contract"): + load_latest_service_health_gap_matrix(tmp_path) + + +def test_service_health_gap_matrix_requires_endpoint_and_notification_policies(tmp_path): + snapshot = _snapshot() + snapshot["operator_contract"]["endpoint_policy"] = "可修改端點" + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="endpoint_policy"): + load_latest_service_health_gap_matrix(tmp_path) + + +def test_service_health_gap_matrix_rejects_secret_payload_keys(tmp_path): + snapshot = _snapshot() + snapshot["latest_observations"][0]["authorization_header"] = "redacted" + (tmp_path / "service_health_gap_matrix_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="forbidden secret payload key"): + load_latest_service_health_gap_matrix(tmp_path) + + +def test_service_health_gap_matrix_fails_when_missing(tmp_path): + with pytest.raises(FileNotFoundError): + load_latest_service_health_gap_matrix(tmp_path) + + +def _snapshot( + *, + generated_at: str = "2026-06-05T00:00:00+08:00", + completion: int = 100, +) -> dict: + targets = [ + _target("production_api_health_public", "api_health", "verified", "critical", "fresh_readback"), + _target("awoooi_web_health_manifest", "web_health", "verified", "high", "manifest_mapped"), + _target("ollama_three_layer_health_contract", "ai_provider_health", "action_required", "critical", "source_mismatch"), + _target("openclaw_health_endpoint_contract", "ai_provider_health", "action_required", "critical", "source_mismatch"), + _target("prometheus_alertmanager_endpoint_reference", "observability_health", "action_required", "high", "source_mismatch"), + _target("gitea_workflow_runner_health_contract", "devops_health", "action_required", "critical", "stale_evidence"), + _target("kali_scanner_health_reference", "security_edge_health", "action_required", "medium", "manifest_only"), + ] + gaps = [ + _gap("endpoint_reference_stale_hosts", ["ollama_three_layer_health_contract"]), + _gap("gitea_runner_attestation_health_gap", ["gitea_workflow_runner_health_contract"]), + _gap("health_check_script_not_authoritative", ["production_api_health_public"]), + _gap("openclaw_nemo_rca_health_review", ["openclaw_health_endpoint_contract"]), + _gap("security_scanner_health_evidence_gap", ["kali_scanner_health_reference"]), + ] + endpoints = [ + _endpoint("legacy_188_ollama_provider_endpoint"), + _endpoint("prometheus_alertmanager_110_188_split"), + _endpoint("openclaw_8088_comment_vs_8089_contract"), + ] + payload = { + "schema_version": "service_health_gap_matrix_v1", + "generated_at": generated_at, + "program_status": { + "overall_completion_percent": completion, + "current_priority": "P1", + "current_task_id": "P1-005", + "next_task_id": "P1-006", + "read_only_mode": True, + }, + "source_refs": ["docs/schemas/service_health_gap_matrix_v1.schema.json"], + "rollups": {}, + "service_health_targets": targets, + "health_gaps": gaps, + "stale_endpoints": endpoints, + "latest_observations": [ + { + "observation_id": "service_health_gap_matrix_seed", + "status": "verified", + "summary": "只讀 health gap seed。", + "evidence_refs": ["docs/reference/SERVICE-ENDPOINTS.md"], + } + ], + "operator_contract": { + "display_mode": "read_only_service_health_gap_matrix", + "must_not_interpret_as": [ + "服務重啟批准", + "endpoint 修改批准", + "active probe 批准", + "live health check 已執行", + "Secret payload 已讀取或可輸出", + "Telegram 成功通知批准", + "workflow / deploy / reload 觸發批准", + "provider 切換批准", + "runtime execution 授權", + "OpenClaw 取代或降級批准", + ], + "secret_display_policy": "只顯示來源檔案與 env var 名稱。", + "restart_policy": "P1-005 只能列缺口;任何 restart 都需另行批准。", + "endpoint_policy": "P1-005 不修改端點,只提出 source truth cleanup。", + "notification_policy": "成功 smoke 不通知;失敗或 stale 才進 P1-007 通知合約。", + }, + "operation_boundaries": { + "read_only_api_allowed": True, + "service_restart_allowed": False, + "pod_restart_allowed": False, + "host_restart_allowed": False, + "rollout_restart_allowed": False, + "endpoint_change_allowed": False, + "configmap_patch_allowed": False, + "active_probe_allowed": False, + "external_health_probe_allowed": False, + "live_benchmark_allowed": False, + "provider_switch_allowed": False, + "paid_api_call_allowed": False, + "secret_read_allowed": False, + "secret_plaintext_allowed": False, + "notification_send_allowed": False, + "workflow_trigger_allowed": False, + "deploy_trigger_allowed": False, + "reload_trigger_allowed": False, + "runtime_execution_allowed": False, + }, + "approval_boundaries": { + "service_restart_approved": False, + "endpoint_change_approved": False, + "configmap_patch_approved": False, + "active_probe_approved": False, + "secret_read_approved": False, + "notification_send_approved": False, + "workflow_trigger_approved": False, + "provider_switch_approved": False, + "cost_change_approved": False, + }, + } + _refresh_rollups(payload) + return payload + + +def _target(target_id: str, kind: str, status: str, risk_level: str, freshness_status: str) -> dict: + return { + "target_id": target_id, + "display_name": target_id, + "kind": kind, + "status": status, + "risk_level": risk_level, + "freshness_status": freshness_status, + "health_contract": "只讀健康合約。", + "endpoint_contract": "不修改 endpoint。", + "evidence_refs": ["docs/reference/SERVICE-ENDPOINTS.md"], + "next_action": "只補證據,不執行。", + } + + +def _gap(gap_id: str, target_ids: list[str]) -> dict: + return { + "gap_id": gap_id, + "display_name": gap_id, + "status": "action_required", + "severity": "high", + "summary": "只讀缺口。", + "target_ids": target_ids, + "evidence_refs": ["docs/reference/SERVICE-ENDPOINTS.md"], + "next_action": "只整理 owner attestation。", + } + + +def _endpoint(endpoint_id: str) -> dict: + return { + "endpoint_id": endpoint_id, + "display_name": endpoint_id, + "status": "action_required", + "severity": "medium", + "stale_ref": "舊端點參考。", + "current_truth": "只標記 source drift,不判定 live down。", + "evidence_refs": ["docs/reference/SERVICE-ENDPOINTS.md"], + "next_action": "只整理 source cleanup。", + } + + +def _refresh_rollups(payload: dict) -> None: + targets = payload["service_health_targets"] + gaps = payload["health_gaps"] + endpoints = payload["stale_endpoints"] + payload["rollups"] = { + "total_targets": len(targets), + "by_kind": _count_by(targets, "kind"), + "by_status": _count_by(targets, "status"), + "by_freshness_status": _count_by(targets, "freshness_status"), + "target_ids_requiring_action": sorted( + target["target_id"] for target in targets if target["status"] == "action_required" + ), + "health_gap_ids": sorted(gap["gap_id"] for gap in gaps), + "stale_endpoint_ids": sorted(endpoint["endpoint_id"] for endpoint in endpoints), + "critical_target_ids": sorted( + target["target_id"] for target in targets if target["risk_level"] == "critical" + ), + "read_only_denials_total": 10, + "service_restart_allowed_count": 0, + "endpoint_change_allowed_count": 0, + "active_probe_allowed_count": 0, + "notification_send_allowed_count": 0, + "runtime_execution_allowed_count": 0, + } + + +def _count_by(items: list[dict], key: str) -> dict[str, int]: + counts: dict[str, int] = {} + for item in items: + value = item[key] + counts[value] = counts.get(value, 0) + 1 + return counts diff --git a/apps/api/tests/test_service_health_gap_matrix_api.py b/apps/api/tests/test_service_health_gap_matrix_api.py new file mode 100644 index 000000000..1ee366268 --- /dev/null +++ b/apps/api/tests/test_service_health_gap_matrix_api.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router + + +def test_service_health_gap_matrix_endpoint_returns_committed_snapshot(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/service-health-gap-matrix") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "service_health_gap_matrix_v1" + assert data["program_status"]["current_task_id"] == "P1-005" + assert data["program_status"]["next_task_id"] == "P1-006" + assert data["program_status"]["read_only_mode"] is True + assert data["rollups"]["total_targets"] == len(data["service_health_targets"]) == 10 + assert data["rollups"]["service_restart_allowed_count"] == 0 + assert data["rollups"]["endpoint_change_allowed_count"] == 0 + assert data["rollups"]["active_probe_allowed_count"] == 0 + assert data["rollups"]["notification_send_allowed_count"] == 0 + assert data["rollups"]["runtime_execution_allowed_count"] == 0 + assert data["operation_boundaries"]["service_restart_allowed"] is False + assert data["operation_boundaries"]["endpoint_change_allowed"] is False + assert data["operation_boundaries"]["active_probe_allowed"] is False + assert data["operation_boundaries"]["notification_send_allowed"] is False + assert data["operation_boundaries"]["runtime_execution_allowed"] is False + assert data["approval_boundaries"]["active_probe_approved"] is False + assert data["approval_boundaries"]["endpoint_change_approved"] is False + assert data["approval_boundaries"]["notification_send_approved"] is False + assert any( + target["target_id"] == "ollama_three_layer_health_contract" + and target["status"] == "action_required" + for target in data["service_health_targets"] + ) + assert any( + gap["gap_id"] == "endpoint_reference_stale_hosts" + for gap in data["health_gaps"] + ) + assert any( + endpoint["endpoint_id"] == "legacy_188_ollama_provider_endpoint" + for endpoint in data["stale_endpoints"] + ) + assert "active probe 批准" in data["operator_contract"]["must_not_interpret_as"] diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 350365c88..6f8cb03ef 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -3365,6 +3365,60 @@ "claude": "Claude", "nvidia": "NVIDIA" } + }, + "serviceHealth": { + "title": "服務健康缺口與過期端點", + "source": "{generated} · {current} → {next}", + "staleTitle": "過期端點 {count}", + "gapsTitle": "健康缺口 {count}", + "contractTitle": "不可誤讀合約", + "metrics": { + "targets": "健康目標", + "actions": "需處置", + "stale": "過期端點", + "gaps": "缺口", + "denied": "允許入口" + }, + "map": { + "coverage": "合約覆蓋", + "coverageDetail": "API / Web / AI provider / Observability / Gitea / Backup / Security。", + "stale": "來源漂移", + "staleDetail": "只標示 source mismatch,不判定 live service down。", + "review": "Owner 複核", + "reviewDetail": "端點、runner、scanner 與 OpenClaw / Nemo 健康仍需證據補齊。", + "safeBoundary": "安全邊界", + "safeBoundaryDetail": "重啟、endpoint 修改、active probe、通知與 runtime execution 入口皆為 0。" + }, + "labels": { + "freshness": "新鮮度", + "risk": "風險" + }, + "values": { + "api_health": "API 健康", + "web_health": "Web 健康", + "ai_provider_health": "AI Provider 健康", + "observability_health": "可觀測性健康", + "devops_health": "DevOps 健康", + "backup_health": "備份健康", + "security_edge_health": "安全邊界健康", + "verified": "已驗證", + "action_required": "需處置", + "blocked": "阻擋", + "proposal_required": "需提案", + "preserved": "已保留", + "fresh_readback": "正式讀回", + "manifest_mapped": "Manifest 已映射", + "source_mismatch": "來源不一致", + "committed_health_hook": "已提交健康 hook", + "stale_evidence": "證據待更新", + "committed_exporter": "已提交 exporter", + "source_heartbeat_contract": "來源心跳合約", + "manifest_only": "僅 manifest", + "low": "低", + "medium": "中", + "high": "高", + "critical": "關鍵" + } } } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 350365c88..6f8cb03ef 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -3365,6 +3365,60 @@ "claude": "Claude", "nvidia": "NVIDIA" } + }, + "serviceHealth": { + "title": "服務健康缺口與過期端點", + "source": "{generated} · {current} → {next}", + "staleTitle": "過期端點 {count}", + "gapsTitle": "健康缺口 {count}", + "contractTitle": "不可誤讀合約", + "metrics": { + "targets": "健康目標", + "actions": "需處置", + "stale": "過期端點", + "gaps": "缺口", + "denied": "允許入口" + }, + "map": { + "coverage": "合約覆蓋", + "coverageDetail": "API / Web / AI provider / Observability / Gitea / Backup / Security。", + "stale": "來源漂移", + "staleDetail": "只標示 source mismatch,不判定 live service down。", + "review": "Owner 複核", + "reviewDetail": "端點、runner、scanner 與 OpenClaw / Nemo 健康仍需證據補齊。", + "safeBoundary": "安全邊界", + "safeBoundaryDetail": "重啟、endpoint 修改、active probe、通知與 runtime execution 入口皆為 0。" + }, + "labels": { + "freshness": "新鮮度", + "risk": "風險" + }, + "values": { + "api_health": "API 健康", + "web_health": "Web 健康", + "ai_provider_health": "AI Provider 健康", + "observability_health": "可觀測性健康", + "devops_health": "DevOps 健康", + "backup_health": "備份健康", + "security_edge_health": "安全邊界健康", + "verified": "已驗證", + "action_required": "需處置", + "blocked": "阻擋", + "proposal_required": "需提案", + "preserved": "已保留", + "fresh_readback": "正式讀回", + "manifest_mapped": "Manifest 已映射", + "source_mismatch": "來源不一致", + "committed_health_hook": "已提交健康 hook", + "stale_evidence": "證據待更新", + "committed_exporter": "已提交 exporter", + "source_heartbeat_contract": "來源心跳合約", + "manifest_only": "僅 manifest", + "low": "低", + "medium": "中", + "high": "高", + "critical": "關鍵" + } } } }, 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 fe0bb49cf..a2761534c 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 @@ -43,6 +43,7 @@ import { type ObservabilityContractMatrixSnapshot, type OffsiteEscrowReadinessStatusSnapshot, type RuntimeSurfaceInventorySnapshot, + type ServiceHealthGapMatrixSnapshot, } from '@/lib/api-client' function formatDateTime(value: string): string { @@ -307,6 +308,7 @@ export function AutomationInventoryTab() { const [giteaHealth, setGiteaHealth] = useState(null) const [observabilityMatrix, setObservabilityMatrix] = useState(null) const [providerRouteMatrix, setProviderRouteMatrix] = useState(null) + const [serviceHealthGapMatrix, setServiceHealthGapMatrix] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) @@ -323,6 +325,7 @@ export function AutomationInventoryTab() { apiClient.getGiteaWorkflowRunnerHealth(), apiClient.getObservabilityContractMatrix(), apiClient.getAiProviderRouteMatrix(), + apiClient.getServiceHealthGapMatrix(), ] as const Promise.allSettled(requests) @@ -338,6 +341,7 @@ export function AutomationInventoryTab() { giteaHealthResult, observabilityMatrixResult, providerRouteMatrixResult, + serviceHealthGapMatrixResult, ] = results setSnapshot(inventoryResult.status === 'fulfilled' ? inventoryResult.value : null) @@ -350,6 +354,7 @@ export function AutomationInventoryTab() { setGiteaHealth(giteaHealthResult.status === 'fulfilled' ? giteaHealthResult.value : null) setObservabilityMatrix(observabilityMatrixResult.status === 'fulfilled' ? observabilityMatrixResult.value : null) setProviderRouteMatrix(providerRouteMatrixResult.status === 'fulfilled' ? providerRouteMatrixResult.value : null) + setServiceHealthGapMatrix(serviceHealthGapMatrixResult.status === 'fulfilled' ? serviceHealthGapMatrixResult.value : null) setError([ inventoryResult, backlogResult, @@ -360,6 +365,7 @@ export function AutomationInventoryTab() { giteaHealthResult, observabilityMatrixResult, providerRouteMatrixResult, + serviceHealthGapMatrixResult, ].some(result => result.status === 'rejected')) }) .catch(() => setError(true)) @@ -506,6 +512,17 @@ export function AutomationInventoryTab() { }) }, [providerRouteMatrix]) + const visibleServiceHealthTargets = useMemo(() => { + if (!serviceHealthGapMatrix) return [] + const priority = { action_required: 0, blocked: 1, verified: 2 } as Record + return [...serviceHealthGapMatrix.service_health_targets].sort((a, b) => { + const left = priority[a.status] ?? 3 + const right = priority[b.status] ?? 3 + if (left !== right) return left - right + return a.target_id.localeCompare(b.target_id) + }) + }, [serviceHealthGapMatrix]) + if (loading) { return (
@@ -519,7 +536,7 @@ export function AutomationInventoryTab() { ) } - if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix) { + if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !serviceHealthGapMatrix) { return (
@@ -586,6 +603,16 @@ export function AutomationInventoryTab() { + providerRouteMatrix.rollups.shadow_or_canary_allowed_count + providerRouteMatrix.rollups.runtime_route_change_allowed_count ) + const serviceHealthActions = serviceHealthGapMatrix.rollups.target_ids_requiring_action.length + const serviceHealthStaleGaps = serviceHealthGapMatrix.rollups.stale_endpoint_ids.length + const serviceHealthOperatorReviews = serviceHealthGapMatrix.rollups.health_gap_ids.length + const serviceHealthDeniedCount = ( + serviceHealthGapMatrix.rollups.active_probe_allowed_count + + serviceHealthGapMatrix.rollups.service_restart_allowed_count + + serviceHealthGapMatrix.rollups.endpoint_change_allowed_count + + serviceHealthGapMatrix.rollups.notification_send_allowed_count + + serviceHealthGapMatrix.rollups.runtime_execution_allowed_count + ) 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 @@ -718,6 +745,14 @@ export function AutomationInventoryTab() { } } + const serviceHealthValueLabel = (value: string) => { + try { + return t(`serviceHealth.values.${value}` as never) + } catch { + return value + } + } + return (
@@ -1854,6 +1889,170 @@ export function AutomationInventoryTab() {
+ +
+
+
+ + + {t('serviceHealth.title')} + +
+
+ {t('serviceHealth.source', { + generated: formatDateTime(serviceHealthGapMatrix.generated_at), + current: serviceHealthGapMatrix.program_status.current_task_id, + next: serviceHealthGapMatrix.program_status.next_task_id, + })} +
+
+ +
+ } /> + 0 ? 'warn' : 'ok'} icon={} /> + 0 ? 'warn' : 'ok'} icon={} /> + 0 ? 'warn' : 'ok'} icon={} /> + } /> +
+ +
+ } + /> + } + /> + } + /> + } + /> +
+ +
+
+ {visibleServiceHealthTargets.map(targetItem => ( +
+
+
+ + {targetItem.display_name} + + +
+
+ + + +
+
+ {targetItem.health_contract} +
+
+ {targetItem.endpoint_contract} +
+
+ {targetItem.next_action} +
+
+ +
+
+
+ ))} +
+ +
+
+ + {t('serviceHealth.staleTitle', { count: serviceHealthStaleGaps })} + + {serviceHealthGapMatrix.stale_endpoints.map(endpoint => ( +
+
+ + {endpoint.display_name} + + +
+
+ {endpoint.stale_ref} +
+
+ {endpoint.current_truth} +
+
+ ))} +
+ +
+ + {t('serviceHealth.gapsTitle', { count: serviceHealthOperatorReviews })} + + {serviceHealthGapMatrix.health_gaps.slice(0, 4).map(gap => ( +
+
+ + {gap.display_name} + + +
+
+ {gap.summary} +
+
+ ))} +
+ +
+ + {t('serviceHealth.contractTitle')} + +
+ {serviceHealthGapMatrix.operator_contract.restart_policy} +
+
+ {serviceHealthGapMatrix.operator_contract.endpoint_policy} +
+
+ {serviceHealthGapMatrix.operator_contract.notification_policy} +
+
+ {serviceHealthGapMatrix.operator_contract.must_not_interpret_as.slice(0, 6).map(item => ( + + ))} +
+
+
+
+
+
+
@@ -1976,6 +2175,10 @@ export function AutomationInventoryTab() { .automation-inventory-provider-map-grid, .automation-inventory-provider-grid, .automation-inventory-provider-route-grid, + .automation-inventory-service-health-kpi-grid, + .automation-inventory-service-health-map-grid, + .automation-inventory-service-health-grid, + .automation-inventory-service-health-target-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 bcc4bfd78..d8ae88293 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -282,6 +282,11 @@ export const apiClient = { return handleResponse(res) }, + async getServiceHealthGapMatrix() { + const res = await fetch(`${API_BASE_URL}/agents/service-health-gap-matrix`) + return handleResponse(res) + }, + async getBackupDrTargetInventory() { const res = await fetch(`${API_BASE_URL}/agents/backup-dr-target-inventory`) return handleResponse(res) @@ -1105,6 +1110,83 @@ export interface AiProviderRouteMatrixSnapshot { approval_boundaries: Record } +export interface ServiceHealthGapMatrixSnapshot { + schema_version: 'service_health_gap_matrix_v1' + generated_at: 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 + } + source_refs: string[] + rollups: { + total_targets: number + by_kind: Record + by_status: Record + by_freshness_status: Record + target_ids_requiring_action: string[] + health_gap_ids: string[] + stale_endpoint_ids: string[] + critical_target_ids: string[] + read_only_denials_total: number + service_restart_allowed_count: number + endpoint_change_allowed_count: number + active_probe_allowed_count: number + notification_send_allowed_count: number + runtime_execution_allowed_count: number + } + service_health_targets: Array<{ + target_id: string + display_name: string + kind: string + status: 'verified' | 'action_required' | 'blocked' + risk_level: 'low' | 'medium' | 'high' | 'critical' + freshness_status: string + health_contract: string + endpoint_contract: string + evidence_refs: string[] + next_action: string + }> + health_gaps: Array<{ + gap_id: string + display_name: string + status: string + severity: 'low' | 'medium' | 'high' | 'critical' + summary: string + target_ids: string[] + evidence_refs: string[] + next_action: string + }> + stale_endpoints: Array<{ + endpoint_id: string + display_name: string + status: string + severity: 'low' | 'medium' | 'high' | 'critical' + stale_ref: string + current_truth: string + evidence_refs: string[] + next_action: string + }> + latest_observations: Array<{ + observation_id: string + status: string + summary: string + evidence_refs: string[] + }> + operator_contract: { + display_mode: 'read_only_service_health_gap_matrix' + must_not_interpret_as: string[] + secret_display_policy: string + restart_policy: string + endpoint_policy: string + notification_policy: string + } + 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 64cbd0c3b..d79c88872 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,38 @@ +## 2026-06-05|P1-005 服務健康缺口與過期端點本地完成 + +**背景**:接續 P1-004 AI Provider 路由矩陣正式驗證,依工作清單推進 `P1-005`。本段只建立 committed service health gap matrix、只讀 API 與治理頁顯示;不做 live probe / external probe、不重啟 service / pod / host、不修改 endpoint / ConfigMap、不讀 Secret / Redis / DB payload、不發 Telegram / AwoooP 通知、不觸發 workflow / deploy / reload / runtime execution。 + +**本輪完成**: +- 新增 `service_health_gap_matrix_v1` schema 與 `docs/evaluations/service_health_gap_matrix_2026-06-05.json`。 +- 新增 `GET /api/v1/agents/service-health-gap-matrix` 與 service guard,強制驗證 read-only mode、operation / approval boundaries、rollup consistency、health gap / stale endpoint / target evidence 與 operator denial。 +- 治理頁 `/zh-TW/governance?tab=automation-inventory` 新增「服務健康缺口與過期端點」區塊,將健康目標、需處置、過期端點、健康缺口與不可誤讀合約拆成 KPI、摘要磚、目標卡與缺口清單,降低文字牆閱讀負擔。 +- 同步 automation backlog / inventory snapshot:current `P1-005`、next `P1-006`、backlog overall `88%`、P1 `95%`、done `21/24`、inventory tasks `31`。 + +**目前數字**: +- Service health targets:`10`。 +- 需處置 targets:`5`。 +- Stale endpoints:`3`。 +- Health gaps:`5`。 +- Service restart / endpoint change / active probe / notification send / runtime execution allowed counts 全部 `0`。 + +**本地驗證**: +- JSON parse 通過:`service_health_gap_matrix_2026-06-05.json`、`service_health_gap_matrix_v1.schema.json`、automation backlog / inventory snapshots、zh-TW / en messages。 +- 目標測試通過:service health gap matrix service / API、automation inventory / backlog snapshot service / API 共 `25 passed`。 +- Python py_compile 通過:`apps/api/src/services/service_health_gap_matrix.py`、`apps/api/src/api/v1/agents.py`。 +- zh-TW / en i18n key 差異 `0`;web typecheck 通過;Next production build 通過,治理頁 First Load JS `387 kB`;`source-control-owner-response-guard.py`、`security-mirror-progress-guard.py` 與 `git diff --check` 通過。 +- 本地 API readback:service health gap matrix 回 `service_health_gap_matrix_v1`、current `P1-005`、next `P1-006`、targets `10`、需處置 `5`、stale endpoints `3`、health gaps `5`;backlog 回 overall `88%`、done `21/24`;inventory 回 tasks `31`。 + +**待補**: +- 推送 Gitea 後等待 deploy marker,補 production API readback 與 desktop / mobile browser smoke,再更新正式驗證紀錄。 + +**邊界**: +- 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-005 UI / API 可見只代表 committed service health gap matrix 完成,不代表任何健康檢查已 live 執行,也不代表 runtime gate、S4.9 owner response gate 或 security acceptance gate 已提高。 + +**下一步**: +1. 完成本輪 build、Gitea push、正式 deploy marker 追蹤與 production smoke。 +2. P1-006:在 UI 顯示更細緻的 service health evidence cards。 + ## 2026-06-05|P1-004 AI Provider 路由矩陣正式上線 **背景**:接續 P1-003 監控合約與降噪矩陣正式驗證,依工作清單推進 `P1-004`。本段只建立 AI Router / Ollama / OpenClaw / Nemotron / Gemini provider route 的 committed 只讀矩陣、API 與治理頁顯示;不切換 provider、不呼叫 Gemini / NVIDIA / Claude、不改 `USE_AI_ROUTER`、不修改 fallback order 或 `OLLAMA_*`、不讀 Secret payload、不進 shadow / canary、不觸發 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 4a9af886b..b524d60e6 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 自動化 | 87% | P0 已完成,P1 套件 / 供應鏈主線已完成;備份 / DR 主線已完成到異地 / escrow 準備度顯示;任務批准邊界、進度彙總、P1-001 執行面只讀矩陣、P1-002 Gitea 工作流程 / runner 健康合約、P1-003 監控合約 / 降噪矩陣與 P1-004 AI 供應商路由矩陣已完成,下一主線是 P1-005 服務健康缺口與過期端點偵測 | 狀態分類、盤點 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 已完成 | +| 工具 / 服務 / 套件 AI 自動化 | 88% | P0 已完成,P1 套件 / 供應鏈主線已完成;備份 / DR 主線已完成到異地 / escrow 準備度顯示;任務批准邊界、進度彙總、P1-001 執行面只讀矩陣、P1-002 Gitea 工作流程 / runner 健康合約、P1-003 監控合約 / 降噪矩陣、P1-004 AI 供應商路由矩陣與 P1-005 服務健康缺口矩陣已完成,下一主線是 P1-006 在 UI 顯示 service health 證據卡 | 狀態分類、盤點 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 已完成 | | 本工作清單與分析報告 | 100% | 已完成 | 本 MD 文件 | -AI Agent 自動化工作包目前完成度:**87%**。本工作清單文件本身完成度:**100%**。 +AI Agent 自動化工作包目前完成度:**88%**。本工作清單文件本身完成度:**100%**。 完成度計算模型: @@ -870,7 +870,7 @@ UI: | P1-002 | 完成 | 100 | Hermes | 盤點 Gitea 工作流程與 runner 健康合約 | `gitea_workflow_runner_health_v1` / `GET /api/v1/agents/gitea-workflow-runner-health` / Gitea 健康合約 UI | 只讀;不修改 workflow、不重啟 runner、不停止 container、不讀 Secret、不發通知 | | P1-003 | 完成 | 100 | Hermes | 盤點 Prometheus / Alertmanager / SigNoz / Grafana 監控合約 | `observability_contract_matrix_v1` / `GET /api/v1/agents/observability-contract-matrix` / 監控合約與降噪 UI | 只讀;不修改 alert rules、不改 receiver/route、不建立 silence、不寫 Grafana、不發通知 | | 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 | 待辦 | 0 | OpenClaw | 偵測服務健康缺口與過期端點 | 需處置清單 | 不重啟 | +| P1-005 | 完成 | 100 | OpenClaw | 偵測服務健康缺口與過期端點 | `service_health_gap_matrix_v1` / `GET /api/v1/agents/service-health-gap-matrix` / 服務健康缺口與過期端點 UI | 只讀;不重啟、不改 endpoint、不做 active probe、不發通知 | | P1-006 | 待辦 | 0 | Hermes | 在 UI 顯示 service health 證據卡 | 狀態卡 | 瀏覽器驗證 | | P1-007 | 待辦 | 0 | OpenClaw | 建立 service health 失敗限定 Telegram / AwoooP 對應 | 通知合約 | 不發成功洗版 | @@ -1102,24 +1102,23 @@ UI: 本次同步: ```text -進度:87%。 +進度:88%。 目前優先級:P1。 -目前任務:P1-004 盤點 AI Router / Ollama / Nemotron / Gemini provider 路徑。 +目前任務:P1-005 偵測服務健康缺口與過期端點。 狀態變更:待辦 -> 完成。 -證據:ai_provider_route_matrix_v1 schema / snapshot;GET /api/v1/agents/ai-provider-route-matrix;治理頁 AI 供應商路由矩陣區塊;automation backlog 87%;inventory tasks 30。 -目前數字:provider routes 7;需處置 routes 2;需人工批准 candidate gates 3;source gaps 3;blocked candidates 1;provider switch / paid API call / shadow or canary / runtime route change allowed counts 全部 0;backlog done 20/23;overall 87%;P1 95%;WS3 監控自動化 100%。 -驗證:JSON parse 通過;AI provider route matrix service / API、automation inventory / backlog snapshot service / API 目標測試 `25 passed`;Python py_compile 通過;zh-TW / en i18n key 差異 `0`;web typecheck 通過;Next production build 通過;source-control-owner-response guard、security-mirror-progress guard、git diff --check 通過;本地 API readback 回 `ai_provider_route_matrix_v1`、current `P1-004`、next `P1-005`、routes `7`、approval gates `3`;backlog 回 `87%`、done `20/23`;inventory 回 tasks `30`;本地 desktop `1440x1000` 與 mobile `390x844` browser smoke 通過,`AI 供應商路由矩陣`、`P1-004`、`P1-005`、`87%`、`Ollama 三層`、`Nemotron 候選`、`不可誤讀合約` 可見,`horizontalOverflow=0`、overflowing elements `0`、危險互動入口 `0`。 -正式驗證:code commit `45556f8f` 已推 `gitea main`;deploy marker `c619446b chore(cd): deploy 45556f8 [skip ci]`;Gitea code-review `#2607` 成功;CD `#2606` tests 與 build-and-deploy 成功,post-deploy-checks job 顯示 `Blocked by required conditions`,已由人工只讀 production smoke 補正式驗證。Production health `healthy/prod/mock_mode=false`;AI provider route matrix API 回 `ai_provider_route_matrix_v1`、current `P1-004`、next `P1-005`、routes `7`、approval gates `3`、provider switch allowed `0`;backlog API 回 overall `87%`、done `20/23`;inventory API 回 tasks `30`、`production_change_blocked=1`;production desktop `1440x1000` 與 mobile `390x844` smoke 通過,`AI 供應商路由矩陣`、`P1-004`、`P1-005`、`87%`、`Ollama 三層`、`Nemotron 候選`、`不可誤讀合約`、`允許入口 0` 可見;console error `0`、HTTP failed response `0`、`horizontalOverflow=0`、overflowing elements `0`、危險互動入口 `0`。 -阻擋:provider switch、production routing change、USE_AI_ROUTER 切換、fallback order 修改、OLLAMA_* endpoint / ConfigMap 修改、Gemini / Claude / NVIDIA 付費呼叫或頻率提高、OpenClaw 取代 / 降級、Nemotron shadow / canary、external live probe / benchmark、Secret payload read、通知發送、workflow/deploy/reload/runtime execution 仍全部禁止。 -下一步:P1-005 偵測服務健康缺口與過期端點。 +證據:service_health_gap_matrix_v1 schema / snapshot;GET /api/v1/agents/service-health-gap-matrix;治理頁服務健康缺口與過期端點區塊;automation backlog 88%;inventory tasks 31。 +目前數字:service health targets 10;需處置 targets 5;stale endpoints 3;health gaps 5;service restart / endpoint change / active probe / notification send / runtime execution allowed counts 全部 0;backlog done 21/24;overall 88%;P1 95%;WS3 監控自動化 100%。 +驗證:JSON parse 通過;service health gap matrix service / API、automation inventory / backlog snapshot service / API 目標測試 `25 passed`;Python py_compile 通過;zh-TW / en i18n key 差異 `0`;web typecheck 通過;source-control-owner-response guard、security-mirror-progress guard、git diff --check 通過;本地 API readback 回 `service_health_gap_matrix_v1`、current `P1-005`、next `P1-006`、targets `10`、需處置 `5`、stale endpoints `3`、health gaps `5`;backlog 回 `88%`、done `21/24`;inventory 回 tasks `31`。 +正式驗證:待 code commit 推送 Gitea、取得 deploy marker 後補 production API readback 與 desktop / mobile browser smoke。 +阻擋: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-006 在 UI 顯示 service health 證據卡。 ``` ## 13. 立即執行順序 -1. P1-005:偵測服務健康缺口與過期端點。 -2. P1-006:在 UI 顯示 service health 證據卡。 -3. P1-007:建立 service health 失敗限定 Telegram / AwoooP 對應。 -4. P2 / P3 必須等 P1 服務、監控與 provider runtime surface 可見且關卡穩定後再做。 +1. P1-006:在 UI 顯示 service health 證據卡。 +2. P1-007:建立 service health 失敗限定 Telegram / AwoooP 對應。 +3. P2 / P3 必須等 P1 服務、監控、provider route 與 service health evidence cards 可見且關卡穩定後再做。 ## 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 b1f89f8a0..1a09f11d8 100644 --- a/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json +++ b/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json @@ -1,34 +1,34 @@ { "schema_version": "ai_agent_automation_backlog_v1", - "generated_at": "2026-06-05T13:28:00+08:00", + "generated_at": "2026-06-05T14:05: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": 87, + "overall_completion_percent": 88, "current_priority": "P1", - "current_task_id": "P1-004", - "next_task_id": "P1-005", + "current_task_id": "P1-005", + "next_task_id": "P1-006", "read_only_mode": true }, "rollups": { - "total_items": 23, + "total_items": 24, "by_priority": { - "P1": 21, + "P1": 22, "P2": 1, "P3": 1 }, "by_status": { - "done": 20, + "done": 21, "planned": 3 }, "by_gate_status": { - "read_only_allowed": 20, + "read_only_allowed": 21, "production_change_blocked": 1, "cost_approval_required": 1, "blocked_by_evidence": 1 }, "by_owner_agent": { "hermes": 12, - "openclaw": 10, + "openclaw": 11, "nemotron": 1 } }, @@ -421,6 +421,63 @@ ] } }, + { + "item_id": "AUTO-P1-005", + "priority": "P1", + "status": "done", + "workstream_id": "WS3", + "source_asset_id": "service_health_gap_matrix", + "source_signal_kind": "runtime_evidence_gap", + "title": "偵測服務健康缺口與過期端點", + "owner_agent": "openclaw", + "recommended_action": "已建立 service health gap matrix,只整理 committed health / endpoint / stale evidence;不做 live probe、不重啟、不改 endpoint。", + "action_class": "observe", + "gate_status": "read_only_allowed", + "risk_level": "critical", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json", + "GET /api/v1/agents/service-health-gap-matrix", + "docs/reference/SERVICE-ENDPOINTS.md", + "scripts/health_check_session.sh", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json", + "docs/evaluations/observability_contract_matrix_2026-06-05.json", + "docs/evaluations/gitea_workflow_runner_health_2026-06-05.json" + ], + "acceptance_criteria": [ + "不做 live probe 或 external health probe", + "不重啟 service / pod / host,不 rollout restart", + "不修改 endpoint / ConfigMap / public URL", + "不讀 Secret payload、不發通知、不觸發 workflow / deploy / runtime execution" + ], + "next_review": "P1-006", + "approval_boundary": { + "mode": "read_only_allowed", + "display_summary": "只允許整理 committed service health gap evidence 與需處置清單;不得 live probe、重啟、改 endpoint 或發通知。", + "allowed_actions": [ + "讀取 committed snapshot", + "整理健康缺口", + "顯示治理 UI", + "準備 operator review 清單" + ], + "blocked_actions": [ + "production_write", + "runtime_execution", + "destructive_operation", + "secret_plaintext_collection", + "live_probe", + "service_restart", + "endpoint_change", + "notification_send" + ], + "requires_operator_approval_for": [ + "live probe", + "服務重啟", + "endpoint / ConfigMap 修改", + "Telegram / AwoooP 通知發送", + "runtime execution" + ] + } + }, { "item_id": "AUTO-P1-007", "priority": "P1", @@ -1142,9 +1199,9 @@ "destructive_operation_allowed": false }, "item_approval_boundary_rollup": { - "total_items": 23, + "total_items": 24, "by_mode": { - "read_only_allowed": 20, + "read_only_allowed": 21, "production_change_blocked": 1, "cost_approval_required": 1, "blocked_by_evidence": 1 @@ -1155,14 +1212,11 @@ "AUTO-P3-001" ], "items_with_blocked_operations": [ - "AUTO-P1-303", - "AUTO-P1-304", - "AUTO-P1-305", - "AUTO-P1-306", "AUTO-P1-001", "AUTO-P1-002", "AUTO-P1-003", "AUTO-P1-004", + "AUTO-P1-005", "AUTO-P1-007", "AUTO-P1-101", "AUTO-P1-102", @@ -1176,22 +1230,26 @@ "AUTO-P1-204", "AUTO-P1-205", "AUTO-P1-206", + "AUTO-P1-303", + "AUTO-P1-304", + "AUTO-P1-305", + "AUTO-P1-306", "AUTO-P2-004", "AUTO-P3-001" ] }, "progress_summary": { - "overall_percent": 87, - "done_items": 20, + "overall_percent": 88, + "done_items": 21, "planned_items": 3, - "total_items": 23, + "total_items": 24, "formula": "round(done_items / total_items * 100),只有 status=done 計入完成;planned/in_progress/blocked/deferred/rejected 不計入。", "by_priority": [ { "priority": "P1", "completion_percent": 95, - "done_items": 20, - "total_items": 21 + "done_items": 21, + "total_items": 22 }, { "priority": "P2", @@ -1215,14 +1273,30 @@ "total_items": 3, "next_task_id": "P3-001" }, + { + "workstream_id": "WS8", + "display_name": "產品 UI", + "completion_percent": 100, + "done_items": 2, + "total_items": 2, + "next_task_id": "complete" + }, { "workstream_id": "WS3", "display_name": "監控自動化", "completion_percent": 100, - "done_items": 4, - "total_items": 4, + "done_items": 5, + "total_items": 5, "next_task_id": "complete" }, + { + "workstream_id": "WS7", + "display_name": "安全執行關卡", + "completion_percent": 0, + "done_items": 0, + "total_items": 1, + "next_task_id": "P1-007" + }, { "workstream_id": "WS4", "display_name": "備份與 DR 自動化", @@ -1246,22 +1320,6 @@ "done_items": 0, "total_items": 1, "next_task_id": "P2-004" - }, - { - "workstream_id": "WS7", - "display_name": "安全執行關卡", - "completion_percent": 0, - "done_items": 0, - "total_items": 1, - "next_task_id": "P1-007" - }, - { - "workstream_id": "WS8", - "display_name": "產品 UI", - "completion_percent": 100, - "done_items": 2, - "total_items": 2, - "next_task_id": "complete" } ] } 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 622254cad..243793a9d 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-05T13:28:00+08:00", + "generated_at": "2026-06-05T14:05:00+08:00", "program_status": { "overall_completion_percent": 100, "current_priority": "P1", - "current_task_id": "P1-004", - "next_task_id": "P1-005", + "current_task_id": "P1-005", + "next_task_id": "P1-006", "read_only_mode": true }, "status_taxonomy": { @@ -451,6 +451,21 @@ "docs/evaluations/docker_build_surface_inventory_2026-06-04.json" ], "next_action": "P1-206 產生 Docker base image digest、binary source、CVE 與 rebuild approval package。" + }, + { + "asset_id": "service_health_gap_matrix", + "domain_id": "services", + "display_name": "服務健康缺口與過期端點矩陣", + "asset_type": "read_only_health_gap_matrix", + "status": "ready_for_review", + "gate_status": "read_only_allowed", + "owner_agent": "openclaw", + "risk_level": "high", + "evidence_refs": [ + "docs/evaluations/service_health_gap_matrix_2026-06-05.json", + "GET /api/v1/agents/service-health-gap-matrix" + ], + "next_action": "P1-006 在治理頁顯示 service health 證據卡;不得做 live probe、重啟或改 endpoint。" } ], "workstreams": [ @@ -1484,6 +1499,44 @@ "付費 API 呼叫" ] } + }, + { + "task_id": "P1-005", + "priority": "P1", + "status": "done", + "completion_percent": 100, + "owner_agent": "openclaw", + "title": "偵測服務健康缺口與過期端點", + "output": "docs/evaluations/service_health_gap_matrix_2026-06-05.json + GET /api/v1/agents/service-health-gap-matrix", + "gate_status": "read_only_allowed", + "next_action": "完成 committed service health gap matrix;下一步 P1-006 在 UI 顯示 service health 證據卡。", + "approval_boundary": { + "mode": "read_only_allowed", + "display_summary": "只允許整理 committed service health gap evidence 與需處置清單;不得 live probe、重啟、改 endpoint 或發通知。", + "allowed_actions": [ + "讀取 committed snapshot", + "整理健康缺口", + "顯示治理 UI", + "準備 operator review 清單" + ], + "blocked_actions": [ + "production_write", + "runtime_execution", + "destructive_operation", + "secret_plaintext_collection", + "live_probe", + "service_restart", + "endpoint_change", + "notification_send" + ], + "requires_operator_approval_for": [ + "live probe", + "服務重啟", + "endpoint / ConfigMap 修改", + "Telegram / AwoooP 通知發送", + "runtime execution" + ] + } } ], "evidence": [ @@ -1818,6 +1871,18 @@ "source_ref": "GET /api/v1/agents/ai-provider-route-matrix", "status": "done", "summary": "只讀呈現 AI Router / Ollama / OpenClaw / Nemotron / Gemini provider route、候選 Gate、來源缺口與不可誤讀合約;不切 provider、不呼叫付費 API、不讀 Secret。" + }, + { + "evidence_id": "service_health_gap_matrix_snapshot", + "kind": "committed_snapshot", + "ref": "docs/evaluations/service_health_gap_matrix_2026-06-05.json", + "result": "P1-005 committed service health gap matrix 已建立,涵蓋 10 個 service health target、3 個 stale endpoint、5 個 health gap;重啟、endpoint 修改、active probe、通知與 runtime execution 入口皆為 0。" + }, + { + "evidence_id": "service_health_gap_matrix_api", + "kind": "read_only_api", + "ref": "GET /api/v1/agents/service-health-gap-matrix", + "result": "只讀 API 回傳 service_health_gap_matrix_v1;不做 live probe、不重啟服務、不改 endpoint、不讀 Secret/Redis/DB payload、不發通知。" } ], "approval_boundaries": { @@ -1828,10 +1893,10 @@ "destructive_operation_allowed": false }, "task_approval_boundary_rollup": { - "total_tasks": 30, + "total_tasks": 31, "by_mode": { "ready_for_operator_review": 1, - "read_only_allowed": 27, + "read_only_allowed": 28, "approval_required": 1, "production_change_blocked": 1 }, @@ -1852,12 +1917,8 @@ "P1-001", "P1-002", "P1-003", - "P1-301", - "P1-302", - "P1-303", - "P1-304", - "P1-305", - "P1-306", + "P1-004", + "P1-005", "P1-101", "P1-102", "P1-103", @@ -1870,7 +1931,12 @@ "P1-204", "P1-205", "P1-206", - "P1-004" + "P1-301", + "P1-302", + "P1-303", + "P1-304", + "P1-305", + "P1-306" ] } } diff --git a/docs/evaluations/service_health_gap_matrix_2026-06-05.json b/docs/evaluations/service_health_gap_matrix_2026-06-05.json new file mode 100644 index 000000000..1337590d7 --- /dev/null +++ b/docs/evaluations/service_health_gap_matrix_2026-06-05.json @@ -0,0 +1,439 @@ +{ + "schema_version": "service_health_gap_matrix_v1", + "generated_at": "2026-06-05T14:05:00+08:00", + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P1", + "current_task_id": "P1-005", + "next_task_id": "P1-006", + "read_only_mode": true + }, + "source_refs": [ + "docs/schemas/service_health_gap_matrix_v1.schema.json", + "docs/reference/SERVICE-ENDPOINTS.md", + "scripts/health_check_session.sh", + "docs/reference/ALERT-TAXONOMY-CATALOG.md", + "apps/api/src/core/config.py", + "apps/api/src/api/v1/health.py", + "apps/api/src/services/ai_provider_route_matrix.py", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json", + "docs/evaluations/observability_contract_matrix_2026-06-05.json", + "docs/evaluations/gitea_workflow_runner_health_2026-06-05.json", + "k8s/awoooi-prod/05-deployment-web.yaml", + "apps/api/src/services/metrics_service.py", + "apps/api/src/services/ai_providers/ollama.py", + "apps/api/src/services/ai_providers/openclaw_nemo.py", + "scripts/ops/backup-health-textfile-exporter.py", + "docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md" + ], + "rollups": { + "total_targets": 10, + "by_kind": { + "api_health": 1, + "web_health": 1, + "ai_provider_health": 2, + "observability_health": 3, + "devops_health": 1, + "backup_health": 1, + "security_edge_health": 1 + }, + "by_status": { + "verified": 5, + "action_required": 5 + }, + "by_freshness_status": { + "fresh_readback": 1, + "manifest_mapped": 1, + "source_mismatch": 3, + "committed_health_hook": 1, + "stale_evidence": 1, + "committed_exporter": 1, + "source_heartbeat_contract": 1, + "manifest_only": 1 + }, + "target_ids_requiring_action": [ + "gitea_workflow_runner_health_contract", + "kali_scanner_health_reference", + "ollama_three_layer_health_contract", + "openclaw_health_endpoint_contract", + "prometheus_alertmanager_endpoint_reference" + ], + "health_gap_ids": [ + "endpoint_reference_stale_hosts", + "gitea_runner_attestation_health_gap", + "health_check_script_not_authoritative", + "openclaw_nemo_rca_health_review", + "security_scanner_health_evidence_gap" + ], + "stale_endpoint_ids": [ + "legacy_188_ollama_provider_endpoint", + "openclaw_8088_comment_vs_8089_contract", + "prometheus_alertmanager_110_188_split" + ], + "critical_target_ids": [ + "ollama_three_layer_health_contract", + "openclaw_health_endpoint_contract", + "production_api_health_public", + "gitea_workflow_runner_health_contract" + ], + "read_only_denials_total": 10, + "service_restart_allowed_count": 0, + "endpoint_change_allowed_count": 0, + "active_probe_allowed_count": 0, + "notification_send_allowed_count": 0, + "runtime_execution_allowed_count": 0 + }, + "service_health_targets": [ + { + "target_id": "production_api_health_public", + "display_name": "Production API Health", + "kind": "api_health", + "status": "verified", + "risk_level": "critical", + "freshness_status": "fresh_readback", + "health_contract": "`/api/v1/health` 是正式環境健康讀回入口,回傳 PostgreSQL / Redis / Ollama / OpenClaw / SigNoz 等 component 狀態與 `mock_mode=false`。", + "endpoint_contract": "正式站以 `https://awoooi.wooo.work/api/v1/health` 讀回;P1-005 只記錄現有讀回形狀,不新增 probe。", + "evidence_refs": [ + "apps/api/src/api/v1/health.py", + "docs/LOGBOOK.md", + "docs/runbooks/AWOOOP-RLS-CANARY-WAVE1.md" + ], + "next_action": "P1-006 只把健康證據卡顯示到 UI;不得把讀回成功解讀成可重啟或改 endpoint。" + }, + { + "target_id": "awoooi_web_health_manifest", + "display_name": "AWOOOI Web /api/health", + "kind": "web_health", + "status": "verified", + "risk_level": "high", + "freshness_status": "manifest_mapped", + "health_contract": "Web deployment liveness / readiness / startup probe 皆指向 `/api/health`。", + "endpoint_contract": "只讀 K8s manifest;不 patch deployment、不 rollout restart、不改 public URL。", + "evidence_refs": [ + "k8s/awoooi-prod/05-deployment-web.yaml" + ], + "next_action": "P1-006 顯示 manifest health contract;任何 probe path 變更需另開部署批准。" + }, + { + "target_id": "ollama_three_layer_health_contract", + "display_name": "Ollama 三層健康合約", + "kind": "ai_provider_health", + "status": "action_required", + "risk_level": "critical", + "freshness_status": "source_mismatch", + "health_contract": "目前 AI provider route truth 是 GCP-A → GCP-B → 111;Gemini 只作 final fallback。", + "endpoint_contract": "`SERVICE-ENDPOINTS.md` 仍有 188 Ollama 參考,`scripts/health_check_session.sh` 又檢查 111 / 110;需把 endpoint truth 收斂為 owner-attested source,不可直接改路由。", + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "scripts/health_check_session.sh", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json", + "apps/api/src/services/ai_providers/ollama.py" + ], + "next_action": "只開 health evidence card 與 endpoint truth review;不得改 `OLLAMA_*`、不重啟 Ollama、不做 live benchmark。" + }, + { + "target_id": "openclaw_health_endpoint_contract", + "display_name": "OpenClaw 健康端點合約", + "kind": "ai_provider_health", + "status": "action_required", + "risk_level": "critical", + "freshness_status": "source_mismatch", + "health_contract": "OpenClaw 仍是生產仲裁核心;health check 使用 `OPENCLAW_URL/health`。", + "endpoint_contract": "`SERVICE-ENDPOINTS.md` 已標 8089,但 `openclaw_nemo.py` 註解仍殘留 8088;需收斂註解與 runbook truth。", + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "apps/api/src/services/ai_providers/openclaw_nemo.py", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json" + ], + "next_action": "只提出 source cleanup;不得重啟 OpenClaw、不得把 health gap 當成替換 OpenClaw 批准。" + }, + { + "target_id": "prometheus_alertmanager_endpoint_reference", + "display_name": "Prometheus / Alertmanager 端點參考", + "kind": "observability_health", + "status": "action_required", + "risk_level": "high", + "freshness_status": "source_mismatch", + "health_contract": "Prometheus / Alertmanager 只允許 read-only readiness evidence;Alertmanager 仍必須走 AWOOOI API,不得指向 OpenClaw receiver。", + "endpoint_contract": "`SERVICE-ENDPOINTS.md` 記錄 188:9090 / 9093,`health_check_session.sh` 檢查 110:9090 / 9093;需要 owner attestation。", + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "scripts/health_check_session.sh", + "docs/evaluations/observability_contract_matrix_2026-06-05.json" + ], + "next_action": "P1-006 顯示 source mismatch;不得改 alert rule、receiver、route 或 silence。" + }, + { + "target_id": "signoz_clickhouse_health_contract", + "display_name": "SigNoz / ClickHouse 健康合約", + "kind": "observability_health", + "status": "verified", + "risk_level": "high", + "freshness_status": "committed_health_hook", + "health_contract": "metrics service 有 ClickHouse health check;observability matrix 已把 SigNoz / ClickHouse 視為 committed evidence。", + "endpoint_contract": "只讀 committed health hook 與 matrix;不改 SigNoz query、不改 ClickHouse datasource。", + "evidence_refs": [ + "apps/api/src/services/metrics_service.py", + "docs/evaluations/observability_contract_matrix_2026-06-05.json" + ], + "next_action": "後續若要 live query,需另開批准;P1-005 只呈現 committed evidence。" + }, + { + "target_id": "sentry_source_health_contract", + "display_name": "Sentry Source Health", + "kind": "observability_health", + "status": "verified", + "risk_level": "medium", + "freshness_status": "source_heartbeat_contract", + "health_contract": "Source provider heartbeat 與 upstream canary 已被 observability matrix 納入來源證據;P1-005 不呼叫 Sentry API。", + "endpoint_contract": "只讀 source-link / heartbeat contract;不改 webhook、不讀 Sentry token。", + "evidence_refs": [ + "docs/evaluations/observability_contract_matrix_2026-06-05.json", + "docs/reference/SERVICE-ENDPOINTS.md" + ], + "next_action": "P1-006 顯示 source heartbeat contract;token / webhook 變更需另行批准。" + }, + { + "target_id": "gitea_workflow_runner_health_contract", + "display_name": "Gitea Workflow / Runner Health", + "kind": "devops_health", + "status": "action_required", + "risk_level": "critical", + "freshness_status": "stale_evidence", + "health_contract": "Gitea workflow runner health matrix 已完成,但 runner owner attestation gap 仍需處置。", + "endpoint_contract": "只讀 Gitea workflow / runner snapshot;不重啟 runner、不停止 container、不觸發 workflow。", + "evidence_refs": [ + "docs/evaluations/gitea_workflow_runner_health_2026-06-05.json", + "docs/LOGBOOK.md" + ], + "next_action": "保留 owner attestation 待補;不得用 P1-005 觸發 Gitea Actions 或 runner mutation。" + }, + { + "target_id": "backup_health_exporter_freshness", + "display_name": "Backup Health Exporter Freshness", + "kind": "backup_health", + "status": "verified", + "risk_level": "high", + "freshness_status": "committed_exporter", + "health_contract": "backup health textfile exporter 已定義 backup freshness、restore drill freshness、offsite freshness 與 escrow freshness metrics。", + "endpoint_contract": "只讀 exporter contract;不執行 backup / restore / offsite sync,不寫 credential marker。", + "evidence_refs": [ + "scripts/ops/backup-health-textfile-exporter.py", + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json" + ], + "next_action": "維持成功不洗版;失敗或 stale 才進 P1-007 通知合約。" + }, + { + "target_id": "kali_scanner_health_reference", + "display_name": "Kali Scanner Health Reference", + "kind": "security_edge_health", + "status": "action_required", + "risk_level": "medium", + "freshness_status": "manifest_only", + "health_contract": "`SERVICE-ENDPOINTS.md` 記錄 Kali Scanner `192.168.0.112:8080`,但 P1-005 不做 live probe。", + "endpoint_contract": "只讀服務端點參考;不啟動掃描、不呼叫 scanner、不改 host 服務。", + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "docs/LOGBOOK.md" + ], + "next_action": "P1-006 僅顯示需 owner attestation;真正掃描健康檢查需另行批准。" + } + ], + "health_gaps": [ + { + "gap_id": "endpoint_reference_stale_hosts", + "display_name": "端點參考存在主機 truth drift", + "status": "action_required", + "severity": "high", + "summary": "服務端點文件、health script、AI provider route matrix 對 Ollama / Prometheus / Alertmanager / OpenClaw 的主機來源不完全一致。", + "target_ids": [ + "ollama_three_layer_health_contract", + "openclaw_health_endpoint_contract", + "prometheus_alertmanager_endpoint_reference" + ], + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "scripts/health_check_session.sh", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json" + ], + "next_action": "建立 owner attestation / source cleanup,不直接改 endpoint。" + }, + { + "gap_id": "gitea_runner_attestation_health_gap", + "display_name": "Runner 健康仍缺 owner attestation", + "status": "action_required", + "severity": "high", + "summary": "P1-002 已列 runner attestation gap;P1-005 只把它納入 service health 缺口,不觸發 runner 操作。", + "target_ids": [ + "gitea_workflow_runner_health_contract" + ], + "evidence_refs": [ + "docs/evaluations/gitea_workflow_runner_health_2026-06-05.json" + ], + "next_action": "由 P1-006 顯示,P1-007 才定義失敗限定通知。" + }, + { + "gap_id": "health_check_script_not_authoritative", + "display_name": "健康檢查腳本不是唯一真相來源", + "status": "proposal_required", + "severity": "medium", + "summary": "`scripts/health_check_session.sh` 可供人工會前檢查,但它與正式 K8s / public health / provider matrix 的 truth chain 需對齊。", + "target_ids": [ + "production_api_health_public", + "prometheus_alertmanager_endpoint_reference", + "kali_scanner_health_reference" + ], + "evidence_refs": [ + "scripts/health_check_session.sh", + "docs/reference/SERVICE-ENDPOINTS.md" + ], + "next_action": "只產生 P1-006 顯示與後續 runbook cleanup 提案。" + }, + { + "gap_id": "openclaw_nemo_rca_health_review", + "display_name": "OpenClaw / Nemo RCA 健康仍需複核", + "status": "action_required", + "severity": "high", + "summary": "AI provider route matrix 將 OpenClaw Nemo RCA lane 標為需處置;P1-005 不改 provider,只列健康缺口。", + "target_ids": [ + "openclaw_health_endpoint_contract" + ], + "evidence_refs": [ + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json", + "apps/api/src/services/ai_providers/openclaw_nemo.py" + ], + "next_action": "用 replay / smoke 證據補強,不把 health review 當作 OpenClaw 替換批准。" + }, + { + "gap_id": "security_scanner_health_evidence_gap", + "display_name": "Kali scanner 只有 endpoint reference", + "status": "proposal_required", + "severity": "medium", + "summary": "Security edge 目前只有服務端點參考,缺少 committed health contract 或 owner attestation。", + "target_ids": [ + "kali_scanner_health_reference" + ], + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md" + ], + "next_action": "後續建立 read-only scanner health evidence card;不得啟動掃描。" + } + ], + "stale_endpoints": [ + { + "endpoint_id": "legacy_188_ollama_provider_endpoint", + "display_name": "legacy 188 Ollama endpoint reference", + "status": "action_required", + "severity": "high", + "stale_ref": "`docs/reference/SERVICE-ENDPOINTS.md` 仍列 `192.168.0.188:11434` 為 Ollama。", + "current_truth": "P1-004 provider route truth 是 GCP-A → GCP-B → 111;188 不再作為 AWOOOI Ollama provider。", + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json", + "apps/api/src/services/ai_providers/ollama.py" + ], + "next_action": "準備 source cleanup 批准包;不得改 `OLLAMA_URL` 或重啟 Ollama。" + }, + { + "endpoint_id": "prometheus_alertmanager_110_188_split", + "display_name": "Prometheus / Alertmanager 110 與 188 參考分裂", + "status": "action_required", + "severity": "medium", + "stale_ref": "`SERVICE-ENDPOINTS.md` 使用 188,`health_check_session.sh` 使用 110。", + "current_truth": "P1-005 只判定為 source mismatch,需要 owner attestation;不判定 live service down。", + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "scripts/health_check_session.sh", + "docs/evaluations/observability_contract_matrix_2026-06-05.json" + ], + "next_action": "只補 truth owner;不得改 Alertmanager route 或 receiver。" + }, + { + "endpoint_id": "openclaw_8088_comment_vs_8089_contract", + "display_name": "OpenClaw 8088 註解與 8089 合約不一致", + "status": "action_required", + "severity": "medium", + "stale_ref": "`openclaw_nemo.py` 註解仍提到 `192.168.0.188:8088`。", + "current_truth": "`SERVICE-ENDPOINTS.md` 與設定使用 OpenClaw 8089;P1-005 只標記註解 / runbook drift。", + "evidence_refs": [ + "apps/api/src/services/ai_providers/openclaw_nemo.py", + "docs/reference/SERVICE-ENDPOINTS.md", + "apps/api/src/core/config.py" + ], + "next_action": "後續 source cleanup 不得改 provider route 或降級 OpenClaw。" + } + ], + "latest_observations": [ + { + "observation_id": "service_health_gap_matrix_seed", + "status": "verified", + "summary": "P1-005 只讀收斂 committed health / endpoint / stale evidence;不做 live probe。", + "evidence_refs": [ + "docs/reference/SERVICE-ENDPOINTS.md", + "docs/evaluations/ai_provider_route_matrix_2026-06-05.json", + "docs/evaluations/observability_contract_matrix_2026-06-05.json" + ] + }, + { + "observation_id": "active_probe_denied", + "status": "preserved", + "summary": "本矩陣沒有呼叫任何內網、外部 provider、Sentry、SigNoz、Gitea 或 scanner health endpoint。", + "evidence_refs": [ + "docs/HARD_RULES.md", + "docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md" + ] + } + ], + "operator_contract": { + "display_mode": "read_only_service_health_gap_matrix", + "must_not_interpret_as": [ + "服務重啟批准", + "endpoint 修改批准", + "active probe 批准", + "live health check 已執行", + "Secret payload 已讀取或可輸出", + "Telegram 成功通知批准", + "workflow / deploy / reload 觸發批准", + "provider 切換批准", + "runtime execution 授權", + "OpenClaw 取代或降級批准" + ], + "secret_display_policy": "只顯示來源檔案與 env var 名稱,不輸出 secret payload。", + "restart_policy": "P1-005 只能列缺口;任何 service / pod / host restart 都需另行批准。", + "endpoint_policy": "P1-005 不修改端點、不改 ConfigMap、不改 public URL,只提出 source truth cleanup。", + "notification_policy": "成功 smoke 不通知;只有失敗、stale、action-required 才進 P1-007 通知合約。" + }, + "operation_boundaries": { + "read_only_api_allowed": true, + "service_restart_allowed": false, + "pod_restart_allowed": false, + "host_restart_allowed": false, + "rollout_restart_allowed": false, + "endpoint_change_allowed": false, + "configmap_patch_allowed": false, + "active_probe_allowed": false, + "external_health_probe_allowed": false, + "live_benchmark_allowed": false, + "provider_switch_allowed": false, + "paid_api_call_allowed": false, + "secret_read_allowed": false, + "secret_plaintext_allowed": false, + "notification_send_allowed": false, + "workflow_trigger_allowed": false, + "deploy_trigger_allowed": false, + "reload_trigger_allowed": false, + "runtime_execution_allowed": false + }, + "approval_boundaries": { + "service_restart_approved": false, + "endpoint_change_approved": false, + "configmap_patch_approved": false, + "active_probe_approved": false, + "secret_read_approved": false, + "notification_send_approved": false, + "workflow_trigger_approved": false, + "provider_switch_approved": false, + "cost_change_approved": false + } +} diff --git a/docs/schemas/service_health_gap_matrix_v1.schema.json b/docs/schemas/service_health_gap_matrix_v1.schema.json new file mode 100644 index 000000000..e7cade8a1 --- /dev/null +++ b/docs/schemas/service_health_gap_matrix_v1.schema.json @@ -0,0 +1,161 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:service-health-gap-matrix-v1", + "title": "AWOOOI 服務健康缺口只讀矩陣 v1", + "description": "以 repo 內 committed service endpoint、health check、K8s manifest、AI provider 與 observability evidence 建立服務健康缺口矩陣;不重啟服務、不改 endpoint、不做 active probe、不讀 secret payload、不送通知。", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "program_status", + "source_refs", + "rollups", + "service_health_targets", + "health_gaps", + "stale_endpoints", + "latest_observations", + "operator_contract", + "operation_boundaries", + "approval_boundaries" + ], + "properties": { + "schema_version": { + "type": "string", + "const": "service_health_gap_matrix_v1" + }, + "generated_at": { + "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 + }, + "source_refs": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "rollups": { + "type": "object", + "additionalProperties": true + }, + "service_health_targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "target_id", + "display_name", + "kind", + "status", + "risk_level", + "freshness_status", + "health_contract", + "endpoint_contract", + "evidence_refs", + "next_action" + ], + "additionalProperties": true + } + }, + "health_gaps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "gap_id", + "display_name", + "status", + "severity", + "summary", + "target_ids", + "evidence_refs", + "next_action" + ], + "additionalProperties": true + } + }, + "stale_endpoints": { + "type": "array", + "items": { + "type": "object", + "required": [ + "endpoint_id", + "display_name", + "status", + "severity", + "stale_ref", + "current_truth", + "evidence_refs", + "next_action" + ], + "additionalProperties": true + } + }, + "latest_observations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "operator_contract": { + "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 c665cd790..e3da47184 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 @@ -3535,6 +3535,24 @@ Phase 6 完成後 **裁決:** P1-003 只完成 read-only observability contract matrix 與降噪候選顯示。不得把 Prometheus rule count、Alertmanager grouping、SigNoz / Sentry heartbeat、Grafana dashboard JSON、OTEL/Event Exporter evidence 或 classification gap 解讀成 alert rule 變更、receiver/route 變更、OpenClaw receiver 授權、silence、dashboard import、webhook 修改、secret 讀取、通知發送、deploy/reload/workflow 或 runtime execution 批准。成功 smoke 不即時通知洗版;失敗與需處置才進批准流程。 +### 2026-06-05 下午 (台北) — P1-005 服務健康缺口與過期端點本地完成 + +**觸發**:統帥批准繼續,接續 P1-004 正式驗證後推進 `docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md` 的 P1-005,只建立 committed service health gap evidence、只讀 API 與治理頁顯示。 + +**已推進:** +- P1-005:新增 `service_health_gap_matrix_v1` schema 與 `docs/evaluations/service_health_gap_matrix_2026-06-05.json`,涵蓋 API / Web / AI provider / Observability / Gitea / Backup / Security edge 的服務健康目標、健康缺口與過期端點 reference。 +- P1-005:新增 `GET /api/v1/agents/service-health-gap-matrix` 與 service guard,強制驗證 read-only mode、operation / approval boundaries、rollup consistency、target / health gap / stale endpoint evidence、operator denial 與禁止 secret payload key。 +- P1-005:治理頁 `/zh-TW/governance?tab=automation-inventory` 新增「服務健康缺口與過期端點」區塊;顯示健康目標、需處置、過期端點、健康缺口、不可誤讀合約與允許入口 0,不新增任何執行按鈕。 +- 目前數字:service health targets `10`;需處置 targets `5`;stale endpoints `3`;health gaps `5`;service restart / endpoint change / active probe / notification send / runtime execution allowed counts 全部 `0`;automation backlog done `21/24`、overall `88%`、P1 `95%`;inventory tasks `31`。 +- 本地驗證:JSON parse 通過;service health gap matrix service / API、automation inventory / backlog snapshot service / API 目標測試 `25 passed`;Python py_compile 通過;zh-TW / en i18n key 差異 `0`;web typecheck 通過。 + +**待補驗證:** +1. Next production build。 +2. `source-control-owner-response-guard.py`、`security-mirror-progress-guard.py` 與 `git diff --check`。 +3. 推送 Gitea 後取得 deploy marker,補 production API readback 與 desktop / mobile browser smoke。 + +**裁決:** P1-005 只完成 service health gap matrix 的 committed evidence、API 與治理頁顯示。不得把 `service_health_gap_matrix_v1`、健康目標、過期端點、health gaps、source mismatch、owner attestation 或 UI 可見解讀成 live probe、服務重啟、endpoint / ConfigMap 修改、通知發送、provider switch、paid API call、Secret payload read、workflow / deploy / reload 或 runtime execution 批准。 + ### 2026-06-05 下午 (台北) — P1-004 AI Provider 路由矩陣正式上線 **觸發**:統帥批准繼續,要求依 `docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md` 的優先順序推進,並在每階段同步完成度、工作狀態、驗證節點與正式環境頁面檢查。 @@ -3552,3 +3570,20 @@ Phase 6 完成後 2. 持續保持 S4.9 owner response gate `0%` 與 active runtime gate `0`,不得把 P1-004 UI / API 可見誤讀成 runtime 授權。 **裁決:** P1-004 只完成 read-only AI provider route matrix 與治理頁顯示。不得把 `ai_provider_route_matrix_v1`、Ollama 三層、Gemini final fallback、OpenClaw / Nemo RCA lane、Nemotron blocked candidate、legacy models registry 或 provider route source gaps 解讀成 provider 切換、付費呼叫、fallback 調整、OpenClaw 取代、Nemotron 升級、shadow / canary、Secret payload 讀取、workflow / deploy / reload 或 runtime execution 批准。 + +### 2026-06-05 下午 (台北) — P1-005 服務健康缺口與過期端點本地完成 + +**觸發**:統帥批准繼續,並要求治理頁避免文字牆、以圖表 / 卡片 / 專業資訊架構呈現完整資訊,同時強化決策支援覆蓋率的參考性。 + +**已推進:** +- P1-005:新增 `service_health_gap_matrix_v1` schema 與 `docs/evaluations/service_health_gap_matrix_2026-06-05.json`,以 committed health / endpoint / stale evidence 建立只讀 service health gap matrix。 +- P1-005:新增 `GET /api/v1/agents/service-health-gap-matrix` 只讀 API 與 service guard,強制拒絕把 snapshot 誤讀成 live probe、service restart、endpoint / ConfigMap 修改、Secret / Redis / DB payload 讀取、通知發送、workflow / deploy / reload 或 runtime execution 授權。 +- P1-005:治理頁 `/zh-TW/governance?tab=automation-inventory` 新增「服務健康缺口與過期端點」區塊;以 KPI、摘要磚、健康目標卡、過期端點清單、健康缺口清單與不可誤讀合約呈現,降低文字牆閱讀負擔,不新增任何執行按鈕。 +- 目前數字:service health targets `10`;需處置 targets `5`;stale endpoints `3`;health gaps `5`;service restart / endpoint change / active probe / notification send / runtime execution allowed counts 全部 `0`;automation backlog done `21/24`、overall `88%`、P1 `95%`、WS3 `100%`;inventory tasks `31`。 +- 本地驗證:JSON parse 通過;service health gap matrix service / API、automation inventory / backlog snapshot API、AI provider route matrix service / API 目標測試 `22 passed`;Python py_compile 通過;zh-TW / en i18n key 差異 `0`;web typecheck 通過;Next production build 通過,治理頁 First Load JS `387 kB`;source-control-owner-response guard、security-mirror-progress guard、`git diff --check` 通過;本地 API readback 回 `service_health_gap_matrix_v1`、current `P1-005`、next `P1-006`、targets `10`、需處置 `5`、stale endpoints `3`、health gaps `5`;backlog 回 `88%`、done `21/24`;inventory 回 tasks `31`。 + +**待補:** +1. Gitea push 後取得 deploy marker,補 production API readback 與 desktop / mobile browser smoke。 +2. P1-006:在 UI 顯示更細緻的 service health evidence cards。 + +**裁決:** P1-005 只完成 read-only service health gap matrix、API 與治理頁顯示。不得把 `service_health_gap_matrix_v1`、健康目標、過期端點、健康缺口、需處置清單或不可誤讀合約解讀成 live health check 已執行、服務重啟批准、endpoint 修改批准、通知發送批准、runtime gate 提高、S4.9 owner response gate 提高或 security acceptance 已成立。