## Phase 1-3: Control Plane + Contract System - awooop_phase1_control_plane_2026-05-04.sql: 12 張核心表 + RLS - awooop_phase1_batch1_rls_2026-05-04.sql: 全部 FORCE RLS + GRANT - packages/awooop-contracts/: 六合約 JSON Schema + golden fixtures - src/models/awooop_contracts.py: Pydantic v2 contract models(extra=forbid) - src/repositories/contract_repository.py: contract lifecycle(draft→published→active) - src/services/contract_service.py: HMAC publish sig + Redis multi-sig activate - src/services/schema_validator.py: LLM output validator(retry×3, E-SCHEMA-001) ## Phase 2: Tenant Isolation - awooop_phase2_budget_ledger_2026-05-04.sql: budget_ledger + RLS - src/services/budget_service.py: Token Budget Hard Kill 三層防線 - src/core/context.py: PROJECT_ID ContextVar(31 background loop 自動繼承) - src/db/base.py + models.py: project_id 欄位 + RLS set_config 注入 - src/hermes/nl_gateway.py: project_id Redis key 前綴(Phase A 雙寫) - src/services/anomaly_counter.py: per-project 改造(Phase A fallback) ## Phase 4: Platform Shell in Shadow Mode - awooop_phase4_run_state_2026-05-04.sql: run_state + step_journal + idempotency - src/services/run_state_machine.py: 8-state FSM + SKIP LOCKED + stale reaper - src/services/platform_runtime.py: UUID v7 + W3C trace_id + shadow_execute - src/services/audit_sink.py: PII/secret redaction 9 patterns - src/api/v1/platform/runs.py: POST/GET /v1/platform/runs(Router→Service 架構) - src/workers/platform_worker.py: SKIP LOCKED worker + heartbeat + reaper loop - src/main.py: platform router + lifespan worker start/stop ## Phase 5: MCP Gateway 五閘門 - awooop_phase5_mcp_gateway_2026-05-04.sql: 4 表 + RLS - src/plugins/mcp/gateway.py: McpGateway(Gate 1~5, E-MCP-GATE-001~009) - src/plugins/mcp/redaction_middleware.py: 雙層 redaction + 16K 截斷 - src/plugins/mcp/registry.py: __provider name mangling(ADR-116) - src/plugins/mcp/credential_resolver.py: k8s secret ref 解析 - tests/test_mcp_credential_isolation.py: 10 個迴歸測試(secret leak 防再現) ## Phase 6-8: EwoooC + Channel Hub + Approval Token - awooop_phase6_ewoooc_onboarding_2026-05-04.sql: ewoooc tenant + 4 read-only MCP tools - awooop_phase7_channel_hub_2026-05-04.sql: conversation_event + outbound_message - src/services/provider_proxy.py: ProviderProxy + PlatformEnvelope(ADR-115) - src/services/channel_hub.py: Telegram inbound mirror + Progressive Feedback(30s) - src/services/awooop_approval_token.py: HS256 + jti NX replay 防護 + suggest mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
227 lines
6.7 KiB
Python
227 lines
6.7 KiB
Python
"""
|
||
MCP Provider Registry - ADR-015 模組化架構
|
||
==========================================
|
||
|
||
Provider 註冊中心,實現依賴注入 (DI) 模式:
|
||
1. 統一管理所有 MCP Tool Providers
|
||
2. 支援動態註冊/反註冊
|
||
3. 支援健康檢查
|
||
|
||
@see docs/adr/ADR-015-mcp-modular-architecture.md
|
||
"""
|
||
|
||
import structlog
|
||
|
||
from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
class AuditedMCPToolProvider(MCPToolProvider):
|
||
"""Provider wrapper that writes every MCP tool call to the audit subsystem."""
|
||
|
||
def __init__(self, provider: MCPToolProvider) -> None:
|
||
# __provider 使用 Python name mangling(_AuditedMCPToolProvider__provider)
|
||
# 防止 caller 透過 wrapper._provider 直接存取 inner provider(ADR-116 封裝要求)
|
||
self.__provider = provider # noqa: SLF001 — intentional name mangling
|
||
|
||
@property
|
||
def name(self) -> str:
|
||
return self.__provider.name
|
||
|
||
@property
|
||
def enabled(self) -> bool:
|
||
return self.__provider.enabled
|
||
|
||
async def list_tools(self) -> list[MCPTool]:
|
||
return await self.__provider.list_tools()
|
||
|
||
async def execute(
|
||
self,
|
||
tool_name: str,
|
||
parameters: dict,
|
||
) -> MCPToolResult:
|
||
from src.services.mcp_audit_service import monotonic_ms, record_mcp_call
|
||
|
||
audit_context = parameters.get("_mcp_audit") if isinstance(parameters, dict) else None
|
||
provider_parameters = {
|
||
key: value for key, value in parameters.items()
|
||
if key != "_mcp_audit"
|
||
}
|
||
started = monotonic_ms()
|
||
result: MCPToolResult | None = None
|
||
try:
|
||
result = await self.__provider.execute(tool_name, provider_parameters)
|
||
return result
|
||
finally:
|
||
duration_ms = monotonic_ms() - started
|
||
await record_mcp_call(
|
||
mcp_server=self.name,
|
||
tool_name=tool_name,
|
||
input_params=parameters,
|
||
output_result=result.output if result else None,
|
||
duration_ms=duration_ms,
|
||
success=bool(result.success) if result else False,
|
||
error_message=result.error if result else "provider_exception",
|
||
session_id=audit_context.get("session_id") if isinstance(audit_context, dict) else None,
|
||
flywheel_node=audit_context.get("flywheel_node") if isinstance(audit_context, dict) else None,
|
||
incident_id=audit_context.get("incident_id") if isinstance(audit_context, dict) else None,
|
||
agent_role=audit_context.get("agent_role") if isinstance(audit_context, dict) else None,
|
||
)
|
||
|
||
async def health_check(self) -> bool:
|
||
return await self.__provider.health_check()
|
||
|
||
|
||
class ProviderRegistry:
|
||
"""
|
||
MCP Tool Provider 註冊中心
|
||
|
||
使用方式:
|
||
# 註冊
|
||
registry = ProviderRegistry()
|
||
registry.register(K8sProvider())
|
||
registry.register(SignOzProvider())
|
||
|
||
# 取得
|
||
k8s = registry.get("kubernetes")
|
||
await k8s.execute("kubectl_get", {...})
|
||
|
||
# 列出所有
|
||
for provider in registry.all():
|
||
print(provider.name)
|
||
"""
|
||
|
||
def __init__(self) -> None:
|
||
self._providers: dict[str, MCPToolProvider] = {}
|
||
|
||
def register(self, provider: MCPToolProvider) -> None:
|
||
"""
|
||
註冊 Provider
|
||
|
||
Args:
|
||
provider: MCPToolProvider 實例
|
||
|
||
Raises:
|
||
ValueError: 如果 Provider 名稱已存在
|
||
"""
|
||
if provider.name in self._providers:
|
||
raise ValueError(f"Provider '{provider.name}' already registered")
|
||
|
||
self._providers[provider.name] = AuditedMCPToolProvider(provider)
|
||
logger.info(
|
||
"provider_registered",
|
||
name=provider.name,
|
||
enabled=provider.enabled,
|
||
)
|
||
|
||
def unregister(self, name: str) -> bool:
|
||
"""
|
||
反註冊 Provider
|
||
|
||
Args:
|
||
name: Provider 名稱
|
||
|
||
Returns:
|
||
bool: 是否成功反註冊
|
||
"""
|
||
if name in self._providers:
|
||
del self._providers[name]
|
||
logger.info("provider_unregistered", name=name)
|
||
return True
|
||
return False
|
||
|
||
def get(self, name: str) -> MCPToolProvider | None:
|
||
"""
|
||
取得 Provider
|
||
|
||
Args:
|
||
name: Provider 名稱
|
||
|
||
Returns:
|
||
MCPToolProvider | None: Provider 實例,若不存在則返回 None
|
||
"""
|
||
provider = self._providers.get(name)
|
||
if provider and not provider.enabled:
|
||
logger.warning("provider_disabled", name=name)
|
||
return None
|
||
return provider
|
||
|
||
def all(self) -> list[MCPToolProvider]:
|
||
"""
|
||
取得所有已啟用的 Providers
|
||
|
||
Returns:
|
||
list[MCPToolProvider]: 已啟用的 Provider 列表
|
||
"""
|
||
return [p for p in self._providers.values() if p.enabled]
|
||
|
||
def names(self) -> list[str]:
|
||
"""
|
||
取得所有已啟用的 Provider 名稱
|
||
|
||
Returns:
|
||
list[str]: Provider 名稱列表
|
||
"""
|
||
return [p.name for p in self.all()]
|
||
|
||
async def health_check_all(self) -> dict[str, bool]:
|
||
"""
|
||
檢查所有 Provider 健康狀態
|
||
|
||
Returns:
|
||
dict[str, bool]: {provider_name: is_healthy}
|
||
"""
|
||
results = {}
|
||
for provider in self.all():
|
||
try:
|
||
results[provider.name] = await provider.health_check()
|
||
except Exception as e:
|
||
logger.warning(
|
||
"provider_health_check_failed",
|
||
name=provider.name,
|
||
error=str(e),
|
||
)
|
||
results[provider.name] = False
|
||
return results
|
||
|
||
def __contains__(self, name: str) -> bool:
|
||
return name in self._providers
|
||
|
||
def __len__(self) -> int:
|
||
return len(self._providers)
|
||
|
||
|
||
# =============================================================================
|
||
# Global Registry Singleton
|
||
# =============================================================================
|
||
|
||
_registry: ProviderRegistry | None = None
|
||
|
||
|
||
def get_provider_registry() -> ProviderRegistry:
|
||
"""
|
||
取得全域 Provider Registry
|
||
|
||
Returns:
|
||
ProviderRegistry: 單例實例
|
||
"""
|
||
global _registry
|
||
if _registry is None:
|
||
_registry = ProviderRegistry()
|
||
return _registry
|
||
|
||
|
||
def register_provider(provider: MCPToolProvider) -> None:
|
||
"""
|
||
便捷函數: 註冊 Provider 到全域 Registry
|
||
"""
|
||
get_provider_registry().register(provider)
|
||
|
||
|
||
def get_provider(name: str) -> MCPToolProvider | None:
|
||
"""
|
||
便捷函數: 從全域 Registry 取得 Provider
|
||
"""
|
||
return get_provider_registry().get(name)
|