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.
- hermes_ssh-0.3.1/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
- hermes_ssh-0.3.1/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
- hermes_ssh-0.3.1/.github/PULL_REQUEST_TEMPLATE.md +23 -0
- hermes_ssh-0.3.1/.github/workflows/ci.yml +43 -0
- hermes_ssh-0.3.1/.gitignore +40 -0
- hermes_ssh-0.3.1/CHANGELOG.md +71 -0
- hermes_ssh-0.3.1/CONTRIBUTING.md +78 -0
- hermes_ssh-0.3.1/LICENSE +21 -0
- hermes_ssh-0.3.1/PKG-INFO +273 -0
- hermes_ssh-0.3.1/README.md +244 -0
- hermes_ssh-0.3.1/SECURITY.md +53 -0
- hermes_ssh-0.3.1/deploy.sh +44 -0
- hermes_ssh-0.3.1/llms.txt +207 -0
- hermes_ssh-0.3.1/pyproject.toml +80 -0
- hermes_ssh-0.3.1/src/ssh_tools/__init__.py +92 -0
- hermes_ssh-0.3.1/src/ssh_tools/approval.py +40 -0
- hermes_ssh-0.3.1/src/ssh_tools/config.py +68 -0
- hermes_ssh-0.3.1/src/ssh_tools/handlers/__init__.py +7 -0
- hermes_ssh-0.3.1/src/ssh_tools/handlers/machines.py +97 -0
- hermes_ssh-0.3.1/src/ssh_tools/handlers/sessions.py +81 -0
- hermes_ssh-0.3.1/src/ssh_tools/handlers/slash.py +129 -0
- hermes_ssh-0.3.1/src/ssh_tools/handlers/terminal.py +75 -0
- hermes_ssh-0.3.1/src/ssh_tools/manager.py +838 -0
- hermes_ssh-0.3.1/src/ssh_tools/migrate.py +114 -0
- hermes_ssh-0.3.1/src/ssh_tools/models.py +107 -0
- hermes_ssh-0.3.1/src/ssh_tools/plugin.yaml +12 -0
- hermes_ssh-0.3.1/src/ssh_tools/py.typed +0 -0
- hermes_ssh-0.3.1/src/ssh_tools/schemas.py +131 -0
- hermes_ssh-0.3.1/src/ssh_tools/storage.py +179 -0
- hermes_ssh-0.3.1/src/ssh_tools/utils.py +36 -0
- hermes_ssh-0.3.1/tests/__init__.py +0 -0
- hermes_ssh-0.3.1/tests/conftest.py +27 -0
- hermes_ssh-0.3.1/tests/test_config.py +62 -0
- hermes_ssh-0.3.1/tests/test_manager.py +1225 -0
- hermes_ssh-0.3.1/tests/test_tools.py +628 -0
- hermes_ssh-0.3.1/tests/test_utils.py +63 -0
- 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.
|
hermes_ssh-0.3.1/LICENSE
ADDED
|
@@ -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
|
+
[](https://github.com/TheEpTic/hermes-plugins/actions/workflows/ci.yml)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
[](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).
|