feat(governance): 接入三 Agent 佈建布局
This commit is contained in:
@@ -35,70 +35,73 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from src.core.logging import get_logger
|
||||
from src.core.sse import get_publisher
|
||||
from src.services.agent_market_governance_snapshot import (
|
||||
load_latest_agent_market_governance_snapshot,
|
||||
)
|
||||
from src.services.agent_service import (
|
||||
AgentService,
|
||||
TaskState,
|
||||
get_agent_service,
|
||||
)
|
||||
from src.services.ai_agent_automation_backlog_snapshot import (
|
||||
load_latest_ai_agent_automation_backlog_snapshot,
|
||||
)
|
||||
from src.services.ai_agent_automation_inventory_snapshot import (
|
||||
load_latest_ai_agent_automation_inventory_snapshot,
|
||||
)
|
||||
from src.services.agent_market_governance_snapshot import (
|
||||
load_latest_agent_market_governance_snapshot,
|
||||
from src.services.ai_agent_deployment_layout import (
|
||||
load_latest_ai_agent_deployment_layout,
|
||||
)
|
||||
from src.services.backup_dr_target_inventory import (
|
||||
load_latest_backup_dr_target_inventory,
|
||||
from src.services.ai_provider_route_matrix import (
|
||||
load_latest_ai_provider_route_matrix,
|
||||
)
|
||||
from src.services.backup_dr_readiness_matrix import (
|
||||
load_latest_backup_dr_readiness_matrix,
|
||||
)
|
||||
from src.services.backup_dr_target_inventory import (
|
||||
load_latest_backup_dr_target_inventory,
|
||||
)
|
||||
from src.services.backup_notification_policy import (
|
||||
load_latest_backup_notification_policy,
|
||||
)
|
||||
from src.services.backup_restore_drill_approval_package_template import (
|
||||
load_latest_backup_restore_drill_approval_package_template,
|
||||
)
|
||||
from src.services.offsite_escrow_readiness_status import (
|
||||
load_latest_offsite_escrow_readiness_status,
|
||||
)
|
||||
from src.services.runtime_surface_inventory import (
|
||||
load_latest_runtime_surface_inventory,
|
||||
)
|
||||
from src.services.gitea_workflow_runner_health import (
|
||||
load_latest_gitea_workflow_runner_health,
|
||||
)
|
||||
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.service_health_gap_matrix import (
|
||||
load_latest_service_health_gap_matrix,
|
||||
)
|
||||
from src.services.service_health_failure_notification_policy import (
|
||||
load_latest_service_health_failure_notification_policy,
|
||||
)
|
||||
from src.services.package_supply_chain_inventory import (
|
||||
load_latest_package_supply_chain_inventory,
|
||||
)
|
||||
from src.services.javascript_package_inventory import (
|
||||
load_latest_javascript_package_inventory,
|
||||
)
|
||||
from src.services.docker_build_surface_inventory import (
|
||||
load_latest_docker_build_surface_inventory,
|
||||
from src.services.dependency_drift_check_plan import (
|
||||
load_latest_dependency_drift_check_plan,
|
||||
)
|
||||
from src.services.dependency_risk_policy import (
|
||||
load_latest_dependency_risk_policy,
|
||||
)
|
||||
from src.services.dependency_drift_check_plan import (
|
||||
load_latest_dependency_drift_check_plan,
|
||||
)
|
||||
from src.services.dependency_upgrade_approval_package_template import (
|
||||
load_latest_dependency_upgrade_approval_package_template,
|
||||
)
|
||||
from src.services.agent_service import (
|
||||
AgentService,
|
||||
TaskState,
|
||||
get_agent_service,
|
||||
from src.services.docker_build_surface_inventory import (
|
||||
load_latest_docker_build_surface_inventory,
|
||||
)
|
||||
from src.services.gitea_workflow_runner_health import (
|
||||
load_latest_gitea_workflow_runner_health,
|
||||
)
|
||||
from src.services.javascript_package_inventory import (
|
||||
load_latest_javascript_package_inventory,
|
||||
)
|
||||
from src.services.observability_contract_matrix import (
|
||||
load_latest_observability_contract_matrix,
|
||||
)
|
||||
from src.services.offsite_escrow_readiness_status import (
|
||||
load_latest_offsite_escrow_readiness_status,
|
||||
)
|
||||
from src.services.package_supply_chain_inventory import (
|
||||
load_latest_package_supply_chain_inventory,
|
||||
)
|
||||
from src.services.runtime_surface_inventory import (
|
||||
load_latest_runtime_surface_inventory,
|
||||
)
|
||||
from src.services.service_health_failure_notification_policy import (
|
||||
load_latest_service_health_failure_notification_policy,
|
||||
)
|
||||
from src.services.service_health_gap_matrix import (
|
||||
load_latest_service_health_gap_matrix,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/agents", tags=["Agent Teams"])
|
||||
@@ -494,6 +497,33 @@ async def get_automation_backlog_snapshot() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent-deployment-layout",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 AI Agent 佈建布局快照",
|
||||
description=(
|
||||
"讀取最新已提交的 OpenClaw / Hermes / NemoTron 佈建布局快照;"
|
||||
"此端點不部署 Agent、不呼叫外部模型、不送 Telegram、不碰 DB/Redis、不讀 Secret payload、"
|
||||
"不批准 SDK/API/shadow/canary/生產路由或主機變更。"
|
||||
),
|
||||
)
|
||||
async def get_agent_deployment_layout() -> dict[str, Any]:
|
||||
"""Return the latest read-only AI Agent deployment layout snapshot."""
|
||||
try:
|
||||
return await asyncio.to_thread(load_latest_ai_agent_deployment_layout)
|
||||
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_agent_deployment_layout_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="AI Agent 佈建布局快照無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/runtime-surface-inventory",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
135
apps/api/src/services/ai_agent_deployment_layout.py
Normal file
135
apps/api/src/services/ai_agent_deployment_layout.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
AI Agent deployment layout snapshot.
|
||||
|
||||
Loads the latest committed, read-only layout for OpenClaw, Hermes, and
|
||||
NemoTron across hosts, packages, tools, services, projects, web surfaces,
|
||||
learning loops, and Telegram notification boundaries. This module never
|
||||
deploys agents, sends Telegram messages, calls providers, or approves writes.
|
||||
"""
|
||||
|
||||
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_agent_deployment_layout_*.json"
|
||||
_SCHEMA_VERSION = "ai_agent_deployment_layout_v1"
|
||||
|
||||
|
||||
def load_latest_ai_agent_deployment_layout(
|
||||
evaluations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the newest committed AI Agent deployment layout snapshot."""
|
||||
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
|
||||
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"no AI Agent deployment layout 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_layout(payload, str(latest))
|
||||
_require_rollup_consistency(payload, str(latest))
|
||||
_require_frontend_redaction(payload, str(latest))
|
||||
_require_target_boundaries(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_layout(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")
|
||||
if program_status.get("deployment_authority") != "layout_only_no_runtime_deploy":
|
||||
raise ValueError(f"{label}: deployment_authority must stay layout_only_no_runtime_deploy")
|
||||
|
||||
boundaries = payload.get("approval_boundaries") or {}
|
||||
blocked_flags = {
|
||||
"sdk_installation_allowed",
|
||||
"paid_api_call_allowed",
|
||||
"shadow_or_canary_allowed",
|
||||
"production_routing_allowed",
|
||||
"destructive_operation_allowed",
|
||||
"secret_plaintext_allowed",
|
||||
"autonomous_host_mutation_allowed",
|
||||
"telegram_direct_send_allowed",
|
||||
}
|
||||
allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
|
||||
if allowed:
|
||||
raise ValueError(f"{label}: approval boundaries must remain false: {allowed}")
|
||||
|
||||
|
||||
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
|
||||
targets = payload.get("deployment_targets") or []
|
||||
rollups = payload.get("rollups") or {}
|
||||
|
||||
if rollups.get("total_targets") != len(targets):
|
||||
raise ValueError(f"{label}: rollups.total_targets must match deployment_targets")
|
||||
if rollups.get("by_domain") != _count_by(targets, "domain_id"):
|
||||
raise ValueError(f"{label}: rollups.by_domain must match deployment_targets")
|
||||
if rollups.get("by_primary_agent") != _count_by(targets, "primary_agent"):
|
||||
raise ValueError(f"{label}: rollups.by_primary_agent must match deployment_targets")
|
||||
if rollups.get("by_deployment_state") != _count_by(targets, "deployment_state"):
|
||||
raise ValueError(f"{label}: rollups.by_deployment_state must match deployment_targets")
|
||||
if rollups.get("by_telegram_policy") != _count_by(targets, "telegram_policy"):
|
||||
raise ValueError(f"{label}: rollups.by_telegram_policy must match deployment_targets")
|
||||
|
||||
blocked_target_ids = sorted(
|
||||
target.get("target_id")
|
||||
for target in targets
|
||||
if target.get("deployment_state") == "blocked_by_gate"
|
||||
or target.get("automation_level") == "blocked"
|
||||
)
|
||||
if sorted(rollups.get("blocked_target_ids") or []) != blocked_target_ids:
|
||||
raise ValueError(f"{label}: rollups.blocked_target_ids must match blocked targets")
|
||||
|
||||
|
||||
def _require_frontend_redaction(payload: dict[str, Any], label: str) -> None:
|
||||
redaction = ((payload.get("collaboration_contract") or {}).get("frontend_redaction") or {})
|
||||
if redaction.get("operator_conversation_display_allowed") is not False:
|
||||
raise ValueError(f"{label}: operator conversation display must stay false")
|
||||
if redaction.get("agent_private_reasoning_display_allowed") is not False:
|
||||
raise ValueError(f"{label}: agent private reasoning display must stay false")
|
||||
|
||||
|
||||
def _require_target_boundaries(payload: dict[str, Any], label: str) -> None:
|
||||
targets = payload.get("deployment_targets") or []
|
||||
missing = [
|
||||
target.get("target_id")
|
||||
for target in targets
|
||||
if not target.get("approval_gate")
|
||||
or not target.get("telegram_policy")
|
||||
or not target.get("communication_channels")
|
||||
]
|
||||
if missing:
|
||||
raise ValueError(f"{label}: deployment targets missing boundary fields: {sorted(missing)}")
|
||||
|
||||
invalid_nemotron_runtime = [
|
||||
target.get("target_id")
|
||||
for target in targets
|
||||
if target.get("primary_agent") == "nemotron"
|
||||
and target.get("automation_level") not in {"observe_only", "blocked"}
|
||||
]
|
||||
if invalid_nemotron_runtime:
|
||||
raise ValueError(f"{label}: Nemotron targets must stay observe_only or blocked")
|
||||
|
||||
|
||||
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
|
||||
186
apps/api/tests/test_ai_agent_deployment_layout.py
Normal file
186
apps/api/tests/test_ai_agent_deployment_layout.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.ai_agent_deployment_layout import (
|
||||
load_latest_ai_agent_deployment_layout,
|
||||
)
|
||||
|
||||
|
||||
def test_load_latest_ai_agent_deployment_layout_reads_committed_snapshot():
|
||||
data = load_latest_ai_agent_deployment_layout()
|
||||
|
||||
assert data["schema_version"] == "ai_agent_deployment_layout_v1"
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["deployment_authority"] == "layout_only_no_runtime_deploy"
|
||||
assert data["rollups"]["total_targets"] == len(data["deployment_targets"]) == 42
|
||||
assert data["rollups"]["by_primary_agent"]["openclaw"] == 17
|
||||
assert data["rollups"]["by_primary_agent"]["hermes"] == 23
|
||||
assert data["rollups"]["by_primary_agent"]["nemotron"] == 2
|
||||
assert data["rollups"]["blocked_target_ids"] == [
|
||||
"host_120",
|
||||
"nemotron_replay_pipeline",
|
||||
]
|
||||
assert data["approval_boundaries"]["telegram_direct_send_allowed"] is False
|
||||
assert data["approval_boundaries"]["production_routing_allowed"] is False
|
||||
assert data["collaboration_contract"]["frontend_redaction"]["operator_conversation_display_allowed"] is False
|
||||
|
||||
nemotron_targets = [
|
||||
target for target in data["deployment_targets"] if target["primary_agent"] == "nemotron"
|
||||
]
|
||||
assert {target["automation_level"] for target in nemotron_targets} <= {"observe_only", "blocked"}
|
||||
|
||||
|
||||
def test_load_latest_ai_agent_deployment_layout_rejects_runtime_authority(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["approval_boundaries"]["production_routing_allowed"] = True
|
||||
(tmp_path / "ai_agent_deployment_layout_2026-06-11.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="approval boundaries"):
|
||||
load_latest_ai_agent_deployment_layout(tmp_path)
|
||||
|
||||
|
||||
def test_load_latest_ai_agent_deployment_layout_rejects_rollup_mismatch(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["by_primary_agent"]["openclaw"] = 99
|
||||
(tmp_path / "ai_agent_deployment_layout_2026-06-11.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="by_primary_agent"):
|
||||
load_latest_ai_agent_deployment_layout(tmp_path)
|
||||
|
||||
|
||||
def test_load_latest_ai_agent_deployment_layout_rejects_nemotron_runtime(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["deployment_targets"][0]["primary_agent"] = "nemotron"
|
||||
snapshot["deployment_targets"][0]["automation_level"] = "hitl_execute_after_approval"
|
||||
snapshot["rollups"]["by_primary_agent"] = {"nemotron": 1}
|
||||
(tmp_path / "ai_agent_deployment_layout_2026-06-11.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Nemotron targets"):
|
||||
load_latest_ai_agent_deployment_layout(tmp_path)
|
||||
|
||||
|
||||
def _snapshot() -> dict:
|
||||
return {
|
||||
"schema_version": "ai_agent_deployment_layout_v1",
|
||||
"generated_at": "2026-06-11T18:20:00+08:00",
|
||||
"program_status": {
|
||||
"overall_completion_percent": 10,
|
||||
"current_priority": "P1",
|
||||
"current_task_id": "P1-401",
|
||||
"next_task_id": "P1-402",
|
||||
"read_only_mode": True,
|
||||
"deployment_authority": "layout_only_no_runtime_deploy",
|
||||
},
|
||||
"agent_contracts": [
|
||||
{
|
||||
"agent_id": "openclaw",
|
||||
"display_name": "OpenClaw",
|
||||
"primary_specialty": "生產仲裁",
|
||||
"deployment_lane": "production_decision_core",
|
||||
"allowed_autonomy": ["只讀診斷"],
|
||||
"must_delegate_to": ["Hermes"],
|
||||
"blocked_actions": ["production_write"],
|
||||
"learning_scope": ["incident"],
|
||||
},
|
||||
{
|
||||
"agent_id": "hermes",
|
||||
"display_name": "Hermes",
|
||||
"primary_specialty": "治理",
|
||||
"deployment_lane": "governance",
|
||||
"allowed_autonomy": ["只讀盤點"],
|
||||
"must_delegate_to": ["OpenClaw"],
|
||||
"blocked_actions": ["production_write"],
|
||||
"learning_scope": ["docs"],
|
||||
},
|
||||
{
|
||||
"agent_id": "nemotron",
|
||||
"display_name": "NemoTron",
|
||||
"primary_specialty": "離線評估",
|
||||
"deployment_lane": "offline_evaluator",
|
||||
"allowed_autonomy": ["smoke"],
|
||||
"must_delegate_to": ["OpenClaw"],
|
||||
"blocked_actions": ["production_route"],
|
||||
"learning_scope": ["replay"],
|
||||
},
|
||||
],
|
||||
"domains": [
|
||||
{
|
||||
"domain_id": "hosts",
|
||||
"display_name": "主機",
|
||||
"description": "測試主機",
|
||||
}
|
||||
],
|
||||
"deployment_targets": [
|
||||
{
|
||||
"target_id": "host_110",
|
||||
"domain_id": "hosts",
|
||||
"display_name": "110",
|
||||
"target_type": "host",
|
||||
"primary_agent": "openclaw",
|
||||
"supporting_agents": ["hermes"],
|
||||
"deployment_state": "active_governed",
|
||||
"automation_level": "observe_only",
|
||||
"capabilities": ["monitor"],
|
||||
"telegram_policy": "failure_only",
|
||||
"learning_inputs": ["metrics"],
|
||||
"communication_channels": ["Prometheus"],
|
||||
"approval_gate": "read_only_allowed",
|
||||
"evidence_refs": ["infra/ansible/inventory/hosts.yml"],
|
||||
"next_action": "觀察。",
|
||||
}
|
||||
],
|
||||
"collaboration_contract": {
|
||||
"message_bus": "Redis Streams",
|
||||
"audit_trail": "AgentSession",
|
||||
"handoff_rules": ["OpenClaw 仲裁"],
|
||||
"frontend_redaction": {
|
||||
"operator_conversation_display_allowed": False,
|
||||
"agent_private_reasoning_display_allowed": False,
|
||||
"display_policy": "只顯示摘要。",
|
||||
},
|
||||
},
|
||||
"learning_contract": {
|
||||
"event_sources": ["incident"],
|
||||
"feedback_loops": ["trust"],
|
||||
"growth_metrics": ["precision"],
|
||||
"retention_policy": "redacted evidence only",
|
||||
},
|
||||
"telegram_contract": {
|
||||
"primary_gateway": "telegram_gateway.py",
|
||||
"bot_roles": ["OpenClaw"],
|
||||
"notification_classes": ["failure"],
|
||||
"redaction_policy": "redacted",
|
||||
"e2e_validation": "ADR-035",
|
||||
},
|
||||
"rollups": {
|
||||
"total_targets": 1,
|
||||
"by_domain": {"hosts": 1},
|
||||
"by_primary_agent": {"openclaw": 1},
|
||||
"by_deployment_state": {"active_governed": 1},
|
||||
"by_telegram_policy": {"failure_only": 1},
|
||||
"blocked_target_ids": [],
|
||||
"approval_required_target_ids": [],
|
||||
},
|
||||
"approval_boundaries": {
|
||||
"sdk_installation_allowed": False,
|
||||
"paid_api_call_allowed": False,
|
||||
"shadow_or_canary_allowed": False,
|
||||
"production_routing_allowed": False,
|
||||
"destructive_operation_allowed": False,
|
||||
"secret_plaintext_allowed": False,
|
||||
"autonomous_host_mutation_allowed": False,
|
||||
"telegram_direct_send_allowed": False,
|
||||
},
|
||||
}
|
||||
31
apps/api/tests/test_ai_agent_deployment_layout_api.py
Normal file
31
apps/api/tests/test_ai_agent_deployment_layout_api.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.agents import router
|
||||
|
||||
|
||||
def test_ai_agent_deployment_layout_endpoint_returns_committed_snapshot():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/agents/agent-deployment-layout")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "ai_agent_deployment_layout_v1"
|
||||
assert data["program_status"]["overall_completion_percent"] == 45
|
||||
assert data["program_status"]["current_task_id"] == "P1-402"
|
||||
assert data["program_status"]["next_task_id"] == "P1-403"
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["rollups"]["total_targets"] == 42
|
||||
assert data["rollups"]["by_domain"]["hosts"] == 6
|
||||
assert data["rollups"]["by_domain"]["websites"] == 5
|
||||
assert data["rollups"]["by_primary_agent"]["nemotron"] == 2
|
||||
assert data["approval_boundaries"]["telegram_direct_send_allowed"] is False
|
||||
assert data["approval_boundaries"]["autonomous_host_mutation_allowed"] is False
|
||||
assert data["collaboration_contract"]["frontend_redaction"]["operator_conversation_display_allowed"] is False
|
||||
assert any(target["target_id"] == "telegram_gateway" for target in data["deployment_targets"])
|
||||
assert any(target["target_id"] == "nemotron_replay_pipeline" for target in data["deployment_targets"])
|
||||
@@ -2965,6 +2965,72 @@
|
||||
"taskBoundaries": "任務邊界",
|
||||
"explicitApprovalTasks": "需明確批准"
|
||||
},
|
||||
"deploymentLayout": {
|
||||
"title": "OpenClaw / Hermes / NemoTron 佈建布局",
|
||||
"source": "{generated} · {current} → {next}",
|
||||
"targetsTitle": "優先佈建目標",
|
||||
"targetsShown": "顯示 {count}/{total}",
|
||||
"agentContractTitle": "Agent 專長分工",
|
||||
"telegramTitle": "Telegram Bot 告警合約",
|
||||
"telegramDetail": "統一入口 {gateway},通知類別 {classes} 種;Agent 不直接持有 token,也不直接發送。",
|
||||
"learningTitle": "主動學習與協作",
|
||||
"learningDetail": "事件來源 {sources}、回饋迴圈 {loops}、成長指標 {metrics};先落只讀證據,再走批准關卡。",
|
||||
"redactionLocked": "前端不顯示對話內容",
|
||||
"directSendBlocked": "Telegram 直送禁止",
|
||||
"frontendSafe": "只顯示狀態、證據與邊界",
|
||||
"metrics": {
|
||||
"targets": "佈建目標",
|
||||
"openclaw": "OpenClaw",
|
||||
"hermes": "Hermes",
|
||||
"nemotron": "NemoTron",
|
||||
"approval": "需批准",
|
||||
"blocked": "阻擋"
|
||||
},
|
||||
"labels": {
|
||||
"domain": "領域",
|
||||
"primary": "主責",
|
||||
"support": "協作",
|
||||
"telegram": "告警",
|
||||
"automation": "自動化",
|
||||
"evidence": "證據",
|
||||
"bus": "協作匯流"
|
||||
},
|
||||
"agents": {
|
||||
"openclaw": "OpenClaw",
|
||||
"hermes": "Hermes",
|
||||
"nemotron": "NemoTron"
|
||||
},
|
||||
"domains": {
|
||||
"hosts": "主機",
|
||||
"packages": "套件",
|
||||
"tools": "工具",
|
||||
"services": "服務",
|
||||
"projects": "專案",
|
||||
"websites": "網站前後台",
|
||||
"learning": "學習協作"
|
||||
},
|
||||
"deploymentStates": {
|
||||
"active_governed": "治理中",
|
||||
"read_only_layout": "只讀布局",
|
||||
"blocked_by_gate": "關卡阻擋",
|
||||
"planned": "規劃中",
|
||||
"candidate_only": "候選"
|
||||
},
|
||||
"automationLevels": {
|
||||
"observe_only": "只讀觀察",
|
||||
"prepare_only": "只準備提案",
|
||||
"dry_run_only": "僅乾跑",
|
||||
"hitl_execute_after_approval": "批准後 HITL 執行",
|
||||
"blocked": "阻擋"
|
||||
},
|
||||
"telegramPolicies": {
|
||||
"failure_only": "僅失敗",
|
||||
"action_required": "需處置",
|
||||
"approval_required": "需批准",
|
||||
"daily_summary_only": "僅日報",
|
||||
"no_direct_notify": "不直送"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"title": "決策指揮摘要",
|
||||
"mode": "只讀決策支援",
|
||||
|
||||
@@ -2965,6 +2965,72 @@
|
||||
"taskBoundaries": "任務邊界",
|
||||
"explicitApprovalTasks": "需明確批准"
|
||||
},
|
||||
"deploymentLayout": {
|
||||
"title": "OpenClaw / Hermes / NemoTron 佈建布局",
|
||||
"source": "{generated} · {current} → {next}",
|
||||
"targetsTitle": "優先佈建目標",
|
||||
"targetsShown": "顯示 {count}/{total}",
|
||||
"agentContractTitle": "Agent 專長分工",
|
||||
"telegramTitle": "Telegram Bot 告警合約",
|
||||
"telegramDetail": "統一入口 {gateway},通知類別 {classes} 種;Agent 不直接持有 token,也不直接發送。",
|
||||
"learningTitle": "主動學習與協作",
|
||||
"learningDetail": "事件來源 {sources}、回饋迴圈 {loops}、成長指標 {metrics};先落只讀證據,再走批准關卡。",
|
||||
"redactionLocked": "前端不顯示對話內容",
|
||||
"directSendBlocked": "Telegram 直送禁止",
|
||||
"frontendSafe": "只顯示狀態、證據與邊界",
|
||||
"metrics": {
|
||||
"targets": "佈建目標",
|
||||
"openclaw": "OpenClaw",
|
||||
"hermes": "Hermes",
|
||||
"nemotron": "NemoTron",
|
||||
"approval": "需批准",
|
||||
"blocked": "阻擋"
|
||||
},
|
||||
"labels": {
|
||||
"domain": "領域",
|
||||
"primary": "主責",
|
||||
"support": "協作",
|
||||
"telegram": "告警",
|
||||
"automation": "自動化",
|
||||
"evidence": "證據",
|
||||
"bus": "協作匯流"
|
||||
},
|
||||
"agents": {
|
||||
"openclaw": "OpenClaw",
|
||||
"hermes": "Hermes",
|
||||
"nemotron": "NemoTron"
|
||||
},
|
||||
"domains": {
|
||||
"hosts": "主機",
|
||||
"packages": "套件",
|
||||
"tools": "工具",
|
||||
"services": "服務",
|
||||
"projects": "專案",
|
||||
"websites": "網站前後台",
|
||||
"learning": "學習協作"
|
||||
},
|
||||
"deploymentStates": {
|
||||
"active_governed": "治理中",
|
||||
"read_only_layout": "只讀布局",
|
||||
"blocked_by_gate": "關卡阻擋",
|
||||
"planned": "規劃中",
|
||||
"candidate_only": "候選"
|
||||
},
|
||||
"automationLevels": {
|
||||
"observe_only": "只讀觀察",
|
||||
"prepare_only": "只準備提案",
|
||||
"dry_run_only": "僅乾跑",
|
||||
"hitl_execute_after_approval": "批准後 HITL 執行",
|
||||
"blocked": "阻擋"
|
||||
},
|
||||
"telegramPolicies": {
|
||||
"failure_only": "僅失敗",
|
||||
"action_required": "需處置",
|
||||
"approval_required": "需批准",
|
||||
"daily_summary_only": "僅日報",
|
||||
"no_direct_notify": "不直送"
|
||||
}
|
||||
},
|
||||
"overview": {
|
||||
"title": "決策指揮摘要",
|
||||
"mode": "只讀決策支援",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { GlassCard } from '@/components/ui/glass-card'
|
||||
import { StatusOrb } from '@/components/ui/status-orb'
|
||||
import {
|
||||
apiClient,
|
||||
type AiAgentDeploymentLayoutSnapshot,
|
||||
type AiProviderRouteMatrixSnapshot,
|
||||
type AiAgentAutomationBacklogSnapshot,
|
||||
type AiAgentAutomationInventorySnapshot,
|
||||
@@ -311,6 +312,7 @@ export function AutomationInventoryTab() {
|
||||
const [giteaHealth, setGiteaHealth] = useState<GiteaWorkflowRunnerHealthSnapshot | null>(null)
|
||||
const [observabilityMatrix, setObservabilityMatrix] = useState<ObservabilityContractMatrixSnapshot | null>(null)
|
||||
const [providerRouteMatrix, setProviderRouteMatrix] = useState<AiProviderRouteMatrixSnapshot | null>(null)
|
||||
const [deploymentLayout, setDeploymentLayout] = useState<AiAgentDeploymentLayoutSnapshot | null>(null)
|
||||
const [serviceHealthGapMatrix, setServiceHealthGapMatrix] = useState<ServiceHealthGapMatrixSnapshot | null>(null)
|
||||
const [serviceHealthNotificationPolicy, setServiceHealthNotificationPolicy] = useState<ServiceHealthFailureNotificationPolicySnapshot | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -329,6 +331,7 @@ export function AutomationInventoryTab() {
|
||||
apiClient.getGiteaWorkflowRunnerHealth(),
|
||||
apiClient.getObservabilityContractMatrix(),
|
||||
apiClient.getAiProviderRouteMatrix(),
|
||||
apiClient.getAiAgentDeploymentLayout(),
|
||||
apiClient.getServiceHealthGapMatrix(),
|
||||
apiClient.getServiceHealthFailureNotificationPolicy(),
|
||||
] as const
|
||||
@@ -346,6 +349,7 @@ export function AutomationInventoryTab() {
|
||||
giteaHealthResult,
|
||||
observabilityMatrixResult,
|
||||
providerRouteMatrixResult,
|
||||
deploymentLayoutResult,
|
||||
serviceHealthGapMatrixResult,
|
||||
serviceHealthNotificationPolicyResult,
|
||||
] = results
|
||||
@@ -360,6 +364,7 @@ export function AutomationInventoryTab() {
|
||||
setGiteaHealth(giteaHealthResult.status === 'fulfilled' ? giteaHealthResult.value : null)
|
||||
setObservabilityMatrix(observabilityMatrixResult.status === 'fulfilled' ? observabilityMatrixResult.value : null)
|
||||
setProviderRouteMatrix(providerRouteMatrixResult.status === 'fulfilled' ? providerRouteMatrixResult.value : null)
|
||||
setDeploymentLayout(deploymentLayoutResult.status === 'fulfilled' ? deploymentLayoutResult.value : null)
|
||||
setServiceHealthGapMatrix(serviceHealthGapMatrixResult.status === 'fulfilled' ? serviceHealthGapMatrixResult.value : null)
|
||||
setServiceHealthNotificationPolicy(serviceHealthNotificationPolicyResult.status === 'fulfilled' ? serviceHealthNotificationPolicyResult.value : null)
|
||||
setError([
|
||||
@@ -372,6 +377,7 @@ export function AutomationInventoryTab() {
|
||||
giteaHealthResult,
|
||||
observabilityMatrixResult,
|
||||
providerRouteMatrixResult,
|
||||
deploymentLayoutResult,
|
||||
serviceHealthGapMatrixResult,
|
||||
serviceHealthNotificationPolicyResult,
|
||||
].some(result => result.status === 'rejected'))
|
||||
@@ -409,6 +415,35 @@ export function AutomationInventoryTab() {
|
||||
.filter(group => group.items.length > 0)
|
||||
}, [backlog])
|
||||
|
||||
const visibleDeploymentTargets = useMemo(() => {
|
||||
if (!deploymentLayout) return []
|
||||
const statePriority = {
|
||||
blocked_by_gate: 0,
|
||||
active_governed: 1,
|
||||
read_only_layout: 2,
|
||||
planned: 3,
|
||||
candidate_only: 4,
|
||||
} as Record<string, number>
|
||||
const telegramPriority = {
|
||||
approval_required: 0,
|
||||
action_required: 1,
|
||||
failure_only: 2,
|
||||
daily_summary_only: 3,
|
||||
no_direct_notify: 4,
|
||||
} as Record<string, number>
|
||||
return [...deploymentLayout.deployment_targets]
|
||||
.sort((a, b) => {
|
||||
const stateLeft = statePriority[a.deployment_state] ?? 5
|
||||
const stateRight = statePriority[b.deployment_state] ?? 5
|
||||
if (stateLeft !== stateRight) return stateLeft - stateRight
|
||||
const telegramLeft = telegramPriority[a.telegram_policy] ?? 5
|
||||
const telegramRight = telegramPriority[b.telegram_policy] ?? 5
|
||||
if (telegramLeft !== telegramRight) return telegramLeft - telegramRight
|
||||
return a.target_id.localeCompare(b.target_id)
|
||||
})
|
||||
.slice(0, 12)
|
||||
}, [deploymentLayout])
|
||||
|
||||
const visibleReadinessRows = useMemo(() => {
|
||||
if (!backupReadiness) return []
|
||||
const priority = { blocked: 0, action_required: 1, deferred: 2, ready: 3 } as Record<string, number>
|
||||
@@ -552,7 +587,7 @@ export function AutomationInventoryTab() {
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
|
||||
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow || !giteaHealth || !observabilityMatrix || !providerRouteMatrix || !deploymentLayout || !serviceHealthGapMatrix || !serviceHealthNotificationPolicy) {
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<GlassCard variant="subtle" padding="lg">
|
||||
@@ -619,6 +654,12 @@ export function AutomationInventoryTab() {
|
||||
+ providerRouteMatrix.rollups.shadow_or_canary_allowed_count
|
||||
+ providerRouteMatrix.rollups.runtime_route_change_allowed_count
|
||||
)
|
||||
const deploymentLayoutTargets = deploymentLayout.rollups.total_targets
|
||||
const deploymentLayoutOpenClawTargets = deploymentLayout.rollups.by_primary_agent.openclaw ?? 0
|
||||
const deploymentLayoutHermesTargets = deploymentLayout.rollups.by_primary_agent.hermes ?? 0
|
||||
const deploymentLayoutNemotronTargets = deploymentLayout.rollups.by_primary_agent.nemotron ?? 0
|
||||
const deploymentLayoutBlockedTargets = deploymentLayout.rollups.blocked_target_ids.length
|
||||
const deploymentLayoutApprovalTargets = deploymentLayout.rollups.approval_required_target_ids.length
|
||||
const serviceHealthActions = serviceHealthGapMatrix.rollups.target_ids_requiring_action.length
|
||||
const serviceHealthStaleGaps = serviceHealthGapMatrix.rollups.stale_endpoint_ids.length
|
||||
const serviceHealthOperatorReviews = serviceHealthGapMatrix.rollups.health_gap_ids.length
|
||||
@@ -771,6 +812,14 @@ export function AutomationInventoryTab() {
|
||||
}
|
||||
}
|
||||
|
||||
const deploymentLayoutValueLabel = (group: string, value: string) => {
|
||||
try {
|
||||
return t(`deploymentLayout.${group}.${value}` as never)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const serviceHealthValueLabel = (value: string) => {
|
||||
try {
|
||||
return t(`serviceHealth.values.${value}` as never)
|
||||
@@ -942,6 +991,124 @@ export function AutomationInventoryTab() {
|
||||
<MetricCard label={t('metrics.explicitApprovalTasks')} value={explicitApprovalTaskCount} tone={explicitApprovalTaskCount > 0 ? 'warn' : 'ok'} icon={<ShieldCheck size={16} />} />
|
||||
</div>
|
||||
|
||||
<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('deploymentLayout.title')}
|
||||
</span>
|
||||
</div>
|
||||
<Chip
|
||||
value={t('deploymentLayout.source', {
|
||||
generated: formatDateTime(deploymentLayout.generated_at),
|
||||
current: deploymentLayout.program_status.current_task_id,
|
||||
next: deploymentLayout.program_status.next_task_id,
|
||||
})}
|
||||
muted
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-deployment-layout-kpi-grid">
|
||||
<MetricCard label={t('deploymentLayout.metrics.targets')} value={deploymentLayoutTargets} icon={<Boxes size={16} />} />
|
||||
<MetricCard label={t('deploymentLayout.metrics.openclaw')} value={deploymentLayoutOpenClawTargets} icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('deploymentLayout.metrics.hermes')} value={deploymentLayoutHermesTargets} icon={<FileText size={16} />} />
|
||||
<MetricCard label={t('deploymentLayout.metrics.nemotron')} value={deploymentLayoutNemotronTargets} tone="warn" icon={<Target size={16} />} />
|
||||
<MetricCard label={t('deploymentLayout.metrics.approval')} value={deploymentLayoutApprovalTargets} tone={deploymentLayoutApprovalTargets > 0 ? 'warn' : 'ok'} icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('deploymentLayout.metrics.blocked')} value={deploymentLayoutBlockedTargets} tone={deploymentLayoutBlockedTargets > 0 ? 'danger' : 'ok'} icon={<AlertTriangle size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.25fr) minmax(0, 0.75fr)', gap: 12 }} className="automation-inventory-deployment-layout-grid">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('deploymentLayout.targetsTitle')}
|
||||
</span>
|
||||
<Chip value={t('deploymentLayout.targetsShown', { count: visibleDeploymentTargets.length, total: deploymentLayoutTargets })} muted />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-deployment-layout-target-grid">
|
||||
{visibleDeploymentTargets.map(target => (
|
||||
<div key={target.target_id} style={{ padding: 11, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, fontWeight: 700, color: '#141413' }}>
|
||||
{target.target_id}
|
||||
</span>
|
||||
<Chip value={deploymentLayoutValueLabel('deploymentStates', target.deployment_state)} muted={target.deployment_state === 'active_governed'} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={`${t('deploymentLayout.labels.domain')}: ${deploymentLayoutValueLabel('domains', target.domain_id)}`} muted />
|
||||
<Chip value={`${t('deploymentLayout.labels.primary')}: ${deploymentLayoutValueLabel('agents', target.primary_agent)}`} />
|
||||
<Chip value={`${t('deploymentLayout.labels.telegram')}: ${deploymentLayoutValueLabel('telegramPolicies', target.telegram_policy)}`} muted={target.telegram_policy !== 'approval_required'} />
|
||||
<Chip value={`${t('deploymentLayout.labels.automation')}: ${deploymentLayoutValueLabel('automationLevels', target.automation_level)}`} muted={target.automation_level === 'observe_only'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{target.next_action}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
{target.supporting_agents.length > 0 ? (
|
||||
<Chip value={`${t('deploymentLayout.labels.support')}: ${target.supporting_agents.map(agent => deploymentLayoutValueLabel('agents', agent)).join(' / ')}`} muted />
|
||||
) : null}
|
||||
<Chip value={`${t('deploymentLayout.labels.evidence')}: ${target.evidence_refs.length}`} muted />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 9, minWidth: 0 }}>
|
||||
<SmallLabel>{t('deploymentLayout.agentContractTitle')}</SmallLabel>
|
||||
{deploymentLayout.agent_contracts.map(contract => (
|
||||
<div key={contract.agent_id} style={{ display: 'flex', flexDirection: 'column', gap: 5, paddingBottom: 7, borderBottom: '0.5px solid #eee9dd' }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413' }}>
|
||||
{deploymentLayoutValueLabel('agents', contract.agent_id)}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45 }}>
|
||||
{contract.primary_specialty}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{contract.allowed_autonomy.slice(0, 2).map(item => (
|
||||
<Chip key={`${contract.agent_id}-${item}`} value={item} muted />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<SmallLabel>{t('deploymentLayout.telegramTitle')}</SmallLabel>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{t('deploymentLayout.telegramDetail', {
|
||||
gateway: deploymentLayout.telegram_contract.primary_gateway,
|
||||
classes: deploymentLayout.telegram_contract.notification_classes.length,
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={t('deploymentLayout.redactionLocked')} />
|
||||
<Chip value={t('deploymentLayout.directSendBlocked')} muted />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<SmallLabel>{t('deploymentLayout.learningTitle')}</SmallLabel>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
|
||||
{t('deploymentLayout.learningDetail', {
|
||||
sources: deploymentLayout.learning_contract.event_sources.length,
|
||||
loops: deploymentLayout.learning_contract.feedback_loops.length,
|
||||
metrics: deploymentLayout.learning_contract.growth_metrics.length,
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={`${t('deploymentLayout.labels.bus')}: ${deploymentLayout.collaboration_contract.message_bus}`} muted />
|
||||
<Chip value={t('deploymentLayout.frontendSafe')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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' }}>
|
||||
@@ -2389,6 +2556,9 @@ export function AutomationInventoryTab() {
|
||||
.automation-inventory-provider-map-grid,
|
||||
.automation-inventory-provider-grid,
|
||||
.automation-inventory-provider-route-grid,
|
||||
.automation-inventory-deployment-layout-kpi-grid,
|
||||
.automation-inventory-deployment-layout-grid,
|
||||
.automation-inventory-deployment-layout-target-grid,
|
||||
.automation-inventory-service-health-kpi-grid,
|
||||
.automation-inventory-service-health-map-grid,
|
||||
.automation-inventory-service-health-evidence-grid,
|
||||
|
||||
@@ -262,6 +262,11 @@ export const apiClient = {
|
||||
return handleResponse<AiAgentAutomationBacklogSnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiAgentDeploymentLayout() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/agent-deployment-layout`)
|
||||
return handleResponse<AiAgentDeploymentLayoutSnapshot>(res)
|
||||
},
|
||||
|
||||
async getRuntimeSurfaceInventory() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/runtime-surface-inventory`)
|
||||
return handleResponse<RuntimeSurfaceInventorySnapshot>(res)
|
||||
@@ -819,6 +824,109 @@ export interface AiAgentAutomationBacklogSnapshot {
|
||||
>
|
||||
}
|
||||
|
||||
export interface AiAgentDeploymentLayoutSnapshot {
|
||||
schema_version: 'ai_agent_deployment_layout_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
|
||||
deployment_authority: 'layout_only_no_runtime_deploy'
|
||||
}
|
||||
agent_contracts: Array<{
|
||||
agent_id: string
|
||||
display_name: string
|
||||
primary_specialty: string
|
||||
deployment_lane: string
|
||||
allowed_autonomy: string[]
|
||||
must_delegate_to: string[]
|
||||
blocked_actions: string[]
|
||||
learning_scope: string[]
|
||||
}>
|
||||
domains: Array<{
|
||||
domain_id: string
|
||||
display_name: string
|
||||
description: string
|
||||
}>
|
||||
deployment_targets: Array<{
|
||||
target_id: string
|
||||
domain_id: string
|
||||
display_name: string
|
||||
target_type: string
|
||||
primary_agent: string
|
||||
supporting_agents: string[]
|
||||
deployment_state:
|
||||
| 'active_governed'
|
||||
| 'read_only_layout'
|
||||
| 'blocked_by_gate'
|
||||
| 'planned'
|
||||
| 'candidate_only'
|
||||
automation_level:
|
||||
| 'observe_only'
|
||||
| 'prepare_only'
|
||||
| 'dry_run_only'
|
||||
| 'hitl_execute_after_approval'
|
||||
| 'blocked'
|
||||
capabilities: string[]
|
||||
telegram_policy:
|
||||
| 'failure_only'
|
||||
| 'action_required'
|
||||
| 'approval_required'
|
||||
| 'daily_summary_only'
|
||||
| 'no_direct_notify'
|
||||
learning_inputs: string[]
|
||||
communication_channels: string[]
|
||||
approval_gate: string
|
||||
evidence_refs: string[]
|
||||
next_action: string
|
||||
}>
|
||||
collaboration_contract: {
|
||||
message_bus: string
|
||||
audit_trail: string
|
||||
handoff_rules: string[]
|
||||
frontend_redaction: {
|
||||
operator_conversation_display_allowed: false
|
||||
agent_private_reasoning_display_allowed: false
|
||||
display_policy: string
|
||||
}
|
||||
}
|
||||
learning_contract: {
|
||||
event_sources: string[]
|
||||
feedback_loops: string[]
|
||||
growth_metrics: string[]
|
||||
retention_policy: string
|
||||
}
|
||||
telegram_contract: {
|
||||
primary_gateway: string
|
||||
bot_roles: string[]
|
||||
notification_classes: string[]
|
||||
redaction_policy: string
|
||||
e2e_validation: string
|
||||
}
|
||||
rollups: {
|
||||
total_targets: number
|
||||
by_domain: Record<string, number>
|
||||
by_primary_agent: Record<string, number>
|
||||
by_deployment_state: Record<string, number>
|
||||
by_telegram_policy: Record<string, number>
|
||||
blocked_target_ids: string[]
|
||||
approval_required_target_ids: string[]
|
||||
}
|
||||
approval_boundaries: Record<
|
||||
| 'sdk_installation_allowed'
|
||||
| 'paid_api_call_allowed'
|
||||
| 'shadow_or_canary_allowed'
|
||||
| 'production_routing_allowed'
|
||||
| 'destructive_operation_allowed'
|
||||
| 'secret_plaintext_allowed'
|
||||
| 'autonomous_host_mutation_allowed'
|
||||
| 'telegram_direct_send_allowed',
|
||||
false
|
||||
>
|
||||
}
|
||||
|
||||
export interface RuntimeSurfaceInventorySnapshot {
|
||||
schema_version: 'runtime_surface_inventory_v1'
|
||||
generated_at: string
|
||||
|
||||
Reference in New Issue
Block a user