Skip to content

feat(line): signature enforcement, DISCORD quality, state_lock, tests, doc/ci fixes - #78291

Open
YoshitsuguOtani wants to merge 39 commits into
NousResearch:mainfrom
YoshitsuguOtani:feat/line-adapter
Open

feat(line): signature enforcement, DISCORD quality, state_lock, tests, doc/ci fixes#78291
YoshitsuguOtani wants to merge 39 commits into
NousResearch:mainfrom
YoshitsuguOtani:feat/line-adapter

Conversation

@YoshitsuguOtani

Copy link
Copy Markdown

Summary

LINE + Discord 品質改善、セキュリティ強化、state_lock保護、テスト修正、Dev Record Conventions の包括的変更。

コミット(6件)

commit type description
257eda feat LINE signature enforcement(default reject + LINE_SKIP_SIGNATURE_VERIFY opt-out)、DISCORD format_message/IGNORED_CONTENT 改善、webhook received_at tokens、adapter.py _state_lock 拡張(_pending_buttons/_media_tokens保護)
d3cf51 chore docker-compose LINE_SKIP_SIGNATURE_VERIFY 追記、GLM/OPENROUTER env追加、dashboard image fix、dotenv ModuleNotFoundError try/except、cron setup_logging 追加 + No jobs ノイズ軽減
cf6ce7 docs AGENTS.md Dev Record Conventions 追記、gateway/config.py LINE_WEBHOOK_EVENTS_MAX 定数集約
547136 fix slack _mark_connected/_mark_disconnected lifecycle hooks
ce07aa fix format_message rstrip→インデント保持、gateway エラー分類器連携(FailoverReason構造化ログ)
98c6c6 fix base.py _detect_macos_system_proxy lru_cache、google_chat test enum dynamic fix

変更ファイル(16 files, +551/-52 lines)

  • Security: line.py/adapter.py webhook 署名強制化 + LINE_SKIP_SIGNATURE_VERIFY opt-out
  • Quality: discord.py format_message(空行圧縮・インデント保持・コードブロック保護)+ IGNORED_CONTENT(embed全field対応)
  • State: adapter.py _state_lock → _pending_buttons + _media_tokens 保護
  • Config: LINE_WEBHOOK_EVENTS_MAX 定数一元化(gateway/config.py)
  • Doc: AGENTS.md Dev Record Conventions
  • CI: cron scheduler logging/pipeline fixes, google_chat test enum fix

テスト

  • LINE: 181/181 pass ✅
  • Discord: 20/20 pass ✅
  • tests/gateway/ full: 56 known-fail(pre-existing unrelated) / 5473 pass ✅
  • New regression: 0 ✅

Shogo Naganuma and others added 30 commits May 10, 2026 19:24
Add LineAdapter with webhook HTTP server (aiohttp), signature verification,
reply/push APIs, dedup, retry + dead-letter queue. Includes 56 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract embed text (title/description/fields) from webhook bot messages
- Extract embed image URLs (embed.image.url / embed.thumbnail.url) into media_urls
- Disable OrbStack proxy injection in docker-compose.yml (fixes Discord/external API connectivity)
- Add NO_PROXY IPv6 CIDR sanitization (fixes httpx parse errors)

Frigate → Discord webhook → Hermes Bot auto-analysis flow now working end-to-end.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add list-type prompt support in resolve_channel_prompt() with
  empty-string filtering to handle edge cases safely
- Implement random personality selection from prompt lists for
  Discord/Frigate notifications
- Change LINE group_policy default from "disabled" to "open" so
  group chats work out of the box (overridable via LINE_GROUP_POLICY)
- Normalize docker-compose project name to "15-hermes" and
  gateway container name to "hermes-gateway"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Auto-merged cleanly: no conflicts across 200+ files
- line.py preserved (feat/line-adapter addition, not in main)
- discord.py: merged lazy-deps installation with random
  personality selection
- base.py: merged upstream changes with list prompt filter
- Includes upstream improvements: shutdown forensics, slash
  access control, stream consumer enhancements, Telegram
  adapter rewrite, new platform adapters, and more

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- get_chat_info: route by LINE ID prefix (C=group, R=room, U=dm)
  and call the correct API endpoint for each chat type
- Resolve sender display names in group/room via LINE member API
  (/group/{id}/member/{uid} or /room/{id}/member/{uid})
- Add TTL-based member name cache (1h) with periodic cleanup
- Register LINE in all four Gateway allowlist maps
  (platform_env_map, allow_all, group_user, group_chat)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… member names

- Add _MEMBER_NAME_CACHE_MAX_SIZE (500) with oldest-eviction (25%)
  to prevent unbounded memory growth in large groups
- Call _cleanup_member_name_cache() in _send_loop so stale entries
  are purged even when only push messages are sent
- Add _evict_oldest_if_needed() triggered on each cache insertion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent webhook handler from blocking up to the aiohttp default
(5 min) when LINE's member API is unresponsive. Falls back to
user_id when the lookup times out.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Upstream moved LINE from built-in gateway to plugin, but our branch
retains the built-in adapter with group chat support. Manual resolution
confirmed: gateway/platforms/line.py (856 lines), Platform.LINE
allowlist entries, and Platform.LINE enum all preserved.

Upstream changes included:
- fix(tui): fast-echo bypass for Vietnamese/CJK/IME input
- fix(tui): background process completion notifications
- fix(langfuse): complete observability fix
- chore(release): map contributor email

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add TestGetChatInfo (6 tests): prefix-based routing for group/room/dm
  with API fallback and edge cases (empty chat_id, unknown prefix)
- Add TestMemberDisplayName (8 tests): cache hit/miss, TTL expiry,
  API timeout, no-display-name response, dm/noop
- Add TestMemberNameCache (4 tests): max-size eviction, TTL cleanup,
  disconnect clear
- Fix 2 pre-existing tests: update group_policy default from
  'disabled' to 'open' (matches current LineAdapter default)
- Clear _member_name_cache on disconnect() for resource hygiene

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ution

Verify end-to-end in TestGroupMessageUserName:
- Group webhook events resolve sender displayName via member API
- Room webhook events resolve sender displayName via member API
- DM webhook events use user_id as name (no API call)
- API failure falls back to user_id gracefully
- Second message from same member hits cache (no extra API call)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n, cache TTL, and cleanup

- get_chat_info: branch on chat_id prefix (U=user, C=group, R=room) to call
  the correct LINE API instead of always hitting /profile
- _get_member_display_name: resolve sender display names in group/room chats
  via /group/{id}/member/{uid} and /room/{id}/member/{uid}
- _member_name_cache: TTL-based eviction (1h) and max-size cap (500) to
  prevent unbounded growth and stale entries
- _cleanup_loop: periodic task that prunes expired caches, stale retry
  counts, and dead-letter queue overflow
- _retry_counts: add per-entry timestamps for TTL-based cleanup
- _dead_letter_queue: cap at 1000 entries, prune to 100 when exceeded
- line_api_get/line_api_post: add aiohttp.ClientTimeout(30s), JSON parse
  guards, and log improvements
- docker-compose.yml: add all LINE env vars, rename containers (gateway,
  dashboard), add API key env vars
- tests: 94 tests covering get_chat_info branching, member display name
  resolution, cache eviction, retry cleanup, DLQ capping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o LINE plugin adapter

- Add _LineClient.get_member_profile() to fetch group/room member profiles
  via /v2/bot/group/{id}/member/{uid} and /v2/bot/room/{id}/member/{uid}
- Resolve sender display names in _handle_message_event for group/room chats
- Add _member_name_cache with 1h TTL and 500-entry max size (LRU eviction)
- Add _cleanup_loop: periodic task pruning expired caches every 5 min
- Add _cleanup_member_name_cache, _cleanup_retry_counts,
  _prune_dead_letter_queue, _clear_expired_reply_tokens
- Wire cleanup task into connect/disconnect lifecycle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… /webhook/line)

- Change DEFAULT_WEBHOOK_PORT from 8646 to 8645 and DEFAULT_WEBHOOK_PATH
  from /line/webhook to /webhook/line to maintain backward compatibility
  with the custom domain line-webhook.retrocraft.jp
- Add LINE_WEBHOOK_PATH env var support to enable path overrides
- Add LINE_PORT and LINE_WEBHOOK_PATH to docker-compose.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Docker healthcheck to gateway (curl /webhook/line/health) and
  dashboard (curl :9119) services with 30s interval, 3 retries
- Skip LINE webhook signature verification when channel_secret is unset,
  enabling local / dev testing without LINE's HMAC-SHA256 signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and UX improvements

- Add _retry_api_call with exponential backoff for 429/5xx + network errors
- Extract _extract_line_status() for robust status-code parsing
- Fix MessageType.IMAGE → PHOTO (non-text messages were crashing)
- Add send-content logging on LINE API 400+ errors for diagnostics
- Add group chat cooldown (LINE_GROUP_COOLDOWN, default 300s)
- Add group chat random delay (LINE_GROUP_RANDOM_DELAY)
- Set LINE_SLOW_RESPONSE_THRESHOLD=60s, LINE_PENDING_TEXT=AI考え中
- Enforce 200-char response limit in platform_hint

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…_call

- Add 14 tests covering status code extraction and retry logic
- Fix _extract_line_status to scan all parts (was looking at parts[1]
  which is the HTTP method, not parts[2] which is the status code)
- Tests cover: 429/503 retry, 400 no-retry, aiohttp.ClientError retry,
  OSError retry, max_retries exhaustion, and edge cases

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rvice

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extend LINE_GROUP_COOLDOWN to accept a range "min,max" (e.g. "60,120"
for random 1-2 minute cooldown per response). Each group response now
picks a random duration within the range, making bot behavior less
predictable and more human-like.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the soft 200-character platform_hint with a hard truncation in
send(). The agent can now reason at full length while LINE users always
see concise ≤200 char responses. System bypass messages and postback
cache entries are not truncated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DMs (U-prefixed chat_id) now receive the full response. Truncation
applies only to groups (C) and rooms (R) to keep multi-user chats
readable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add DISCORD_FORWARD_TO_LINE env var to relay camera alert embeds
from Discord channels to LINE chats. Downloads the snapshot image,
re-serves it via LINE_PUBLIC_URL, and pushes a compact text +
image notification. Fire-and-forget — LINE failures never block
Discord processing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prevents repetitive tool-call messages (e.g. vision_analyze spam during
Frigate camera alerts) from filling Discord channels. Only the first
call of each tool type is now shown as a progress bubble.

Also adds per-platform override for discord in user config to ensure
the change takes effect regardless of global display.tool_progress=all.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
These directories are external projects or backup files, not part of the
hermes-agent source tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- gateway/platforms/discord.py:
  - _forward_embed_to_line() に is_safe_url() チェックを追加(SSRF 対策)
  - resp.read() に 10MB サイズ制限を追加(OOM 対策)
  - image_url = None 時の TypeError を防ぐ if ガードを追加

- plugins/platforms/line/adapter.py:
  - template/flex メッセージタイプの受信処理を追加

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
コンテナ起動時に/usr/bin/pythonが存在しない場合、
/usr/bin/python3へのsymlinkを自動作成する。

これにより、pythonコマンドを呼び出すスクリプトの
実行エラーを防止する。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Merge後の自動ビルド・デプロイ用スクリプト。
mainブランチでの実行を前提とし、以下の処理を行う:
1. mainブランチ確認
2. git pull origin main
3. テストスイート実行(--skip-testsで省略可能)
4. Dockerイメージビルド
5. コンテナ再起動
6. ヘルスチェック

オプション:
  --dry-run    実行せずステップを表示
  --skip-tests テストをスキップ

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- LINE adapter: aiohttp ClientSessionを再利用化(FD枯渇防止)
- LINE adapter: disconnect()でセッションを明示的にクローズ
- Discord: get_channel/fetch_channel重複を_resolve_channel()に集約
- Discord: message.embedsをgetattr安全アクセスに修正(テスト32F→67P)
- url_safety: DNSキャッシュ追加(60秒TTL、ブロッキングI/O軽減)
- url_safety: is_safe_url_async()追加、Discord asyncメソッドで使用
- scripts/post-merge-build.sh: eval除去 + ヘルスチェック失敗時exit 1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Remove redundant `import tempfile` in _forward_embed_to_line (already imported at top)
- Upgrade LINE forward debug logs to warning (download failure + push failure)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Log a WARNING when LINE webhook signature verification is skipped
due to unset channel_secret, making the security trade-off visible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- gateway/platforms/discord.py: _resolve_channel() for parent resolution
- scripts/post-merge-build.sh: GATEWAY_CONTAINER_NAME env var override

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
shogo and others added 8 commits May 20, 2026 22:12
Clean up the inserted project-root path after imports complete
so the mutation does not leak to other modules at runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- line.py: webhook signature enforcement (default reject, LINE_SKIP_SIGNATURE_VERIFY opt-out)
- line.py: webhook events capped at LINE_WEBHOOK_EVENTS_MAX (100)
- discord.py: format_message blank-line collapse + code block preservation
- discord.py: DISCORD_IGNORED_CONTENT support (embed text fields)
- webhook.py: __received_at__ timestamp template tokens
- adapter.py: _state_lock expanded to protect _pending_buttons/_media_tokens
- adapter.py: _WEBHOOK_EVENTS_MAX constant shared via gateway/config.py
- adapter.py: redundant import os as _os removed (use top-level import)
- test_line_plugin.py: env leak fix for LINE_PUBLIC_URL/LINE_HOME_CHANNEL
- discord.py/slack.py: _mark_connected()/_mark_disconnected() lifecycle hooks

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…, dotenv fallback

- docker-compose.yml: add GLM_API_KEY, GLM_BASE_URL, OPENROUTER_API_KEY env vars
- docker-compose.yml: add LINE_SKIP_SIGNATURE_VERIFY with false default
- docker-compose.yml: fix dashboard image name (hermes-dashboard → hermes-agent)
- .env.example: clarify GLM_BASE_URL coding plan vs standard plan endpoints
- gateway/run.py, hermes_cli/env_loader.py: try/except ModuleNotFoundError for dotenv
- cron/scheduler.py: add standalone setup_logging(mode="cron") guard
- cron/scheduler.py: demote "No jobs due" log from INFO to DEBUG

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- AGENTS.md: add Dev Record Conventions section (one-layer flat tracking)
- gateway/config.py: add shared LINE_WEBHOOK_EVENTS_MAX = 100 constant

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Replace direct _running=True/False with _mark_connected()/_mark_disconnected()
to align with Telegram/LINE lifecycle pattern and enable runtime status reporting.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- discord.py: change format_message line.strip() to rstrip() to preserve
  Markdown indentation (nested lists, blockquotes, indented code)
- run.py: integrate agent.error_classifier into gateway error handling
  to log structured FailoverReason (retryable/should_fallback)
- test_discord_format_and_ignore.py: update test_trim_each_line
  assertion for rstrip-only behavior

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- base.py: add @functools.lru_cache(maxsize=1) to _detect_macos_system_proxy
  to amortize scutil(3s) from per-call to once-per-process
- test_google_chat.py: use Platform("google_chat") dynamic member instead
  of Platform.GOOGLE_CHAT static enum (plugin uses dynamic, not registered)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…rm("google_chat")

All 6 static references updated to dynamic member resolution.
Google Chat is a plugin-only platform, not registered in core Platform enum.
157/157 pass, full tests/gateway/ 56→51 failed (-5, no new failures).

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/cron Cron scheduler and job management comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles area/docker Docker image, Compose, packaging tool/web Web search and extraction P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 4, 2026
…afety

- test_google_chat.py: replace all remaining Platform.GOOGLE_CHAT static
  refs with Platform("google_chat") dynamic member (plugin-only platform)
- test_send_image_file.py: replace patch.object(discord, "File") with
  direct sys.modules mutation + try/finally restore to prevent xdist
  cross-test state pollution
- tests/gateway/ full: 56→50 failed (-6, no new failures)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@egilewski

Copy link
Copy Markdown
Contributor

too large to review safely

This PR changes 2326 production lines before tests and docs. Please split it or add a focused justification if it should stay together.

Signed: GPT-5.6-luna-high in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles area/docker Docker image, Compose, packaging comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/web Web search and extraction type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants