Просмотр исходного кода

Name the config key Hermes actually reads, in all three docs

README told users to set `gateway.platforms.chatto.extra.home_channel`. Nothing
registers `chatto` — Hermes keys its platform map by Platform.value, and the
plugin's is its directory name, so that path configured nothing and said so
with no error at all.

The platform segment is the directory name `hermes-chatto-plugin`;
plugin.yaml's `name: chatto-platform` is an alias _missing_ resolves onto it
(gateway/config.py), accepted on read but never what Hermes writes back. Fixed
README.md and AGENTS.md, and added the reason, since PLATFORM_NAME reads like
the key and is not.

The same audit turned up two more silent no-ops in the block users copy:
`platforms:` was nested under `gateway:` (only the flat form is read —
GatewayConfig.from_dict picks that key top-level), and the manual install step
named `plugins/platforms/chatto/`, a directory that would change the config key
the paragraph below it documents.

Three tests pin it, each mutation-checked: the README example must parse to a
top-level platforms key with a resolvable platform name, both docs must name the
real key, and the install path must match the config key. The first two fail on
the gateway-nesting and on `chatto:` respectively.
Paul Klumpp 2 недель назад
Родитель
Сommit
3b18018f84
3 измененных файлов с 88 добавлено и 17 удалено
  1. 10 2
      AGENTS.md
  2. 23 15
      README.md
  3. 55 0
      test_adapter.py

+ 10 - 2
AGENTS.md

@@ -451,8 +451,16 @@ roster design in the behavioural contract below.
 
 A config value lives as a `ConfigField` class attribute in `platform_config.py`.
 From the attribute name it derives the `config.yaml` key under
-`gateway.platforms.chatto.extra` (the *config_key*, overridable per field) and
-the `CHATTO_*` environment variable (upper-cased config_key). Resolution order
+`gateway.platforms.hermes-chatto-plugin.extra` (the *config_key*, overridable
+per field) and the `CHATTO_*` environment variable (upper-cased config_key).
+That platform segment is the plugin's **directory** name, not
+`ChattoConstants.PLATFORM_NAME` (`chatto-platform`): Hermes keys its platform
+map by `Platform.value`, and `_missing_` resolves the manifest name to the
+directory name, so `Platform("chatto-platform").value` is
+`"hermes-chatto-plugin"`. Both spellings are accepted on read — Hermes rewrites
+the alias to the directory name and writes only the latter back out, so quote
+the directory name in docs. `chatto` is rejected outright: nothing registers
+it. Resolution order
 per instance: env var → extra → default. Declaring a field is enough for the
 adapter, the env-enablement seeding and the setup wizard to pick it up;
 `plugin.yaml` additionally repeats the env names as wizard prompts, so keep

+ 23 - 15
README.md

@@ -76,7 +76,9 @@ Hermes clones the plugin from that Git URL into its platform-plugin directory an
 
 ### Option B: Manual Installation
 
-Copy the plugin files into the Hermes Agent plugin directory so they sit at `~/.hermes/plugins/platforms/chatto/`. Hermes auto-discovers platform plugins on gateway startup — no manual registration needed.
+Copy the plugin files into the Hermes Agent plugin directory so they sit at `~/.hermes/plugins/platforms/hermes-chatto-plugin/`. Hermes auto-discovers platform plugins on gateway startup — no manual registration needed.
+
+Keep that directory name: it is what `config.yaml` keys the platform under (see [Home Channel](#home-channel)), so a different folder name also changes the config path.
 
 ## Configuration
 
@@ -139,21 +141,27 @@ Secrets (`CHATTO_TOKEN`) belong in `~/.hermes/.env`, never in `config.yaml`.
 Or configure via `~/.hermes/config.yaml`:
 
 ```yaml
-gateway:
-  platforms:
-    chatto:
-      enabled: true
-      extra:
-        base_url: https://chat.example.com
-        home_channel: REljMv5Pgolo6Y9
-        require_mention_rooms: []     # rooms that answer only @mentions
-        optional_mention_rooms: []    # rooms that answer everything
-        allowed_users: []          # empty = deny all (or set allow_all_users)
-        allow_all_users: true
-        edit_dispatch: true        # process message edits as corrections
-        edit_window: 300           # seconds after posting an edit may land
+platforms:
+  hermes-chatto-plugin:
+    enabled: true
+    extra:
+      base_url: https://chat.example.com
+      home_channel: REljMv5Pgolo6Y9
+      require_mention_rooms: []     # rooms that answer only @mentions
+      optional_mention_rooms: []    # rooms that answer everything
+      allowed_users: []          # empty = deny all (or set allow_all_users)
+      allow_all_users: true
+      edit_dispatch: true        # process message edits as corrections
+      edit_window: 300           # seconds after posting an edit may land
 ```
 
+> **Note:** `platforms:` is a **top-level** key — it does not nest under `gateway:`.
+
+> **Note:** The platform key is the plugin's directory name, `hermes-chatto-plugin`.
+> The manifest name `chatto-platform` is accepted as an alias, but Hermes rewrites
+> it to the directory name — write the directory name so the file matches what
+> Hermes itself saves.
+
 > **Note:** Environment variables override `config.yaml` values.
 
 ### Environment Variables
@@ -339,7 +347,7 @@ For deployment-level hardening beyond the bot's own gates, see the security docu
 The home channel is the default outbound target for proactive messages — cron job output, reminders, and notifications when there is no inbound conversation context to reply into. Set it via:
 
 - `CHATTO_HOME_CHANNEL` env var (room ID)
-- `config.yaml` → `gateway.platforms.chatto.extra.home_channel`
+- `config.yaml` → `gateway.platforms.hermes-chatto-plugin.extra.home_channel` (the platform's key is its plugin *directory* name; `chatto-platform` from `plugin.yaml` is accepted as an alias)
 
 Without a set home channel, context-less cron/notification deliveries have no Chatto target and are not sent — set one if you plan to use Hermes' proactive output. The home channel follows the same participation rules as any room: a silent home channel still receives cron/notification posts, but the bot converses there only if it is listed in one of the mention lists too.
 

+ 55 - 0
test_adapter.py

@@ -117,6 +117,12 @@ def _registered_tool_names(ctx) -> list[str]:
     return [tool["name"] for tool in ctx.registered_tools]
 
 
+def _read_repo_file(name):
+    """One of this repo's own documents (README.md, AGENTS.md), as text."""
+    with open(os.path.join(PLUGIN_ROOT, name)) as handle:
+        return handle.read()
+
+
 def _load_plugin_tools():
     """Load the plugin's ``tools.py`` under a private module name.
 
@@ -416,6 +422,55 @@ class TestRegistration:
         with patch.object(ChattoAdapter, "send_video", BasePlatformAdapter.send_video):
             assert "video" not in _capabilities()
 
+    def test_readme_yaml_example_is_shaped_like_a_config_hermes_reads(self):
+        """The example is what users paste into ~/.hermes/config.yaml. Two ways
+        it silently did nothing: `platforms:` nested under `gateway:` (only the
+        flat form is read — GatewayConfig.from_dict picks that key top-level),
+        and a platform key that resolves to no member (unknown keys are skipped
+        without an error). Shape is asserted without Hermes resolution, because
+        a checkout is not necessarily installed in a Hermes tree.
+        """
+        import re
+
+        import yaml
+
+        readme = _read_repo_file("README.md")
+        block = re.search(r"```yaml\n(platforms:.*?)```", readme, re.DOTALL)
+        assert block, "README has no config.yaml example block"
+        parsed = yaml.safe_load(block.group(1))
+        assert "platforms" in parsed, (
+            "config.yaml example must carry platforms: at the top level — nested "
+            "under gateway: it is silently ignored"
+        )
+        assert parsed["platforms"], "example configures no platform"
+        (platform_key,) = parsed["platforms"]
+        assert platform_key in ("hermes-chatto-plugin", "chatto-platform"), (
+            f"documented platform key {platform_key!r} is neither the plugin's "
+            "directory name nor its plugin.yaml name, so it configures nothing"
+        )
+        assert parsed["platforms"][platform_key]["extra"]["home_channel"]
+
+    def test_documented_config_path_names_a_platform_key(self):
+        """The docs name a `platforms.<key>.extra` path. `chatto` is neither the
+        plugin's directory name nor its plugin.yaml name, so it matches no
+        member and configures nothing."""
+        for doc in ("README.md", "AGENTS.md"):
+            text = _read_repo_file(doc)
+            assert "platforms.hermes-chatto-plugin.extra" in text, doc
+            assert "platforms.chatto.extra" not in text, doc
+
+    def test_documented_install_path_matches_the_documented_config_key(self):
+        """Both install paths must name the same directory: Hermes keys the
+        platform by it, so a README that installs into `chatto/` documents a
+        config path that will not match what it installs."""
+        readme = _read_repo_file("README.md")
+        assert "plugins/platforms/hermes-chatto-plugin/" in readme
+        # Only our own install instruction — the acknowledgements quote a
+        # third-party repo URL whose path is theirs and must stay verbatim.
+        install_steps = readme.split("## Acknowledgements")[0]
+        assert install_steps, "README lost its Acknowledgements heading"
+        assert "platforms/chatto/" not in install_steps
+
     def test_provides_tools_is_deliverable_in_a_cli_session(self):
         """``provides_tools`` asks Hermes to register the tools without the
         adapter, which it can only do through a ``tools.py``. A manifest that