Skip to content

Commit e9fd0fe

Browse files
jkyberneeesclaude
andauthored
fix(security): audit remediation — Telegram path traversal + approval-grant leak (#25)
* fix(security): patch Telegram path traversal and approval-grant leak Adds FIX_PLAN.md (audit remediation tracker) and fixes the two highest- priority, lowest-risk findings: - Telegram document path traversal (Critical): DownloadDocument wrote the attacker-controlled filename through filepath.Join with no sanitization, allowing writes outside ~/.odek/media/ (e.g. ../../.ssh/authorized_keys or ../../.odek/config.json) — a remote arbitrary-write/RCE primitive. New sanitizeDocName() strips directory components and rejects degenerate names. Tests: TestSanitizeDocName, TestDownloadDocument_NoTraversal. - Batch approval grant leak (High): the approved-batch trustAll grant was reset via `defer` inside the iteration loop, so it only fired at function return — leaving trustAll set for every later iteration and auto-approving all subsequent dangerous tools. The grant is now reset at the end of each iteration's tool phase. Regression test: TestBatchApprovalTrustAllNotLeakedAcrossIterations. Remaining audit items are tracked in FIX_PLAN.md; design-sensitive ones (fail-closed Telegram auth, WS auth token) are left for maintainer direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(loop): add function-exit backstop for batch trustAll grant Verification (vprotocol axis 2.5) found the per-iteration reset alone did not cover abnormal exits: an early return or panic within an iteration that had trustAll set could leak the grant into a later prompt, since wsApprover (per-connection) and TelegramApprover (per-chat) are reused across prompts. Restore a function-level deferred reset as a backstop — matching the safety the original defer provided — while keeping the per-iteration reset as the primary cross-iteration fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): fail-closed Telegram authorization (#3) An empty allowlist previously meant "allow all": isAllowed returned true when both AllowedChats and AllowedUsers were empty, and DefaultConfig left them empty with no warning. Anyone who found the bot could drive the agent and its shell/file tools. Now authorization is fail-closed: - ValidateConfig refuses to start when no allowlist is configured, unless the operator explicitly sets ODEK_TELEGRAM_ALLOW_ALL=true. - isAllowed denies when both allowlists are empty and AllowAllUsers is unset (defense in depth beyond the startup gate). - Startup logs a loud warning when running open via the opt-in. Adds the AllowAllUsers field + ODEK_TELEGRAM_ALLOW_ALL env var, wires it through HandlerConfig, and updates docs (TELEGRAM.md, SECURITY.md, .env.example). Existing deployments that set an allowlist are unaffected. Tests: TestValidateConfig_noAllowlistFailsClosed / _allowAllOptIn / _allowlistSatisfiesCheck, TestConfigFromEnv_allowAll, TestIsAllowed_* updated for the new semantics. Routing tests opt in via AllowAllUsers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): gate Telegram callback queries with isAllowed (#3) Challenging the #3 fix surfaced a remaining authorization bypass: HandleUpdate routes callback queries (inline-button presses) straight to handleCallback, which never called isAllowed — only handleMessage did. A characterization test (TestHandleUpdate_CallbackQueryNotAllowed) even documented the gap as "current behavior". Callbacks could therefore drive per-chat approval/clarify state and trigger outbound AnswerCallbackQuery without authorization. handleCallback now applies the same allowlist as messages, using cq.Message.Chat.ID and cq.From.ID (user 0 when From is absent). Legitimate flows are unaffected: anyone who can press a button legitimately is already allowlisted (they had to send a message to start the run). Updated the characterization test to assert the new deny behavior and added TestHandleCallback_RespectsAllowlist. Callback routing tests opt in via AllowAllUsers. A residual group-chat concern (any member can press another user's buttons when only AllowedChats is set) is noted in FIX_PLAN.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 3cf96f6 commit e9fd0fe

18 files changed

Lines changed: 681 additions & 47 deletions

FIX_PLAN.md

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# FIX_PLAN.md
2+
3+
Remediation plan for bugs and security vulnerabilities identified in the
4+
security/correctness audit (2026-06-10). Findings are ordered by priority.
5+
Each entry lists the location, the flaw, the concrete fix, and verification
6+
steps. Check items off as they land.
7+
8+
## Priority order (fix these first)
9+
10+
1. **#1** — Telegram document path traversal (remote arbitrary-write/RCE). ✅ Fixed
11+
2. **#3** — Fail-open Telegram authorization (amplifies #1). ✅ Fixed
12+
3. **#2**`trustAll` never resets within a run (one click unlocks the turn). ✅ Fixed
13+
4. **#4 / #5** — WS auth + serve-mode approval deadlock.
14+
15+
> Status: #1, #2, and #3 are implemented and tested in this PR. The remaining
16+
> items are open. #4 and #6 involve a behavior/default change (WS token) and are
17+
> left for maintainer direction.
18+
19+
---
20+
21+
## 1. Path traversal → arbitrary file write via Telegram document filename
22+
- **Status:** ✅ Fixed — `sanitizeDocName` strips directory components; tests in
23+
`download_test.go` (`TestSanitizeDocName`, `TestDownloadDocument_NoTraversal`).
24+
- **Severity:** Critical
25+
- **Location:** `internal/telegram/download.go:156-164`
26+
- **Flaw:** `DownloadDocument` uses the sender-controlled `Document.FileName`
27+
directly: `safeName := fileName; localPath := filepath.Join(dir, safeName)`.
28+
`filepath.Join` collapses `../`, so a document named
29+
`../../../home/<user>/.odek/config.json` (or `../../.ssh/authorized_keys`)
30+
writes outside `~/.odek/media/`. Both path and contents are attacker-
31+
controlled → RCE/persistence. Photo/voice paths are safe (hashed names).
32+
- **Fix:**
33+
- `safeName = filepath.Base(filepath.Clean(fileName))`.
34+
- Reject if the result is empty, `.`, `..`, or still contains a path
35+
separator; fall back to the generated `doc_<fileID>` name.
36+
- Confirm the final `localPath` is still within `dir` (defense in depth):
37+
resolve both with `filepath.Abs` and check `strings.HasPrefix`.
38+
- **Verify:** unit test in `internal/telegram` sending a `FileName` of
39+
`../../evil.txt` and asserting the write stays under `~/.odek/media/`.
40+
41+
## 2. Approval bypass: `trustAll` never resets within a run
42+
- **Status:** ✅ Fixed — grant is now reset at the end of each iteration's tool
43+
phase (not via `defer`). Regression test:
44+
`TestBatchApprovalTrustAllNotLeakedAcrossIterations` in `loop_test.go`.
45+
- **Severity:** High
46+
- **Location:** `internal/loop/loop.go:885-891`
47+
- **Flaw:** `defer ta.SetTrustAll(false)` sits inside the
48+
`for i := 0; i < e.maxIter` loop, so it fires at function return, not at
49+
iteration end. After one approved batch, all later dangerous ops auto-approve
50+
for the rest of the turn (incl. destructive/blocked classes).
51+
- **Fix:** scope the reset to the iteration — set `trustAll` true before tool
52+
execution and `false` immediately after (e.g. wrap the phase in a closure, or
53+
add an explicit `ta.SetTrustAll(false)` at the end of the iteration body).
54+
Do not rely on `defer` inside the loop.
55+
- **Verify:** test that a second dangerous tool call in a later iteration of the
56+
same run still triggers `PromptCommand`.
57+
58+
## 3. Fail-open Telegram authorization
59+
- **Status:** ✅ Fixed — `ValidateConfig` now refuses to start with no allowlist
60+
unless `ODEK_TELEGRAM_ALLOW_ALL=true` is set; `isAllowed` is fail-closed
61+
(empty lists + no opt-in → deny); startup logs a loud warning when running
62+
open. **Also closed a callback-query authorization bypass** found while
63+
challenging the fix: `handleCallback` (inline-button presses) did not call
64+
`isAllowed` — it now does, so callbacks are gated like messages. Tests:
65+
`TestValidateConfig_noAllowlistFailsClosed`, `TestValidateConfig_allowAllOptIn`,
66+
`TestConfigFromEnv_allowAll`, `TestIsAllowed_EmptyAllowlist` (now asserts deny),
67+
`TestIsAllowed_EmptyAllowlistWithAllowAll`, `TestHandleCallback_RespectsAllowlist`,
68+
`TestHandleUpdate_CallbackQueryNotAllowed` (now asserts deny).
69+
- **Note (not fixed — pre-existing, lower priority):** in a *group* chat where
70+
only `AllowedChats` is set, any group member can press another user's
71+
approval/clarify buttons (callback passes the chat-level allowlist). Tightening
72+
this requires binding an approval to the specific user who triggered the run —
73+
out of scope for this PR.
74+
- **Severity:** High
75+
- **Location:** `internal/telegram/handler.go:505-533`; defaults in
76+
`internal/telegram/config.go`
77+
- **Flaw:** `isAllowed` returns `true` when both `AllowedChats` and
78+
`AllowedUsers` are empty, and the default config leaves both empty with no
79+
warning. Anyone who finds the bot gets full (possibly godmode) access.
80+
- **Fix:** make the bot fail-closed — if no allow-list is configured, either
81+
refuse to start (log a fatal/clear error) or run with tools disabled.
82+
Emit a loud startup warning when the allow-list is empty.
83+
- **Verify:** start the bot with no `ODEK_TELEGRAM_ALLOWED_*` env vars and
84+
confirm it refuses / restricts rather than serving all users.
85+
86+
## 4. WebSocket `/ws` (and `/api/*`) has no authentication
87+
- **Severity:** High
88+
- **Location:** `cmd/odek/serve.go:148,874-888` (and the `/api/*` handlers
89+
~904-1057)
90+
- **Flaw:** the only gate is an Origin check that returns `nil` for empty
91+
Origin, so any non-browser client (no Origin header) gets full agent control.
92+
`/api/*` handlers have no Origin check at all. Safety depends solely on the
93+
loopback bind; `--addr 0.0.0.0` removes it.
94+
- **Fix:**
95+
- Add a per-process bearer/session token required on the WS handshake and all
96+
`/api/*` requests (generate on startup, print to the operator).
97+
- Reject non-loopback `RemoteAddr` unless a token is explicitly configured.
98+
- Apply the same origin/token gate to every `/api/*` handler.
99+
- **Verify:** connect with `websocat` (no Origin) and confirm rejection without
100+
a token; confirm `--addr 0.0.0.0` still requires the token.
101+
102+
## 5. Web UI approvals deadlock and always time out
103+
- **Severity:** High
104+
- **Location:** `cmd/odek/serve.go:494-590`; `cmd/odek/wsapprover.go:153-184`
105+
- **Flaw:** `handleWS` is the only goroutine reading the socket and calls
106+
`RunWithMessages` synchronously. The approver blocks on `HandleResponse`,
107+
which is only called from that same (now-blocked) receive loop, so
108+
`approval_response` is never read → 60s timeout → denial. Approval UI in serve
109+
mode is dead.
110+
- **Fix:** read the WebSocket on a dedicated goroutine (channel-feed the receive
111+
loop), or run the prompt on a separate goroutine from the socket reader so
112+
responses can be processed while a prompt is in flight.
113+
- **Verify:** in serve mode, trigger a dangerous tool and confirm the browser
114+
approval is received and honored without timing out.
115+
116+
## 6. `write_file`/`patch` `~/.odek/` carve-out enables privilege escalation
117+
- **Severity:** High
118+
- **Location:** `cmd/odek/file_tool.go:760-768`;
119+
`internal/danger/classifier.go:177-186`; `patch` wiring in `cmd/odek/main.go`
120+
- **Flaw:** `confineToCWD` allows any path under `~/.odek/`, and
121+
`~/.odek/config.json` classifies as auto-allowed `LocalWrite`. A confined/
122+
untrusted sub-agent can rewrite its own config to disable the sandbox / enable
123+
YOLO on the next run, or drop an auto-loaded `SKILL.md`. Shell rc files
124+
(`~/.bashrc`, `~/.zshrc`, `~/.profile`) also fall through to `LocalWrite`.
125+
`patch` is created without `restrictToCWD`.
126+
- **Fix:**
127+
- Classify `~/.odek/config.json`, `~/.odek/skills/`, and the shell rc/profile
128+
files (and crontab paths) as `SystemWrite` (prompt/deny).
129+
- Exclude those paths from the `confineToCWD` carve-out.
130+
- Give `patch` the same `restrictToCWD` confinement as `write_file`.
131+
- **Verify:** confined agent attempt to write `~/.odek/config.json` and
132+
`~/.bashrc` must hit an approval/deny path, not auto-allow.
133+
134+
## 7. Data race in `wsApprover` can fatally crash serve
135+
- **Severity:** Medium
136+
- **Location:** `cmd/odek/wsapprover.go:117,175`
137+
- **Flaw:** `a.approveAll[cls] || a.trustAll` read unlocked at :117 while
138+
`a.approveAll[cls] = true` writes unlocked at :175. Parallel tool goroutines
139+
share one approver → concurrent map read/write is a fatal Go runtime error.
140+
- **Fix:** guard all `approveAll`/`trustAll` access with the existing mutex
141+
(mirror `internal/telegram/approver.go`, which locks correctly).
142+
- **Verify:** run the serve approval path under `go test -race` with parallel
143+
tool calls.
144+
145+
## 8. SSRF: URL gate is string-only, no dial-time IP check
146+
- **Severity:** Medium
147+
- **Location:** `internal/danger/classifier.go:195-244`;
148+
`cmd/odek/browser_tool.go`; `cmd/odek/perf_tools.go`
149+
- **Flaw:** `ClassifyURL` inspects only the literal hostname. A domain whose A
150+
record resolves to `169.254.169.254` / `10.x` / `192.168.x` classifies as
151+
plain `NetworkEgress`, and the HTTP clients use a default transport with no
152+
post-resolution IP guard → SSRF / DNS-rebinding to cloud metadata & internal
153+
services when egress is allowed.
154+
- **Fix:** install a custom `DialContext` on the browser/http_batch clients that
155+
re-checks the resolved `net.IP` against loopback/private/link-local/ULA ranges
156+
and refuses them. This also closes the redirect-hop and rebinding window.
157+
- **Verify:** request a host that resolves to `169.254.169.254` and confirm the
158+
dial is refused.
159+
160+
## 9. Sub-agent results lost on >64KB NDJSON line, then full-timeout hang
161+
- **Severity:** Medium
162+
- **Location:** `cmd/odek/subagent_tool.go:247-285`
163+
- **Flaw:** `runTask` reads child stdout with a default `bufio.Scanner` (64KB
164+
token cap). Streamed `tool_call` events embedding full tool args (e.g. a large
165+
`write_file`) exceed 64KB → `ErrTooLong` → reader stops → child blocks on a
166+
full pipe → `cmd.Wait` hangs until the 120s timeout kills a task that actually
167+
succeeded.
168+
- **Fix:** call `scanner.Buffer(make([]byte, 0, 64*1024), maxCap)` with a large
169+
cap, or switch to a `bufio.Reader` with `ReadString('\n')`.
170+
- **Verify:** sub-agent task whose streamed event line exceeds 64KB returns its
171+
result instead of timing out.
172+
173+
## 10. `/new` deletes the per-chat mutex while a run holds it
174+
- **Severity:** High (correctness/data-integrity)
175+
- **Location:** `cmd/odek/telegram.go:263`
176+
- **Flaw:** `/new` runs `chatMu.Delete(chatID)` on the update loop,
177+
unsynchronized with in-flight `handleChatMessage` goroutines. The next message
178+
`LoadOrStore`s a fresh mutex and `TryLock`s it → two concurrent runs for the
179+
same chat corrupt interleaved `sessionManager.Save` writes and clobber each
180+
other's approver.
181+
- **Fix:** do not delete the mutex on `/new` (a small per-chat leak is
182+
acceptable), or only delete it while holding the lock and with no active run.
183+
- **Verify:** send `/new` during an active run, then a message; confirm the two
184+
runs do not execute concurrently.
185+
186+
---
187+
188+
## Secondary findings (track, lower priority)
189+
190+
- **Telegram bot token leaked into logs**`internal/telegram/bot.go:117-120`.
191+
`*url.Error` includes the token-bearing URL. Strip credentials from logged
192+
URLs (log method only).
193+
- **`currentPromptCancel` single global slot**`cmd/odek/serve.go:37,581`.
194+
`/api/cancel` cancels the wrong prompt with multiple tabs; A finishing clobbers
195+
B's cancel. Track per-connection / per-prompt-ID.
196+
- **`/stop` cannot interrupt a pending `clarify`**`cmd/odek/telegram.go:1353`.
197+
Circular wait until the 10-minute timeout. Make clarify observe `agentCtx`.
198+
- **Sub-agent / panic edge cases in loop**
199+
`internal/loop/loop.go:927-945`: a panicked tool returns an empty result
200+
(early `return` before `results[idx]` is set); the model gets no error.
201+
- **UTF-8 byte-slicing in Telegram previews**
202+
`cmd/odek/telegram.go:305-327`. `taskPreview[:50]`/`[:80]` slice by byte and
203+
can emit invalid UTF-8 (Telegram 400). Also `Messages[0]` can index an empty
204+
slice and previews the system prompt. Use rune-aware truncation (see
205+
`truncate()` in `subagent.go`).
206+
- **`activeTaskWG.Add` races `gracefulRestart`'s `Wait`**
207+
`cmd/odek/telegram.go:1063-1076` vs `:879-961`. Invert to Add-first,
208+
re-check-flag, Done+return on bail.
209+
- **`serve --open` never opens a browser**
210+
`cmd/odek/serve.go:1104-1113`. `os.StartProcess` with a bare name does no PATH
211+
lookup. Use `exec.LookPath` / `exec.Command`.
212+
- **SearXNG `secret_key` weak sentinel**
213+
`docker/searxng/settings.yml` ships `ultrasecretkey` with a matching compose
214+
default. Mitigated (no host port) but should hard-fail on the sentinel.
215+
- **Sessions dir created `0755`**`internal/session/session.go:71`. Tighten to
216+
`0700` (files are already `0600`).
217+
- **`ffprobe` leading-dash arg injection**`cmd/odek/vision_tool.go:101-106`.
218+
Low impact; add a `--` separator before positional paths.
219+
220+
## Operational follow-up
221+
222+
- **Rotate the credentials** in the working-tree `docker/.env`
223+
(`ODEK_API_KEY`, `ODEK_TELEGRAM_BOT_TOKEN`) if this tree was ever shared,
224+
imaged, or backed up. (`docker/.env` is gitignored and was never committed —
225+
only `.env.example` is tracked.)
226+
227+
## Confirmed NOT vulnerable (do not re-investigate)
228+
229+
- `shell.go` — argv-form `exec.Command` + token-based fail-closed danger
230+
classifier.
231+
- File tools — `O_NOFOLLOW` + atomic temp-write/rename (symlink TOCTOU closed).
232+
- `subagent_key.go` — API key handed off via an unlinked FD, not env/args.
233+
- `mcp.go` — MCP servers come from local resolved config, not remote-supplied
234+
definitions; tool descriptions are injection-scanned and outputs wrapped.
235+
- `docker/.env` — gitignored, never committed (see operational follow-up for the
236+
on-disk copy).

cmd/odek/telegram.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,18 @@ func telegramCmd(args []string) error {
159159

160160
// 8. Set handler config from cfg.
161161
handler.Config = telegram.HandlerConfig{
162-
AllowedChats: cfg.AllowedChats,
163-
BotUsername: cfg.BotUsername,
164-
MaxMsgLength: cfg.MaxMsgLength,
165-
AllowedUsers: cfg.AllowedUsers,
162+
AllowedChats: cfg.AllowedChats,
163+
BotUsername: cfg.BotUsername,
164+
MaxMsgLength: cfg.MaxMsgLength,
165+
AllowedUsers: cfg.AllowedUsers,
166+
AllowAllUsers: cfg.AllowAllUsers,
167+
}
168+
169+
// Loud warning when running without an allowlist (only reachable when the
170+
// operator explicitly set ODEK_TELEGRAM_ALLOW_ALL; ValidateConfig blocks
171+
// the accidental case).
172+
if cfg.AllowAllUsers && len(cfg.AllowedChats) == 0 && len(cfg.AllowedUsers) == 0 {
173+
handlerLog.Warn("telegram bot is running with NO allowlist — ANY user can drive the agent (ODEK_TELEGRAM_ALLOW_ALL=true)")
166174
}
167175

168176
// 9. Build system prompt: explicit override > IDENTITY.md > compiled default

docker/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ GIT_COMMITTER_EMAIL=you@example.com
4141

4242
# ── Telegram bot (only for the telegram-* profiles) ──────────────────────
4343
# Get a token from @BotFather; restrict to YOUR chat/user id.
44+
# REQUIRED: set at least one allowlist below — the bot refuses to start with
45+
# none (fail-closed). Any Telegram user who finds an open bot can drive the
46+
# agent and its shell/file tools.
4447
# ODEK_TELEGRAM_BOT_TOKEN=123456:ABC-your-bot-token
4548
# ODEK_TELEGRAM_ALLOWED_CHATS=11111111 # comma-separated chat IDs
4649
# ODEK_TELEGRAM_ALLOWED_USERS=11111111 # comma-separated user IDs (optional)
50+
# ODEK_TELEGRAM_ALLOW_ALL=true # DANGER: run with NO allowlist (any user). Not recommended.
4751
# Chat ID that `odek run --deliver` and scheduled reminders send to.
4852
# Required for Telegram delivery; usually your own chat ID from ALLOWED_CHATS.
4953
# ODEK_TELEGRAM_DEFAULT_CHAT_ID=11111111

docs/SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ odek audit <session-id> | jq … # programmatic triage
217217

218218
`AllowedChats` and `AllowedUsers` are loaded from `[telegram]` config or `ODEK_TELEGRAM_ALLOWED_CHATS` / `…_USERS` env vars. When non-empty, the handler rejects any update whose `chat.id` / `user.id` is not in the list **before** any tool call is reached. Denied attempts are logged so you can notice scanning.
219219

220-
If you run the bot at all, **set the allowlist**. The bot is the only internet-exposed surface, and the agent it drives has full host access.
220+
Authorization is **fail-closed**: if neither allowlist is configured, the bot refuses to start (`ValidateConfig` returns an error), and at runtime `isAllowed` denies every update. The bot is the only internet-exposed surface and the agent it drives has full host access, so an empty allowlist must never silently mean "allow everyone". To intentionally run an open bot you must explicitly set `ODEK_TELEGRAM_ALLOW_ALL=true`, which logs a loud warning at startup.
221221

222222
### 13. Identity anchoring (legacy)
223223

docs/TELEGRAM.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ All configuration flows through `TelegramConfig` and can be set via environment
3131
| Variable | Field | Default |
3232
|---|---|---|
3333
| `ODEK_TELEGRAM_BOT_TOKEN` | Token | — (required) |
34-
| `ODEK_TELEGRAM_ALLOWED_CHATS` | AllowedChats | all |
35-
| `ODEK_TELEGRAM_ALLOWED_USERS` | AllowedUsers | all |
34+
| `ODEK_TELEGRAM_ALLOWED_CHATS` | AllowedChats | — (see below) |
35+
| `ODEK_TELEGRAM_ALLOWED_USERS` | AllowedUsers | — (see below) |
36+
| `ODEK_TELEGRAM_ALLOW_ALL` | AllowAllUsers | false |
3637
| `ODEK_TELEGRAM_BOT_USERNAME` | BotUsername ||
3738
| `ODEK_TELEGRAM_POLL_INTERVAL` | PollInterval | 1s |
3839
| `ODEK_TELEGRAM_POLL_TIMEOUT` | PollTimeout | 30s |
@@ -181,10 +182,19 @@ All callbacks return a response string (may be empty) and an error. The `Handle`
181182
### Access Control
182183

183184
`HandlerConfig` supports:
184-
- **AllowedChats** — restrict to specific chat IDs (empty = allow all)
185-
- **AllowedUsers** — restrict to specific user IDs (empty = allow all)
185+
- **AllowedChats** — restrict to specific chat IDs
186+
- **AllowedUsers** — restrict to specific user IDs
187+
- **AllowAllUsers** — explicit opt-in to run with no allowlist (see below)
186188
- **BotUsername** — for `@mention` detection in groups
187189

190+
> **Fail-closed authorization.** The bot **refuses to start** if neither
191+
> `ODEK_TELEGRAM_ALLOWED_CHATS` nor `ODEK_TELEGRAM_ALLOWED_USERS` is set, so an
192+
> open bot — where any Telegram user could drive the agent and its shell/file
193+
> tools — can never be deployed by accident. To intentionally run an open bot,
194+
> set `ODEK_TELEGRAM_ALLOW_ALL=true` (logged as a loud warning at startup). At
195+
> runtime, with both allowlists empty and `AllowAllUsers` unset, every update is
196+
> denied.
197+
188198
### Inline Keyboards
189199

190200
The handler uses `sync.Map` for `TelegramApprover` instances, keyed by `chatID`. This allows the agent to send inline keyboard approval requests (yes/no) and receive responses via callback queries. The handler intercepts callback queries matching pending approval requests before dispatching to `OnCallbackQuery`.

0 commit comments

Comments
 (0)