import pytest from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult from src.plugins.mcp.registry import AuditedMCPToolProvider from src.services.ai_providers.agent_loop import AgentToolExecutor from src.services.ai_providers.interfaces import AIResult from src.services.ai_providers.permissions import ( filter_tools_for_agent, is_tool_allowed, ) from src.services.ai_providers.tool_schema import ( anthropic_tool_schema, openai_tool_schema, to_provider_tool_name, tool_by_provider_name, ) class FakeProvider(MCPToolProvider): def __init__(self, name="kubernetes"): self.calls = [] self._name = name @property def name(self): return self._name async def list_tools(self): return [] async def execute(self, tool_name, parameters): self.calls.append((tool_name, parameters)) return MCPToolResult(success=True, execution_id="exec-1", output={"ok": True}) def _tool(server: str, name: str) -> MCPTool: return MCPTool( name=name, description=f"{server}.{name}", input_schema={"type": "object", "properties": {}}, server_name=server, ) def test_agent_tool_permissions_are_role_scoped(): k8s_get = _tool("kubernetes", "kubectl_get") k8s_restart = _tool("kubernetes", "kubectl_restart") prom_query = _tool("prometheus", "prometheus_query") db_tool = _tool("database", "write_km_entry") assert is_tool_allowed(k8s_restart, "openclaw") is True assert is_tool_allowed(k8s_get, "nemotron") is True assert is_tool_allowed(k8s_restart, "nemotron") is False assert is_tool_allowed(prom_query, "hermes") is True assert is_tool_allowed(db_tool, "hermes") is True assert is_tool_allowed(k8s_restart, "elephant_alpha") is False assert is_tool_allowed(_tool("database", "execute_sql"), "elephant_alpha") is False assert is_tool_allowed(_tool("ssh_host", "run_command"), "elephant_alpha") is False filtered = filter_tools_for_agent([k8s_get, k8s_restart, prom_query], "nemotron") assert [tool.name for tool in filtered] == ["kubectl_get", "prometheus_query"] def test_tool_schema_round_trips_provider_safe_names(): tool = _tool("kubernetes", "kubectl_get") safe_name = to_provider_tool_name(tool) assert safe_name == "kubernetes__kubectl_get" assert anthropic_tool_schema(tool)["name"] == safe_name assert openai_tool_schema(tool)["function"]["name"] == safe_name assert tool_by_provider_name([tool], safe_name) is tool def test_openclaw_agent_loop_shadow_parser_normalizes_json(): from src.services.openclaw import OpenClawService raw = """```json { "root_cause_check": "current RCA still needs pod evidence", "evidence_used": ["event spike", "error rate"], "confidence_delta": -0.42, "missing_evidence": ["deployment rollout history"], "human_or_ai_next_step": "query rollout history with read-only tools" } ```""" parsed = OpenClawService._parse_agent_loop_shadow_response(raw) assert parsed["parse_status"] == "ok" assert parsed["root_cause_check"] == "current RCA still needs pod evidence" assert parsed["evidence_used"] == ["event spike", "error rate"] assert parsed["confidence_delta"] == -0.15 assert parsed["missing_evidence"] == ["deployment rollout history"] def test_openclaw_agent_loop_shadow_parser_never_boosts_confidence(): from src.services.openclaw import OpenClawService parsed = OpenClawService._parse_agent_loop_shadow_response( '{"root_cause_check":"looks good","confidence_delta":0.2}' ) assert parsed["confidence_delta"] == 0.0 @pytest.mark.asyncio async def test_audited_provider_strips_internal_audit_context(monkeypatch): audit_calls = [] async def fake_record_mcp_call(**kwargs): audit_calls.append(kwargs) monkeypatch.setattr( "src.services.mcp_audit_service.record_mcp_call", fake_record_mcp_call, ) provider = FakeProvider() audited = AuditedMCPToolProvider(provider) result = await audited.execute( "kubectl_get", { "resource": "pods", "_mcp_audit": { "agent_role": "openclaw", "session_id": "session-1", "incident_id": "INC-1", "flywheel_node": "sense", }, }, ) assert result.success is True assert provider.calls == [("kubectl_get", {"resource": "pods"})] assert audit_calls[0]["agent_role"] == "openclaw" assert audit_calls[0]["session_id"] == "session-1" assert audit_calls[0]["incident_id"] == "INC-1" @pytest.mark.asyncio async def test_agent_tool_executor_blocks_disallowed_tool(): restart_tool = _tool("kubernetes", "kubectl_restart") provider = FakeProvider() executor = AgentToolExecutor( available_tools=[restart_tool], providers={"kubernetes": provider}, agent_role="nemotron", incident_id="INC-1", ) result = await executor.execute("kubernetes__kubectl_restart", {"deployment": "api"}) assert result.success is False assert "not allowed" in (result.error or "") assert provider.calls == [] @pytest.mark.asyncio async def test_openclaw_agent_loop_shadow_uses_read_only_tools(monkeypatch): from src.core.config import settings from src.services.openclaw import OpenClawService monkeypatch.setattr(settings, "ENABLE_OPENCLAW_AGENT_LOOP_SHADOW", True) monkeypatch.setattr(settings, "OPENCLAW_AGENT_LOOP_MAX_ITERATIONS", 2) class FakeAIProvider: def __init__(self): self.seen_tools = [] async def analyze_with_tools( self, prompt, available_tools, tool_executor, max_iterations=5, agent_role="openclaw", context=None, ): self.seen_tools = available_tools return AIResult( raw_response='{"root_cause_check":"ok"}', success=True, provider="ollama_agent_loop", latency_ms=12.3, ) class FakeAIRegistry: def __init__(self, provider): self.provider = provider def get(self, name): return self.provider if name == "ollama" else None class FakeMCPRegistry: def all(self): return [FakeProvider("database"), FakeProvider("ssh_host")] async def fake_list_tools(self): if self.name == "database": return [ _tool("database", "list_incidents"), _tool("database", "execute_sql"), ] return [_tool("ssh_host", "run_command")] fake_ai_provider = FakeAIProvider() monkeypatch.setattr(FakeProvider, "list_tools", fake_list_tools) monkeypatch.setattr("src.services.ai_router.get_ai_registry", lambda: FakeAIRegistry(fake_ai_provider)) monkeypatch.setattr("src.plugins.mcp.registry.get_provider_registry", lambda: FakeMCPRegistry()) service = object.__new__(OpenClawService) proposal = {"risk_level": "medium", "action": "investigate"} await service._maybe_run_openclaw_agent_loop_shadow( proposal=proposal, incident_id="INC-1", severity="P2", signals=[{"alertname": "ApiErrorRateHigh"}], affected_services=["awoooi-api"], expert_context={"source": "test"}, ) assert proposal["agent_loop_shadow"]["success"] is True assert proposal["agent_loop_shadow"]["decision_impact"] == "none" assert proposal["agent_loop_shadow"]["structured"]["parse_status"] == "ok" assert proposal["agent_loop_shadow"]["structured"]["root_cause_check"] == "ok" assert [tool.name for tool in fake_ai_provider.seen_tools] == ["list_incidents"]