Skip to content

feat(feishu): exec approval interactive cards with Handler pattern#60328

Closed
schumilin wants to merge 13 commits into
openclaw:mainfrom
schumilin:feat/feishu-exec-approval-v2
Closed

feat(feishu): exec approval interactive cards with Handler pattern#60328
schumilin wants to merge 13 commits into
openclaw:mainfrom
schumilin:feat/feishu-exec-approval-v2

Conversation

@schumilin

Copy link
Copy Markdown
Contributor

Summary

  • Problem: Feishu channel delivers exec approval requests as plain text — users must manually type /approve <id> <decision> to respond, while Discord and Telegram users get one-click interactive buttons.
  • Why it matters: Feishu is a primary messaging platform for many teams. Without interactive approval cards, the exec approval UX is significantly worse than other channels, making Feishu a second-class citizen.
  • What changed: Added a FeishuExecApprovalHandler that sends Feishu Interactive Cards (V2) with Allow Once / Allow Always / Deny buttons, tracks all delivered cards, and updates them in-place on resolution or expiry. Follows the same ExecApprovalChannelRuntime + createApproverRestrictedNativeApprovalCapability architecture used by Discord and Telegram.
  • What did NOT change (scope boundary): No changes to commands-approve.ts or the core approval authorization flow. No changes to Discord or Telegram exec approval behavior. The Feishu card UX approval flow (non-exec, existing feature) is untouched.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

N/A — new feature.

Regression Test Plan (if applicable)

N/A — new feature.

User-visible / Behavior Changes

  • When channels.feishu.execApprovals.enabled is true with configured approvers, exec approval requests are delivered as interactive Feishu cards instead of plain text.
  • Cards display: approval ID, command, working directory, host.
  • Three buttons: 允许一次 (Allow Once) / 始终允许 (Allow Always) / 拒绝 (Deny).
  • Cards update in-place when resolved (green for allowed, red for denied, grey for expired).
  • New config option target: "dm" (default) | "channel" | "both" controls where approval cards are sent.

Diagram (if applicable)

Before:
[exec needs approval] -> [plain text message to Feishu] -> [user types /approve id allow-once]

After:
[exec needs approval] -> [FeishuExecApprovalHandler receives gateway event]
                       -> [sends Interactive Card with buttons to configured targets]
                       -> [user clicks button] -> [card-action.ts calls gateway RPC]
                       -> [gateway broadcasts resolved event]
                       -> [handler updates ALL tracked cards in-place]

Cross-channel consistency:

Capability Discord Telegram Feishu (this PR)
Handler class DiscordExecApprovalHandler TelegramExecApprovalHandler FeishuExecApprovalHandler
Runtime ExecApprovalChannelRuntime createChannelNativeApprovalRuntime createChannelNativeApprovalRuntime
Approval capability createApproverRestrictedNativeApprovalCapability Same Same
Gateway event subscription WebSocket WebSocket WebSocket
Pending message tracking Yes Yes Yes
Multi-target card updates on resolve Yes Yes (clears buttons) Yes (updates card)
Timeout / expiry handling Yes Yes Yes

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? Yes — handler opens a WebSocket to the local gateway (same as Discord/Telegram handlers). Button clicks call exec.approval.resolve via callGateway RPC.
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • Risk + mitigation: The gateway WebSocket uses the existing operator.approvals scope with authenticated credentials. Button clicks verify isFeishuExecApprovalClientEnabled and isFeishuExecApprovalApprover before calling the RPC.

Repro + Verification

Environment

  • OS: macOS
  • Runtime: Node 22+
  • Integration/channel: Feishu (WebSocket connection mode)
  • Relevant config: channels.feishu.execApprovals: { enabled: true, approvers: ["ou_xxx"], target: "dm" }

Steps

  1. Configure Feishu exec approvals with at least one approver
  2. Set tools.exec.security: "allowlist" and tools.exec.ask: "on-miss" to require approval
  3. Send a message to the bot in Feishu asking it to run a command (e.g. "帮我运行 ls /tmp")
  4. Verify an interactive card appears with three buttons
  5. Click Allow Once / Allow Always / Deny
  6. Verify the card updates in-place with the decision result

Expected

  • Interactive card with three buttons appears in Feishu
  • Clicking a button resolves the approval and updates the card in-place
  • Resolved card shows decision, approval ID, and operator as @username

Actual

  • Interactive card with three buttons appears in Feishu DM ✅
  • Clicking Allow Once / Allow Always / Deny resolves the approval and updates the card in-place ✅
  • Resolved card shows correct decision label, approval ID, and operator as @username via Feishu at-tag ✅

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Unit tests: 28 passed (exec-approvals: 18, card-ux-exec-approval: 10). Build clean. Format/lint clean.

Human Verification (required)

  • Verified scenarios: Unit tests for config resolution, approver checks, card template generation, target routing, forwarding suppression. End-to-end manual testing with a real Feishu bot (DM mode, WebSocket connection).
  • Edge cases checked: disabled config, empty approvers, non-Feishu resolvedBy display, expired state, card action timeout (Feishu 200340).
  • Manual E2E verified: Allow Once (card updates to green "已允许(一次)"), Allow Always (card updates to green "已允许(始终)"), Deny (card updates to red "已拒绝"). All three decisions correctly resolve the approval, update the card in-place, and show the operator via Feishu at-tag.
  • What you did not verify: Group chat mode, multi-target delivery, cross-surface resolution (resolve from terminal while Feishu card is pending), card expiry timeout.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — new feature, opt-in via config.
  • Config/env changes? Yes — new channels.feishu.execApprovals config block.
  • Migration needed? No

Risks and Mitigations

  • Risk: Handler WebSocket connection adds a persistent connection per enabled Feishu account.
    • Mitigation: Same pattern as Discord/Telegram; connection is idle 99% of the time. Handler is only created when execApprovals.enabled: true.

AI Disclosure

  • AI-assisted (Claude Code)
  • Fully tested (unit tests + manual E2E with real Feishu bot)
  • We understand what the code does
  • Codex review (not yet run)

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added channel: feishu Channel integration: feishu size: XL labels Apr 3, 2026
@schumilin
schumilin force-pushed the feat/feishu-exec-approval-v2 branch from ed25944 to 45ec88d Compare April 3, 2026 14:08
@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds interactive Feishu approval cards for exec approval requests, bringing Feishu to parity with Discord and Telegram. The implementation follows the established ExecApprovalChannelRuntime + createApproverRestrictedNativeApprovalCapability pattern and is well-structured with 28 passing unit tests and documented manual E2E verification.

Key changes:

  • New FeishuExecApprovalHandler class manages card delivery and in-place updates via the WebSocket gateway subscription
  • Button click handling in card-action.ts correctly completes the Feishu card action token before the async gateway RPC (avoiding error 200340)
  • Forwarding suppression (shouldSuppressLocalFeishuExecApprovalPrompt, shouldSuppressFeishuExecApprovalForwardingFallback) cleanly separates the native handler path from the forwarding fallback path
  • buildResolvedPayload return type now supports undefined (fall-through) in addition to ReplyPayload | null, which is a clean and backwards-compatible extension
  • monitor.account.ts relaxes chatId validation to handle both WebSocket and long-connection SDK variants — all downstream consumers already guard against empty strings

Two style-level issues found:

  1. finalizeResolved discards _request and omits the command/cwd from the resolved card; since the pending card is replaced in-place, the approver loses context about what was approved/denied
  2. normalizeFeishuDirectApproverId is a misleading no-op that describes format validation in its comment but does not enforce it

Confidence Score: 4/5

Safe to merge; no functional regressions found, only two style-level suggestions.

The architecture is clean, follows established patterns, and is backed by 28 unit tests plus manual E2E verification. The two issues flagged (missing command/cwd in resolved card, misleading no-op normalizer) are UX/style gaps rather than correctness or security bugs. The core logic — target routing, approver gating, token deduplication, double-delivery prevention, and error handling — is all sound.

extensions/feishu/src/exec-approvals-handler.ts (resolved card context) and extensions/feishu/src/exec-approvals.ts (normalizer correctness)

Comments Outside Diff (1)

  1. extensions/feishu/src/exec-approvals.ts, line 1465-1472 (link)

    P2 normalizeFeishuDirectApproverId is a no-op wrapper

    The comment says "Feishu user IDs start with ou_ or on_" implying format validation, but the function body is identical to normalizeApproverId — it doesn't actually check for those prefixes. This is misleading and can mask misconfigured approver IDs (e.g. "123456" silently passes through and will never match a real open_id).

    Either enforce the format:

    function normalizeFeishuDirectApproverId(value: string | number): string | undefined {
      const normalized = normalizeApproverId(value);
      if (!normalized) return undefined;
      if (!normalized.startsWith("ou_") && !normalized.startsWith("on_")) return undefined;
      return normalized;
    }

    Or simplify to just use normalizeApproverId directly in the resolveApprovalApprovers call (removing the dead wrapper).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/feishu/src/exec-approvals.ts
    Line: 1465-1472
    
    Comment:
    **`normalizeFeishuDirectApproverId` is a no-op wrapper**
    
    The comment says "Feishu user IDs start with `ou_` or `on_`" implying format validation, but the function body is identical to `normalizeApproverId` — it doesn't actually check for those prefixes. This is misleading and can mask misconfigured approver IDs (e.g. `"123456"` silently passes through and will never match a real open_id).
    
    Either enforce the format:
    ```ts
    function normalizeFeishuDirectApproverId(value: string | number): string | undefined {
      const normalized = normalizeApproverId(value);
      if (!normalized) return undefined;
      if (!normalized.startsWith("ou_") && !normalized.startsWith("on_")) return undefined;
      return normalized;
    }
    ```
    Or simplify to just use `normalizeApproverId` directly in the `resolveApprovalApprovers` call (removing the dead wrapper).
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/feishu/src/exec-approvals-handler.ts
Line: 138-163

Comment:
**Resolved card drops command/cwd from the pending card**

The pending card shows the command and working directory (built in `buildPendingContent`), but `finalizeResolved` ignores `_request` and omits them from the resolved card. Because Feishu updates the card in-place (the pending card is replaced), the approver loses visibility into which command was approved/denied once the card flips state.

For exec approvals, the data is readily available in `_request`:

```ts
private async finalizeResolved(
  request: ApprovalRequest,
  resolved: ApprovalResolved,
  messages: PendingMessage[],
): Promise<void> {
  const execRequest = resolved.id && (request as ExecApprovalRequest).request
    ? (request as ExecApprovalRequest)
    : null;
  const commandDisplay = execRequest
    ? resolveExecApprovalCommandDisplay(execRequest.request)
    : null;
  await Promise.allSettled(
    messages.map(async (message) => {
      try {
        const resolvedCard = createExecApprovalResolvedCard({
          approvalId: resolved.id,
          decision: resolved.decision,
          resolvedBy: resolved.resolvedBy ?? undefined,
          command: commandDisplay?.commandText,
          cwd: execRequest?.request.cwd ?? undefined,
        });
        ...
```

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/feishu/src/exec-approvals.ts
Line: 1465-1472

Comment:
**`normalizeFeishuDirectApproverId` is a no-op wrapper**

The comment says "Feishu user IDs start with `ou_` or `on_`" implying format validation, but the function body is identical to `normalizeApproverId` — it doesn't actually check for those prefixes. This is misleading and can mask misconfigured approver IDs (e.g. `"123456"` silently passes through and will never match a real open_id).

Either enforce the format:
```ts
function normalizeFeishuDirectApproverId(value: string | number): string | undefined {
  const normalized = normalizeApproverId(value);
  if (!normalized) return undefined;
  if (!normalized.startsWith("ou_") && !normalized.startsWith("on_")) return undefined;
  return normalized;
}
```
Or simplify to just use `normalizeApproverId` directly in the `resolveApprovalApprovers` call (removing the dead wrapper).

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

Reviews (1): Last reviewed commit: "fix(feishu): resolve card action timeout..." | Re-trigger Greptile

Comment on lines +138 to +163
private async finalizeResolved(
_request: ApprovalRequest,
resolved: ApprovalResolved,
messages: PendingMessage[],
): Promise<void> {
await Promise.allSettled(
messages.map(async (message) => {
try {
const resolvedCard = createExecApprovalResolvedCard({
approvalId: resolved.id,
decision: resolved.decision,
resolvedBy: resolved.resolvedBy ?? undefined,
});
await updateCardFeishu({
cfg: this.opts.cfg,
messageId: message.messageId,
card: resolvedCard,
accountId: this.opts.accountId,
});
} catch (err) {
log.error(
`feishu exec approvals: failed to update card ${message.messageId}: ${String(err)}`,
);
}
}),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Resolved card drops command/cwd from the pending card

The pending card shows the command and working directory (built in buildPendingContent), but finalizeResolved ignores _request and omits them from the resolved card. Because Feishu updates the card in-place (the pending card is replaced), the approver loses visibility into which command was approved/denied once the card flips state.

For exec approvals, the data is readily available in _request:

private async finalizeResolved(
  request: ApprovalRequest,
  resolved: ApprovalResolved,
  messages: PendingMessage[],
): Promise<void> {
  const execRequest = resolved.id && (request as ExecApprovalRequest).request
    ? (request as ExecApprovalRequest)
    : null;
  const commandDisplay = execRequest
    ? resolveExecApprovalCommandDisplay(execRequest.request)
    : null;
  await Promise.allSettled(
    messages.map(async (message) => {
      try {
        const resolvedCard = createExecApprovalResolvedCard({
          approvalId: resolved.id,
          decision: resolved.decision,
          resolvedBy: resolved.resolvedBy ?? undefined,
          command: commandDisplay?.commandText,
          cwd: execRequest?.request.cwd ?? undefined,
        });
        ...
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/exec-approvals-handler.ts
Line: 138-163

Comment:
**Resolved card drops command/cwd from the pending card**

The pending card shows the command and working directory (built in `buildPendingContent`), but `finalizeResolved` ignores `_request` and omits them from the resolved card. Because Feishu updates the card in-place (the pending card is replaced), the approver loses visibility into which command was approved/denied once the card flips state.

For exec approvals, the data is readily available in `_request`:

```ts
private async finalizeResolved(
  request: ApprovalRequest,
  resolved: ApprovalResolved,
  messages: PendingMessage[],
): Promise<void> {
  const execRequest = resolved.id && (request as ExecApprovalRequest).request
    ? (request as ExecApprovalRequest)
    : null;
  const commandDisplay = execRequest
    ? resolveExecApprovalCommandDisplay(execRequest.request)
    : null;
  await Promise.allSettled(
    messages.map(async (message) => {
      try {
        const resolvedCard = createExecApprovalResolvedCard({
          approvalId: resolved.id,
          decision: resolved.decision,
          resolvedBy: resolved.resolvedBy ?? undefined,
          command: commandDisplay?.commandText,
          cwd: execRequest?.request.cwd ?? undefined,
        });
        ...
```

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d61c190. finalizeResolved now extracts command/cwd from the request and passes them to createExecApprovalResolvedCard, so the resolved card retains full context about what was approved/denied.

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

Copy link
Copy Markdown
Contributor

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: ed2594462a

ℹ️ 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 on lines +1085 to +1087
approvalCapability: {
...feishuApprovalCapability,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Preserve legacy Feishu /approve authorization path

Adding approvalCapability here causes Feishu approval auth to be taken from feishuApprovalCapability (exec-approver list) instead of the existing auth: feishuApprovalAuth allowlist path, because channel capability auth takes precedence over legacy auth wiring. In setups that have existing Feishu approvers via channels.feishu.allowFrom but do not configure channels.feishu.execApprovals.approvers (the new feature is supposed to be opt-in), /approve now resolves to unauthorized for both exec and plugin approvals, so pending approvals can be sent but cannot be resolved.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d61c190. getFeishuExecApprovalApprovers now falls back to allowFrom (matching the Telegram pattern), so existing setups that rely on channels.feishu.allowFrom for approval authorization continue to work without requiring explicit execApprovals.approvers config.

@schumilin

Copy link
Copy Markdown
Contributor Author

Both findings addressed in d61c190:

  1. Resolved card context: finalizeResolved now extracts command/cwd from the request and includes them in the resolved card, so approvers retain visibility into what was approved/denied.

  2. normalizeFeishuDirectApproverId no-op: Now validates ou_/on_ prefix — IDs without valid Feishu open_id prefixes are rejected. Also added allowFrom fallback to getFeishuExecApprovalApprovers (matching Telegram's pattern) to preserve legacy authorization paths.

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

Copy link
Copy Markdown
Contributor

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: d61c1906ce

ℹ️ 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".

accountId?: string | null;
payload: ReplyPayload;
}): boolean {
return feishuExecApprovalProfile.shouldSuppressLocalPrompt(params);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Gate local prompt suppression on approval request filters

shouldSuppressLocalFeishuExecApprovalPrompt suppresses any exec-approval payload whenever the Feishu client is enabled, but it does not check agentFilter/sessionFilter. In the same module, shouldHandleFeishuExecApprovalRequest does enforce those filters, so filtered requests can be skipped by the native handler while their fallback text prompt is still suppressed, leaving no Feishu approval prompt for those requests. Suppression should be aligned with the same request-level filter predicate used for handling.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This matches Telegram's behavior — shouldSuppressLocalTelegramExecApprovalPrompt also delegates to profile.shouldSuppressLocalPrompt without additional agentFilter/sessionFilter checks. If this needs to be tightened, it should be done at the createChannelExecApprovalProfile framework level to keep all channels consistent.

Comment thread extensions/feishu/src/exec-approvals.ts Outdated
Comment on lines +30 to +31
if (!normalized.startsWith("ou_") && !normalized.startsWith("on_")) {
return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Reject unsupported on_ approver IDs or authorize them correctly

This normalizer now accepts on_ IDs as valid approvers, but approval authorization compares against Feishu open_id values (for example card actions pass event.operator.open_id), so on_... entries cannot actually pass approver checks at runtime. That makes config silently accept approvers that can never approve requests. Either restrict accepted IDs to the identifier type used by auth checks or add consistent union-id matching across authorization paths.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9a6891a. Approver normalizer now only accepts ou_ (open_id) prefix. on_ (union_id) is rejected since card action callbacks use event.operator.open_id which is always ou_-prefixed.

@schumilin
schumilin force-pushed the feat/feishu-exec-approval-v2 branch from 9a6891a to 3722fd1 Compare April 4, 2026 00:20

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

Copy link
Copy Markdown
Contributor

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: 3722fd119d

ℹ️ 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/feishu/src/exec-approvals.ts Outdated
Comment on lines +33 to +34
if (!normalized.startsWith("ou_")) {
return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Normalize Feishu approver IDs before prefix checks

getFeishuExecApprovalApprovers now uses normalizeFeishuDirectApproverId for both explicit approvers and allowFrom, but that normalizer only accepts raw lowercase ou_... strings. Existing valid Feishu allowlist forms such as user:ou_xxx, feishu:user:ou_xxx, or uppercase variants are filtered out, which can empty the approver set and make /approve unauthorized after approvalCapability takes precedence over legacy auth. This is a regression for configurations that previously relied on the broader Feishu ID normalization behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cde6083. normalizeFeishuDirectApproverId now uses parseFeishuDirectConversationId to handle prefixed forms (user:ou_xxx, feishu:user:ou_xxx, etc.) before validating the ou_ prefix.

Comment thread extensions/feishu/src/exec-approvals.ts Outdated
Comment on lines +87 to +93
channel:
request.request.turnSourceChannel?.trim().toLowerCase() === "feishu" ? null : "feishu",
});
return (
!boundAccountId ||
!accountId ||
normalizeAccountId(boundAccountId) === normalizeAccountId(accountId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Restrict ambiguous non-Feishu requests to one account

The account-matching predicate treats boundAccountId === null as a match for every Feishu account, and for non-Feishu turn sources this path is guaranteed because resolveApprovalRequestAccountId is called with channel: "feishu". In multi-account setups, a single non-Feishu approval request can therefore be handled by multiple Feishu handlers, causing duplicate prompts and leaking command details across accounts. The matcher should gate this ambiguous case (for example, only allow when there is exactly one eligible Feishu account), as done in other channel implementations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cde6083. Added countFeishuExecApprovalEligibleAccounts and matchesFeishuRequestAccount — for non-Feishu turn sources with no bound account, the handler only proceeds when there is exactly one eligible Feishu account, matching Telegram's pattern.

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

Copy link
Copy Markdown
Contributor

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: cde60833a5

ℹ️ 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 on lines +1108 to +1110
approvalCapability: {
...feishuApprovalCapability,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Wire Feishu routing guards into approval capability

This capability only spreads feishuApprovalCapability, so the new forwarding safeguards in exec-approval-forwarding.ts are never applied. In practice, requests that bypass native suppression (for example, approvals originating from a non-Feishu turn source with channels.feishu.execApprovals.target: "dm") can still be forwarded by the generic exec forwarder to Feishu channel targets, which leaks command/cwd details into chats that the Feishu target mode was meant to exclude. The channel capability needs to register the Feishu-specific pending payload/suppression hooks instead of relying solely on the native adapter.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is already handled by the framework. createApproverRestrictedNativeApprovalCapability internally wires up shouldSuppressForwardingFallback (see src/plugin-sdk/approval-delivery-helpers.ts:112), so feishuApprovalCapability already includes forwarding suppression. Telegram doesn't register it separately either — it's built into the capability.

Comment thread extensions/feishu/src/exec-approvals.ts Outdated
Comment on lines +77 to +79
// Strip "user:" prefix for DM targets
const normalized = to.startsWith("user:") ? to.slice(5) : to;
return normalized === normalizedSenderId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Normalize Feishu target IDs before auth matching

Recipient authorization compares target.to by stripping only the user: prefix, so equivalent Feishu ID forms like dm:ou_xxx, open_id:ou_xxx, or provider-prefixed forms are treated as different users. If teams configure approvals.exec.targets with those accepted Feishu address forms, the intended recipient fails isTargetRecipient and /approve is rejected unless they are also in the approver list. This matcher should normalize with the same Feishu target parser used elsewhere.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7692637. isFeishuExecApprovalTargetRecipient now uses parseFeishuDirectConversationId to normalize all Feishu address forms (dm:ou_xxx, open_id:ou_xxx, feishu:user:ou_xxx, bare ou_xxx) consistently with the approver normalization path.

cowboy129 and others added 12 commits April 5, 2026 17:24
Add Feishu Interactive Card (V2) support for exec approval workflow:
- Send pending approval cards with Allow Once / Allow Always / Deny buttons
- Resolve approvals via direct gateway RPC (callGateway) instead of
  synthetic /approve commands, eliminating duplicate messages
- Update cards in-place on resolution showing command, result, and operator
- Display operator as Feishu user capsule (<at> mention)
- Suppress default resolved text notification for Feishu channel
- Support both DM and group chat contexts
- Chinese localization for all card labels

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Revert commands-approve.ts to upstream, add only Feishu channel checks
  (preserves plugin routing, explicit authorization, and fallback logic)
- Use static chunkTextForOutbound in feishu outbound (fixes chunker test)
- Quote <at> tag id attribute in resolved card
- Regenerate schema.base.generated.ts

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ssion

- Verify event.operator.open_id is a configured approver before resolving
  exec approval via card button RPC (prevents unauthorized button clicks)
- Only suppress resolved text notification when resolved via Feishu card
  action (resolvedBy starts with "feishu:"); fall through to default text
  notification when resolved from other surfaces (terminal, web, etc.)
- Support undefined return from buildResolvedPayload to signal "use default"

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Fix resolvedBy check to match "Feishu card approval" (the gateway
  clientDisplayName) instead of "feishu:" prefix
- Preserve "sandbox" host value in Feishu approval payloads instead of
  collapsing non-"node" hosts to "gateway"

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Check isFeishuExecApprovalClientEnabled before processing card button
  clicks, so disabling the feature prevents stale cards from resolving
- Log card patch failures and fall back to sending a new resolved card
  instead of silently swallowing the error

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ayload

Pass replyInThread to sendCardFeishu in the sendPayload path so card
replies in threaded contexts are posted as thread replies instead of
inline replies, matching the existing sendText behavior.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Refactor Feishu exec approval to use the same ExecApprovalChannelRuntime
+ createApproverRestrictedNativeApprovalCapability architecture as
Discord and Telegram.

- Add FeishuExecApprovalHandler with gateway WebSocket subscription,
  pending message tracking, multi-target card updates on resolve, and
  timeout/expiry management
- Add approval-native.ts adapter for proper authorization, target
  routing, and forwarding suppression
- Rewrite exec-approvals.ts to use createChannelExecApprovalProfile
- Simplify card-action.ts button handler to RPC-only (card updates
  delegated to handler)
- Support expired card state and smart resolvedBy display
- Fix sendOutboundText replyInThread passthrough

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…zation

- Fix empty approvalId in expired card (Greptile P2)
- Extract host normalization to local variable in forwarding path (Greptile P2)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Complete card action token before async gateway RPC to avoid Feishu
  200340 timeout error
- Extract Feishu open_id from gateway clientDisplayName for proper
  <at> tag in resolved cards

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…r validation, allowFrom fallback

- Include command/cwd in resolved card so approvers retain context
  after in-place update (Greptile P2)
- Validate ou_/on_ prefix in normalizeFeishuDirectApproverId to
  reject misconfigured approver IDs (Greptile P2)
- Fall back to allowFrom when resolving approvers, matching Telegram
  pattern to preserve legacy /approve authorization (Codex P1)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
on_ (union_id) would silently fail approver checks at runtime since
card action callbacks use event.operator.open_id (ou_ prefix).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…atching

- Parse prefixed forms (user:ou_xxx, feishu:user:ou_xxx) via
  parseFeishuDirectConversationId before ou_ validation (Codex P1)
- Gate non-Feishu turn sources to single eligible account to prevent
  duplicate prompts in multi-account setups (Codex P2)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@schumilin
schumilin force-pushed the feat/feishu-exec-approval-v2 branch from cde6083 to d42a6e5 Compare April 5, 2026 09:24
Use the same Feishu ID parser for target recipient matching as for
approver normalization, so all address forms (dm:ou_xxx, open_id:ou_xxx,
feishu:user:ou_xxx) are handled consistently. (Codex P2)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

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

Copy link
Copy Markdown
Contributor

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: 7692637b4c

ℹ️ 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 on lines +32 to +34
const parsed = parseFeishuDirectConversationId(value);
if (parsed) {
return parsed;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Validate parsed Feishu approver IDs before returning them

normalizeFeishuDirectApproverId returns any value produced by parseFeishuDirectConversationId without checking that it is an ou_ open_id, so entries like user:foo or user:OU_123 are treated as configured approvers. Those values increase approver count and enable the native exec-approval client, but they can never match card callback event.operator.open_id, which leaves approvals effectively unresolvable in production configs that use prefixed IDs. Fresh evidence is that parseFeishuDirectConversationId accepts any user: suffix, so this early return bypasses the intended ou_ validation.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Keep this PR open because current main still lacks the central Feishu native exec approval card feature and the branch contains meaningful source work, but the patch is not merge-ready due to approval-boundary defects, config/docs mismatch, and missing inspectable real Feishu proof.

Canonical path: Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review.

So I’m closing this here because the remaining work is already tracked in the canonical issue.

Review details

Best possible solution:

Close this stale PR. The latest review rated it F, the branch still lacks merge-ready proof, and there has been no human follow-up after the durable review.

Do we have a high-confidence way to reproduce the issue?

Yes for the review blockers: source inspection shows the parsed-prefix path bypasses the intended ou_ validation, current main provides a narrower approval gateway resolver, and the Feishu config schema diverges from shared native approval mode. The full Feishu UX path still lacks inspectable after-fix proof.

Is this the best way to solve the issue?

No in the current branch shape. The handler/card direction is plausible, but the mergeable solution should use the focused approval gateway seam, validate approver IDs after parsing, align the new config with sibling native clients, and document the new surface.

Security review:

Security review needs attention: The diff adds button-driven exec approval resolution and broad gateway SDK exports; approver identity validation and gateway scope need attention before merge.

  • [medium] Broad gateway RPC exported to plugins — src/plugin-sdk/infra-runtime.ts:52
    The PR exports callGateway and gateway client constants from openclaw/plugin-sdk/infra-runtime, a broad compatibility barrel, even though current main has a focused approval-only gateway resolver.
    Confidence: 0.9
  • [medium] Parsed approver IDs bypass open-id validation — extensions/feishu/src/exec-approvals.ts:32
    The Feishu approver normalizer returns parsed user:/dm:/open_id: suffixes before checking for an ou_ open_id, so invalid IDs can enable approval clients that callbacks can never authorize.
    Confidence: 0.92

AGENTS.md: found and applied where relevant.

What I checked:

  • stale F-rated PR: PR was opened 2026-04-03T14:03:34Z, is older than 60 days, and the latest review rated it F.
  • proof blocker: real behavior proof is missing and proof tier is F, so this branch is not merge-ready without contributor follow-up.
  • no human follow-up: live comments and timeline hydrated by apply contain no non-automation activity after the ClawSweeper review.

Likely related people:

  • steipete: GitHub commit history shows repeated recent work on Feishu card actions and approval SDK helper surfaces relevant to this PR. (role: recent Feishu and approval SDK contributor; confidence: high; commits: a1090b604312, 77d04a39d87a, 0b8aabe8648e; files: extensions/feishu/src/card-action.ts, src/plugin-sdk/approval-client-helpers.ts, src/plugin-sdk/approval-gateway-runtime.ts)
  • gumadeiras: Commit history shows native approval lifecycle and approval capability seam work that this PR builds on. (role: native approval SDK seam contributor; confidence: high; commits: d78512b09d1d, 28fc5d9b5e19, c87c8e66bf7d; files: src/plugin-sdk/approval-delivery-helpers.ts, src/plugin-sdk/approval-gateway-runtime.ts, extensions/discord/src/approval-native.ts)
  • Takhoffman: GitHub history shows structured Feishu card actions and interactive approval flows were introduced in the Feishu card-action path. (role: introduced Feishu interactive card behavior; confidence: high; commits: fa62231afca3, f4dbd78afd64; files: extensions/feishu/src/card-action.ts)
  • drobison00: Commit history shows recent changes to Feishu card-action callback context behavior, which is adjacent to this PR's button callback path. (role: recent Feishu card-action contributor; confidence: medium; commits: 90979d7c3ef7; files: extensions/feishu/src/card-action.ts, extensions/feishu/src/monitor.account.ts)

Codex review notes: model internal, reasoning high; reviewed against 583b7195b4c2.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper clawsweeper Bot added P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 10, 2026
@leolld

leolld commented Jun 13, 2026

Copy link
Copy Markdown

Why hasn't it been merged all along? Feishu's interactive card approval has been missing, and the user experience is very poor

@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Jun 14, 2026
@clawsweeper

clawsweeper Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

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

Labels

channel: feishu Channel integration: feishu merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants