feat(platform): 顯示全域產品資產納管
This commit is contained in:
@@ -9,6 +9,7 @@ ADR-106(AwoooP 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),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user