hermes-ssh 0.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 (37) hide show
  1. hermes_ssh-0.3.1/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  2. hermes_ssh-0.3.1/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
  3. hermes_ssh-0.3.1/.github/PULL_REQUEST_TEMPLATE.md +23 -0
  4. hermes_ssh-0.3.1/.github/workflows/ci.yml +43 -0
  5. hermes_ssh-0.3.1/.gitignore +40 -0
  6. hermes_ssh-0.3.1/CHANGELOG.md +71 -0
  7. hermes_ssh-0.3.1/CONTRIBUTING.md +78 -0
  8. hermes_ssh-0.3.1/LICENSE +21 -0
  9. hermes_ssh-0.3.1/PKG-INFO +273 -0
  10. hermes_ssh-0.3.1/README.md +244 -0
  11. hermes_ssh-0.3.1/SECURITY.md +53 -0
  12. hermes_ssh-0.3.1/deploy.sh +44 -0
  13. hermes_ssh-0.3.1/llms.txt +207 -0
  14. hermes_ssh-0.3.1/pyproject.toml +80 -0
  15. hermes_ssh-0.3.1/src/ssh_tools/__init__.py +92 -0
  16. hermes_ssh-0.3.1/src/ssh_tools/approval.py +40 -0
  17. hermes_ssh-0.3.1/src/ssh_tools/config.py +68 -0
  18. hermes_ssh-0.3.1/src/ssh_tools/handlers/__init__.py +7 -0
  19. hermes_ssh-0.3.1/src/ssh_tools/handlers/machines.py +97 -0
  20. hermes_ssh-0.3.1/src/ssh_tools/handlers/sessions.py +81 -0
  21. hermes_ssh-0.3.1/src/ssh_tools/handlers/slash.py +129 -0
  22. hermes_ssh-0.3.1/src/ssh_tools/handlers/terminal.py +75 -0
  23. hermes_ssh-0.3.1/src/ssh_tools/manager.py +838 -0
  24. hermes_ssh-0.3.1/src/ssh_tools/migrate.py +114 -0
  25. hermes_ssh-0.3.1/src/ssh_tools/models.py +107 -0
  26. hermes_ssh-0.3.1/src/ssh_tools/plugin.yaml +12 -0
  27. hermes_ssh-0.3.1/src/ssh_tools/py.typed +0 -0
  28. hermes_ssh-0.3.1/src/ssh_tools/schemas.py +131 -0
  29. hermes_ssh-0.3.1/src/ssh_tools/storage.py +179 -0
  30. hermes_ssh-0.3.1/src/ssh_tools/utils.py +36 -0
  31. hermes_ssh-0.3.1/tests/__init__.py +0 -0
  32. hermes_ssh-0.3.1/tests/conftest.py +27 -0
  33. hermes_ssh-0.3.1/tests/test_config.py +62 -0
  34. hermes_ssh-0.3.1/tests/test_manager.py +1225 -0
  35. hermes_ssh-0.3.1/tests/test_tools.py +628 -0
  36. hermes_ssh-0.3.1/tests/test_utils.py +63 -0
  37. hermes_ssh-0.3.1/uv.lock +405 -0
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: Bug Report
3
+ about: Report a bug in hermes-ssh
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ **Describe the bug**
10
+ A clear description of what the bug is.
11
+
12
+ **To reproduce**
13
+ Steps to reproduce the behavior:
14
+ 1. ...
15
+ 2. ...
16
+
17
+ **Expected behavior**
18
+ What you expected to happen.
19
+
20
+ **Environment**
21
+ - OS: [e.g. Ubuntu 24.04, macOS 15]
22
+ - Python version: [e.g. 3.12.3]
23
+ - Hermes version: [e.g. 0.5.0]
24
+ - hermes-ssh version: [e.g. 0.1.0]
25
+
26
+ **Additional context**
27
+ Any other context (logs, config, etc.).
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: Feature Request
3
+ about: Suggest a feature for hermes-ssh
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ **Description**
10
+ What you'd like to happen.
11
+
12
+ **Use case**
13
+ Why this would be useful.
14
+
15
+ **Alternatives considered**
16
+ Any workarounds or alternative approaches you've thought of.
@@ -0,0 +1,23 @@
1
+ ## Description
2
+
3
+ What does this PR do?
4
+
5
+ ## Type of change
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] Breaking change
10
+ - [ ] Documentation update
11
+
12
+ ## Testing
13
+
14
+ - [ ] Tests pass (`pytest`)
15
+ - [ ] Formatting passes (`black --check`)
16
+ - [ ] Type check passes (`mypy`)
17
+ - [ ] New tests added (if bug fix)
18
+
19
+ ## Checklist
20
+
21
+ - [ ] Code follows project style
22
+ - [ ] Self-review completed
23
+ - [ ] Documentation updated (if needed)
@@ -0,0 +1,43 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ lint:
18
+ name: Format & Type Check
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.13"
25
+ - run: pip install black==26.5.1 mypy==1.16.0
26
+ - run: black --check src/ssh_tools/ tests/
27
+ - run: mypy src/ssh_tools/
28
+
29
+ test:
30
+ name: Tests (Python ${{ matrix.python-version }})
31
+ runs-on: ubuntu-latest
32
+ strategy:
33
+ matrix:
34
+ python-version: ["3.11", "3.12", "3.13"]
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: ${{ matrix.python-version }}
40
+ - run: pip install -e '.[dev]'
41
+ - run: black --check src/ssh_tools/ tests/
42
+ - run: mypy src/ssh_tools/
43
+ - run: pytest
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+
14
+ # Testing
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+
23
+ # OS
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # Runtime data (secrets, credentials, logs — NEVER commit)
28
+ **/data/
29
+ **/sockets/
30
+ *.tmp
31
+ *.lock
32
+ !**/uv.lock
33
+
34
+ # Environment
35
+ .env
36
+ .env.*
37
+
38
+ # Hermes runtime (if symlinked from install)
39
+ *.log
40
+ node_modules
@@ -0,0 +1,71 @@
1
+ # Changelog
2
+
3
+ ## [0.3.1] - 2026-07-13
4
+
5
+ - Fix the release artifact path so the tagged build can reach PyPI.
6
+
7
+ ## [0.3.0] - 2026-07-13
8
+
9
+ - Fail closed when Hermes command approvals are unavailable.
10
+ - Never signal persisted PIDs after restart; only tracked process groups can be killed.
11
+ - Preserve shared SSH ControlMaster sockets when killing individual commands.
12
+ - Make `cryptography` a required dependency and add packaged-plugin discovery metadata.
13
+ - Declare provided tools in the Hermes plugin manifest.
14
+
15
+ ## 0.2.0 — Bug hunt, security hardening, documentation
16
+
17
+ ### New features
18
+
19
+ - **Background commands** — run long commands with `background=true`, poll status, read output when done
20
+ - **Output truncation** — outputs exceeding `max_output_chars` (50K) saved under the restricted plugin output directory; LLM can `read_file` the full output
21
+ - **Command audit log** — every command logged with timestamps, machine, exit code, and session ID (`~/.hermes/ssh-tools/command_log.jsonl`)
22
+ - **Poll/read_output on ssh_terminal** — check background command status directly from the terminal tool
23
+ - **Machine name validation** — names must match `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`; prevents path traversal and glob injection
24
+
25
+ ### Bug fixes
26
+
27
+ - `ssh_terminal` poll/read_output no longer requires machine/command parameters
28
+ - Background process dict uses atomic `pop()` to prevent output loss on concurrent polls
29
+ - Output files written with 0o600 permissions in the restricted plugin output directory
30
+ - Batch session cleanup now removes orphaned saved output files
31
+ - `_write_json` calls `fsync` before `os.replace` to prevent data loss on crash
32
+ - Orphaned SSH control socket files removed after session kill
33
+ - `slash.py` no longer uses `assert` in production code (stripped with `python -O`)
34
+ - Stale help text fixed: `max_output_lines` → `max_output_chars`
35
+ - `list_command_log` reads file tail instead of entire file (unbounded memory)
36
+ - `_log_command` uses single `os.open` instead of double open TOCTOU
37
+ - `prune_closed` handles sessions with naive (non-timezone) timestamps
38
+ - `_load_machines`/`_load_sessions` validate JSON structure (dict check)
39
+ - Startup cleans orphaned `.tmp` files from data directory
40
+ - Background sessions registered in JSON before process reference stored
41
+ - `timeout` parameter coerced to int (string input no longer crashes)
42
+ - Tool schemas updated: poll/read_output descriptions mention session_id
43
+ - `require()` docstring corrected (non-empty → non-None)
44
+
45
+ ### Security
46
+
47
+ - Data directory created with 0o700 permissions
48
+ - Audit log created with 0o600 permissions
49
+ - Output files written with 0o600 permissions
50
+ - Machine names validated against `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`
51
+ - `_cleanup_output_files` uses `iterdir()` + prefix matching instead of glob (prevents glob injection)
52
+
53
+ ### Documentation
54
+
55
+ - `llms.txt` added — installation and usage guide for LLMs
56
+ - README rewritten with full feature documentation
57
+ - CHANGELOG updated
58
+
59
+ ## 0.1.0 — Initial release
60
+
61
+ - `ssh_terminal` — run commands on remote machines via SSH
62
+ - `ssh_machines` — machine registry with aliases, tags, and connectivity tests
63
+ - `ssh_sessions` — session tracking with idle detection and cleanup
64
+ - `ControlMaster` — persistent SSH connections with 5-minute reuse window
65
+ - `bash -c` wrapping with `pipefail` for reliable pipeline exit codes
66
+ - `/ssh` slash command for quick machine inspection and command execution
67
+ - Background idle checker with configurable timeout
68
+ - Atomic JSON writes with temp files for crash safety
69
+ - Thread-safe operations via locks
70
+ - 77 tests covering config, manager, tool handlers, and edge cases
71
+ - CI with black, mypy, and pytest across Python 3.11–3.13
@@ -0,0 +1,78 @@
1
+ # Contributing
2
+
3
+ PRs welcome. Here's the workflow.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/TheEpTic/hermes-plugins.git
9
+ cd hermes-plugins/hermes-ssh
10
+ python -m venv .venv && source .venv/bin/activate
11
+ pip install -e '.[dev]'
12
+ ```
13
+
14
+ ## Development
15
+
16
+ ```bash
17
+ black src/ssh_tools/ tests/
18
+ mypy src/ssh_tools/
19
+ pytest
20
+ ```
21
+
22
+ ## Testing
23
+
24
+ Run the full test suite with:
25
+
26
+ ```bash
27
+ pytest
28
+ ```
29
+
30
+ For coverage:
31
+
32
+ ```bash
33
+ pytest --cov=ssh_tools --cov-report=term-missing
34
+ ```
35
+
36
+ Aim for **90%+ coverage** on new code. The test suite uses pytest fixtures defined in `tests/conftest.py` for mocking SSH connections and the manager. When adding a new tool or handler, write tests that cover:
37
+
38
+ - Happy path (valid input, successful execution)
39
+ - Error cases (missing params, connection failures, timeouts)
40
+ - Edge cases (empty output, very large output, special characters)
41
+
42
+ ## Guidelines
43
+
44
+ - **Tests required for bug fixes.** Each fix gets a test that reproduces the issue.
45
+ - **Separate PRs for separate concerns.** Don't bundle unrelated changes.
46
+ - Run `black` and `mypy` before pushing. CI will catch it if you don't.
47
+
48
+ ## Project Structure
49
+
50
+ ```
51
+ src/ssh_tools/
52
+ ├── __init__.py # Plugin registration + Hermes hooks
53
+ ├── config.py # SSHConfig dataclass (all settings)
54
+ ├── manager.py # SSHManager — machines, sessions, execution
55
+ ├── models.py # Machine, Session dataclasses
56
+ ├── schemas.py # Tool schemas (what the LLM sees)
57
+ ├── utils.py # Shared helpers (ok/err response builders)
58
+ ├── py.typed # PEP 561 marker
59
+ └── handlers/
60
+ ├── __init__.py
61
+ ├── terminal.py # ssh_terminal tool handler
62
+ ├── machines.py # ssh_machines tool handler
63
+ ├── sessions.py # ssh_sessions tool handler
64
+ └── slash.py # /ssh slash command
65
+ tests/
66
+ ├── conftest.py # Shared fixtures
67
+ ├── test_config.py
68
+ ├── test_manager.py
69
+ └── test_tools.py
70
+ ```
71
+
72
+ ## Architecture
73
+
74
+ - **`SSHManager`** owns all state. Thread-safe. No module-level mutable state.
75
+ - **Handlers** are thin wrappers — validate params, dispatch to manager, return JSON.
76
+ - **`utils.py`** provides `ok()`, `err()`, `require()` to eliminate boilerplate.
77
+ - **`/ssh` slash command** uses a factory pattern (`create_slash_handler`) for testability.
78
+ - **Idle checker** runs as a background daemon thread, cleaning up stale sessions.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheEpTic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-ssh
3
+ Version: 0.3.1
4
+ Summary: SSH remote execution plugin for Hermes Agent
5
+ Project-URL: Homepage, https://github.com/TheEpTic/hermes-plugins
6
+ Project-URL: Repository, https://github.com/TheEpTic/hermes-plugins
7
+ Project-URL: Issues, https://github.com/TheEpTic/hermes-plugins/issues
8
+ Project-URL: Changelog, https://github.com/TheEpTic/hermes-plugins/blob/main/hermes-ssh/CHANGELOG.md
9
+ Author-email: TheEpTic <nexus@eptic.me>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,devops,hermes,remote,ssh
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: cryptography<49,>=48.0.1
24
+ Provides-Extra: dev
25
+ Requires-Dist: black==26.5.1; extra == 'dev'
26
+ Requires-Dist: mypy==1.16.0; extra == 'dev'
27
+ Requires-Dist: pytest==9.0.3; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # hermes-ssh
31
+
32
+ [![CI](https://github.com/TheEpTic/hermes-plugins/actions/workflows/ci.yml/badge.svg)](https://github.com/TheEpTic/hermes-plugins/actions/workflows/ci.yml)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
34
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
35
+
36
+ SSH remote execution plugin for [Hermes Agent](https://github.com/NousResearch/hermes-agent).
37
+
38
+ Run commands on remote servers, track sessions, reuse connections — all from inside Hermes.
39
+
40
+ ```
41
+ /ssh web1 uptime
42
+ ssh_machines add name=web1 host=192.168.1.50
43
+ ssh_sessions list
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ > **Requires Python 3.11+** and an OpenSSH client (`ssh`) on the host system.
49
+
50
+ ### Option 1: Deploy script (recommended)
51
+
52
+ ```bash
53
+ git clone https://github.com/TheEpTic/hermes-plugins.git
54
+ cd hermes-plugins/hermes-ssh
55
+ ./deploy.sh
56
+ ```
57
+
58
+ Then restart Hermes with `/reset`.
59
+
60
+ ### Option 2: Manual symlink
61
+
62
+ ```bash
63
+ git clone https://github.com/TheEpTic/hermes-plugins.git
64
+ ln -s "$(pwd)/hermes-plugins/hermes-ssh/src/ssh_tools" ~/.hermes/plugins/hermes-ssh
65
+ ```
66
+
67
+ Then `/reset` in Hermes. The symlink points at the source tree, but Python modules are imported once, so code changes still require `/reset` or a Hermes process restart before they load.
68
+
69
+ ### Option 3: As a Python package
70
+
71
+ ```bash
72
+ pip install git+https://github.com/TheEpTic/hermes-plugins.git#subdirectory=hermes-ssh
73
+ ```
74
+
75
+ Then enable it and restart Hermes:
76
+
77
+ ```bash
78
+ hermes plugins enable hermes-ssh
79
+ ```
80
+
81
+ ## Features
82
+
83
+ ### `ssh_terminal` — Run Commands
84
+
85
+ Execute any command on a remote machine. Commands run through `bash -c` with `pipefail`, so pipelines work correctly.
86
+
87
+ ```bash
88
+ # Synchronous (waits for completion)
89
+ ssh_terminal machine=web1 command="df -h"
90
+
91
+ # Background (returns immediately)
92
+ ssh_terminal machine=web1 command="tail -f /var/log/syslog" background=true
93
+
94
+ # With timeout
95
+ ssh_terminal machine=web1 command="make -j4" timeout=300
96
+ ```
97
+
98
+ **Output truncation:** When output exceeds `max_output_chars` (default: 50,000), the full output is saved under the plugin's restricted output directory and a summary with the file path is returned. The LLM can then use `read_file` to access the complete output.
99
+
100
+ **Background commands:** Long-running commands can be backgrounded. The plugin tracks the process and lets you poll for status or retrieve output later.
101
+
102
+ ```bash
103
+ # Check if still running
104
+ ssh_terminal poll=<session_id>
105
+
106
+ # Read output from completed command
107
+ ssh_terminal read_output=<session_id>
108
+
109
+ # Or via ssh_sessions
110
+ ssh_sessions action=poll session_id=<session_id>
111
+ ssh_sessions action=read_output session_id=<session_id>
112
+ ```
113
+
114
+ ### `ssh_machines` — Machine Registry
115
+
116
+ Register servers once, refer to them by name or alias.
117
+
118
+ ```bash
119
+ # Add a server
120
+ ssh_machines action=add name=web1 host=192.168.1.50 user=deploy key=~/.ssh/id_ed25519
121
+
122
+ # Add with aliases and tags
123
+ ssh_machines action=add name=prod-web host=10.0.0.1 aliases=web1,tags=production,web
124
+
125
+ # List all machines
126
+ ssh_machines action=list
127
+
128
+ # Test connectivity
129
+ ssh_machines action=test name=web1
130
+
131
+ # Get full details
132
+ ssh_machines action=inspect name=web1
133
+ ```
134
+
135
+ Machine names must be alphanumeric with dots, hyphens, or underscores (1-64 chars). Slashes, spaces, and glob characters are rejected.
136
+
137
+ ### `ssh_sessions` — Session Tracking
138
+
139
+ Every command creates a session. Sessions track the PID, machine, command count, and idle time.
140
+
141
+ ```bash
142
+ # List active sessions
143
+ ssh_sessions action=list
144
+
145
+ # Kill a session (terminates the SSH process)
146
+ ssh_sessions action=kill session_id=<session_id>
147
+
148
+ # Clean up all idle sessions (>30 min)
149
+ ssh_sessions action=cleanup
150
+
151
+ # Remove old closed sessions (>24 hours)
152
+ ssh_sessions action=prune
153
+ ```
154
+
155
+ Idle sessions are automatically killed by a background checker after 30 minutes. Closed sessions are pruned after 24 hours.
156
+
157
+ ### `/ssh` Slash Command
158
+
159
+ Quick access from chat without remembering tool names:
160
+
161
+ ```
162
+ /ssh # List machines and sessions
163
+ /ssh web1 # Inspect a machine
164
+ /ssh web1 uptime # Run a command
165
+ /ssh web1 docker ps # Run a command
166
+ /ssh test # Test connectivity to all machines
167
+ /ssh cleanup # Kill all idle sessions
168
+ /ssh help # Show help
169
+ ```
170
+
171
+ ## Configuration
172
+
173
+ All settings live in `src/ssh_tools/config.py` as an `SSHConfig` dataclass:
174
+
175
+ | Setting | Default | Description |
176
+ |---------|---------|-------------|
177
+ | `default_port` | 22 | SSH port for new machines |
178
+ | `default_user` | root | SSH user for new machines |
179
+ | `connect_timeout` | 5s | SSH handshake timeout |
180
+ | `command_timeout` | 30s | Command execution timeout |
181
+ | `max_output_chars` | 50,000 | Output truncation threshold |
182
+ | `idle_check_interval` | 60s | Seconds between idle checks |
183
+ | `idle_timeout_minutes` | 30m | Auto-kill after this idle time |
184
+ | `closed_prune_hours` | 24h | Remove closed sessions after this |
185
+ | `strict_host_key_checking` | accept-new | SSH host key verification |
186
+
187
+ ## Architecture
188
+
189
+ ```
190
+ src/ssh_tools/
191
+ ├── __init__.py # Plugin registration + Hermes hooks
192
+ ├── config.py # SSHConfig (immutable dataclass)
193
+ ├── manager.py # SSHManager — all state and operations
194
+ ├── models.py # Machine, Session dataclasses
195
+ ├── schemas.py # Tool schemas (LLM-facing)
196
+ ├── utils.py # ok(), err(), require() helpers
197
+ ├── py.typed # PEP 561 marker
198
+ └── handlers/
199
+ ├── terminal.py # ssh_terminal (execute, poll, read_output)
200
+ ├── machines.py # ssh_machines (add/list/remove/test/inspect)
201
+ ├── sessions.py # ssh_sessions (list/kill/cleanup/prune/poll/read_output)
202
+ └── slash.py # /ssh slash command
203
+ ```
204
+
205
+ **Key design decisions:**
206
+
207
+ - `SSHManager` owns all state. Thread-safe. No module-level mutable state.
208
+ - Handlers are thin closures — validate params, dispatch to manager, return JSON.
209
+ - JSON files use atomic writes (temp file + `os.replace`) for crash safety.
210
+ - Data directory has restricted permissions (0o700). Audit log and output files use 0o600.
211
+ - Machine names are validated to prevent path traversal and glob injection.
212
+ - Connection reuse via `ControlMaster` with 5-minute persist window.
213
+
214
+ ## Security
215
+
216
+ See [SECURITY.md](SECURITY.md) for the full picture.
217
+
218
+ **Defaults you should know about:**
219
+
220
+ - `StrictHostKeyChecking=accept-new` — accepts first-seen host keys but rejects changed keys. Set to `yes` for strict production hosts.
221
+ - Machine credentials are encrypted at rest in `~/.hermes/ssh-tools/machines.json`. Data directory is 0o700.
222
+ - All commands execute with the permissions of the Hermes agent process.
223
+
224
+ **Hardening applied:**
225
+
226
+ - Machine names validated against `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`
227
+ - Output files written with 0o600 permissions
228
+ - Audit log created with 0o600 permissions
229
+ - Atomic JSON writes prevent corruption on crash
230
+ - Orphaned temp files cleaned on startup
231
+
232
+ ## Requirements
233
+
234
+ - Python 3.11+
235
+ - OpenSSH client (`ssh`)
236
+ - [Hermes Agent](https://github.com/NousResearch/hermes-agent)
237
+
238
+ ## Troubleshooting
239
+
240
+ **Connection refused**
241
+ The remote host may not be listening on the expected port, or a firewall is blocking the connection. Verify with `ssh -v user@host` outside of Hermes.
242
+
243
+ **Permission denied (publickey)**
244
+ The SSH key path stored in the machine registry may be incorrect, or the remote host doesn't have the corresponding public key in `~/.ssh/authorized_keys`. Verify with `ssh -i /path/to/key user@host`.
245
+
246
+ **Command timeout**
247
+ Commands exceeding `command_timeout` (default 30s) are killed. Increase the timeout or use `background=true` for long-running work.
248
+
249
+ **Output looks truncated**
250
+ This is intentional — large outputs are saved under the restricted plugin output directory and a summary is returned. Use `read_output` or `read_file` on the returned path for the full output.
251
+
252
+ **Session stuck as "active" after process died**
253
+ If the agent restarted, background process references are lost. Use `ssh_sessions action=cleanup` to kill stale sessions, or `ssh_sessions action=prune` to remove old closed ones.
254
+
255
+ ## Development
256
+
257
+ ```bash
258
+ git clone https://github.com/TheEpTic/hermes-plugins.git
259
+ cd hermes-plugins/hermes-ssh
260
+ python -m venv .venv && source .venv/bin/activate
261
+ pip install -e '.[dev]'
262
+
263
+ # Run checks
264
+ black --check src/ssh_tools/ tests/
265
+ mypy src/ssh_tools/
266
+ pytest
267
+ ```
268
+
269
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
270
+
271
+ ## License
272
+
273
+ MIT — see [LICENSE](LICENSE).