Add NE capability descriptors and native async exec jobs for agents.

Expose recommended_mode/hints on getManagedNe, support background exec-jobs with getNeExecJob in MCP, and allow linux_shell on MikroTik as well as Linux.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-06 18:32:14 +08:00
parent c6a98e2526
commit 8bdaaaacbf
20 changed files with 928 additions and 88 deletions

110
netx_api/ne_capability.py Normal file
View file

@ -0,0 +1,110 @@
"""Agent-facing capability descriptor for managed NE exec.
Derived from device_type + exec_policy + runtime settings — not stored in DB.
Callers (MCP / UI) should read this before choosing short CLI vs on-device script vs async job.
"""
from __future__ import annotations
from typing import Any
from .ne_exec import _EXEC_BATCH_MAX_TARGETS, _EXEC_READ_TIMEOUT_DEFAULT, _EXEC_READ_TIMEOUT_MAX
from .ne_exec_guard import (
EXEC_POLICY_LINUX_SHELL,
EXEC_POLICY_READONLY,
EXEC_POLICY_UNRESTRICTED,
_LINUX_SHELL_MAX_LEN,
_LINUX_SHELL_MAX_LINES,
_READONLY_MAX_LEN,
allows_open_exec_policy,
effective_exec_policy,
is_linux_device_type,
is_mikrotik_device_type,
normalize_exec_policy,
)
def _device_family(device_type: str | None) -> str:
if is_linux_device_type(device_type):
return "linux"
if is_mikrotik_device_type(device_type):
return "mikrotik"
return "network_cli"
def _max_commands() -> int:
from .config import settings
from .ne_exec import _EXEC_MAX_COMMANDS_CAP
raw = int(getattr(settings, "ne_exec_max_commands", 5) or 5)
return max(1, min(_EXEC_MAX_COMMANDS_CAP, raw))
def build_ne_capability(
*,
device_type: str | None,
exec_policy: str | None,
vendor: str | None = None,
hop_enabled: bool = False,
) -> dict[str, Any]:
"""Return a stable capability object for agents and ops UIs."""
stored = normalize_exec_policy(exec_policy)
effective = effective_exec_policy(exec_policy, device_type=device_type)
family = _device_family(device_type)
shell_ok = effective in (EXEC_POLICY_LINUX_SHELL, EXEC_POLICY_UNRESTRICTED)
open_eligible = allows_open_exec_policy(device_type)
max_cmds = _max_commands()
if effective == EXEC_POLICY_READONLY:
recommended = "show_only"
hints = [
"readonly: only show/display/ping/traceroute (optional pipe filters).",
"For shell/scripts set exec_policy=linux_shell on a Linux or MikroTik NE "
"(requires NETX_NE_EXEC_POLICY_ENABLED).",
]
elif family == "linux":
recommended = "script_on_device"
hints = [
"Prefer short on-device scripts for multi-step work; avoid huge inline heredocs in MCP args.",
"Long / multi-NE work: execManagedNe async=true then poll getNeExecJob.",
f"Max {max_cmds} commands per call; combine steps in one script when needed.",
]
elif family == "mikrotik":
recommended = "short_cli"
hints = [
"Prefer short RouterOS lines (comment=/find=); avoid one ultra-long line that may truncate.",
"Multiline scripts allowed under linux_shell; keep each logical change small and idempotent.",
"Long / multi-NE work: execManagedNe async=true then poll getNeExecJob.",
]
else:
recommended = "show_only"
hints = ["network_cli: use vendor show/display; write changes are blocked under readonly."]
if hop_enabled:
hints.append("hop_enabled: expect higher latency; raise read_timeout_sec toward 90–120 if needed.")
return {
"vendor": str(vendor or "").strip() or None,
"device_type": str(device_type or "").strip() or None,
"device_family": family,
"exec_policy_stored": stored,
"exec_policy_effective": effective,
"allows_shell_scripts": bool(shell_ok),
"allows_multiline": bool(shell_ok),
"open_policy_eligible": bool(open_eligible),
"max_commands_per_call": max_cmds,
"max_command_len": _LINUX_SHELL_MAX_LEN if shell_ok else _READONLY_MAX_LEN,
"max_command_lines": _LINUX_SHELL_MAX_LINES if shell_ok else 1,
"read_timeout_sec": {
"default": _EXEC_READ_TIMEOUT_DEFAULT,
"max": _EXEC_READ_TIMEOUT_MAX,
},
"supports_batch_exec": True,
"batch_max_targets": _EXEC_BATCH_MAX_TARGETS,
"supports_async_job": True,
"recommended_mode": recommended,
"hints": hints,
}
__all__ = ["build_ne_capability"]