fix(telegram): improve error message for 403 bot-not-member errors#48296
fix(telegram): improve error message for 403 bot-not-member errors#48296Jimmy-xuzimo wants to merge 18 commits intoopenclaw:mainfrom
Conversation
The 🥱 (yawn) emoji has a negative connotation of boredom, which can be interpreted as the AI being bored with the user's request. Changing it to ⏳ (hourglass) better represents the 'operation in progress but taking longer than expected' meaning of the stallSoft state. This also aligns the implementation with the documented default in the StatusReactionEmojis type definition. Fixes openclaw#28602
When memory.backend is set to "qmd" but the qmd binary is not installed, the system now: 1. Shows a clear warning during 'openclaw doctor' with actionable fix steps 2. Logs a warning at startup when QMD fallback activates 3. Provides helpful error messages pointing to installation docs This prevents silent failures where users think QMD is working but it's actually falling back to builtin backend without notice. Fixes openclaw#25910
- Remove unused 'version' variable in checkQmdBinaryAvailable() - Fix logic bug: skip QMD creation when binary is unavailable - Restore QMD cache fast path before binary probe to avoid latency regression
…oad 'spawn' error pattern that masks real errors - Revert unrelated emoji change (stallSoft back to 🥱)
- Add cwd parameter to checkQmdBinaryAvailable() to probe from agent workspace This ensures relative paths (e.g., ./bin/qmd) resolve correctly against the agent workspace instead of the gateway process directory. - Limit Windows .cmd extension to known shim names (qmd, npm, npx) This prevents breaking custom executables that rely on PATHEXT resolution (e.g., my-qmd-wrapper -> my-qmd-wrapper.exe). - Update callers in search-manager.ts and doctor-memory-search.ts to pass the appropriate working directory.
Fix unescaped dollar signs in Brave Search documentation that were causing incorrect rendering in Mintlify/MDX. - docs/brave-search.md: Escape /month as \/month - docs/tools/web.md: Escape /month as \/month - docs/reference/api-usage-costs.md: Escape /month as \/month Fixes openclaw#44979
- Fix Windows executable resolution to use PATH-only search (avoid CWD) - Fix explicit path handling for commands like C:\tools\qmd - Fix timeout test to actually exercise timeout mechanism using mocks - Import resolveWindowsExecutablePath from plugin-sdk/windows-spawn
- Use resolveCliSpawnInvocation to handle Windows npm shims consistently with runtime spawn semantics - Remove unused resolveWindowsExecutablePath import - Simplify timeout test to avoid mocking issues
Convert ANTHROPIC_MODEL_ALIASES from a const to a lazy-initialized function getAnthropicModelAliases() to avoid Temporal Dead Zone (TDZ) issues during module loading. The error occurred when parseModelRef was called during config loading before the ANTHROPIC_MODEL_ALIASES constant was initialized. Fixes openclaw#45057
…x compatibility Fixes openclaw#48279 ACP TypeScript SDK v0.16.0 renamed unstable_listSessions to listSessions. This change updates the AcpGatewayAgent class to use the new method name, restoring compatibility with the latest SDK version. Changes: - Rename unstable_listSessions method to listSessions in translator.ts - Add comprehensive unit tests for listSessions functionality Testing: - All 6 new unit tests pass - Verified default and custom limit handling - Tested empty session list and metadata inclusion
Fixes openclaw#48273 When Telegram returns a 403 'bot is not a member of the channel chat' error, users now receive a clear, actionable error message instead of the raw API error. Changes: - Add BOT_NOT_MEMBER_RE regex to detect bot membership errors - Add wrapTelegramBotNotMemberError function with helpful context - Update createRequestWithChatNotFound to also handle bot-not-member errors - Add comprehensive unit tests for the new error handling The new error message provides specific guidance based on chat type: - For channels: add the bot as an administrator or member - For groups: re-add the bot if it was removed - For DMs: ensure the user started a conversation with /start Testing: - Added 2 new test cases for bot-not-member error handling - Verified regex matches various error message formats - Confirmed error messages include chat_id and actionable guidance
🔒 Aisle Security AnalysisWe found 4 potential security issue(s) in this PR:
1. 🟡 DoS risk: status requests spawn external
|
| Property | Value |
|---|---|
| Severity | Medium |
| CWE | CWE-400 |
| Location | src/memory/search-manager.ts:38-45 |
Description
getMemorySearchManager() now probes QMD availability by executing qmd --version via checkQmdBinaryAvailable(...) even for purpose: "status" calls, and status managers are explicitly not cached.
This creates a resource-exhaustion vector:
- Any caller that can trigger a status check (e.g. the gateway method
doctor.memory.status, which is inoperator.readscope) can cause a new OS process spawn per request. - There is no memoization, TTL, or in-flight de-duplication of the probe; concurrent/polled status calls can accumulate child processes.
- The probe uses a 5s timeout, so a slow/hung
qmdbinary (or misconfiguration) can tie up resources longer per request.
Vulnerable code:
const qmdCheck = await checkQmdBinaryAvailable(resolved.qmd.command, 5000, workspaceDir);Recommendation
Avoid spawning a process per status request.
Mitigations (one or combine):
- Cache probe results per
(agentId, resolved.qmd.command)with a TTL (e.g. 30–300s), and reuse the last result forpurpose: "status". - De-duplicate in-flight probes so concurrent calls await the same Promise.
- Consider a cheaper probe (e.g.
which/wherelookup orfs.accessfor explicit paths) and only run--versionon first use.
Example (sketch):
const QMD_PROBE_CACHE = new Map<string, { ts: number; result: QmdProbeResult; inFlight?: Promise<QmdProbeResult> }>();
async function getQmdProbe(key: string, run: () => Promise<QmdProbeResult>) {
const now = Date.now();
const entry = QMD_PROBE_CACHE.get(key);
if (entry?.result && now - entry.ts < 60_000) return entry.result;
if (entry?.inFlight) return await entry.inFlight;
const inFlight = run().finally(() => {
const cur = QMD_PROBE_CACHE.get(key);
if (cur) delete cur.inFlight;
});
QMD_PROBE_CACHE.set(key, { ts: now, result: entry?.result ?? { available: false, error: "" } as any, inFlight });
const result = await inFlight;
QMD_PROBE_CACHE.set(key, { ts: Date.now(), result });
return result;
}2. 🟡 Untrusted search path leads to unintended binary execution in checkQmdBinaryAvailable (CWE-426)
| Property | Value |
|---|---|
| Severity | Medium |
| CWE | CWE-426 |
| Location | src/memory/backend-config.ts:375-398 |
Description
The new checkQmdBinaryAvailable() helper executes a user-supplied/bare command name via execFile() to probe availability.
On Windows (and in some PATH configurations), if the requested command is not found on PATH, resolveCliSpawnInvocation() can leave spawnInvocation.command as a bare name (e.g. "qmd"). execFile()/spawn() then falls back to OS search rules which include the current working directory. This can lead to execution of an attacker-planted qmd.exe/qmd.cmd in the process CWD when the user runs openclaw from an untrusted directory.
Key points:
- Input:
commandultimately comes from config (memory.qmd.command) or callers. - The function intends to check “on PATH”, but it may execute a same-named binary from the working directory when PATH resolution fails.
- The provided
cwdis ignored for bare command names (effectiveCwdbecomesundefined), which means Node usesprocess.cwd().
Vulnerable code:
const effectiveCwd = hasPath ? cwd : undefined;
await execFileAsync(spawnInvocation.command, spawnInvocation.argv, {
cwd: effectiveCwd,
shell: spawnInvocation.shell,
});Attack scenario (Windows):
- User configures
memory.backend=qmdbut does not have QMD installed / not on PATH. - User runs
openclawfrom an untrusted folder containing a maliciousqmd.cmd/qmd.exe. checkQmdBinaryAvailable("qmd")executes the planted binary during “doctor/status” flows, causing local code execution with the privileges of the OpenClaw process.
Recommendation
Harden the probe so it does not execute a bare command that failed PATH resolution.
Recommended changes (Windows-focused):
- Avoid executing when PATH resolution did not yield a path (prevents CWD hijacking):
const spawnInvocation = resolveCliSpawnInvocation({ /* ... */ });
// If the user provided a bare name, require resolution to an explicit path.
if (!hasPath && process.platform === "win32" && !/[\\/]/.test(spawnInvocation.command)) {
return {
available: false,
error: `QMD binary "${command}" not found on PATH. Please install QMD or check your configuration.`,
};
}-
Consider setting
allowShellFallback: falsefor the probe (or exposing it viaresolveCliSpawnInvocation) so the availability check does not rely on shell execution. -
Optionally, resolve executables without running them by iterating
PATHentries and checkingfs.access()(andPATHEXTon Windows) instead of spawning.
3. 🔵 Unbounded limit in ACP listSessions can trigger large session enumeration (DoS)
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-400 |
| Location | src/acp/translator.ts:219-222 |
Description
The ACP listSessions method forwards a client-controlled limit from params._meta to the gateway without any upper bound.
- Input (user-controlled):
params._meta.limitin the ACP request - Weak validation:
readNumberonly checkstypeof value === "number" && Number.isFinite(value)(no min/max/integer enforcement) - Sink: forwarded to
this.gateway.request("sessions.list", { limit }) - Downstream: gateway
sessions.listschema enforces onlyminimum: 1forlimit(nomaximum), so extremely large values are accepted
Impact:
- A malicious/buggy ACP client can request an excessively large
limit, causing the gateway to return all sessions from disk-backed store. - This can create a very large JSON response and high CPU/memory usage in both gateway and ACP process, potentially leading to degraded performance or denial of service (CWE-400).
Vulnerable code:
async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
const limit = readNumber(params._meta, ["limit"]) ?? 100;
const result = await this.gateway.request<SessionsListResult>("sessions.list", { limit });
// ...
}Recommendation
Enforce a reasonable upper bound for limit (and coerce to an integer) either at the ACP layer, the gateway layer, or both.
Example fix in src/acp/translator.ts:
const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 200; // choose based on UX/perf
const raw = readNumber(params._meta, ["limit"]);
const limit = Math.min(
MAX_LIMIT,
Math.max(1, Math.floor(raw ?? DEFAULT_LIMIT)),
);Additionally, update the gateway schema to reject overly large limits:
// src/gateway/protocol/schema/sessions.ts
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),This prevents resource exhaustion even if another caller bypasses ACP and calls sessions.list directly.
4. 🔵 Sensitive recipient target leaked via Telegram bot-not-member error message (logged/persisted)
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-532 |
| Location | src/telegram/send.ts:399-411 |
Description
The new wrapTelegramBotNotMemberError wraps Telegram API errors by creating a new Error whose message includes the unredacted input parameter:
params.inputis ultimately the caller-providedtotarget (chat id / @username / t.me link / internal prefixes), passed increateRequestWithChatNotFound({ input: to }).- The wrapped error message is then:
- logged through
withTelegramApiErrorLogging()(which logsformatErrorMessage(err)), and - persisted into the outbound delivery queue as
lastError(viafailDelivery(..., err.message, ...)).
- logged through
This can cause information disclosure of recipient identifiers (chat IDs/usernames/links) into logs and on-disk queue state, and can also enable log amplification (very long to strings) because there is no truncation/size bound.
Vulnerable code:
`Input was: ${JSON.stringify(params.input)}.`While JSON.stringify helps avoid newline/ANSI escape log-forging (it escapes control chars), it does not address privacy (PII) or log-size concerns.
Recommendation
Avoid embedding raw request/target input directly in error messages that may be logged/persisted.
Recommended changes:
- Redact + truncate the input before adding it to the message, or omit it by default.
- Prefer structured error metadata (fields) over concatenating into the human message.
Example (redact + truncate):
function safePreview(value: string, max = 128): string {
const truncated = value.length > max ? value.slice(0, max) + "…" : value;
return redactSensitiveText(truncated);
}
// ...
`Input was: ${JSON.stringify(safePreview(params.input))}.`Or: remove Input was: from the thrown error message and attach it as non-logged diagnostic data when verbose/diagnostic flags are enabled.
Analyzed PR: #48296 at commit 311ad68
Last updated on: 2026-03-16T16:08:22Z
Greptile SummaryThis PR adds actionable error handling for Telegram 403 "bot is not a member" errors, following the same pattern as the existing
Confidence Score: 4/5
|
src/telegram/send.ts
Outdated
| const MESSAGE_NOT_MODIFIED_RE = | ||
| /400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i; | ||
| const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i; | ||
| const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot is not a member of the channel chat/i; |
There was a problem hiding this comment.
Regex only matches "channel chat", misses supergroup and group variants
The current regex anchors to the literal string "channel chat", but the Telegram Bot API returns different error messages depending on the chat type:
- Channels →
"bot is not a member of the channel chat" - Supergroups →
"bot is not a member of the supergroup chat" - Regular groups →
"bot is not a member of the group chat"(or"bot was kicked from the group chat")
The PR description explicitly claims to handle the "bot was removed from a group" scenario, but the current regex will silently miss those cases and fall through to the raw Telegram error.
| const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot is not a member of the channel chat/i; | |
| const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/send.ts
Line: 106
Comment:
**Regex only matches "channel chat", misses supergroup and group variants**
The current regex anchors to the literal string `"channel chat"`, but the Telegram Bot API returns different error messages depending on the chat type:
- Channels → `"bot is not a member of the channel chat"`
- Supergroups → `"bot is not a member of the supergroup chat"`
- Regular groups → `"bot is not a member of the group chat"` (or `"bot was kicked from the group chat"`)
The PR description explicitly claims to handle the "bot was removed from a group" scenario, but the current regex will silently miss those cases and fall through to the raw Telegram error.
```suggestion
const BOT_NOT_MEMBER_RE = /403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i;
```
How can I resolve this? If you propose a fix, please make it concise.Addresses review comment on PR openclaw#48296 The previous regex only matched 'channel chat' errors, but Telegram API returns different messages for different chat types: - Channels: 'bot is not a member of the channel chat' - Supergroups: 'bot is not a member of the supergroup chat' - Groups: 'bot is not a member of the group chat' or 'bot was kicked from the group chat' Updated regex to match all variants: /403:\s*Forbidden:\s*bot (?:is not a member|was kicked from the)/i This ensures consistent error handling across all chat types.
|
@greptile-apps 感谢审查评论!我已修复了正则表达式。 修复内容:
现在匹配的错误消息:
改进效果:
请查看最新的提交 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e47f9b356
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Check if string type is an option - if so, preserve string values as-is | ||
| // This fixes issues like Discord IDs (numeric-looking strings) being coerced to numbers | ||
| const hasStringVariant = variants.some((v) => schemaType(v) === "string"); | ||
| if (typeof value === "string" && hasStringVariant) { |
There was a problem hiding this comment.
Restore numeric coercion for string/number unions
This condition short-circuits all string inputs whenever a union contains a string variant, so anyOf/oneOf schemas like [{type:"number"},{type:"string"}] now keep values such as "30" as strings and never reach the numeric coercion block below. That changes persisted types for mixed-union fields and regresses the previous behavior expected by the config-form coercion tests, so numeric union inputs can now be serialized with the wrong type.
Useful? React with 👍 / 👎.
| } | ||
| // Binary exists but --version failed (non-zero exit) - still treat as available | ||
| // This handles older QMD versions that may not support --version flag | ||
| return { available: true, path: spawnInvocation.command }; |
There was a problem hiding this comment.
Mark non-version qmd probe failures as unavailable
The catch-all fallback returns available: true for every non-"not found" error, which incorrectly treats execution failures like EACCES/EPERM (or other unusable binary states) as healthy. In that scenario the doctor/check path reports QMD available, but runtime initialization still fails and silently falls back to builtin memory, so users get misleading health diagnostics instead of actionable remediation.
Useful? React with 👍 / 👎.
Summary
Fixes #48273
When Telegram returns a 403 "bot is not a member of the channel chat" error, users now receive a clear, actionable error message instead of the raw API error.
Problem
Users configuring Telegram outbound messaging were seeing confusing errors like:
This error occurs when:
The raw API error doesn't explain how to fix the issue.
Solution
Added error handling to detect the "bot not member" error and provide actionable guidance:
Changes
BOT_NOT_MEMBER_REregex pattern to detect bot membership errorswrapTelegramBotNotMemberError()function with helpful contextcreateRequestWithChatNotFound()to also handle bot-not-member errorsNew Error Message
Instead of the raw API error, users now see:
Testing
src/telegram/send.test.ts:Impact
Related