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
96 changes: 96 additions & 0 deletions .plans/missing-platform-plugin-shims.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Missing Platform Plugin Shims β€” Known Issue (undocumented until now)

**Status:** Open β€” documented but not yet fixed, per explicit decision during a
2026-07-09 cleanup pass (`claude/hermes-errors-xh2x9x`).

## Problem

`hermes_cli/gateway.py::_PLATFORMS` used to carry inline setup-wizard metadata
(setup instructions, `vars` schemas, token env vars) for every messaging
platform. Commit `e39b468` ("fix(gateway): source environment env and
auto-wrap dispatch tool args") removed the inline entries for **telegram,
slack, matrix, whatsapp, email, sms, wecom, and feishu**, replacing each with
a comment claiming it "moved to `plugins/platforms/<name>/` β€” setup metadata
discovered dynamically via the platform registry entry registered by
`plugins/platforms/<name>/adapter.py::register()`."

Those 8 plugin directories were never created. `git log --all` shows zero
history for any of `plugins/platforms/{telegram,slack,matrix,whatsapp,email,
sms,wecom,feishu}/`. The directories that *do* exist under `plugins/platforms/`
are: `discord`, `google_chat`, `irc`, `line`, `ntfy`, `simplex`, `teams` β€” all
successfully discovered via `platform_registry`.

## Impact

- **Gateway message routing is NOT affected.** The real adapter
implementations (`gateway/platforms/telegram.py`, `slack.py`, `matrix.py`,
`whatsapp.py`, `email.py`, `sms.py`, `wecom.py`, `feishu.py`) are all still
present and presumably functional β€” existing users with env vars already
configured should be unaffected.
- **`hermes setup gateway`'s interactive picker no longer lists these 8
platforms at all** β€” new users cannot configure Telegram (or any of the
other 7) through the setup wizard; they'd have to hand-edit env vars with
no guided setup_instructions.
- **`_platform_status()` / `_all_platforms()`-driven status displays** (setup
wizard, possibly other menus) silently omit these 8 platforms rather than
showing "not configured".

## Evidence / repro

```
python3 -c "
from hermes_cli.plugins import discover_plugins
discover_plugins()
from gateway.platform_registry import platform_registry
print(sorted(e.name for e in platform_registry.all_entries()))
"
# -> ['discord', 'google_chat', 'irc', 'line', 'ntfy', 'simplex', 'teams']
# telegram/slack/matrix/whatsapp/email/sms/wecom/feishu are absent
```

Test files currently failing because of this (all pre-existing failures,
not caused by this cleanup pass):

- `tests/hermes_cli/test_gateway_platform_gating.py` β€”
`TestMatrixHiddenOnWindows::test_matrix_present_on_linux`,
`test_matrix_present_on_macos`, `test_other_platforms_unaffected_on_windows`
(asserts telegram/matrix are in the picker; they aren't, on any platform)
- `tests/hermes_cli/test_setup.py` β€”
`test_setup_gateway_skips_service_install_when_systemctl_missing`,
`test_setup_gateway_in_container_shows_docker_guidance` (both rely on
Matrix showing as "configured" in the picker so `setup_gateway()` reaches
its "Messaging platforms configured!" branch)
- `tests/hermes_cli/test_setup_openclaw_migration.py::TestGetSectionConfigSummary` β€”
`test_gateway_lists_platforms` (expects "Telegram" in the openclaw-migration
config summary; only "Discord" shows), `test_gateway_recognises_whatsapp_enabled`
(WhatsApp isn't recognized at all, summary is `None`)
- `tests/gateway/test_setup_feishu.py` β€” all 14 tests fail with
`ImportError: cannot import name '_setup_feishu' from 'hermes_cli.gateway'`.
This one's slightly different: the function itself (not just the picker
metadata) was removed from `gateway.py` with a comment claiming it "moved to
plugins/platforms/feishu/adapter.py::interactive_setup" β€” that function
doesn't exist anywhere either. Whichever option below is chosen for Feishu
needs to restore an `interactive_setup`-equivalent entry point, not just a
`register()` metadata shim.

## Options considered (not yet decided)

1. **Restore Telegram only** as a proof of concept, matching
`plugins/platforms/discord/adapter.py`'s registration pattern, and follow
up on the other 7 separately.
2. **Restore all 8** plugin adapter shims β€” larger effort, 8 new files, each
needs to correctly reproduce the removed `vars`/`setup_instructions`/
`install_hint`/`is_connected`/`check_fn` contract without introducing
drift from the real `gateway/platforms/*.py` adapters.
3. **Revert to inline `_PLATFORMS` definitions** for these 8 β€” lowest risk,
but reverses whatever the plugin-registry refactor was meant to
accomplish (see `e39b468`'s commit message/diff for the rest of the
intended architecture).
4. Something else β€” worth checking with whoever authored `e39b468` whether
the plugin shims exist in an unpushed branch/local checkout, since the
commit message describes them as already done.

## Next step

Needs an explicit decision on which option above before someone picks this
up β€” flagged here per user request rather than acted on unilaterally.
4 changes: 2 additions & 2 deletions acp_registry/agent.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"id": "hermes-agent",
"name": "Hermes Agent",
"version": "0.14.0",
"version": "0.15.0",
"description": "Self-improving open-source AI agent by Nous Research with ACP editor integration, persistent memory, skills, and rich tool support.",
"repository": "https://github.com/NousResearch/hermes-agent",
"website": "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp",
"authors": ["Nous Research"],
"license": "MIT",
"distribution": {
"uvx": {
"package": "hermes-agent[acp]==0.14.0",
"package": "hermes-agent[acp]==0.15.0",
"args": ["hermes-acp"]
}
}
Expand Down
1 change: 1 addition & 0 deletions hermes_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def _delete_delegate_children(conn, parent_ids: List[str]) -> List[str]:
_WAL_INCOMPAT_MARKERS = (
"locking protocol", # SQLITE_PROTOCOL on NFS/SMB
"not authorized", # Some FUSE mounts block WAL pragma outright
"disk i/o error", # Flaky network FS during WAL setup
)

# Last SessionDB() init error, per-process. Surfaced in /resume and
Expand Down
10 changes: 7 additions & 3 deletions tests/acp/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ def test_save_session_preserves_existing_messages_on_encode_failure(self, manage

db = manager._get_db()
messages = db.get_messages_as_conversation(state.session_id)
assert messages == [{"role": "user", "content": "original"}]
assert len(messages) == 1
first = {k: v for k, v in messages[0].items() if k != "timestamp"}
assert first == {"role": "user", "content": "original"}

def test_cleanup_clears_all(self, manager):
s1 = manager.create_session()
Expand Down Expand Up @@ -501,7 +503,9 @@ def test_assistant_reasoning_fields_persisted(self, manager):

restored = manager.get_session(state.session_id)
assert restored is not None
assert restored.history == [{
assert len(restored.history) == 1
first = {k: v for k, v in restored.history[0].items() if k != "timestamp"}
assert first == {
"role": "assistant",
"content": "hello",
"reasoning": "step-by-step",
Expand All @@ -511,7 +515,7 @@ def test_assistant_reasoning_fields_persisted(self, manager):
"codex_reasoning_items": [
{"type": "reasoning", "id": "rs_123", "encrypted_content": "enc_blob"},
],
}]
}

def test_restore_preserves_persisted_provider_snapshot(self, tmp_path, monkeypatch):
"""Restored ACP sessions should keep their original runtime provider."""
Expand Down
1 change: 1 addition & 0 deletions tests/hermes_cli/test_auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ class _Entry:
last_status = None
last_error_code = None
last_status_at = None
extra = {}

class _Pool:
def entries(self):
Expand Down
38 changes: 34 additions & 4 deletions tests/hermes_cli/test_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
import hermes_cli.gateway as gateway


class _TTY:
"""Minimal stdin stand-in for tests that need an interactive session.

``gateway_command()`` decides whether to prompt based on
``sys.stdin.isatty()``; pytest's real stdin isn't a TTY, so tests that
exercise the interactive prompt path swap it in via monkeypatch.
"""

def isatty(self):
return True


def _install_fake_gateway_run(monkeypatch, start_gateway):
module = ModuleType("gateway.run")
module.start_gateway = start_gateway
Expand Down Expand Up @@ -235,13 +247,16 @@ def test_gateway_install_in_container_with_operational_systemd_uses_systemd(monk
monkeypatch.setattr(gateway, "is_wsl", lambda: False)
monkeypatch.setattr(gateway, "is_macos", lambda: False)
monkeypatch.setattr(gateway, "is_managed", lambda: False)
# gateway_command() only prompts when stdin is a TTY; pytest's stdin
# isn't one, so simulate an interactive session to exercise the prompts.
monkeypatch.setattr(gateway.sys, "stdin", _TTY())

calls = []
monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question, default)) or True)
monkeypatch.setattr(
gateway,
"systemd_install",
lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", force, system, run_as_user, enable_on_startup)),
lambda force=False, system=False, run_as_user=None, enable_on_startup=True, non_interactive=False: calls.append(("install", force, system, run_as_user, enable_on_startup)),
)
monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start", system)))

Expand Down Expand Up @@ -380,6 +395,10 @@ def fake_run(cmd, check=False, **kwargs):

monkeypatch.setattr(gateway.subprocess, "run", fake_run)
monkeypatch.setattr(gateway, "_ensure_linger_enabled", lambda: helper_calls.append(True))
# This test doesn't exercise the temp-HERMES_HOME guard; the sandbox's
# own HERMES_HOME can legitimately resolve under a temp dir, which would
# otherwise make generate_systemd_unit()'s real output trip the guard.
monkeypatch.setattr(gateway, "_refuse_temp_home_service_write", lambda *a, **k: False)

gateway.systemd_install(force=False)

Expand Down Expand Up @@ -408,6 +427,10 @@ def fake_run(cmd, check=False, **kwargs):
monkeypatch.setattr(gateway.subprocess, "run", fake_run)
monkeypatch.setattr(gateway, "_ensure_user_systemd_env", lambda: None)
monkeypatch.setattr(gateway, "_ensure_linger_enabled", lambda: helper_calls.append(True))
# See test_systemd_install_checks_linger_status: sandbox HERMES_HOME can
# legitimately live under a temp dir, which would otherwise trip the
# temp-HERMES_HOME write guard this test isn't exercising.
monkeypatch.setattr(gateway, "_refuse_temp_home_service_write", lambda *a, **k: False)

gateway.systemd_install(force=False, enable_on_startup=False)

Expand Down Expand Up @@ -479,6 +502,9 @@ def test_conflicting_systemd_units_warning(monkeypatch, tmp_path, capsys):


def test_install_linux_gateway_from_setup_system_choice_without_root_prints_followup(monkeypatch, capsys):
"""Non-root system-scope install refuses without printing a self-elevation
(``sudo ...``) recipe -- deliberately, per the guard's own comment: this
path shouldn't hand back a copy-pasteable elevation command."""
monkeypatch.setattr(gateway, "prompt_linux_gateway_install_scope", lambda: "system")
monkeypatch.setattr(gateway.os, "geteuid", lambda: 1000)
monkeypatch.setattr(gateway, "_default_system_service_user", lambda: "alice")
Expand All @@ -488,8 +514,9 @@ def test_install_linux_gateway_from_setup_system_choice_without_root_prints_foll

out = capsys.readouterr().out
assert (scope, did_install) == ("system", False)
assert "sudo hermes gateway install --system --run-as-user alice" in out
assert "sudo hermes gateway start --system" in out
assert "System service install requires root" in out
assert "hermes gateway install" in out
assert "sudo" not in out


def test_install_linux_gateway_from_setup_system_choice_as_root_installs(monkeypatch):
Expand Down Expand Up @@ -531,14 +558,17 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):
monkeypatch.setattr(gateway, "is_wsl", lambda: False)
monkeypatch.setattr(gateway, "is_macos", lambda: False)
monkeypatch.setattr(gateway, "is_managed", lambda: False)
# See test_gateway_install_in_container_with_operational_systemd_uses_systemd:
# prompting only happens when stdin looks like a TTY.
monkeypatch.setattr(gateway.sys, "stdin", _TTY())

answers = iter([False, False])
calls = []
monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question, default)) or next(answers))
monkeypatch.setattr(
gateway,
"systemd_install",
lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", force, system, run_as_user, enable_on_startup)),
lambda force=False, system=False, run_as_user=None, enable_on_startup=True, non_interactive=False: calls.append(("install", force, system, run_as_user, enable_on_startup)),
)
monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start", system)))

Expand Down
4 changes: 4 additions & 0 deletions tests/hermes_cli/test_gateway_linger.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ def fake_run(cmd, check=False, **kwargs):
helper_calls = []
monkeypatch.setattr(gateway.subprocess, "run", fake_run)
monkeypatch.setattr(gateway, "_ensure_linger_enabled", lambda: helper_calls.append(True))
# This test doesn't exercise the temp-HERMES_HOME guard; the sandbox's
# own HERMES_HOME can legitimately resolve under a temp dir, which would
# otherwise make generate_systemd_unit()'s real output trip the guard.
monkeypatch.setattr(gateway, "_refuse_temp_home_service_write", lambda *a, **k: False)

gateway.systemd_install(force=False)

Expand Down
Loading
Loading