feat(aiops): enable read-only agent loop canary
All checks were successful
CD Pipeline / tests (push) Successful in 1m43s
Code Review / ai-code-review (push) Successful in 31s
CD Pipeline / build-and-deploy (push) Successful in 10m22s
CD Pipeline / post-deploy-checks (push) Successful in 4m3s

This commit is contained in:
Your Name
2026-05-01 14:20:16 +08:00
parent 33a7148916
commit f8e44971c1
9 changed files with 315 additions and 8 deletions

View File

@@ -1709,6 +1709,153 @@ Focus on:
# 2026-03-31 Claude Code: 統帥批准實作
# =========================================================================
async def _maybe_run_openclaw_agent_loop_shadow(
self,
*,
proposal: dict,
incident_id: str,
severity: str,
signals: list[dict],
affected_services: list[str],
expert_context: dict | None = None,
) -> None:
"""
ADR-105 P1: read-only Agent Loop shadow investigation.
This is intentionally non-decisive: it proves MCP tool_use/audit wiring
with local models and read-only tools, then falls back silently to the
regular proposal when disabled or unavailable.
"""
if not settings.ENABLE_OPENCLAW_AGENT_LOOP_SHADOW:
return
try:
from src.plugins.mcp.registry import get_provider_registry
from src.services.ai_providers.agent_loop import AgentToolExecutor
from src.services.ai_providers.permissions import is_read_only_tool
from src.services.ai_router import get_ai_registry
ai_registry = get_ai_registry()
provider = ai_registry.get("ollama") or ai_registry.get("ollama_188")
if provider is None or not hasattr(provider, "analyze_with_tools"):
logger.warning(
"openclaw_agent_loop_shadow_skipped",
incident_id=incident_id,
reason="no_local_tool_provider",
)
return
mcp_registry = get_provider_registry()
providers = {p.name: p for p in mcp_registry.all()}
allowed_servers = {"kubernetes", "prometheus", "signoz", "database", "rag", "grafana"}
available_tools = []
for mcp_provider in providers.values():
if mcp_provider.name not in allowed_servers:
continue
try:
provider_tools = await mcp_provider.list_tools()
except Exception as exc:
logger.warning(
"openclaw_agent_loop_tool_list_failed",
incident_id=incident_id,
provider=mcp_provider.name,
error=str(exc),
)
continue
available_tools.extend(
tool for tool in provider_tools
if tool.server_name in allowed_servers and is_read_only_tool(tool)
)
if not available_tools:
logger.warning(
"openclaw_agent_loop_shadow_skipped",
incident_id=incident_id,
reason="no_readonly_tools",
)
return
executor = AgentToolExecutor(
available_tools=available_tools,
providers=providers,
agent_role="openclaw",
incident_id=incident_id,
flywheel_node="reason",
)
shadow_prompt = self._build_agent_loop_shadow_prompt(
proposal=proposal,
incident_id=incident_id,
severity=severity,
signals=signals,
affected_services=affected_services,
expert_context=expert_context,
)
result = await provider.analyze_with_tools(
prompt=shadow_prompt,
available_tools=available_tools,
tool_executor=executor.execute,
max_iterations=settings.OPENCLAW_AGENT_LOOP_MAX_ITERATIONS,
agent_role="openclaw",
context={
"incident_id": incident_id,
"severity": severity,
"task_type": "diagnose",
},
)
proposal["agent_loop_shadow"] = {
"enabled": True,
"success": result.success,
"provider": result.provider,
"tokens": result.tokens,
"latency_ms": round(result.latency_ms, 1),
"error": result.error,
"preview": (result.raw_response or "")[:700],
}
logger.info(
"openclaw_agent_loop_shadow_complete",
incident_id=incident_id,
provider=result.provider,
success=result.success,
tools_available=len(available_tools),
latency_ms=round(result.latency_ms, 1),
)
except Exception as exc:
logger.warning(
"openclaw_agent_loop_shadow_failed",
incident_id=incident_id,
error=str(exc),
)
def _build_agent_loop_shadow_prompt(
self,
*,
proposal: dict,
incident_id: str,
severity: str,
signals: list[dict],
affected_services: list[str],
expert_context: dict | None = None,
) -> str:
"""Build a compact read-only investigation prompt for Agent Loop shadow mode."""
return f"""你是 OpenClaw 的唯讀 shadow investigator。你可以使用 MCP 工具查證,但不得要求任何寫入、重啟、刪除或通知動作。
請只回傳 JSON
{{
"root_cause_check": "你對目前根因的查證結論",
"evidence_used": ["最多 5 條具體證據"],
"confidence_delta": -0.1,
"missing_evidence": ["還缺什麼證據"],
"human_or_ai_next_step": "下一個安全步驟"
}}
Incident: {incident_id}
Severity: {severity}
Affected services: {json.dumps(affected_services, ensure_ascii=False)}
Signals: {json.dumps(signals[:5], ensure_ascii=False, default=str)}
Current proposal: {json.dumps(proposal, ensure_ascii=False, default=str)}
Expert context: {json.dumps(expert_context or {}, ensure_ascii=False, default=str)}
"""
async def generate_incident_proposal_with_tools(
self,
incident_id: str,
@@ -1762,6 +1909,15 @@ Focus on:
if not success or proposal is None:
return proposal, provider, success
await self._maybe_run_openclaw_agent_loop_shadow(
proposal=proposal,
incident_id=incident_id,
severity=severity,
signals=signals,
affected_services=affected_services,
expert_context=expert_context,
)
# Step 2: 判斷是否需要 Nemotron
risk_level = proposal.get("risk_level", "low").lower()
if risk_level == "low":