feat(mcp): add governed cross-product control plane
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
This commit is contained in:
116
apps/api/tests/test_mcp_control_plane_api.py
Normal file
116
apps/api/tests/test_mcp_control_plane_api.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# ruff: noqa: E402
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
|
||||
|
||||
from src.api.v1.mcp_control_plane import router
|
||||
from src.services.mcp_control_plane_service import (
|
||||
build_mcp_control_plane_overview,
|
||||
load_mcp_control_plane_catalog,
|
||||
)
|
||||
from src.services.product_governance_matrix_readback import (
|
||||
load_latest_product_governance_matrix_readback,
|
||||
)
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
_OPERATIONS_DIR = _REPO_ROOT / "docs" / "operations"
|
||||
|
||||
|
||||
def test_catalog_contains_exact_12_products_and_explicit_missing_rows() -> None:
|
||||
catalog = load_mcp_control_plane_catalog(_OPERATIONS_DIR)
|
||||
|
||||
product_ids = {row["product_id"] for row in catalog["products"]}
|
||||
assert len(product_ids) == 12
|
||||
assert {"ewoooc", "momo-pro-system"}.issubset(product_ids)
|
||||
assert catalog["architecture"]["separate_gateway_allowed"] is False
|
||||
assert catalog["architecture"]["separate_rag_database_allowed"] is False
|
||||
assert catalog["source_policy"]["github_allowed"] is False
|
||||
assert catalog["security_policy"]["external_rag_auto_write_allowed"] is False
|
||||
|
||||
capabilities = {
|
||||
row["capability_id"]: row for row in catalog["capabilities"]
|
||||
}
|
||||
assert capabilities["rejected.github"]["decision"] == "rejected"
|
||||
assert capabilities["quarantine.agent-bounty-mcp"]["decision"] == "quarantine"
|
||||
assert capabilities["external.smithery-site-audit-shadow"]["decision"] == "evaluation_shadow_candidate"
|
||||
assert capabilities["external.smithery-site-audit-shadow"]["deployment_allowed"] is False
|
||||
assert capabilities["rejected.smithery-technical-analysis"]["decision"] == "rejected"
|
||||
assert "tool_output_contains_cross_tool_prompt_injection" in capabilities[
|
||||
"quarantine.agent-bounty-mcp"
|
||||
]["blockers"]
|
||||
assert not any(
|
||||
row["deployment_allowed"]
|
||||
for row in catalog["capabilities"]
|
||||
if row["source_type"] == "external"
|
||||
)
|
||||
|
||||
|
||||
def test_overview_joins_committed_product_matrix_without_runtime_claims() -> None:
|
||||
catalog = load_mcp_control_plane_catalog(_OPERATIONS_DIR)
|
||||
matrix = load_latest_product_governance_matrix_readback(_OPERATIONS_DIR)
|
||||
|
||||
payload = build_mcp_control_plane_overview(
|
||||
catalog=catalog,
|
||||
product_matrix=matrix,
|
||||
generated_at="2026-07-15T19:00:00+08:00",
|
||||
)
|
||||
|
||||
assert payload["schema_version"] == "mcp_control_plane_overview_v1"
|
||||
assert payload["status"] == "partial_degraded_with_safe_next_action"
|
||||
assert payload["summary"]["product_count"] == 12
|
||||
assert payload["summary"]["mcp_source_detected_product_count"] == 7
|
||||
assert payload["summary"]["external_promotion_ready_count"] == 0
|
||||
assert payload["summary"]["github_in_scope_count"] == 0
|
||||
assert payload["runtime"]["status"] == "not_requested"
|
||||
|
||||
products = {row["product_id"]: row for row in payload["products"]}
|
||||
assert products["ewoooc"]["canonical_repo"] == "wooo/ewoooc"
|
||||
assert products["momo-pro-system"]["canonical_repo"] == "wooo/momo-pro-system"
|
||||
assert products["agent-bounty-protocol"]["inventory_state"] == "source_detected_quarantined"
|
||||
|
||||
|
||||
def test_catalog_rejects_external_deployment_without_immutable_receipts(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
catalog = load_mcp_control_plane_catalog(_OPERATIONS_DIR)
|
||||
external = next(
|
||||
row for row in catalog["capabilities"] if row["source_type"] == "external"
|
||||
)
|
||||
external["deployment_allowed"] = True
|
||||
(tmp_path / "mcp-control-plane-catalog.snapshot.json").write_text(
|
||||
json.dumps(catalog),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
try:
|
||||
load_mcp_control_plane_catalog(tmp_path)
|
||||
except ValueError as exc:
|
||||
assert "cannot deploy without immutable receipts" in str(exc)
|
||||
else:
|
||||
raise AssertionError("unsafe external deployment must be rejected")
|
||||
|
||||
|
||||
def test_overview_endpoint_returns_real_committed_catalog_without_runtime() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/mcp-control-plane/overview?include_runtime=false"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["summary"]["product_count"] == 12
|
||||
assert data["runtime"]["status"] == "not_requested"
|
||||
assert data["operation_boundaries"]["github_allowed"] is False
|
||||
assert data["operation_boundaries"]["public_response_redacted_aggregate_only"] is True
|
||||
assert data["operation_boundaries"]["mutation_endpoint_exposed"] is False
|
||||
assert data["operation_boundaries"]["operator_auth_required_for_future_mutation"] is True
|
||||
@@ -123,3 +123,41 @@ async def test_execute_tool_resolves_provider_by_tool_manifest(
|
||||
assert parameters["_mcp_audit"]["gateway_path"] == "awooop_mcp_gateway"
|
||||
assert parameters["_mcp_audit"]["incident_id"] == "INC-GW"
|
||||
assert parameters["_mcp_audit"]["flywheel_node"] == "sense"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_redacts_input_and_output_at_gateway_boundary(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
class SensitiveProvider(FakeProvider):
|
||||
async def execute(self, tool_name: str, parameters: dict) -> MCPToolResult:
|
||||
self.calls.append((tool_name, parameters))
|
||||
return MCPToolResult(
|
||||
success=True,
|
||||
execution_id="provider-redacted",
|
||||
output={
|
||||
"connection": "postgresql://reader:plain-password@10.0.1.5/prod"
|
||||
},
|
||||
)
|
||||
|
||||
provider = SensitiveProvider()
|
||||
monkeypatch.setattr(
|
||||
gateway_module,
|
||||
"get_provider_registry",
|
||||
lambda: FakeProviderRegistry(provider),
|
||||
)
|
||||
|
||||
result = await McpGateway(FakeDb())._execute_tool(
|
||||
GatewayContext(
|
||||
project_id="awoooi",
|
||||
agent_id="pre_decision_investigator",
|
||||
tool_name="kubectl_get",
|
||||
),
|
||||
SimpleNamespace(tool_name="kubectl_get"),
|
||||
{"namespace": "awoooi-prod", "api_key_value": "do-not-store"},
|
||||
)
|
||||
|
||||
sent = provider.calls[0][1]
|
||||
assert sent["api_key_value"] == "[REDACTED:CREDENTIAL_ISOLATION]"
|
||||
assert "plain-password" not in str(result.output)
|
||||
assert "10.0.1.5" not in str(result.output)
|
||||
|
||||
@@ -8,6 +8,7 @@ from src.plugins.mcp import gateway as gateway_module
|
||||
from src.plugins.mcp.gateway import (
|
||||
GateApprovalError,
|
||||
GateCheckResult,
|
||||
GateRateLimitError,
|
||||
GatewayContext,
|
||||
McpGateway,
|
||||
)
|
||||
@@ -24,6 +25,11 @@ class FakeRedis:
|
||||
async def aclose(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
async def eval(self, _script: str, _keys: int, key: str, _window: int) -> int:
|
||||
value = int(self.values.get(key, "0")) + 1
|
||||
self.values[key] = str(value)
|
||||
return value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gate5_uses_shared_redis_pool_without_closing(
|
||||
@@ -102,3 +108,48 @@ async def test_gate5_read_scope_skips_redis(
|
||||
|
||||
assert called is False
|
||||
assert gate_result.gate5_approval is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gateway_distributed_rate_limit_uses_redis_and_sets_receipt(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
redis = FakeRedis()
|
||||
monkeypatch.setattr(gateway_module, "get_redis", lambda: redis)
|
||||
gate_result = GateCheckResult()
|
||||
|
||||
await McpGateway(db=None)._enforce_distributed_rate_limit(
|
||||
GatewayContext(
|
||||
project_id="awoooi",
|
||||
agent_id="openclaw-sre",
|
||||
tool_name="k8s_get",
|
||||
required_scope="read",
|
||||
),
|
||||
gate_result,
|
||||
)
|
||||
|
||||
assert gate_result.distributed_rate_limit is True
|
||||
assert redis.values[
|
||||
"mcp_rate_limit:awoooi:openclaw-sre:k8s_get:read"
|
||||
] == "1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gateway_distributed_rate_limit_fails_closed_when_redis_is_down(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
def unavailable() -> FakeRedis:
|
||||
raise RuntimeError("redis unavailable")
|
||||
|
||||
monkeypatch.setattr(gateway_module, "get_redis", unavailable)
|
||||
|
||||
with pytest.raises(GateRateLimitError):
|
||||
await McpGateway(db=None)._enforce_distributed_rate_limit(
|
||||
GatewayContext(
|
||||
project_id="awoooi",
|
||||
agent_id="openclaw-sre",
|
||||
tool_name="k8s_apply",
|
||||
required_scope="write",
|
||||
),
|
||||
GateCheckResult(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user