Skip to content
Closed
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
2 changes: 1 addition & 1 deletion hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4475,7 +4475,7 @@ def _ensure_hermes_home_managed(home: Path):
"category": "messaging",
},
"IRC_SERVER": {
"description": "IRC server hostname (e.g. irc.libera.chat)",
"description": "IRC server hostname (your own ircd, e.g. irc.example.invalid)",
"prompt": "IRC server",
"url": None,
"password": False,
Expand Down
15 changes: 12 additions & 3 deletions plugins/platforms/irc/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
irc:
enabled: true
extra:
server: irc.libera.chat
server: irc.example.invalid # use your own ircd — see docs
port: 6697
nickname: hermes-bot
channel: "#hermes"
Expand Down Expand Up @@ -560,10 +560,19 @@ def interactive_setup() -> None:
return

print_info("Connect Hermes to an IRC network. Uses Python stdlib — no extra packages needed.")
print_info(" Works with Libera.Chat, OFTC, your own ZNC/InspIRCd, etc.")
print_info(" Works with your own ircd (Ergo, InspIRCd, ZNC, etc.) or any network whose")
print_info(" policy explicitly allows LLM-driven agents.")
print_warning(
"Public IRC networks such as Libera.Chat ban LLM-driven agents under their "
"bot policy (https://libera.chat/news/bot-policy-update). Point Hermes at "
"an ircd you control — see https://ergo.chat/ or self-host an ircd via Docker."
)
print()

server = prompt("IRC server hostname (e.g. irc.libera.chat)", default=existing_server or "")
server = prompt(
"IRC server hostname (your own ircd, e.g. irc.example.invalid)",
default=existing_server or "",
)
if not server:
print_warning("Server is required — skipping IRC setup")
return
Expand Down
2 changes: 1 addition & 1 deletion plugins/platforms/irc/plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ author: Nous Research
# platform-plugin env var injector in ``hermes_cli/config.py``.
requires_env:
- name: IRC_SERVER
description: "IRC server hostname (e.g. irc.libera.chat)"
description: "IRC server hostname (your own ircd, e.g. irc.example.invalid)"
prompt: "IRC server"
password: false
- name: IRC_CHANNEL
Expand Down
4 changes: 2 additions & 2 deletions tests/gateway/test_irc_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_init_from_config_extra(self, monkeypatch):
cfg = PlatformConfig(
enabled=True,
extra={
"server": "irc.libera.chat",
"server": "irc.example.invalid",
"port": 6697,
"nickname": "hermes",
"channel": "#hermes-dev",
Expand All @@ -91,7 +91,7 @@ def test_init_from_config_extra(self, monkeypatch):
)
adapter = IRCAdapter(cfg)

assert adapter.server == "irc.libera.chat"
assert adapter.server == "irc.example.invalid"
assert adapter.port == 6697
assert adapter.nickname == "hermes"
assert adapter.channel == "#hermes-dev"
Expand Down
2 changes: 1 addition & 1 deletion tests/gateway/test_platform_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def test_from_dict_accepts_plugin_platform(self):
data = {
"platforms": {
"telegram": {"enabled": True, "token": "test-token"},
"irc": {"enabled": True, "extra": {"server": "irc.libera.chat"}},
"irc": {"enabled": True, "extra": {"server": "irc.example.invalid"}},
}
}
cfg = GatewayConfig.from_dict(data)
Expand Down
84 changes: 81 additions & 3 deletions tests/hermes_cli/test_setup_irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def test_irc_status_configured_when_env_set(self, monkeypatch):

plat = _register_irc_platform()
try:
monkeypatch.setenv("IRC_SERVER", "irc.libera.chat")
monkeypatch.setenv("IRC_SERVER", "irc.example.invalid")
monkeypatch.setenv("IRC_CHANNEL", "#hermes")
monkeypatch.setenv("IRC_NICKNAME", "hermes-bot")

Expand All @@ -115,7 +115,7 @@ def test_irc_status_partial_when_only_server_set(self, monkeypatch):
try:
monkeypatch.delenv("IRC_CHANNEL", raising=False)
monkeypatch.delenv("IRC_NICKNAME", raising=False)
monkeypatch.setenv("IRC_SERVER", "irc.libera.chat")
monkeypatch.setenv("IRC_SERVER", "irc.example.invalid")

status = gateway_mod._platform_status(plat)
assert status == "not configured"
Expand Down Expand Up @@ -225,7 +225,7 @@ def test_setup_gateway_irc_counts_as_messaging_platform(self, monkeypatch, capsy
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
_register_irc_platform()
try:
monkeypatch.setenv("IRC_SERVER", "irc.libera.chat")
monkeypatch.setenv("IRC_SERVER", "irc.example.invalid")
monkeypatch.setenv("IRC_CHANNEL", "#hermes")
monkeypatch.setenv("IRC_NICKNAME", "hermes-bot")

Expand Down Expand Up @@ -254,3 +254,81 @@ def test_setup_gateway_irc_counts_as_messaging_platform(self, monkeypatch, capsy
assert "Messaging platforms configured!" in out
finally:
_unregister_irc_platform()


# ── IRC policy regression: do not recommend public IRC networks (#61181) ────


class TestIRCPolicyWarningInSetup:
"""The interactive setup must warn that public IRC networks ban agents.

Libera.Chat's bot policy explicitly forbids LLM-driven / autonomous
agents (https://libera.chat/news/bot-policy-update). Hermes' setup
wizard must surface that warning and recommend a self-hosted ircd
instead of defaulting to ``irc.libera.chat``. Regression for #61181.
"""

def test_interactive_setup_warns_about_public_network_policy(self, monkeypatch, capsys, tmp_path):
"""interactive_setup() prints a policy warning that mentions Libera.Chat."""
# Ensure the wizard doesn't bail on the configured-server early-exit path.
monkeypatch.delenv("IRC_SERVER", raising=False)
monkeypatch.setenv("HERMES_HOME", str(tmp_path))

# interactive_setup() lazy-imports helpers from ``hermes_cli.setup``;
# patch them there so the wizard runs to completion without stdin.
import hermes_cli.setup as cli_setup
from plugins.platforms.irc import adapter as irc_adapter

monkeypatch.setattr(cli_setup, "prompt", lambda *args, **kwargs: kwargs.get("default") or "")
monkeypatch.setattr(cli_setup, "prompt_yes_no", lambda *args, **kwargs: kwargs.get("default", True))
# ``save_env_value`` writes to HERMES_HOME; stub to a no-op to keep
# the test hermetic.
monkeypatch.setattr(cli_setup, "save_env_value", lambda *args, **kwargs: None)
monkeypatch.setattr(cli_setup, "get_env_value", lambda *args, **kwargs: "")

irc_adapter.interactive_setup()

out = capsys.readouterr().out

# The setup wizard must surface a ban/policy warning…
assert "Libera.Chat" in out, f"policy warning missing in setup output: {out!r}"
assert "ban" in out.lower() or "forbid" in out.lower() or "policy" in out.lower(), (
f"expected a ban/policy warning, got: {out!r}"
)
# …and recommend a self-hosted ircd.
assert "ircd" in out.lower() or "self-host" in out.lower() or "ergo" in out.lower(), (
f"setup should recommend a self-hosted ircd, got: {out!r}"
)

def test_interactive_setup_default_prompt_does_not_recommend_libera(self, monkeypatch, capsys, tmp_path):
"""The default server prompt does NOT suggest irc.libera.chat any more."""
monkeypatch.delenv("IRC_SERVER", raising=False)
monkeypatch.setenv("HERMES_HOME", str(tmp_path))

import hermes_cli.setup as cli_setup
from plugins.platforms.irc import adapter as irc_adapter

captured = {}

def fake_prompt(question, default="", **_kw):
captured.setdefault("questions", []).append(question)
return default or ""

monkeypatch.setattr(cli_setup, "prompt", fake_prompt)
monkeypatch.setattr(cli_setup, "prompt_yes_no", lambda *args, **kwargs: kwargs.get("default", True))
monkeypatch.setattr(cli_setup, "save_env_value", lambda *args, **kwargs: None)
monkeypatch.setattr(cli_setup, "get_env_value", lambda *args, **kwargs: "")

irc_adapter.interactive_setup()

server_prompts = [q for q in captured["questions"] if "IRC server hostname" in q]
assert server_prompts, f"expected an IRC server prompt, got: {captured['questions']!r}"
# None of the server prompts should hard-default to Libera.Chat.
for q in server_prompts:
assert "irc.libera.chat" not in q, (
f"maintainer must not re-introduce Libera.Chat as the default: {q!r}"
)
# At least one prompt should signal a self-hosted ircd.
assert any("irc.example.invalid" in q for q in server_prompts), (
f"server prompt should recommend a private ircd, got: {server_prompts!r}"
)
2 changes: 1 addition & 1 deletion website/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ Connect Hermes to an IRC server. No external dependencies. See [the IRC messagin

| Variable | Description |
|----------|-------------|
| `IRC_SERVER` | IRC server hostname (e.g. `irc.libera.chat`). Required. |
| `IRC_SERVER` | IRC server hostname (your own ircd, e.g. `irc.example.invalid`). Required. |
| `IRC_CHANNEL` | Channel(s) to join (e.g. `#hermes`); comma-separate for multiple. Required. |
| `IRC_NICKNAME` | Bot nickname (default: `hermes-bot`). Required. |
| `IRC_PORT` | Server port (default: `6697` with TLS, `6667` without). |
Expand Down
10 changes: 6 additions & 4 deletions website/docs/user-guide/messaging/irc.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# IRC

The IRC adapter connects Hermes to any IRC server and relays messages between an IRC channel (or direct messages) and the agent. It speaks the IRC protocol over Python's stdlib `asyncio` — **no external dependencies, no SDK, no daemon**. It works with public networks like [Libera.Chat](https://libera.chat/) and any self-hosted ircd.
The IRC adapter connects Hermes to any IRC server and relays messages between an IRC channel (or direct messages) and the agent. It speaks the IRC protocol over Python's stdlib `asyncio` — **no external dependencies, no SDK, no daemon**. It works with any self-hosted ircd (Ergo, InspIRCd, ZNC, etc.).

> ⚠️ **Do not point Hermes at public IRC networks.** Most public networks — including [Libera.Chat](https://libera.chat/news/bot-policy-update) — explicitly ban LLM-driven / autonomous agents, and Hermes connecting to them can get both the bot and the operator klined. Run a self-hosted ircd instead (e.g. [Ergo](https://ergo.chat/) in a Docker container).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked Libera.Chat policy supports warning against autonomous Hermes clients there, but it does not establish that most public IRC networks explicitly ban them. Please scope this sentence to Libera.Chat or cite the broader claim.


IRC is plain text: there is no voice, image, file, thread, reaction, typing, or streaming support — replies are sent as `PRIVMSG` lines, with long messages split to fit the IRC line limit.

> Run `hermes gateway setup` and pick **IRC** for a guided walk-through.

## Prerequisites

- An IRC server to connect to (e.g. `irc.libera.chat`)
- An IRC server to connect to — preferably one you operate (e.g. a local [Ergo](https://ergo.chat/) instance at `irc.example.invalid`)
- A channel to join (e.g. `#hermes`) — comma-separate to join several
- A nickname for the bot (default: `hermes-bot`)
- Optional: a registered nick + NickServ password if your network requires identification
Expand All @@ -25,7 +27,7 @@ gateway:
irc:
enabled: true
extra:
server: irc.libera.chat
server: irc.example.invalid # your own ircd
port: 6697
nickname: hermes-bot
channel: "#hermes"
Expand All @@ -40,7 +42,7 @@ gateway:

| Variable | Required | Description |
|----------|:--------:|-------------|
| `IRC_SERVER` | ✅ | IRC server hostname (e.g. `irc.libera.chat`) |
| `IRC_SERVER` | ✅ | IRC server hostname (your own ircd, e.g. `irc.example.invalid`) |
| `IRC_CHANNEL` | ✅ | Channel(s) to join — comma-separate for multiple |
| `IRC_NICKNAME` | ✅ | Bot nickname (default: `hermes-bot`) |
| `IRC_PORT` | — | Server port (default: `6697` with TLS, `6667` without) |
Expand Down