Skip to content
Open
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
13 changes: 13 additions & 0 deletions integrations/hermes/compartment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,16 @@ def shutdown(self) -> None:
self._vault.save()
except Exception as exc:
logger.warning("compartment shutdown save failed: %s", exc)


def register(ctx) -> None:
"""Plugin entry point: hand Hermes the provider instance.

Hermes's memory loader calls ``register(ctx)`` first and only falls back
to scanning the module for a ``MemoryProvider`` subclass when it is
missing, so this changes nothing for existing installs. It is required by
the Nous plugin catalog's admission probe (``hermes plugins validate``),
which imports the plugin in a bare interpreter and fails with "no
register() function" without it.
"""
ctx.register_memory_provider(CompartmentMemoryProvider())
13 changes: 13 additions & 0 deletions src/compartment/data/hermes-plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,16 @@ def shutdown(self) -> None:
self._vault.save()
except Exception as exc:
logger.warning("compartment shutdown save failed: %s", exc)


def register(ctx) -> None:
"""Plugin entry point: hand Hermes the provider instance.

Hermes's memory loader calls ``register(ctx)`` first and only falls back
to scanning the module for a ``MemoryProvider`` subclass when it is
missing, so this changes nothing for existing installs. It is required by
the Nous plugin catalog's admission probe (``hermes plugins validate``),
which imports the plugin in a bare interpreter and fails with "no
register() function" without it.
"""
ctx.register_memory_provider(CompartmentMemoryProvider())
83 changes: 83 additions & 0 deletions tests/test_hermes_plugin_register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""The Hermes provider plugin exposes the ``register(ctx)`` entry point.

Hermes discovers a user-installed memory provider by scanning its
``__init__.py`` for the ``MemoryProvider`` contract and loads it through
``register(ctx)`` first, falling back to the subclass scan. The Nous plugin
catalog's admission gate (``hermes plugins validate``) has no fallback: it
imports the plugin in a bare interpreter, calls ``register`` against a
recording context, and fails the entry with "no register() function" when
it is absent. Both shipped copies must carry it.
"""
import importlib.util
import pathlib
import sys
import types

import pytest

ROOT = pathlib.Path(__file__).resolve().parents[1]
PLUGIN_INITS = (
ROOT / "integrations" / "hermes" / "compartment" / "__init__.py",
ROOT / "src" / "compartment" / "data" / "hermes-plugin" / "__init__.py",
)


class _RecordingContext:
"""The shape of Hermes's probe context: every registration is a no-op."""

def __init__(self):
self.memory_providers = []

def register_memory_provider(self, provider):
self.memory_providers.append(provider)

def __getattr__(self, _name):
return lambda *args, **kwargs: None


def _load_plugin(path: pathlib.Path, monkeypatch):
"""Import the plugin by file path the way Hermes does, with the one
Hermes module it imports (``agent.memory_provider``) stubbed out."""
agent_pkg = types.ModuleType("agent")
memory_provider = types.ModuleType("agent.memory_provider")

class MemoryProvider: # the ABC the plugin subclasses
pass

memory_provider.MemoryProvider = MemoryProvider
agent_pkg.memory_provider = memory_provider
monkeypatch.setitem(sys.modules, "agent", agent_pkg)
monkeypatch.setitem(sys.modules, "agent.memory_provider", memory_provider)

name = f"compartment_hermes_plugin_{path.parent.name.replace('-', '_')}"
spec = importlib.util.spec_from_file_location(
name, path, submodule_search_locations=[str(path.parent)])
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, name, module)
spec.loader.exec_module(module)
return module, MemoryProvider


@pytest.mark.parametrize("path", PLUGIN_INITS, ids=lambda p: p.parent.name)
def test_register_hands_hermes_the_provider(path, monkeypatch):
module, base = _load_plugin(path, monkeypatch)
ctx = _RecordingContext()

module.register(ctx)

assert len(ctx.memory_providers) == 1
provider = ctx.memory_providers[0]
assert isinstance(provider, base)
assert provider.name == "compartment"
# The picker's "no setup needed" hint comes from an empty schema.
assert provider.get_config_schema() == []


@pytest.mark.parametrize("path", PLUGIN_INITS, ids=lambda p: p.parent.name)
def test_register_never_imports_compartment_at_load(path, monkeypatch):
"""Importing the plugin and registering must not need the compartment
package: Hermes enumerates providers before anything is installed, and
the catalog probe runs in a bare interpreter."""
monkeypatch.setitem(sys.modules, "compartment", None) # import -> ImportError
module, _ = _load_plugin(path, monkeypatch)
module.register(_RecordingContext())
Loading