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:
oliver 2026-08-12 19:38:26 +08:00
parent 2aff012b48
commit f6f129931f
61 changed files with 18057 additions and 16327 deletions

View file

@ -89,6 +89,13 @@ class AdminScheduledJobsApiTests(unittest.TestCase):
runs = self.client.get(f"/admin/api/scheduled-jobs/{job_id}/runs", headers=headers)
self.assertTrue(runs.json().get("ok"))
stats = self.client.get("/admin/api/scheduled-jobs/stats", headers=headers)
self.assertEqual(stats.status_code, 200, stats.text)
body = stats.json()
self.assertTrue(body.get("ok"), body)
self.assertIn("recent_fail_rate_pct", body)
self.assertIn("jobs_by_status", body)
patch = self.client.patch(
f"/admin/api/scheduled-jobs/{job_id}",
headers=headers,

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""Unit tests for execManagedNe arg normalize / detect helpers."""
from __future__ import annotations
from runtime.chat.exec_managed_ne_guard import (
is_batch_exec_args,
is_exec_managed_ne_tool,
normalize_exec_managed_ne_args,
)
def test_is_exec_managed_ne_tool_aliases() -> None:
assert is_exec_managed_ne_tool("mcp__netx__execManagedNe")
assert is_exec_managed_ne_tool("netx_exec_managed_ne")
assert not is_exec_managed_ne_tool("mcp__netx__listCliTargets")
def test_is_batch_exec_args() -> None:
assert is_batch_exec_args({"ne_ids": ["a", "b"], "commands": ["show version"]})
assert is_batch_exec_args({"ume_ne_ids": ["u1"]})
assert is_batch_exec_args({"targets": [{"ne_id": "x"}]})
assert not is_batch_exec_args({"ne_id": "x", "commands": ["show version"]})
assert not is_batch_exec_args({"ne_ids": []})
def test_normalize_exec_managed_ne_args_defaults_and_clamps() -> None:
assert normalize_exec_managed_ne_args({})["read_timeout_sec"] == 60
assert normalize_exec_managed_ne_args({"read_timeout_sec": 5})["read_timeout_sec"] == 10
assert normalize_exec_managed_ne_args({"read_timeout_sec": 999})["read_timeout_sec"] == 120
assert normalize_exec_managed_ne_args({"read_timeout_sec": 90})["read_timeout_sec"] == 90

View file

@ -130,15 +130,16 @@ def test_retry_forbidden_blocks_same_tool_different_args(tmp_path: Path) -> None
assert blocked.get("retry_forbidden") is True
def test_distinct_exec_managed_ne_calls_not_capped_by_count(tmp_path: Path) -> None:
"""Field ops may legitimately CLI many NEs; only identical-arg loops are blocked."""
def test_distinct_exec_managed_ne_calls_respect_single_budget(tmp_path: Path, monkeypatch) -> None:
"""Single-NE loops are capped; batch ne_ids remains allowed after the budget."""
monkeypatch.setenv("AIA_EXEC_MANAGED_NE_SINGLE_BUDGET", "3")
store = SqliteStore(str(tmp_path / "cli.sqlite"))
sess = store.create_session("t")
calls = {"n": 0}
def _handler(args):
calls["n"] += 1
return {"ok": True, "data": {"ne_id": args.get("ne_id"), "n": calls["n"]}}
return {"ok": True, "data": {"ne_id": args.get("ne_id") or args.get("ne_ids"), "n": calls["n"]}}
reg = ToolRegistry(
[
@ -158,7 +159,7 @@ def test_distinct_exec_managed_ne_calls_not_capped_by_count(tmp_path: Path) -> N
turn_uuid="turn-cli-many",
lang="en",
)
for i in range(8):
for i in range(3):
ToolExecutor().execute_tool_uses(
ctx=ctx,
assistant_msg_id=i + 1,
@ -171,4 +172,96 @@ def test_distinct_exec_managed_ne_calls_not_capped_by_count(tmp_path: Path) -> N
],
signature_budget=2,
)
assert calls["n"] == 8
assert calls["n"] == 3
_, blocked_results = ToolExecutor().execute_tool_uses(
ctx=ctx,
assistant_msg_id=99,
tool_uses=[
LLMToolCall(
id="blocked",
name="mcp__netx__execManagedNe",
arguments={"ne_id": "ne-x", "commands": ["disp"]},
)
],
signature_budget=2,
)
blocked, _ = blocked_results["blocked"]
assert calls["n"] == 3
assert blocked.get("error_code") == "cli_call_budget_exceeded"
_, batch_results = ToolExecutor().execute_tool_uses(
ctx=ctx,
assistant_msg_id=100,
tool_uses=[
LLMToolCall(
id="batch",
name="mcp__netx__execManagedNe",
arguments={"ne_ids": ["a", "b", "c"], "commands": ["show version"]},
)
],
signature_budget=2,
)
batch_out, _ = batch_results["batch"]
assert calls["n"] == 4
assert batch_out.get("ok") is True
def test_exec_managed_ne_fail_budget_blocks_further_single(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("AIA_EXEC_MANAGED_NE_FAIL_BUDGET", "2")
monkeypatch.setenv("AIA_EXEC_MANAGED_NE_SINGLE_BUDGET", "10")
store = SqliteStore(str(tmp_path / "cli-fail.sqlite"))
sess = store.create_session("t")
calls = {"n": 0}
def _handler(_args):
calls["n"] += 1
return {"ok": False, "error_code": "tool_timeout_or_failed", "error": "timeout"}
reg = ToolRegistry(
[
ToolSpec(
name="mcp__netx__execManagedNe",
description="exec",
parameters={"type": "object", "properties": {"ne_id": {"type": "string"}}},
handler=_handler,
read_only=False,
)
]
)
ctx = ToolExecutionContext(
store=store,
tools=reg,
session_id=sess.id,
turn_uuid="turn-cli-fail",
lang="en",
)
for i in range(2):
ToolExecutor().execute_tool_uses(
ctx=ctx,
assistant_msg_id=i + 1,
tool_uses=[
LLMToolCall(
id=f"f{i}",
name="mcp__netx__execManagedNe",
arguments={"ne_id": f"ne-{i}", "commands": ["disp"]},
)
],
signature_budget=2,
)
assert calls["n"] == 2
_, results = ToolExecutor().execute_tool_uses(
ctx=ctx,
assistant_msg_id=9,
tool_uses=[
LLMToolCall(
id="f3",
name="mcp__netx__execManagedNe",
arguments={"ne_id": "ne-9", "commands": ["disp"]},
)
],
signature_budget=2,
)
blocked, _ = results["f3"]
assert calls["n"] == 2
assert blocked.get("error_code") == "cli_fail_budget_exceeded"

View file

@ -89,3 +89,50 @@ def test_oclaw_hooks_log_dir_legacy_when_env_set(
state.mkdir()
d = log_paths.oclaw_hooks_log_dir(state_dir_if_legacy=state)
assert d == (state / "logs").resolve()
def test_safe_rotating_handler_swallows_permission_error(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
import logging.handlers
from svc.observability.logging_setup import SafeRotatingFileHandler
path = tmp_path / "oclaw.log"
path.write_text("seed\n", encoding="utf-8")
handler = SafeRotatingFileHandler(str(path), maxBytes=1_000_000, backupCount=1, encoding="utf-8")
def _boom(self) -> None: # noqa: ANN001
raise PermissionError("WinError 32 simulated")
monkeypatch.setattr(logging.handlers.RotatingFileHandler, "doRollover", _boom)
handler.doRollover() # must not raise
handler.close()
def test_safe_rotating_handler_swallows_winerror_32(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
import logging.handlers
from svc.observability.logging_setup import SafeRotatingFileHandler
path = tmp_path / "oclaw2.log"
path.write_text("seed\n", encoding="utf-8")
handler = SafeRotatingFileHandler(str(path), maxBytes=1_000_000, backupCount=1, encoding="utf-8")
def _boom(self) -> None: # noqa: ANN001
err = OSError("locked")
err.winerror = 32 # type: ignore[attr-defined]
raise err
monkeypatch.setattr(logging.handlers.RotatingFileHandler, "doRollover", _boom)
handler.doRollover()
handler.close()
def test_worker_dict_config_uses_safe_rotating_handler(tmp_path: Path) -> None:
cfg = logging_setup.build_worker_logging_dict_config(log_root=tmp_path / "logs")
assert cfg["handlers"]["oclaw_file"]["class"] == "svc.observability.logging_setup.SafeRotatingFileHandler"

View file

@ -93,11 +93,18 @@ class McpAdminApiTests(unittest.TestCase):
ins = self.client.post(
"/admin/api/mcp/install",
json={"source_type": "npm", "source_ref": "demo-mcp", "server_id": "demo-mcp", "entry_command": "python", "entry_args": ["-V"], "dry_run": True},
json={
"mcpServers": {
"demo-mcp": {"command": "python", "args": ["-V"]},
},
"dry_run": True,
},
headers=self._headers(),
)
self.assertEqual(ins.status_code, 200)
self.assertTrue(ins.json().get("ok"), ins.json())
data = ins.json()
self.assertTrue(data.get("ok"), data)
self.assertIn("demo-mcp", data.get("installed") or [])
toggle = self.client.post(
"/admin/api/mcp/toggle",
@ -120,6 +127,52 @@ class McpAdminApiTests(unittest.TestCase):
items = failures.json().get("items") or []
self.assertTrue(any(str(x.get("server_id") or "") == "demo-mcp" for x in items))
def test_cursor_install_config_and_export(self) -> None:
ins = self.client.post(
"/admin/api/mcp/install",
json={
"mcpServers": {
"remote-demo": {
"url": "https://example.com/mcp",
"headers": {"Authorization": "Bearer ${DEMO_TOKEN}"},
}
},
"dry_run": True,
},
headers=self._headers(),
)
self.assertEqual(ins.status_code, 200)
self.assertTrue(ins.json().get("ok"), ins.json())
cfg = self.client.post(
"/admin/api/mcp/config",
json={
"server_id": "remote-demo",
"url": "https://example.com/mcp-v2",
"headers": {"Authorization": "Bearer ${DEMO_TOKEN}"},
"enabled": True,
"timeout_s": 45,
},
headers=self._headers(),
)
self.assertEqual(cfg.status_code, 200)
cfg_data = cfg.json()
self.assertTrue(cfg_data.get("ok"), cfg_data)
self.assertEqual(str((cfg_data.get("cursor") or {}).get("url") or ""), "https://example.com/mcp-v2")
servers = self.client.get("/admin/api/mcp/servers", headers=self._headers())
self.assertEqual(servers.status_code, 200)
rows = servers.json().get("servers") or []
row = next((x for x in rows if str(x.get("server_id") or "") == "remote-demo"), None)
self.assertTrue(isinstance(row, dict), rows)
self.assertEqual(str((row.get("cursor") or {}).get("url") or ""), "https://example.com/mcp-v2")
export = self.client.get("/admin/api/mcp/export", headers=self._headers())
self.assertEqual(export.status_code, 200)
doc = export.json().get("document") or {}
self.assertIn("mcpServers", doc)
self.assertIn("remote-demo", doc.get("mcpServers") or {})
self.assertNotIn("servers", doc)
def test_healthcheck_and_tools_sync(self) -> None:
script = self._write_mcp_server()
store = get_assistant_store()

View file

@ -0,0 +1,78 @@
from __future__ import annotations
import unittest
from runtime.tools.mcp.cursor_config import (
build_cursor_mcp_export,
config_payload_to_upsert_fields,
parse_cursor_mcp_document,
registry_row_to_cursor_server,
)
class CursorConfigTests(unittest.TestCase):
def test_parse_stdio_and_remote(self) -> None:
items = parse_cursor_mcp_document(
{
"mcpServers": {
"fetch": {"command": "npx", "args": ["-y", "mcp-fetch-server"], "env": {"A": "1"}},
"remote": {
"url": "https://example.com/mcp",
"headers": {"Authorization": "Bearer ${TOKEN}"},
},
}
}
)
by_id = {x["server_id"]: x for x in items}
self.assertEqual(by_id["fetch"]["source_type"], "local")
self.assertEqual(by_id["fetch"]["entry_command"], "npx")
self.assertEqual(by_id["fetch"]["entry_args"], ["-y", "mcp-fetch-server"])
self.assertIn("A", by_id["fetch"]["env_schema"])
self.assertEqual(by_id["remote"]["source_type"], "npm")
self.assertEqual(by_id["remote"]["source_ref"], "mcp-remote")
self.assertEqual(by_id["remote"]["entry_command"], "npx")
self.assertIn("mcp-remote", by_id["remote"]["entry_args"])
self.assertIn("https://example.com/mcp", by_id["remote"]["entry_args"])
self.assertIn("TOKEN", by_id["remote"]["env_schema"])
def test_url_only_without_type_is_remote(self) -> None:
items = parse_cursor_mcp_document({"mcpServers": {"x": {"url": "https://example.com/sse"}}})
self.assertEqual(items[0]["source_ref"], "mcp-remote")
def test_roundtrip_mcp_remote(self) -> None:
row = {
"server_id": "web",
"entry_command": "npx",
"entry_args": ["-y", "mcp-remote", "https://example.com/sse", "--header", "Authorization: Bearer ${K}"],
"env_schema": {"K": {"default": "", "type": "string"}},
}
cursor = registry_row_to_cursor_server(row)
self.assertEqual(cursor.get("url"), "https://example.com/sse")
self.assertEqual((cursor.get("headers") or {}).get("Authorization"), "Bearer ${K}")
export = build_cursor_mcp_export([row])
self.assertIn("web", export["mcpServers"])
def test_config_payload_preserves_mcp_remote(self) -> None:
existing = {
"server_id": "web",
"source_type": "npm",
"source_ref": "mcp-remote",
"entry_command": "npx",
"entry_args": ["-y", "mcp-remote", "https://old.example/sse"],
"env_schema": {},
"enabled": True,
"timeout_s": 30.0,
"required_permissions": [],
"risk_level": "high",
"version": "",
}
fields = config_payload_to_upsert_fields(
{"server_id": "web", "url": "https://new.example/sse", "headers": {"X": "1"}},
existing=existing,
)
self.assertEqual(fields["source_ref"], "mcp-remote")
self.assertIn("https://new.example/sse", fields["entry_args"])
if __name__ == "__main__":
unittest.main()

View file

@ -1,42 +0,0 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from runtime.tools.mcp.market import infer_install_template, search_mcp_market, trending_mcp_market
class McpMarketTests(unittest.TestCase):
@patch("runtime.tools.mcp.market.search_github_repos")
@patch("runtime.tools.mcp.market.search_npm_packages")
@patch("runtime.tools.mcp.market.search_pypi_packages")
def test_aggregate_results(self, pypi_mock, npm_mock, gh_mock) -> None:
gh_mock.return_value = [{"source_type": "github", "name": "a"}]
npm_mock.return_value = [{"source_type": "npm", "name": "b"}]
pypi_mock.return_value = [{"source_type": "pypi", "name": "c"}]
rows = search_mcp_market("demo", per_source_limit=3)
self.assertEqual(len(rows), 3)
self.assertEqual(rows[0]["source_type"], "github")
self.assertEqual(rows[1]["source_type"], "npm")
self.assertEqual(rows[2]["source_type"], "pypi")
def test_infer_install_template(self) -> None:
t1 = infer_install_template("npm", "@acme/mcp-demo")
self.assertEqual(t1["entry_command"], "npx")
self.assertTrue(isinstance(t1["entry_args"], list))
t2 = infer_install_template("pypi", "mcp-demo")
self.assertEqual(t2["entry_command"], "python")
self.assertEqual(t2["entry_args"][:1], ["-m"])
@patch("runtime.tools.mcp.market.search_mcp_market")
def test_trending_cache(self, search_mock) -> None:
search_mock.return_value = [{"source_type": "github", "name": "a", "stars": 9}]
rows = trending_mcp_market(force_refresh=True, per_source_limit=2)
self.assertEqual(len(rows), 1)
rows2 = trending_mcp_market(force_refresh=False, per_source_limit=2)
self.assertEqual(len(rows2), 1)
if __name__ == "__main__":
unittest.main()

View file

@ -16,7 +16,7 @@ from runtime.tools.tool_validation import format_invalid_arguments_error, valida
class McpTimeoutAndCacheTests(unittest.TestCase):
def test_exec_managed_ne_timeout_override(self) -> None:
self.assertEqual(mcp_timeout_for_tool("execManagedNe", 30.0), 320.0)
self.assertEqual(mcp_timeout_for_tool("execManagedNe", 30.0), 620.0)
self.assertEqual(mcp_timeout_for_tool("ping", 30.0), 30.0)
self.assertGreaterEqual(mcp_timeout_for_tool("sqlQueryUme", 30.0), 90.0)
@ -48,7 +48,7 @@ class McpTimeoutAndCacheTests(unittest.TestCase):
],
)
specs = {s.name: s for s in materialize_mcp_tools(store)}
self.assertEqual(specs["mcp__netx__execManagedNe"].timeout_s, 320.0)
self.assertEqual(specs["mcp__netx__execManagedNe"].timeout_s, 620.0)
self.assertEqual(specs["mcp__netx__listCliTargets"].timeout_s, 30.0)
def test_list_cli_targets_ttl_cache(self) -> None:

View file

@ -24,6 +24,8 @@ def test_ops_short_intent_suppresses_inventory_cli_tools() -> None:
assert is_ops_short_intent_suppressed_tool("mcp__netx__listCliTargets", intent="fiber_cut")
assert is_ops_short_intent_suppressed_tool("mcp__netx__execManagedNe", intent="offline")
assert is_ops_short_intent_suppressed_tool("run_command", intent="excel_export")
assert is_ops_short_intent_suppressed_tool("write_xlsx", intent="fiber_cut")
assert is_ops_short_intent_suppressed_tool("write_xlsx", intent="excel_export")
assert not is_ops_short_intent_suppressed_tool("ume_alarm_xlsx_report", intent="fiber_cut")
assert not is_ops_short_intent_suppressed_tool("mcp__netx__queryUmeAlarmsRaw", intent="alarm_tally")
assert not is_ops_short_intent_suppressed_tool("mcp__netx__execManagedNe", intent="continue")
@ -50,7 +52,7 @@ def test_filter_tool_specs_for_ops_short_intent_keeps_report_path() -> None:
names = {t.name for t in kept}
assert "ume_alarm_xlsx_report" in names
assert "mcp__netx__aggregateUmeAlarms" in names
assert "write_xlsx" in names
assert "write_xlsx" not in names
assert "mcp__netx__listCliTargets" not in names
assert "mcp__netx__execManagedNe" not in names
assert "run_command" not in names

View file

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
"""Tests for scheduled job failure classification + summary."""
from __future__ import annotations
from pathlib import Path
from runtime.scheduler.failure_class import classify_scheduled_job_error, enrich_scheduled_job_run_dict
from svc.persistence.sqlite_store import SqliteStore
def test_classify_scheduled_job_error_classes() -> None:
assert classify_scheduled_job_error("overlapping_run", status="skipped") == "overlap"
assert classify_scheduled_job_error("read_timeout after 60s", status="failed") == "timeout"
assert classify_scheduled_job_error("WhatsApp Connection Closed", status="failed") == "delivery"
assert classify_scheduled_job_error("insufficient_scope:sql:query", status="failed") == "auth"
assert classify_scheduled_job_error("Unregistered tool: mcp__netx__x", status="failed") == "mcp"
assert classify_scheduled_job_error("stale_running_cleared", status="failed") == "stale"
assert classify_scheduled_job_error("boom", status="failed") == "runtime"
assert classify_scheduled_job_error("", status="success") == ""
def test_enrich_run_dict_adds_failure_class() -> None:
out = enrich_scheduled_job_run_dict({"status": "failed", "error": "tool timeout"})
assert out["failure_class"] == "timeout"
def test_scheduled_job_failure_summary(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "jobs.sqlite"))
# Ensure tenant user exists if required by store create paths.
job = store.scheduled_job_create(
tenant_id="default",
name="fail-board",
description="",
schedule_kind="interval",
schedule_expr="3600",
timezone_name="UTC",
prompt_text="ping",
specialist="ops",
lang="en",
created_by_user_id="u1",
source="test",
)
run_ok = store.scheduled_job_run_create(
job_id=job.id, tenant_id="default", scheduled_at="2026-08-01T00:00:00+00:00", status="queued"
)
store.scheduled_job_run_update(
run_id=run_ok.id,
tenant_id="default",
patch={"status": "success", "finished_at": "2026-08-01T00:01:00+00:00", "error": ""},
)
run_fail = store.scheduled_job_run_create(
job_id=job.id, tenant_id="default", scheduled_at="2026-08-01T01:00:00+00:00", status="queued"
)
store.scheduled_job_run_update(
run_id=run_fail.id,
tenant_id="default",
patch={
"status": "failed",
"finished_at": "2026-08-01T01:01:00+00:00",
"error": "read_timeout after 90s",
},
)
store.scheduled_job_mark_run(
job_id=job.id, tenant_id="default", last_run_status="failed", pause_after=False
)
summary = store.scheduled_job_failure_summary(tenant_id="default", recent_limit=50)
assert summary["recent_failed"] >= 1
assert summary["recent_success"] >= 1
assert "timeout" in (summary.get("recent_fail_classes") or {})
assert any(x.get("id") == job.id for x in summary.get("jobs_last_failed") or [])
d = store.scheduled_job_run_to_dict(store.scheduled_job_run_get(run_id=run_fail.id, tenant_id="default"))
assert d.get("failure_class") == "timeout"