forked from admin/VolcanicAgenticCore
343 lines
25 KiB
Markdown
343 lines
25 KiB
Markdown
# Telegram bot
|
|
|
|
`hosts/Volcanic.AgenticCore.Telegram` lets you chat with the agent from Telegram. It uses long polling (no public
|
|
URL, no webhook), gives every private chat its own agent session and workspace, and runs the model through the
|
|
Claude Code provider (`claude-code/claude-opus-5-5`) on your Claude subscription. In groups it stays silent and only
|
|
watches for the topics its users set up as [triggers](#groups-and-triggers).
|
|
|
|
## Setup
|
|
|
|
1. Create a bot with [@BotFather](https://t.me/BotFather) (`/newbot`) and copy the token.
|
|
2. Find your numeric Telegram user id, for example by messaging [@userinfobot](https://t.me/userinfobot).
|
|
3. Install Claude Code on the machine that runs the bot and log in (`claude`, then `/login`). Check with
|
|
`claude --version`; see [claude-code-provider.md](claude-code-provider.md).
|
|
4. Run:
|
|
|
|
```bash
|
|
TELEGRAM_BOT_TOKEN=123456:ABC... \
|
|
TELEGRAM_ALLOWED_USER_IDS=111111111 \
|
|
dotnet run --project hosts/Volcanic.AgenticCore.Telegram
|
|
```
|
|
|
|
Open the bot in Telegram and say hello.
|
|
|
|
## Configuration
|
|
|
|
Environment variables, or the `Telegram` section of `appsettings.json` (`Telegram__Name` in the environment; the Google
|
|
settings use the `GoogleCalendar` section, the scheduler the `Scheduler` section):
|
|
|
|
| Setting | Default | Notes |
|
|
|---|---|---|
|
|
| `TELEGRAM_BOT_TOKEN` / `Telegram:BotToken` | — | Required |
|
|
| `TELEGRAM_ALLOWED_USER_IDS` / `Telegram:AllowedUserIds` | — | Required, comma separated. The bot refuses to start without it |
|
|
| `ADMIN_USER_ID` / `Telegram:AdminUserId` | — (off) | The Telegram user id that may use `/impersonate` (see [Impersonation](#impersonation)). Need not be on the allow-list. Must be positive |
|
|
| `Telegram:WorkspaceRoot` | `~/.volcanic/telegram-workspaces` | Chat `<id>` works in `<root>/<id>`; put `AGENTS.md`, `PERSONA.md`, `MEMORY.md` there |
|
|
| `Telegram:Model` | `claude-code/claude-opus-5-5` | Any `provider/model` the Claude Code provider serves |
|
|
| `Telegram:EditInterval` | `00:00:01.5` | Minimum time between edits of the "Working..." status message |
|
|
| `Telegram:InputDebounce` | `00:00:00.6` | How long the bot waits after a message for more before sending them to the agent as one request. `0` disables the wait; messages still being read together (e.g. a photo still downloading and a text after it) are still sent as one request. Must not be negative |
|
|
| `Telegram:ReconnectInitialDelay` / `ReconnectMaxDelay` | `1 s` / `60 s` | Backoff after lost connection |
|
|
| `DEFAULT_TIMEZONE` / `Telegram:DefaultTimeZone` | — (the machine's zone) | IANA zone for chats that haven't set one with `/timezone`, e.g. `Europe/Berlin`. An unknown name stops startup. Also the default for the core (`AgenticCoreOptions.DefaultTimeZone`) |
|
|
| `Scheduler:Enabled` | `true` | Scheduled prompts and watcher scripts (see [Scheduling](#scheduling)) |
|
|
| `Scheduler:LegacyStorePath` | `<agent dir>/scheduler/jobs.json` | The retired shared job file. Jobs live in each chat's `<workspace>/.volcanic/scheduler/jobs.json`; on start the jobs found here move into their chats' files and this file is renamed `jobs.json.migrated-<timestamp>` |
|
|
| `Scheduler:DefaultScriptTimeout` | `00:05:00` | Watcher timeout when a job sets none (1 s to 1 h) |
|
|
| `Scheduler:MaxJobsPerTarget` | `50` | Jobs per chat |
|
|
| `Scheduler:Shell` | `/bin/sh -c` (`cmd.exe /d /s /c` on Windows) | The watcher shell as a list, command appended; same as `bash` by default |
|
|
| `OPENAI_API_KEY` / `Telegram:Speech:ApiKey` | — | Turns voice transcription on. Hidden from `bash` and redacted from tool output |
|
|
| `STT_MODEL` / `Telegram:Speech:Model` | `gpt-4o-transcribe` | `gpt-4o-mini-transcribe` and `whisper-1` also work |
|
|
| `Telegram:Speech:BaseUrl` | `https://api.openai.com/v1` | Any OpenAI-compatible transcription endpoint |
|
|
| `Telegram:Speech:Language` | — | Optional ISO-639-1 hint, e.g. `de` |
|
|
| `OPENROUTER_API_KEY` / `Telegram:Triggers:ApiKey` | — | Turns on [groups and triggers](#groups-and-triggers). Hidden from `bash` and redacted from tool output |
|
|
| `TRIGGER_MODEL` / `Telegram:Triggers:Model` | `typesafe/jev-1.13` | The Jev model that classifies group messages |
|
|
| `Telegram:Triggers:BaseUrl` | `https://openrouter.ai/api/v1` | Requests go to `{BaseUrl}/systemone` |
|
|
| `Telegram:Triggers:DefaultThreshold` | `0.7` | Yes-probability a message needs when the trigger sets none |
|
|
| `Telegram:Triggers:Debounce` | `00:00:10` | Matches of one trigger within this window (opened by the first) reach the chat as one prompt |
|
|
| `Telegram:Triggers:MaxRetries` | `3` | Retries after a failed Jev call (network errors, timeouts, 408, 429, 5xx) |
|
|
| `Telegram:Triggers:MaxConcurrency` | `4` | Jev calls in flight at once |
|
|
| `Telegram:Triggers:MaxTriggersPerChat` | `20` | Triggers per private chat |
|
|
| `Telegram:Triggers:MaxSnooze` | `30.00:00:00` | Longest snooze |
|
|
| `Telegram:Triggers:MaxQueuedPerTrigger` | `5000` | Messages kept per snoozed trigger; past it the oldest are dropped |
|
|
| `Telegram:InlineImageMaxBytes` | `3750000` | Image files up to this size are also shown to the model; must be positive |
|
|
| `GOOGLE_CLIENT_ID` / `GoogleCalendar:ClientId` | — | Optional. With the secret, every chat gets the `calendar` tool and the `/calendar_*` commands; see [google-calendar.md](google-calendar.md) |
|
|
| `GOOGLE_CLIENT_SECRET` / `GoogleCalendar:ClientSecret` | — | Optional; redacted from tool output and hidden from `bash` |
|
|
| `PROCESS_ENV_ALLOW` | — | Comma-separated names or globs let through to `bash`, scheduled scripts and MCP servers despite the deny-list (e.g. `GH_TOKEN` for `gh`). Added to `ProcessEnvironment:Allow` |
|
|
| `ProcessEnvironment:Deny` / `ProcessEnvironment:Allow` | built-in deny-list | Added to the defaults (`*_API_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD`, their `Section__Key` forms and the named provider credentials), never replacing them. Allowing a name that is denied by name stops startup |
|
|
| `ProcessEnvironment:HardenHostProcess` | `true` | Linux: the bot process is made non-dumpable. `ProcessEnvironment__HardenHostProcess=false` for a debugging session only |
|
|
| `PLAYWRIGHT_ENABLED` / `Mcp:Playwright:Enabled` | `false` | Browser tools through `@playwright/mcp` ([mcp.md](mcp.md)) |
|
|
| `PLAYWRIGHT_HEADLESS`, `PLAYWRIGHT_BROWSER`, `PLAYWRIGHT_EXTRA_ARGS`, `PLAYWRIGHT_COMMAND` | `true`, `chromium`, —, `npx` | See [mcp.md](mcp.md#the-playwright-preset) |
|
|
| `Mcp:Servers:<name>` | — | Other stdio MCP servers ([mcp.md](mcp.md#configuration)) |
|
|
|
|
Sessions are stored as usual under `~/.volcanic/agent/sessions/telegram-<chatId>/`; after a restart each chat resumes
|
|
its most recent conversation. Each chat's time zone is kept in `<agent dir>/telegram/chat-settings.json`, so it
|
|
survives `/new` and restarts.
|
|
|
|
## Using it
|
|
|
|
Write normally. While the agent works, a status message stays at the bottom of the chat:
|
|
|
|
```
|
|
⚙️ Working...
|
|
✓ read: src/Program.cs
|
|
✗ bash: dotnet test
|
|
▶ bash: dotnet build
|
|
```
|
|
|
|
It lists the last five tool calls (`▶` running, `✓` done, `✗` failed) and is deleted when the run finishes.
|
|
Answers are not streamed: each one is sent complete when the model finishes it (split into several messages past
|
|
Telegram's 4096 character limit).
|
|
A message sent while the agent works steers the current run, and the bot reacts to it with 👀 so you can see it was
|
|
added. Besides text, you can send photos, files and voice messages (see [What you can send](#what-you-can-send)).
|
|
|
|
Along with your text, the agent sees:
|
|
|
|
- **Replies and quotes.** Reply to one of the bot's messages (or one of yours), or quote part of it, and the agent gets
|
|
a line such as `[Reply to your message: "Which plant do you mean?"]` before your text.
|
|
- **Forwards**, labelled with their source: `[Forwarded from Daily News (Anna K.)]`.
|
|
- **The local time** you sent the message, in the chat's time zone (`/timezone`; by default `Telegram:DefaultTimeZone`, else the zone of the machine running the bot):
|
|
`[Sat 2026-09-26 14:03 Europe/Berlin]`, once per request.
|
|
|
|
Messages sent within about 0.6 s of each other (`Telegram:InputDebounce`), such as a forward with your comment or
|
|
an album, reach the agent as one request. Every message waits that long before it's sent. Commands that an extension
|
|
handles (for example `/claude-code-doctor`) are sent at once, as typed, with no time header. `/new`, `/stop` and
|
|
`/resume` drop a message that is still waiting. A forwarded message is never taken as a command.
|
|
|
|
After a start the bot sends `🟢 Online.` to the owner (the first id in `AllowedUserIds`), once. If the owner never
|
|
started a chat with the bot, Telegram refuses the message; the bot logs that and carries on.
|
|
|
|
| Command | |
|
|
|---|---|
|
|
| `/new` | Start a new conversation |
|
|
| `/stop` | Stop the current run |
|
|
| `/status` | Model, thinking level, session, context usage, time zone, number of scheduled jobs, workspace |
|
|
| `/sessions`, `/resume <id>` | List and resume earlier conversations (an id prefix is enough) |
|
|
| `/compact` | Summarise the conversation to free context |
|
|
| `/thinking [level]` | Show or set the thinking level (Claude Code `--effort`) |
|
|
| `/timezone [name\|reset]` | Show or set this chat's time zone (IANA name such as `Asia/Tokyo`; `reset` returns to the default) |
|
|
| `/jobs` | This chat's scheduled jobs with their next run |
|
|
| `/cancel <id>` | Cancel one of them |
|
|
| `/help` | Short help |
|
|
| `/calendar_connect` | Connect this chat's Google Calendar (link and code; the bot confirms when you approve) |
|
|
| `/calendar_status` | The connected account and time zone, or the pending code |
|
|
| `/calendar_disconnect` | Revoke and delete this chat's calendar token, or cancel a pending connect |
|
|
|
|
Any other `/command` is passed to the session, so extension commands such as `/claude-code-doctor` and
|
|
`/claude-code-login` work. `/mcp` lists the MCP servers (see [mcp.md](mcp.md#mcp)).
|
|
When an extension asks something, the question appears with buttons (or as a reply prompt for free text).
|
|
|
|
The `/calendar_*` commands exist only when both the Google client id and secret are set (`GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` or the `GoogleCalendar` section).
|
|
|
|
If the Claude subscription session behind the bot expires, the **first id in `AllowedUserIds`** (the bot owner)
|
|
gets the re-login link and code prompt in their own private chat, regardless of which chat's request triggered
|
|
it. That user must already have started a chat with the bot — Telegram bots cannot message a user first — or
|
|
the login goes unanswered and the triggering request fails ("no code was entered"). The link, the steps and the
|
|
code prompt arrive as one message. While that prompt is pending, the owner's next plain text message in that chat
|
|
is taken as the code and is not sent to the agent; photos, files, voice messages and `/commands` still behave
|
|
normally. A forwarded message never counts as the code. Don't ask the bot something else in the owner chat until the code is in: a second free-text
|
|
question in that chat (for example from an extension) replaces the pending prompt and cancels the login. See
|
|
[claude-code-provider.md](claude-code-provider.md#session-expiry-and-re-login).
|
|
|
|
### What you can send
|
|
|
|
- **Text and photos.** A photo, with or without a caption, is passed to the model as an image.
|
|
- **Documents, videos and audio files** are saved to `inbox/<yyyy-MM-dd>-<name>` in the chat's workspace (a number is
|
|
added when the name is taken: `-1`, `-2`, ...). The agent gets the caption plus a line such as
|
|
`[File received: inbox/2026-09-26-report.pdf (application/pdf, 1.2 MB)]`. An image sent "as file" (JPEG, PNG, WebP,
|
|
GIF) up to 3.75 MB (`Telegram:InlineImageMaxBytes`) is also shown to the model; a larger one, or one whose contents
|
|
aren't really one of those formats, reaches it as a path only.
|
|
- **Voice messages and audio files** are transcribed with OpenAI (`gpt-4o-transcribe` by default). The bot echoes
|
|
`🎤 <transcript>` (cut to 1024 characters) and the agent gets `[Voice message] <transcript>` (an audio file also gets its `[File received: …]`
|
|
line). Without an API key the bot replies with how to set one up (`OPENAI_API_KEY`). A transcription is tried up
|
|
to 3 times (1 s, then 2 s apart) when the request fails on the network, times out, or gets HTTP 408, 429 or 5xx;
|
|
other errors (a bad key, a rejected file) fail at once.
|
|
- Telegram lets bots download at most 20 MB. A bigger file is refused with a note, and nothing reaches the agent.
|
|
- A message with an attachment is never a command and never answers a pending question: its caption is prompt text.
|
|
- Anything else (stickers, video notes, locations, polls) gets `I can read text, photos, files and voice messages.`
|
|
|
|
### Files from the agent
|
|
|
|
The agent's `send` tool delivers a file from the chat's workspace into the chat, before the agent's reply. Images go
|
|
as photos (up to 10 MB), other files as documents (up to 50 MB), with an optional caption of up to 1024 characters. A
|
|
photo Telegram rejects (for example for its dimensions) is resent as a document. `send` accepts only files inside the
|
|
chat's workspace and delivers only to that chat.
|
|
|
|
### Scheduling
|
|
|
|
Ask the agent in plain words. It has a `schedule` tool with two kinds of job:
|
|
|
|
- "Remind me at 9 tomorrow to call the dentist" or "Every weekday at 8, summarise what's in `inbox/`" is a
|
|
**prompt**: when it fires, the agent handles it like a message from you, with the same time header, and writes the
|
|
reminder or the summary.
|
|
- "Check this page every 10 minutes and tell me only if the price drops below 50" is a **watcher**.
|
|
|
|
For a watcher the agent writes a script (usually in `scripts/`, tested with `bash` first). Every run the bot runs the
|
|
script itself, **without the agent**:
|
|
|
|
- exit 0 with no output means "nothing yet" and costs nothing: no model call, nothing in the conversation;
|
|
- exit 0 with output means the condition is met: the output always goes to the agent, which writes you the message
|
|
(one model call per match);
|
|
- a non-zero exit or a timeout is reported once, straight to the chat, with the last 20 lines of the script's errors.
|
|
Later identical failures stay quiet until the script succeeds again.
|
|
|
|
Scripts run in the chat's workspace with the same filtered environment as `bash` and `TZ` set to the chat's zone.
|
|
When the chat has a connected Google Calendar they also get `GOOGLE_ACCESS_TOKEN`, a short-lived token for that
|
|
calendar ([google-calendar.md](google-calendar.md#watcher-scripts)). Known secrets and that token are redacted from
|
|
their output.
|
|
|
|
When a job fires while the agent is working, a scheduled prompt waits until the current work is done; it never
|
|
interrupts. Occurrences that come due while the same job is still running are skipped.
|
|
|
|
Times are in the chat's zone. `/timezone Asia/Tokyo` (or telling the agent where you are; it has a `set_timezone`
|
|
tool) changes it for the date in the agent's instructions, `bash`, scheduled scripts, message timestamps and Claude
|
|
Code. Recurring jobs follow the new zone ("every day at 9" means 9 where you are now); one-off jobs keep their moment
|
|
and are just shown in the new zone. The reply says how many of each.
|
|
|
|
After a restart, a one-off that came due while the bot was down is delivered as soon as it's back, with
|
|
"This was due at …; the bot was offline." in the prompt. Recurring jobs don't catch up:
|
|
missed occurrences are skipped.
|
|
|
|
Jobs are per chat: each chat's jobs are stored in its workspace, in `.volcanic/scheduler/jobs.json`, and `/jobs` and
|
|
the agent only see this chat's jobs. The file is read on start and rewritten on every change, so edit it only while the
|
|
bot is stopped; a job in a chat's file always belongs to that chat. Watcher failure alerts aren't part of the
|
|
conversation, so the agent can't see them later. With
|
|
`Scheduler:Enabled=false`, `/jobs` and `/cancel` reply that scheduling is turned off, and `/timezone` still works.
|
|
|
|
#### Importing VolcanicSecretary jobs
|
|
|
|
Copy VolcanicSecretary's `.secretary/jobs.json` to the chat's `.volcanic/scheduler/jobs.json`
|
|
(`/data/workspaces/<chatId>/.volcanic/scheduler/jobs.json` in the container) and start the bot. The scheduler
|
|
recognises the old format (a bare array without chat ids), converts it for the chat whose folder it is in, saves it in
|
|
the current format, and keeps the original next to it as `jobs.json.secretary-<timestamp>`. Each job keeps its id
|
|
(a new one if another chat already uses it), schedule and last result:
|
|
|
|
- a `text` job becomes a prompt. With `deliver: direct` (sent as is, no model call) the text gets `Remind me: `, so the
|
|
agent passes it on, as it does for the retired reminder kind;
|
|
- a `script` job becomes a watcher with the same command and timeout. Its output now always goes to the agent;
|
|
- a 6-field cron with seconds `0` loses that field. Other jobs our scheduler can't express (seconds, invalid fields)
|
|
are dropped with a warning in the log.
|
|
|
|
Copy the workspace's `scripts/` into the owner's workspace too: watcher commands such as `python scripts/check.py` run
|
|
from there. A one-off that is already due fires on the first start.
|
|
|
|
### Groups and triggers
|
|
|
|
With `OPENROUTER_API_KEY` set, the bot can sit in Telegram groups and tell you, in your private chat, when something
|
|
you care about comes up. It never writes in a group and never answers there; a group gets no agent session.
|
|
|
|
**Setup, once per bot and group**
|
|
|
|
1. In [@BotFather](https://t.me/BotFather), `/setprivacy` → your bot → **Disable**. With privacy mode on, a bot only
|
|
sees commands, mentions and replies in groups (the startup log warns about it). Alternatively make the bot a group
|
|
admin. Telegram applies the change to groups the bot joins afterwards, so re-add it to groups it is already in.
|
|
2. Add the bot to the group. It watches a group only if an allowed user added it, or an allowed user posts there once
|
|
while it is a member; groups anyone else adds it to are ignored. Removing the bot pauses the group's triggers.
|
|
|
|
**Triggers**
|
|
|
|
Ask the bot in your private chat, for example "tell me when someone in Dev Team talks about deploys or outages". The
|
|
agent uses its `trigger` tool: it lists the watched groups, writes a yes/no question about one message with what counts
|
|
and what doesn't, and confirms it with you before saving. You must be a member of the group. `/triggers` lists your
|
|
triggers.
|
|
|
|
Every message in a watched group (text or caption; voice notes, files and stickers without a caption are skipped) goes
|
|
to TypeSafe's [Jev](https://openrouter.ai/typesafe/jev-1.13), a fast classifier, through OpenRouter's `/systemone`
|
|
endpoint: one call per message carries every trigger on that group as its own question, with the message, its sender,
|
|
the group and the message it replies to. Jev returns the probability that the answer is yes; a trigger matches at or
|
|
above its threshold (0.7 unless set). Matches of one trigger within 10 s reach your chat as one prompt listing each
|
|
message with its score and a link, and the agent decides what to tell you. A failed call is retried 3 times.
|
|
|
|
Matches carry their scores, so tuning is conversational: say a match wasn't relevant and the agent tightens the
|
|
question or raises the threshold; describe a message it missed and it lowers the threshold or broadens the question.
|
|
|
|
**Snooze.** "Keep quiet until tonight" snoozes a trigger (or all of yours) for a duration or until a time. Meanwhile
|
|
messages are collected in your workspace (`.volcanic/triggers/snoozed/`), and when the snooze ends they go to Jev as
|
|
one batch; if anything is related you get one prompt with the whole batch (a large one is saved under
|
|
`.volcanic/triggers/digests/` and the prompt points to it). Unrelated batches stay silent. A snooze that ends while
|
|
the bot is down is handled when it starts.
|
|
|
|
**Logs.** Every group message gets one log line with its Jev results, for example
|
|
`Group "Dev Team" (-100123…) msg 789 from Alice (@alice): "prod deploy is failing…" → 1001/t1 deploys=0.93 MATCH`,
|
|
or why it wasn't classified (unknown group, no text, no triggers, snoozed, Jev failed). Message texts are cut to 200
|
|
characters there.
|
|
|
|
Watched groups live in `<agent dir>/telegram/groups.json`; each chat's triggers in its workspace under
|
|
`.volcanic/triggers/triggers.json`.
|
|
|
|
### Impersonation
|
|
|
|
The admin (`ADMIN_USER_ID`) can act as another user with `/impersonate <user_id>`; `/impersonate 0` (or the admin's
|
|
own id) stops. The command shows up in the admin's command menu only, and from anyone else it is an ordinary unknown
|
|
command passed to the agent.
|
|
|
|
While it is on, the bot treats the admin's messages, commands, button presses and answers exactly as the target user's:
|
|
the allow-list check (impersonating someone outside it means the bot ignores the admin until they stop), the chat, its
|
|
workspace, session, time zone, jobs and calendar. Everything the target's chat sends goes to the admin's chat instead:
|
|
replies, questions, files, notices and scheduled deliveries, including runs started by the target user's own messages.
|
|
Meanwhile the target user is blocked: whatever they send (messages, commands, button presses) is dropped, and the bot
|
|
answers "🔒 An admin is working with your account right now. Please try again later." The admin's own chat (their own jobs, for
|
|
example) keeps working as before.
|
|
|
|
Impersonation is kept in memory only: a restart ends it.
|
|
|
|
### Google Calendar
|
|
|
|
Each chat connects its own Google account with `/calendar_connect`, and the agent's `calendar` tool only uses that
|
|
chat's account, so nobody sees another chat's calendar through the agent. The bot polls Google by itself after
|
|
sending the link and code and confirms in the chat once access is approved. Setup of the Google Cloud app, token
|
|
storage and time zones are in [google-calendar.md](google-calendar.md).
|
|
|
|
## Security
|
|
|
|
The agent has the built-in tools, including `bash`, and runs them as the bot's OS user. Only allow-listed user ids
|
|
are served (everyone else is ignored and logged), and only in private chats. In groups the bot only classifies
|
|
messages for triggers: it never runs the agent for a group member, and the messages of everyone in a watched group
|
|
(not only allowed users) are sent to OpenRouter and logged. `ADMIN_USER_ID` can act as any
|
|
allow-listed user with `/impersonate`, so it gets that user's workspace, sessions, jobs and connected calendar. Run it in the container image
|
|
([docker.md](docker.md)), which is the recommended setup, or at least as a dedicated user, and treat the workspace root
|
|
as writable by anyone on the allow-list.
|
|
|
|
Scheduled watcher scripts run unattended as the bot's OS user, with the same trust as the `bash` tool that every
|
|
allow-listed user already has, and the same filtered environment.
|
|
|
|
`send` refuses paths outside the chat's workspace, symlinks included, so a prompt-injected "send ~/.ssh/id_rsa" fails
|
|
visibly (it is no barrier against `bash cp`). The speech API key is kept out of the agent's processes like the bot
|
|
token.
|
|
|
|
Secrets are removed from the environment of `bash`, scheduled scripts and MCP servers: every variable on the deny-list
|
|
(see `ProcessEnvironment:Deny` above) is dropped, and so is any variable whose value is the bot token, whatever its
|
|
name (a Coolify alias, for example). `PROCESS_ENV_ALLOW` lets a named variable through, for example `GH_TOKEN` so the
|
|
agent can use `gh`. At startup the bot logs which rules apply, names only:
|
|
`Process environment: 15 deny rules, allow: GH_TOKEN`.
|
|
|
|
On Linux the bot's own environment can't be read through `/proc/<pid>/environ` or ptrace: at startup the process
|
|
makes itself non-dumpable (`prctl(PR_SET_DUMPABLE, 0)`). Set `DOTNET_EnableDiagnostics=0` in the bot's environment
|
|
too (the Docker image, [docker.md](docker.md), sets it): otherwise the .NET diagnostic socket still hands the
|
|
environment to any process of the same user, and the bot logs a warning saying so.
|
|
|
|
The bot token and the other configured secrets are replaced by `[REDACTED]` in tool output, so the model never sees
|
|
them in a tool result, whichever file or process they were read from. Encoded forms (base64, URL-encoded, split
|
|
across lines) aren't matched.
|
|
|
|
`ProcessEnvironment:HardenHostProcess=false` (`ProcessEnvironment__HardenHostProcess=false` in the environment),
|
|
together with unsetting `DOTNET_EnableDiagnostics`, turns the hardening off for a debugging session. While it is on:
|
|
|
|
- the bot writes no core dumps, including .NET's own `createdump` on a crash;
|
|
- `/proc/<pid>/*` of the bot is owned by root, so tools that read it for that pid (such as `lsof -p`) see less;
|
|
- `dotnet-dump`, `dotnet-trace`, `dotnet-counters` and debuggers can't attach.
|
|
|
|
The agent's own processes are unaffected. Turn it back on once you're done.
|
|
|
|
This is not a sandbox. The agent still runs as the bot's user and can read the agent dir, session transcripts and any
|
|
file that user can read. The container is the isolation boundary.
|
|
|
|
## Connection loss
|
|
|
|
Polling uses Telegram.Bot's built-in loop with a hardened error handler:
|
|
|
|
- a failed `getUpdates` waits 1 s, 2 s, 4 s … up to 60 s (±20 % jitter) before the next attempt, instead of retrying
|
|
in a tight loop; the first successful poll resets the delay and logs `Telegram connection restored`;
|
|
- the update offset only moves past updates that were handed on, so reconnecting neither loses nor repeats messages;
|
|
- if the loop itself ever ends while the host runs, it is restarted from the last offset;
|
|
- a `401` (revoked or wrong token) is the one error it does not retry: the host logs it and exits with code 1.
|
|
|
|
A reply that fails to send (Telegram unreachable mid-run) is retried after 1 s and 3 s, then logged as lost; the
|
|
render loop keeps going either way.
|