mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 23:11:37 +08:00
Compare commits
1 Commits
skill/gith
...
feat/unaut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc1be62a9c |
@@ -32,6 +32,15 @@ def _coerce_bool(value: Any, default: bool = True) -> bool:
|
|||||||
return bool(value)
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_unauthorized_dm_behavior(value: Any, default: str = "pair") -> str:
|
||||||
|
"""Normalize unauthorized DM behavior to a supported value."""
|
||||||
|
if isinstance(value, str):
|
||||||
|
normalized = value.strip().lower()
|
||||||
|
if normalized in {"pair", "ignore"}:
|
||||||
|
return normalized
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
class Platform(Enum):
|
class Platform(Enum):
|
||||||
"""Supported messaging platforms."""
|
"""Supported messaging platforms."""
|
||||||
LOCAL = "local"
|
LOCAL = "local"
|
||||||
@@ -215,6 +224,9 @@ class GatewayConfig:
|
|||||||
# Session isolation in shared chats
|
# Session isolation in shared chats
|
||||||
group_sessions_per_user: bool = True # Isolate group/channel sessions per participant when user IDs are available
|
group_sessions_per_user: bool = True # Isolate group/channel sessions per participant when user IDs are available
|
||||||
|
|
||||||
|
# Unauthorized DM policy
|
||||||
|
unauthorized_dm_behavior: str = "pair" # "pair" or "ignore"
|
||||||
|
|
||||||
# Streaming configuration
|
# Streaming configuration
|
||||||
streaming: StreamingConfig = field(default_factory=StreamingConfig)
|
streaming: StreamingConfig = field(default_factory=StreamingConfig)
|
||||||
|
|
||||||
@@ -289,6 +301,7 @@ class GatewayConfig:
|
|||||||
"always_log_local": self.always_log_local,
|
"always_log_local": self.always_log_local,
|
||||||
"stt_enabled": self.stt_enabled,
|
"stt_enabled": self.stt_enabled,
|
||||||
"group_sessions_per_user": self.group_sessions_per_user,
|
"group_sessions_per_user": self.group_sessions_per_user,
|
||||||
|
"unauthorized_dm_behavior": self.unauthorized_dm_behavior,
|
||||||
"streaming": self.streaming.to_dict(),
|
"streaming": self.streaming.to_dict(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +344,10 @@ class GatewayConfig:
|
|||||||
stt_enabled = data.get("stt", {}).get("enabled") if isinstance(data.get("stt"), dict) else None
|
stt_enabled = data.get("stt", {}).get("enabled") if isinstance(data.get("stt"), dict) else None
|
||||||
|
|
||||||
group_sessions_per_user = data.get("group_sessions_per_user")
|
group_sessions_per_user = data.get("group_sessions_per_user")
|
||||||
|
unauthorized_dm_behavior = _normalize_unauthorized_dm_behavior(
|
||||||
|
data.get("unauthorized_dm_behavior"),
|
||||||
|
"pair",
|
||||||
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
platforms=platforms,
|
platforms=platforms,
|
||||||
@@ -343,9 +360,21 @@ class GatewayConfig:
|
|||||||
always_log_local=data.get("always_log_local", True),
|
always_log_local=data.get("always_log_local", True),
|
||||||
stt_enabled=_coerce_bool(stt_enabled, True),
|
stt_enabled=_coerce_bool(stt_enabled, True),
|
||||||
group_sessions_per_user=_coerce_bool(group_sessions_per_user, True),
|
group_sessions_per_user=_coerce_bool(group_sessions_per_user, True),
|
||||||
|
unauthorized_dm_behavior=unauthorized_dm_behavior,
|
||||||
streaming=StreamingConfig.from_dict(data.get("streaming", {})),
|
streaming=StreamingConfig.from_dict(data.get("streaming", {})),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_unauthorized_dm_behavior(self, platform: Optional[Platform] = None) -> str:
|
||||||
|
"""Return the effective unauthorized-DM behavior for a platform."""
|
||||||
|
if platform:
|
||||||
|
platform_cfg = self.platforms.get(platform)
|
||||||
|
if platform_cfg and "unauthorized_dm_behavior" in platform_cfg.extra:
|
||||||
|
return _normalize_unauthorized_dm_behavior(
|
||||||
|
platform_cfg.extra.get("unauthorized_dm_behavior"),
|
||||||
|
self.unauthorized_dm_behavior,
|
||||||
|
)
|
||||||
|
return self.unauthorized_dm_behavior
|
||||||
|
|
||||||
|
|
||||||
def load_gateway_config() -> GatewayConfig:
|
def load_gateway_config() -> GatewayConfig:
|
||||||
"""
|
"""
|
||||||
@@ -416,6 +445,38 @@ def load_gateway_config() -> GatewayConfig:
|
|||||||
if "always_log_local" in yaml_cfg:
|
if "always_log_local" in yaml_cfg:
|
||||||
gw_data["always_log_local"] = yaml_cfg["always_log_local"]
|
gw_data["always_log_local"] = yaml_cfg["always_log_local"]
|
||||||
|
|
||||||
|
if "unauthorized_dm_behavior" in yaml_cfg:
|
||||||
|
gw_data["unauthorized_dm_behavior"] = _normalize_unauthorized_dm_behavior(
|
||||||
|
yaml_cfg.get("unauthorized_dm_behavior"),
|
||||||
|
"pair",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bridge per-platform unauthorized_dm_behavior from config.yaml
|
||||||
|
platforms_data = gw_data.setdefault("platforms", {})
|
||||||
|
if not isinstance(platforms_data, dict):
|
||||||
|
platforms_data = {}
|
||||||
|
gw_data["platforms"] = platforms_data
|
||||||
|
for plat in Platform:
|
||||||
|
if plat == Platform.LOCAL:
|
||||||
|
continue
|
||||||
|
platform_cfg = yaml_cfg.get(plat.value)
|
||||||
|
if not isinstance(platform_cfg, dict):
|
||||||
|
continue
|
||||||
|
if "unauthorized_dm_behavior" not in platform_cfg:
|
||||||
|
continue
|
||||||
|
plat_data = platforms_data.setdefault(plat.value, {})
|
||||||
|
if not isinstance(plat_data, dict):
|
||||||
|
plat_data = {}
|
||||||
|
platforms_data[plat.value] = plat_data
|
||||||
|
extra = plat_data.setdefault("extra", {})
|
||||||
|
if not isinstance(extra, dict):
|
||||||
|
extra = {}
|
||||||
|
plat_data["extra"] = extra
|
||||||
|
extra["unauthorized_dm_behavior"] = _normalize_unauthorized_dm_behavior(
|
||||||
|
platform_cfg.get("unauthorized_dm_behavior"),
|
||||||
|
gw_data.get("unauthorized_dm_behavior", "pair"),
|
||||||
|
)
|
||||||
|
|
||||||
# Discord settings → env vars (env vars take precedence)
|
# Discord settings → env vars (env vars take precedence)
|
||||||
discord_cfg = yaml_cfg.get("discord", {})
|
discord_cfg = yaml_cfg.get("discord", {})
|
||||||
if isinstance(discord_cfg, dict):
|
if isinstance(discord_cfg, dict):
|
||||||
|
|||||||
@@ -1257,6 +1257,13 @@ class GatewayRunner:
|
|||||||
if "@" in user_id:
|
if "@" in user_id:
|
||||||
check_ids.add(user_id.split("@")[0])
|
check_ids.add(user_id.split("@")[0])
|
||||||
return bool(check_ids & allowed_ids)
|
return bool(check_ids & allowed_ids)
|
||||||
|
|
||||||
|
def _get_unauthorized_dm_behavior(self, platform: Optional[Platform]) -> str:
|
||||||
|
"""Return how unauthorized DMs should be handled for a platform."""
|
||||||
|
config = getattr(self, "config", None)
|
||||||
|
if config and hasattr(config, "get_unauthorized_dm_behavior"):
|
||||||
|
return config.get_unauthorized_dm_behavior(platform)
|
||||||
|
return "pair"
|
||||||
|
|
||||||
async def _handle_message(self, event: MessageEvent) -> Optional[str]:
|
async def _handle_message(self, event: MessageEvent) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@@ -1277,7 +1284,7 @@ class GatewayRunner:
|
|||||||
if not self._is_user_authorized(source):
|
if not self._is_user_authorized(source):
|
||||||
logger.warning("Unauthorized user: %s (%s) on %s", source.user_id, source.user_name, source.platform.value)
|
logger.warning("Unauthorized user: %s (%s) on %s", source.user_id, source.user_name, source.platform.value)
|
||||||
# In DMs: offer pairing code. In groups: silently ignore.
|
# In DMs: offer pairing code. In groups: silently ignore.
|
||||||
if source.chat_type == "dm":
|
if source.chat_type == "dm" and self._get_unauthorized_dm_behavior(source.platform) == "pair":
|
||||||
platform_name = source.platform.value if source.platform else "unknown"
|
platform_name = source.platform.value if source.platform else "unknown"
|
||||||
code = self.pairing_store.generate_code(
|
code = self.pairing_store.generate_code(
|
||||||
platform_name, source.user_id, source.user_name or ""
|
platform_name, source.user_id, source.user_name or ""
|
||||||
|
|||||||
@@ -115,6 +115,22 @@ class TestGatewayConfigRoundtrip:
|
|||||||
assert restored.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}}
|
assert restored.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}}
|
||||||
assert restored.group_sessions_per_user is False
|
assert restored.group_sessions_per_user is False
|
||||||
|
|
||||||
|
def test_roundtrip_preserves_unauthorized_dm_behavior(self):
|
||||||
|
config = GatewayConfig(
|
||||||
|
unauthorized_dm_behavior="ignore",
|
||||||
|
platforms={
|
||||||
|
Platform.WHATSAPP: PlatformConfig(
|
||||||
|
enabled=True,
|
||||||
|
extra={"unauthorized_dm_behavior": "pair"},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
restored = GatewayConfig.from_dict(config.to_dict())
|
||||||
|
|
||||||
|
assert restored.unauthorized_dm_behavior == "ignore"
|
||||||
|
assert restored.platforms[Platform.WHATSAPP].extra["unauthorized_dm_behavior"] == "pair"
|
||||||
|
|
||||||
|
|
||||||
class TestLoadGatewayConfig:
|
class TestLoadGatewayConfig:
|
||||||
def test_bridges_quick_commands_from_config_yaml(self, tmp_path, monkeypatch):
|
def test_bridges_quick_commands_from_config_yaml(self, tmp_path, monkeypatch):
|
||||||
@@ -158,3 +174,21 @@ class TestLoadGatewayConfig:
|
|||||||
config = load_gateway_config()
|
config = load_gateway_config()
|
||||||
|
|
||||||
assert config.quick_commands == {}
|
assert config.quick_commands == {}
|
||||||
|
|
||||||
|
def test_bridges_unauthorized_dm_behavior_from_config_yaml(self, tmp_path, monkeypatch):
|
||||||
|
hermes_home = tmp_path / ".hermes"
|
||||||
|
hermes_home.mkdir()
|
||||||
|
config_path = hermes_home / "config.yaml"
|
||||||
|
config_path.write_text(
|
||||||
|
"unauthorized_dm_behavior: ignore\n"
|
||||||
|
"whatsapp:\n"
|
||||||
|
" unauthorized_dm_behavior: pair\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||||
|
|
||||||
|
config = load_gateway_config()
|
||||||
|
|
||||||
|
assert config.unauthorized_dm_behavior == "ignore"
|
||||||
|
assert config.platforms[Platform.WHATSAPP].extra["unauthorized_dm_behavior"] == "pair"
|
||||||
|
|||||||
137
tests/gateway/test_unauthorized_dm_behavior.py
Normal file
137
tests/gateway/test_unauthorized_dm_behavior.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
||||||
|
from gateway.platforms.base import MessageEvent
|
||||||
|
from gateway.session import SessionSource
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_auth_env(monkeypatch) -> None:
|
||||||
|
for key in (
|
||||||
|
"TELEGRAM_ALLOWED_USERS",
|
||||||
|
"DISCORD_ALLOWED_USERS",
|
||||||
|
"WHATSAPP_ALLOWED_USERS",
|
||||||
|
"SLACK_ALLOWED_USERS",
|
||||||
|
"SIGNAL_ALLOWED_USERS",
|
||||||
|
"EMAIL_ALLOWED_USERS",
|
||||||
|
"SMS_ALLOWED_USERS",
|
||||||
|
"MATTERMOST_ALLOWED_USERS",
|
||||||
|
"MATRIX_ALLOWED_USERS",
|
||||||
|
"DINGTALK_ALLOWED_USERS",
|
||||||
|
"GATEWAY_ALLOWED_USERS",
|
||||||
|
"TELEGRAM_ALLOW_ALL_USERS",
|
||||||
|
"DISCORD_ALLOW_ALL_USERS",
|
||||||
|
"WHATSAPP_ALLOW_ALL_USERS",
|
||||||
|
"SLACK_ALLOW_ALL_USERS",
|
||||||
|
"SIGNAL_ALLOW_ALL_USERS",
|
||||||
|
"EMAIL_ALLOW_ALL_USERS",
|
||||||
|
"SMS_ALLOW_ALL_USERS",
|
||||||
|
"MATTERMOST_ALLOW_ALL_USERS",
|
||||||
|
"MATRIX_ALLOW_ALL_USERS",
|
||||||
|
"DINGTALK_ALLOW_ALL_USERS",
|
||||||
|
"GATEWAY_ALLOW_ALL_USERS",
|
||||||
|
):
|
||||||
|
monkeypatch.delenv(key, raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_event(platform: Platform, user_id: str, chat_id: str) -> MessageEvent:
|
||||||
|
return MessageEvent(
|
||||||
|
text="hello",
|
||||||
|
message_id="m1",
|
||||||
|
source=SessionSource(
|
||||||
|
platform=platform,
|
||||||
|
user_id=user_id,
|
||||||
|
chat_id=chat_id,
|
||||||
|
user_name="tester",
|
||||||
|
chat_type="dm",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_runner(platform: Platform, config: GatewayConfig):
|
||||||
|
from gateway.run import GatewayRunner
|
||||||
|
|
||||||
|
runner = object.__new__(GatewayRunner)
|
||||||
|
runner.config = config
|
||||||
|
adapter = SimpleNamespace(send=AsyncMock())
|
||||||
|
runner.adapters = {platform: adapter}
|
||||||
|
runner.pairing_store = MagicMock()
|
||||||
|
runner.pairing_store.is_approved.return_value = False
|
||||||
|
return runner, adapter
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unauthorized_dm_pairs_by_default(monkeypatch):
|
||||||
|
_clear_auth_env(monkeypatch)
|
||||||
|
config = GatewayConfig(
|
||||||
|
platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)},
|
||||||
|
)
|
||||||
|
runner, adapter = _make_runner(Platform.WHATSAPP, config)
|
||||||
|
runner.pairing_store.generate_code.return_value = "ABC12DEF"
|
||||||
|
|
||||||
|
result = await runner._handle_message(
|
||||||
|
_make_event(
|
||||||
|
Platform.WHATSAPP,
|
||||||
|
"15551234567@s.whatsapp.net",
|
||||||
|
"15551234567@s.whatsapp.net",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
runner.pairing_store.generate_code.assert_called_once_with(
|
||||||
|
"whatsapp",
|
||||||
|
"15551234567@s.whatsapp.net",
|
||||||
|
"tester",
|
||||||
|
)
|
||||||
|
adapter.send.assert_awaited_once()
|
||||||
|
assert "ABC12DEF" in adapter.send.await_args.args[1]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unauthorized_whatsapp_dm_can_be_ignored(monkeypatch):
|
||||||
|
_clear_auth_env(monkeypatch)
|
||||||
|
config = GatewayConfig(
|
||||||
|
platforms={
|
||||||
|
Platform.WHATSAPP: PlatformConfig(
|
||||||
|
enabled=True,
|
||||||
|
extra={"unauthorized_dm_behavior": "ignore"},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
runner, adapter = _make_runner(Platform.WHATSAPP, config)
|
||||||
|
|
||||||
|
result = await runner._handle_message(
|
||||||
|
_make_event(
|
||||||
|
Platform.WHATSAPP,
|
||||||
|
"15551234567@s.whatsapp.net",
|
||||||
|
"15551234567@s.whatsapp.net",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
runner.pairing_store.generate_code.assert_not_called()
|
||||||
|
adapter.send.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_global_ignore_suppresses_pairing_reply(monkeypatch):
|
||||||
|
_clear_auth_env(monkeypatch)
|
||||||
|
config = GatewayConfig(
|
||||||
|
unauthorized_dm_behavior="ignore",
|
||||||
|
platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")},
|
||||||
|
)
|
||||||
|
runner, adapter = _make_runner(Platform.TELEGRAM, config)
|
||||||
|
|
||||||
|
result = await runner._handle_message(
|
||||||
|
_make_event(
|
||||||
|
Platform.TELEGRAM,
|
||||||
|
"12345",
|
||||||
|
"12345",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
runner.pairing_store.generate_code.assert_not_called()
|
||||||
|
adapter.send.assert_not_awaited()
|
||||||
@@ -1090,6 +1090,21 @@ group_sessions_per_user: true # true = per-user isolation in groups/channels, f
|
|||||||
|
|
||||||
For the behavior details and examples, see [Sessions](/docs/user-guide/sessions) and the [Discord guide](/docs/user-guide/messaging/discord).
|
For the behavior details and examples, see [Sessions](/docs/user-guide/sessions) and the [Discord guide](/docs/user-guide/messaging/discord).
|
||||||
|
|
||||||
|
## Unauthorized DM Behavior
|
||||||
|
|
||||||
|
Control what Hermes does when an unknown user sends a direct message:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
unauthorized_dm_behavior: pair
|
||||||
|
|
||||||
|
whatsapp:
|
||||||
|
unauthorized_dm_behavior: ignore
|
||||||
|
```
|
||||||
|
|
||||||
|
- `pair` is the default. Hermes denies access, but replies with a one-time pairing code in DMs.
|
||||||
|
- `ignore` silently drops unauthorized DMs.
|
||||||
|
- Platform sections override the global default, so you can keep pairing enabled broadly while making one platform quieter.
|
||||||
|
|
||||||
## Quick Commands
|
## Quick Commands
|
||||||
|
|
||||||
Define custom commands that run shell commands without invoking the LLM — zero token usage, instant execution. Especially useful from messaging platforms (Telegram, Discord, etc.) for quick server checks or utility scripts.
|
Define custom commands that run shell commands without invoking the LLM — zero token usage, instant execution. Especially useful from messaging platforms (Telegram, Discord, etc.) for quick server checks or utility scripts.
|
||||||
|
|||||||
@@ -97,6 +97,18 @@ WHATSAPP_MODE=bot # "bot" or "self-chat"
|
|||||||
WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers (with country code, no +)
|
WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers (with country code, no +)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional behavior settings in `~/.hermes/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
unauthorized_dm_behavior: pair
|
||||||
|
|
||||||
|
whatsapp:
|
||||||
|
unauthorized_dm_behavior: ignore
|
||||||
|
```
|
||||||
|
|
||||||
|
- `unauthorized_dm_behavior: pair` is the global default. Unknown DM senders get a pairing code.
|
||||||
|
- `whatsapp.unauthorized_dm_behavior: ignore` makes WhatsApp stay silent for unauthorized DMs, which is usually the better choice for a private number.
|
||||||
|
|
||||||
Then start the gateway:
|
Then start the gateway:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -162,6 +174,7 @@ whatsapp:
|
|||||||
| **Bridge crashes or reconnect loops** | Restart the gateway, update Hermes, and re-pair if the session was invalidated by a WhatsApp protocol change. |
|
| **Bridge crashes or reconnect loops** | Restart the gateway, update Hermes, and re-pair if the session was invalidated by a WhatsApp protocol change. |
|
||||||
| **Bot stops working after WhatsApp update** | Update Hermes to get the latest bridge version, then re-pair. |
|
| **Bot stops working after WhatsApp update** | Update Hermes to get the latest bridge version, then re-pair. |
|
||||||
| **Messages not being received** | Verify `WHATSAPP_ALLOWED_USERS` includes the sender's number (with country code, no `+` or spaces). |
|
| **Messages not being received** | Verify `WHATSAPP_ALLOWED_USERS` includes the sender's number (with country code, no `+` or spaces). |
|
||||||
|
| **Bot replies to strangers with a pairing code** | Set `whatsapp.unauthorized_dm_behavior: ignore` in `~/.hermes/config.yaml` if you want unauthorized DMs to be silently ignored instead. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -173,6 +186,13 @@ of authorized users. Without this setting, the gateway will **deny all incoming
|
|||||||
safety measure.
|
safety measure.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
By default, unauthorized DMs still receive a pairing code reply. If you want a private WhatsApp number to stay completely silent to strangers, set:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
whatsapp:
|
||||||
|
unauthorized_dm_behavior: ignore
|
||||||
|
```
|
||||||
|
|
||||||
- The `~/.hermes/whatsapp/session` directory contains full session credentials — protect it like a password
|
- The `~/.hermes/whatsapp/session` directory contains full session credentials — protect it like a password
|
||||||
- Set file permissions: `chmod 700 ~/.hermes/whatsapp/session`
|
- Set file permissions: `chmod 700 ~/.hermes/whatsapp/session`
|
||||||
- Use a **dedicated phone number** for the bot to isolate risk from your personal account
|
- Use a **dedicated phone number** for the bot to isolate risk from your personal account
|
||||||
|
|||||||
@@ -151,6 +151,19 @@ For more flexible authorization, Hermes includes a code-based pairing system. In
|
|||||||
3. The bot owner runs `hermes pairing approve <platform> <code>` on the CLI
|
3. The bot owner runs `hermes pairing approve <platform> <code>` on the CLI
|
||||||
4. The user is permanently approved for that platform
|
4. The user is permanently approved for that platform
|
||||||
|
|
||||||
|
Control how unauthorized direct messages are handled in `~/.hermes/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
unauthorized_dm_behavior: pair
|
||||||
|
|
||||||
|
whatsapp:
|
||||||
|
unauthorized_dm_behavior: ignore
|
||||||
|
```
|
||||||
|
|
||||||
|
- `pair` is the default. Unauthorized DMs get a pairing code reply.
|
||||||
|
- `ignore` silently drops unauthorized DMs.
|
||||||
|
- Platform sections override the global default, so you can keep pairing on Telegram while keeping WhatsApp silent.
|
||||||
|
|
||||||
**Security features** (based on OWASP + NIST SP 800-63-4 guidance):
|
**Security features** (based on OWASP + NIST SP 800-63-4 guidance):
|
||||||
|
|
||||||
| Feature | Details |
|
| Feature | Details |
|
||||||
|
|||||||
Reference in New Issue
Block a user