Skip to content

refactor(exec): centralize native approval delivery#57516

Closed
scoootscooob wants to merge 1 commit into
openclaw:codex/exec-approval-foundationfrom
scoootscooob:codex/exec-approval-runtime
Closed

refactor(exec): centralize native approval delivery#57516
scoootscooob wants to merge 1 commit into
openclaw:codex/exec-approval-foundationfrom
scoootscooob:codex/exec-approval-runtime

Conversation

@scoootscooob

@scoootscooob scoootscooob commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a shared native approval delivery planner in core based on channel-exposed capabilities
  • add plugin-sdk helpers for approval delivery capabilities and planning
  • move Discord and Telegram native approval delivery onto the shared planner instead of channel-local target selection logic
  • keep channel code focused on transport adapters and native target resolution

Issues

Related: #57460
Related: #48532

Testing

  • pnpm test -- src/infra/approval-native-delivery.test.ts src/plugin-sdk/approval-delivery-helpers.test.ts extensions/discord/src/monitor/exec-approvals.test.ts extensions/telegram/src/exec-approvals-handler.test.ts
  • pnpm plugin-sdk:api:check
  • pnpm build

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: discord Channel integration: discord channel: telegram Channel integration: telegram size: XL maintainer Maintainer-authored PR labels Mar 30, 2026
@greptile-apps

greptile-apps Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors the exec-approval plumbing for Discord and Telegram by extracting a shared createExecApprovalChannelRuntime that centralises gateway subscription, pending-state lifecycle, and timeout management. It also fixes two real bugs on the Telegram side: approval button callbacks were parsed but never actually resolved (the gateway call was missing), and typed /approve commands from configured Telegram approvers were being rejected when isAuthorizedSender was false.

Key changes:

  • New src/infra/exec-approval-channel-runtime.ts — generic runtime parameterised on a TPending type; Discord and Telegram each supply deliverRequested, finalizeResolved, and finalizeExpired adapters.
  • exec-approval-reply.ts — new shared helpers: buildExecApprovalCommandText, buildExecApprovalActionDescriptors, buildExecApprovalInteractiveReply, parseExecApprovalCommandText. Buttons are now built from a single source of truth instead of channel-specific ad-hoc strings.
  • exec-approval-resolver.ts (new) — one-shot gateway connection for resolving a Telegram button callback without going through the persistent channel handler.
  • bot-handlers.runtime.ts — callback handler now calls resolveExecApproval and returns after an approval button press, closing the pre-existing gap where the button was silently swallowed.
  • commands-approve.tsisAuthorizedTelegramApprovalSender bypass allows configured Telegram approvers to use /approve even when the chat's general access is locked down.
  • button-types.ts — filters out buttons whose callback_data exceeds Telegram's 64-byte limit before rendering.

Minor findings (P2 only):

  • finalizeResolved and clearPending in TelegramExecApprovalHandler are functionally identical; the only difference is the suppressive void resolved; statement on the unused parameter. Consider using _resolved and delegating to clearPending.
  • The onClosefailReady guard in resolveTelegramExecApproval is a no-op after onHelloOk has already settled the ready-promise; the contract (that gatewayClient.request() is responsible for surfacing a mid-request disconnect) is correct but undocumented.

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 style/clarity suggestions that do not affect correctness.

The PR introduces a well-structured shared runtime with solid test coverage across the new exec-approval-channel-runtime, the updated Discord/Telegram handlers, and the new commands.test.ts case for Telegram approvers. The two real bugs (approval buttons not resolving, typed /approve rejected for Telegram approvers) are correctly fixed and covered by tests. Only minor P2 issues remain.

No files require special attention; the two P2 comments are in extensions/telegram/src/exec-approvals-handler.ts and extensions/telegram/src/exec-approval-resolver.ts.

Important Files Changed

Filename Overview
src/infra/exec-approval-channel-runtime.ts New shared runtime correctly centralises gateway lifecycle, pending tracking, and timeout management with a clean generic adapter pattern.
extensions/telegram/src/exec-approval-resolver.ts One-shot gateway resolver for Telegram button callbacks; minor undocumented contract around post-handshake close events.
extensions/telegram/src/exec-approvals-handler.ts Migrated to shared runtime; finalizeResolved and clearPending are functionally identical with a void resolved; smell.
extensions/discord/src/monitor/exec-approvals.ts Cleanly migrated to shared runtime; pending entries are now returned from deliverRequested rather than managed inline.
src/auto-reply/reply/commands-approve.ts Correctly adds isAuthorizedTelegramApprovalSender bypass so configured Telegram approvers can use /approve regardless of general chat access.
extensions/telegram/src/bot-handlers.runtime.ts Approval button callbacks now call resolveExecApproval and return early, fixing the pre-existing gap where button presses were silently dropped.
extensions/telegram/src/button-types.ts Added correct 64-byte callback_data guard; filter applied per-chunk which is intentional and tested.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/telegram/src/exec-approvals-handler.ts
Line: 341

Comment:
**`void resolved` suppresses unused-param warning instead of fixing it**

`finalizeResolved` accepts a `resolved` parameter but never reads it — its body is identical to `clearPending`. The `void resolved;` statement is an uncommon idiom for suppressing the lint warning and is likely to confuse future readers.

The cleanest fix is to either prefix the parameter with `_` (standard TypeScript convention for intentionally unused params) or delegate directly to `clearPending`:

```ts
// Option A – rename and delegate
private async finalizeResolved(
  _resolved: ExecApprovalResolved,
  messages: PendingMessage[],
): Promise<void> {
  await this.clearPending(messages);
}
```

Or, since both `finalizeResolved` and `finalizeExpired` do the exact same thing, the runtime constructor could just pass `clearPending` for both callbacks.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/telegram/src/exec-approval-resolver.ts
Line: 48-50

Comment:
**`onClose` fires as no-op once `onHelloOk` has already settled the promise**

After `onHelloOk` marks `readySettled = true`, a subsequent `onClose` call hits the early-return guard in `failReady` and does nothing. If the gateway drops the connection *between* `await ready` resolving and `await gatewayClient.request(...)` completing, there is no in-flight cancellation signal — the `request()` call itself must surface the error.

This is fine as long as `gatewayClient.request()` throws when the underlying socket is closed (which is the typical expectation), but it means a brief disconnect window after authentication produces a silent hang until the request eventually times out on the gateway side. Worth a comment here to make the contract explicit:

```suggestion
    onClose: (code, reason) => {
      // failReady is a no-op once onHelloOk has already resolved `ready`;
      // any in-flight gatewayClient.request() is responsible for surfacing
      // the disconnect as its own error at that point.
      failReady(new Error(`gateway closed (${code}): ${reason}`));
    },
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(exec): unify channel approval runtim..." | Re-trigger Greptile

Comment thread extensions/telegram/src/exec-approvals-handler.ts Outdated
Comment thread extensions/telegram/src/exec-approval-resolver.ts
@scoootscooob scoootscooob force-pushed the codex/exec-approval-runtime branch from b4ebef5 to 98d2f23 Compare March 30, 2026 06:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98d2f23177

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/commands-approve.ts Outdated
Comment thread src/auto-reply/reply/commands-approve.ts Outdated
Comment thread extensions/telegram/src/exec-approval-resolver.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d26857820

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/infra/exec-approval-channel-runtime.ts Outdated
Comment thread extensions/discord/src/monitor/exec-approvals.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d31154eb9c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/commands-approve.ts Outdated
Comment thread extensions/telegram/src/bot-handlers.runtime.ts Outdated
@scoootscooob scoootscooob changed the title fix(exec): unify channel approval runtime refactor(exec): unify channel approval runtime Mar 30, 2026
@scoootscooob scoootscooob force-pushed the codex/exec-approval-runtime branch from d31154e to 0fe7655 Compare March 30, 2026 08:47
@aisle-research-bot

aisle-research-bot Bot commented Mar 30, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Discord native approval delivery trusts unverified origin channel from request fields/sessionKey
2 🟠 High Authorization bypass for /approve when channel approval approvers list is empty (default-allow)
3 🟡 Medium Telegram approval callback leaks raw error details into chat
1. 🟠 Discord native approval delivery trusts unverified origin channel from request fields/sessionKey
Property Value
Severity High
CWE CWE-345
Location extensions/discord/src/approval-native.ts:83-101

Description

The Discord native approval adapter resolves the origin delivery target using request-controlled fields (turnSourceChannel/turnSourceTo) and a regex parse of sessionKey without verifying that these values are bound to an authenticated/recorded session.

This enables a forged approval request (exec or plugin) to cause the system to send approval content and/or the “check your DMs” redirect notice to an attacker-chosen Discord channel ID:

  • If turnSourceChannel === "discord" and turnSourceTo is provided, the adapter accepts it even when turnSourceAccountId is missing/empty, so there is no binding to the configured Discord account.
  • If session lookup fails or the session is not Discord, it falls back to extractDiscordChannelId(sessionKey) which only regex-parses digits from the provided sessionKey, allowing arbitrary channel IDs even when there is no session-store entry.

Vulnerable code:

if (turnSourceChannel === "discord" && turnSourceTo) {
  if (params.accountId && turnSourceAccountId && normalizeAccountId(turnSourceAccountId) !== normalizeAccountId(params.accountId)) {
    return null;
  }
  return { to: turnSourceTo };
}

const sessionTarget = resolveRequestSessionTarget(params);
if (!sessionTarget || sessionTarget.channel !== "discord") {
  const channelId = extractDiscordChannelId(params.request.request.sessionKey?.trim() || null);
  return channelId ? { to: channelId } : null;
}

Downstream, resolveChannelNativeApprovalDeliveryPlan can include this originTarget as a delivery target and/or use it for notifyOriginWhenDmOnly, leading to unauthorized messaging and potential leakage of approval details into arbitrary channels.

Recommendation

Do not trust turnSourceTo/sessionKey to select an origin channel unless it is cryptographically/authentically bound to a previously recorded session.

Recommended fixes (choose one or combine):

  1. Remove the regex fallback and only allow origin resolution from a validated session-store entry:
const sessionTarget = resolveRequestSessionTarget(params);
if (!sessionTarget || sessionTarget.channel !== "discord") return null;
  1. If turnSourceTo is allowed, require turnSourceAccountId and verify it matches the configured account, and ensure the origin target matches what’s stored for the session (or another server-side mapping) rather than taking arbitrary IDs:
if (turnSourceChannel === "discord" && turnSourceTo) {
  if (!params.accountId || !turnSourceAccountId) return null;
  if (normalizeAccountId(turnSourceAccountId) !== normalizeAccountId(params.accountId)) return null;// optionally: verify turnSourceTo equals sessionTarget.to (if sessionKey present)
}
  1. Consider setting requireMatchingTurnSourceChannel: true for Discord as well, and/or introduce a signed origin token that cannot be forged by plugins/clients.
2. 🟠 Authorization bypass for /approve when channel approval approvers list is empty (default-allow)
Property Value
Severity High
CWE CWE-285
Location src/plugin-sdk/approval-auth-helpers.ts:30-33

Description

The new /approve routing allows requests when there is an explicit channel approval authorization (resolveApprovalCommandAuthorization(...).explicit && authorized) even if params.command.isAuthorizedSender is false.

Several channel plugins implement auth.authorizeActorAction via createResolvedApproverActionAuthAdapter(). That helper currently default-allows approvals when the resolved approvers list is empty:

  • authorizeActorAction returns { authorized: true } when approvers.length === 0
  • Because authorizeActorAction exists, resolveApprovalCommandAuthorization() marks the result as explicit: true
  • handleApproveCommand then treats this as an explicit approval grant and accepts /approve from otherwise-unauthorized senders

Impact:

  • If an operator has a channel plugin with approval auth enabled but has not configured any approvers (or configuration resolves to an empty approvers list), any user who can message the bot in that channel can approve exec/plugin approvals, bypassing command allowlists/owner checks.

Vulnerable code:

const approvers = params.resolveApprovers({ cfg, accountId });
if (approvers.length === 0) {
  return { authorized: true } as const;
}

Recommendation

Change the default behavior so an empty approver list does not grant approval permission.

Safer options:

  1. Treat empty approvers as “no explicit approval auth configured” and let callers fall back to other checks (e.g., params.command.isAuthorizedSender). You can do this by returning undefined (so resolveApprovalCommandAuthorization() sets explicit:false) or by returning { authorized: true, explicit: false } in a different interface.

  2. Fail closed when approvers are empty:

const approvers = params.resolveApprovers({ cfg, accountId });
if (approvers.length === 0) {
  return {
    authorized: false,
    reason: `❌ No approvers are configured for ${approvalKind} approvals on ${params.channelLabel}.`,
  } as const;
}

Additionally, consider updating /approve gating to require either params.command.isAuthorizedSender or an explicit approval auth that is backed by a non-empty allowlist/approver set.

3. 🟡 Telegram approval callback leaks raw error details into chat
Property Value
Severity Medium
CWE CWE-209
Location extensions/telegram/src/bot-handlers.runtime.ts:1357-1363

Description

In the Telegram callback handler, failures while resolving an approval callback are replied back into the originating chat with the raw stringified error.

  • Input: the approval callback is triggered in a chat (often a group/supergroup) by any user pressing an inline button.
  • Sink: replyToCallbackChat sends errStr (derived from String(resolveErr)) into the chat.
  • Impact: internal error messages can disclose sensitive operational details (gateway URL/state, IDs, stack fragments, or upstream service responses). This is particularly risky in group chats where many users can see the message.

Vulnerable code:

} catch (resolveErr) {
  const errStr = String(resolveErr);
  logVerbose(`telegram: failed to resolve approval callback ${approvalCallback.approvalId}: ${errStr}`);
  await replyToCallbackChat(`❌ Failed to submit approval: ${errStr}`);
  return;
}

Recommendation

Do not echo raw exception strings to chats. Return a generic error to the user and log full details server-side.

Example:

} catch (resolveErr) {
  logVerbose(
    `telegram: failed to resolve approval callback ${approvalCallback.approvalId}: ${String(resolveErr)}`,
  );
  await replyToCallbackChat("❌ Failed to submit approval. Please try again or contact an administrator.");
  return;
}

Optionally, map known gateway errors to user-safe messages (e.g., "approval expired") and omit untrusted/verbose details.


Analyzed PR: #57516 at commit 9e1426b

Last updated on: 2026-03-30T15:44:26Z

@scoootscooob scoootscooob self-assigned this Mar 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd410cba47

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/telegram/src/exec-approvals-handler.ts
@scoootscooob scoootscooob force-pushed the codex/exec-approval-runtime branch from 297f8a3 to 7eb063e Compare March 30, 2026 10:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7eb063e70e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/discord/src/approval-native.ts Outdated
@scoootscooob scoootscooob force-pushed the codex/exec-approval-runtime branch from 7eb063e to 90df658 Compare March 30, 2026 10:34
@scoootscooob scoootscooob changed the title refactor(exec): unify channel approval runtime refactor(exec): centralize native approval delivery Mar 30, 2026
@scoootscooob scoootscooob changed the base branch from main to codex/exec-approval-foundation March 30, 2026 10:35
@scoootscooob scoootscooob force-pushed the codex/exec-approval-foundation branch from 686e553 to 728f0ac Compare March 30, 2026 11:00
@scoootscooob scoootscooob force-pushed the codex/exec-approval-runtime branch from fca29c6 to 7f725bb Compare March 30, 2026 11:02
@scoootscooob scoootscooob force-pushed the codex/exec-approval-foundation branch from 728f0ac to 933d9d8 Compare March 30, 2026 11:09
@scoootscooob scoootscooob force-pushed the codex/exec-approval-runtime branch from 7f725bb to 20111ae Compare March 30, 2026 11:13
@scoootscooob scoootscooob force-pushed the codex/exec-approval-runtime branch from 20111ae to 9e1426b Compare March 30, 2026 15:40
@scoootscooob scoootscooob force-pushed the codex/exec-approval-foundation branch from 0fae95b to c625a3c Compare March 30, 2026 15:42
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review


P2 Badge Reject foreign-bot /approve callback mentions

This callback path resolves approvals whenever approvalCallback is present, but it never validates whether a /approve@... payload is addressed to the current bot. Because parseExecApprovalCommandText accepts optional bot mentions, a callback payload like /approve@other_bot <id> allow-once will still be executed here, whereas the normal command handler rejects foreign mentions. In practice this lets mis-addressed or crafted inline callback payloads submit approval decisions through this bot instead of being ignored.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@scoootscooob

Copy link
Copy Markdown
Contributor Author

Superseded by the reviewable split here:

Closing this stacked PR so the approval work only has two active review surfaces: one main approval/runtime/routing/auth PR and one shell /approve hardening PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord channel: telegram Channel integration: telegram docs Improvements or additions to documentation maintainer Maintainer-authored PR size: XL

Projects

None yet

1 participant