feat(governance): 新增 AI Provider 路由矩陣
This commit is contained in:
@@ -68,6 +68,9 @@ from src.services.gitea_workflow_runner_health import (
|
||||
from src.services.observability_contract_matrix import (
|
||||
load_latest_observability_contract_matrix,
|
||||
)
|
||||
from src.services.ai_provider_route_matrix import (
|
||||
load_latest_ai_provider_route_matrix,
|
||||
)
|
||||
from src.services.package_supply_chain_inventory import (
|
||||
load_latest_package_supply_chain_inventory,
|
||||
)
|
||||
@@ -567,6 +570,34 @@ async def get_observability_contract_matrix() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/ai-provider-route-matrix",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 AI Provider 路由只讀矩陣",
|
||||
description=(
|
||||
"讀取最新已提交的 AI Router / Ollama / OpenClaw / Nemotron / Gemini provider route matrix;"
|
||||
"此端點不切換 provider、不呼叫 Gemini / NVIDIA / Claude、不改 USE_AI_ROUTER、"
|
||||
"不修改 fallback order、不讀 Secret payload、不進 shadow / canary、"
|
||||
"不觸發 workflow / deploy / reload / runtime execution。"
|
||||
),
|
||||
)
|
||||
async def get_ai_provider_route_matrix() -> dict[str, Any]:
|
||||
"""Return the latest read-only AI provider route matrix."""
|
||||
try:
|
||||
return await asyncio.to_thread(load_latest_ai_provider_route_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("ai_provider_route_matrix_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="AI Provider 路由只讀矩陣快照無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/backup-dr-target-inventory",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
254
apps/api/src/services/ai_provider_route_matrix.py
Normal file
254
apps/api/src/services/ai_provider_route_matrix.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
AI provider route matrix snapshot.
|
||||
|
||||
Loads the latest committed, read-only AI Router / Ollama / OpenClaw /
|
||||
Nemotron / Gemini provider route matrix. This module never switches providers,
|
||||
calls paid APIs, probes external providers, reads secrets, changes runtime
|
||||
routes, enters shadow/canary, triggers workflows, 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 = "ai_provider_route_matrix_*.json"
|
||||
_SCHEMA_VERSION = "ai_provider_route_matrix_v1"
|
||||
|
||||
|
||||
def load_latest_ai_provider_route_matrix(
|
||||
evaluations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the newest committed AI provider route matrix snapshot."""
|
||||
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
|
||||
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"no AI provider route 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_route_evidence(payload, str(latest))
|
||||
_require_candidate_gates(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 = {
|
||||
"provider_switch_allowed",
|
||||
"production_routing_change_allowed",
|
||||
"use_ai_router_toggle_allowed",
|
||||
"fallback_order_change_allowed",
|
||||
"ollama_endpoint_change_allowed",
|
||||
"paid_api_call_allowed",
|
||||
"paid_api_frequency_increase_allowed",
|
||||
"external_provider_probe_allowed",
|
||||
"live_benchmark_allowed",
|
||||
"shadow_or_canary_allowed",
|
||||
"openclaw_replacement_allowed",
|
||||
"nemotron_shadow_allowed",
|
||||
"gemini_direct_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:
|
||||
routes = payload.get("provider_routes") or []
|
||||
gates = payload.get("candidate_gates") or []
|
||||
gaps = payload.get("source_gaps") or []
|
||||
rollups = payload.get("rollups") or {}
|
||||
|
||||
if rollups.get("total_routes") != len(routes):
|
||||
raise ValueError(f"{label}: rollups.total_routes must match provider_routes")
|
||||
if rollups.get("by_kind") != _count_by(routes, "kind"):
|
||||
raise ValueError(f"{label}: rollups.by_kind must match provider_routes")
|
||||
if rollups.get("by_status") != _count_by(routes, "status"):
|
||||
raise ValueError(f"{label}: rollups.by_status must match provider_routes")
|
||||
if rollups.get("by_route_gate") != _count_by(routes, "route_gate"):
|
||||
raise ValueError(f"{label}: rollups.by_route_gate must match provider_routes")
|
||||
|
||||
requiring_action = sorted(
|
||||
route.get("route_id")
|
||||
for route in routes
|
||||
if route.get("status") == "action_required"
|
||||
)
|
||||
if sorted(rollups.get("route_ids_requiring_action") or []) != requiring_action:
|
||||
raise ValueError(f"{label}: rollups.route_ids_requiring_action must match routes")
|
||||
|
||||
approval_gates = sorted(
|
||||
gate.get("gate_id")
|
||||
for gate in gates
|
||||
if gate.get("approval_required") is True
|
||||
)
|
||||
if sorted(rollups.get("candidate_gate_ids_requiring_approval") or []) != approval_gates:
|
||||
raise ValueError(
|
||||
f"{label}: rollups.candidate_gate_ids_requiring_approval must match candidate_gates"
|
||||
)
|
||||
|
||||
if sorted(rollups.get("source_gap_ids") or []) != sorted(gap.get("gap_id") for gap in gaps):
|
||||
raise ValueError(f"{label}: rollups.source_gap_ids must match source_gaps")
|
||||
|
||||
zero_count_fields = {
|
||||
"provider_switch_allowed_count",
|
||||
"paid_api_call_allowed_count",
|
||||
"shadow_or_canary_allowed_count",
|
||||
"runtime_route_change_allowed_count",
|
||||
}
|
||||
non_zero = sorted(field for field in zero_count_fields if rollups.get(field) != 0)
|
||||
if non_zero:
|
||||
raise ValueError(f"{label}: route permission rollup counts must remain 0: {non_zero}")
|
||||
|
||||
|
||||
def _require_route_evidence(payload: dict[str, Any], label: str) -> None:
|
||||
routes = payload.get("provider_routes") or []
|
||||
missing = sorted(
|
||||
route.get("route_id")
|
||||
for route in routes
|
||||
if not route.get("current_policy")
|
||||
or not route.get("provider_order")
|
||||
or not route.get("fallback_policy")
|
||||
or not route.get("evidence_refs")
|
||||
or not route.get("next_action")
|
||||
)
|
||||
if missing:
|
||||
raise ValueError(f"{label}: provider_routes must include policy, order, evidence, next_action: {missing}")
|
||||
|
||||
required_route_ids = {
|
||||
"ai_router_execution_policy",
|
||||
"ollama_global_endpoint_order",
|
||||
"alert_ai_ollama_first_lane",
|
||||
"openclaw_nemo_rca_lane",
|
||||
"nemotron_tool_calling_candidate",
|
||||
"gemini_final_fallback_policy",
|
||||
}
|
||||
present = {route.get("route_id") for route in routes}
|
||||
missing_required = sorted(required_route_ids - present)
|
||||
if missing_required:
|
||||
raise ValueError(f"{label}: missing required provider routes: {missing_required}")
|
||||
|
||||
blocked_candidates = sorted(
|
||||
route.get("route_id")
|
||||
for route in routes
|
||||
if route.get("kind") == "nemotron_candidate" and route.get("status") != "blocked"
|
||||
)
|
||||
if blocked_candidates:
|
||||
raise ValueError(f"{label}: Nemotron candidates must remain blocked: {blocked_candidates}")
|
||||
|
||||
|
||||
def _require_candidate_gates(payload: dict[str, Any], label: str) -> None:
|
||||
gates = payload.get("candidate_gates") or []
|
||||
required_ids = {
|
||||
"provider_switch_gate",
|
||||
"nemotron_replay_gate",
|
||||
"paid_provider_call_gate",
|
||||
}
|
||||
present = {gate.get("gate_id") for gate in gates}
|
||||
missing = sorted(required_ids - present)
|
||||
if missing:
|
||||
raise ValueError(f"{label}: missing required candidate gates: {missing}")
|
||||
|
||||
not_approval_required = sorted(
|
||||
gate.get("gate_id")
|
||||
for gate in gates
|
||||
if gate.get("approval_required") is not True
|
||||
)
|
||||
if not_approval_required:
|
||||
raise ValueError(f"{label}: candidate gates must require approval: {not_approval_required}")
|
||||
|
||||
|
||||
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 = {
|
||||
"provider 切換批准",
|
||||
"production routing change 批准",
|
||||
"Gemini / Claude / NVIDIA 付費呼叫批准",
|
||||
"OpenClaw 取代或降級批准",
|
||||
"Nemotron 進 shadow / canary 批准",
|
||||
"fallback order 修改批准",
|
||||
"Ollama endpoint / ConfigMap 修改批准",
|
||||
"Secret payload 已讀取或可輸出",
|
||||
"外部 live probe 或 benchmark 批准",
|
||||
"workflow / deploy / reload 觸發批准",
|
||||
"runtime execution 授權",
|
||||
}
|
||||
if not required_denials.issubset(must_not_interpret_as):
|
||||
raise ValueError(f"{label}: operator_contract.must_not_interpret_as is missing required denials")
|
||||
|
||||
provider_switch_policy = str(contract.get("provider_switch_policy") or "")
|
||||
if "P1-004" not in provider_switch_policy or "只能盤點與顯示" not in provider_switch_policy:
|
||||
raise ValueError(f"{label}: provider_switch_policy must preserve P1-004 read-only 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",
|
||||
"gemini_api_key_value",
|
||||
"nvidia_api_key_value",
|
||||
"claude_api_key_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
|
||||
@@ -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"] == 83
|
||||
assert data["program_status"]["overall_completion_percent"] == 87
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-003"
|
||||
assert data["program_status"]["next_task_id"] == "P1-004"
|
||||
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"] == 19
|
||||
assert data["rollups"]["by_status"]["done"] == 20
|
||||
assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 20
|
||||
assert data["progress_summary"]["overall_percent"] == 83
|
||||
assert data["progress_summary"]["done_items"] == 19
|
||||
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["item_approval_boundary_rollup"]["items_requiring_explicit_approval"] == [
|
||||
@@ -55,6 +55,12 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho
|
||||
assert p1_003["status"] == "done"
|
||||
assert p1_003["next_review"] == "P1-004"
|
||||
assert "observability_contract_matrix_2026-06-05.json" in p1_003["evidence_refs"][0]
|
||||
p1_004 = next(item for item in data["backlog_items"] if item["item_id"] == "AUTO-P1-004")
|
||||
assert p1_004["status"] == "done"
|
||||
assert p1_004["next_review"] == "P1-005"
|
||||
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_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"]
|
||||
|
||||
@@ -18,13 +18,15 @@ 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-003"
|
||||
assert data["program_status"]["next_task_id"] == "P1-004"
|
||||
assert data["task_approval_boundary_rollup"]["total_tasks"] == len(data["tasks"]) == 29
|
||||
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["task_approval_boundary_rollup"]["by_mode"]["production_change_blocked"] == 1
|
||||
assert data["task_approval_boundary_rollup"]["tasks_requiring_explicit_approval"] == [
|
||||
"P0-001",
|
||||
"P0-004",
|
||||
"P1-004",
|
||||
]
|
||||
assert data["approval_boundaries"]["sdk_installation_allowed"] is False
|
||||
assert data["approval_boundaries"]["paid_api_call_allowed"] is False
|
||||
@@ -41,6 +43,11 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert p1_003["status"] == "done"
|
||||
assert p1_003["approval_boundary"]["mode"] == "read_only_allowed"
|
||||
assert "observability_contract_matrix_2026-06-05.json" in p1_003["output"]
|
||||
p1_004 = next(task for task in data["tasks"] if task["task_id"] == "P1-004")
|
||||
assert p1_004["status"] == "done"
|
||||
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"]
|
||||
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"])
|
||||
@@ -72,3 +79,4 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert any(evidence["evidence_id"] == "runtime_surface_inventory_api" for evidence in data["evidence"])
|
||||
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"])
|
||||
|
||||
345
apps/api/tests/test_ai_provider_route_matrix.py
Normal file
345
apps/api/tests/test_ai_provider_route_matrix.py
Normal file
@@ -0,0 +1,345 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.ai_provider_route_matrix import load_latest_ai_provider_route_matrix
|
||||
|
||||
|
||||
def test_load_latest_ai_provider_route_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 / "ai_provider_route_matrix_2026-06-04.json").write_text(
|
||||
json.dumps(older),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(newer),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
loaded = load_latest_ai_provider_route_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_routes"] == 6
|
||||
assert loaded["operation_boundaries"]["provider_switch_allowed"] is False
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_requires_read_only_mode(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["program_status"]["read_only_mode"] = False
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="read_only_mode"):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_blocks_provider_and_paid_call_changes(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["operation_boundaries"]["provider_switch_allowed"] = True
|
||||
snapshot["operation_boundaries"]["paid_api_call_allowed"] = True
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="operation boundaries"):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_requires_rollup_consistency(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["route_ids_requiring_action"] = []
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="route_ids_requiring_action"):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_requires_nemotron_to_stay_blocked(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["provider_routes"][4]["status"] = "verified"
|
||||
_refresh_rollups(snapshot)
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Nemotron candidates"):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_requires_candidate_gates_to_require_approval(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["candidate_gates"][0]["approval_required"] = False
|
||||
snapshot["rollups"]["candidate_gate_ids_requiring_approval"] = [
|
||||
"nemotron_replay_gate",
|
||||
"paid_provider_call_gate",
|
||||
]
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="candidate gates"):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_requires_operator_denials(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["operator_contract"]["must_not_interpret_as"].remove(
|
||||
"provider 切換批准"
|
||||
)
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="operator_contract"):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_rejects_secret_payload_keys(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["latest_observations"][0]["authorization_header"] = "redacted"
|
||||
(tmp_path / "ai_provider_route_matrix_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="forbidden secret payload key"):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_fails_when_missing(tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_latest_ai_provider_route_matrix(tmp_path)
|
||||
|
||||
|
||||
def _snapshot(
|
||||
*,
|
||||
generated_at: str = "2026-06-05T00:00:00+08:00",
|
||||
completion: int = 100,
|
||||
) -> dict:
|
||||
routes = [
|
||||
_route(
|
||||
"ai_router_execution_policy",
|
||||
"AI Router 執行決策核心",
|
||||
"ai_router_core",
|
||||
"action_required",
|
||||
"route_preserved",
|
||||
),
|
||||
_route(
|
||||
"ollama_global_endpoint_order",
|
||||
"Ollama GCP-A → GCP-B → 111",
|
||||
"ollama_failover",
|
||||
"verified",
|
||||
"route_preserved",
|
||||
),
|
||||
_route(
|
||||
"alert_ai_ollama_first_lane",
|
||||
"告警 AI Ollama-first Lane",
|
||||
"alert_governance_lane",
|
||||
"verified",
|
||||
"route_preserved",
|
||||
),
|
||||
_route(
|
||||
"openclaw_nemo_rca_lane",
|
||||
"OpenClaw Nemo RCA Lane",
|
||||
"openclaw_nemo",
|
||||
"action_required",
|
||||
"review_required",
|
||||
),
|
||||
_route(
|
||||
"nemotron_tool_calling_candidate",
|
||||
"Nemotron Tool Calling 候選",
|
||||
"nemotron_candidate",
|
||||
"blocked",
|
||||
"candidate_blocked",
|
||||
),
|
||||
_route(
|
||||
"gemini_final_fallback_policy",
|
||||
"Gemini Final Fallback",
|
||||
"paid_cloud_fallback",
|
||||
"verified",
|
||||
"route_preserved",
|
||||
),
|
||||
]
|
||||
gates = [
|
||||
_gate("provider_switch_gate", "production_change_blocked"),
|
||||
_gate("nemotron_replay_gate", "blocked_by_evidence"),
|
||||
_gate("paid_provider_call_gate", "cost_approval_required"),
|
||||
]
|
||||
gaps = [
|
||||
{
|
||||
"gap_id": "ai_router_comment_drift",
|
||||
"display_name": "AI Router 註解與現況 drift",
|
||||
"status": "action_required",
|
||||
"severity": "medium",
|
||||
"summary": "舊註解需整理,不能誤讀成現行路由。",
|
||||
"evidence_refs": ["apps/api/src/services/ai_router.py"],
|
||||
"next_action": "只整理 source truth,不改 provider logic。",
|
||||
}
|
||||
]
|
||||
return {
|
||||
"schema_version": "ai_provider_route_matrix_v1",
|
||||
"generated_at": generated_at,
|
||||
"program_status": {
|
||||
"overall_completion_percent": completion,
|
||||
"current_priority": "P1",
|
||||
"current_task_id": "P1-004",
|
||||
"next_task_id": "P1-005",
|
||||
"read_only_mode": True,
|
||||
},
|
||||
"source_refs": ["docs/schemas/ai_provider_route_matrix_v1.schema.json"],
|
||||
"rollups": {
|
||||
"total_routes": len(routes),
|
||||
"by_kind": _count_by(routes, "kind"),
|
||||
"by_status": _count_by(routes, "status"),
|
||||
"by_route_gate": _count_by(routes, "route_gate"),
|
||||
"route_ids_requiring_action": [
|
||||
"ai_router_execution_policy",
|
||||
"openclaw_nemo_rca_lane",
|
||||
],
|
||||
"candidate_gate_ids_requiring_approval": [
|
||||
"nemotron_replay_gate",
|
||||
"paid_provider_call_gate",
|
||||
"provider_switch_gate",
|
||||
],
|
||||
"source_gap_ids": ["ai_router_comment_drift"],
|
||||
"read_only_denials_total": 12,
|
||||
"provider_switch_allowed_count": 0,
|
||||
"paid_api_call_allowed_count": 0,
|
||||
"shadow_or_canary_allowed_count": 0,
|
||||
"runtime_route_change_allowed_count": 0,
|
||||
},
|
||||
"provider_routes": routes,
|
||||
"candidate_gates": gates,
|
||||
"source_gaps": gaps,
|
||||
"latest_observations": [
|
||||
{
|
||||
"observation_id": "provider_matrix_seed",
|
||||
"status": "verified",
|
||||
"summary": "只讀 route matrix seed。",
|
||||
"evidence_refs": ["apps/api/src/services/ai_router.py"],
|
||||
}
|
||||
],
|
||||
"operator_contract": {
|
||||
"display_mode": "read_only_ai_provider_route_matrix",
|
||||
"must_not_interpret_as": [
|
||||
"provider 切換批准",
|
||||
"production routing change 批准",
|
||||
"Gemini / Claude / NVIDIA 付費呼叫批准",
|
||||
"OpenClaw 取代或降級批准",
|
||||
"Nemotron 進 shadow / canary 批准",
|
||||
"fallback order 修改批准",
|
||||
"Ollama endpoint / ConfigMap 修改批准",
|
||||
"Secret payload 已讀取或可輸出",
|
||||
"外部 live probe 或 benchmark 批准",
|
||||
"workflow / deploy / reload 觸發批准",
|
||||
"runtime execution 授權",
|
||||
],
|
||||
"secret_display_policy": "只顯示 env var 名稱。",
|
||||
"provider_switch_policy": "P1-004 只能盤點與顯示;切換 provider 需另行批准。",
|
||||
"cost_policy": "付費呼叫需批准。",
|
||||
"runtime_policy": "active runtime gate 維持 0。",
|
||||
},
|
||||
"operation_boundaries": {
|
||||
"read_only_api_allowed": True,
|
||||
"provider_switch_allowed": False,
|
||||
"production_routing_change_allowed": False,
|
||||
"use_ai_router_toggle_allowed": False,
|
||||
"fallback_order_change_allowed": False,
|
||||
"ollama_endpoint_change_allowed": False,
|
||||
"paid_api_call_allowed": False,
|
||||
"paid_api_frequency_increase_allowed": False,
|
||||
"external_provider_probe_allowed": False,
|
||||
"live_benchmark_allowed": False,
|
||||
"shadow_or_canary_allowed": False,
|
||||
"openclaw_replacement_allowed": False,
|
||||
"nemotron_shadow_allowed": False,
|
||||
"gemini_direct_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": {
|
||||
"provider_switch_approved": False,
|
||||
"production_routing_change_approved": False,
|
||||
"cost_change_approved": False,
|
||||
"shadow_or_canary_approved": False,
|
||||
"external_provider_call_approved": False,
|
||||
"openclaw_replacement_approved": False,
|
||||
"nemotron_replay_approved": False,
|
||||
"secret_access_approved": False,
|
||||
"runtime_execution_approved": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _route(
|
||||
route_id: str,
|
||||
display_name: str,
|
||||
kind: str,
|
||||
status: str,
|
||||
route_gate: str,
|
||||
) -> dict:
|
||||
return {
|
||||
"route_id": route_id,
|
||||
"display_name": display_name,
|
||||
"kind": kind,
|
||||
"status": status,
|
||||
"risk_level": "critical",
|
||||
"route_gate": route_gate,
|
||||
"evidence_status": "committed_source",
|
||||
"current_policy": "只讀盤點 provider route。",
|
||||
"provider_order": ["ollama_gcp_a", "ollama_gcp_b", "ollama_local"],
|
||||
"fallback_policy": "不切 provider。",
|
||||
"evidence_refs": ["apps/api/src/services/ai_router.py"],
|
||||
"next_action": "準備批准包,不改 runtime。",
|
||||
}
|
||||
|
||||
|
||||
def _gate(gate_id: str, status: str) -> dict:
|
||||
return {
|
||||
"gate_id": gate_id,
|
||||
"display_name": gate_id,
|
||||
"status": status,
|
||||
"approval_required": True,
|
||||
"summary": "需人工批准。",
|
||||
"evidence_refs": ["docs/HARD_RULES.md"],
|
||||
"next_action": "維持阻擋。",
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _refresh_rollups(snapshot: dict) -> None:
|
||||
routes = snapshot["provider_routes"]
|
||||
gates = snapshot["candidate_gates"]
|
||||
snapshot["rollups"]["by_status"] = _count_by(routes, "status")
|
||||
snapshot["rollups"]["by_route_gate"] = _count_by(routes, "route_gate")
|
||||
snapshot["rollups"]["route_ids_requiring_action"] = sorted(
|
||||
route["route_id"] for route in routes if route["status"] == "action_required"
|
||||
)
|
||||
snapshot["rollups"]["candidate_gate_ids_requiring_approval"] = sorted(
|
||||
gate["gate_id"] for gate in gates if gate["approval_required"] is True
|
||||
)
|
||||
41
apps/api/tests/test_ai_provider_route_matrix_api.py
Normal file
41
apps/api/tests/test_ai_provider_route_matrix_api.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.agents import router
|
||||
|
||||
|
||||
def test_ai_provider_route_matrix_endpoint_returns_committed_snapshot():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/agents/ai-provider-route-matrix")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "ai_provider_route_matrix_v1"
|
||||
assert data["program_status"]["current_task_id"] == "P1-004"
|
||||
assert data["program_status"]["next_task_id"] == "P1-005"
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["rollups"]["total_routes"] == len(data["provider_routes"]) == 7
|
||||
assert data["rollups"]["provider_switch_allowed_count"] == 0
|
||||
assert data["rollups"]["paid_api_call_allowed_count"] == 0
|
||||
assert data["rollups"]["shadow_or_canary_allowed_count"] == 0
|
||||
assert data["rollups"]["runtime_route_change_allowed_count"] == 0
|
||||
assert data["operation_boundaries"]["provider_switch_allowed"] is False
|
||||
assert data["operation_boundaries"]["paid_api_call_allowed"] is False
|
||||
assert data["operation_boundaries"]["runtime_execution_allowed"] is False
|
||||
assert data["approval_boundaries"]["provider_switch_approved"] is False
|
||||
assert data["approval_boundaries"]["cost_change_approved"] is False
|
||||
assert any(route["route_id"] == "ollama_global_endpoint_order" for route in data["provider_routes"])
|
||||
assert any(route["route_id"] == "alert_ai_ollama_first_lane" for route in data["provider_routes"])
|
||||
nemotron = next(
|
||||
route for route in data["provider_routes"]
|
||||
if route["route_id"] == "nemotron_tool_calling_candidate"
|
||||
)
|
||||
assert nemotron["status"] == "blocked"
|
||||
assert nemotron["route_gate"] == "candidate_blocked"
|
||||
assert data["operator_contract"]["display_mode"] == "read_only_ai_provider_route_matrix"
|
||||
assert "provider 切換批准" in data["operator_contract"]["must_not_interpret_as"]
|
||||
@@ -3307,6 +3307,64 @@
|
||||
"deferred": "延後",
|
||||
"proposal_required": "需提案"
|
||||
}
|
||||
},
|
||||
"providerRoute": {
|
||||
"title": "AI 供應商路由矩陣",
|
||||
"source": "{generated} · {current} → {next}",
|
||||
"gatesTitle": "候選關卡",
|
||||
"gapsTitle": "來源缺口 {count}",
|
||||
"contractTitle": "不可誤讀合約",
|
||||
"metrics": {
|
||||
"routes": "路徑",
|
||||
"actions": "需處置",
|
||||
"gates": "需批准關卡",
|
||||
"blocked": "阻擋候選",
|
||||
"denied": "允許入口"
|
||||
},
|
||||
"map": {
|
||||
"ollama": "Ollama 三層",
|
||||
"ollamaDetail": "GCP-A → GCP-B → 111,Gemini 僅在三層失敗後備援。",
|
||||
"candidate": "候選狀態",
|
||||
"candidateDetail": "Nemotron 仍被 replay / smoke gate 阻擋。",
|
||||
"cost": "費用邊界",
|
||||
"costDetail": "Gemini / Claude / NVIDIA 呼叫、quota 或頻率提升皆需批准。",
|
||||
"safeBoundary": "安全邊界",
|
||||
"safeBoundaryDetail": "供應商切換、shadow/canary、runtime 路由變更入口皆為 0。"
|
||||
},
|
||||
"labels": {
|
||||
"evidence": "證據"
|
||||
},
|
||||
"values": {
|
||||
"ai_router_core": "AI Router 核心",
|
||||
"ollama_failover": "Ollama 備援",
|
||||
"alert_governance_lane": "告警治理路徑",
|
||||
"openclaw_nemo": "OpenClaw / Nemo",
|
||||
"nemotron_candidate": "Nemotron 候選",
|
||||
"paid_cloud_fallback": "付費雲端備援",
|
||||
"legacy_registry": "舊版登錄表",
|
||||
"verified": "已驗證",
|
||||
"action_required": "需處置",
|
||||
"blocked": "阻擋",
|
||||
"route_preserved": "路由保留",
|
||||
"review_required": "需複核",
|
||||
"candidate_blocked": "候選阻擋",
|
||||
"source_mismatch": "來源差異",
|
||||
"committed_source": "已提交來源",
|
||||
"committed_manifest": "已提交 manifest",
|
||||
"blocked_replay_recorded": "Replay 阻擋已記錄",
|
||||
"production_change_blocked": "生產變更阻擋",
|
||||
"blocked_by_evidence": "證據阻擋",
|
||||
"cost_approval_required": "費用需批准",
|
||||
"proposal_required": "需提案",
|
||||
"ollama": "Ollama",
|
||||
"ollama_gcp_a": "Ollama GCP-A",
|
||||
"ollama_gcp_b": "Ollama GCP-B",
|
||||
"ollama_local": "Ollama 111",
|
||||
"nemotron": "Nemotron",
|
||||
"gemini": "Gemini",
|
||||
"claude": "Claude",
|
||||
"nvidia": "NVIDIA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3307,6 +3307,64 @@
|
||||
"deferred": "延後",
|
||||
"proposal_required": "需提案"
|
||||
}
|
||||
},
|
||||
"providerRoute": {
|
||||
"title": "AI 供應商路由矩陣",
|
||||
"source": "{generated} · {current} → {next}",
|
||||
"gatesTitle": "候選關卡",
|
||||
"gapsTitle": "來源缺口 {count}",
|
||||
"contractTitle": "不可誤讀合約",
|
||||
"metrics": {
|
||||
"routes": "路徑",
|
||||
"actions": "需處置",
|
||||
"gates": "需批准關卡",
|
||||
"blocked": "阻擋候選",
|
||||
"denied": "允許入口"
|
||||
},
|
||||
"map": {
|
||||
"ollama": "Ollama 三層",
|
||||
"ollamaDetail": "GCP-A → GCP-B → 111,Gemini 僅在三層失敗後備援。",
|
||||
"candidate": "候選狀態",
|
||||
"candidateDetail": "Nemotron 仍被 replay / smoke gate 阻擋。",
|
||||
"cost": "費用邊界",
|
||||
"costDetail": "Gemini / Claude / NVIDIA 呼叫、quota 或頻率提升皆需批准。",
|
||||
"safeBoundary": "安全邊界",
|
||||
"safeBoundaryDetail": "供應商切換、shadow/canary、runtime 路由變更入口皆為 0。"
|
||||
},
|
||||
"labels": {
|
||||
"evidence": "證據"
|
||||
},
|
||||
"values": {
|
||||
"ai_router_core": "AI Router 核心",
|
||||
"ollama_failover": "Ollama 備援",
|
||||
"alert_governance_lane": "告警治理路徑",
|
||||
"openclaw_nemo": "OpenClaw / Nemo",
|
||||
"nemotron_candidate": "Nemotron 候選",
|
||||
"paid_cloud_fallback": "付費雲端備援",
|
||||
"legacy_registry": "舊版登錄表",
|
||||
"verified": "已驗證",
|
||||
"action_required": "需處置",
|
||||
"blocked": "阻擋",
|
||||
"route_preserved": "路由保留",
|
||||
"review_required": "需複核",
|
||||
"candidate_blocked": "候選阻擋",
|
||||
"source_mismatch": "來源差異",
|
||||
"committed_source": "已提交來源",
|
||||
"committed_manifest": "已提交 manifest",
|
||||
"blocked_replay_recorded": "Replay 阻擋已記錄",
|
||||
"production_change_blocked": "生產變更阻擋",
|
||||
"blocked_by_evidence": "證據阻擋",
|
||||
"cost_approval_required": "費用需批准",
|
||||
"proposal_required": "需提案",
|
||||
"ollama": "Ollama",
|
||||
"ollama_gcp_a": "Ollama GCP-A",
|
||||
"ollama_gcp_b": "Ollama GCP-B",
|
||||
"ollama_local": "Ollama 111",
|
||||
"nemotron": "Nemotron",
|
||||
"gemini": "Gemini",
|
||||
"claude": "Claude",
|
||||
"nvidia": "NVIDIA"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ import { GlassCard } from '@/components/ui/glass-card'
|
||||
import { StatusOrb } from '@/components/ui/status-orb'
|
||||
import {
|
||||
apiClient,
|
||||
type AiProviderRouteMatrixSnapshot,
|
||||
type AiAgentAutomationBacklogSnapshot,
|
||||
type AiAgentAutomationInventorySnapshot,
|
||||
type BackupDrReadinessMatrixSnapshot,
|
||||
@@ -305,6 +306,7 @@ export function AutomationInventoryTab() {
|
||||
const [runtimeSurface, setRuntimeSurface] = useState<RuntimeSurfaceInventorySnapshot | null>(null)
|
||||
const [giteaHealth, setGiteaHealth] = useState<GiteaWorkflowRunnerHealthSnapshot | null>(null)
|
||||
const [observabilityMatrix, setObservabilityMatrix] = useState<ObservabilityContractMatrixSnapshot | null>(null)
|
||||
const [providerRouteMatrix, setProviderRouteMatrix] = useState<AiProviderRouteMatrixSnapshot | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
@@ -320,6 +322,7 @@ export function AutomationInventoryTab() {
|
||||
apiClient.getRuntimeSurfaceInventory(),
|
||||
apiClient.getGiteaWorkflowRunnerHealth(),
|
||||
apiClient.getObservabilityContractMatrix(),
|
||||
apiClient.getAiProviderRouteMatrix(),
|
||||
] as const
|
||||
|
||||
Promise.allSettled(requests)
|
||||
@@ -334,6 +337,7 @@ export function AutomationInventoryTab() {
|
||||
runtimeSurfaceResult,
|
||||
giteaHealthResult,
|
||||
observabilityMatrixResult,
|
||||
providerRouteMatrixResult,
|
||||
] = results
|
||||
|
||||
setSnapshot(inventoryResult.status === 'fulfilled' ? inventoryResult.value : null)
|
||||
@@ -345,6 +349,7 @@ export function AutomationInventoryTab() {
|
||||
setRuntimeSurface(runtimeSurfaceResult.status === 'fulfilled' ? runtimeSurfaceResult.value : null)
|
||||
setGiteaHealth(giteaHealthResult.status === 'fulfilled' ? giteaHealthResult.value : null)
|
||||
setObservabilityMatrix(observabilityMatrixResult.status === 'fulfilled' ? observabilityMatrixResult.value : null)
|
||||
setProviderRouteMatrix(providerRouteMatrixResult.status === 'fulfilled' ? providerRouteMatrixResult.value : null)
|
||||
setError([
|
||||
inventoryResult,
|
||||
backlogResult,
|
||||
@@ -354,6 +359,7 @@ export function AutomationInventoryTab() {
|
||||
offsiteEscrowResult,
|
||||
giteaHealthResult,
|
||||
observabilityMatrixResult,
|
||||
providerRouteMatrixResult,
|
||||
].some(result => result.status === 'rejected'))
|
||||
})
|
||||
.catch(() => setError(true))
|
||||
@@ -489,6 +495,17 @@ export function AutomationInventoryTab() {
|
||||
})
|
||||
}, [observabilityMatrix])
|
||||
|
||||
const visibleProviderRoutes = useMemo(() => {
|
||||
if (!providerRouteMatrix) return []
|
||||
const priority = { action_required: 0, blocked: 1, verified: 2 } as Record<string, number>
|
||||
return [...providerRouteMatrix.provider_routes].sort((a, b) => {
|
||||
const left = priority[a.status] ?? 3
|
||||
const right = priority[b.status] ?? 3
|
||||
if (left !== right) return left - right
|
||||
return a.route_id.localeCompare(b.route_id)
|
||||
})
|
||||
}, [providerRouteMatrix])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 20, display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-kpi-grid">
|
||||
@@ -502,7 +519,7 @@ export function AutomationInventoryTab() {
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix) {
|
||||
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix) {
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<GlassCard variant="subtle" padding="lg">
|
||||
@@ -559,6 +576,16 @@ export function AutomationInventoryTab() {
|
||||
const observabilityProposalCount = observabilityMatrix.rollups.noise_reduction_opportunities_total
|
||||
const observabilityClassificationGaps = observabilityMatrix.rollups.classification_gap_ids.length
|
||||
const observabilityApprovalRequired = observabilityMatrix.rollups.approval_required_opportunity_ids.length
|
||||
const providerRouteActions = providerRouteMatrix.rollups.route_ids_requiring_action.length
|
||||
const providerRouteGateApprovals = providerRouteMatrix.rollups.candidate_gate_ids_requiring_approval.length
|
||||
const providerSourceGaps = providerRouteMatrix.rollups.source_gap_ids.length
|
||||
const providerBlockedRoutes = providerRouteMatrix.rollups.by_status.blocked ?? 0
|
||||
const providerRouteDeniedCount = (
|
||||
providerRouteMatrix.rollups.provider_switch_allowed_count
|
||||
+ providerRouteMatrix.rollups.paid_api_call_allowed_count
|
||||
+ providerRouteMatrix.rollups.shadow_or_canary_allowed_count
|
||||
+ providerRouteMatrix.rollups.runtime_route_change_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
|
||||
@@ -683,6 +710,14 @@ export function AutomationInventoryTab() {
|
||||
}
|
||||
}
|
||||
|
||||
const providerRouteValueLabel = (value: string) => {
|
||||
try {
|
||||
return t(`providerRoute.values.${value}` as never)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="automation-inventory-tab-root" style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 16, minWidth: 0 }}>
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
@@ -1653,6 +1688,172 @@ export function AutomationInventoryTab() {
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
|
||||
<Route size={14} style={{ color: '#d97757' }} />
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('providerRoute.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f' }}>
|
||||
{t('providerRoute.source', {
|
||||
generated: formatDateTime(providerRouteMatrix.generated_at),
|
||||
current: providerRouteMatrix.program_status.current_task_id,
|
||||
next: providerRouteMatrix.program_status.next_task_id,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-provider-kpi-grid">
|
||||
<MetricCard label={t('providerRoute.metrics.routes')} value={providerRouteMatrix.rollups.total_routes} icon={<Route size={16} />} />
|
||||
<MetricCard label={t('providerRoute.metrics.actions')} value={providerRouteActions} tone={providerRouteActions > 0 ? 'warn' : 'ok'} icon={<AlertTriangle size={16} />} />
|
||||
<MetricCard label={t('providerRoute.metrics.gates')} value={providerRouteGateApprovals} tone="warn" icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('providerRoute.metrics.blocked')} value={providerBlockedRoutes} tone={providerBlockedRoutes > 0 ? 'danger' : 'ok'} icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('providerRoute.metrics.denied')} value={providerRouteDeniedCount} tone="ok" icon={<BellOff size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-provider-map-grid">
|
||||
<SummaryTile
|
||||
label={t('providerRoute.map.ollama')}
|
||||
value="3"
|
||||
detail={t('providerRoute.map.ollamaDetail')}
|
||||
tone="ok"
|
||||
icon={<GitBranch size={16} />}
|
||||
/>
|
||||
<SummaryTile
|
||||
label={t('providerRoute.map.candidate')}
|
||||
value={`${providerBlockedRoutes}`}
|
||||
detail={t('providerRoute.map.candidateDetail')}
|
||||
tone="danger"
|
||||
icon={<Target size={16} />}
|
||||
/>
|
||||
<SummaryTile
|
||||
label={t('providerRoute.map.cost')}
|
||||
value={`${providerRouteGateApprovals}`}
|
||||
detail={t('providerRoute.map.costDetail')}
|
||||
tone="warn"
|
||||
icon={<Gauge size={16} />}
|
||||
/>
|
||||
<SummaryTile
|
||||
label={t('providerRoute.map.safeBoundary')}
|
||||
value="0"
|
||||
detail={t('providerRoute.map.safeBoundaryDetail')}
|
||||
tone="ok"
|
||||
icon={<ShieldCheck size={16} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.35fr) minmax(0, 0.65fr)', gap: 12 }} className="automation-inventory-provider-grid">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-provider-route-grid">
|
||||
{visibleProviderRoutes.map(routeItem => (
|
||||
<div key={routeItem.route_id} style={{ padding: 11, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
|
||||
<span style={{
|
||||
fontFamily: 'Syne, sans-serif',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: '#141413',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{routeItem.display_name}
|
||||
</span>
|
||||
<Chip value={providerRouteValueLabel(routeItem.status)} muted={routeItem.status === 'verified'} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={providerRouteValueLabel(routeItem.kind)} />
|
||||
<Chip value={providerRouteValueLabel(routeItem.route_gate)} muted={routeItem.route_gate === 'route_preserved'} />
|
||||
<Chip value={`${t('providerRoute.labels.evidence')}: ${providerRouteValueLabel(routeItem.evidence_status)}`} muted />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
{routeItem.provider_order.map(provider => (
|
||||
<Chip key={`${routeItem.route_id}-${provider}`} value={providerRouteValueLabel(provider)} />
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{routeItem.current_policy}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{routeItem.fallback_policy}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{routeItem.next_action}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={routeItem.evidence_refs[0] ?? t('backupEvidence.noEvidence')} muted />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('providerRoute.gatesTitle')}
|
||||
</span>
|
||||
{providerRouteMatrix.candidate_gates.map(gate => (
|
||||
<div key={gate.gate_id} style={{ display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, fontWeight: 700, color: '#141413', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{gate.display_name}
|
||||
</span>
|
||||
<Chip value={providerRouteValueLabel(gate.status)} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{gate.summary}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('providerRoute.gapsTitle', { count: providerSourceGaps })}
|
||||
</span>
|
||||
{providerRouteMatrix.source_gaps.map(gap => (
|
||||
<div key={gap.gap_id} style={{ display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, fontWeight: 700, color: '#141413', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{gap.display_name}
|
||||
</span>
|
||||
<Chip value={providerRouteValueLabel(gap.status)} muted={gap.status === 'proposal_required'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{gap.summary}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('providerRoute.contractTitle')}
|
||||
</span>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{providerRouteMatrix.operator_contract.provider_switch_policy}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{providerRouteMatrix.operator_contract.cost_policy}
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{providerRouteMatrix.operator_contract.runtime_policy}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
{providerRouteMatrix.operator_contract.must_not_interpret_as.slice(0, 6).map(item => (
|
||||
<Chip key={item} value={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.2fr) minmax(0, 0.8fr)', gap: 12 }} className="automation-inventory-bottom-grid">
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
@@ -1771,6 +1972,10 @@ export function AutomationInventoryTab() {
|
||||
.automation-inventory-observability-map-grid,
|
||||
.automation-inventory-observability-grid,
|
||||
.automation-inventory-observability-surface-grid,
|
||||
.automation-inventory-provider-kpi-grid,
|
||||
.automation-inventory-provider-map-grid,
|
||||
.automation-inventory-provider-grid,
|
||||
.automation-inventory-provider-route-grid,
|
||||
.automation-inventory-bottom-grid,
|
||||
.automation-inventory-task-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
|
||||
@@ -277,6 +277,11 @@ export const apiClient = {
|
||||
return handleResponse<ObservabilityContractMatrixSnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiProviderRouteMatrix() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/ai-provider-route-matrix`)
|
||||
return handleResponse<AiProviderRouteMatrixSnapshot>(res)
|
||||
},
|
||||
|
||||
async getBackupDrTargetInventory() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/backup-dr-target-inventory`)
|
||||
return handleResponse<BackupDrTargetInventorySnapshot>(res)
|
||||
@@ -1025,6 +1030,81 @@ export interface ObservabilityContractMatrixSnapshot {
|
||||
approval_boundaries: Record<string, false>
|
||||
}
|
||||
|
||||
export interface AiProviderRouteMatrixSnapshot {
|
||||
schema_version: 'ai_provider_route_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_routes: number
|
||||
by_kind: Record<string, number>
|
||||
by_status: Record<string, number>
|
||||
by_route_gate: Record<string, number>
|
||||
route_ids_requiring_action: string[]
|
||||
candidate_gate_ids_requiring_approval: string[]
|
||||
source_gap_ids: string[]
|
||||
read_only_denials_total: number
|
||||
provider_switch_allowed_count: number
|
||||
paid_api_call_allowed_count: number
|
||||
shadow_or_canary_allowed_count: number
|
||||
runtime_route_change_allowed_count: number
|
||||
}
|
||||
provider_routes: Array<{
|
||||
route_id: string
|
||||
display_name: string
|
||||
kind: string
|
||||
status: 'verified' | 'action_required' | 'blocked'
|
||||
risk_level: 'low' | 'medium' | 'high' | 'critical'
|
||||
route_gate: string
|
||||
evidence_status: string
|
||||
current_policy: string
|
||||
provider_order: string[]
|
||||
fallback_policy: string
|
||||
evidence_refs: string[]
|
||||
next_action: string
|
||||
}>
|
||||
candidate_gates: Array<{
|
||||
gate_id: string
|
||||
display_name: string
|
||||
status: string
|
||||
approval_required: boolean
|
||||
summary: string
|
||||
evidence_refs: string[]
|
||||
next_action: string
|
||||
}>
|
||||
source_gaps: Array<{
|
||||
gap_id: string
|
||||
display_name: string
|
||||
status: string
|
||||
severity: 'low' | 'medium' | 'high' | 'critical'
|
||||
summary: 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_ai_provider_route_matrix'
|
||||
must_not_interpret_as: string[]
|
||||
secret_display_policy: string
|
||||
provider_switch_policy: string
|
||||
cost_policy: string
|
||||
runtime_policy: string
|
||||
}
|
||||
operation_boundaries: Record<string, boolean>
|
||||
approval_boundaries: Record<string, false>
|
||||
}
|
||||
|
||||
export interface BackupDrTargetInventorySnapshot {
|
||||
schema_version: 'backup_dr_target_inventory_v1'
|
||||
generated_at: string
|
||||
|
||||
Reference in New Issue
Block a user