Telegram: fix exec approvals and topic follow-up routing#37233
Telegram: fix exec approvals and topic follow-up routing#37233huntharo merged 12 commits intoopenclaw:mainfrom
Conversation
🔒 Aisle Security AnalysisWe found 4 potential security issue(s) in this PR:
1. 🟠 Brute-forceable exec approval resolution via unrestricted prefix matching (and ID disclosure on ambiguity)
DescriptionThe Impact:
Why this is exploitable:
Concrete scenario:
RecommendationMitigate prefix-guessing and misbinding by tightening resolution semantics:
Example (minimum prefix length + avoid leaking IDs): // exec.approval.resolve
const raw = p.id.trim();
if (raw.length < 8) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "approval id prefix too short"));
return;
}
const resolved = manager.lookupPendingId(raw);
if (resolved.kind === "ambiguous") {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "ambiguous approval id prefix; use full id"));
return;
}And in the schema: export const ExecApprovalResolveParamsSchema = Type.Object({
id: Type.String({ minLength: 8, maxLength: 128 }),
decision: NonEmptyString,
}, { additionalProperties: false });2. 🟡 Audit/notification evasion via user-controllable suppressNotifyOnExit in node system.run
DescriptionThe new Data flow (all in this diff):
Security impact:
Vulnerable code: // src/gateway/server-node-events.ts
const notifyOnExit = cfg.tools?.exec?.notifyOnExit !== false;
if (!notifyOnExit) {
return;
}
if (obj.suppressNotifyOnExit === true) {
return;
}RecommendationDo not allow untrusted callers to suppress exec notifications. Options (pick one depending on intended feature semantics):
Example hardening (strip for normal callers): // src/gateway/node-invoke-system-run-approval.ts
function pickSystemRunParams(raw: Record<string, unknown>): Record<string, unknown> {
const next: Record<string, unknown> = {};
for (const key of [
"command","rawCommand","systemRunPlan","cwd","env","timeoutMs",
"needsScreenRecording","agentId","sessionKey","runId",
// "suppressNotifyOnExit" // do not forward from user input
]) {
if (key in raw) next[key] = raw[key];
}
return next;
}
// If needed, inject suppressNotifyOnExit only for vetted internal paths.3. 🟡 Telegram exec approval prompts can be misdelivered to attacker-chosen chat via untrusted turnSourceTo
DescriptionThe new If Impact:
Vulnerable code (target selection trusts request fields): if (turnSourceChannel === "telegram" && turnSourceTo) {
if (turnSourceAccountId && normalizeAccountId(turnSourceAccountId) !== normalizeAccountId(params.accountId)) {
return null;
}
const threadId = /* parse */;
return { to: turnSourceTo, threadId: Number.isFinite(threadId) ? threadId : undefined };
}Sensitive content is then delivered to await this.sendMessage(target.to, payload.text ?? "", { ... });Notes:
RecommendationDo not treat Hardening options (preferably combine 1 + 2):
const direct = /* parse turnSourceTo/threadId */;
const sessionTarget = resolveRequestSessionTarget(params);
if (
!sessionTarget ||
sessionTarget.channel !== "telegram" ||
sessionTarget.to !== direct.to ||
(sessionTarget.threadId ?? undefined) !== (direct.threadId ?? undefined)
) {
return null; // or fallbackToDm
}
Additionally, consider enforcing this upstream in the gateway: reject/strip 4. 🔵 Telegram exec approval prompts can be silently dropped due to unconditional local suppression
DescriptionThe new Telegram suppression logic drops any This causes a loss of the user-visible approval prompt (and can make the bot appear to “hang”) in situations such as:
Because the suppression paths also mark the reply as delivered ( Vulnerable logic: export function shouldSuppressLocalTelegramExecApprovalPrompt(...) {
void params.cfg;
void params.accountId;
return getExecApprovalReplyMetadata(params.payload) !== null;
}and its usage in Telegram dispatch paths:
RecommendationOnly suppress the local approval prompt when you can ensure an alternative delivery path exists for this approval request. Minimum hardening (match Discord behavior): gate suppression on Telegram exec-approvals being enabled for the account. import { isTelegramExecApprovalClientEnabled } from "./exec-approvals.js";
export function shouldSuppressLocalTelegramExecApprovalPrompt(params: {
cfg: OpenClawConfig;
accountId?: string | null;
payload: ReplyPayload;
}): boolean {
return (
isTelegramExecApprovalClientEnabled({ cfg: params.cfg, accountId: params.accountId }) &&
getExecApprovalReplyMetadata(params.payload) !== null
);
}Stronger fix (prevents silent hangs):
This preserves availability and prevents missing security-relevant prompts due to handler outages or filter misconfiguration. Analyzed PR: #37233 at commit Last updated on: 2026-03-10T05:10:33Z |
Greptile SummaryThis PR fixes a multi-part regression in Telegram exec-approval flows: approval-id mismatch/rotation, missing approver gating on Telegram's Key changes:
Two issues found:
Confidence Score: 3/5
Last reviewed commit: 12025dd |
| lines.push("Mode: foreground (interactive approvals available in this chat)."); | ||
| lines.push( | ||
| "Background mode note: non-interactive runs cannot wait for chat approvals; use pre-approved policy (allow-always or ask=off).", | ||
| ); | ||
| lines.push("Reply with: /approve <id> allow-once|allow-always|deny"); |
There was a problem hiding this comment.
Forwarder approval messages will not receive inline buttons.
buildRequestMessage uses a literal <id> placeholder instead of the actual approval ID. The extractApprovalIdFromText function relies on a regex pattern [A-Za-z0-9][A-Za-z0-9._:-]* to extract the approval ID from the "Reply with:" line. The literal <id> won't match this pattern, so injectTelegramApprovalButtons will skip button injection for forwarder-delivered messages.
This means approval notifications routed through the forwarder won't have interactive inline buttons on Telegram, even though agent-generated messages (via bash-tools) will.
To fix, inject the actual request.id:
| lines.push("Reply with: /approve <id> allow-once|allow-always|deny"); | |
| lines.push(`Reply with: /approve ${request.id} allow-once|allow-always|deny`); |
Context Used: Rule from dashboard - AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/exec-approval-forwarder.ts
Line: 198
Comment:
Forwarder approval messages will not receive inline buttons.
`buildRequestMessage` uses a literal `<id>` placeholder instead of the actual approval ID. The `extractApprovalIdFromText` function relies on a regex pattern `[A-Za-z0-9][A-Za-z0-9._:-]*` to extract the approval ID from the "Reply with:" line. The literal `<id>` won't match this pattern, so `injectTelegramApprovalButtons` will skip button injection for forwarder-delivered messages.
This means approval notifications routed through the forwarder won't have interactive inline buttons on Telegram, even though agent-generated messages (via bash-tools) will.
To fix, inject the actual `request.id`:
```suggestion
lines.push(`Reply with: /approve ${request.id} allow-once|allow-always|deny`);
```
**Context Used:** Rule from `dashboard` - AGENTS.md ([source](https://app.greptile.com/review/custom-context?memory=a1d58d20-b4dd-4cbb-973a-9fd7824e1921))
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c3caaa1b8
ℹ️ 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".
| if (!heartbeatsEnabled && !isEventDrivenReason) { | ||
| return { status: "skipped", reason: "disabled" }; | ||
| } | ||
| if (!isHeartbeatEnabledForAgent(cfg, agentId)) { | ||
| if (!isHeartbeatEnabledForAgent(cfg, agentId) && !isEventDrivenReason) { | ||
| return { status: "skipped", reason: "disabled" }; | ||
| } | ||
| if (!resolveHeartbeatIntervalMs(cfg, undefined, heartbeat)) { | ||
| if (!resolveHeartbeatIntervalMs(cfg, undefined, heartbeat) && !isEventDrivenReason) { | ||
| return { status: "skipped", reason: "disabled" }; |
There was a problem hiding this comment.
Honor heartbeat disable switches for event-driven wakes
runHeartbeatOnce now bypasses the global and per-agent disable guards whenever the reason is event-driven, so exec-event/cron/hook/wake requests will still run even after heartbeats were explicitly disabled (globally via set-heartbeats false or for an agent without heartbeat enabled). In multi-agent setups this re-enables follow-up turns for agents that were intentionally disabled, which is a behavior regression from the prior disabled skip path.
Useful? React with 👍 / 👎.
| const text = payload.text?.trim(); | ||
| if (!text || !text.includes("/approve")) { | ||
| return payload; |
There was a problem hiding this comment.
Restrict approval button injection to approval prompts
This helper injects actionable Telegram /approve buttons for any text payload that happens to contain /approve, without verifying the message is an approval-pending response. As a result, ordinary replies (help text, copied logs, or examples) can get one-tap approval actions, and if the extracted ID matches a real pending request the click can resolve it unintentionally.
Useful? React with 👍 / 👎.
8dc2fc3 to
a9efbee
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9efbee85d
ℹ️ 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".
| const autoApprovalButtonsEnabled = | ||
| resolveTelegramInlineButtonsScope({ cfg, accountId: route.accountId }) !== "off"; |
There was a problem hiding this comment.
Honor dm/group scope before auto-injecting approval buttons
autoApprovalButtonsEnabled currently treats every scope except off as eligible, so accounts configured with capabilities.inlineButtons: "dm" will still get /approve buttons in group chats (and group scope will still inject in DMs). Those callbacks are then rejected by the existing scope gate in src/telegram/bot-handlers.ts:1126-1130, leaving users with visible but non-functional approval buttons. This same !== "off" check is also repeated in src/telegram/bot-native-commands.ts:564-565 and src/infra/outbound/deliver.ts:308-309, so injection should be gated by chat type/scope, not just off.
Useful? React with 👍 / 👎.
|
@greptile-apps please review latest code |
| const exact = this.pending.get(normalized); | ||
| if (exact && exact.record.resolvedAtMs === undefined) { | ||
| return { kind: "exact", id: normalized }; | ||
| } | ||
|
|
||
| const lowerPrefix = normalized.toLowerCase(); | ||
| const matches: string[] = []; | ||
| for (const [id, entry] of this.pending.entries()) { | ||
| if (entry.record.resolvedAtMs !== undefined) { | ||
| continue; | ||
| } | ||
| if (id.toLowerCase().startsWith(lowerPrefix)) { | ||
| matches.push(id); | ||
| } | ||
| } | ||
|
|
||
| if (matches.length === 1) { | ||
| return { kind: "prefix", id: matches[0] }; | ||
| } | ||
| if (matches.length > 1) { | ||
| return { kind: "ambiguous", ids: matches }; | ||
| } | ||
| return { kind: "none" }; |
There was a problem hiding this comment.
When an exact entry is found but is already resolved (resolvedAtMs !== undefined), this function doesn't return early and falls through to the prefix scan. This could match a different, still-pending approval whose ID happens to start with the submitted (now-resolved) string.
For example:
- Resolved: "abc123-111-xxxx" (full ID)
- Pending: "abc123-111-yyyy" (full ID, shares prefix)
- User submits: "abc123-111-xxxx"
The function finds the exact match but it's resolved, so it continues to the prefix scan and matches the wrong pending entry instead of returning "unknown or expired".
To fix, return immediately when an exact match is found but already resolved:
| const exact = this.pending.get(normalized); | |
| if (exact && exact.record.resolvedAtMs === undefined) { | |
| return { kind: "exact", id: normalized }; | |
| } | |
| const lowerPrefix = normalized.toLowerCase(); | |
| const matches: string[] = []; | |
| for (const [id, entry] of this.pending.entries()) { | |
| if (entry.record.resolvedAtMs !== undefined) { | |
| continue; | |
| } | |
| if (id.toLowerCase().startsWith(lowerPrefix)) { | |
| matches.push(id); | |
| } | |
| } | |
| if (matches.length === 1) { | |
| return { kind: "prefix", id: matches[0] }; | |
| } | |
| if (matches.length > 1) { | |
| return { kind: "ambiguous", ids: matches }; | |
| } | |
| return { kind: "none" }; | |
| const exact = this.pending.get(normalized); | |
| if (exact) { | |
| if (exact.record.resolvedAtMs === undefined) { | |
| return { kind: "exact", id: normalized }; | |
| } | |
| // Exact ID found but already resolved — do not fall through to prefix match. | |
| return { kind: "none" }; | |
| } | |
| const lowerPrefix = normalized.toLowerCase(); | |
| const matches: string[] = []; | |
| for (const [id, entry] of this.pending.entries()) { | |
| if (entry.record.resolvedAtMs !== undefined) { | |
| continue; | |
| } | |
| if (id.toLowerCase().startsWith(lowerPrefix)) { | |
| matches.push(id); | |
| } | |
| } | |
| if (matches.length === 1) { | |
| return { kind: "prefix", id: matches[0] }; | |
| } | |
| if (matches.length > 1) { | |
| return { kind: "ambiguous", ids: matches }; | |
| } | |
| return { kind: "none" }; |
This ensures that a full ID that was already consumed returns the correct "unknown or expired" error instead of potentially matching a concurrent unresolved entry.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/exec-approval-manager.ts
Line: 185-207
Comment:
When an exact entry is found but is already resolved (`resolvedAtMs !== undefined`), this function doesn't return early and falls through to the prefix scan. This could match a different, still-pending approval whose ID happens to start with the submitted (now-resolved) string.
For example:
- Resolved: "abc123-111-xxxx" (full ID)
- Pending: "abc123-111-yyyy" (full ID, shares prefix)
- User submits: "abc123-111-xxxx"
The function finds the exact match but it's resolved, so it continues to the prefix scan and matches the wrong pending entry instead of returning "unknown or expired".
To fix, return immediately when an exact match is found but already resolved:
```suggestion
const exact = this.pending.get(normalized);
if (exact) {
if (exact.record.resolvedAtMs === undefined) {
return { kind: "exact", id: normalized };
}
// Exact ID found but already resolved — do not fall through to prefix match.
return { kind: "none" };
}
const lowerPrefix = normalized.toLowerCase();
const matches: string[] = [];
for (const [id, entry] of this.pending.entries()) {
if (entry.record.resolvedAtMs !== undefined) {
continue;
}
if (id.toLowerCase().startsWith(lowerPrefix)) {
matches.push(id);
}
}
if (matches.length === 1) {
return { kind: "prefix", id: matches[0] };
}
if (matches.length > 1) {
return { kind: "ambiguous", ids: matches };
}
return { kind: "none" };
```
This ensures that a full ID that was already consumed returns the correct "unknown or expired" error instead of potentially matching a concurrent unresolved entry.
How can I resolve this? If you propose a fix, please make it concise.c520c5b to
982bb95
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 982bb955c0
ℹ️ 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".
| let sanitizedPayload = canInjectTelegramButtons | ||
| ? injectTelegramApprovalButtons(payload) | ||
| : payload; |
There was a problem hiding this comment.
Preserve markdown escaping when injecting Telegram approval buttons
Injecting buttons here adds channelData to plain approval text payloads, which makes deliverOutboundPayloads take the Telegram sendPayload path (instead of chunked sendText) and sends raw text with textMode: "html" (src/channels/plugins/outbound/telegram.ts). Approval prompts include unescaped shell commands, so common inputs like &&, <, or & can trigger Telegram HTML parse errors and drop the approval message, leaving elevated runs pending with no actionable prompt.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 596a5f5df2
ℹ️ 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".
596a5f5 to
4eaece3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4eaece320d
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9577900767
ℹ️ 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".
| if (!explicitTo) { | ||
| return true; |
There was a problem hiding this comment.
Restrict Telegram thread carry to Telegram-source sessions
When heartbeat.target is telegram and heartbeat.to is unset, this branch always returns true, so runHeartbeatOnce forwards parseSessionThreadInfo(sessionKey).threadId as explicitThreadId for every event-driven wake. For non-Telegram threaded sessions (for example agent:...:main:thread:42 or Discord thread keys), that foreign thread id is then attached to Telegram delivery and can misroute the follow-up into an unrelated topic or fail with Telegram thread-not-found errors. The carry decision should require a Telegram group source session before reusing the source thread id.
Useful? React with 👍 / 👎.
|
❌ I'm refactoring to remove the heartbeat logic while preserving the ability for the notification of completion after the approval. |
9577900 to
c5a884a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c5a884ab6b
ℹ️ 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".
| @@ -4,7 +4,7 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message- | |||
| import { requireGatewayClientScopeForInternalChannel } from "./command-gates.js"; | |||
| import type { CommandHandler } from "./commands-types.js"; | |||
|
|
|||
| const COMMAND = "/approve"; | |||
| const COMMAND_WITH_MENTION_REGEX = /^\/approve(?:@[^\s]+)?(?:\s|$)/i; | |||
There was a problem hiding this comment.
Ignore /approve mentions targeting other Telegram bots
The new parser accepts any /approve@... prefix and then resolves approvals without checking whether the mention is for this bot. In a Telegram group with multiple bots, /approve@otherbot <id> allow-once will now be treated as a valid OpenClaw approval command, so an authorized operator can accidentally resolve OpenClaw’s pending approval while trying to control another bot. This is a regression from the old behavior (which rejected mention forms) and needs an explicit bot-username match before accepting mentioned /approve commands.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3766f858f4
ℹ️ 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".
| const unavailableReason = | ||
| preResolvedDecision === null | ||
| ? "no-approval-route" | ||
| : initiatingSurface.kind === "disabled" |
There was a problem hiding this comment.
Avoid marking auto-fallback approvals as unavailable
unavailableReason is set to "no-approval-route" as soon as preResolvedDecision === null, but the async branch still applies askFallback and can continue execution (for example with askFallback: "full"). In that case users receive an approval-unavailable message that tells them to retry even though the command may already be running, which can trigger duplicate side effects when they retry; the unavailable status should only be emitted when fallback logic will actually deny execution (this same pattern is mirrored in the node-host path).
Useful? React with 👍 / 👎.
| const requestChannel = normalizeMessageChannel(params.request.request.turnSourceChannel ?? ""); | ||
| if (requestChannel !== "telegram") { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Keep explicit Telegram forward targets for Telegram runs
This skip gate suppresses Telegram forwarding whenever the source channel is Telegram, regardless of which Telegram destination was configured. Since it never checks whether the forward target matches the source chat/topic, explicit approvals.exec.forward.targets (for example a separate ops/audit Telegram chat) are filtered out for Telegram-originated approvals, so those routes stop receiving approval prompts after this change.
Useful? React with 👍 / 👎.
3766f85 to
f243379
Compare
|
Merged via squash.
Thanks @huntharo! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f243379094
ℹ️ 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".
| this.pending.set(request.id, { | ||
| timeoutId, | ||
| messages: sentMessages, | ||
| }); |
There was a problem hiding this comment.
Register pending Telegram approvals before sending prompts
handleRequested only inserts the request into this.pending after all sendMessage calls finish, so there is a race where an approval resolved from another surface (Web UI/Discord/CLI) can trigger handleResolved first, return early because no pending entry exists, and never clear the just-sent buttons. In that case Telegram users keep stale approval buttons that fail with unknown/expired ID errors until timeout.
Useful? React with 👍 / 👎.
| @@ -147,6 +155,48 @@ function shouldSkipDiscordForwarding( | |||
| return Boolean(execApprovals?.enabled && (execApprovals.approvers?.length ?? 0) > 0); | |||
There was a problem hiding this comment.
Honor Discord filters before suppressing text forward targets
shouldSkipDiscordForwarding suppresses Discord forward targets as soon as account-level exec approvals are enabled with approvers, but it does not account for agentFilter/sessionFilter. Because DiscordExecApprovalsHandler.shouldHandle still rejects filtered requests, those requests can be dropped by both paths (no forwarded text prompt and no Discord component prompt), leaving approvals without a delivery route and forcing timeout.
Useful? React with 👍 / 👎.
Summary
Describe the problem and fix in 2–5 bullets:
unknown approval iddue to approval-id mismatch/rotation between prompt and resolve path, approved runs could still not post the follow-up result promptly, and heartbeat-delivered follow-ups could land in group#generalinstead of the active topic./approvehandling, support short unique id prefixes, inject canonical Telegram approval buttons from the exact pending approval id across Telegram delivery paths, allow event-driven heartbeat wakes (exec-event) to run even when periodic heartbeat config is off, and preserve explicit session topic/thread IDs for exec-event heartbeat delivery routing.Specific Issues Fixed
execApprovalson in Discord - Send/coding_agentexec approval from TelegramexecApprovalsconfig gate orapproverslistChange Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
/approve <id> allow-once|allow-always|denyactions and inline buttons./approveaccepts Telegram bot mentions (for example/approve@bot ...).#general).Security Impact (required)
No)No)No)No)No)Yes, explain risk + mitigation:Repro + Verification
Environment
Steps
/approve <id> allow-once) or inline callback.Example Prompt
Expected
Actual
main(broken), approval submit fails withunknown approval idand command remains pending.Evidence
Attach at least one:
Main branch logs (broken behavior)
Branch logs (intermediate behavior after approval-id fix)
Branch logs (remaining delayed follow-up symptom)
Before - Telegram - Cannot Approve Exec on Telegram - At All
/approve [id] allow-once- says it doesn't know that IDAfter - Telegram - Command Presented / BUTTONS!!!
After - Telegram - Command Logged / Buttons Cleared After Click / Approval Works
After - Discord -
execApprovalsdisabled or enabled but DM-onlyHuman Verification (required)
What you personally verified (not just CI), and how:
/approvecommand parsing, approval id prefix resolution, Telegram inline callback flow./approve@bot), unknown/expired ids, ambiguous prefix handling, callback-data length limits.Compatibility / Migration
Yes)No)No)Failure Recovery (if this breaks)
telegram-exec-approvals.unknown approval idon valid approvals; delayed/no post-approval follow-up response until a later user message wakes execution.Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.