ssh-licco 2.2.1__tar.gz → 2.3.1__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 (55) hide show
  1. {ssh_licco-2.2.1/ssh_licco.egg-info → ssh_licco-2.3.1}/PKG-INFO +41 -3
  2. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/README.md +40 -2
  3. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/pyproject.toml +1 -1
  4. {ssh_licco-2.2.1 → ssh_licco-2.3.1/ssh_licco.egg-info}/PKG-INFO +41 -3
  5. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_licco.egg-info/SOURCES.txt +0 -2
  6. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/__init__.py +2 -2
  7. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/__init__.py +0 -4
  8. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/execute.py +0 -9
  9. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/schemas.py +0 -61
  10. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/utils.py +0 -48
  11. ssh_licco-2.2.1/ssh_mcp/approval.py +0 -220
  12. ssh_licco-2.2.1/ssh_mcp/handlers/approval.py +0 -131
  13. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/LICENSE +0 -0
  14. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/setup.cfg +0 -0
  15. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_licco.egg-info/dependency_links.txt +0 -0
  16. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_licco.egg-info/entry_points.txt +0 -0
  17. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_licco.egg-info/requires.txt +0 -0
  18. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_licco.egg-info/top_level.txt +0 -0
  19. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/__main__.py +0 -0
  20. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/audit_logger.py +0 -0
  21. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/batch_executor.py +0 -0
  22. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/cli.py +0 -0
  23. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/clients/__init__.py +0 -0
  24. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/clients/additional_clients.py +0 -0
  25. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/clients/factory.py +0 -0
  26. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/clients/interface.py +0 -0
  27. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/clients/paramiko_client.py +0 -0
  28. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/config_manager.py +0 -0
  29. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/connection_config.py +0 -0
  30. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/connection_pool.py +0 -0
  31. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/exceptions.py +0 -0
  32. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/executor.py +0 -0
  33. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/connect.py +0 -0
  34. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/context.py +0 -0
  35. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/docker.py +0 -0
  36. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/file_transfer.py +0 -0
  37. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/host.py +0 -0
  38. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/key.py +0 -0
  39. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/process.py +0 -0
  40. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/handlers/session.py +0 -0
  41. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/key_manager.py +0 -0
  42. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/logging_config.py +0 -0
  43. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/runtime_guard.py +0 -0
  44. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/secret_provider.py +0 -0
  45. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/security.py +0 -0
  46. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/server.py +0 -0
  47. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/service.py +0 -0
  48. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/session_manager.py +0 -0
  49. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/tunnel.py +0 -0
  50. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/ssh_mcp/watchdog.py +0 -0
  51. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/tests/test_config_manager.py +0 -0
  52. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/tests/test_key_manager.py +0 -0
  53. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/tests/test_security.py +0 -0
  54. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/tests/test_server.py +0 -0
  55. {ssh_licco-2.2.1 → ssh_licco-2.3.1}/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.2.1
3
+ Version: 2.3.1
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
@@ -267,7 +267,7 @@ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
267
267
  | 3. 双层命令拦截 | `SSH_REMOTE_GUARD=true` + 远端 ForceCommand | 跳板机侧禁元字符 + 远端二次白名单校验 |
268
268
  | 4. 灾难性命令硬拦截(v2.2.0 新增) | 默认开启,不可关闭 | rm -rf 绝对路径、mkfs、raw-disk dd、fork-bomb、chmod -R 777/000 /、`> /dev/sd*` 等在 MCP 网关层被**无条件**拒绝;任何安全级别、`confirm_dangerous=true` 等均无法绕过 |
269
269
 
270
- > **关于 v2.1.0 引入的"高危操作审批"项**:v2.1.0 曾提供 `SSH_APPROVAL_GATE=true` + 3 个审批工具作为第 4 项加固。v2.2.0 起该项被**硬拦截取代**——审批流程依赖 AI 自报命令,存在闭环风险;硬拦截更直接,灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。`SSH_APPROVAL_GATE` 环境变量与 `ssh_mcp/approval.py` 代码仍保留作为参考,但不建议在生产启用。
270
+ > **关于 v2.1.0 引入的"高危操作审批"项**:v2.1.0 曾提供 `SSH_APPROVAL_GATE=true` + 3 个审批工具作为第 4 项加固。v2.2.0 起该项被**硬拦截取代**——审批流程依赖 AI 自报命令,存在闭环风险;硬拦截更直接,灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。`SSH_APPROVAL_GATE` 环境变量与 `ssh_mcp/approval.py` / `ssh_mcp/handlers/approval.py` 代码已在 v2.2.0 删除。
271
271
 
272
272
  完整方案见 [docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)。
273
273
 
@@ -287,7 +287,7 @@ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
287
287
  | `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
288
288
  | `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
289
289
 
290
- > **关于 v2.1.0 引入的 3 个审批工具**(`ssh_request_approval` / `ssh_approve_command` / `ssh_list_approvals`):已从 MCP `list_tools()` 移除。审批流程依赖 AI 自报命令、运维侧背书,存在闭环风险;v2.2.0 的硬拦截更直接——灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。相关代码(`ssh_mcp/approval.py`、`ssh_mcp/handlers/approval.py`)保留作为参考,但不暴露给 MCP
290
+ > **关于 v2.1.0 引入的 3 个审批工具**(`ssh_request_approval` / `ssh_approve_command` / `ssh_list_approvals`):已从 MCP `list_tools()` 移除,**代码已在 v2.2.0 删除**(`ssh_mcp/approval.py`、`ssh_mcp/handlers/approval.py`)。审批流程依赖 AI 自报命令、运维侧背书,存在闭环风险;v2.2.0 的硬拦截更直接——灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。
291
291
 
292
292
  ### 📖 详细文档
293
293
 
@@ -453,6 +453,44 @@ localhost:5432 - accepting connections
453
453
 
454
454
  - **[MCP_CONFIG_GUIDE.md](MCP_CONFIG_GUIDE.md)** - 包含所有配置选项的详细说明
455
455
 
456
+ ### 🌐 完整环境变量速查(v2.3.0)
457
+
458
+ > 下表所有变量均被代码读取。**注意**:
459
+ > - `SSH_RATE_LIMIT` 是 **bool** 总开关(true/false),`SSH_RATE_LIMIT_MAX` 才是次数上限,两者分开配置
460
+ > - 主机密钥检查(strict_host_key_checking)**不通过 env 配置**,请用 `ssh_connect` 工具参数或 `hosts.json`
461
+ > - 详细分组说明见 [docs/SECURITY_HARDENING.md#环境变量速查](docs/SECURITY_HARDENING.md#环境变量速查v230-完整列表)
462
+
463
+ | 分类 | 变量 | 默认 | 说明 |
464
+ |------|------|------|------|
465
+ | **安全** | `SSH_SECURITY_LEVEL` | `balanced` | 安全级别:`strict` / `balanced` / `relaxed` |
466
+ | | `SSH_BASE_DIR` | `/home` | 路径校验基目录 |
467
+ | | `SSH_EXTRA_ALLOWED_COMMANDS` | (空) | 额外允许的命令(逗号分隔) |
468
+ | | `SSH_ALLOWED_COMMANDS_FILE` | (空) | 命令白名单 JSON 文件路径 |
469
+ | | `SSH_AUDIT_LOG_PATH` | (空) | 审计日志文件路径 |
470
+ | **限流** | `SSH_RATE_LIMIT` | `true` | 限流总开关(bool) |
471
+ | | `SSH_RATE_LIMIT_MAX` | `30` | 限流次数上限 |
472
+ | | `SSH_RATE_LIMIT_WINDOW` | `60` | 限流窗口(秒) |
473
+ | **加固点 1** | `SSH_RUNTIME_GUARD` | `false` | runtime guard 总开关 |
474
+ | | `SSH_RUNTIME_ALLOW_ROOT` | `false` | 是否允许 root 启动 |
475
+ | | `SSH_RUNTIME_ALLOWED_USERS` | (空) | 允许启动的账号白名单 |
476
+ | | `SSH_RUNTIME_BLOCK_SUDO` | `true` | 是否拦截 sudo 上下文 |
477
+ | **加固点 2** | `SSH_SECRET_PROVIDER_ENABLED` | `false` | secret provider 总开关 |
478
+ | | `SSH_SECRET_PROVIDER` | `env` | 凭证 provider:`env` / `command` / `http` |
479
+ | | `SSH_SECRET_ENV_KEY_<NAME>` | — | env provider:私钥环境变量 |
480
+ | | `SSH_SECRET_COMMAND_<NAME>` | — | command provider:拉取私钥的命令 |
481
+ | | `SSH_SECRET_HTTP_URL_<NAME>` | — | http provider:拉取私钥的 URL |
482
+ | | `SSH_SECRET_HTTP_TOKEN` | — | http provider:Bearer token |
483
+ | | `SSH_SECRET_HTTP_TIMEOUT` | `15` | http provider:HTTP 超时(秒) |
484
+ | **加固点 3** | `SSH_REMOTE_GUARD` | `false` | 远端元字符拦截总开关 |
485
+ | **加固点 4** | *(无 env)* | — | 灾难性命令硬拦截,零配置零绕过 |
486
+ | **连接默认** | `SSH_HOST` / `SSH_PORT` / `SSH_USER` / `SSH_PASSWORD` | `127.0.0.1` / `22` / `root` / (空) | 单 host 模式默认连接参数 |
487
+ | | `SSH_TIMEOUT` | `60` | 连接超时(秒) |
488
+ | | `SSH_KEEPALIVE_INTERVAL` | `30` | keepalive 间隔(秒) |
489
+ | | `SSH_SESSION_TIMEOUT` | `7200` | 会话超时(秒) |
490
+ | | `SSH_CLIENT_TYPE` | `paramiko` | SSH 客户端实现:`paramiko` / `asyncssh` |
491
+ | | `SSH_FORCE_ENV_CONFIG` | `false` | 强制 env 配置覆盖 hosts.json |
492
+ | | `SSH_SUDO_PASSWORD` | (空) | sudo 密码,配合 `use_sudo=true` 走 `sudo -S` |
493
+
456
494
  ---
457
495
 
458
496
  ## ⚠️ 依赖版本兼容性
@@ -234,7 +234,7 @@ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
234
234
  | 3. 双层命令拦截 | `SSH_REMOTE_GUARD=true` + 远端 ForceCommand | 跳板机侧禁元字符 + 远端二次白名单校验 |
235
235
  | 4. 灾难性命令硬拦截(v2.2.0 新增) | 默认开启,不可关闭 | rm -rf 绝对路径、mkfs、raw-disk dd、fork-bomb、chmod -R 777/000 /、`> /dev/sd*` 等在 MCP 网关层被**无条件**拒绝;任何安全级别、`confirm_dangerous=true` 等均无法绕过 |
236
236
 
237
- > **关于 v2.1.0 引入的"高危操作审批"项**:v2.1.0 曾提供 `SSH_APPROVAL_GATE=true` + 3 个审批工具作为第 4 项加固。v2.2.0 起该项被**硬拦截取代**——审批流程依赖 AI 自报命令,存在闭环风险;硬拦截更直接,灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。`SSH_APPROVAL_GATE` 环境变量与 `ssh_mcp/approval.py` 代码仍保留作为参考,但不建议在生产启用。
237
+ > **关于 v2.1.0 引入的"高危操作审批"项**:v2.1.0 曾提供 `SSH_APPROVAL_GATE=true` + 3 个审批工具作为第 4 项加固。v2.2.0 起该项被**硬拦截取代**——审批流程依赖 AI 自报命令,存在闭环风险;硬拦截更直接,灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。`SSH_APPROVAL_GATE` 环境变量与 `ssh_mcp/approval.py` / `ssh_mcp/handlers/approval.py` 代码已在 v2.2.0 删除。
238
238
 
239
239
  完整方案见 [docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)。
240
240
 
@@ -254,7 +254,7 @@ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
254
254
  | `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
255
255
  | `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
256
256
 
257
- > **关于 v2.1.0 引入的 3 个审批工具**(`ssh_request_approval` / `ssh_approve_command` / `ssh_list_approvals`):已从 MCP `list_tools()` 移除。审批流程依赖 AI 自报命令、运维侧背书,存在闭环风险;v2.2.0 的硬拦截更直接——灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。相关代码(`ssh_mcp/approval.py`、`ssh_mcp/handlers/approval.py`)保留作为参考,但不暴露给 MCP
257
+ > **关于 v2.1.0 引入的 3 个审批工具**(`ssh_request_approval` / `ssh_approve_command` / `ssh_list_approvals`):已从 MCP `list_tools()` 移除,**代码已在 v2.2.0 删除**(`ssh_mcp/approval.py`、`ssh_mcp/handlers/approval.py`)。审批流程依赖 AI 自报命令、运维侧背书,存在闭环风险;v2.2.0 的硬拦截更直接——灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。
258
258
 
259
259
  ### 📖 详细文档
260
260
 
@@ -420,6 +420,44 @@ localhost:5432 - accepting connections
420
420
 
421
421
  - **[MCP_CONFIG_GUIDE.md](MCP_CONFIG_GUIDE.md)** - 包含所有配置选项的详细说明
422
422
 
423
+ ### 🌐 完整环境变量速查(v2.3.0)
424
+
425
+ > 下表所有变量均被代码读取。**注意**:
426
+ > - `SSH_RATE_LIMIT` 是 **bool** 总开关(true/false),`SSH_RATE_LIMIT_MAX` 才是次数上限,两者分开配置
427
+ > - 主机密钥检查(strict_host_key_checking)**不通过 env 配置**,请用 `ssh_connect` 工具参数或 `hosts.json`
428
+ > - 详细分组说明见 [docs/SECURITY_HARDENING.md#环境变量速查](docs/SECURITY_HARDENING.md#环境变量速查v230-完整列表)
429
+
430
+ | 分类 | 变量 | 默认 | 说明 |
431
+ |------|------|------|------|
432
+ | **安全** | `SSH_SECURITY_LEVEL` | `balanced` | 安全级别:`strict` / `balanced` / `relaxed` |
433
+ | | `SSH_BASE_DIR` | `/home` | 路径校验基目录 |
434
+ | | `SSH_EXTRA_ALLOWED_COMMANDS` | (空) | 额外允许的命令(逗号分隔) |
435
+ | | `SSH_ALLOWED_COMMANDS_FILE` | (空) | 命令白名单 JSON 文件路径 |
436
+ | | `SSH_AUDIT_LOG_PATH` | (空) | 审计日志文件路径 |
437
+ | **限流** | `SSH_RATE_LIMIT` | `true` | 限流总开关(bool) |
438
+ | | `SSH_RATE_LIMIT_MAX` | `30` | 限流次数上限 |
439
+ | | `SSH_RATE_LIMIT_WINDOW` | `60` | 限流窗口(秒) |
440
+ | **加固点 1** | `SSH_RUNTIME_GUARD` | `false` | runtime guard 总开关 |
441
+ | | `SSH_RUNTIME_ALLOW_ROOT` | `false` | 是否允许 root 启动 |
442
+ | | `SSH_RUNTIME_ALLOWED_USERS` | (空) | 允许启动的账号白名单 |
443
+ | | `SSH_RUNTIME_BLOCK_SUDO` | `true` | 是否拦截 sudo 上下文 |
444
+ | **加固点 2** | `SSH_SECRET_PROVIDER_ENABLED` | `false` | secret provider 总开关 |
445
+ | | `SSH_SECRET_PROVIDER` | `env` | 凭证 provider:`env` / `command` / `http` |
446
+ | | `SSH_SECRET_ENV_KEY_<NAME>` | — | env provider:私钥环境变量 |
447
+ | | `SSH_SECRET_COMMAND_<NAME>` | — | command provider:拉取私钥的命令 |
448
+ | | `SSH_SECRET_HTTP_URL_<NAME>` | — | http provider:拉取私钥的 URL |
449
+ | | `SSH_SECRET_HTTP_TOKEN` | — | http provider:Bearer token |
450
+ | | `SSH_SECRET_HTTP_TIMEOUT` | `15` | http provider:HTTP 超时(秒) |
451
+ | **加固点 3** | `SSH_REMOTE_GUARD` | `false` | 远端元字符拦截总开关 |
452
+ | **加固点 4** | *(无 env)* | — | 灾难性命令硬拦截,零配置零绕过 |
453
+ | **连接默认** | `SSH_HOST` / `SSH_PORT` / `SSH_USER` / `SSH_PASSWORD` | `127.0.0.1` / `22` / `root` / (空) | 单 host 模式默认连接参数 |
454
+ | | `SSH_TIMEOUT` | `60` | 连接超时(秒) |
455
+ | | `SSH_KEEPALIVE_INTERVAL` | `30` | keepalive 间隔(秒) |
456
+ | | `SSH_SESSION_TIMEOUT` | `7200` | 会话超时(秒) |
457
+ | | `SSH_CLIENT_TYPE` | `paramiko` | SSH 客户端实现:`paramiko` / `asyncssh` |
458
+ | | `SSH_FORCE_ENV_CONFIG` | `false` | 强制 env 配置覆盖 hosts.json |
459
+ | | `SSH_SUDO_PASSWORD` | (空) | sudo 密码,配合 `use_sudo=true` 走 `sudo -S` |
460
+
423
461
  ---
424
462
 
425
463
  ## ⚠️ 依赖版本兼容性
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssh-licco"
3
- version = "2.2.1"
3
+ version = "2.3.1"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-licco
3
- Version: 2.2.1
3
+ Version: 2.3.1
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
@@ -267,7 +267,7 @@ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
267
267
  | 3. 双层命令拦截 | `SSH_REMOTE_GUARD=true` + 远端 ForceCommand | 跳板机侧禁元字符 + 远端二次白名单校验 |
268
268
  | 4. 灾难性命令硬拦截(v2.2.0 新增) | 默认开启,不可关闭 | rm -rf 绝对路径、mkfs、raw-disk dd、fork-bomb、chmod -R 777/000 /、`> /dev/sd*` 等在 MCP 网关层被**无条件**拒绝;任何安全级别、`confirm_dangerous=true` 等均无法绕过 |
269
269
 
270
- > **关于 v2.1.0 引入的"高危操作审批"项**:v2.1.0 曾提供 `SSH_APPROVAL_GATE=true` + 3 个审批工具作为第 4 项加固。v2.2.0 起该项被**硬拦截取代**——审批流程依赖 AI 自报命令,存在闭环风险;硬拦截更直接,灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。`SSH_APPROVAL_GATE` 环境变量与 `ssh_mcp/approval.py` 代码仍保留作为参考,但不建议在生产启用。
270
+ > **关于 v2.1.0 引入的"高危操作审批"项**:v2.1.0 曾提供 `SSH_APPROVAL_GATE=true` + 3 个审批工具作为第 4 项加固。v2.2.0 起该项被**硬拦截取代**——审批流程依赖 AI 自报命令,存在闭环风险;硬拦截更直接,灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。`SSH_APPROVAL_GATE` 环境变量与 `ssh_mcp/approval.py` / `ssh_mcp/handlers/approval.py` 代码已在 v2.2.0 删除。
271
271
 
272
272
  完整方案见 [docs/SECURITY_HARDENING.md](docs/SECURITY_HARDENING.md)。
273
273
 
@@ -287,7 +287,7 @@ SSH 跳板模式部署的生产必做加固,补齐跳板机单点风险:
287
287
  | `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
288
288
  | `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
289
289
 
290
- > **关于 v2.1.0 引入的 3 个审批工具**(`ssh_request_approval` / `ssh_approve_command` / `ssh_list_approvals`):已从 MCP `list_tools()` 移除。审批流程依赖 AI 自报命令、运维侧背书,存在闭环风险;v2.2.0 的硬拦截更直接——灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。相关代码(`ssh_mcp/approval.py`、`ssh_mcp/handlers/approval.py`)保留作为参考,但不暴露给 MCP
290
+ > **关于 v2.1.0 引入的 3 个审批工具**(`ssh_request_approval` / `ssh_approve_command` / `ssh_list_approvals`):已从 MCP `list_tools()` 移除,**代码已在 v2.2.0 删除**(`ssh_mcp/approval.py`、`ssh_mcp/handlers/approval.py`)。审批流程依赖 AI 自报命令、运维侧背书,存在闭环风险;v2.2.0 的硬拦截更直接——灾难性命令在 MCP 网关层就被拒绝,运维侧不需要再走"先申请再审批"流程。
291
291
 
292
292
  ### 📖 详细文档
293
293
 
@@ -453,6 +453,44 @@ localhost:5432 - accepting connections
453
453
 
454
454
  - **[MCP_CONFIG_GUIDE.md](MCP_CONFIG_GUIDE.md)** - 包含所有配置选项的详细说明
455
455
 
456
+ ### 🌐 完整环境变量速查(v2.3.0)
457
+
458
+ > 下表所有变量均被代码读取。**注意**:
459
+ > - `SSH_RATE_LIMIT` 是 **bool** 总开关(true/false),`SSH_RATE_LIMIT_MAX` 才是次数上限,两者分开配置
460
+ > - 主机密钥检查(strict_host_key_checking)**不通过 env 配置**,请用 `ssh_connect` 工具参数或 `hosts.json`
461
+ > - 详细分组说明见 [docs/SECURITY_HARDENING.md#环境变量速查](docs/SECURITY_HARDENING.md#环境变量速查v230-完整列表)
462
+
463
+ | 分类 | 变量 | 默认 | 说明 |
464
+ |------|------|------|------|
465
+ | **安全** | `SSH_SECURITY_LEVEL` | `balanced` | 安全级别:`strict` / `balanced` / `relaxed` |
466
+ | | `SSH_BASE_DIR` | `/home` | 路径校验基目录 |
467
+ | | `SSH_EXTRA_ALLOWED_COMMANDS` | (空) | 额外允许的命令(逗号分隔) |
468
+ | | `SSH_ALLOWED_COMMANDS_FILE` | (空) | 命令白名单 JSON 文件路径 |
469
+ | | `SSH_AUDIT_LOG_PATH` | (空) | 审计日志文件路径 |
470
+ | **限流** | `SSH_RATE_LIMIT` | `true` | 限流总开关(bool) |
471
+ | | `SSH_RATE_LIMIT_MAX` | `30` | 限流次数上限 |
472
+ | | `SSH_RATE_LIMIT_WINDOW` | `60` | 限流窗口(秒) |
473
+ | **加固点 1** | `SSH_RUNTIME_GUARD` | `false` | runtime guard 总开关 |
474
+ | | `SSH_RUNTIME_ALLOW_ROOT` | `false` | 是否允许 root 启动 |
475
+ | | `SSH_RUNTIME_ALLOWED_USERS` | (空) | 允许启动的账号白名单 |
476
+ | | `SSH_RUNTIME_BLOCK_SUDO` | `true` | 是否拦截 sudo 上下文 |
477
+ | **加固点 2** | `SSH_SECRET_PROVIDER_ENABLED` | `false` | secret provider 总开关 |
478
+ | | `SSH_SECRET_PROVIDER` | `env` | 凭证 provider:`env` / `command` / `http` |
479
+ | | `SSH_SECRET_ENV_KEY_<NAME>` | — | env provider:私钥环境变量 |
480
+ | | `SSH_SECRET_COMMAND_<NAME>` | — | command provider:拉取私钥的命令 |
481
+ | | `SSH_SECRET_HTTP_URL_<NAME>` | — | http provider:拉取私钥的 URL |
482
+ | | `SSH_SECRET_HTTP_TOKEN` | — | http provider:Bearer token |
483
+ | | `SSH_SECRET_HTTP_TIMEOUT` | `15` | http provider:HTTP 超时(秒) |
484
+ | **加固点 3** | `SSH_REMOTE_GUARD` | `false` | 远端元字符拦截总开关 |
485
+ | **加固点 4** | *(无 env)* | — | 灾难性命令硬拦截,零配置零绕过 |
486
+ | **连接默认** | `SSH_HOST` / `SSH_PORT` / `SSH_USER` / `SSH_PASSWORD` | `127.0.0.1` / `22` / `root` / (空) | 单 host 模式默认连接参数 |
487
+ | | `SSH_TIMEOUT` | `60` | 连接超时(秒) |
488
+ | | `SSH_KEEPALIVE_INTERVAL` | `30` | keepalive 间隔(秒) |
489
+ | | `SSH_SESSION_TIMEOUT` | `7200` | 会话超时(秒) |
490
+ | | `SSH_CLIENT_TYPE` | `paramiko` | SSH 客户端实现:`paramiko` / `asyncssh` |
491
+ | | `SSH_FORCE_ENV_CONFIG` | `false` | 强制 env 配置覆盖 hosts.json |
492
+ | | `SSH_SUDO_PASSWORD` | (空) | sudo 密码,配合 `use_sudo=true` 走 `sudo -S` |
493
+
456
494
  ---
457
495
 
458
496
  ## ⚠️ 依赖版本兼容性
@@ -9,7 +9,6 @@ ssh_licco.egg-info/requires.txt
9
9
  ssh_licco.egg-info/top_level.txt
10
10
  ssh_mcp/__init__.py
11
11
  ssh_mcp/__main__.py
12
- ssh_mcp/approval.py
13
12
  ssh_mcp/audit_logger.py
14
13
  ssh_mcp/batch_executor.py
15
14
  ssh_mcp/cli.py
@@ -34,7 +33,6 @@ ssh_mcp/clients/factory.py
34
33
  ssh_mcp/clients/interface.py
35
34
  ssh_mcp/clients/paramiko_client.py
36
35
  ssh_mcp/handlers/__init__.py
37
- ssh_mcp/handlers/approval.py
38
36
  ssh_mcp/handlers/connect.py
39
37
  ssh_mcp/handlers/context.py
40
38
  ssh_mcp/handlers/docker.py
@@ -1,8 +1,8 @@
1
1
  # SSH-LICCO 版本信息
2
2
  # 所有代码中的版本信息都从这里读取
3
- # 版本号:2.2.1
3
+ # 版本号:2.3.1
4
4
 
5
- __version__ = "2.2.1"
5
+ __version__ = "2.3.1"
6
6
  __author__ = "Li Qi"
7
7
  __email__ = "1985694657@qq.com"
8
8
  __license__ = "MIT"
@@ -7,7 +7,6 @@ from collections.abc import Awaitable, Callable
7
7
  from mcp.types import TextContent
8
8
 
9
9
  from . import schemas
10
- from .approval import handle_approve_command, handle_list_approvals, handle_request_approval
11
10
  from .connect import handle_connect, handle_disconnect
12
11
  from .context import HandlerContext
13
12
  from .docker import handle_docker
@@ -30,9 +29,6 @@ HANDLERS: dict[str, Handler] = {
30
29
  "ssh_generate_key": handle_generate_key,
31
30
  "ssh_session": handle_session,
32
31
  "ssh_process": handle_process,
33
- "ssh_request_approval": handle_request_approval,
34
- "ssh_approve_command": handle_approve_command,
35
- "ssh_list_approvals": handle_list_approvals,
36
32
  }
37
33
 
38
34
  __all__ = ["HANDLERS", "schemas"]
@@ -13,7 +13,6 @@ from ..security import SecurityError, command_validator
13
13
  from .connect import handle_connect
14
14
  from .context import HandlerContext
15
15
  from .utils import (
16
- check_approval_gate,
17
16
  diagnose_exit_code,
18
17
  diagnose_startup_failure,
19
18
  ensure_session,
@@ -55,14 +54,6 @@ async def handle_execute(ctx: HandlerContext, args: dict) -> list[TextContent]:
55
54
  ]
56
55
  command = normalized
57
56
 
58
- # 加固点 4:高危操作审批门禁
59
- # 当 SSH_APPROVAL_GATE=true 且命令被判定为高危时,必须携带有效 approval_id 才能执行。
60
- approval_id = args.get("approval_id")
61
- approval_err = check_approval_gate(command, approval_id)
62
- if approval_err:
63
- ctx.logger.warning(f"Command blocked by approval gate: {command}")
64
- return [TextContent(type="text", text=approval_err)]
65
-
66
57
  # Resolve session via session_id / name / host / env fallback
67
58
  session_id = await ensure_session(ctx, args, handle_connect)
68
59
  if not session_id:
@@ -137,10 +137,6 @@ TOOLS: dict[str, Tool] = {
137
137
  "default": False,
138
138
  "description": "Bypass security validation for known-dangerous commands (e.g. rm -rf /path). Use with caution — only for operations you explicitly intend to perform.",
139
139
  },
140
- "approval_id": {
141
- "type": "string",
142
- "description": "加固点 4:高危操作审批 ID。当 SSH_APPROVAL_GATE=true 且命令被判定为高危时,必须先调用 ssh_request_approval 获取 approval_id,再带上此参数执行。否则高危命令会被拒绝。",
143
- },
144
140
  "remote_guard": {
145
141
  "type": "boolean",
146
142
  "default": False,
@@ -496,61 +492,4 @@ TOOLS: dict[str, Tool] = {
496
492
  "required": ["session_id", "action"],
497
493
  },
498
494
  ),
499
- "ssh_request_approval": Tool(
500
- name="ssh_request_approval",
501
- description="加固点 4:高危操作审批 — AI 提交审批申请。当 SSH_APPROVAL_GATE=true 时,rm -rf、reboot、iptables 等高危命令必须先调用本工具申请审批,获得 approval_id 后再调用 ssh_execute 携带 approval_id 执行。AI 不能直接下发高危命令。",
502
- inputSchema={
503
- "type": "object",
504
- "properties": {
505
- "command": {
506
- "type": "string",
507
- "description": "需要审批的高危命令(必须与后续 ssh_execute 的 command 完全一致,否则校验失败)。",
508
- },
509
- "reason": {
510
- "type": "string",
511
- "description": "申请理由:为什么需要执行此高危命令、预期影响、回滚方案。",
512
- },
513
- },
514
- "required": ["command", "reason"],
515
- },
516
- ),
517
- "ssh_approve_command": Tool(
518
- name="ssh_approve_command",
519
- description="加固点 4:高危操作审批 — 运维人员人工审批 AI 提交的申请。返回审批结果。审批通过后 AI 可用获得的 approval_id 执行命令(一次性,用后即焚)。",
520
- inputSchema={
521
- "type": "object",
522
- "properties": {
523
- "approval_id": {
524
- "type": "string",
525
- "description": "待审批的 approval_id(由 ssh_request_approval 返回)。",
526
- },
527
- "decision": {
528
- "type": "string",
529
- "enum": ["approved", "rejected"],
530
- "description": "审批决定:approved=同意执行,rejected=拒绝。",
531
- },
532
- "reviewer": {
533
- "type": "string",
534
- "description": "审批人标识(运维人员姓名/账号),用于审计。",
535
- },
536
- "comment": {"type": "string", "description": "审批意见(可选)。"},
537
- },
538
- "required": ["approval_id", "decision", "reviewer"],
539
- },
540
- ),
541
- "ssh_list_approvals": Tool(
542
- name="ssh_list_approvals",
543
- description="加固点 4:高危操作审批 — 列出审批记录。运维人员查看待处理队列或历史记录。action=pending 只看待审批,action=all 查看全部(最近 100 条)。",
544
- inputSchema={
545
- "type": "object",
546
- "properties": {
547
- "action": {
548
- "type": "string",
549
- "enum": ["pending", "all"],
550
- "default": "pending",
551
- "description": "pending=仅待审批,all=全部记录(最近 100 条)。",
552
- }
553
- },
554
- },
555
- ),
556
495
  }
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
6
5
  import re
7
6
  import shlex
8
7
  from collections.abc import Awaitable, Callable
@@ -116,53 +115,6 @@ def normalize_command_for_remote_guard(command: str) -> tuple[str, str | None]:
116
115
  return command, None
117
116
 
118
117
 
119
- def check_approval_gate(command: str, approval_id: str | None) -> str | None:
120
- """加固点 4:高危操作审批门禁。
121
-
122
- 当 SSH_APPROVAL_GATE=true 时,高危命令(CRITICAL/HIGH 风险)必须携带有效
123
- approval_id(由 ssh_request_approval 工具申请、人工 approve 后获得)才能执行。
124
-
125
- 返回 None 表示放行;返回字符串表示拒绝原因(作为 MCP 返回文本)。
126
- """
127
- if os.getenv("SSH_APPROVAL_GATE", "false").lower() != "true":
128
- return None # 审批门禁未启用
129
-
130
- from ..security import RiskLevel, command_validator
131
-
132
- risk = command_validator.assess_risk_level(command)
133
-
134
- # 仅 CRITICAL / HIGH 风险需要审批;MEDIUM/LOW/SAFE 直接放行
135
- if risk not in (RiskLevel.CRITICAL, RiskLevel.HIGH):
136
- return None
137
-
138
- if not approval_id:
139
- return (
140
- f"❌ 高危操作审批门禁拦截\n\n"
141
- f"Command: {command}\n"
142
- f"Risk: {risk.value}\n\n"
143
- f"此命令被判定为「{risk.value}」风险,启用了人工审批门禁(SSH_APPROVAL_GATE=true)。\n"
144
- f"AI 不能直接下发此类高危运维命令,必须先申请审批:\n\n"
145
- f" 1. 调用 ssh_request_approval 工具,提交 command 与 reason;\n"
146
- f" 2. 等待人工审批通过,获得 approval_id;\n"
147
- f" 3. 用 ssh_execute 携带 approval_id 重新执行。\n\n"
148
- f"高危命令类别:rm -rf、reboot/shutdown、iptables/防火墙修改、mkfs、dd 覆盘等。"
149
- )
150
-
151
- # 校验 approval_id 有效性
152
- from ..approval import ApprovalGate
153
-
154
- gate = ApprovalGate.instance()
155
- ok, reason = gate.verify(approval_id, command)
156
- if not ok:
157
- return (
158
- f"❌ 审批校验失败\n\n"
159
- f"approval_id: {approval_id}\n"
160
- f"Reason: {reason}\n\n"
161
- f"请重新申请审批(ssh_request_approval)或确认 approval_id 是否匹配当前命令。"
162
- )
163
- return None
164
-
165
-
166
118
  def diagnose_exit_code(exit_code: int, log_tail: str, stderr: str) -> str:
167
119
  """根据退出码和日志输出诊断命令失败原因"""
168
120
  combined = (log_tail + " " + stderr).lower()
@@ -1,220 +0,0 @@
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()
@@ -1,131 +0,0 @@
1
- """Handlers for approval workflow tools."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
-
7
- from mcp.types import TextContent
8
-
9
- from .context import HandlerContext
10
-
11
-
12
- def _fmt_ts(ts: float | None) -> str:
13
- """Format a Unix timestamp to a human-readable string."""
14
- if ts is None:
15
- return "N/A"
16
- return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
17
-
18
-
19
- async def handle_request_approval(ctx: HandlerContext, args: dict) -> list[TextContent]:
20
- """加固点 4:AI 提交高危命令审批申请。"""
21
- from ..approval import ApprovalGate
22
- from ..security import command_validator
23
-
24
- command = args.get("command", "").strip()
25
- reason = args.get("reason", "").strip()
26
- if not command:
27
- return [TextContent(type="text", text="command 不能为空")]
28
- if not reason:
29
- return [TextContent(type="text", text="reason 不能为空:必须说明为什么需要执行此高危命令")]
30
-
31
- risk = command_validator.assess_risk_level(command)
32
- gate = ApprovalGate.instance()
33
- rec = gate.request(command, reason)
34
-
35
- return [
36
- TextContent(
37
- type="text",
38
- text=(
39
- f"✅ 审批申请已提交\n\n"
40
- f"approval_id: {rec.approval_id}\n"
41
- f"command: {command}\n"
42
- f"risk: {risk.value}\n"
43
- f"reason: {reason}\n"
44
- f"requested_at: {_fmt_ts(rec.requested_at)}\n"
45
- f"TTL: {rec.ttl}s(超时自动失效)\n\n"
46
- f"⏳ 等待运维人员审批。\n"
47
- " 审批人请调用 ssh_approve_command("
48
- f"approval_id='{rec.approval_id}', "
49
- "decision='approved'|'rejected', reviewer='...')\n"
50
- " 审批通过后,调用 ssh_execute("
51
- f"command=..., approval_id='{rec.approval_id}') 执行。\n"
52
- " 注意:approval_id 一次性消费,命令必须与此申请完全一致。"
53
- ),
54
- )
55
- ]
56
-
57
-
58
- async def handle_approve_command(ctx: HandlerContext, args: dict) -> list[TextContent]:
59
- """加固点 4:运维人员审批 AI 提交的申请。"""
60
- from ..approval import ApprovalGate
61
-
62
- approval_id = args.get("approval_id", "").strip()
63
- decision = args.get("decision", "").strip()
64
- reviewer = args.get("reviewer", "").strip()
65
- comment = args.get("comment", "")
66
-
67
- if not approval_id:
68
- return [TextContent(type="text", text="approval_id 不能为空")]
69
- if decision not in ("approved", "rejected"):
70
- return [TextContent(type="text", text="decision 必须是 approved 或 rejected")]
71
- if not reviewer:
72
- return [TextContent(type="text", text="reviewer 不能为空:必须填写审批人标识")]
73
-
74
- gate = ApprovalGate.instance()
75
- try:
76
- rec = gate.approve(approval_id, reviewer, decision)
77
- except (KeyError, ValueError) as e:
78
- return [TextContent(type="text", text=f"审批失败:{e}")]
79
-
80
- status_emoji = "✅" if rec.status == "approved" else "❌"
81
- return [
82
- TextContent(
83
- type="text",
84
- text=(
85
- f"{status_emoji} 审批已完成\n\n"
86
- f"approval_id: {rec.approval_id}\n"
87
- f"command: {rec.command}\n"
88
- f"status: {rec.status}\n"
89
- f"reviewer: {rec.reviewer}\n"
90
- f"decided_at: {_fmt_ts(rec.decided_at)}\n"
91
- )
92
- + (f"comment: {comment}\n" if comment else "")
93
- + (
94
- "\n👉 审批通过,AI 可执行:ssh_execute("
95
- f"command='{rec.command}', approval_id='{rec.approval_id}')"
96
- if rec.status == "approved"
97
- else "\n🚫 审批拒绝,AI 不可执行此命令。"
98
- ),
99
- )
100
- ]
101
-
102
-
103
- async def handle_list_approvals(ctx: HandlerContext, args: dict) -> list[TextContent]:
104
- """加固点 4:列出审批记录。"""
105
- from ..approval import ApprovalGate
106
-
107
- action = args.get("action", "pending")
108
- gate = ApprovalGate.instance()
109
- recs = gate.list_pending() if action == "pending" else gate.list_all()
110
-
111
- if not recs:
112
- return [
113
- TextContent(type="text", text=f"没有{('待审批' if action == 'pending' else '')}记录。")
114
- ]
115
-
116
- lines = [f"📋 审批记录({action},共 {len(recs)} 条)\n" + "=" * 60]
117
- for r in recs:
118
- lines.append(
119
- f"\napproval_id: {r.approval_id}\n"
120
- f" command: {r.command}\n"
121
- f" status: {r.status}\n"
122
- f" risk_reason: {r.reason}\n"
123
- f" requested_at: {_fmt_ts(r.requested_at)}\n"
124
- + (
125
- f" reviewer: {r.reviewer}\n decided_at: {_fmt_ts(r.decided_at)}\n"
126
- if r.decided_at
127
- else ""
128
- )
129
- + (f" consumed_at: {_fmt_ts(r.consumed_at)}\n" if r.consumed_at else "")
130
- )
131
- return [TextContent(type="text", text="\n".join(lines))]
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
File without changes