Skip to content
Open
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
31 changes: 25 additions & 6 deletions gateway/platforms/qqbot/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,21 @@ async def _handle_c2c_message(
if not self._is_dm_allowed(user_openid):
return

source = self.build_source(
chat_id=user_openid,
user_id=user_openid,
chat_type="dm",
)
allow_attachments = True
gateway_runner = getattr(self, "gateway_runner", None)
if (
gateway_runner is not None
and hasattr(gateway_runner, "_is_user_authorized")
and not gateway_runner._is_user_authorized(source)
):
logger.debug("[%s] C2C message blocked by ACL: user=%s", self._log_tag, user_openid)
allow_attachments = False

text = content
attachments_raw = d.get("attachments")
logger.info(
Expand All @@ -1156,7 +1171,15 @@ async def _handle_c2c_message(
)

# Process all attachments uniformly (images, voice, files)
att_result = await self._process_attachments(attachments_raw)
if allow_attachments:
att_result = await self._process_attachments(attachments_raw)
else:
att_result = {
"image_urls": [],
"image_media_types": [],
"voice_transcripts": [],
"attachment_info": "",
}
image_urls = att_result["image_urls"]
image_media_types = att_result["image_media_types"]
voice_transcripts = att_result["voice_transcripts"]
Expand Down Expand Up @@ -1195,11 +1218,7 @@ async def _handle_c2c_message(

self._chat_type_map[user_openid] = "c2c"
event = MessageEvent(
source=self.build_source(
chat_id=user_openid,
user_id=user_openid,
chat_type="dm",
),
source=source,
text=text,
message_type=self._detect_message_type(image_urls, image_media_types),
raw_message=d,
Expand Down
4 changes: 4 additions & 0 deletions gateway/platforms/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,10 @@ def _validate_signature(
self, request: "web.Request", body: bytes, secret: str
) -> bool:
"""Validate webhook signature (GitHub, GitLab, generic HMAC-SHA256)."""
if re.fullmatch(r"\$\{[^}]+\}", secret.strip()):
logger.warning("[webhook] Rejecting unresolved placeholder secret")
return False

# GitHub: X-Hub-Signature-256 = sha256=<hex>
gh_sig = request.headers.get("X-Hub-Signature-256", "")
if gh_sig:
Expand Down
1 change: 1 addition & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5371,6 +5371,7 @@ def _is_user_authorized(self, source: SessionSource) -> bool:
user_id = source.user_id
if not user_id:
return False
team_id = (source.guild_id or "").strip() if source.platform == Platform.SLACK else ""

platform_env_map = {
Platform.TELEGRAM: "TELEGRAM_ALLOWED_USERS",
Expand Down
34 changes: 34 additions & 0 deletions tests/test_tui_gateway_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,18 @@ def test_load_enabled_toolsets_honors_builtin_env_if_config_fails(monkeypatch):
assert server._load_enabled_toolsets() == ["web"]


def test_load_enabled_toolsets_preserves_empty_config_allowlist(monkeypatch):
monkeypatch.delenv("HERMES_TUI_TOOLSETS", raising=False)

import hermes_cli.config as config_mod
import hermes_cli.tools_config as tools_config_mod

monkeypatch.setattr(config_mod, "load_config", lambda: {"platform_toolsets": {"cli": []}})
monkeypatch.setattr(tools_config_mod, "_get_platform_tools", lambda *a, **kw: set())

assert server._load_enabled_toolsets() == []


def test_load_enabled_toolsets_all_env_means_all(monkeypatch):
monkeypatch.setenv("HERMES_TUI_TOOLSETS", "all")

Expand Down Expand Up @@ -4580,6 +4592,16 @@ def test_make_agent_handles_null_agent_config(monkeypatch):
assert mock_agent.call_args.kwargs["max_iterations"] == 80


def test_make_agent_preserves_empty_toolset_allowlist(monkeypatch):
_setup_make_agent_mocks(monkeypatch, {})
monkeypatch.setattr(server, "_load_enabled_toolsets", lambda: [])

with patch("run_agent.AIAgent") as mock_agent:
server._make_agent("sid1", "key1")

assert mock_agent.call_args.kwargs["enabled_toolsets"] == []


class _FakeAgentForBackground:
base_url = None
api_key = None
Expand Down Expand Up @@ -4634,6 +4656,18 @@ def test_background_agent_kwargs_handles_null_agent_config(monkeypatch):
assert kwargs["max_iterations"] == 40


def test_background_agent_kwargs_preserves_empty_toolset_allowlist(monkeypatch):
class AgentWithNoTools(_FakeAgentForBackground):
enabled_toolsets = []

monkeypatch.setattr(server, "_load_cfg", lambda: {})
monkeypatch.setattr(server, "_load_enabled_toolsets", lambda: ["web"])

kwargs = server._background_agent_kwargs(AgentWithNoTools(), "task_1")

assert kwargs["enabled_toolsets"] == []


def test_config_show_displays_nested_max_turns(monkeypatch):
monkeypatch.setattr(
server,
Expand Down
27 changes: 16 additions & 11 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,7 @@ def _load_enabled_toolsets() -> list[str] | None:
)
if fallback_notice is not None:
print(fallback_notice, file=sys.stderr, flush=True)
return enabled or None
return enabled
except Exception:
if fallback_notice is not None:
print(
Expand Down Expand Up @@ -1803,8 +1803,11 @@ def _background_agent_kwargs(agent, task_id: str) -> dict:
"acp_args": getattr(agent, "acp_args", None) or None,
"model": getattr(agent, "model", None) or _resolve_model(),
"max_iterations": _cfg_max_turns(cfg, 25),
"enabled_toolsets": getattr(agent, "enabled_toolsets", None)
or _load_enabled_toolsets(),
"enabled_toolsets": (
agent.enabled_toolsets
if getattr(agent, "enabled_toolsets", None) is not None
else _load_enabled_toolsets()
),
Comment on lines +1806 to +1810
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Using getattr(agent, "enabled_toolsets", None) is not None to check if the parent agent has an explicit toolset allowlist introduces a bug when the parent agent has enabled_toolsets = None (which represents "all tools enabled").

In this case, getattr(agent, "enabled_toolsets", None) returns None, causing the condition to evaluate to False and falling back to _load_enabled_toolsets(). If _load_enabled_toolsets() returns a restricted list (e.g., ["web"]), the background agent will be restricted to ["web"] even though the parent agent had all tools enabled (None).

Using hasattr(agent, "enabled_toolsets") instead correctly preserves None while still falling back to _load_enabled_toolsets() if the attribute is missing entirely.

Suggested change
"enabled_toolsets": (
agent.enabled_toolsets
if getattr(agent, "enabled_toolsets", None) is not None
else _load_enabled_toolsets()
),
"enabled_toolsets": (
agent.enabled_toolsets
if hasattr(agent, "enabled_toolsets")
else _load_enabled_toolsets()
),

"quiet_mode": True,
"verbose_logging": False,
"ephemeral_system_prompt": getattr(agent, "ephemeral_system_prompt", None)
Expand Down Expand Up @@ -6223,11 +6226,12 @@ def _(rid, params: dict) -> dict:
from toolsets import get_all_toolsets, get_toolset_info

session = _sessions.get(params.get("session_id", ""))
enabled = (
set(getattr(session["agent"], "enabled_toolsets", []) or [])
enabled_toolsets = (
getattr(session["agent"], "enabled_toolsets", None)
if session
else set(_load_enabled_toolsets() or [])
else _load_enabled_toolsets()
)
enabled = set(enabled_toolsets or [])

items = []
for name in sorted(get_all_toolsets().keys()):
Expand All @@ -6239,7 +6243,7 @@ def _(rid, params: dict) -> dict:
"name": name,
"description": info["description"],
"tool_count": info["tool_count"],
"enabled": name in enabled if enabled else True,
"enabled": True if enabled_toolsets is None else name in enabled,
"tools": info["resolved_tools"],
}
)
Expand Down Expand Up @@ -6363,11 +6367,12 @@ def _(rid, params: dict) -> dict:
from toolsets import get_all_toolsets, get_toolset_info

session = _sessions.get(params.get("session_id", ""))
enabled = (
set(getattr(session["agent"], "enabled_toolsets", []) or [])
enabled_toolsets = (
getattr(session["agent"], "enabled_toolsets", None)
if session
else set(_load_enabled_toolsets() or [])
else _load_enabled_toolsets()
)
enabled = set(enabled_toolsets or [])

items = []
for name in sorted(get_all_toolsets().keys()):
Expand All @@ -6379,7 +6384,7 @@ def _(rid, params: dict) -> dict:
"name": name,
"description": info["description"],
"tool_count": info["tool_count"],
"enabled": name in enabled if enabled else True,
"enabled": True if enabled_toolsets is None else name in enabled,
}
)
return _ok(rid, {"toolsets": items})
Expand Down
Loading