oclaw/runtime/chat/ops_report_first_guard.py
oliver 4827278a9c Enforce report-first short intents and async large execManagedNe batches.
Block CLI/inventory until ume_alarm_xlsx_report (or aggregate) succeeds, and return job_id for big NE batches via get_ne_exec_job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 22:17:16 +08:00

161 lines
4.9 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.

"""Report-first gate for ops short intents: CLI/inventory only after report path."""
from __future__ import annotations
import json
from typing import Any
from runtime.application.gateway.ops_short_intent import ops_short_intent_should_filter_tools
from runtime.tools.playbook_contracts import playbook_example_for_tool, short_intent_first_step
# Tools blocked until a report-path tool succeeds this turn.
_CLI_BEFORE_REPORT_TOOLS = frozenset(
{
"listclitargets",
"listmanagedne",
"getmanagedne",
"execmanagedne",
"queryumeneinventory",
"getumene",
"findtopologypaths",
"sqlqueryume",
"netx_list_managed_ne",
"netx_get_managed_ne",
"netx_exec_managed_ne",
"netx_sql_query_ume",
"netx_list_cli_targets",
}
)
# Successful call to any of these unlocks CLI for the rest of the turn.
_REPORT_PATH_TOOLS = frozenset(
{
"ume_alarm_xlsx_report",
"aggregateumealarms",
"aggregateumealarmsraw",
}
)
def _tool_key(name: str) -> str:
raw = str(name or "").strip()
if not raw:
return ""
if "__" in raw:
raw = raw.rsplit("__", 1)[-1]
return raw.strip().lower().replace("-", "_")
def is_cli_before_report_tool(tool_name: str) -> bool:
return _tool_key(tool_name) in _CLI_BEFORE_REPORT_TOOLS
def is_report_path_tool(tool_name: str) -> bool:
return _tool_key(tool_name) in _REPORT_PATH_TOOLS
def _parse_json_obj(raw: Any) -> dict[str, Any]:
if isinstance(raw, dict):
return dict(raw)
if isinstance(raw, str) and raw.strip():
try:
data = json.loads(raw)
return data if isinstance(data, dict) else {}
except Exception:
return {}
return {}
def turn_has_successful_report_path(
store: Any,
*,
session_id: str,
turn_uuid: str,
) -> bool:
"""True if this turn already has a successful report/aggregate tool result."""
tu = str(turn_uuid or "").strip()
sid = str(session_id or "").strip()
if not tu or not sid or store is None:
return False
try:
rows = store.get_messages(session_id=sid, limit=500)
except Exception:
return False
for m in rows or []:
if str(getattr(m, "role", "") or "").strip().lower() != "tool":
continue
if str(getattr(m, "turn_uuid", "") or "").strip() != tu:
continue
ep = _parse_json_obj(getattr(m, "event_payload", None))
name = str(ep.get("tool_name") or "").strip()
if not name:
tc = _parse_json_obj(getattr(m, "tool_calls", None))
name = str(tc.get("name") or "").strip()
if not is_report_path_tool(name):
continue
if ep.get("ok") is True:
return True
try:
payload = json.loads(str(getattr(m, "content", "") or "") or "{}")
except Exception:
payload = {}
if isinstance(payload, dict) and payload.get("ok") is True:
return True
return False
def report_first_block_payload(*, intent: str | None, lang: str = "en") -> dict[str, Any]:
en = str(lang or "").strip().lower().startswith("en")
step = short_intent_first_step(intent)
tool = "ume_alarm_xlsx_report"
example = playbook_example_for_tool(tool, intent=intent) or {"mode": "list", "deliverable": True}
if step:
tool, example = step[0], step[1]
hint = (
f"Report-first gate: call {tool} successfully before CLI/inventory this turn. "
"Device confirmation via execManagedNe is allowed only after the report path returns ok."
if en
else f"报表优先:本轮先成功调用 {tool},再允许 CLI/清单。"
"设备核实(execManagedNe)仅在报表路径 ok 之后。"
)
return {
"ok": False,
"error_code": "report_first_required",
"failure_class": "report_first",
"error": "report_first_required",
"hint": hint,
"next_tool": tool,
"example": example,
"intent": str(intent or ""),
}
def maybe_block_cli_before_report(
*,
tool_name: str,
intent: str | None,
store: Any,
session_id: str,
turn_uuid: str,
lang: str = "en",
local_report_ok: bool = False,
) -> dict[str, Any] | None:
"""Return a block payload when CLI/inventory is used before report on short intents."""
if not ops_short_intent_should_filter_tools(intent):
return None
if not is_cli_before_report_tool(tool_name):
return None
if local_report_ok:
return None
if turn_has_successful_report_path(store, session_id=session_id, turn_uuid=turn_uuid):
return None
return report_first_block_payload(intent=intent, lang=lang)
__all__ = [
"is_cli_before_report_tool",
"is_report_path_tool",
"maybe_block_cli_before_report",
"report_first_block_payload",
"turn_has_successful_report_path",
]