ssh-licco 2.0.2__tar.gz → 2.1.2__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 (57) hide show
  1. {ssh_licco-2.0.2/ssh_licco.egg-info → ssh_licco-2.1.2}/PKG-INFO +21 -4
  2. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/README.md +20 -3
  3. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/pyproject.toml +5 -1
  4. {ssh_licco-2.0.2 → ssh_licco-2.1.2/ssh_licco.egg-info}/PKG-INFO +21 -4
  5. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_licco.egg-info/SOURCES.txt +21 -0
  6. ssh_licco-2.1.2/ssh_licco.egg-info/entry_points.txt +2 -0
  7. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/__init__.py +9 -9
  8. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/__main__.py +6 -1
  9. ssh_licco-2.1.2/ssh_mcp/approval.py +220 -0
  10. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/audit_logger.py +23 -47
  11. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/batch_executor.py +43 -60
  12. ssh_licco-2.1.2/ssh_mcp/cli.py +135 -0
  13. ssh_licco-2.1.2/ssh_mcp/clients/additional_clients.py +2 -0
  14. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/clients/factory.py +11 -6
  15. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/clients/interface.py +23 -16
  16. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/clients/paramiko_client.py +105 -130
  17. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/config_manager.py +8 -8
  18. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/connection_config.py +30 -23
  19. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/connection_pool.py +19 -29
  20. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/exceptions.py +20 -8
  21. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/executor.py +24 -21
  22. ssh_licco-2.1.2/ssh_mcp/handlers/__init__.py +38 -0
  23. ssh_licco-2.1.2/ssh_mcp/handlers/approval.py +131 -0
  24. ssh_licco-2.1.2/ssh_mcp/handlers/connect.py +252 -0
  25. ssh_licco-2.1.2/ssh_mcp/handlers/context.py +21 -0
  26. ssh_licco-2.1.2/ssh_mcp/handlers/docker.py +94 -0
  27. ssh_licco-2.1.2/ssh_mcp/handlers/execute.py +582 -0
  28. ssh_licco-2.1.2/ssh_mcp/handlers/file_transfer.py +152 -0
  29. ssh_licco-2.1.2/ssh_mcp/handlers/host.py +92 -0
  30. ssh_licco-2.1.2/ssh_mcp/handlers/key.py +41 -0
  31. ssh_licco-2.1.2/ssh_mcp/handlers/process.py +218 -0
  32. ssh_licco-2.1.2/ssh_mcp/handlers/schemas.py +556 -0
  33. ssh_licco-2.1.2/ssh_mcp/handlers/session.py +151 -0
  34. ssh_licco-2.1.2/ssh_mcp/handlers/utils.py +515 -0
  35. ssh_licco-2.1.2/ssh_mcp/key_manager.py +180 -0
  36. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/logging_config.py +25 -11
  37. ssh_licco-2.1.2/ssh_mcp/runtime_guard.py +178 -0
  38. ssh_licco-2.1.2/ssh_mcp/secret_provider.py +226 -0
  39. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/security.py +431 -220
  40. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/server.py +25 -27
  41. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/service.py +25 -45
  42. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/session_manager.py +184 -90
  43. ssh_licco-2.1.2/ssh_mcp/tunnel.py +115 -0
  44. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/watchdog.py +14 -11
  45. ssh_licco-2.1.2/tests/test_config_manager.py +205 -0
  46. ssh_licco-2.1.2/tests/test_server.py +599 -0
  47. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/tests/test_session_manager.py +5 -0
  48. ssh_licco-2.0.2/ssh_mcp/clients/additional_clients.py +0 -8
  49. ssh_licco-2.0.2/ssh_mcp/key_manager.py +0 -131
  50. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/LICENSE +0 -0
  51. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/setup.cfg +0 -0
  52. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_licco.egg-info/dependency_links.txt +0 -0
  53. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_licco.egg-info/requires.txt +0 -0
  54. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_licco.egg-info/top_level.txt +0 -0
  55. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/ssh_mcp/clients/__init__.py +0 -0
  56. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/tests/test_key_manager.py +0 -0
  57. {ssh_licco-2.0.2 → ssh_licco-2.1.2}/tests/test_security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-licco
3
- Version: 2.0.2
3
+ Version: 2.1.2
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
@@ -217,15 +217,29 @@ export SSH_EXTRA_ALLOWED_COMMANDS="git,pip,npm"
217
217
 
218
218
  - **[MCP_CONFIG_GUIDE.md](MCP_CONFIG_GUIDE.md)** - 完整配置指南,包含 5 种使用场景示例
219
219
  - **[SECURITY_CONFIG_GUIDE.md](SECURITY_CONFIG_GUIDE.md)** - 安全配置详解
220
+ - **[docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)** - 生产加固四项方案(v2.1.0 新增)
221
+
222
+ ### 🏛️ 生产加固四项(v2.1.0 新增)
223
+
224
+ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
225
+
226
+ | 加固点 | 开关 | 说明 |
227
+ |--------|------|------|
228
+ | 1. 运行账号最小权限 | `SSH_RUNTIME_GUARD=true` | 专用普通账号运行,拒绝 root/sudo 启动 |
229
+ | 2. 密钥不落地磁盘 | `SSH_SECRET_PROVIDER_ENABLED=true` | 私钥从 KMS 临时拉取到内存,用完清零,不落盘 |
230
+ | 3. 双层命令拦截 | `SSH_REMOTE_GUARD=true` + 远端 ForceCommand | 跳板机侧禁元字符 + 远端二次白名单校验 |
231
+ | 4. 高危操作审批 | `SSH_APPROVAL_GATE=true` | rm/reboot/iptables 等高危命令需人工审批,AI 不能直接下发 |
232
+
233
+ 完整方案见 [docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)。
220
234
 
221
235
  ---
222
236
 
223
- ## 🛠️ 可用工具(v2.0.2 扩充至 9 个)
237
+ ## 🛠️ 可用工具(v2.1.0 扩充至 12 个)
224
238
 
225
239
  | 工具 | 描述 | 核心能力 |
226
240
  |------|------|---------|
227
241
  | `ssh_connect` | 连接管理 | 自动读取环境变量/配置,支持密码+密钥认证,可保存配置,登录后自动执行命令 |
228
- | `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式 |
242
+ | `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式;v2.1.0 新增 `approval_id`(高危审批)与 `remote_guard`(双层拦截)参数 |
229
243
  | `ssh_disconnect` | 会话管理 | 断开指定会话 OR 列出所有活跃会话 |
230
244
  | `ssh_file_transfer` | 文件传输 | 上传/下载/列表/写入/追加/删除/创建目录/查看元信息(8 种操作) |
231
245
  | `ssh_host` | 主机管理 | `action=list/add/remove` 增删查主机配置 |
@@ -233,6 +247,9 @@ export SSH_EXTRA_ALLOWED_COMMANDS="git,pip,npm"
233
247
  | `ssh_generate_key` | 密钥生成 | RSA / Ed25519 密钥对 |
234
248
  | `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
235
249
  | `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
250
+ | `ssh_request_approval` | 高危审批申请(v2.1.0 新增) | AI 提交高危命令审批申请,返回 approval_id 等待人工审批 |
251
+ | `ssh_approve_command` | 高危审批决定(v2.1.0 新增) | 运维人员人工审批 AI 申请,approved/rejected |
252
+ | `ssh_list_approvals` | 审批记录查询(v2.1.0 新增) | 列出待审批队列或全部历史记录 |
236
253
 
237
254
  ### 📖 详细文档
238
255
 
@@ -558,7 +575,7 @@ ssh-licco 的核心依赖是:
558
575
 
559
576
  | 指标 | 状态 |
560
577
  |------|------|
561
- | **测试用例** | 344 passed, 0 skipped |
578
+ | **测试用例** | 348 passed, 0 skipped |
562
579
  | **覆盖率** | 16 个源模块全覆盖 |
563
580
  | **测试框架** | pytest + pytest-asyncio |
564
581
 
@@ -184,15 +184,29 @@ export SSH_EXTRA_ALLOWED_COMMANDS="git,pip,npm"
184
184
 
185
185
  - **[MCP_CONFIG_GUIDE.md](MCP_CONFIG_GUIDE.md)** - 完整配置指南,包含 5 种使用场景示例
186
186
  - **[SECURITY_CONFIG_GUIDE.md](SECURITY_CONFIG_GUIDE.md)** - 安全配置详解
187
+ - **[docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)** - 生产加固四项方案(v2.1.0 新增)
188
+
189
+ ### 🏛️ 生产加固四项(v2.1.0 新增)
190
+
191
+ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
192
+
193
+ | 加固点 | 开关 | 说明 |
194
+ |--------|------|------|
195
+ | 1. 运行账号最小权限 | `SSH_RUNTIME_GUARD=true` | 专用普通账号运行,拒绝 root/sudo 启动 |
196
+ | 2. 密钥不落地磁盘 | `SSH_SECRET_PROVIDER_ENABLED=true` | 私钥从 KMS 临时拉取到内存,用完清零,不落盘 |
197
+ | 3. 双层命令拦截 | `SSH_REMOTE_GUARD=true` + 远端 ForceCommand | 跳板机侧禁元字符 + 远端二次白名单校验 |
198
+ | 4. 高危操作审批 | `SSH_APPROVAL_GATE=true` | rm/reboot/iptables 等高危命令需人工审批,AI 不能直接下发 |
199
+
200
+ 完整方案见 [docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)。
187
201
 
188
202
  ---
189
203
 
190
- ## 🛠️ 可用工具(v2.0.2 扩充至 9 个)
204
+ ## 🛠️ 可用工具(v2.1.0 扩充至 12 个)
191
205
 
192
206
  | 工具 | 描述 | 核心能力 |
193
207
  |------|------|---------|
194
208
  | `ssh_connect` | 连接管理 | 自动读取环境变量/配置,支持密码+密钥认证,可保存配置,登录后自动执行命令 |
195
- | `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式 |
209
+ | `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式;v2.1.0 新增 `approval_id`(高危审批)与 `remote_guard`(双层拦截)参数 |
196
210
  | `ssh_disconnect` | 会话管理 | 断开指定会话 OR 列出所有活跃会话 |
197
211
  | `ssh_file_transfer` | 文件传输 | 上传/下载/列表/写入/追加/删除/创建目录/查看元信息(8 种操作) |
198
212
  | `ssh_host` | 主机管理 | `action=list/add/remove` 增删查主机配置 |
@@ -200,6 +214,9 @@ export SSH_EXTRA_ALLOWED_COMMANDS="git,pip,npm"
200
214
  | `ssh_generate_key` | 密钥生成 | RSA / Ed25519 密钥对 |
201
215
  | `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
202
216
  | `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
217
+ | `ssh_request_approval` | 高危审批申请(v2.1.0 新增) | AI 提交高危命令审批申请,返回 approval_id 等待人工审批 |
218
+ | `ssh_approve_command` | 高危审批决定(v2.1.0 新增) | 运维人员人工审批 AI 申请,approved/rejected |
219
+ | `ssh_list_approvals` | 审批记录查询(v2.1.0 新增) | 列出待审批队列或全部历史记录 |
203
220
 
204
221
  ### 📖 详细文档
205
222
 
@@ -525,7 +542,7 @@ ssh-licco 的核心依赖是:
525
542
 
526
543
  | 指标 | 状态 |
527
544
  |------|------|
528
- | **测试用例** | 344 passed, 0 skipped |
545
+ | **测试用例** | 348 passed, 0 skipped |
529
546
  | **覆盖率** | 16 个源模块全覆盖 |
530
547
  | **测试框架** | pytest + pytest-asyncio |
531
548
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssh-licco"
3
- version = "2.0.2"
3
+ version = "2.1.2"
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"
@@ -26,6 +26,8 @@ dependencies = [
26
26
  "pydantic>=2.0.0",
27
27
  "pydantic-settings>=2.0.0",
28
28
  ]
29
+ [project.scripts]
30
+ ssh-licco = "ssh_mcp.server:run_server"
29
31
  [project.urls]
30
32
  Homepage = "https://github.com/Echoqili/ssh-licco"
31
33
  Repository = "https://github.com/Echoqili/ssh-licco"
@@ -48,6 +50,8 @@ target-version = "py310"
48
50
  [tool.ruff.lint]
49
51
  select = ["E", "F", "W", "I", "N", "UP", "B", "C4"]
50
52
  ignore = []
53
+ [tool.ruff.lint.per-file-ignores]
54
+ "ssh_mcp/handlers/schemas.py" = ["E501"]
51
55
  [tool.mypy]
52
56
  python_version = "3.10"
53
57
  warn_return_any = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-licco
3
- Version: 2.0.2
3
+ Version: 2.1.2
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
@@ -217,15 +217,29 @@ export SSH_EXTRA_ALLOWED_COMMANDS="git,pip,npm"
217
217
 
218
218
  - **[MCP_CONFIG_GUIDE.md](MCP_CONFIG_GUIDE.md)** - 完整配置指南,包含 5 种使用场景示例
219
219
  - **[SECURITY_CONFIG_GUIDE.md](SECURITY_CONFIG_GUIDE.md)** - 安全配置详解
220
+ - **[docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)** - 生产加固四项方案(v2.1.0 新增)
221
+
222
+ ### 🏛️ 生产加固四项(v2.1.0 新增)
223
+
224
+ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
225
+
226
+ | 加固点 | 开关 | 说明 |
227
+ |--------|------|------|
228
+ | 1. 运行账号最小权限 | `SSH_RUNTIME_GUARD=true` | 专用普通账号运行,拒绝 root/sudo 启动 |
229
+ | 2. 密钥不落地磁盘 | `SSH_SECRET_PROVIDER_ENABLED=true` | 私钥从 KMS 临时拉取到内存,用完清零,不落盘 |
230
+ | 3. 双层命令拦截 | `SSH_REMOTE_GUARD=true` + 远端 ForceCommand | 跳板机侧禁元字符 + 远端二次白名单校验 |
231
+ | 4. 高危操作审批 | `SSH_APPROVAL_GATE=true` | rm/reboot/iptables 等高危命令需人工审批,AI 不能直接下发 |
232
+
233
+ 完整方案见 [docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)。
220
234
 
221
235
  ---
222
236
 
223
- ## 🛠️ 可用工具(v2.0.2 扩充至 9 个)
237
+ ## 🛠️ 可用工具(v2.1.0 扩充至 12 个)
224
238
 
225
239
  | 工具 | 描述 | 核心能力 |
226
240
  |------|------|---------|
227
241
  | `ssh_connect` | 连接管理 | 自动读取环境变量/配置,支持密码+密钥认证,可保存配置,登录后自动执行命令 |
228
- | `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式 |
242
+ | `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式;v2.1.0 新增 `approval_id`(高危审批)与 `remote_guard`(双层拦截)参数 |
229
243
  | `ssh_disconnect` | 会话管理 | 断开指定会话 OR 列出所有活跃会话 |
230
244
  | `ssh_file_transfer` | 文件传输 | 上传/下载/列表/写入/追加/删除/创建目录/查看元信息(8 种操作) |
231
245
  | `ssh_host` | 主机管理 | `action=list/add/remove` 增删查主机配置 |
@@ -233,6 +247,9 @@ export SSH_EXTRA_ALLOWED_COMMANDS="git,pip,npm"
233
247
  | `ssh_generate_key` | 密钥生成 | RSA / Ed25519 密钥对 |
234
248
  | `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
235
249
  | `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
250
+ | `ssh_request_approval` | 高危审批申请(v2.1.0 新增) | AI 提交高危命令审批申请,返回 approval_id 等待人工审批 |
251
+ | `ssh_approve_command` | 高危审批决定(v2.1.0 新增) | 运维人员人工审批 AI 申请,approved/rejected |
252
+ | `ssh_list_approvals` | 审批记录查询(v2.1.0 新增) | 列出待审批队列或全部历史记录 |
236
253
 
237
254
  ### 📖 详细文档
238
255
 
@@ -558,7 +575,7 @@ ssh-licco 的核心依赖是:
558
575
 
559
576
  | 指标 | 状态 |
560
577
  |------|------|
561
- | **测试用例** | 344 passed, 0 skipped |
578
+ | **测试用例** | 348 passed, 0 skipped |
562
579
  | **覆盖率** | 16 个源模块全覆盖 |
563
580
  | **测试框架** | pytest + pytest-asyncio |
564
581
 
@@ -4,12 +4,15 @@ pyproject.toml
4
4
  ssh_licco.egg-info/PKG-INFO
5
5
  ssh_licco.egg-info/SOURCES.txt
6
6
  ssh_licco.egg-info/dependency_links.txt
7
+ ssh_licco.egg-info/entry_points.txt
7
8
  ssh_licco.egg-info/requires.txt
8
9
  ssh_licco.egg-info/top_level.txt
9
10
  ssh_mcp/__init__.py
10
11
  ssh_mcp/__main__.py
12
+ ssh_mcp/approval.py
11
13
  ssh_mcp/audit_logger.py
12
14
  ssh_mcp/batch_executor.py
15
+ ssh_mcp/cli.py
13
16
  ssh_mcp/config_manager.py
14
17
  ssh_mcp/connection_config.py
15
18
  ssh_mcp/connection_pool.py
@@ -17,16 +20,34 @@ ssh_mcp/exceptions.py
17
20
  ssh_mcp/executor.py
18
21
  ssh_mcp/key_manager.py
19
22
  ssh_mcp/logging_config.py
23
+ ssh_mcp/runtime_guard.py
24
+ ssh_mcp/secret_provider.py
20
25
  ssh_mcp/security.py
21
26
  ssh_mcp/server.py
22
27
  ssh_mcp/service.py
23
28
  ssh_mcp/session_manager.py
29
+ ssh_mcp/tunnel.py
24
30
  ssh_mcp/watchdog.py
25
31
  ssh_mcp/clients/__init__.py
26
32
  ssh_mcp/clients/additional_clients.py
27
33
  ssh_mcp/clients/factory.py
28
34
  ssh_mcp/clients/interface.py
29
35
  ssh_mcp/clients/paramiko_client.py
36
+ ssh_mcp/handlers/__init__.py
37
+ ssh_mcp/handlers/approval.py
38
+ ssh_mcp/handlers/connect.py
39
+ ssh_mcp/handlers/context.py
40
+ ssh_mcp/handlers/docker.py
41
+ ssh_mcp/handlers/execute.py
42
+ ssh_mcp/handlers/file_transfer.py
43
+ ssh_mcp/handlers/host.py
44
+ ssh_mcp/handlers/key.py
45
+ ssh_mcp/handlers/process.py
46
+ ssh_mcp/handlers/schemas.py
47
+ ssh_mcp/handlers/session.py
48
+ ssh_mcp/handlers/utils.py
49
+ tests/test_config_manager.py
30
50
  tests/test_key_manager.py
31
51
  tests/test_security.py
52
+ tests/test_server.py
32
53
  tests/test_session_manager.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ssh-licco = ssh_mcp.server:run_server
@@ -1,8 +1,8 @@
1
1
  # SSH-LICCO 版本信息
2
2
  # 所有代码中的版本信息都从这里读取
3
- # 版本号:2.0.2
3
+ # 版本号:2.1.2
4
4
 
5
- __version__ = "2.0.2"
5
+ __version__ = "2.1.2"
6
6
  __author__ = "Li Qi"
7
7
  __email__ = "1985694657@qq.com"
8
8
  __license__ = "MIT"
@@ -11,17 +11,17 @@ __url__ = "https://github.com/Echoqili/ssh-licco"
11
11
 
12
12
  from ssh_mcp.connection_config import ConnectionConfig
13
13
  from ssh_mcp.exceptions import (
14
- SSHException,
15
- ConnectionException,
16
14
  AuthenticationException,
15
+ ClientNotAvailableException,
17
16
  CommandExecutionException,
18
- FileTransferException,
19
- SessionException,
20
- TimeoutException,
21
17
  ConfigurationException,
22
- ClientNotAvailableException,
18
+ ConnectionException,
19
+ FileTransferException,
23
20
  PoolExhaustedException,
24
21
  RetryExhaustedException,
22
+ SessionException,
23
+ SSHException,
24
+ TimeoutException,
25
25
  )
26
26
  from ssh_mcp.service import get_ssh_service
27
27
 
@@ -45,4 +45,4 @@ __all__ = [
45
45
  "PoolExhaustedException",
46
46
  "RetryExhaustedException",
47
47
  "get_ssh_service",
48
- ]
48
+ ]
@@ -1,4 +1,9 @@
1
1
  """SSH-LICCO MCP Server entry point for `python -m ssh_mcp`."""
2
+
3
+ from ssh_mcp.logging_config import suppress_known_warnings
4
+
5
+ suppress_known_warnings()
6
+
2
7
  from ssh_mcp.server import run_server
3
8
 
4
- run_server()
9
+ run_server()
@@ -0,0 +1,220 @@
1
+ """SSH-LICCO 高危操作审批门禁 — 加固点 4
2
+
3
+ 生产跳板机场景下,AI 不能直接一键下发高危运维命令(rm、重启服务、修改防火墙等)。
4
+ 本模块实现审批工作流:
5
+
6
+ AI 调用 ssh_execute(高危命令)
7
+
8
+ │ 被审批门禁拦截(_check_approval_gate)
9
+
10
+ AI 调用 ssh_request_approval(command, reason)
11
+ │ → 生成 approval_id,写入待审批队列(持久化 JSON)
12
+ │ → 返回 approval_id 与提示「等待人工审批」
13
+
14
+ 运维人员人工审批
15
+ │ 调用 ssh_approve_command(approval_id, decision, reviewer)
16
+ │ → approval 状态变更为 approved / rejected
17
+
18
+ AI 再次调用 ssh_execute(command, approval_id)
19
+ │ → 审批门禁校验 approval_id 有效且 approved、命令匹配
20
+
21
+ 执行命令(一次性,用后即焚)
22
+
23
+ 设计要点:
24
+ - 审批记录持久化到 JSON 文件(SSH_APPROVAL_STORE,默认 ~/.ssh_licco/approvals.json),
25
+ 进程重启后审批状态不丢失。
26
+ - approval_id 是 UUID,不可猜测。
27
+ - 一次性消费:approved 的 approval_id 校验通过后立即标记为 consumed,防止复用。
28
+ - 命令必须严格匹配(shlex 规范化后比对),防止「申请 rm -rf /tmp/a,执行 rm -rf /」。
29
+ - 审批有 TTL(默认 1 小时),超时自动失效。
30
+ - 文件读写加锁,支持并发请求。
31
+
32
+ 启用方式:
33
+ SSH_APPROVAL_GATE=true # 开启审批门禁
34
+ SSH_APPROVAL_STORE=/path/to/approvals.json # 审批记录存储路径
35
+ SSH_APPROVAL_TTL=3600 # 审批有效期(秒),默认 3600
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import json
41
+ import os
42
+ import shlex
43
+ import threading
44
+ import time
45
+ import uuid
46
+ from dataclasses import asdict, dataclass
47
+ from pathlib import Path
48
+
49
+
50
+ @dataclass
51
+ class ApprovalRecord:
52
+ approval_id: str
53
+ command: str # 原始命令
54
+ command_norm: str # shlex 规范化后的命令(用于比对)
55
+ reason: str # 申请理由
56
+ status: str # pending | approved | rejected | consumed | expired
57
+ requested_at: float # 申请时间戳
58
+ decided_at: float | None = None # 审批决定时间
59
+ reviewer: str | None = None # 审批人
60
+ consumed_at: float | None = None # 消费(执行)时间
61
+ ttl: int = 3600 # 有效期(秒)
62
+
63
+
64
+ class ApprovalGate:
65
+ """审批门禁单例。"""
66
+
67
+ _instance: ApprovalGate | None = None
68
+ _lock = threading.Lock()
69
+
70
+ def __init__(self):
71
+ store_path = os.getenv(
72
+ "SSH_APPROVAL_STORE",
73
+ str(Path.home() / ".ssh_licco" / "approvals.json"),
74
+ )
75
+ self._store_path = Path(store_path)
76
+ self._store_path.parent.mkdir(parents=True, exist_ok=True)
77
+ self._ttl = int(os.getenv("SSH_APPROVAL_TTL", "3600"))
78
+ self._file_lock = threading.Lock()
79
+ self._records: dict[str, ApprovalRecord] = {}
80
+ self._load()
81
+
82
+ @classmethod
83
+ def instance(cls) -> ApprovalGate:
84
+ with cls._lock:
85
+ if cls._instance is None:
86
+ cls._instance = cls()
87
+ return cls._instance
88
+
89
+ # ── 持久化 ──
90
+ def _load(self) -> None:
91
+ if not self._store_path.exists():
92
+ self._records = {}
93
+ return
94
+ try:
95
+ with open(self._store_path, encoding="utf-8") as f:
96
+ data = json.load(f)
97
+ self._records = {k: ApprovalRecord(**v) for k, v in data.items()}
98
+ except Exception:
99
+ self._records = {}
100
+
101
+ def _save(self) -> None:
102
+ data = {k: asdict(v) for k, v in self._records.items()}
103
+ tmp = self._store_path.with_suffix(".tmp")
104
+ with open(tmp, "w", encoding="utf-8") as f:
105
+ json.dump(data, f, ensure_ascii=False, indent=2)
106
+ tmp.replace(self._store_path) # 原子替换
107
+
108
+ @staticmethod
109
+ def _normalize(command: str) -> str:
110
+ """shlex 规范化命令,用于申请与执行时的命令比对。"""
111
+ try:
112
+ parts = shlex.split(command)
113
+ return " ".join(parts)
114
+ except ValueError:
115
+ return command.strip()
116
+
117
+ # ── 公开 API ──
118
+ def request(self, command: str, reason: str = "") -> ApprovalRecord:
119
+ """AI 提交审批申请。返回 ApprovalRecord(status=pending)。"""
120
+ with self._file_lock:
121
+ self._purge_expired_locked()
122
+ rec = ApprovalRecord(
123
+ approval_id=uuid.uuid4().hex,
124
+ command=command,
125
+ command_norm=self._normalize(command),
126
+ reason=reason,
127
+ status="pending",
128
+ requested_at=time.time(),
129
+ ttl=self._ttl,
130
+ )
131
+ self._records[rec.approval_id] = rec
132
+ self._save()
133
+ return rec
134
+
135
+ def approve(
136
+ self, approval_id: str, reviewer: str, decision: str = "approved"
137
+ ) -> ApprovalRecord:
138
+ """运维人员审批。decision = approved | rejected。"""
139
+ with self._file_lock:
140
+ self._purge_expired_locked()
141
+ rec = self._records.get(approval_id)
142
+ if rec is None:
143
+ raise KeyError(f"approval_id 不存在:{approval_id}")
144
+ if rec.status == "expired":
145
+ raise ValueError("审批已过期,请重新申请")
146
+ if rec.status in ("approved", "rejected", "consumed"):
147
+ raise ValueError(f"审批已处理(status={rec.status}),不可重复操作")
148
+ if decision not in ("approved", "rejected"):
149
+ raise ValueError("decision 必须是 approved 或 rejected")
150
+ rec.status = decision
151
+ rec.decided_at = time.time()
152
+ rec.reviewer = reviewer
153
+ self._save()
154
+ return rec
155
+
156
+ def verify(self, approval_id: str, command: str) -> tuple[bool, str]:
157
+ """执行前校验 approval_id。返回 (是否通过, 原因)。"""
158
+ with self._file_lock:
159
+ self._purge_expired_locked()
160
+ rec = self._records.get(approval_id)
161
+ if rec is None:
162
+ return False, f"approval_id 不存在:{approval_id}"
163
+ if rec.status == "expired":
164
+ return False, "审批已过期,请重新申请"
165
+ if rec.status == "pending":
166
+ return False, "审批仍在 pending,尚未被人工审批"
167
+ if rec.status == "rejected":
168
+ return False, f"审批已被拒绝(reviewer={rec.reviewer})"
169
+ if rec.status == "consumed":
170
+ return False, "此 approval_id 已被使用过(一次性消费),请重新申请"
171
+ if rec.status != "approved":
172
+ return False, f"审批状态异常:{rec.status}"
173
+
174
+ # 命令必须严格匹配(规范化后比对)
175
+ cmd_norm = self._normalize(command)
176
+ if cmd_norm != rec.command_norm:
177
+ return False, (
178
+ f"命令与审批申请不匹配。\n"
179
+ f" 申请命令:{rec.command}\n"
180
+ f" 执行命令:{command}\n"
181
+ f"拒绝执行,防止「申请 A 命令、执行 B 命令」绕过审批。"
182
+ )
183
+
184
+ # 校验通过,标记为已消费(一次性)
185
+ rec.status = "consumed"
186
+ rec.consumed_at = time.time()
187
+ self._save()
188
+ return True, f"审批校验通过(reviewer={rec.reviewer}),已消费。"
189
+
190
+ def list_pending(self) -> list[ApprovalRecord]:
191
+ """列出所有 pending 审批(供运维人员查看待处理队列)。"""
192
+ with self._file_lock:
193
+ self._purge_expired_locked()
194
+ return [r for r in self._records.values() if r.status == "pending"]
195
+
196
+ def list_all(self, limit: int = 100) -> list[ApprovalRecord]:
197
+ """列出所有审批记录(最近的在前)。"""
198
+ with self._file_lock:
199
+ self._purge_expired_locked()
200
+ recs = sorted(
201
+ self._records.values(),
202
+ key=lambda r: r.requested_at,
203
+ reverse=True,
204
+ )
205
+ return recs[:limit]
206
+
207
+ def _purge_expired_locked(self) -> None:
208
+ """清理过期审批(pending 超过 TTL 标记为 expired)。"""
209
+ now = time.time()
210
+ changed = False
211
+ for rec in self._records.values():
212
+ if rec.status == "pending" and (now - rec.requested_at) > rec.ttl:
213
+ rec.status = "expired"
214
+ changed = True
215
+ # approved 但未消费的也按 TTL 失效(从 decided_at 起算)
216
+ if rec.status == "approved" and rec.decided_at and (now - rec.decided_at) > rec.ttl:
217
+ rec.status = "expired"
218
+ changed = True
219
+ if changed:
220
+ self._save()