ADMIN_USER_IDS (Telegram:AdminUserIds) takes a comma separated list; the older ADMIN_USER_ID / Telegram:AdminUserId still works, also takes a list and is merged. Empty entries and duplicates are ignored, non-numeric entries fail validation. Impersonation is now per admin: one admin per target, an impersonating admin can't be impersonated, and every admin's chat menu lists /impersonate.
277 lines
21 KiB
Markdown
277 lines
21 KiB
Markdown
# Running the Telegram bot in Docker
|
|
|
|
The repo's `Dockerfile` and `docker-compose.yml` run the [Telegram bot](telegram-bot.md) as one container, on
|
|
Coolify or any Docker host. The bot uses long polling, so the container needs no open port and no domain.
|
|
|
|
Local run:
|
|
|
|
```bash
|
|
cp .env.example .env # fill in TELEGRAM_BOT_TOKEN and TELEGRAM_ALLOWED_USER_IDS
|
|
docker compose up -d --build
|
|
docker compose logs -f bot
|
|
```
|
|
|
|
`dotnet run` (see [telegram-bot.md](telegram-bot.md#setup)) stays the development path.
|
|
|
|
## What's in the image
|
|
|
|
- The ASP.NET Core 10 runtime (`mcr.microsoft.com/dotnet/aspnet:10.0-noble`) and the published host in `/app`. No
|
|
.NET SDK, so the agent can't build .NET projects. The image needs the ASP.NET Core runtime, not just the plain
|
|
.NET runtime, because the [A2A server](a2a.md) hosts Kestrel when it's turned on.
|
|
- Tools for the agent's `bash`: `bash`, `git`, `ripgrep`, `python3` (with `python-is-python3` and `python3-venv`),
|
|
`curl`, `ca-certificates`, `tzdata`, `jq`, `less`, `procps`, `openssh-client`.
|
|
- Node.js 24 from NodeSource, with `npm` and `npx`.
|
|
- The `claude` CLI (`@anthropic-ai/claude-code`), which the Claude Code provider drives.
|
|
- Optionally `@playwright/mcp` 0.0.82 (the `playwright-mcp` binary) and Chromium under `/opt/ms-playwright`
|
|
(`PLAYWRIGHT_BROWSERS_PATH`), for the browser tools in [mcp.md](mcp.md).
|
|
|
|
The bot and everything it starts run as `app` (uid/gid 1654, `HOME=/home/app`). The image's own user stays root:
|
|
the entrypoint `setpriv` drops to `app` before it starts `dotnet`, so PID 1 (`docker-init`, from compose's
|
|
`init: true`) is owned by root and the agent can't read the container's environment from `/proc/1/environ`. It also
|
|
empties the capability bounding set and sets `no_new_privs`, so nothing the agent runs regains root through a setuid
|
|
binary such as `su` or `mount`. Don't set `user:` in the compose file: `setpriv` needs root to switch users. For the
|
|
same reason `docker exec` opens a root shell; add `-u app` to act as the bot. Any command that writes under `HOME`
|
|
(`npm`, `npx`, `git config`, `gh auth`, `claude`) must run with `-u app`: as root it leaves root-owned files in
|
|
`/home/app` (`HOME` is set image-wide), and the bot's `npx` and `git` then fail with `EACCES` until the next redeploy.
|
|
An interactive `app` session should also start from a clean environment (see
|
|
[First run and Claude login](#first-run-and-claude-login)).
|
|
|
|
## Build arguments
|
|
|
|
| Argument | Default | Purpose |
|
|
|---|---|---|
|
|
| `INSTALL_PLAYWRIGHT` | `false` in the `Dockerfile`, `true` in `docker-compose.yml` | Adds Playwright MCP and Chromium, about 400 MB |
|
|
| `CLAUDE_CODE_VERSION` | `2.1` | npm version spec for `claude`: `2.1` is the newest 2.1.x at build time; an exact version such as `2.1.3` pins it |
|
|
| `NODE_MAJOR` | `24` | Node.js major version from NodeSource |
|
|
| `PLAYWRIGHT_MCP_VERSION` | `0.0.82` | Must equal the version in `PlaywrightPreset.DefaultPackage`; `PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage` fails when they differ. The compose file doesn't override it |
|
|
|
|
The compose file passes `INSTALL_PLAYWRIGHT` and `CLAUDE_CODE_VERSION` from `.env` (or Coolify's variables).
|
|
Without compose:
|
|
|
|
```bash
|
|
docker build --build-arg INSTALL_PLAYWRIGHT=true -t volcanic-agentic-core .
|
|
docker run -d --init --shm-size 1g --restart unless-stopped --env-file .env \
|
|
-e PLAYWRIGHT_ENABLED=true -e PLAYWRIGHT_HEADLESS=true -e PLAYWRIGHT_EXTRA_ARGS=--no-sandbox \
|
|
-e PLAYWRIGHT_COMMAND=playwright-mcp \
|
|
-v workspaces:/data/workspaces -v agent:/data/agent -v claude:/data/claude volcanic-agentic-core
|
|
```
|
|
|
|
`--init` is required: without it `dotnet` is PID 1, so nothing reaps the zombies that killed `claude` trees and `bash`
|
|
children leave behind, and PID 1 is no longer root's. `--shm-size 1g` keeps Chromium from crashing. The compose
|
|
file's defaults (the `-e` lines, which win over `--env-file`) don't apply to a plain `docker run`, and neither does
|
|
its `TELEGRAM_MODEL` / `LOG_LEVEL` mapping: pass `-e Telegram__Model=...` instead.
|
|
|
|
## Configuration
|
|
|
|
Everything is set through environment variables: `.env` locally, the resource's environment variables on Coolify.
|
|
The compose file maps them into the container.
|
|
|
|
| Variable | Default | Purpose | Agent's processes see it |
|
|
|---|---|---|---|
|
|
| `TELEGRAM_BOT_TOKEN` | — | Required. The token from @BotFather | No, and redacted from tool output |
|
|
| `TELEGRAM_ALLOWED_USER_IDS` | — | Required. Comma-separated Telegram user ids; the first one is the owner (the `🟢 Online.` message, Claude re-login prompts) | Yes |
|
|
| `ADMIN_USER_IDS` | — | Telegram user ids, comma separated, that may `/impersonate` other users ([telegram-bot.md](telegram-bot.md#impersonation)) | Yes |
|
|
| `ADMIN_USER_ID` | — | Older single-admin form; also takes a list and is merged with `ADMIN_USER_IDS` | Yes |
|
|
| `TELEGRAM_MODEL` | `claude-code/claude-opus-5-5` | Model every chat uses; compose passes it as `Telegram__Model` | Yes, as `Telegram__Model` |
|
|
| `TZ` | `UTC` | The container's time zone | The agent's `bash` gets the chat's zone as `TZ` instead |
|
|
| `LOG_LEVEL` | `Information` | Passed as `Logging__LogLevel__Default` | Yes |
|
|
| `OPENAI_API_KEY` | — | Turns on speech to text for voice notes and the `voice` tool | No, and redacted |
|
|
| `STT_MODEL` | `gpt-4o-transcribe` | Transcription model | Yes |
|
|
| `TTS_MODEL` | `gpt-4o-mini-tts` | Speech model of the `voice` tool | Yes |
|
|
| `TTS_VOICE` | `nova` | Default voice of the `voice` tool | Yes |
|
|
| `TTS_PROVIDER` | — (ElevenLabs if configured) | `elevenlabs` or `openai`: which service the `voice` tool prefers; the other is the fallback | Yes |
|
|
| `ELEVENLABS_API_KEY` | — | With `ELEVENLABS_VOICE_ID`, makes ElevenLabs speak for the `voice` tool | No, and redacted |
|
|
| `ELEVENLABS_VOICE_ID` | — | Default ElevenLabs voice (id, or a voice name in the account) | Yes |
|
|
| `ELEVENLABS_MODEL` | `eleven_multilingual_v2` | ElevenLabs model; `eleven_v3` also reads `instructions` as audio tags | Yes |
|
|
| `OPENROUTER_API_KEY` | — | Turns on group watching with topic triggers, classified by Jev through OpenRouter ([telegram-bot.md](telegram-bot.md#groups-and-triggers)) | No, and redacted |
|
|
| `TRIGGER_MODEL` | `typesafe/jev-1.13` | The Jev model triggers use | Yes |
|
|
| `GOOGLE_CLIENT_ID` | — | With the secret, turns on Google Calendar ([google-calendar.md](google-calendar.md)) | Yes |
|
|
| `GOOGLE_CLIENT_SECRET` | — | See above. Passed to the agent's processes (gmail/gdrive skills), its value redacted from tool output | Yes, and redacted |
|
|
| `MCP_ENCRYPTION_SECRET` | — | Seals each chat's remote MCP keys; without it the bot token does ([mcp.md](mcp.md#remote-servers-per-chat)) | No, and redacted |
|
|
| `A2A_ENABLED` | `false` | Serves other agents over A2A on port 8080 ([a2a.md](a2a.md), [telegram-bot.md#a2a](telegram-bot.md#a2a)); passed as `A2A__Enabled`. Needs a domain in Coolify (see below) | No |
|
|
| `A2A_PUBLIC_URL` | — | External base URL for the agent card and push webhooks, e.g. `https://bot.example.com`; passed as `A2A__PublicUrl`. Required for push notifications to work | No |
|
|
| `A2A_CLIENT_ENABLED` | `false` | Lets each chat's assistant call other A2A agents (the `a2a` tool, agents in the chat's `.volcanic/a2a/agents.json`; [a2a.md](a2a.md#client)); passed as `A2AClient__Enabled`. Independent of `A2A_ENABLED`; needs no port or domain, only outbound HTTPS | No |
|
|
| `DEFAULT_TIMEZONE` | the container's `TZ` | IANA zone for chats that never ran `/timezone`, e.g. `Europe/Berlin`. An unknown name stops startup | Yes |
|
|
| `PROCESS_ENV_ALLOW` | — | Comma-separated names or globs let through despite the deny rules, e.g. `GH_TOKEN` | Yes |
|
|
| `PLAYWRIGHT_ENABLED` | `true` | Browser tools on or off. Set it to `false` for an image built with `INSTALL_PLAYWRIGHT=false` | Yes |
|
|
| `PLAYWRIGHT_HEADLESS` | `true` | The container has no display | Yes |
|
|
| `PLAYWRIGHT_BROWSER` | `chromium` | Only Chromium is installed in the image | Yes |
|
|
| `PLAYWRIGHT_EXTRA_ARGS` | `--no-sandbox` | Extra `playwright-mcp` arguments. Keep `--no-sandbox` when you override it: Chromium's sandbox needs user namespaces, which Docker's default seccomp profile denies | Yes |
|
|
| `PLAYWRIGHT_COMMAND` | `playwright-mcp` | The preinstalled server, so the first browser call doesn't wait for an `npx` download | Yes |
|
|
| `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_0`, `GIT_CONFIG_VALUE_0` | `1`, `safe.directory`, `*` | Fixed in the compose file: git trusts every repository, so repositories copied in from the host (owned by another uid) don't fail with "dubious ownership" | Yes |
|
|
|
|
`TELEGRAM_MODEL` and `LOG_LEVEL` are read by the compose file only, which passes them to the bot as `Telegram__Model`
|
|
and `Logging__LogLevel__Default`; `INSTALL_PLAYWRIGHT` and `CLAUDE_CODE_VERSION` are build arguments (see above).
|
|
|
|
> **Never set `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `CLAUDE_CODE_OAUTH_TOKEN`,
|
|
> `CLAUDE_CODE_USE_BEDROCK` or `CLAUDE_CODE_USE_VERTEX`.** The Claude Code provider runs on a Claude subscription only
|
|
> and refuses to run when one of them is present. In Coolify, check shared, project and team variables too: they are
|
|
> the likely way one gets in.
|
|
|
|
### What the agent can't see
|
|
|
|
The process environment filter removes variables from the environment of the agent's `bash`, scheduled scripts and
|
|
MCP servers:
|
|
|
|
- anything matching the default deny rules: `*_API_KEY`, `*_APIKEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD` and their
|
|
`Section__Key` forms (`*__APIKEY`, `*__*TOKEN`, `*__*SECRET`, `*__*PASSWORD`), plus `TELEGRAM_BOT_TOKEN`,
|
|
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` and `CLAUDE_CODE_OAUTH_TOKEN`;
|
|
- any variable whose value is the bot token, the speech or OpenRouter API key or the Google client secret, whatever its name.
|
|
|
|
The one exception is `GOOGLE_CLIENT_SECRET` itself: the bot always allows it, so skills such as gmail and gdrive can
|
|
use the OAuth client. Its value is still replaced by `[REDACTED]` in tool output, like all the values above. The
|
|
startup log names the rules, never values: `Process environment: 14 deny rules, allow: GOOGLE_CLIENT_SECRET`.
|
|
|
|
To give the agent a credential on purpose, add the variable in Coolify, add it to `environment:` in the compose file,
|
|
and list its name in `PROCESS_ENV_ALLOW` (`PROCESS_ENV_ALLOW=GH_TOKEN` lets `gh` work). A name that is on the deny
|
|
list by name, such as `TELEGRAM_BOT_TOKEN`, can't be allowed: trying stops startup. Listing `GOOGLE_CLIENT_SECRET`
|
|
there is harmless.
|
|
|
|
### Other settings
|
|
|
|
Any setting of the bot is reachable as `Section__Key`: add the variable in Coolify and a matching line under
|
|
`environment:` in `docker-compose.yml`. For example `Telegram__ReconnectMaxDelay=00:02:00`, `Telegram__InputDebounce`,
|
|
`Telegram__Speech__Language`, `Scheduler__MaxJobsPerTarget`, `GoogleCalendar__*`, `ProcessEnvironment__Deny__0`, and
|
|
the provider's `VOLCANIC_CLAUDE_CODE_IDLE_TIMEOUT_MS` / `VOLCANIC_CLAUDE_CODE_TOTAL_TIMEOUT_MS`. The settings are
|
|
listed in [telegram-bot.md](telegram-bot.md#configuration) and [claude-code-provider.md](claude-code-provider.md#configuration).
|
|
|
|
The image sets these; normally leave them alone:
|
|
|
|
| Variable | Value | |
|
|
|---|---|---|
|
|
| `Telegram__WorkspaceRoot` | `/data/workspaces` | One directory per chat |
|
|
| `VOLCANIC_AGENT_DIR` | `/data/agent` | Sessions, settings, scheduler jobs, calendar tokens |
|
|
| `CLAUDE_CONFIG_DIR` | `/data/claude` | The Claude login |
|
|
| `HOME` | `/home/app` | |
|
|
| `DOTNET_EnableDiagnostics` | `0` | Turns off the .NET diagnostic socket, which would hand the bot's environment to the agent's processes. Unset it, together with `ProcessEnvironment__HardenHostProcess=false`, only to attach `dotnet-dump` or `dotnet-trace` while debugging |
|
|
| `DOTNET_CLI_TELEMETRY_OPTOUT`, `DOTNET_NOLOGO` | `1` | |
|
|
|
|
## Volumes
|
|
|
|
The compose file binds them to directories under `/media/disk1/containers/volcanic_secretary_core/` on the host
|
|
(`workspaces/`, `agent/`, `claude/`); edit the three `volumes:` lines to put them elsewhere.
|
|
|
|
| Volume | Path | Contents |
|
|
|---|---|---|
|
|
| `workspaces` | `/data/workspaces` | One directory per chat (`/data/workspaces/<chatId>`): `AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`, skills (`.volcanic/skills/<name>/SKILL.md`), `inbox/` (received files), `browser/` (screenshots, downloads), project-level `.volcanic/extensions/`, the Google Calendar token (`.volcanic/google-calendar/token.json`), scheduled jobs (`.volcanic/scheduler/jobs.json`), A2A callers and tasks, remote agents and watches (`.volcanic/a2a/`, [a2a.md](a2a.md#files), [a2a.md](a2a.md#client)) |
|
|
| `agent` | `/data/agent` | Sessions (`sessions/telegram-<chatId>/`), `settings.json`, `extensions/*.dll`, chat time zones (`telegram/chat-settings.json`); a `scheduler/jobs.json` from before jobs were per chat is moved into the workspaces on start |
|
|
| `claude` | `/data/claude` | The Claude login (credentials and onboarding state) |
|
|
|
|
Bind mounts survive redeploys and deleting the resource. Docker creates missing host directories as root; the
|
|
entrypoint chowns the three mount roots (not their contents) to uid 1654 before it drops to `app`, so files copied in
|
|
from elsewhere need `chown -R 1654:1654 <dir>` on the host. Named volumes (`-v workspaces:/data/workspaces`, as in
|
|
the `docker run` example) work too. On an NTFS disk (`fuseblk`, as `/media/disk1` is) ownership and modes aren't
|
|
stored: everything shows as root with mode 777, `chown` is a silent no-op, and `app` can still write.
|
|
|
|
`HOME` (`/home/app`) isn't a volume: `~/.gitconfig`, `~/.ssh`, `gh` logins and the npm cache are lost on redeploy.
|
|
|
|
## Deploying on Coolify
|
|
|
|
1. Create a resource from this repository with the *Docker Compose* build pack.
|
|
2. Set `TELEGRAM_BOT_TOKEN` and `TELEGRAM_ALLOWED_USER_IDS` (owner first) and any optional variables from
|
|
[Configuration](#configuration). Leave the domain empty, unless you're turning A2A on (next).
|
|
3. Deploy. The log shows lines such as `Using Claude Code at /usr/bin/claude, model claude-code/claude-opus-5-5` and
|
|
`Allowed users: <ids>. Workspaces under /data/workspaces`, and the owner gets `🟢 Online.`.
|
|
|
|
There is no healthcheck, because by default the bot serves no HTTP. Lost connectivity is retried with backoff
|
|
inside the process. A rejected token (`401`) exits with code 1 and invalid configuration exits with code 2;
|
|
`restart: unless-stopped` turns either into a restart loop you can see in the logs.
|
|
|
|
### A2A
|
|
|
|
With `A2A_ENABLED=true` the bot also listens on port 8080 for [A2A](a2a.md) requests. The compose file already
|
|
`expose`s it to the Coolify network; give the resource a **domain** so Coolify's proxy routes it there (without a
|
|
domain, nothing outside the container can reach the port). Set `A2A_PUBLIC_URL` to that domain
|
|
(`https://bot.example.com`) so the agent card and push webhooks advertise the right address — without it they fall
|
|
back to the inbound request's own host, which is wrong behind a reverse proxy. The log gains
|
|
`A2A: listening on ...`; if port 8080 is somehow already taken, the bot logs a critical line and exits with code 3
|
|
instead of starting half-working. Leaving `A2A_ENABLED` unset or `false` opens no port at all, same as before.
|
|
|
|
`A2A_CLIENT_ENABLED=true` turns on the other direction: the bot calls other agents. That needs no domain. With the
|
|
server on as well and `A2A_PUBLIC_URL` set, remote agents can push task updates to
|
|
`<A2A_PUBLIC_URL>/a2a/push/<watchId>`; without it the bot subscribes or polls. The log shows
|
|
`A2A client: on, ...` and, shortly after start, `A2A client: N watch(es) restored`.
|
|
|
|
## First run and Claude login
|
|
|
|
1. The owner opens the bot, sends `/start`, then `/claude-code-login`.
|
|
2. The login link and the code prompt arrive as one message. Open the link, sign in, and paste the code shown on the
|
|
page back into the chat. The credentials are stored in `/data/claude`.
|
|
3. `/claude-code-doctor` should now report that you're logged in through `claude.ai` and that the bridge works.
|
|
|
|
When the login expires later, the owner gets the same prompt automatically and the paused request continues after
|
|
the code is pasted.
|
|
|
|
Without the bot:
|
|
|
|
```bash
|
|
docker exec -u app -it <container> env -i HOME=/home/app \
|
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin CLAUDE_CONFIG_DIR=/data/claude TERM=xterm \
|
|
claude auth login --claudeai
|
|
```
|
|
|
|
The `-u app` matters: `docker exec` runs as root by default, and credentials written by root can't be read by the bot.
|
|
So does `env -i`: a `docker exec` process gets the whole container environment, bot token and API keys included, and
|
|
while it runs as `app` the agent's `bash` or a scheduled watcher can read it from `/proc/<pid>/environ`.
|
|
|
|
## Updating
|
|
|
|
Redeploy to rebuild the image. The `claude` CLI can't update itself inside the container (its install directory is
|
|
owned by root), so a new version only arrives with a build. The `npm install` layer is cached, though: to pick up a
|
|
newer 2.1.x, build without cache (Coolify's force deploy without cache, or `docker compose build --no-cache`), or set
|
|
`CLAUDE_CODE_VERSION` to an exact version, which also pins it. A Claude Code upgrade invalidates the prompt cache
|
|
once.
|
|
|
|
Bumping Playwright MCP means changing `PlaywrightPreset.DefaultPackage` and `ARG PLAYWRIGHT_MCP_VERSION` in the
|
|
`Dockerfile` in the same commit; the unit test above catches a mismatch.
|
|
|
|
## Extensions and project context
|
|
|
|
Extension assemblies go into `/data/agent/extensions/` (every chat) or `/data/workspaces/<chatId>/.volcanic/extensions/`
|
|
(one chat), then restart:
|
|
|
|
```bash
|
|
docker cp MyExtension.dll <container>:/data/agent/extensions/
|
|
docker exec <container> chown app:app /data/agent/extensions/MyExtension.dll
|
|
docker restart <container>
|
|
```
|
|
|
|
Build them against the same `Volcanic.AgenticCore.Abstractions` version as the image. See [extensions.md](extensions.md).
|
|
|
|
Project context files (`AGENTS.md` or `CLAUDE.md`, `PERSONA.md`, `MEMORY.md`) live in the chat's workspace directory.
|
|
The agent can edit them, or you can with `docker exec -u app -it <container> env -i HOME=/home/app
|
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin TERM=xterm bash` (clean environment, for the reason
|
|
given under [First run and Claude login](#first-run-and-claude-login)).
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause and fix |
|
|
|---|---|
|
|
| `[executable_missing]` in a reply, or `Claude Code (claude) was not found on PATH` at startup | The image has no working `claude`. Rebuild and check that `claude --version` ran in the build log |
|
|
| `[auth]` in a reply | The Claude login is missing or expired: `/claude-code-login`, or the `docker exec -u app` command above |
|
|
| `[mcp_startup]` in a reply | The bridge files are missing from `/app`: `Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll` and `Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.runtimeconfig.json`. The build checks for both, so rebuild from a clean tree |
|
|
| `Permission denied` under `/data` | Files in the bind-mounted directories owned by another user: `chown -R 1654:1654 /media/disk1/containers/volcanic_secretary_core` on the host |
|
|
| `Telegram reports another poller or a webhook for this bot (409)` | Another instance polls with the same token (an old container, a local `dotnet run`), or the bot has a webhook. Stop the other instance; remove a webhook with `https://api.telegram.org/bot<token>/deleteWebhook` |
|
|
| `Telegram rejected the bot token (401 Unauthorized)`, restart loop | Wrong or revoked `TELEGRAM_BOT_TOKEN` |
|
|
| Browser tools missing or failing | The image was built with `INSTALL_PLAYWRIGHT=false`, `PLAYWRIGHT_ENABLED` is off, or `PLAYWRIGHT_EXTRA_ARGS` lost `--no-sandbox`. Chromium crashes also come from a small `/dev/shm`; keep `shm_size: 1gb` |
|
|
| Build fails at `dotnet publish` with `NETSDK1004: Assets file '.../obj/project.assets.json' not found` | The `Dockerfile` restores packages in a cached layer that copies an explicit list of project files; a project the host references is missing from it. Add its `.csproj` `COPY` line next to the others (the A2A server and client projects were added there after exactly this failure) |
|
|
| The provider refuses to run because of an API key | One of the `ANTHROPIC_*` / `CLAUDE_CODE_*` variables above is set, possibly as a shared Coolify variable. Remove it |
|
|
|
|
## Security
|
|
|
|
The container is the isolation boundary. Inside it, the agent's `bash` runs as `app`, the same user as the bot: it
|
|
can read the workspaces, the agent directory with every chat's sessions, and the Claude login in `/data/claude`.
|
|
The configured secrets are kept out of its way: they are filtered out of its environment, the bot process is
|
|
non-dumpable (its `/proc/<pid>/environ` is unreadable and it can't be attached to), PID 1 is root's so
|
|
`/proc/1/environ` is unreadable too, the diagnostic socket is off, setuid binaries can't raise its privileges, and
|
|
secret values are redacted from tool output.
|
|
|
|
That stops secrets leaking by accident, not an agent that goes after them on purpose. `app` owns `/data/workspaces`
|
|
and `/data/agent`, so the agent can drop a DLL into `<workspace>/.volcanic/extensions/` (loaded into the bot process,
|
|
with its unfiltered environment, at the next `/new`) or add an MCP server whose `env` uses `${env:TELEGRAM_BOT_TOKEN}`
|
|
to `/data/agent/settings.json`, then kill the bot so that `restart: unless-stopped` brings it back with that server.
|
|
Treat everyone on the allow-list as able to obtain every secret you configure. None of this is a sandbox: anyone on
|
|
the allow-list can run any command in the container.
|
|
|
|
Details in [telegram-bot.md](telegram-bot.md#security).
|