image_generate wrote the file into the server's own data/images/ and handed
that host path to the model. It is a path in nobody's vocabulary: not the
caller's home, not their container. Telegram's send_attachment therefore
resolved it under the user's home and answered "file not found", and
read_file, execute_cmd and the viewer could not reach it either. The web URL
was the only surface that worked, which is why the failure only ever showed
on Telegram -- and why the model there, having no working way to hand the
file over, started inventing send_photo and send_media.
Placement moves to the tool, the one place holding a ToolContext:
- The manager returns bytes (generate_bytes) and no longer knows where an
image goes. It has no UserFs and no session, so it never could have.
- run_with saves through uploads::save_to_home into uploads/{session}/. The
returned path is agent vocabulary, so every consumer resolves it, and that
is the one directory the media inliner is authorized to read from -- a
vision model can be shown the image it just made. execute_async, the
context-free path, now fails loudly rather than writing somewhere nobody
can read; same shape as execute_cmd.
- The extension is sniffed rather than assumed png: it is what decides
whether Telegram sends the picture inline or as an anonymous document, and
providers return jpeg and webp too. The file is named after the prompt, so
it reads as something in the explorer and in Telegram.
The result still carries a url, since the chat renders Markdown images and
 beats naming a file the user then has to open. It points at
/api/file?path=..., which resolves through the caller's own UserFs. The old
/api/images/{id} route is removed: it had no writer left once placement
moved, and it addressed one instance-wide directory behind require_auth
alone, with no notion of who owned the image -- the same shape as the /data
static mount removed before it. That leaves data_root unused, so the manager
no longer knows about the server's filesystem at all.
Docs: the Telegram page explains send_attachment as the channel's equivalent
of show_file_to_user; the ComfyUI page says where a generated image lands and
which of the two handles to use where.
Also introduces CHANGELOG.md and the standing rule for it in CLAUDE.md.
2.6 KiB
Telegram Bot
- Plugin id:
telegram - Category: Messaging channel
- Runs: connects out to the Telegram Bot API — needs internet access
What it does
Connects a private Telegram bot to this instance, so a user can chat with their assistant directly from Telegram — same assistant, same memory, same tools as the web app. Pending approvals (e.g. "can I run this command?") are shown as Telegram messages with inline buttons the user can tap to approve or deny, right from their phone.
One bot serves everyone on the instance; each person pairs their own Telegram chat to their own account — nobody sees anyone else's conversation.
Requirements
- A Telegram bot token. Get one by messaging @BotFather on Telegram, sending
/newbot, and following the prompts — it replies with a token that looks like123456789:AAF....
Enabling & configuring (admin)
- Plugins page → Telegram Bot → enable, then Configure.
- Field:
token(required) — the bot token from BotFather. Stored as a secret field, not shown again after saving.
Per-user pairing (self-service)
Once the bot is enabled and a user has been granted access to the plugin (admin: Users → that person → Plugins → tick Telegram):
- The user opens Telegram, finds the bot (by the username chosen in BotFather), and sends it any message.
- The bot replies with a short pairing code.
- The user opens the Telegram page in the web app's sidebar and pastes the code into the pairing code field.
That's the whole flow — no admin involvement needed for a normal pairing. (An admin can alternatively bind a chat to a user directly using the telegram_pairing tool from the assistant, e.g. if a user can't access the web app.)
Notes
- Output sent to Telegram is automatically constrained to Telegram-safe HTML formatting (bold, italic, code blocks, links, quotes) — no Markdown, no tables. This is handled automatically; nothing to configure.
- Revoking a user's plugin access immediately stops that person's Telegram chat from working, without needing them to re-pair if access is restored later.
- You can send a file from your workspace into the Telegram chat with
send_attachment, giving it a path in your usual vocabulary (~/report.pdf,uploads/…,shared/…, or an absolute path inside your sandbox). Images and videos arrive inline, anything else as a downloadable file. This is the Telegram equivalent ofshow_file_to_user, which only exists in the web and mobile apps — on Telegram a generated image or a document you produced has to be sent this way, or the user never sees it.