mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 07:51:45 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e662ff6f |
@@ -2221,6 +2221,13 @@ class GatewayRunner:
|
||||
# are system-generated and must skip user authorization.
|
||||
if getattr(event, "internal", False):
|
||||
pass
|
||||
elif source.user_id is None:
|
||||
# Messages with no user identity (Telegram service messages,
|
||||
# channel forwards, anonymous admin actions) cannot be
|
||||
# authorized — drop silently instead of triggering the pairing
|
||||
# flow with a None user_id.
|
||||
logger.debug("Ignoring message with no user_id from %s", source.platform.value)
|
||||
return None
|
||||
elif not self._is_user_authorized(source):
|
||||
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.
|
||||
@@ -6597,6 +6604,8 @@ class GatewayRunner:
|
||||
chat_id=context.source.chat_id,
|
||||
chat_name=context.source.chat_name or "",
|
||||
thread_id=str(context.source.thread_id) if context.source.thread_id else "",
|
||||
user_id=str(context.source.user_id) if context.source.user_id else "",
|
||||
user_name=str(context.source.user_name) if context.source.user_name else "",
|
||||
)
|
||||
|
||||
def _clear_session_env(self, tokens: list) -> None:
|
||||
@@ -6809,6 +6818,8 @@ class GatewayRunner:
|
||||
platform_name = watcher.get("platform", "")
|
||||
chat_id = watcher.get("chat_id", "")
|
||||
thread_id = watcher.get("thread_id", "")
|
||||
user_id = watcher.get("user_id", "")
|
||||
user_name = watcher.get("user_name", "")
|
||||
agent_notify = watcher.get("notify_on_complete", False)
|
||||
notify_mode = self._load_background_notifications_mode()
|
||||
|
||||
@@ -6864,6 +6875,8 @@ class GatewayRunner:
|
||||
platform=_platform_enum,
|
||||
chat_id=chat_id,
|
||||
thread_id=thread_id or None,
|
||||
user_id=user_id or None,
|
||||
user_name=user_name or None,
|
||||
)
|
||||
synth_event = MessageEvent(
|
||||
text=synth_text,
|
||||
|
||||
@@ -46,12 +46,16 @@ _SESSION_PLATFORM: ContextVar[str] = ContextVar("HERMES_SESSION_PLATFORM", defau
|
||||
_SESSION_CHAT_ID: ContextVar[str] = ContextVar("HERMES_SESSION_CHAT_ID", default="")
|
||||
_SESSION_CHAT_NAME: ContextVar[str] = ContextVar("HERMES_SESSION_CHAT_NAME", default="")
|
||||
_SESSION_THREAD_ID: ContextVar[str] = ContextVar("HERMES_SESSION_THREAD_ID", default="")
|
||||
_SESSION_USER_ID: ContextVar[str] = ContextVar("HERMES_SESSION_USER_ID", default="")
|
||||
_SESSION_USER_NAME: ContextVar[str] = ContextVar("HERMES_SESSION_USER_NAME", default="")
|
||||
|
||||
_VAR_MAP = {
|
||||
"HERMES_SESSION_PLATFORM": _SESSION_PLATFORM,
|
||||
"HERMES_SESSION_CHAT_ID": _SESSION_CHAT_ID,
|
||||
"HERMES_SESSION_CHAT_NAME": _SESSION_CHAT_NAME,
|
||||
"HERMES_SESSION_THREAD_ID": _SESSION_THREAD_ID,
|
||||
"HERMES_SESSION_USER_ID": _SESSION_USER_ID,
|
||||
"HERMES_SESSION_USER_NAME": _SESSION_USER_NAME,
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +64,8 @@ def set_session_vars(
|
||||
chat_id: str = "",
|
||||
chat_name: str = "",
|
||||
thread_id: str = "",
|
||||
user_id: str = "",
|
||||
user_name: str = "",
|
||||
) -> list:
|
||||
"""Set all session context variables and return reset tokens.
|
||||
|
||||
@@ -74,6 +80,8 @@ def set_session_vars(
|
||||
_SESSION_CHAT_ID.set(chat_id),
|
||||
_SESSION_CHAT_NAME.set(chat_name),
|
||||
_SESSION_THREAD_ID.set(thread_id),
|
||||
_SESSION_USER_ID.set(user_id),
|
||||
_SESSION_USER_NAME.set(user_name),
|
||||
]
|
||||
return tokens
|
||||
|
||||
@@ -87,6 +95,8 @@ def clear_session_vars(tokens: list) -> None:
|
||||
_SESSION_CHAT_ID,
|
||||
_SESSION_CHAT_NAME,
|
||||
_SESSION_THREAD_ID,
|
||||
_SESSION_USER_ID,
|
||||
_SESSION_USER_NAME,
|
||||
]
|
||||
for var, token in zip(vars_in_order, tokens):
|
||||
var.reset(token)
|
||||
|
||||
@@ -195,6 +195,105 @@ async def test_internal_event_does_not_trigger_pairing(monkeypatch, tmp_path):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notify_on_complete_preserves_user_identity(monkeypatch, tmp_path):
|
||||
"""Synthetic completion event should carry user_id and user_name from the watcher."""
|
||||
import tools.process_registry as pr_module
|
||||
|
||||
sessions = [
|
||||
SimpleNamespace(
|
||||
output_buffer="done\n", exited=True, exit_code=0, command="echo test"
|
||||
),
|
||||
]
|
||||
monkeypatch.setattr(pr_module, "process_registry", _FakeRegistry(sessions))
|
||||
|
||||
async def _instant_sleep(*_a, **_kw):
|
||||
pass
|
||||
monkeypatch.setattr(asyncio, "sleep", _instant_sleep)
|
||||
|
||||
runner = _build_runner(monkeypatch, tmp_path)
|
||||
adapter = runner.adapters[Platform.DISCORD]
|
||||
|
||||
watcher = _watcher_dict_with_notify()
|
||||
watcher["user_id"] = "user-42"
|
||||
watcher["user_name"] = "alice"
|
||||
|
||||
await runner._run_process_watcher(watcher)
|
||||
|
||||
assert adapter.handle_message.await_count == 1
|
||||
event = adapter.handle_message.await_args.args[0]
|
||||
assert event.source.user_id == "user-42"
|
||||
assert event.source.user_name == "alice"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_none_user_id_skips_pairing(monkeypatch, tmp_path):
|
||||
"""A non-internal event with user_id=None should be silently dropped."""
|
||||
import gateway.run as gateway_run
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||
(tmp_path / "config.yaml").write_text("", encoding="utf-8")
|
||||
|
||||
runner = GatewayRunner(GatewayConfig())
|
||||
adapter = SimpleNamespace(send=AsyncMock())
|
||||
runner.adapters[Platform.TELEGRAM] = adapter
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
chat_id="123",
|
||||
chat_type="dm",
|
||||
user_id=None,
|
||||
)
|
||||
event = MessageEvent(
|
||||
text="service message",
|
||||
source=source,
|
||||
internal=False,
|
||||
)
|
||||
|
||||
result = await runner._handle_message(event)
|
||||
|
||||
# Should return None (dropped) and NOT send any pairing message
|
||||
assert result is None
|
||||
assert adapter.send.await_count == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_none_user_id_does_not_generate_pairing_code(monkeypatch, tmp_path):
|
||||
"""A message with user_id=None must never call generate_code."""
|
||||
import gateway.run as gateway_run
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||
(tmp_path / "config.yaml").write_text("", encoding="utf-8")
|
||||
|
||||
runner = GatewayRunner(GatewayConfig())
|
||||
adapter = SimpleNamespace(send=AsyncMock())
|
||||
runner.adapters[Platform.DISCORD] = adapter
|
||||
|
||||
generate_called = False
|
||||
original_generate = runner.pairing_store.generate_code
|
||||
|
||||
def tracking_generate(*args, **kwargs):
|
||||
nonlocal generate_called
|
||||
generate_called = True
|
||||
return original_generate(*args, **kwargs)
|
||||
|
||||
runner.pairing_store.generate_code = tracking_generate
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="456",
|
||||
chat_type="dm",
|
||||
user_id=None,
|
||||
)
|
||||
event = MessageEvent(text="anonymous", source=source, internal=False)
|
||||
|
||||
await runner._handle_message(event)
|
||||
|
||||
assert not generate_called, (
|
||||
"Pairing code should NOT be generated for messages with user_id=None"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_internal_event_without_user_triggers_pairing(monkeypatch, tmp_path):
|
||||
"""Verify the normal (non-internal) path still triggers pairing for unknown users."""
|
||||
|
||||
@@ -18,6 +18,8 @@ def test_set_session_env_sets_contextvars(monkeypatch):
|
||||
chat_id="-1001",
|
||||
chat_name="Group",
|
||||
chat_type="group",
|
||||
user_id="123456",
|
||||
user_name="alice",
|
||||
thread_id="17585",
|
||||
)
|
||||
context = SessionContext(source=source, connected_platforms=[], home_channels={})
|
||||
@@ -25,6 +27,8 @@ def test_set_session_env_sets_contextvars(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_CHAT_ID", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_CHAT_NAME", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_USER_ID", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_USER_NAME", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_THREAD_ID", raising=False)
|
||||
|
||||
tokens = runner._set_session_env(context)
|
||||
@@ -33,6 +37,8 @@ def test_set_session_env_sets_contextvars(monkeypatch):
|
||||
assert get_session_env("HERMES_SESSION_PLATFORM") == "telegram"
|
||||
assert get_session_env("HERMES_SESSION_CHAT_ID") == "-1001"
|
||||
assert get_session_env("HERMES_SESSION_CHAT_NAME") == "Group"
|
||||
assert get_session_env("HERMES_SESSION_USER_ID") == "123456"
|
||||
assert get_session_env("HERMES_SESSION_USER_NAME") == "alice"
|
||||
assert get_session_env("HERMES_SESSION_THREAD_ID") == "17585"
|
||||
|
||||
# os.environ should NOT be touched
|
||||
@@ -50,6 +56,8 @@ def test_clear_session_env_restores_previous_state(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_CHAT_ID", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_CHAT_NAME", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_USER_ID", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_USER_NAME", raising=False)
|
||||
monkeypatch.delenv("HERMES_SESSION_THREAD_ID", raising=False)
|
||||
|
||||
source = SessionSource(
|
||||
@@ -57,12 +65,15 @@ def test_clear_session_env_restores_previous_state(monkeypatch):
|
||||
chat_id="-1001",
|
||||
chat_name="Group",
|
||||
chat_type="group",
|
||||
user_id="123456",
|
||||
user_name="alice",
|
||||
thread_id="17585",
|
||||
)
|
||||
context = SessionContext(source=source, connected_platforms=[], home_channels={})
|
||||
|
||||
tokens = runner._set_session_env(context)
|
||||
assert get_session_env("HERMES_SESSION_PLATFORM") == "telegram"
|
||||
assert get_session_env("HERMES_SESSION_USER_ID") == "123456"
|
||||
|
||||
runner._clear_session_env(tokens)
|
||||
|
||||
@@ -70,6 +81,8 @@ def test_clear_session_env_restores_previous_state(monkeypatch):
|
||||
assert get_session_env("HERMES_SESSION_PLATFORM") == ""
|
||||
assert get_session_env("HERMES_SESSION_CHAT_ID") == ""
|
||||
assert get_session_env("HERMES_SESSION_CHAT_NAME") == ""
|
||||
assert get_session_env("HERMES_SESSION_USER_ID") == ""
|
||||
assert get_session_env("HERMES_SESSION_USER_NAME") == ""
|
||||
assert get_session_env("HERMES_SESSION_THREAD_ID") == ""
|
||||
|
||||
|
||||
|
||||
@@ -227,6 +227,8 @@ class TestCheckpointNotify:
|
||||
"session_key": "sk1",
|
||||
"watcher_platform": "telegram",
|
||||
"watcher_chat_id": "123",
|
||||
"watcher_user_id": "u123",
|
||||
"watcher_user_name": "alice",
|
||||
"watcher_thread_id": "42",
|
||||
"watcher_interval": 5,
|
||||
"notify_on_complete": True,
|
||||
@@ -236,6 +238,8 @@ class TestCheckpointNotify:
|
||||
assert recovered == 1
|
||||
assert len(registry.pending_watchers) == 1
|
||||
assert registry.pending_watchers[0]["notify_on_complete"] is True
|
||||
assert registry.pending_watchers[0]["user_id"] == "u123"
|
||||
assert registry.pending_watchers[0]["user_name"] == "alice"
|
||||
|
||||
def test_recover_defaults_false(self, registry, tmp_path):
|
||||
"""Old checkpoint entries without the field default to False."""
|
||||
|
||||
@@ -438,6 +438,8 @@ class TestCheckpoint:
|
||||
s = _make_session()
|
||||
s.watcher_platform = "telegram"
|
||||
s.watcher_chat_id = "999"
|
||||
s.watcher_user_id = "u123"
|
||||
s.watcher_user_name = "alice"
|
||||
s.watcher_thread_id = "42"
|
||||
s.watcher_interval = 60
|
||||
registry._running[s.id] = s
|
||||
@@ -447,6 +449,8 @@ class TestCheckpoint:
|
||||
assert len(data) == 1
|
||||
assert data[0]["watcher_platform"] == "telegram"
|
||||
assert data[0]["watcher_chat_id"] == "999"
|
||||
assert data[0]["watcher_user_id"] == "u123"
|
||||
assert data[0]["watcher_user_name"] == "alice"
|
||||
assert data[0]["watcher_thread_id"] == "42"
|
||||
assert data[0]["watcher_interval"] == 60
|
||||
|
||||
@@ -460,6 +464,8 @@ class TestCheckpoint:
|
||||
"session_key": "sk1",
|
||||
"watcher_platform": "telegram",
|
||||
"watcher_chat_id": "123",
|
||||
"watcher_user_id": "u123",
|
||||
"watcher_user_name": "alice",
|
||||
"watcher_thread_id": "42",
|
||||
"watcher_interval": 60,
|
||||
}]))
|
||||
@@ -471,6 +477,8 @@ class TestCheckpoint:
|
||||
assert w["session_id"] == "proc_live"
|
||||
assert w["platform"] == "telegram"
|
||||
assert w["chat_id"] == "123"
|
||||
assert w["user_id"] == "u123"
|
||||
assert w["user_name"] == "alice"
|
||||
assert w["thread_id"] == "42"
|
||||
assert w["check_interval"] == 60
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ class ProcessSession:
|
||||
# Watcher/notification metadata (persisted for crash recovery)
|
||||
watcher_platform: str = ""
|
||||
watcher_chat_id: str = ""
|
||||
watcher_user_id: str = ""
|
||||
watcher_user_name: str = ""
|
||||
watcher_thread_id: str = ""
|
||||
watcher_interval: int = 0 # 0 = no watcher configured
|
||||
notify_on_complete: bool = False # Queue agent notification on exit
|
||||
@@ -970,6 +972,8 @@ class ProcessRegistry:
|
||||
"session_key": s.session_key,
|
||||
"watcher_platform": s.watcher_platform,
|
||||
"watcher_chat_id": s.watcher_chat_id,
|
||||
"watcher_user_id": s.watcher_user_id,
|
||||
"watcher_user_name": s.watcher_user_name,
|
||||
"watcher_thread_id": s.watcher_thread_id,
|
||||
"watcher_interval": s.watcher_interval,
|
||||
"notify_on_complete": s.notify_on_complete,
|
||||
@@ -1031,6 +1035,8 @@ class ProcessRegistry:
|
||||
detached=True, # Can't read output, but can report status + kill
|
||||
watcher_platform=entry.get("watcher_platform", ""),
|
||||
watcher_chat_id=entry.get("watcher_chat_id", ""),
|
||||
watcher_user_id=entry.get("watcher_user_id", ""),
|
||||
watcher_user_name=entry.get("watcher_user_name", ""),
|
||||
watcher_thread_id=entry.get("watcher_thread_id", ""),
|
||||
watcher_interval=entry.get("watcher_interval", 0),
|
||||
notify_on_complete=entry.get("notify_on_complete", False),
|
||||
@@ -1049,6 +1055,8 @@ class ProcessRegistry:
|
||||
"session_key": session.session_key,
|
||||
"platform": session.watcher_platform,
|
||||
"chat_id": session.watcher_chat_id,
|
||||
"user_id": session.watcher_user_id,
|
||||
"user_name": session.watcher_user_name,
|
||||
"thread_id": session.watcher_thread_id,
|
||||
"notify_on_complete": session.notify_on_complete,
|
||||
})
|
||||
|
||||
@@ -1427,8 +1427,12 @@ def terminal_tool(
|
||||
if _gw_platform and not check_interval:
|
||||
_gw_chat_id = _gse("HERMES_SESSION_CHAT_ID", "")
|
||||
_gw_thread_id = _gse("HERMES_SESSION_THREAD_ID", "")
|
||||
_gw_user_id = _gse("HERMES_SESSION_USER_ID", "")
|
||||
_gw_user_name = _gse("HERMES_SESSION_USER_NAME", "")
|
||||
proc_session.watcher_platform = _gw_platform
|
||||
proc_session.watcher_chat_id = _gw_chat_id
|
||||
proc_session.watcher_user_id = _gw_user_id
|
||||
proc_session.watcher_user_name = _gw_user_name
|
||||
proc_session.watcher_thread_id = _gw_thread_id
|
||||
proc_session.watcher_interval = 5
|
||||
process_registry.pending_watchers.append({
|
||||
@@ -1437,6 +1441,8 @@ def terminal_tool(
|
||||
"session_key": session_key,
|
||||
"platform": _gw_platform,
|
||||
"chat_id": _gw_chat_id,
|
||||
"user_id": _gw_user_id,
|
||||
"user_name": _gw_user_name,
|
||||
"thread_id": _gw_thread_id,
|
||||
"notify_on_complete": True,
|
||||
})
|
||||
@@ -1457,10 +1463,14 @@ def terminal_tool(
|
||||
watcher_platform = _gse2("HERMES_SESSION_PLATFORM", "")
|
||||
watcher_chat_id = _gse2("HERMES_SESSION_CHAT_ID", "")
|
||||
watcher_thread_id = _gse2("HERMES_SESSION_THREAD_ID", "")
|
||||
watcher_user_id = _gse2("HERMES_SESSION_USER_ID", "")
|
||||
watcher_user_name = _gse2("HERMES_SESSION_USER_NAME", "")
|
||||
|
||||
# Store on session for checkpoint persistence
|
||||
proc_session.watcher_platform = watcher_platform
|
||||
proc_session.watcher_chat_id = watcher_chat_id
|
||||
proc_session.watcher_user_id = watcher_user_id
|
||||
proc_session.watcher_user_name = watcher_user_name
|
||||
proc_session.watcher_thread_id = watcher_thread_id
|
||||
proc_session.watcher_interval = effective_interval
|
||||
|
||||
@@ -1470,6 +1480,8 @@ def terminal_tool(
|
||||
"session_key": session_key,
|
||||
"platform": watcher_platform,
|
||||
"chat_id": watcher_chat_id,
|
||||
"user_id": watcher_user_id,
|
||||
"user_name": watcher_user_name,
|
||||
"thread_id": watcher_thread_id,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user