mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 23:24:22 +08:00
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:
parent
c6a98e2526
commit
8bdaaaacbf
20 changed files with 928 additions and 88 deletions
|
|
@ -3,7 +3,9 @@
|
|||
Policies (per managed NE ``exec_policy``):
|
||||
|
||||
- ``readonly`` (default): network CLI only — show/display/ping/traceroute.
|
||||
- ``linux_shell``: single-line shell; no network prefix/pipe rules; no write-deny list.
|
||||
- ``linux_shell``: open shell/script on capable NEs (Linux, MikroTik RouterOS) —
|
||||
pipes/&&/;/quotes/heredoc / RouterOS multiline scripts allowed;
|
||||
no network prefix/pipe rules; no write-deny list.
|
||||
- ``unrestricted``: same as linux_shell (lab open); kept distinct for audit/UI.
|
||||
"""
|
||||
|
||||
|
|
@ -20,6 +22,11 @@ EXEC_POLICIES = frozenset(
|
|||
{EXEC_POLICY_READONLY, EXEC_POLICY_LINUX_SHELL, EXEC_POLICY_UNRESTRICTED}
|
||||
)
|
||||
|
||||
# readonly: short show/ping lines. linux_shell: scripts / heredoc file writes for agents.
|
||||
_READONLY_MAX_LEN = 500
|
||||
_LINUX_SHELL_MAX_LEN = 65_536
|
||||
_LINUX_SHELL_MAX_LINES = 2_000
|
||||
|
||||
# Block obvious config-change / destructive patterns (case-insensitive).
|
||||
_BLOCKED_RE = re.compile(
|
||||
r"(?i)("
|
||||
|
|
@ -63,6 +70,16 @@ def is_linux_device_type(device_type: str | None) -> bool:
|
|||
return low in ("linux", "linux_ssh", "linux_telnet") or low.startswith("linux_")
|
||||
|
||||
|
||||
def is_mikrotik_device_type(device_type: str | None) -> bool:
|
||||
low = str(device_type or "").strip().lower()
|
||||
return low in ("mikrotik_routeros", "mikrotik_switchos") or low.startswith("mikrotik_")
|
||||
|
||||
|
||||
def allows_open_exec_policy(device_type: str | None) -> bool:
|
||||
"""Device types that may use linux_shell / unrestricted (multiline scripts)."""
|
||||
return is_linux_device_type(device_type) or is_mikrotik_device_type(device_type)
|
||||
|
||||
|
||||
def exec_policy_feature_enabled() -> bool:
|
||||
"""Global kill-switch: off → always readonly (UI hidden, API rejects open policies)."""
|
||||
from .config import settings
|
||||
|
|
@ -71,11 +88,11 @@ def exec_policy_feature_enabled() -> bool:
|
|||
|
||||
|
||||
def effective_exec_policy(raw: str | None, *, device_type: str | None = None) -> str:
|
||||
"""Policy used at exec time (forces readonly when feature off or non-linux)."""
|
||||
"""Policy used at exec time (forces readonly when feature off or ineligible type)."""
|
||||
if not exec_policy_feature_enabled():
|
||||
return EXEC_POLICY_READONLY
|
||||
pol = normalize_exec_policy(raw)
|
||||
if pol != EXEC_POLICY_READONLY and not is_linux_device_type(device_type):
|
||||
if pol != EXEC_POLICY_READONLY and not allows_open_exec_policy(device_type):
|
||||
return EXEC_POLICY_READONLY
|
||||
return pol
|
||||
|
||||
|
|
@ -85,14 +102,14 @@ def require_exec_policy_writable(
|
|||
*,
|
||||
device_type: str | None = None,
|
||||
) -> str:
|
||||
"""Normalize for create/update; reject open policies when feature off or non-linux."""
|
||||
"""Normalize for create/update; reject open policies when feature off or ineligible type."""
|
||||
pol = normalize_exec_policy(raw)
|
||||
if pol == EXEC_POLICY_READONLY:
|
||||
return pol
|
||||
if not exec_policy_feature_enabled():
|
||||
raise HTTPException(status_code=400, detail="exec_policy_feature_disabled")
|
||||
if not is_linux_device_type(device_type):
|
||||
raise HTTPException(status_code=400, detail="exec_policy_requires_linux_device_type")
|
||||
if not allows_open_exec_policy(device_type):
|
||||
raise HTTPException(status_code=400, detail="exec_policy_requires_shell_device_type")
|
||||
return pol
|
||||
|
||||
|
||||
|
|
@ -109,11 +126,14 @@ def _validate_pipe_segments(cmd: str) -> None:
|
|||
raise HTTPException(status_code=400, detail="command_pipe_not_allowed")
|
||||
|
||||
|
||||
def _validate_single_line(cmd: str) -> None:
|
||||
if any(ch in cmd for ch in ("\n", "\r")):
|
||||
raise HTTPException(status_code=400, detail="command_chars_not_allowed")
|
||||
def _validate_linux_shell_command(cmd: str) -> None:
|
||||
"""Shell policy: allow multiline (heredoc / scripts); still reject exotic separators."""
|
||||
if any(sep in cmd for sep in _FORBIDDEN_LINE_SEPARATORS):
|
||||
raise HTTPException(status_code=400, detail="command_chars_not_allowed")
|
||||
# Count lines after normalizing CRLF; blank trailing newline from strip() is already gone.
|
||||
line_count = cmd.count("\n") + 1
|
||||
if line_count > _LINUX_SHELL_MAX_LINES:
|
||||
raise HTTPException(status_code=400, detail="command_too_many_lines")
|
||||
|
||||
|
||||
def _validate_readonly_command(cmd: str) -> None:
|
||||
|
|
@ -133,12 +153,17 @@ def validate_ne_exec_command(command: str, *, policy: str = EXEC_POLICY_READONLY
|
|||
cmd = str(command or "").strip()
|
||||
if not cmd:
|
||||
raise HTTPException(status_code=400, detail="empty_command")
|
||||
if len(cmd) > 500:
|
||||
raise HTTPException(status_code=400, detail="command_too_long")
|
||||
pol = normalize_exec_policy(policy)
|
||||
max_len = (
|
||||
_LINUX_SHELL_MAX_LEN
|
||||
if pol in (EXEC_POLICY_LINUX_SHELL, EXEC_POLICY_UNRESTRICTED)
|
||||
else _READONLY_MAX_LEN
|
||||
)
|
||||
if len(cmd) > max_len:
|
||||
raise HTTPException(status_code=400, detail="command_too_long")
|
||||
if pol in (EXEC_POLICY_LINUX_SHELL, EXEC_POLICY_UNRESTRICTED):
|
||||
# One command string per slot; shell metacharacters (|;&&`$) allowed.
|
||||
_validate_single_line(cmd)
|
||||
# Shell metacharacters (|;&&`$'"<<) and newlines (heredoc) allowed.
|
||||
_validate_linux_shell_command(cmd)
|
||||
return
|
||||
_validate_readonly_command(cmd)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue