feat(platform): 顯示全域產品資產納管
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m33s
CD Pipeline / build-and-deploy (push) Successful in 4m17s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-14 15:18:54 +08:00
parent 30f2f490c7
commit fb5c6fbadd
7 changed files with 1146 additions and 8 deletions

View File

@@ -29,9 +29,87 @@ class TenantItem(BaseModel):
created_at: datetime
class TenantAssetSummary(BaseModel):
tenant_table_count: int
product_surface_count: int
public_route_count: int
public_gateway_snapshot_route_count: int
source_candidate_repo_count: int
source_in_scope_repo_count: int
source_primary_ready_count: int
owner_response_received_count: int
owner_response_accepted_count: int
runtime_gate_count: int
action_button_count: int
class TenantProductSurface(BaseModel):
product_id: str
product_name: str
project_id: str
category: str
surface_kind: str
owner_lane: str
coverage_status: str
public_routes: list[str]
source_keys: list[str]
public_route_count: int
source_repo_count: int
missing_public_routes: list[str]
owner_response_received_count: int
owner_response_accepted_count: int
runtime_gate_count: int
action_button_count: int
class TenantPublicRouteAsset(BaseModel):
domain: str
product_id: str
product_name: str
category: str
coverage_status: str
control_tier: str
upstream_count: int
admin_route_count: int
websocket_route_count: int
public_route_smoke_required: bool
route_smoke_accepted: bool
owner_response_accepted: bool
runtime_gate_count: int
action_button_count: int
source: str
class TenantSourceRepoAsset(BaseModel):
github_repo: str
source_key: str
product_id: str
product_name: str
category: str
scope_status: str
readiness_state: str
risk: str
primary_ready: bool
blocker_count: int
runtime_gate_count: int
action_button_count: int
class TenantAssetInventory(BaseModel):
schema_version: str
mode: str
evidence_refs: list[str]
summary: TenantAssetSummary
products: list[TenantProductSurface]
public_routes: list[TenantPublicRouteAsset]
source_repos: list[TenantSourceRepoAsset]
boundaries: list[str]
class ListTenantsResponse(BaseModel):
tenants: list[TenantItem]
total: int
asset_inventory: TenantAssetInventory
@router.get(

View File

@@ -9,6 +9,7 @@ ADR-106AwoooP Agent Platform
from __future__ import annotations
import asyncio
import json
import os
import re
import time
@@ -16,6 +17,7 @@ import uuid
from collections import defaultdict
from collections.abc import Mapping
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any, get_args
from urllib.parse import urlencode
from uuid import UUID
@@ -65,6 +67,7 @@ from src.services.operator_summary_cache import (
store_operator_summary_async,
)
from src.services.run_state_machine import transition
from src.services.snapshot_paths import resolve_repo_root
logger = structlog.get_logger(__name__)
@@ -130,6 +133,454 @@ _CALLBACK_REPLY_AUDIT_SUMMARY_SCHEMA_VERSION = (
# Tenants
# =============================================================================
_PUBLIC_GATEWAY_INVENTORY_SNAPSHOT = (
"docs/security/public-gateway-preflight-inventory.snapshot.json"
)
_SOURCE_CONTROL_READINESS_SNAPSHOT = (
"docs/security/source-control-primary-readiness-gate.snapshot.json"
)
_DOMAIN_PRODUCT_OVERRIDES: dict[str, dict[str, str]] = {
"aiops.wooo.work": {
"product_id": "awoooi",
"product_name": "AWOOOI / AwoooP / IwoooS",
"category": "core_platform",
},
"awoooi.wooo.work": {
"product_id": "awoooi",
"product_name": "AWOOOI / AwoooP / IwoooS",
"category": "core_platform",
},
"mo.wooo.work": {
"product_id": "ewoooc",
"product_name": "EwoooC / Mo",
"category": "business_product",
},
"vibework.wooo.work": {
"product_id": "vibework",
"product_name": "VibeWork",
"category": "business_product",
},
"agent.wooo.work": {
"product_id": "agent-bounty-protocol",
"product_name": "Agent Bounty Protocol",
"category": "business_product",
},
"stock.wooo.work": {
"product_id": "stockplatform",
"product_name": "StockPlatform",
"category": "business_product",
},
"bitan.wooo.work": {
"product_id": "bitan-pharmacy",
"product_name": "Bitan Pharmacy",
"category": "business_product",
},
"tsenyang.com": {
"product_id": "tsenyang-website",
"product_name": "TsenYang Website",
"category": "public_site",
},
"www.tsenyang.com": {
"product_id": "tsenyang-website",
"product_name": "TsenYang Website",
"category": "public_site",
},
"vtuber.wooo.work": {
"product_id": "vtuber",
"product_name": "VTuber",
"category": "public_site",
},
"gitea.wooo.work": {
"product_id": "source-control",
"product_name": "Source Control / DevOps",
"category": "platform_tool",
},
"gitlab.wooo.work": {
"product_id": "source-control",
"product_name": "Source Control / DevOps",
"category": "platform_tool",
},
"harbor.wooo.work": {
"product_id": "source-control",
"product_name": "Source Control / DevOps",
"category": "platform_tool",
},
"registry.wooo.work": {
"product_id": "source-control",
"product_name": "Source Control / DevOps",
"category": "platform_tool",
},
"sentry.wooo.work": {
"product_id": "observability-tooling",
"product_name": "Observability / LLMOps",
"category": "platform_tool",
},
"signoz.wooo.work": {
"product_id": "observability-tooling",
"product_name": "Observability / LLMOps",
"category": "platform_tool",
},
"langfuse.wooo.work": {
"product_id": "observability-tooling",
"product_name": "Observability / LLMOps",
"category": "platform_tool",
},
}
_CANONICAL_PRODUCT_SURFACES: tuple[dict[str, Any], ...] = (
{
"product_id": "awoooi",
"product_name": "AWOOOI / AwoooP / IwoooS",
"project_id": "awoooi",
"category": "core_platform",
"surface_kind": "platform_product",
"owner_lane": "S4.9-S4.13",
"coverage_status": "read_only_visible",
"public_routes": ["awoooi.wooo.work", "aiops.wooo.work"],
"source_keys": ["wooo/awoooi"],
},
{
"product_id": "ewoooc",
"product_name": "EwoooC / Mo",
"project_id": "ewoooc",
"category": "business_product",
"surface_kind": "product_console",
"owner_lane": "S4.10",
"coverage_status": "owner_response_required",
"public_routes": ["mo.wooo.work"],
"source_keys": ["wooo/ewoooc / root/momo-pro-system / momo working trees"],
},
{
"product_id": "vibework",
"product_name": "VibeWork",
"project_id": "vibework",
"category": "business_product",
"surface_kind": "product_site",
"owner_lane": "S4.10",
"coverage_status": "owner_response_required",
"public_routes": ["vibework.wooo.work"],
"source_keys": ["vibework"],
},
{
"product_id": "agent-bounty-protocol",
"product_name": "Agent Bounty Protocol",
"project_id": "agent-bounty-protocol",
"category": "business_product",
"surface_kind": "agent_protocol",
"owner_lane": "S4.10",
"coverage_status": "owner_response_required",
"public_routes": ["agent.wooo.work"],
"source_keys": ["agent-bounty-protocol"],
},
{
"product_id": "stockplatform",
"product_name": "StockPlatform",
"project_id": "stockplatform",
"category": "business_product",
"surface_kind": "product_site",
"owner_lane": "P0-CRON-001",
"coverage_status": "read_only_visible",
"public_routes": ["stock.wooo.work"],
"source_keys": ["stockplatform"],
},
{
"product_id": "bitan-pharmacy",
"product_name": "Bitan Pharmacy",
"project_id": "bitan-pharmacy",
"category": "business_product",
"surface_kind": "product_site",
"owner_lane": "P0-CRON-002",
"coverage_status": "read_only_visible",
"public_routes": ["bitan.wooo.work"],
"source_keys": ["bitan-pharmacy"],
},
{
"product_id": "tsenyang-website",
"product_name": "TsenYang Website",
"project_id": "tsenyang-website",
"category": "public_site",
"surface_kind": "public_site",
"owner_lane": "S4.10",
"coverage_status": "owner_response_required",
"public_routes": ["tsenyang.com", "www.tsenyang.com"],
"source_keys": ["tsenyang-website"],
},
{
"product_id": "vtuber",
"product_name": "VTuber",
"project_id": "vtuber",
"category": "public_site",
"surface_kind": "public_site",
"owner_lane": "public_gateway",
"coverage_status": "read_only_visible",
"public_routes": ["vtuber.wooo.work"],
"source_keys": [],
},
{
"product_id": "source-control",
"product_name": "Source Control / DevOps",
"project_id": "__platform__",
"category": "platform_tool",
"surface_kind": "tooling",
"owner_lane": "S4.9-S4.13",
"coverage_status": "read_only_visible",
"public_routes": [
"gitea.wooo.work",
"gitlab.wooo.work",
"harbor.wooo.work",
"registry.wooo.work",
],
"source_keys": ["wooo/wooo-infra-config"],
},
{
"product_id": "observability-tooling",
"product_name": "Observability / LLMOps",
"project_id": "__platform__",
"category": "platform_tool",
"surface_kind": "tooling",
"owner_lane": "P1-003",
"coverage_status": "read_only_visible",
"public_routes": ["sentry.wooo.work", "signoz.wooo.work", "langfuse.wooo.work"],
"source_keys": [],
},
)
_ADDITIONAL_PUBLIC_ROUTES: tuple[dict[str, Any], ...] = (
{
"domain": "awoooi.wooo.work",
"coverage_status": "read_only_visible",
"source": "awoooi_canonical_route",
},
{
"domain": "vibework.wooo.work",
"coverage_status": "owner_response_required",
"source": "vibework_onboarding_handoff",
},
{
"domain": "agent.wooo.work",
"coverage_status": "owner_response_required",
"source": "agent_bounty_onboarding_handoff",
},
)
def _load_committed_snapshot(relative_path: str) -> dict[str, Any]:
repo_root = resolve_repo_root(Path(__file__))
snapshot_path = repo_root / relative_path
try:
return json.loads(snapshot_path.read_text(encoding="utf-8"))
except FileNotFoundError:
logger.warning("tenant_asset_snapshot_missing", path=relative_path)
except json.JSONDecodeError as exc:
logger.warning(
"tenant_asset_snapshot_invalid_json",
path=relative_path,
error=str(exc),
)
return {}
def _domain_product(domain: str) -> dict[str, str]:
return _DOMAIN_PRODUCT_OVERRIDES.get(
domain,
{
"product_id": domain.replace(".", "-"),
"product_name": domain,
"category": "public_route",
},
)
def _route_asset_from_gateway_row(row: Mapping[str, Any]) -> dict[str, Any]:
domain = str(row.get("domain") or "")
product = _domain_product(domain)
route_smoke_accepted = bool(row.get("route_smoke_accepted"))
owner_response_accepted = bool(row.get("owner_response_accepted"))
coverage_status = (
"verified"
if route_smoke_accepted and owner_response_accepted
else "read_only_pending_smoke"
)
return {
"domain": domain,
"product_id": product["product_id"],
"product_name": product["product_name"],
"category": product["category"],
"coverage_status": coverage_status,
"control_tier": row.get("control_tier") or "unknown",
"upstream_count": int(row.get("upstream_count") or 0),
"admin_route_count": int(row.get("admin_route_count") or 0),
"websocket_route_count": int(row.get("websocket_route_count") or 0),
"public_route_smoke_required": bool(row.get("public_route_smoke_required")),
"route_smoke_accepted": route_smoke_accepted,
"owner_response_accepted": owner_response_accepted,
"runtime_gate_count": 0,
"action_button_count": 0,
"source": "public_gateway_preflight_inventory",
}
def _build_route_assets(public_gateway_snapshot: Mapping[str, Any]) -> list[dict[str, Any]]:
seen_domains: set[str] = set()
route_assets: list[dict[str, Any]] = []
for row in public_gateway_snapshot.get("route_impacts") or []:
if not isinstance(row, Mapping):
continue
asset = _route_asset_from_gateway_row(row)
if not asset["domain"] or asset["domain"] in seen_domains:
continue
seen_domains.add(asset["domain"])
route_assets.append(asset)
for row in _ADDITIONAL_PUBLIC_ROUTES:
domain = str(row["domain"])
if domain in seen_domains:
continue
product = _domain_product(domain)
route_assets.append(
{
"domain": domain,
"product_id": product["product_id"],
"product_name": product["product_name"],
"category": product["category"],
"coverage_status": row["coverage_status"],
"control_tier": "candidate",
"upstream_count": 0,
"admin_route_count": 0,
"websocket_route_count": 0,
"public_route_smoke_required": True,
"route_smoke_accepted": False,
"owner_response_accepted": False,
"runtime_gate_count": 0,
"action_button_count": 0,
"source": row["source"],
}
)
return route_assets
def _build_source_repo_assets(
source_control_snapshot: Mapping[str, Any],
) -> list[dict[str, Any]]:
source_assets: list[dict[str, Any]] = []
for row in source_control_snapshot.get("repo_readiness") or []:
if not isinstance(row, Mapping):
continue
source_key = str(row.get("source_key") or "")
github_repo = str(row.get("github_repo") or "")
product = {
"product_id": (source_key or github_repo).replace("/", "-") or "unknown-source",
"product_name": source_key or github_repo or "unknown-source",
"category": "source_repo",
}
for surface in _CANONICAL_PRODUCT_SURFACES:
if source_key in surface.get("source_keys", []) or github_repo in surface.get(
"source_keys", []
):
product = {
"product_id": str(surface["product_id"]),
"product_name": str(surface["product_name"]),
"category": str(surface["category"]),
}
break
source_assets.append(
{
"github_repo": github_repo,
"source_key": source_key,
"product_id": product["product_id"],
"product_name": product["product_name"],
"category": product["category"],
"scope_status": row.get("scope_status") or "unknown",
"readiness_state": row.get("readiness_state") or "unknown",
"risk": row.get("risk") or "UNKNOWN",
"primary_ready": bool(row.get("primary_ready")),
"blocker_count": len(row.get("blockers") or []),
"runtime_gate_count": 0,
"action_button_count": 0,
}
)
return source_assets
def build_tenant_asset_inventory(tenants: list[Mapping[str, Any]]) -> dict[str, Any]:
"""Build a read-only product / route / source-control asset view for tenants."""
public_gateway = _load_committed_snapshot(_PUBLIC_GATEWAY_INVENTORY_SNAPSHOT)
source_control = _load_committed_snapshot(_SOURCE_CONTROL_READINESS_SNAPSHOT)
public_routes = _build_route_assets(public_gateway)
source_repos = _build_source_repo_assets(source_control)
public_route_domains = {asset["domain"] for asset in public_routes}
source_keys = {asset["source_key"] for asset in source_repos}
products: list[dict[str, Any]] = []
for surface in _CANONICAL_PRODUCT_SURFACES:
public_route_count = len(
[domain for domain in surface["public_routes"] if domain in public_route_domains]
)
source_repo_count = len(
[source for source in surface["source_keys"] if source in source_keys]
)
missing_public_routes = [
domain for domain in surface["public_routes"] if domain not in public_route_domains
]
products.append(
{
**surface,
"public_route_count": public_route_count,
"source_repo_count": source_repo_count,
"missing_public_routes": missing_public_routes,
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
}
)
source_summary = source_control.get("summary") or {}
gateway_summary = public_gateway.get("summary") or {}
return {
"schema_version": "awooop_tenant_asset_inventory_v1",
"mode": "read_only_global_asset_inventory",
"evidence_refs": [
_PUBLIC_GATEWAY_INVENTORY_SNAPSHOT,
_SOURCE_CONTROL_READINESS_SNAPSHOT,
"docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md",
],
"summary": {
"tenant_table_count": len(tenants),
"product_surface_count": len(products),
"public_route_count": len(public_routes),
"public_gateway_snapshot_route_count": int(
gateway_summary.get("route_impact_count") or 0
),
"source_candidate_repo_count": int(
source_summary.get("candidate_repo_count") or len(source_repos)
),
"source_in_scope_repo_count": int(source_summary.get("in_scope_repo_count") or 0),
"source_primary_ready_count": int(source_summary.get("primary_ready_count") or 0),
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
"products": products,
"public_routes": public_routes,
"source_repos": source_repos,
"boundaries": [
"read_only_inventory_only=true",
"owner_response_received_count=0",
"owner_response_accepted_count=0",
"runtime_execution_authorized=false",
"active_runtime_gate_count=0",
"action_buttons_allowed=false",
"repo_creation_authorized=false",
"refs_sync_authorized=false",
"workflow_modification_authorized=false",
"public_route_change_authorized=false",
],
}
async def list_tenants() -> dict[str, Any]:
"""列出所有 AwoooP 租戶Operator Console不依 RLS 過濾)。"""
async with get_db_context("awoooi") as db:
@@ -158,7 +609,11 @@ async def list_tenants() -> dict[str, Any]:
}
for r in rows
]
return {"tenants": tenants, "total": len(tenants)}
return {
"tenants": tenants,
"total": len(tenants),
"asset_inventory": build_tenant_asset_inventory(tenants),
}
# =============================================================================

View File

@@ -0,0 +1,83 @@
from __future__ import annotations
from src.api.v1.platform.tenants import ListTenantsResponse
from src.services.platform_operator_service import build_tenant_asset_inventory
def test_tenant_asset_inventory_merges_products_routes_and_repos() -> None:
tenants = [
{
"project_id": "awoooi",
"display_name": "AWOOOI",
"migration_mode": "shadow",
"budget_limit_usd": None,
"is_active": True,
"created_at": "2026-06-14T00:00:00+08:00",
}
]
inventory = build_tenant_asset_inventory(tenants)
summary = inventory["summary"]
assert inventory["schema_version"] == "awooop_tenant_asset_inventory_v1"
assert summary["tenant_table_count"] == 1
assert summary["product_surface_count"] >= 10
assert summary["public_route_count"] >= 17
assert summary["source_candidate_repo_count"] == 10
assert summary["source_in_scope_repo_count"] == 9
assert summary["source_primary_ready_count"] == 0
assert summary["owner_response_accepted_count"] == 0
assert summary["runtime_gate_count"] == 0
assert summary["action_button_count"] == 0
product_ids = {item["product_id"] for item in inventory["products"]}
assert {
"awoooi",
"ewoooc",
"vibework",
"agent-bounty-protocol",
"stockplatform",
"bitan-pharmacy",
"tsenyang-website",
}.issubset(product_ids)
route_domains = {item["domain"] for item in inventory["public_routes"]}
assert {
"awoooi.wooo.work",
"stock.wooo.work",
"bitan.wooo.work",
"mo.wooo.work",
"vibework.wooo.work",
"agent.wooo.work",
"tsenyang.com",
}.issubset(route_domains)
source_repos = {item["github_repo"] for item in inventory["source_repos"]}
assert "owenhytsai/VibeWork" in source_repos
assert "owenhytsai/agent-bounty-protocol" in source_repos
assert all(item["runtime_gate_count"] == 0 for item in inventory["source_repos"])
assert all(item["action_button_count"] == 0 for item in inventory["public_routes"])
def test_tenant_response_model_keeps_asset_inventory_contract() -> None:
tenant = {
"project_id": "awoooi",
"display_name": "AWOOOI",
"migration_mode": "shadow",
"budget_limit_usd": None,
"is_active": True,
"created_at": "2026-06-14T00:00:00+08:00",
}
inventory = build_tenant_asset_inventory([tenant])
response = ListTenantsResponse.model_validate(
{
"tenants": [tenant],
"total": 1,
"asset_inventory": inventory,
}
)
assert response.asset_inventory.summary.product_surface_count >= 10
assert response.asset_inventory.summary.runtime_gate_count == 0
assert response.asset_inventory.products[0].runtime_gate_count == 0