Files
awoooi/apps/api/src/plugins/mcp/registry.py
Your Name 8629ac709b
Some checks failed
run-migration / migrate (push) Failing after 59s
Code Review / ai-code-review (push) Successful in 1m8s
Type Sync Check / check-type-sync (push) Successful in 2m27s
feat(awooop): Phase 1-8 完整實作 — AwoooP Agent Platform 六平面架構
## 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>
2026-05-04 19:31:53 +08:00

227 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 providerADR-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)