remote-cmd-manager 2.3.0__tar.gz → 2.5.0__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 (83) hide show
  1. {remote_cmd_manager-2.3.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.5.0}/PKG-INFO +29 -21
  2. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/README.md +24 -16
  3. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/pyproject.toml +15 -8
  4. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/__init__.py +12 -2
  5. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/_version.py +1 -1
  6. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/json_host_repository.py +61 -2
  7. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/sqlite_host_repository.py +43 -2
  8. remote_cmd_manager-2.5.0/remote_cmd/service/_host_runner.py +202 -0
  9. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_types.py +33 -1
  10. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/async_batch_executor.py +95 -46
  11. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/batch_executor.py +31 -3
  12. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/storage_factory.py +37 -11
  13. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/credential_guard.py +18 -0
  14. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/exceptions.py +17 -0
  15. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/logging_utils.py +7 -2
  16. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0/remote_cmd_manager.egg-info}/PKG-INFO +29 -21
  17. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/SOURCES.txt +1 -0
  18. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/requires.txt +3 -2
  19. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_async_batch_executor.py +165 -0
  20. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_batch_executor.py +100 -1
  21. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_logging_utils.py +14 -0
  22. remote_cmd_manager-2.5.0/tests/test_output_policy.py +172 -0
  23. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_release_metadata.py +13 -0
  24. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_repository.py +69 -0
  25. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_sqlite_repository.py +49 -2
  26. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_storage_factory.py +40 -2
  27. remote_cmd_manager-2.3.0/remote_cmd/service/_host_runner.py +0 -115
  28. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/LICENSE +0 -0
  29. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/MANIFEST.in +0 -0
  30. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/config.example.yaml +0 -0
  31. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/basic_usage.py +0 -0
  32. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/deploy_script.py +0 -0
  33. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/nginx_batch_update.py +0 -0
  34. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/system_health_check.py +0 -0
  35. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/__init__.py +0 -0
  36. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/main.py +0 -0
  37. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/__init__.py +0 -0
  38. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_connection_pool.py +0 -0
  39. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_ssh_client.py +0 -0
  40. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host.py +0 -0
  41. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host_manager.py +0 -0
  42. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/ssh_client.py +0 -0
  43. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/sync_connection_pool.py +0 -0
  44. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/__init__.py +0 -0
  45. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/host_repository.py +0 -0
  46. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/__init__.py +0 -0
  47. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_pool_policy.py +0 -0
  48. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/credential_provider.py +0 -0
  49. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/host_service.py +0 -0
  50. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/retry_policy.py +0 -0
  51. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/ssh_service.py +0 -0
  52. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/task_runner.py +0 -0
  53. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/__init__.py +0 -0
  54. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/config.py +0 -0
  55. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/crypto.py +0 -0
  56. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
  57. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
  58. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
  59. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/requirements.txt +0 -0
  60. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/setup.cfg +0 -0
  61. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/__init__.py +0 -0
  62. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/conftest.py +0 -0
  63. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/integration/conftest.py +0 -0
  64. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/integration/test_ssh_connection.py +0 -0
  65. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/performance/__init__.py +0 -0
  66. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/performance/conftest.py +0 -0
  67. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/performance/test_benchmarks.py +0 -0
  68. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_async_ssh_client.py +0 -0
  69. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_cli.py +0 -0
  70. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_config.py +0 -0
  71. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_credential_provider.py +0 -0
  72. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_crypto.py +0 -0
  73. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_exceptions.py +0 -0
  74. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_host.py +0 -0
  75. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_host_manager.py +0 -0
  76. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_host_service.py +0 -0
  77. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_keyring_provider.py +0 -0
  78. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_pool_policy.py +0 -0
  79. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_retry_policy.py +0 -0
  80. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_client.py +0 -0
  81. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_service.py +0 -0
  82. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_sync_connection_pool.py +0 -0
  83. {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_task_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remote_cmd_manager
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more.
5
5
  Author-email: Vae-Scrooge <vaescrooge@gmail.com>
6
6
  License: MIT
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: System Administrators
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
@@ -27,17 +26,17 @@ Classifier: Environment :: Console
27
26
  Classifier: Operating System :: POSIX :: Linux
28
27
  Classifier: Operating System :: MacOS
29
28
  Classifier: Operating System :: Microsoft :: Windows
30
- Requires-Python: >=3.9
29
+ Requires-Python: >=3.10
31
30
  Description-Content-Type: text/markdown
32
31
  License-File: LICENSE
33
- Requires-Dist: paramiko>=3.0
32
+ Requires-Dist: paramiko<6,>=5.0
34
33
  Requires-Dist: click>=8.0
35
34
  Requires-Dist: cryptography>=41.0
36
35
  Requires-Dist: rich>=13.0
37
36
  Requires-Dist: PyYAML>=6.0
38
37
  Requires-Dist: aiofiles>=3.0
39
38
  Provides-Extra: async
40
- Requires-Dist: asyncssh>=2.14.0; extra == "async"
39
+ Requires-Dist: asyncssh<3,>=2.24.0; extra == "async"
41
40
  Provides-Extra: cloud
42
41
  Requires-Dist: boto3>=1.34; extra == "cloud"
43
42
  Provides-Extra: dev
@@ -46,6 +45,7 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
46
45
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
47
46
  Requires-Dist: ruff>=0.4; extra == "dev"
48
47
  Requires-Dist: mypy>=1.10; extra == "dev"
48
+ Requires-Dist: asyncssh<3,>=2.24.0; extra == "dev"
49
49
  Requires-Dist: keyring>=24.0; extra == "dev"
50
50
  Provides-Extra: docs
51
51
  Requires-Dist: pdoc==15.0.4; extra == "docs"
@@ -55,7 +55,7 @@ Dynamic: license-file
55
55
  <img src="https://img.shields.io/pypi/v/remote_cmd_manager?style=for-the-badge&logo=pypi&logoColor=white&label=PyPI" alt="PyPI">
56
56
  <img src="https://img.shields.io/pypi/dm/remote_cmd_manager?style=for-the-badge&logo=python&logoColor=white&label=Downloads" alt="Downloads">
57
57
  <img src="https://img.shields.io/github/stars/Vae-Scrooge/remote-cmd?style=for-the-badge&logo=github" alt="Stars">
58
- <img src="https://img.shields.io/badge/python-3.9%2B-blue?style=for-the-badge&logo=python" alt="Python">
58
+ <img src="https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge&logo=python" alt="Python">
59
59
  <img src="https://img.shields.io/github/license/Vae-Scrooge/remote-cmd?style=for-the-badge" alt="License">
60
60
  <img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
61
61
  </p>
@@ -89,28 +89,36 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
89
89
 
90
90
  ---
91
91
 
92
- ## v2.3.0 Release Highlights
92
+ ## v2.5.0 Release Highlights
93
93
 
94
- v2.3.0 makes blocking SFTP operations timeout-safe, hardens async cancellation cleanup,
95
- and aligns the supported Python range and release tooling. See the
94
+ v2.5.0 is a hardening and scalability release: dependency security floors, Python 3.9
95
+ removal, bounded async scheduling, and explicit output-retention / credential-persistence
96
+ policies. Backward-compatible defaults are preserved where behavior is involved. See the
96
97
  [full migration notes](./CHANGELOG.md) before upgrading automated callers.
97
98
 
98
- ### SFTP Timeouts & Reliability
99
+ ### Security & Platform
99
100
 
100
- - Blocking SFTP operations now support an inactivity timeout (default: `ConnectionConfig.timeout`, 30 s): uploads, downloads, directory listing, directory creation, removal, and file-info calls accept an optional `timeout` argument.
101
- - The synchronous `SSHClient` applies the timeout to the Paramiko SFTP channel, so a stalled read/write aborts the actual operation and raises `SSHFileTransferError`.
102
- - The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
103
- - Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
101
+ - Minimum supported Python is now **3.10** (3.9 reached EOL on 2025-10-31).
102
+ - Security floors raised: **Paramiko >= 5.0,<6** and **AsyncSSH >= 2.24.0,<3** (2.23.x and earlier are affected by 2026 AsyncSSH advisories).
104
103
 
105
- ### Release Engineering
104
+ ### Scalability
106
105
 
107
- - Python 3.9–3.14 are officially supported and CI-tested; `Requires-Python` remains `>=3.9`.
108
- - CI runs release-metadata validation for CHANGELOG.md-only changes.
109
- - Publish builds pin `build==1.5.0` and `twine==7.0.0`.
106
+ - `AsyncBatchExecutor` now uses a **bounded worker queue**: only `min(max_concurrency, host count)` worker tasks are created instead of one task per host, so scheduling memory no longer grows with fleet size.
107
+ - New `OutputPolicy` (exported from `remote_cmd`) gives an explicit, mutually exclusive alternative to `max_output_bytes`; `None` still means full retention in v2.5, and a bounded default is planned for v3.0.
108
+
109
+ ### Credential Safety
110
+
111
+ - `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
112
+ - JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
113
+ - The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
114
+
115
+ ### Compatibility
116
+
117
+ - `max_output_bytes=None` still retains full output, and the public `BatchResult` / `BatchHostResult` schemas are unchanged; the legacy `max_output_bytes` parameter continues to work alongside `OutputPolicy`.
110
118
 
111
119
  ## Table of Contents
112
120
 
113
- - [v2.3.0 Release Highlights](#v230-release-highlights)
121
+ - [v2.5.0 Release Highlights](#v250-release-highlights)
114
122
  - [Why Remote CMD?](#why-remote-cmd)
115
123
  - [Quick Start](#quick-start)
116
124
  - [Use Cases](#use-cases)
@@ -258,7 +266,7 @@ with SSHClient(config) as client:
258
266
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
259
267
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
260
268
  | **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
261
- | **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
269
+ | **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
262
270
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
263
271
  | **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
264
272
  | **Connection Test** | Test all host SSH connections and report status |
@@ -348,7 +356,7 @@ Good first issues are labelled `good first issue` in the
348
356
  developed independently as a focused alternative to heavyweight tools for the
349
357
  ad-hoc SSH tasks that come up in day-to-day server work.
350
358
 
351
- - **Project health:** CI runs on every PR, Python 3.9+ is supported, and the
359
+ - **Project health:** CI runs on every PR, Python 3.10+ is supported, and the
352
360
  public API is versioned under [semantic versioning](https://semver.org/).
353
361
  - **Your code, your servers:** usage stays open under the MIT license — nothing
354
362
  is telemetry-driven or locked behind a service.
@@ -2,7 +2,7 @@
2
2
  <img src="https://img.shields.io/pypi/v/remote_cmd_manager?style=for-the-badge&logo=pypi&logoColor=white&label=PyPI" alt="PyPI">
3
3
  <img src="https://img.shields.io/pypi/dm/remote_cmd_manager?style=for-the-badge&logo=python&logoColor=white&label=Downloads" alt="Downloads">
4
4
  <img src="https://img.shields.io/github/stars/Vae-Scrooge/remote-cmd?style=for-the-badge&logo=github" alt="Stars">
5
- <img src="https://img.shields.io/badge/python-3.9%2B-blue?style=for-the-badge&logo=python" alt="Python">
5
+ <img src="https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge&logo=python" alt="Python">
6
6
  <img src="https://img.shields.io/github/license/Vae-Scrooge/remote-cmd?style=for-the-badge" alt="License">
7
7
  <img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
8
8
  </p>
@@ -36,28 +36,36 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
36
36
 
37
37
  ---
38
38
 
39
- ## v2.3.0 Release Highlights
39
+ ## v2.5.0 Release Highlights
40
40
 
41
- v2.3.0 makes blocking SFTP operations timeout-safe, hardens async cancellation cleanup,
42
- and aligns the supported Python range and release tooling. See the
41
+ v2.5.0 is a hardening and scalability release: dependency security floors, Python 3.9
42
+ removal, bounded async scheduling, and explicit output-retention / credential-persistence
43
+ policies. Backward-compatible defaults are preserved where behavior is involved. See the
43
44
  [full migration notes](./CHANGELOG.md) before upgrading automated callers.
44
45
 
45
- ### SFTP Timeouts & Reliability
46
+ ### Security & Platform
46
47
 
47
- - Blocking SFTP operations now support an inactivity timeout (default: `ConnectionConfig.timeout`, 30 s): uploads, downloads, directory listing, directory creation, removal, and file-info calls accept an optional `timeout` argument.
48
- - The synchronous `SSHClient` applies the timeout to the Paramiko SFTP channel, so a stalled read/write aborts the actual operation and raises `SSHFileTransferError`.
49
- - The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
50
- - Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
48
+ - Minimum supported Python is now **3.10** (3.9 reached EOL on 2025-10-31).
49
+ - Security floors raised: **Paramiko >= 5.0,<6** and **AsyncSSH >= 2.24.0,<3** (2.23.x and earlier are affected by 2026 AsyncSSH advisories).
51
50
 
52
- ### Release Engineering
51
+ ### Scalability
53
52
 
54
- - Python 3.9–3.14 are officially supported and CI-tested; `Requires-Python` remains `>=3.9`.
55
- - CI runs release-metadata validation for CHANGELOG.md-only changes.
56
- - Publish builds pin `build==1.5.0` and `twine==7.0.0`.
53
+ - `AsyncBatchExecutor` now uses a **bounded worker queue**: only `min(max_concurrency, host count)` worker tasks are created instead of one task per host, so scheduling memory no longer grows with fleet size.
54
+ - New `OutputPolicy` (exported from `remote_cmd`) gives an explicit, mutually exclusive alternative to `max_output_bytes`; `None` still means full retention in v2.5, and a bounded default is planned for v3.0.
55
+
56
+ ### Credential Safety
57
+
58
+ - `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
59
+ - JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
60
+ - The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
61
+
62
+ ### Compatibility
63
+
64
+ - `max_output_bytes=None` still retains full output, and the public `BatchResult` / `BatchHostResult` schemas are unchanged; the legacy `max_output_bytes` parameter continues to work alongside `OutputPolicy`.
57
65
 
58
66
  ## Table of Contents
59
67
 
60
- - [v2.3.0 Release Highlights](#v230-release-highlights)
68
+ - [v2.5.0 Release Highlights](#v250-release-highlights)
61
69
  - [Why Remote CMD?](#why-remote-cmd)
62
70
  - [Quick Start](#quick-start)
63
71
  - [Use Cases](#use-cases)
@@ -205,7 +213,7 @@ with SSHClient(config) as client:
205
213
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
206
214
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
207
215
  | **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
208
- | **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
216
+ | **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
209
217
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
210
218
  | **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
211
219
  | **Connection Test** | Test all host SSH connections and report status |
@@ -295,7 +303,7 @@ Good first issues are labelled `good first issue` in the
295
303
  developed independently as a focused alternative to heavyweight tools for the
296
304
  ad-hoc SSH tasks that come up in day-to-day server work.
297
305
 
298
- - **Project health:** CI runs on every PR, Python 3.9+ is supported, and the
306
+ - **Project health:** CI runs on every PR, Python 3.10+ is supported, and the
299
307
  public API is versioned under [semantic versioning](https://semver.org/).
300
308
  - **Your code, your servers:** usage stays open under the MIT license — nothing
301
309
  is telemetry-driven or locked behind a service.
@@ -7,7 +7,7 @@ name = "remote_cmd_manager"
7
7
  dynamic = ["version"]
8
8
  description = "Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more."
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
12
  authors = [
13
13
  { name = "Vae-Scrooge", email = "vaescrooge@gmail.com" },
@@ -19,7 +19,6 @@ classifiers = [
19
19
  "Intended Audience :: System Administrators",
20
20
  "License :: OSI Approved :: MIT License",
21
21
  "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.9",
23
22
  "Programming Language :: Python :: 3.10",
24
23
  "Programming Language :: Python :: 3.11",
25
24
  "Programming Language :: Python :: 3.12",
@@ -33,7 +32,7 @@ classifiers = [
33
32
  "Operating System :: Microsoft :: Windows",
34
33
  ]
35
34
  dependencies = [
36
- "paramiko>=3.0",
35
+ "paramiko>=5.0,<6",
37
36
  "click>=8.0",
38
37
  "cryptography>=41.0",
39
38
  "rich>=13.0",
@@ -43,22 +42,24 @@ dependencies = [
43
42
 
44
43
  [project.optional-dependencies]
45
44
  # 异步原生执行内核:启用 BatchExecutor(use_async=True) / AsyncSSHClient / AsyncConnectionPool 时需要
46
- async = ["asyncssh>=2.14.0"]
45
+ # 下限锁定在 2.24.x:2.23.x 及更早版本存在 2026 年公开 CVE(SCP 路径穿越等)。
46
+ async = ["asyncssh>=2.24.0,<3"]
47
47
  # 云平台主机发现(Phase 4 预留)
48
48
  cloud = ["boto3>=1.34"]
49
49
  # 开发与测试
50
+ # 包含 async extra:默认测试套件会 import 异步模块,仅装 dev 无法收集。
50
51
  dev = [
51
52
  "pytest>=8.0",
52
53
  "pytest-asyncio>=0.23",
53
54
  "pytest-cov>=4.0",
54
55
  "ruff>=0.4",
55
56
  "mypy>=1.10",
57
+ "asyncssh>=2.24.0,<3",
56
58
  # 测试 keyring 凭据提供者(生产为可选依赖,测试环境固定安装)
57
59
  "keyring>=24.0",
58
60
  ]
59
- # 文档生成(pdoc>=16 需 Python>=3.10,固定到 15.x 以兼容 requires-python>=3.9)。
60
- # 精确 pin 15.0.4:pdoc HTML 输出随版本/解释器变化,docs/api 漂移门禁
61
- # (scripts/check_docs_drift.py)要求生成结果可复现。
61
+ # 文档生成。pdoc 固定到 15.0.4:docs/api 漂移门禁(scripts/check_docs_drift.py)
62
+ # 要求生成结果可复现。
62
63
  docs = ["pdoc==15.0.4"]
63
64
 
64
65
  [project.urls]
@@ -88,7 +89,7 @@ markers = [
88
89
  ]
89
90
 
90
91
  [tool.ruff]
91
- target-version = "py39"
92
+ target-version = "py310"
92
93
  line-length = 100
93
94
 
94
95
  [tool.ruff.lint]
@@ -109,6 +110,12 @@ ignore = [
109
110
  "N812",
110
111
  "N999",
111
112
  "B904",
113
+ # PEP 604 / 新式导入重写推迟到 v2.6(保持 v2.5 变更面最小)
114
+ "UP006", # dict/list 泛型重写
115
+ "UP007", # Union -> X|Y
116
+ "UP035", # typing.* 导入重写
117
+ "UP041", # IOError -> TimeoutError 别名
118
+ "UP045", # Optional -> X|None
112
119
  ]
113
120
 
114
121
  [tool.ruff.lint.per-file-ignores]
@@ -48,7 +48,7 @@ Remote CMD - SSH 远程服务器管理工具
48
48
  - 文档: 参见 docs/ 目录
49
49
 
50
50
  Author: Vae-Scrooge
51
- Version: 2.3.0(单一真相源见 remote_cmd._version)
51
+ Version: 2.5.0(单一真相源见 remote_cmd._version)
52
52
  License: MIT
53
53
  """
54
54
 
@@ -96,10 +96,16 @@ from remote_cmd.service import (
96
96
  HostService,
97
97
  SSHService,
98
98
  )
99
- from remote_cmd.service.batch_executor import BatchExecutor, BatchHostResult, BatchResult
99
+ from remote_cmd.service.batch_executor import (
100
+ BatchExecutor,
101
+ BatchHostResult,
102
+ BatchResult,
103
+ OutputPolicy,
104
+ )
100
105
  from remote_cmd.service.credential_provider import KeyringCredentialProvider
101
106
  from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
102
107
  from remote_cmd.utils.crypto import CredentialEncryption
108
+ from remote_cmd.utils.exceptions import PlaintextCredentialWarning
103
109
  from remote_cmd.utils.logging_utils import (
104
110
  SensitiveDataFilter,
105
111
  get_logger,
@@ -134,6 +140,8 @@ if _HAS_ASYNC:
134
140
  "BatchExecutor",
135
141
  "BatchResult",
136
142
  "BatchHostResult",
143
+ "OutputPolicy",
144
+ "PlaintextCredentialWarning",
137
145
  "SyncConnectionPool",
138
146
  "TaskRunner",
139
147
  "Task",
@@ -167,6 +175,8 @@ else:
167
175
  "BatchExecutor",
168
176
  "BatchResult",
169
177
  "BatchHostResult",
178
+ "OutputPolicy",
179
+ "PlaintextCredentialWarning",
170
180
  "SyncConnectionPool",
171
181
  "TaskRunner",
172
182
  "Task",
@@ -9,4 +9,4 @@
9
9
  - ``__version__`` 由 ``remote_cmd/__init__.py`` 导入并继续作为公共 API 导出。
10
10
  """
11
11
 
12
- __version__ = "2.3.0"
12
+ __version__ = "2.5.0"
@@ -4,8 +4,14 @@ JSON 文件主机仓库实现
4
4
  使用 JSON 文件存储主机配置,支持:
5
5
  - 原子写入(先写临时文件再重命名,防止崩溃导致数据丢失)
6
6
  - 可选加密(通过 CredentialEncryption 加密 password 字段)
7
+ - 明文持久化策略(v2.5)
7
8
  - config version management
8
9
  - 自动从旧版本迁移
10
+
11
+ 并发语义(single-writer):
12
+ - 本实现面向**单进程/单写入者**"; 多进程并发写同一 JSON 文件
13
+ 可能丢失更新(读-改-写无跨进程锁)。需要多进程/多写入者时,
14
+ 请使用 SqliteHostRepository(WAL + busy_timeout 处理并发写)。
9
15
  """
10
16
 
11
17
  import builtins
@@ -14,13 +20,15 @@ import json
14
20
  import logging
15
21
  import os
16
22
  import tempfile
23
+ import warnings
17
24
  from pathlib import Path
18
25
  from typing import Optional
19
26
 
20
27
  from remote_cmd.core.host import Host
21
28
  from remote_cmd.repository.host_repository import HostRepository
22
- from remote_cmd.utils.credential_guard import PasswordGuard
29
+ from remote_cmd.utils.credential_guard import PasswordGuard, is_plaintext_password
23
30
  from remote_cmd.utils.crypto import CredentialEncryption
31
+ from remote_cmd.utils.exceptions import CredentialError, PlaintextCredentialWarning
24
32
 
25
33
  logger = logging.getLogger(__name__)
26
34
 
@@ -36,6 +44,16 @@ class JsonHostRepository(HostRepository):
36
44
  filepath: JSON 文件路径
37
45
  encryption: 可选的凭据加密器(设置后自动加密 password)
38
46
  auto_load: 初始化时是否自动加载已有文件(默认 True)
47
+ allow_plaintext_credentials: 明文密码持久化策略(v2.5;默认 None
48
+ 为兼容模式)。三态:
49
+ - None:允许,但在 flush 会持久化明文密码时发出
50
+ PlaintextCredentialWarning(兼容 v2.4 及更早)
51
+ - True:显式允许明文落盘(不再告警)
52
+ - False:flush 遇到明文密码时抛出 CredentialError
53
+ v3.0 起默认值计划切换为 False。
54
+
55
+ 并发语义:单进程/单写入者(见模块 docstring);多进程场景请使用
56
+ SqliteHostRepository。
39
57
  """
40
58
 
41
59
  def __init__(
@@ -43,10 +61,12 @@ class JsonHostRepository(HostRepository):
43
61
  filepath: str,
44
62
  encryption: Optional[CredentialEncryption] = None,
45
63
  auto_load: bool = True,
64
+ allow_plaintext_credentials: Optional[bool] = None,
46
65
  ) -> None:
47
66
  self._filepath = Path(filepath)
48
67
  self._encryption = encryption
49
68
  self._guard = PasswordGuard(encryption)
69
+ self._allow_plaintext = allow_plaintext_credentials
50
70
  self._hosts: dict[str, Host] = {}
51
71
 
52
72
  if auto_load and self._filepath.exists():
@@ -100,7 +120,14 @@ class JsonHostRepository(HostRepository):
100
120
  # ========================================================================
101
121
 
102
122
  def flush(self) -> None:
103
- """原子写入 JSON 文件"""
123
+ """原子写入 JSON 文件
124
+
125
+ 注意:JSON 持久化是单进程/单写入者语义,多进程并发写同一文件
126
+ 可能丢失更新(无跨进程锁);多进程场景请使用 SqliteHostRepository。
127
+
128
+ Raises:
129
+ CredentialError: allow_plaintext_credentials=False 且存在明文密码
130
+ """
104
131
  data = self._serialize_hosts()
105
132
  self._atomic_write(data)
106
133
 
@@ -112,12 +139,44 @@ class JsonHostRepository(HostRepository):
112
139
  if self._guard.enabled:
113
140
  for host_data in hosts_dict.values():
114
141
  host_data["password"] = self._guard.encrypt(host_data.get("password"))
142
+ else:
143
+ self._enforce_plaintext_policy(hosts_dict)
115
144
 
116
145
  return {
117
146
  "version": CONFIG_VERSION,
118
147
  "hosts": hosts_dict,
119
148
  }
120
149
 
150
+ def _enforce_plaintext_policy(self, hosts_dict: dict) -> None:
151
+ """执行明文密码持久化策略(未配置加密器时)。
152
+
153
+ 集合所有违规主机,一次 flush 最多发出一次警告或抛出一次异常。
154
+ """
155
+ if self._allow_plaintext is True:
156
+ return
157
+ offenders = [
158
+ name
159
+ for name, data in hosts_dict.items()
160
+ if is_plaintext_password(data.get("password"))
161
+ ]
162
+ if not offenders:
163
+ return
164
+ if self._allow_plaintext is False:
165
+ raise CredentialError(
166
+ "refusing to persist plaintext credentials for hosts: "
167
+ f"{', '.join(sorted(offenders))}. "
168
+ "Pass encryption=... to encrypt at rest, or set "
169
+ "allow_plaintext_credentials=True to explicitly opt in."
170
+ )
171
+ warnings.warn(
172
+ f"Plaintext credentials will be persisted for {len(offenders)} host(s): "
173
+ f"{', '.join(sorted(offenders))}. Pass encryption=... to encrypt at rest, or "
174
+ "allow_plaintext_credentials=True to suppress this warning "
175
+ "(v3.0 will reject plaintext persistence by default).",
176
+ PlaintextCredentialWarning,
177
+ stacklevel=4,
178
+ )
179
+
121
180
  def _load(self) -> None:
122
181
  """从 JSON 文件加载主机配置"""
123
182
  try:
@@ -22,12 +22,14 @@ import logging
22
22
  import sqlite3
23
23
  import threading
24
24
  import time
25
+ import warnings
25
26
  from typing import Optional
26
27
 
27
28
  from remote_cmd.core.host import Host
28
29
  from remote_cmd.repository.host_repository import HostRepository
29
- from remote_cmd.utils.credential_guard import PasswordGuard
30
+ from remote_cmd.utils.credential_guard import PasswordGuard, is_plaintext_password
30
31
  from remote_cmd.utils.crypto import CredentialEncryption
32
+ from remote_cmd.utils.exceptions import CredentialError, PlaintextCredentialWarning
31
33
 
32
34
  logger = logging.getLogger(__name__)
33
35
 
@@ -79,9 +81,19 @@ class SqliteHostRepository(HostRepository):
79
81
  auto_create: 是否自动创建表和数据库,默认 True
80
82
  encryption: 可选的凭据加密器(设置后 save() 自动加密 password)
81
83
  busy_timeout_ms: 写锁等待上限(毫秒),默认 5000
84
+ allow_plaintext_credentials: 明文密码持久化策略(v2.5;默认 None
85
+ 为兼容模式)。三态:
86
+ - None:允许,但 save() 将要持久化明文密码时发出
87
+ PlaintextCredentialWarning(兼容 v2.4 及更早)
88
+ - True:显式允许明文落盘(不再告警)
89
+ - False:save() 遇到明文密码时抛出 CredentialError
90
+ v3.0 起默认值计划切换为 False。
82
91
 
83
92
  注意: 密码的加密依赖传入 encryption。若直接以明文密码调用 save()
84
93
  且未提供 encryption,明文会被持久化到数据库。请勿绕过 HostService。
94
+
95
+ 并发:WAL + busy_timeout 处理多进程并发写,适合作为多写入者后端
96
+ (与 JsonHostRepository 的 single-writer 语义不同)。
85
97
  """
86
98
 
87
99
  def __init__(
@@ -91,12 +103,14 @@ class SqliteHostRepository(HostRepository):
91
103
  auto_create: bool = True,
92
104
  encryption: Optional[CredentialEncryption] = None,
93
105
  busy_timeout_ms: int = DEFAULT_BUSY_TIMEOUT_MS,
106
+ allow_plaintext_credentials: Optional[bool] = None,
94
107
  ) -> None:
95
108
  self._db_path = db_path
96
109
  self._lock = threading.Lock()
97
110
  self._encryption = encryption
98
111
  self._guard = PasswordGuard(encryption)
99
112
  self._busy_timeout_ms = busy_timeout_ms
113
+ self._allow_plaintext = allow_plaintext_credentials
100
114
 
101
115
  if auto_create:
102
116
  self._init_db()
@@ -246,7 +260,13 @@ class SqliteHostRepository(HostRepository):
246
260
  # ========================================================================
247
261
 
248
262
  def save(self, host: Host) -> None:
249
- """保存或更新主机"""
263
+ """保存或更新主机
264
+
265
+ Raises:
266
+ CredentialError: allow_plaintext_credentials=False 且密码为明文
267
+ """
268
+ if not self._guard.enabled:
269
+ self._enforce_plaintext_policy(host.name, host.password)
250
270
  with self._lock, self._txn(write=True) as conn:
251
271
  tags_json = json.dumps(host.tags or [], ensure_ascii=False)
252
272
  # 配置了加密器时,明文密码先加密再落库
@@ -289,6 +309,27 @@ class SqliteHostRepository(HostRepository):
289
309
 
290
310
  return self._row_to_host(row)
291
311
 
312
+ def _enforce_plaintext_policy(self, name: str, password: Optional[str]) -> None:
313
+ """执行明文密码持久化策略(未配置加密器时)。"""
314
+ if self._allow_plaintext is True:
315
+ return
316
+ if not is_plaintext_password(password):
317
+ return
318
+ if self._allow_plaintext is False:
319
+ raise CredentialError(
320
+ f"refusing to persist plaintext credential for host '{name}'. "
321
+ "Pass encryption=... to encrypt at rest, or set "
322
+ "allow_plaintext_credentials=True to explicitly opt in."
323
+ )
324
+ warnings.warn(
325
+ f"Plaintext credential will be persisted for host '{name}'. "
326
+ "Pass encryption=... to encrypt at rest, or "
327
+ "allow_plaintext_credentials=True to suppress this warning "
328
+ "(v3.0 will reject plaintext persistence by default).",
329
+ PlaintextCredentialWarning,
330
+ stacklevel=4,
331
+ )
332
+
292
333
  def delete(self, name: str) -> None:
293
334
  """按名称删除主机"""
294
335
  with self._lock, self._txn(write=True) as conn: