feat(governance): 新增 AI Provider 路由矩陣
All checks were successful
CD Pipeline / tests (push) Successful in 1m29s
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / build-and-deploy (push) Successful in 3m51s
CD Pipeline / post-deploy-checks (push) Successful in 1m30s

This commit is contained in:
Your Name
2026-06-05 13:28:38 +08:00
parent 77abbe4cf9
commit 45556f8fd1
17 changed files with 1819 additions and 59 deletions

View File

@@ -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],

View 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

View File

@@ -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"]

View File

@@ -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"])

View 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
)

View 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"]

View File

@@ -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 → 111Gemini 僅在三層失敗後備援。",
"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"
}
}
}
},

View File

@@ -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 → 111Gemini 僅在三層失敗後備援。",
"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"
}
}
}
},

View File

@@ -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;

View File

@@ -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