Skip to content

fix(channels): deliver send_file via Telegram and harden cross-channel dispatch#56

Merged
pufit merged 1 commit into
ClickHouse:mainfrom
neomnezia:hun7er/send-file-telegram-delivery
Apr 30, 2026
Merged

fix(channels): deliver send_file via Telegram and harden cross-channel dispatch#56
pufit merged 1 commit into
ClickHouse:mainfrom
neomnezia:hun7er/send-file-telegram-delivery

Conversation

@neomnezia
Copy link
Copy Markdown
Contributor

Summary

mcp__nerve__send_file was leaving phantom [mcp__nerve__send_file] headers in Telegram and only persisting a DB record for the web frontend — files never reached the chat. This PR makes the tool actually deliver via the bound channel and closes three P1 cross-channel leakage paths.

What changed

  • New SEND_FILES channel capability + BaseChannel.send_file interface.
  • Telegram send_document impl with path/exists checks. No client-side size cap — the Telegram API enforces per-deployment limits (50 MiB on api.telegram.org, 2 GiB on self-hosted Bot API), and the surrounding try/except already handles the rejection.
  • Web send_file no-ops since SendFileBlock already handles UI delivery.

Cross-channel hardening

  • AgentEngine tracks active channel per session (_active_channel set in run(), cleared on exit) and exposes get_active_channel(session_id).
  • ChannelRouter.send_file(session_id, file_path, channel=...) requires an explicit channel. Cached _message_context target is reused only when its channel matches the requested one — never to pick the destination channel. channel=None returns False unconditionally, so cron/planner/notifications/web routes that don't thread a channel through engine.run() can no longer leak files to a stale Telegram chat from a prior inbound message.
  • _send_file_impl reads engine.get_active_channel and forwards.
  • Path-aware workspace containment check (Path.relative_to / ValueError) replaces the bypassable str.startswith guard — closes a sibling-prefix exfiltration vector exposed by real Telegram delivery (e.g. workspace /srv/ws previously accepted /srv/ws-evil/secret.txt).

Files

  • nerve/channels/base.pyCapability.SEND_FILES, BaseChannel.send_file abstract.
  • nerve/channels/telegram.pysend_file via bot.send_document, capability advertised.
  • nerve/channels/web.py — no-op (UI already covered).
  • nerve/channels/router.py — explicit-channel dispatch, cached-context match guard.
  • nerve/agent/engine.py_active_channel map, get_active_channel(), run()/exit lifecycle.
  • nerve/agent/tools.py_send_file_impl resolves channel via engine, path containment check.
  • tests/test_send_file.py — 28 new tests (router dispatch, Telegram channel, tool impl, engine accessor, leakage + sibling-prefix bypass scenarios).

Test plan

  • pytest tests/test_send_file.py -v → 28/28 pass
  • Full suite: 394 passed, 2 skipped
  • Smoke-tested end-to-end on Telegram: agent-invoked send_file on a workspace file delivers the document to the chat
  • Secret/PII scan on diff — clean

🤖 Generated with Claude Code

…l dispatch

Phantom `[mcp__nerve__send_file]` headers in Telegram fixed: the tool
now actually delivers files via the bound channel, not just persists a
DB record for the web frontend.

- `SEND_FILES` channel capability + `BaseChannel.send_file` interface.
- Telegram `send_document` impl (path/exists checks; relies on Telegram
  API to enforce per-deployment size limits — works on self-hosted Bot
  API where the cap is 2 GiB, not just 50 MiB on api.telegram.org).
- Web `send_file` no-ops since the existing `SendFileBlock` card already
  handles UI delivery.

Cross-channel hardening:

- `AgentEngine` tracks active channel per session (`_active_channel`
  set in `run()`, cleared on exit) and exposes
  `get_active_channel(session_id)`.
- `ChannelRouter.send_file(session_id, file_path, channel=...)` requires
  an explicit channel. Cached `_message_context` target is reused only
  when its channel matches the requested one — never to pick the
  destination channel. `channel=None` returns False unconditionally
  (cron / planner / notifications / web routes that don't pass a
  channel through `engine.run()` no longer leak files to a stale
  Telegram chat from a prior inbound message).
- `_send_file_impl` reads `engine.get_active_channel` and forwards.
- Path-aware workspace containment check (`Path.relative_to` /
  `ValueError`) replaces the bypassable `str.startswith` guard —
  closes a sibling-prefix exfiltration vector exposed by real
  Telegram delivery.

Test coverage: 28 unit tests across router, Telegram channel, tool
impl, engine accessor — including the cross-channel leakage scenarios
and sibling-prefix bypass attempt. 394 passed, 2 skipped on full suite.
Copy link
Copy Markdown
Member

@pufit pufit left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution! This is a great feature!

@pufit pufit merged commit 440d94e into ClickHouse:main Apr 30, 2026
neomnezia added a commit to neomnezia/nerve that referenced this pull request Apr 30, 2026
Resolves conflict in nerve/channels/telegram.py by taking upstream version
(fork's pre-squash drafts of PRs ClickHouse#55 and ClickHouse#56 superseded by upstream squash-merges).

Brings in:
- PR ClickHouse#60 — optional Langfuse observability
- PR ClickHouse#56 — send_file Telegram delivery + cross-channel hardening
- PR ClickHouse#55 — adaptive-thinking effort cap per model

Drops fork-unique tests/test_telegram_send.py: imported symbols
(FLOODWAIT_GAP_S, PREVIEW_FOOTER) that upstream squash-refactored away;
upstream's tests/test_send_file.py provides equivalent coverage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants