remote-cmd-manager 2.4.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 (82) hide show
  1. {remote_cmd_manager-2.4.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.5.0}/PKG-INFO +25 -19
  2. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/README.md +20 -14
  3. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/pyproject.toml +15 -8
  4. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/__init__.py +12 -2
  5. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/_version.py +1 -1
  6. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/json_host_repository.py +61 -2
  7. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/sqlite_host_repository.py +43 -2
  8. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_host_runner.py +31 -1
  9. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_types.py +33 -1
  10. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/async_batch_executor.py +83 -48
  11. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/batch_executor.py +11 -4
  12. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/storage_factory.py +37 -11
  13. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/credential_guard.py +18 -0
  14. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/exceptions.py +17 -0
  15. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0/remote_cmd_manager.egg-info}/PKG-INFO +25 -19
  16. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/SOURCES.txt +1 -0
  17. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/requires.txt +3 -2
  18. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_async_batch_executor.py +47 -0
  19. remote_cmd_manager-2.5.0/tests/test_output_policy.py +172 -0
  20. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_release_metadata.py +13 -0
  21. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_repository.py +69 -0
  22. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_sqlite_repository.py +49 -2
  23. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_storage_factory.py +40 -2
  24. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/LICENSE +0 -0
  25. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/MANIFEST.in +0 -0
  26. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/config.example.yaml +0 -0
  27. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/basic_usage.py +0 -0
  28. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/deploy_script.py +0 -0
  29. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/nginx_batch_update.py +0 -0
  30. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/system_health_check.py +0 -0
  31. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/__init__.py +0 -0
  32. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/main.py +0 -0
  33. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/__init__.py +0 -0
  34. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_connection_pool.py +0 -0
  35. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_ssh_client.py +0 -0
  36. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host.py +0 -0
  37. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host_manager.py +0 -0
  38. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/ssh_client.py +0 -0
  39. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/sync_connection_pool.py +0 -0
  40. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/__init__.py +0 -0
  41. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/host_repository.py +0 -0
  42. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/__init__.py +0 -0
  43. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_pool_policy.py +0 -0
  44. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/credential_provider.py +0 -0
  45. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/host_service.py +0 -0
  46. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/retry_policy.py +0 -0
  47. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/ssh_service.py +0 -0
  48. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/task_runner.py +0 -0
  49. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/__init__.py +0 -0
  50. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/config.py +0 -0
  51. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/crypto.py +0 -0
  52. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/logging_utils.py +0 -0
  53. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
  54. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
  55. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
  56. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/requirements.txt +0 -0
  57. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/setup.cfg +0 -0
  58. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/__init__.py +0 -0
  59. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/conftest.py +0 -0
  60. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/integration/conftest.py +0 -0
  61. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/integration/test_ssh_connection.py +0 -0
  62. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/performance/__init__.py +0 -0
  63. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/performance/conftest.py +0 -0
  64. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/performance/test_benchmarks.py +0 -0
  65. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_async_ssh_client.py +0 -0
  66. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_batch_executor.py +0 -0
  67. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_cli.py +0 -0
  68. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_config.py +0 -0
  69. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_credential_provider.py +0 -0
  70. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_crypto.py +0 -0
  71. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_exceptions.py +0 -0
  72. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_host.py +0 -0
  73. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_host_manager.py +0 -0
  74. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_host_service.py +0 -0
  75. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_keyring_provider.py +0 -0
  76. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_logging_utils.py +0 -0
  77. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_pool_policy.py +0 -0
  78. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_retry_policy.py +0 -0
  79. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_client.py +0 -0
  80. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_service.py +0 -0
  81. {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_sync_connection_pool.py +0 -0
  82. {remote_cmd_manager-2.4.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.4.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,30 +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.4.0 Release Highlights
92
+ ## v2.5.0 Release Highlights
93
93
 
94
- v2.4.0 adds an opt-in retained-output cap for batch execution and cleans up logging
95
- handler resources, with default behavior and public result schemas unchanged. 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
- ### Bounded Batch Output
99
+ ### Security & Platform
99
100
 
100
- - `BatchExecutor` / `AsyncBatchExecutor` accept an optional `max_output_bytes` (default `None`): `None` keeps full `stdout`/`stderr`, while a positive value caps each host's retained streams and appends a `[output truncated: N bytes omitted]` marker.
101
- - Truncation is deterministic and UTF-8 byte-boundary safe, applies to `stdout` and `stderr` independently, and never changes command success/failure or exit codes.
102
- - The cap bounds the retained `BatchResult`; the SSH client may still hold full output transiently during execution, so it is not a process-wide RSS limit.
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).
103
103
 
104
- ### Reliability
104
+ ### Scalability
105
105
 
106
- - `setup_logging` now closes previous root-logger handlers before removing them, avoiding unclosed-file resource warnings when logging is reconfigured.
107
- - Connection-pool lifetime wording in the documentation now matches the actual behavior: internal pools are created lazily per host and closed as soon as that host finishes (including its retries).
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.
108
114
 
109
115
  ### Compatibility
110
116
 
111
- - Default behavior is unchanged (`max_output_bytes=None` retains full output) and the public `BatchResult` / `BatchHostResult` schemas are unchanged.
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`.
112
118
 
113
119
  ## Table of Contents
114
120
 
115
- - [v2.4.0 Release Highlights](#v240-release-highlights)
121
+ - [v2.5.0 Release Highlights](#v250-release-highlights)
116
122
  - [Why Remote CMD?](#why-remote-cmd)
117
123
  - [Quick Start](#quick-start)
118
124
  - [Use Cases](#use-cases)
@@ -350,7 +356,7 @@ Good first issues are labelled `good first issue` in the
350
356
  developed independently as a focused alternative to heavyweight tools for the
351
357
  ad-hoc SSH tasks that come up in day-to-day server work.
352
358
 
353
- - **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
354
360
  public API is versioned under [semantic versioning](https://semver.org/).
355
361
  - **Your code, your servers:** usage stays open under the MIT license — nothing
356
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,30 +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.4.0 Release Highlights
39
+ ## v2.5.0 Release Highlights
40
40
 
41
- v2.4.0 adds an opt-in retained-output cap for batch execution and cleans up logging
42
- handler resources, with default behavior and public result schemas unchanged. 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
- ### Bounded Batch Output
46
+ ### Security & Platform
46
47
 
47
- - `BatchExecutor` / `AsyncBatchExecutor` accept an optional `max_output_bytes` (default `None`): `None` keeps full `stdout`/`stderr`, while a positive value caps each host's retained streams and appends a `[output truncated: N bytes omitted]` marker.
48
- - Truncation is deterministic and UTF-8 byte-boundary safe, applies to `stdout` and `stderr` independently, and never changes command success/failure or exit codes.
49
- - The cap bounds the retained `BatchResult`; the SSH client may still hold full output transiently during execution, so it is not a process-wide RSS limit.
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).
50
50
 
51
- ### Reliability
51
+ ### Scalability
52
52
 
53
- - `setup_logging` now closes previous root-logger handlers before removing them, avoiding unclosed-file resource warnings when logging is reconfigured.
54
- - Connection-pool lifetime wording in the documentation now matches the actual behavior: internal pools are created lazily per host and closed as soon as that host finishes (including its retries).
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.
55
61
 
56
62
  ### Compatibility
57
63
 
58
- - Default behavior is unchanged (`max_output_bytes=None` retains full output) and the public `BatchResult` / `BatchHostResult` schemas are unchanged.
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`.
59
65
 
60
66
  ## Table of Contents
61
67
 
62
- - [v2.4.0 Release Highlights](#v240-release-highlights)
68
+ - [v2.5.0 Release Highlights](#v250-release-highlights)
63
69
  - [Why Remote CMD?](#why-remote-cmd)
64
70
  - [Quick Start](#quick-start)
65
71
  - [Use Cases](#use-cases)
@@ -297,7 +303,7 @@ Good first issues are labelled `good first issue` in the
297
303
  developed independently as a focused alternative to heavyweight tools for the
298
304
  ad-hoc SSH tasks that come up in day-to-day server work.
299
305
 
300
- - **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
301
307
  public API is versioned under [semantic versioning](https://semver.org/).
302
308
  - **Your code, your servers:** usage stays open under the MIT license — nothing
303
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.4.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.4.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:
@@ -23,7 +23,7 @@ from typing import Optional, Union
23
23
 
24
24
  from remote_cmd.core.host import Host
25
25
  from remote_cmd.core.ssh_client import CommandResult, ConnectionConfig
26
- from remote_cmd.service._types import BatchHostResult
26
+ from remote_cmd.service._types import BatchHostResult, OutputPolicy
27
27
  from remote_cmd.service.host_service import HostService
28
28
  from remote_cmd.utils.exceptions import ValidationError
29
29
 
@@ -56,6 +56,36 @@ def validate_max_output_bytes(value: Optional[int]) -> Optional[int]:
56
56
  return value
57
57
 
58
58
 
59
+ def resolve_max_output_bytes(
60
+ max_output_bytes: Optional[int],
61
+ output_policy: Optional[OutputPolicy],
62
+ ) -> Optional[int]:
63
+ """解析执行器的输出保留配置,返回生效的 max_output_bytes。
64
+
65
+ v2.5 引入 ``OutputPolicy`` 作为 ``max_output_bytes`` 的显式替代;
66
+ 两者只能传且最多传一个,防止歧义。
67
+
68
+ Args:
69
+ max_output_bytes: legacy 参数(与 ``output_policy`` 互斥)
70
+ output_policy: ``OutputPolicy`` 实例(优先)
71
+
72
+ Returns:
73
+ Optional[int]: 生效的保留上限(``None`` 表示不限)
74
+
75
+ Raises:
76
+ ValidationError: 两者同时给出,或值非法
77
+ """
78
+ if max_output_bytes is not None and output_policy is not None:
79
+ raise ValidationError(
80
+ "ambiguous output policy: pass either max_output_bytes or "
81
+ "output_policy, not both"
82
+ )
83
+ if output_policy is not None:
84
+ return validate_max_output_bytes(output_policy.max_output_bytes)
85
+ return validate_max_output_bytes(max_output_bytes)
86
+
87
+
88
+
59
89
  def truncate_output(text: str, max_bytes: Optional[int]) -> str:
60
90
  """按 UTF-8 字节上限确定性地截断单个输出流。
61
91
 
@@ -13,10 +13,42 @@ from collections.abc import Awaitable
13
13
  from dataclasses import dataclass, field
14
14
  from typing import Callable, Optional
15
15
 
16
+ from remote_cmd.utils.exceptions import ValidationError
17
+
16
18
  # 进度回调签名:completed, total, current_host_name;async 或 sync 均可
17
19
  ProgressCallback = Callable[[int, int, str], Optional[Awaitable[None]]]
18
20
 
19
21
 
22
+ @dataclass(frozen=True)
23
+ class OutputPolicy:
24
+ """批量执行的输出保留策略(v2.5 引入,为 v3 默认值迁移预留)。
25
+
26
+ Args:
27
+ max_output_bytes: 每台主机每个输出流(stdout/stderr)保留的最大
28
+ 字节数(UTF-8)。``None`` 保留完整输出(兼容默认,与 v2.4 一致);
29
+ 正整数时确定性截断并追加 ``[output truncated: N bytes omitted]``
30
+ 标记(语义见 _host_runner.truncate_output)。
31
+
32
+ Raises:
33
+ ValidationError: max_output_bytes 非 None、非正整数
34
+
35
+ Warning:
36
+ ``None`` 表示无保留上限:大批量 + 大输出组合下 BatchResult 的内存
37
+ 占用可能显著。v3.0 计划改为有界默认(候选 1 MiB)。
38
+ """
39
+
40
+ max_output_bytes: Optional[int] = None
41
+
42
+ def __post_init__(self) -> None:
43
+ value = self.max_output_bytes
44
+ if value is None:
45
+ return
46
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
47
+ raise ValidationError(
48
+ f"max_output_bytes must be None or a positive integer, got: {value!r}"
49
+ )
50
+
51
+
20
52
  @dataclass
21
53
  class BatchHostResult:
22
54
  """
@@ -90,4 +122,4 @@ class BatchResult:
90
122
  )
91
123
 
92
124
 
93
- __all__ = ["BatchHostResult", "BatchResult", "ProgressCallback"]
125
+ __all__ = ["BatchHostResult", "BatchResult", "OutputPolicy", "ProgressCallback"]