Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ jobs:
- name: Build source archive
run: |
TAG="${{ github.ref_name }}"
ARCHIVE="hermes-nodes-plugin-${TAG#v}.tar.gz"
mkdir -p dist
git archive --format=tar.gz --prefix="hermes-nodes-plugin-${TAG#v}/" \
ARCHIVE="hermes-node-plugin-${TAG#v}.tar.gz"
# Use git archive to create the tarball
git archive --format=tar.gz --prefix="hermes-node-plugin-${TAG#v}/" \
-o "dist/${ARCHIVE}" HEAD
echo "artifact=${ARCHIVE}" >> "$GITHUB_ENV"

Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ for getting changes reviewed and merged.
file, ship the cleanup as a separate PR.
- **Tests required for behavior changes.** A bug fix without a regression
test is incomplete. The test suite is `pytest tests/ -v`; the project also
has E2E tests gated on `-m e2e` that need the `hermes-nodes` Go binary.
has E2E tests gated on `-m e2e` that need the `hermes-node` Go binary.
- **Don't reformat unrelated code.** Keep diffs focused. A drive-by `black`
pass makes the reviewer read more than they need to.

## Local setup

```bash
git clone https://github.com/blaspat/hermes-nodes-plugin.git
cd hermes-nodes-plugin
git clone https://github.com/blaspat/hermes-node-plugin.git
cd hermes-node-plugin

# Run the test suite
pytest tests/ -v
Expand Down Expand Up @@ -89,7 +89,7 @@ Don't file public issues for security bugs. See

- Unit tests live in `tests/` and mirror the source layout.
- E2E tests in `tests/e2e/` are gated behind `@pytest.mark.e2e` and need
the Go binary from [`hermes-nodes`](https://github.com/blaspat/hermes-nodes)
the Go binary from [`hermes-node`](https://github.com/blaspat/hermes-node)
built and on `PATH`. Run them with `pytest tests/e2e/ -v -m e2e`.
- New features need at least one happy-path test and one failure-path test
in the same PR.
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# hermes-nodes-plugin
# Hermes Node Plugin
A Hermes Agent plugin that turns any Hermes profile into a “brain” to command remote nodes over an authenticated WebSocket.

## Table of Contents
Expand All @@ -17,14 +17,14 @@ Install Python 3.11+ and have a Hermes Agent set up.
Install the plugin:

```bash
git clone https://github.com/blaspat/hermes-nodes-plugin \
~/.hermes/plugins/hermes-nodes-plugin
git clone https://github.com/blaspat/hermes-node-plugin \
~/.hermes/plugins/hermes-node-plugin
```
Then, update your `config.yaml`. Add `hermes-nodes-plugin`
Then, update your `config.yaml`. Add `hermes-node-plugin`
```yaml
plugins:
enabled:
- hermes-nodes-plugin
- hermes-node-plugin
```

## Core Features
Expand All @@ -36,7 +36,7 @@ plugins:
## Usage

### 1. Configure
Edit `~/.hermes/hermes-nodes.yaml` with host/port/TLS settings.
Edit `~/.hermes/hermes-node.yaml` with host/port/TLS settings.

Optional retry settings (defaults shown):

Expand Down Expand Up @@ -127,7 +127,7 @@ hermes node revoke --name my-devbox
A: All interactions are logged to `~/.hermes/logs/nodes-audit.log` and retained per `audit_retention_days`.

## Related
- **[hermes‑nodes](`github.com/blaspat/hermes-nodes`):** Remote node binary.
- **[Hermes Node (client)](`github.com/blaspat/hermes-node`):** Remote node binary.
- **[Hermes Agent](`github.com/NousResearch/hermes-agent`):** Core framework.

---
Expand Down
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- **Token format:** 32 random bytes, base64url-encoded, generated with `secrets.token_urlsafe(32)`. ~256 bits of entropy.
- **Token comparison:** constant-time on the server (`hmac.compare_digest`).
- **Token storage (server):** Fernet-encrypted (AES-128-CBC + HMAC-SHA256) at `~/.hermes/nodes/tokens.json`. The Fernet key is loaded from `HERMES_NODES_TOKEN_KEY`.
- **Token storage (node):** plaintext in `~/.hermes-nodes/config.toml`, file mode `0600`. **v1 limitation** — see "Future hardening" below.
- **Token storage (node):** plaintext in `~/.hermes-node/config.toml`, file mode `0600`. **v1 limitation** — see "Future hardening" below.
- **Token lifetime:** unlimited. Revoked only by `hermes node revoke`. v1 has no auto-rotation.

## Transport
Expand All @@ -44,7 +44,7 @@

**Both sides log every call.** Format is identical: one JSON object per line.

**Node-side log:** `~/.hermes-nodes/audit.log` (configurable). Default retention: 90 days (configurable).
**Node-side log:** `~/.hermes-node/audit.log` (configurable). Default retention: 90 days (configurable).
**Server-side log:** `~/.hermes/logs/nodes-audit.log`. Default retention: 1 year (configurable).

**Fields:**
Expand Down Expand Up @@ -88,7 +88,7 @@ This list is important for a security review. The node binary:
- ❌ Does not capture keystrokes or clipboard.
- ❌ Does not modify system settings, install software, or change firewall rules.
- ❌ Does not open any inbound network ports.
- ❌ Does not write to disk outside `~/.hermes-nodes/` (the config dir).
- ❌ Does not write to disk outside `~/.hermes-node/` (the config dir).
- ❌ Does not read files outside the configured `allowed_paths`.
- ❌ Does not load dynamic code or evaluate external input.
- ❌ Does not phone home (no telemetry, no update checks, no analytics).
Expand Down
26 changes: 13 additions & 13 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""hermes-nodes-plugin: Hermes Agent plugin for remote node control.
"""hermes-node-plugin: Hermes Agent plugin for remote node control.

Pairs with the ``hermes-nodes`` Go binary. The plugin turns Agent (or any
Pairs with the ``hermes-node`` Go binary. The plugin turns Agent (or any
Hermes agent) into a "brain" that can exec / read / write on paired
remote nodes over an authenticated WSS connection.

Expand Down Expand Up @@ -78,7 +78,7 @@ async def _on_session_end_lazy(session_id: str = "", **kwargs) -> None:
ctx.register_hook("on_session_start", _on_session_start_lazy)
ctx.register_hook("on_session_end", _on_session_end_lazy)
except Exception as exc:
log.warning("hermes-nodes-plugin: hook registration failed: %s", exc)
log.warning("hermes-node-plugin: hook registration failed: %s", exc)

# ------------------------------------------------------------------ #
# 2. CLI subcommand — guarded separately #
Expand All @@ -98,14 +98,14 @@ def _node_handler_lazy(args) -> None:
ctx.register_cli_command(
"node",
help=(
"Manage paired hermes-nodes (WSS node server). "
"Manage paired hermes-node (WSS node server). "
"Subcommands: pair, list, revoke, status."
),
setup_fn=_setup_node_subcommand_lazy,
handler_fn=_node_handler_lazy,
)
except Exception as exc:
log.warning("hermes-nodes-plugin: CLI registration failed: %s", exc)
log.warning("hermes-node-plugin: CLI registration failed: %s", exc)

# ------------------------------------------------------------------ #
# 3. Agent tools #
Expand Down Expand Up @@ -136,7 +136,7 @@ def _node_handler_lazy(args) -> None:
emoji=emoji,
)
except Exception as exc:
log.warning("hermes-nodes-plugin: tool registration failed: %s", exc)
log.warning("hermes-node-plugin: tool registration failed: %s", exc)

# ------------------------------------------------------------------ #
# 4. Auto-start the WSS server #
Expand Down Expand Up @@ -187,15 +187,15 @@ def _start_server() -> None:

loop.run_until_complete(_on_session_start())
log.info(
"hermes-nodes-plugin: WSS server started on port %d"
"hermes-node-plugin: WSS server started on port %d"
" (background thread)",
_check_port,
)
# Keep the loop alive so uvicorn tasks continue running.
loop.run_forever()
except Exception as exc:
log.warning(
"hermes-nodes-plugin: server background thread failed: %s",
"hermes-node-plugin: server background thread failed: %s",
exc,
)
finally:
Expand All @@ -205,20 +205,20 @@ def _start_server() -> None:
import threading

t = threading.Thread(
target=_start_server, daemon=True, name="hermes-nodes-wss"
target=_start_server, daemon=True, name="hermes-node-wss"
)
t.start()
except Exception as exc:
log.warning(
"hermes-nodes-plugin: could not start server thread: %s", exc
"hermes-node-plugin: could not start server thread: %s", exc
)
else:
log.debug(
"hermes-nodes-plugin: port %d already bound — "
"hermes-node-plugin: port %d already bound — "
"server likely already running. Skipping auto-start.",
_check_port,
)
else:
log.debug(
"hermes-nodes-plugin: auto-start disabled (HERMES_NODES_AUTO_START=0)"
)
"hermes-node-plugin: auto-start disabled (HERMES_NODES_AUTO_START=0)"
)
12 changes: 6 additions & 6 deletions audit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Append-only JSONL audit log for hermes-nodes-plugin calls (Task 2.9).
"""Append-only JSONL audit log for hermes-node-plugin calls (Task 2.9).

Every ``node_exec`` / ``node_read`` / ``node_write`` call (successful,
errored, timed out, or refused because the node is offline) is recorded
Expand Down Expand Up @@ -369,7 +369,7 @@ def record(
# the write rather than losing the row
# silently or breaking the call site.
logger.warning(
"hermes-nodes audit: record() called on a closed writer "
"hermes-node audit: record() called on a closed writer "
"(node=%r, action=%r, status=%r) — row dropped",
node,
action,
Expand Down Expand Up @@ -406,7 +406,7 @@ def record(
# rotation errors that would otherwise violate the
# ``record()`` never-raises contract (Issue #11).
logger.warning(
"hermes-nodes audit: failed to write entry "
"hermes-node audit: failed to write entry "
"(node=%r, action=%r, status=%r): %s",
node,
action,
Expand Down Expand Up @@ -471,7 +471,7 @@ def purge_expired_rotations(self, *, now: float | None = None) -> int:
entries = sorted(parent.iterdir())
except OSError as exc:
logger.warning(
"hermes-nodes audit: cannot list %s to purge rotations: %s",
"hermes-node audit: cannot list %s to purge rotations: %s",
parent,
exc,
)
Expand All @@ -493,7 +493,7 @@ def purge_expired_rotations(self, *, now: float | None = None) -> int:
except OSError as exc:
# Don't fail the whole sweep on one unreadable file.
logger.warning(
"hermes-nodes audit: cannot delete expired rotation %s: %s",
"hermes-node audit: cannot delete expired rotation %s: %s",
entry,
exc,
)
Expand Down Expand Up @@ -665,7 +665,7 @@ def _resolve_audit_config(env: Mapping[str, str] | None = None) -> AuditConfig:
retention = int(retention_raw)
except (TypeError, ValueError):
logger.warning(
"hermes-nodes audit: invalid %s=%r; using default %d",
"hermes-node audit: invalid %s=%r; using default %d",
RETENTION_ENV_VAR,
retention_raw,
DEFAULT_RETENTION_DAYS,
Expand Down
8 changes: 4 additions & 4 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def setup_node_cli(subparser: argparse.ArgumentParser) -> None:
that's been built since the plugin first installed.
"""
subparser.description = (
"Manage paired hermes-nodes (WSS node server). "
"Manage paired hermes-node (WSS node server). "
"Subcommands: pair, list, revoke, status."
)
subs = subparser.add_subparsers(dest="node_action")
Expand Down Expand Up @@ -422,10 +422,10 @@ def _cmd_status(args: argparse.Namespace | None = None) -> int:
try:
s.connect((config.connect_host, config.port))
s.close()
print(f"hermes-nodes server: listening on {config.connect_host}:{config.port}")
print(f"hermes-node server: listening on {config.connect_host}:{config.port}")
return 0
except (OSError, socket.timeout):
print("hermes-nodes server: not running")
print("hermes-node server: not running")
return 1


Expand Down Expand Up @@ -494,7 +494,7 @@ def main() -> None:
"""
parser = argparse.ArgumentParser(
prog="hermes-node",
description="Manage paired hermes-nodes (WSS node server).",
description="Manage paired hermes-node (WSS node server).",
)
setup_node_cli(parser)
args = parser.parse_args()
Expand Down
8 changes: 4 additions & 4 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Configuration loader for the hermes-nodes plugin.
"""Configuration loader for the hermes-node plugin.

Precedence (highest to lowest):

1. Environment variables (``HERMES_NODES_*``).
2. YAML config file at ``~/.hermes/hermes-nodes.yaml`` (path overridable
2. YAML config file at ``~/.hermes/hermes-node.yaml`` (path overridable
via ``load_config(config_path=...)``).
3. Built-in defaults baked into :class:`NodeServerConfig`.

Expand Down Expand Up @@ -90,7 +90,7 @@
# Constants
# ---------------------------------------------------------------------------

DEFAULT_CONFIG_PATH = Path("~/.hermes/hermes-nodes.yaml").expanduser()
DEFAULT_CONFIG_PATH = Path("~/.hermes/hermes-node.yaml").expanduser()
DEFAULT_TOKEN_STORE_PATH = Path("~/.hermes/nodes/tokens.json")
DEFAULT_TOKEN_STORE_STR = "~/.hermes/nodes/tokens.json"

Expand Down Expand Up @@ -628,7 +628,7 @@ def load_config(
Tests use this to inject fixtures without touching the real
process environment.
config_path: Override the YAML file location (defaults to
``~/.hermes/hermes-nodes.yaml``). A missing file is *not* an
``~/.hermes/hermes-node.yaml``). A missing file is *not* an
error — the loader falls through to dataclass defaults.

Returns:
Expand Down
4 changes: 2 additions & 2 deletions docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The wire protocol spoken between a Hermes Agent brain (Python, on the
server/VPS) and a Hermes Node (Go, on the laptop/remote machine) is
**defined canonically in
[`hermes-nodes/PROTOCOL.md`](https://github.com/blaspat/hermes-nodes/blob/main/PROTOCOL.md)**.
[`hermes-node/PROTOCOL.md`](https://github.com/blaspat/hermes-node/blob/main/PROTOCOL.md)**.
Both implementations must conform to that contract.

This document exists for two reasons:
Expand All @@ -24,7 +24,7 @@ doc wins. File a PR against this file to bring it back in sync.
## 1. Canonical protocol

See
[`hermes-nodes/PROTOCOL.md`](https://github.com/blaspat/hermes-nodes/blob/main/PROTOCOL.md).
[`hermes-node/PROTOCOL.md`](https://github.com/blaspat/hermes-node/blob/main/PROTOCOL.md).
That doc covers:

- Connection lifecycle (hello / hello_ack / auth / exec / bye)
Expand Down
8 changes: 4 additions & 4 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# hermes-nodes-plugin: Requirements
# hermes-node-plugin: Requirements

This document is the source of truth for what the `hermes-nodes-plugin` package must do. The implementation plan in `/home/User/.hermes/plans/2026-06-04_001727-hermes-nodes.md` derives from this.
This document is the source of truth for what the `hermes-node-plugin` package must do. The implementation plan derives from this.

> **Audience:** Agent (implements), Quinn (validates against), User (approves).

Expand Down Expand Up @@ -58,7 +58,7 @@ This document is the source of truth for what the `hermes-nodes-plugin` package

### FR-4: Configuration

**FR-4.1** Plugin reads configuration from `~/.hermes/hermes-nodes.yaml` and env vars. Env vars override file values. Defaults if neither set:
**FR-4.1** Plugin reads configuration from `~/.hermes/hermes-node.yaml` and env vars. Env vars override file values. Defaults if neither set:
- `host`: `127.0.0.1` (the safe default — assumes nginx is fronting TLS)
- `port`: `7000`
- `tls_cert_path`: unset (TLS termination is expected at the reverse proxy)
Expand Down Expand Up @@ -173,7 +173,7 @@ All of the following must be true:
5. ✅ The plugin's unit test suite passes in CI with >= 80% coverage.
6. ✅ The e2e test in `tests/e2e/test_full_flow.py` passes on Linux amd64 (CI), and the install scripts work on a clean Mac and a clean Windows machine (manual verification by User).
7. ✅ `SECURITY-REVIEW.md` exists and is suitable for showing to a corporate security team.
8. ✅ A `pip install git+https://github.com/blaspat/hermes-nodes-plugin.git` in any Hermes profile's venv results in the plugin auto-loading and the `hermes node ...` commands appearing in the CLI.
8. ✅ A `pip install git+https://github.com/blaspat/hermes-node-plugin.git` in any Hermes profile's venv results in the plugin auto-loading and the `hermes node ...` commands appearing in the CLI.

---

Expand Down
Loading
Loading