mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
Ship Cursor-only MCP admin and modular Admin/Chat UI.
Plugins/Skills install and edit via mcpServers JSON, with clearer row actions, soft reloads, and instant loading placeholders; also drop MCP market and harden related scheduler/logging/exec guards. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2aff012b48
commit
f6f129931f
61 changed files with 18057 additions and 16327 deletions
182
runtime/chat/exec_managed_ne_guard.py
Normal file
182
runtime/chat/exec_managed_ne_guard.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
"""Guards for mcp__netx__execManagedNe / netx_exec_managed_ne spam on field turns."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _env_int(name: str, default: int, *, min_v: int = 1, max_v: int = 50) -> int:
|
||||
raw = str(os.getenv(name) or "").strip()
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
n = int(raw)
|
||||
except Exception:
|
||||
return default
|
||||
return max(min_v, min(int(n), max_v))
|
||||
|
||||
|
||||
def exec_managed_ne_single_budget() -> int:
|
||||
"""Max single-NE execManagedNe calls per turn before requiring ne_ids/ume_ne_ids batch."""
|
||||
return _env_int("AIA_EXEC_MANAGED_NE_SINGLE_BUDGET", 6, min_v=2, max_v=30)
|
||||
|
||||
|
||||
def exec_managed_ne_fail_budget() -> int:
|
||||
"""Max failed execManagedNe (any mode) per turn before blocking further single-NE calls."""
|
||||
return _env_int("AIA_EXEC_MANAGED_NE_FAIL_BUDGET", 5, min_v=2, max_v=30)
|
||||
|
||||
|
||||
def is_exec_managed_ne_tool(name: str) -> bool:
|
||||
raw = str(name or "").strip()
|
||||
if not raw:
|
||||
return False
|
||||
if "__" in raw:
|
||||
raw = raw.rsplit("__", 1)[-1]
|
||||
key = raw.strip().lower().replace("-", "_")
|
||||
return key in {"execmanagedne", "netx_exec_managed_ne", "exec_managed_ne"}
|
||||
|
||||
|
||||
def is_batch_exec_args(args: dict[str, Any] | None) -> bool:
|
||||
a = args if isinstance(args, dict) else {}
|
||||
for key in ("ne_ids", "ume_ne_ids", "targets"):
|
||||
val = a.get(key)
|
||||
if isinstance(val, list) and len(val) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def normalize_exec_managed_ne_args(args: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Default / clamp read_timeout_sec so agents stop hitting 30s walls."""
|
||||
out = dict(args or {})
|
||||
rts = out.get("read_timeout_sec")
|
||||
if rts is None or str(rts).strip() == "":
|
||||
out["read_timeout_sec"] = 60
|
||||
else:
|
||||
try:
|
||||
out["read_timeout_sec"] = max(10, min(120, int(rts)))
|
||||
except Exception:
|
||||
out["read_timeout_sec"] = 60
|
||||
return out
|
||||
|
||||
|
||||
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 load_turn_exec_managed_ne_stats(
|
||||
store: Any,
|
||||
*,
|
||||
session_id: str,
|
||||
turn_uuid: str,
|
||||
) -> tuple[int, int, int]:
|
||||
"""Return (single_calls, batch_calls, fail_calls) already persisted this turn."""
|
||||
tu = str(turn_uuid or "").strip()
|
||||
sid = str(session_id or "").strip()
|
||||
single = batch = fails = 0
|
||||
if not tu or not sid:
|
||||
return single, batch, fails
|
||||
try:
|
||||
rows = store.get_messages(session_id=sid, limit=500)
|
||||
except Exception:
|
||||
return single, batch, fails
|
||||
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:
|
||||
raw_tc = getattr(m, "tool_calls", None)
|
||||
tc = _parse_json_obj(raw_tc)
|
||||
name = str(tc.get("name") or "").strip()
|
||||
if not is_exec_managed_ne_tool(name):
|
||||
continue
|
||||
mode = str(ep.get("exec_ne_mode") or "").strip().lower()
|
||||
if mode == "batch":
|
||||
batch += 1
|
||||
else:
|
||||
# Missing mode (older rows) → treat as single (conservative).
|
||||
single += 1
|
||||
if ep.get("ok") is False:
|
||||
fails += 1
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(str(getattr(m, "content", "") or "") or "{}")
|
||||
except Exception:
|
||||
payload = {}
|
||||
if isinstance(payload, dict) and payload.get("ok") is False:
|
||||
fails += 1
|
||||
return single, batch, fails
|
||||
|
||||
|
||||
def budget_block_payload(
|
||||
*,
|
||||
reason: str,
|
||||
lang: str = "en",
|
||||
single_used: int,
|
||||
single_budget: int,
|
||||
fail_used: int = 0,
|
||||
fail_budget: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
en = str(lang or "").strip().lower().startswith("en")
|
||||
if reason == "fail_budget":
|
||||
code = "cli_fail_budget_exceeded"
|
||||
hint = (
|
||||
f"execManagedNe already failed {fail_used}/{fail_budget} times this turn. "
|
||||
"Stop one-NE loops; use one execManagedNe(ne_ids|ume_ne_ids=..., commands=...) batch "
|
||||
"or summarize reachable failures — do not keep probing."
|
||||
if en
|
||||
else f"本轮 execManagedNe 已失败 {fail_used}/{fail_budget} 次。"
|
||||
"停止单台循环;改用一次 ne_ids/ume_ne_ids 批量,或汇总可达性失败,勿继续盲探。"
|
||||
)
|
||||
err = "cli_fail_budget_exceeded"
|
||||
else:
|
||||
code = "cli_call_budget_exceeded"
|
||||
hint = (
|
||||
f"Single-NE execManagedNe budget exhausted ({single_used}/{single_budget} this turn). "
|
||||
"For more NEs call ONE execManagedNe with ne_ids[] or ume_ne_ids[] (shared commands). "
|
||||
"Do not loop one-NE execManagedNe."
|
||||
if en
|
||||
else f"单台 execManagedNe 预算已用尽(本轮 {single_used}/{single_budget})。"
|
||||
"更多网元请一次传入 ne_ids[] / ume_ne_ids[] 批量执行,禁止逐台循环。"
|
||||
)
|
||||
err = "cli_call_budget_exceeded"
|
||||
return {
|
||||
"ok": False,
|
||||
"error_code": code,
|
||||
"failure_class": "retry_guard",
|
||||
"error": err,
|
||||
"hint": hint,
|
||||
"example": {
|
||||
"ume_ne_ids": ["<id1>", "<id2>", "<id3>"],
|
||||
"commands": ["show version"],
|
||||
"read_timeout_sec": 90,
|
||||
"concurrency": 4,
|
||||
},
|
||||
"single_used": int(single_used),
|
||||
"single_budget": int(single_budget),
|
||||
"fail_used": int(fail_used),
|
||||
"fail_budget": int(fail_budget),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"budget_block_payload",
|
||||
"exec_managed_ne_fail_budget",
|
||||
"exec_managed_ne_single_budget",
|
||||
"is_batch_exec_args",
|
||||
"is_exec_managed_ne_tool",
|
||||
"load_turn_exec_managed_ne_stats",
|
||||
"normalize_exec_managed_ne_args",
|
||||
]
|
||||
|
|
@ -1087,6 +1087,24 @@ class ToolExecutor:
|
|||
session_id=ctx.session_id,
|
||||
turn_uuid=str(ctx.turn_uuid or ""),
|
||||
)
|
||||
from runtime.chat.exec_managed_ne_guard import (
|
||||
budget_block_payload,
|
||||
exec_managed_ne_fail_budget,
|
||||
exec_managed_ne_single_budget,
|
||||
is_batch_exec_args,
|
||||
is_exec_managed_ne_tool,
|
||||
load_turn_exec_managed_ne_stats,
|
||||
)
|
||||
|
||||
prior_single_exec, _prior_batch_exec, prior_exec_fails = load_turn_exec_managed_ne_stats(
|
||||
ctx.store,
|
||||
session_id=ctx.session_id,
|
||||
turn_uuid=str(ctx.turn_uuid or ""),
|
||||
)
|
||||
single_exec_budget = exec_managed_ne_single_budget()
|
||||
fail_exec_budget = exec_managed_ne_fail_budget()
|
||||
local_single_exec = 0
|
||||
local_exec_fails = 0
|
||||
|
||||
results_by_id: dict[str, tuple[dict[str, Any], int]] = {}
|
||||
runnable_tool_uses: list[LLMToolCall] = []
|
||||
|
|
@ -1216,6 +1234,48 @@ class ToolExecutor:
|
|||
},
|
||||
)
|
||||
continue
|
||||
if is_exec_managed_ne_tool(tool_name):
|
||||
batchish = is_batch_exec_args(dict(tc.arguments or {}))
|
||||
single_used = int(prior_single_exec) + int(local_single_exec)
|
||||
fail_used = int(prior_exec_fails) + int(local_exec_fails)
|
||||
if (not batchish) and fail_used >= int(fail_exec_budget):
|
||||
results_by_id[tc.id] = (
|
||||
budget_block_payload(
|
||||
reason="fail_budget",
|
||||
lang=str(ctx.lang or "en"),
|
||||
single_used=single_used,
|
||||
single_budget=single_exec_budget,
|
||||
fail_used=fail_used,
|
||||
fail_budget=fail_exec_budget,
|
||||
),
|
||||
0,
|
||||
)
|
||||
_trace(
|
||||
"cli_fail_budget_exceeded",
|
||||
{"tool_name": tc.name, "fail_used": fail_used, "fail_budget": fail_exec_budget},
|
||||
)
|
||||
continue
|
||||
if (not batchish) and single_used >= int(single_exec_budget):
|
||||
results_by_id[tc.id] = (
|
||||
budget_block_payload(
|
||||
reason="call_budget",
|
||||
lang=str(ctx.lang or "en"),
|
||||
single_used=single_used,
|
||||
single_budget=single_exec_budget,
|
||||
fail_used=fail_used,
|
||||
fail_budget=fail_exec_budget,
|
||||
),
|
||||
0,
|
||||
)
|
||||
_trace(
|
||||
"cli_call_budget_exceeded",
|
||||
{
|
||||
"tool_name": tc.name,
|
||||
"single_used": single_used,
|
||||
"single_budget": single_exec_budget,
|
||||
},
|
||||
)
|
||||
continue
|
||||
count = int(sig_seen.get(sig, 0))
|
||||
name_low = str(tc.name or "").strip().lower()
|
||||
listish = name_low.endswith(
|
||||
|
|
@ -1270,6 +1330,8 @@ class ToolExecutor:
|
|||
)
|
||||
continue
|
||||
first_tool_call_id_by_signature[sig] = str(tc.id or "")
|
||||
if is_exec_managed_ne_tool(tool_name) and not is_batch_exec_args(dict(tc.arguments or {})):
|
||||
local_single_exec += 1
|
||||
runnable_tool_uses.append(tc)
|
||||
|
||||
for batch in partition_tool_use_batches(runnable_tool_uses, ctx.tools):
|
||||
|
|
@ -1332,6 +1394,16 @@ class ToolExecutor:
|
|||
result = normalize_tool_result(result)
|
||||
if isinstance(result, dict) and result.get("ok") is False:
|
||||
failed_signatures.add(f"{tc.name}:{self._json_dumps_safe(dict(tc.arguments or {}))}")
|
||||
if is_exec_managed_ne_tool(str(tc.name or "")):
|
||||
# Count blocked budget responses too so fail-budget can engage same turn.
|
||||
if str(result.get("error_code") or "") not in {
|
||||
"cli_call_budget_exceeded",
|
||||
"cli_fail_budget_exceeded",
|
||||
"identical_retry_blocked",
|
||||
"retry_forbidden_blocked",
|
||||
"tool_loop_guard",
|
||||
}:
|
||||
local_exec_fails += 1
|
||||
if isinstance(result, dict) and _result_is_retry_forbidden(result):
|
||||
retry_forbidden_tools.add(str(tc.name or ""))
|
||||
persisted_result, ingested_refs = ingest_embedded_image_blobs_as_refs(
|
||||
|
|
@ -1376,6 +1448,9 @@ class ToolExecutor:
|
|||
tool_content = self._json_dumps_safe(result_for_llm)
|
||||
t_db2 = time.perf_counter()
|
||||
tool_sig = f"{tc.name}:{self._json_dumps_safe(dict(tc.arguments or {}))}"
|
||||
exec_ne_mode = ""
|
||||
if is_exec_managed_ne_tool(str(tc.name or "")):
|
||||
exec_ne_mode = "batch" if is_batch_exec_args(dict(tc.arguments or {})) else "single"
|
||||
msg_row = ctx.store.add_message(
|
||||
session_id=ctx.session_id,
|
||||
role="tool",
|
||||
|
|
@ -1393,6 +1468,7 @@ class ToolExecutor:
|
|||
"retry_forbidden": bool(
|
||||
isinstance(result, dict) and _result_is_retry_forbidden(result)
|
||||
),
|
||||
**({"exec_ne_mode": exec_ne_mode} if exec_ne_mode else {}),
|
||||
},
|
||||
)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ class TurnIdleTracker:
|
|||
"identical_retry_blocked",
|
||||
"retry_forbidden_blocked",
|
||||
"tool_loop_guard",
|
||||
"cli_call_budget_exceeded",
|
||||
"cli_fail_budget_exceeded",
|
||||
}:
|
||||
guard += 1
|
||||
stats = RoundStats(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue