Skip to content

feat: package skeleton#2

Merged
blaspat merged 1 commit into
mainfrom
feat/package-skeleton
Jun 4, 2026
Merged

feat: package skeleton#2
blaspat merged 1 commit into
mainfrom
feat/package-skeleton

Conversation

@blaspat

@blaspat blaspat commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

Bootstrap hermes-nodes-plugin as a pip-installable package that auto-loads into Hermes via the hermes_agent.plugins entry-point group.

What's in this PR

  • pyproject.toml: setuptools build, project metadata, dev extras (pytest, pytest-asyncio), hermes_agent.plugins entry point, setuptools package discovery, pytest config
  • hermes_nodes_plugin/__init__.py: stub register(ctx) that no-ops; __version__ = 0.1.0
  • tests/test_smoke.py: 4 tests covering import, register contract, idempotency, and entry-point discovery
  • tests/__init__.py, .gitignore: standard Python project layout

Design decision (please review)

The plan said entry-point target should be hermes_nodes_plugin:register (module:function form). I diverged to hermes_nodes_plugin (module only) after simulating Hermes's loader:

# hermes_cli/plugins.py:_load_plugin
module = self._load_entrypoint_module(manifest)   # <-- ep.load()
register_fn = getattr(module, "register", None)
if register_fn is None:
    loaded.error = "no register() function"

ep.load() on a module:function target returns the function, not the module. Then getattr(function_obj, "register", None) is always None, so Hermes silently logs "no register() function" and the plugin never loads. The module-only form is what Hermes's own dev guide documents.

I added a regression-guard test that fails if anyone reverts to the module:function form. If you specifically want the module:function form, the fix has to happen in Hermes's loader (out of scope for this repo).

Verification

  • pip install -e . succeeds in a fresh venv (3.11.15)
  • pytest -v — 4/4 pass
  • ruff check . — clean
  • Static security scan — clean (no secrets, no shell injection, no eval/exec, no pickle, no SQL injection)
  • Independent reviewer subagent — passed

Refs

  • Plan: §Task 2.1
  • Spec: REQUIREMENTS.md §FR-3

Test plan for reviewer

git clone https://github.com/blaspat/hermes-nodes-plugin.git
cd hermes-nodes-plugin
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -v

Bootstrap hermes-nodes-plugin as a pip-installable package that
auto-loads into Hermes via the hermes_agent.plugins entry-point group.

What:
- pyproject.toml: setuptools build, project metadata, dev extras (pytest,
  pytest-asyncio), hermes_agent.plugins entry point, setuptools package
  discovery, pytest config.
- hermes_nodes_plugin/__init__.py: stub register(ctx) that no-ops;
  __version__ = '0.1.0'.
- tests/test_smoke.py: 4 tests — package import, register contract with
  mock ctx, idempotency, and entry-point discovery via importlib.metadata
  with a regression guard against the module:function form (Hermes's
  loader expects ep.load() to return a module, not a function).
- tests/__init__.py, .gitignore: standard Python project layout.

Design decision: the entry point targets the module (hermes_nodes_plugin),
not the module:function form (hermes_nodes_plugin:register). The
module:function form causes ep.load() to return the function object,
which breaks Hermes's _load_plugin since it does getattr(module, 'register')
expecting a module. This is the documented pattern in Hermes's plugin
dev guide.

Refs: plan §Task 2.1, REQUIREMENTS.md §FR-3.
Acceptance: pip install -e . works, register is importable, 4/4 smoke
tests pass, ruff clean, no security concerns in static scan.

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
@blaspat blaspat marked this pull request as ready for review June 4, 2026 11:00
@blaspat blaspat merged commit 0423830 into main Jun 4, 2026
@blaspat blaspat deleted the feat/package-skeleton branch June 6, 2026 04:28
blaspat added a commit that referenced this pull request Jun 12, 2026
- #2: Defer expanduser() via get_store_path() helper
- #3: Remove --name fallback, make required
- #5: Add max_length=32 to ts field
- #6: Add logger.warning on auth failure
- #7: Add test_pair_suggestion_has_name
- #8: --name in all Go pair test calls

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
blaspat added a commit that referenced this pull request Jun 12, 2026
* fix: add ts field to hello and auth Pydantic models

The Go client sends a ts timestamp on every envelope via MarshalJSON
(envelope + payload flattened to top level). Both _HelloMessage and
_AuthMessage had extra="forbid" but neither declared ts, so every
hello/auth message from the Go client was rejected as containing an
unknown field → close 4003 (message out of order).

Add ts: str | None = None to both models so the field is accepted
without relaxing the extra="forbid" guard against truly unknown
fields.

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix: include /ws/nodes path in pair output and README

The pair command prints a copy-pasteable command, but the example URL
didn't include the /ws/nodes path. Updated:

  - cli.py: <host:port> → <host:port>/ws/nodes
  - README.md example: wss://vps.yourdomain.com:6969 → ...:6969/ws/nodes

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix: include --name flag in hermes-node pair suggestion

The suggested laptop command didn't include --name <name>, so the
operator would pair without knowing to pass the server-registered
name. The Go client then defaults the name to 'config.toml' (from
filepath.Base(configPath)), which doesn't match, causing
authentication-failed on the first connect.

Also update the README example to match.

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix: apply Quinn review findings #2, #3, #5, #6, #7, #8

- #2: Defer expanduser() via get_store_path() helper
- #3: Remove --name fallback, make required
- #5: Add max_length=32 to ts field
- #6: Add logger.warning on auth failure
- #7: Add test_pair_suggestion_has_name
- #8: --name in all Go pair test calls

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

---------

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
blaspat added a commit that referenced this pull request Jun 15, 2026
* fix: add ts field to hello and auth Pydantic models

The Go client sends a ts timestamp on every envelope via MarshalJSON
(envelope + payload flattened to top level). Both _HelloMessage and
_AuthMessage had extra="forbid" but neither declared ts, so every
hello/auth message from the Go client was rejected as containing an
unknown field → close 4003 (message out of order).

Add ts: str | None = None to both models so the field is accepted
without relaxing the extra="forbid" guard against truly unknown
fields.

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix: include /ws/nodes path in pair output and README

The pair command prints a copy-pasteable command, but the example URL
didn't include the /ws/nodes path. Updated:

  - cli.py: <host:port> → <host:port>/ws/nodes
  - README.md example: wss://vps.yourdomain.com:6969 → ...:6969/ws/nodes

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix: include --name flag in hermes-node pair suggestion

The suggested laptop command didn't include --name <name>, so the
operator would pair without knowing to pass the server-registered
name. The Go client then defaults the name to 'config.toml' (from
filepath.Base(configPath)), which doesn't match, causing
authentication-failed on the first connect.

Also update the README example to match.

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix: apply Quinn review findings #2, #3, #5, #6, #7, #8

- #2: Defer expanduser() via get_store_path() helper
- #3: Remove --name fallback, make required
- #5: Add max_length=32 to ts field
- #6: Add logger.warning on auth failure
- #7: Add test_pair_suggestion_has_name
- #8: --name in all Go pair test calls

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

* fix: accept session_id kwarg in lifecycle hook wrappers

Hermes passes session_id as a keyword argument to hook callbacks.
The wrappers were missing the parameter, causing:
  TypeError: _on_session_start_lazy() got an unexpected keyword argument 'session_id'

Fix: add session_id: str =  to both _on_session_start_lazy and
_on_session_end_lazy. The parameter is accepted but unused (the
lifecycle functions themselves don't need it).

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>

---------

Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant