ssh-licco 2.5.1__tar.gz → 2.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {ssh_licco-2.5.1/ssh_licco.egg-info → ssh_licco-2.5.3}/PKG-INFO +2 -2
  2. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/README.md +1 -1
  3. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/pyproject.toml +2 -1
  4. {ssh_licco-2.5.1 → ssh_licco-2.5.3/ssh_licco.egg-info}/PKG-INFO +2 -2
  5. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_licco.egg-info/SOURCES.txt +2 -1
  6. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/__init__.py +2 -2
  7. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/security.py +3 -1
  8. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/tests/test_security.py +9 -0
  9. ssh_licco-2.5.3/tests/test_watchdog.py +181 -0
  10. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/LICENSE +0 -0
  11. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/setup.cfg +0 -0
  12. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_licco.egg-info/dependency_links.txt +0 -0
  13. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_licco.egg-info/entry_points.txt +0 -0
  14. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_licco.egg-info/requires.txt +0 -0
  15. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_licco.egg-info/top_level.txt +0 -0
  16. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/__main__.py +0 -0
  17. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/audit_logger.py +0 -0
  18. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/batch_executor.py +0 -0
  19. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/cli.py +0 -0
  20. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/clients/__init__.py +0 -0
  21. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/clients/additional_clients.py +0 -0
  22. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/clients/factory.py +0 -0
  23. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/clients/interface.py +0 -0
  24. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/clients/paramiko_client.py +0 -0
  25. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/config_manager.py +0 -0
  26. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/connection_config.py +0 -0
  27. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/connection_pool.py +0 -0
  28. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/exceptions.py +0 -0
  29. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/executor.py +0 -0
  30. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/__init__.py +0 -0
  31. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/connect.py +0 -0
  32. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/context.py +0 -0
  33. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/docker.py +0 -0
  34. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/execute.py +0 -0
  35. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/file_transfer.py +0 -0
  36. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/host.py +0 -0
  37. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/key.py +0 -0
  38. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/process.py +0 -0
  39. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/schemas.py +0 -0
  40. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/session.py +0 -0
  41. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/handlers/utils.py +0 -0
  42. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/key_manager.py +0 -0
  43. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/logging_config.py +0 -0
  44. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/server.py +0 -0
  45. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/service.py +0 -0
  46. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/session_manager.py +0 -0
  47. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/tunnel.py +0 -0
  48. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/ssh_mcp/watchdog.py +0 -0
  49. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/tests/test_config_manager.py +0 -0
  50. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/tests/test_key_manager.py +0 -0
  51. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/tests/test_server.py +0 -0
  52. {ssh_licco-2.5.1 → ssh_licco-2.5.3}/tests/test_session_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-licco
3
- Version: 2.5.1
3
+ Version: 2.5.3
4
4
  Summary: SSH Model Context Protocol Server - Enable SSH functionality for AI models
5
5
  Author-email: SSH LICCO Team <contact@ssh-licco.dev>
6
6
  License: MIT
@@ -622,7 +622,7 @@ ssh-licco 的核心依赖是:
622
622
 
623
623
  | 指标 | 状态 |
624
624
  |------|------|
625
- | **测试用例** | 423 passed, 0 skipped |
625
+ | **测试用例** | 424 passed, 0 skipped |
626
626
  | **覆盖率** | 16 个源模块全覆盖 |
627
627
  | **测试框架** | pytest + pytest-asyncio |
628
628
 
@@ -589,7 +589,7 @@ ssh-licco 的核心依赖是:
589
589
 
590
590
  | 指标 | 状态 |
591
591
  |------|------|
592
- | **测试用例** | 423 passed, 0 skipped |
592
+ | **测试用例** | 424 passed, 0 skipped |
593
593
  | **覆盖率** | 16 个源模块全覆盖 |
594
594
  | **测试框架** | pytest + pytest-asyncio |
595
595
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssh-licco"
3
- version = "2.5.1"
3
+ version = "2.5.3"
4
4
  description = "SSH Model Context Protocol Server - Enable SSH functionality for AI models"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -59,4 +59,5 @@ warn_unused_configs = true
59
59
  disallow_untyped_defs = false
60
60
  [tool.pytest.ini_options]
61
61
  asyncio_mode = "auto"
62
+ asyncio_default_fixture_loop_scope = "function"
62
63
  testpaths = ["tests"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-licco
3
- Version: 2.5.1
3
+ Version: 2.5.3
4
4
  Summary: SSH Model Context Protocol Server - Enable SSH functionality for AI models
5
5
  Author-email: SSH LICCO Team <contact@ssh-licco.dev>
6
6
  License: MIT
@@ -622,7 +622,7 @@ ssh-licco 的核心依赖是:
622
622
 
623
623
  | 指标 | 状态 |
624
624
  |------|------|
625
- | **测试用例** | 423 passed, 0 skipped |
625
+ | **测试用例** | 424 passed, 0 skipped |
626
626
  | **覆盖率** | 16 个源模块全覆盖 |
627
627
  | **测试框架** | pytest + pytest-asyncio |
628
628
 
@@ -46,4 +46,5 @@ tests/test_config_manager.py
46
46
  tests/test_key_manager.py
47
47
  tests/test_security.py
48
48
  tests/test_server.py
49
- tests/test_session_manager.py
49
+ tests/test_session_manager.py
50
+ tests/test_watchdog.py
@@ -1,8 +1,8 @@
1
1
  # SSH-LICCO 版本信息
2
2
  # 所有代码中的版本信息都从这里读取
3
- # 版本号:2.5.1
3
+ # 版本号:2.5.3
4
4
 
5
- __version__ = "2.5.1"
5
+ __version__ = "2.5.3"
6
6
  __author__ = "Li Qi"
7
7
  __email__ = "1985694657@qq.com"
8
8
  __license__ = "MIT"
@@ -949,7 +949,9 @@ def generate_backup_command(targets: list[str]) -> str:
949
949
  def format_backup_prompt(command: str, targets: list[str]) -> str:
950
950
  """生成备份确认提示文案。"""
951
951
  targets_str = ", ".join(targets)
952
- return f"""⚠️ 检测到递归删除操作
952
+ # rmdir 删除的是目录但非递归;rm -r/rf 属于递归删除。
953
+ operation_type = "目录删除操作" if command.lstrip().startswith("rmdir") else "递归删除操作"
954
+ return f"""⚠️ 检测到{operation_type}
953
955
 
954
956
  📋 将要执行的命令:
955
957
  {command}
@@ -271,3 +271,12 @@ def test_format_backup_prompt_mentions_backup_before_delete() -> None:
271
271
  assert "backup_before_delete=true" in prompt
272
272
  assert "backup_before_delete=false" in prompt
273
273
  assert "./data" in prompt
274
+
275
+
276
+ def test_format_backup_prompt_for_rmdir() -> None:
277
+ """rmdir 的提示文案应使用“检测到目录删除操作”。"""
278
+ from ssh_mcp.security import format_backup_prompt
279
+ prompt = format_backup_prompt("rmdir ./empty_dir", ["./empty_dir"])
280
+ assert "检测到目录删除操作" in prompt
281
+ assert "检测到递归删除操作" not in prompt
282
+ assert "backup_before_delete=true" in prompt
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ from datetime import datetime
6
+ from unittest.mock import AsyncMock, MagicMock, patch
7
+
8
+ import pytest
9
+
10
+ from ssh_mcp.watchdog import (
11
+ GlobalExceptionHandler,
12
+ Watchdog,
13
+ WatchdogEvent,
14
+ WatchdogStatus,
15
+ get_watchdog,
16
+ setup_global_exception_handler,
17
+ )
18
+
19
+
20
+ class TestWatchdogStatus:
21
+ def test_values(self):
22
+ assert WatchdogStatus.RUNNING.value == "running"
23
+ assert WatchdogStatus.STOPPED.value == "stopped"
24
+ assert WatchdogStatus.PAUSED.value == "paused"
25
+
26
+
27
+ class TestWatchdogEvent:
28
+ def test_creation(self):
29
+ event = WatchdogEvent(
30
+ timestamp=datetime.now(),
31
+ event_type="TEST",
32
+ message="test event",
33
+ )
34
+ assert event.event_type == "TEST"
35
+ assert event.message == "test event"
36
+ assert event.details == {}
37
+
38
+ def test_with_details(self):
39
+ event = WatchdogEvent(
40
+ timestamp=datetime.now(),
41
+ event_type="TEST",
42
+ message="test",
43
+ details={"key": "value"},
44
+ )
45
+ assert event.details["key"] == "value"
46
+
47
+
48
+ class TestWatchdog:
49
+ def setup_method(self):
50
+ Watchdog._instance = None
51
+
52
+ def _fresh_watchdog(self) -> Watchdog:
53
+ Watchdog._instance = None
54
+ return Watchdog()
55
+
56
+ def test_singleton(self):
57
+ w1 = Watchdog()
58
+ w2 = Watchdog()
59
+ assert w1 is w2
60
+
61
+ def test_initial_status_stopped(self):
62
+ w = self._fresh_watchdog()
63
+ assert w.status == WatchdogStatus.STOPPED
64
+
65
+ def test_register_task(self):
66
+ w = self._fresh_watchdog()
67
+ w.register_task("task-1", "Test Task", timeout=60)
68
+ tasks = w.get_tasks()
69
+ assert len(tasks) == 1
70
+ assert tasks[0].task_id == "task-1"
71
+
72
+ def test_unregister_task(self):
73
+ w = self._fresh_watchdog()
74
+ w.register_task("task-1", "Test Task")
75
+ w.unregister_task("task-1")
76
+ assert len(w.get_tasks()) == 0
77
+
78
+ def test_update_heartbeat(self):
79
+ w = self._fresh_watchdog()
80
+ w.register_task("task-1", "Test Task")
81
+ old_hb = w.get_tasks()[0].last_heartbeat
82
+ w.update_task_heartbeat("task-1")
83
+ new_hb = w.get_tasks()[0].last_heartbeat
84
+ assert new_hb >= old_hb
85
+
86
+ def test_update_heartbeat_nonexistent(self):
87
+ w = self._fresh_watchdog()
88
+ w.update_task_heartbeat("nonexistent")
89
+
90
+ def test_update_progress(self):
91
+ w = self._fresh_watchdog()
92
+ w.register_task("task-1", "Test Task")
93
+ w.update_task_progress("task-1", 50)
94
+ assert w.get_tasks()[0].progress == 50
95
+
96
+ def test_capture_exception(self):
97
+ w = self._fresh_watchdog()
98
+ w.capture_exception(ValueError("test error"), context="test")
99
+ events = w.get_events()
100
+ assert len(events) >= 1
101
+ assert events[0].event_type == "SYSTEM_ERROR"
102
+
103
+ def test_get_events_limit(self):
104
+ w = self._fresh_watchdog()
105
+ for i in range(5):
106
+ w.capture_exception(ValueError(f"error {i}"))
107
+ events = w.get_events(limit=3)
108
+ assert len(events) <= 3
109
+
110
+ def test_add_recovery_callback(self):
111
+ w = self._fresh_watchdog()
112
+ callback = MagicMock()
113
+ w.add_recovery_callback(callback)
114
+ assert callback in w._recovery_callbacks
115
+
116
+ def test_start_sets_running(self):
117
+ w = self._fresh_watchdog()
118
+ with patch.object(w, "_monitor_loop", MagicMock()):
119
+ with patch("asyncio.create_task", return_value=MagicMock()):
120
+ w.start()
121
+ assert w.status == WatchdogStatus.RUNNING
122
+ w.stop()
123
+
124
+ def test_stop_sets_stopped(self):
125
+ w = self._fresh_watchdog()
126
+ with patch.object(w, "_monitor_loop", MagicMock()):
127
+ with patch("asyncio.create_task", return_value=MagicMock()):
128
+ w.start()
129
+ w.stop()
130
+ assert w.status == WatchdogStatus.STOPPED
131
+
132
+ def test_start_already_running(self):
133
+ w = self._fresh_watchdog()
134
+ with patch.object(w, "_monitor_loop", MagicMock()):
135
+ with patch("asyncio.create_task", return_value=MagicMock()):
136
+ w.start()
137
+ with patch.object(w, "_monitor_loop", MagicMock()):
138
+ with patch("asyncio.create_task", return_value=MagicMock()):
139
+ w.start()
140
+ assert w.status == WatchdogStatus.RUNNING
141
+ w.stop()
142
+
143
+
144
+ class TestGlobalExceptionHandler:
145
+ def test_enable(self):
146
+ handler = GlobalExceptionHandler()
147
+ mock_loop = MagicMock()
148
+ mock_loop.set_exception_handler = MagicMock()
149
+ mock_loop.get_exception_handler = MagicMock(return_value=None)
150
+ with patch("asyncio.get_event_loop", return_value=mock_loop):
151
+ handler.enable()
152
+ assert handler._enabled is True
153
+
154
+ def test_disable(self):
155
+ handler = GlobalExceptionHandler()
156
+ mock_loop = MagicMock()
157
+ mock_loop.set_exception_handler = MagicMock()
158
+ mock_loop.get_exception_handler = MagicMock(return_value=None)
159
+ mock_loop.set_exception_handler = MagicMock()
160
+ with patch("asyncio.get_event_loop", return_value=mock_loop):
161
+ handler.enable()
162
+ handler.disable()
163
+ assert handler._enabled is False
164
+
165
+ def test_disable_not_enabled(self):
166
+ handler = GlobalExceptionHandler()
167
+ handler.disable()
168
+ assert handler._enabled is False
169
+
170
+ def test_enable_already_enabled(self):
171
+ handler = GlobalExceptionHandler()
172
+ handler._enabled = True
173
+ handler.enable()
174
+ assert handler._enabled is True
175
+
176
+
177
+ class TestGetWatchdog:
178
+ def test_returns_watchdog(self):
179
+ Watchdog._instance = None
180
+ w = get_watchdog()
181
+ assert isinstance(w, Watchdog)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes