hermes - ✅(Solved) Fix [Bug]: [Feishu][v0.9.0] approval card clicks can fall through to unsupported /card path [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#11600Fetched 2026-04-18 05:59:59
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Participants
Timeline (top)
cross-referenced ×1referenced ×1

Fix Action

Fix / Workaround

Clicking a Feishu approval button should resolve the pending approval and patch/update the original approval card in place.

PR fix notes

PR #11740: fix(feishu): drop non-approval card callbacks instead of replying "Unknown command /card" (#11600)

Description (problem / solution / changelog)

What does this PR do?

Fixes #11600.

The Feishu adapter has a dead-end code path: every card.action.trigger callback whose payload does not carry a hermes_action key is converted into a synthetic /card <tag> <value> MessageType.COMMAND event and pushed through the gateway slash-command dispatcher. Because Hermes has never registered a card command, the dispatcher falls through to the Unrecognized slash command branch at gateway/run.py:3319 and replies to the user with:

Unknown command /card. Type /commands to see what's available, or resend without the leading slash to send as a regular message.

That reply fires on every click in any interactive card that isn't one of Hermes's own approval cards — and, critically, it also fires when a real approval callback's payload shape doesn't quite match _handle_approval_card_action's expectations (SDK version drift, proxy wrappers, etc.), which is the exact release-level symptom #11600 reports on v0.9.0. The user then gets a misleading "Unknown command" reply that has nothing to do with approvals, while the pending approval silently fails to resolve.

Root cause

gateway/platforms/feishu.py:2168 synthesises /card <tag>:

synthetic_text = f"/card {action_tag}"
...
synthetic_event = MessageEvent(
    text=synthetic_text,
    message_type=MessageType.COMMAND,
    ...
)
await self._handle_message_with_guards(synthetic_event)

Grep for card handler registration:

$ python -c "from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS; print('card' in GATEWAY_KNOWN_COMMANDS)"
False

…and hermes_cli/commands.py / COMMAND_REGISTRY do not define one. The /card synthesis was documented at website/docs/user-guide/messaging/feishu.md:220-228 as a generic extension point but was never wired up to anything. The feature landed with the original Feishu integration in ca4907df (#3817) and has been unreachable since.

Smallest safe fix

  • _handle_card_action_event becomes a log-and-drop handler. It still walks the SDK event shape to maintain the 15-minute dedup token cache (so repeat SDK deliveries don't re-log) and emits a compact DEBUG line with the action_tag and the keys of the value dict (never the raw value — user-typed payloads must not land at INFO). No synthetic command, no _handle_message_with_guards call.
  • Approval branch is completely unchanged. Hermes-generated cards still carry hermes_action in their button value and route through _on_card_action_trigger_handle_approval_card_action_resolve_approval exactly as before. See the green/red resolved-card tests in TestCardActionCallbackResponse, all unchanged.
  • Updated website/docs/user-guide/messaging/feishu.md to describe the actual behaviour (approval cards work, unrecognized card callbacks are dropped) instead of the aspirational /card claim.

Compat / behaviour truth table

Card callback shapeBeforeAfter
Approval (hermes_action present)Resolves approval inlineUnchanged
Non-approval, dict value, no hermes_action/card <tag> command → user sees Unknown command /cardDropped with DEBUG log
Non-approval, non-dict action.value (SDK variant / JSON-string)/card <tag> command (same noise)Dropped with DEBUG log
Repeat SDK delivery of same tokenDedupedStill deduped
Missing chat_id / operatorAlready droppedStill dropped

No user-facing behaviour is silently lost — the only removed path was one that produced a misleading error reply.

Narrow scope — why only the log-drop

I deliberately did not:

  • Introduce a new MessageType.CARD_ACTION event type. That's a feature, not a fix. If plugin extensibility for Feishu card events is desired later, it can be added as an additive follow-up without re-introducing the misleading /card reply.
  • Try to repair why some approval-card clicks fail to resolve at all (the other half of #11600's first symptom). That depends on SDK event shape behaviour that this repo can't observe without a trace from the reporter — it belongs in its own investigation. Dropping the /card synthesis at least means that when an approval path fails, the user stops seeing a wrong-looking "Unknown command" reply, so the separable bug becomes observably separate in production.
  • Extend the GATEWAY_KNOWN_COMMANDS allow-list to include card. Making the dispatcher not complain about an unimplemented command would hide a real design gap.

Pre-emptive reviewer Q&A

Q: Could anyone depend on the /card synthesis? No registered command handles it; no plugin hook consumes it. The behaviour was exclusively "user sees Unknown command /card". External dependency on that is implausible.

Q: Should the drop log at INFO? No. These callbacks fire on every user click on any non-Hermes card in a shared chat. INFO-level logging would be noisy for anything larger than a demo deployment. DEBUG is reachable via HERMES_LOG_LEVEL=DEBUG for anyone actually debugging Feishu card flows.

Q: Why log value keys but not the full value? Feishu cards can embed user-typed strings in the value payload. Keys are bounded metadata (card authors control them); full values can be arbitrary text including PII. Keys-only preserves debuggability without routing user content through INFO/DEBUG logs.

Q: What happens to the P2CardActionTriggerResponse return value now that non-approval callbacks don't schedule agent work? Unchanged. _on_card_action_trigger still returns P2CardActionTriggerResponse() for non-approval callbacks — Feishu's SDK requires some response to the callback so the button doesn't spin forever. We just don't push an agent event onto the loop.

Q: Dedup cache behaviour? Still maintained. The first callback for a given token logs its drop; subsequent deliveries of the same token short-circuit via the existing _is_card_action_duplicate check. Covered by test_duplicate_token_still_deduped.

Q: Security? The drop path is strictly less privileged than the synthesise-command path: we now don't forward user-controlled action_tag / action_value strings into the agent pipeline as a command. That removes a small but real injection surface: a card author could have crafted an action.value whose JSON serialization contains slash-command tokens, and every click would have sent that as a /card … command under a Hermes-user-looking MessageEvent. The new drop path never constructs a MessageEvent at all, so the agent pipeline never sees card-supplied strings.

Regression coverage

tests/gateway/test_feishu_approval_buttons.py — the pre-existing test_routes_as_synthetic_command (which pinned the broken behaviour) is replaced with test_non_approval_callback_does_not_synthesize_command (same code path, inverted assertion). Plus 4 new tests:

  • test_non_approval_callback_does_not_synthesize_command — no MessageEvent emitted, no chat-info / sender-profile lookups wasted.
  • test_duplicate_token_still_deduped — processed-token cache still recognises repeat SDK deliveries.
  • test_non_dict_action_value_is_safe — SDK variant / malformed payload (action.value as a string) doesn't crash.
  • test_missing_chat_id_is_safe — truncated payloads still handled.
  • test_non_approval_trigger_does_not_reach_approval_handler — pins the routing invariant end-to-end through _on_card_action_trigger.

On unpatched origin/main, 3 of these tests fail with the expected "_handle_message_with_guards should not have been called. Called 1 times" / similar assertion errors, confirming they catch the regression. The remaining two pin behaviour preserved by the fix.

How to test

  1. Focused suite:

    source venv/bin/activate
    python -m pytest tests/gateway/test_feishu_approval_buttons.py -q

    22 passed (17 existing + 5 new).

  2. Verify the tests catch the bug on origin/main:

    git stash push gateway/platforms/feishu.py
    python -m pytest tests/gateway/test_feishu_approval_buttons.py::TestNonApprovalCardAction -v

    → 3 fail with _handle_message_with_guards was called 1 times. Restore with git stash pop.

  3. CI-aligned broad suite:

    python -m pytest tests/ -q --ignore=tests/integration --ignore=tests/e2e --tb=short -n auto

    → 12385 passed, 39 skipped, 7 pre-existing baseline failures (tests/gateway/test_matrix.py, tests/gateway/test_approve_deny_commands.py ×2, tests/hermes_cli/test_gateway_wsl.py ×2, tests/tools/test_file_staleness.py ×2). All 7 reproduce on clean origin/main with identical assertions — none in the touched code path.

Tested on: macOS 15 (Darwin 25.5.0), Python 3.11.15.

Related Issue

Fixes #11600

Related / adjacent (not fixed here): #9585, #10073, #9533, #10256, #10412, #10301, #10801 — several of these may have been partially masked by the same /card noise. After this lands, the signal-to-noise on future Feishu approval reports should improve because the dispatcher can no longer conflate approval failures with unknown-command replies.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • gateway/platforms/feishu.py: rewrite _handle_card_action_event as log-and-drop; update _on_card_action_trigger docstring; update send_exec_approval docstring to reference the actual handler chain.
  • tests/gateway/test_feishu_approval_buttons.py: replace test_routes_as_synthetic_command with test_non_approval_callback_does_not_synthesize_command, add 4 additional regression tests.
  • website/docs/user-guide/messaging/feishu.md: rewrite the "Interactive Card Actions" section to describe actual behaviour.

Adjacent surfaces checked

  • _handle_approval_card_action / _resolve_approval / send_exec_approval — unchanged; approval resolution path verified by the (untouched) TestResolveApproval and TestCardActionCallbackResponse suites.
  • _is_card_action_duplicate / dedup token cache — unchanged; new test pins it still works.
  • _on_card_action_trigger — only docstring + one added clarifying comment; routing logic byte-for-byte unchanged.
  • GATEWAY_KNOWN_COMMANDS / hermes_cli/commands.py — confirmed card is not registered anywhere (see "Root cause" above).
  • Other gateway platforms (matrix.py, slack.py, etc.) — none synthesize /card; no cross-platform impact.
  • Plugin hooks — grepped hooks.emit / invoke_hook for any card:* event names; none exist.

Checklist

Code

  • I've read the Contributing Guide
  • Commit messages follow Conventional Commits (fix(scope):)
  • Searched for existing PRs — none on #11600
  • Only changes related to this fix
  • Ran the CI-aligned test command and classified failures baseline-vs-change
  • Added tests for my changes (5 new; 3 fail on origin/main without the fix, 2 pin preserved behaviour)
  • Tested on macOS 15 (Darwin 25.5.0), Python 3.11.15

Documentation & Housekeeping

  • Updated website/docs/user-guide/messaging/feishu.md to describe actual behaviour
  • No config-key changes
  • No architecture/workflow changes
  • Cross-platform impact: none — only touches the Feishu adapter
  • No tool descriptions/schemas touched

Notes for reviewers

  • No standalone lint command is defined; CI runs python -m pytest tests/ -q --ignore=tests/integration --ignore=tests/e2e --tb=short -n auto (matches .github/workflows/tests.yml).
  • No hermes_cli/** changes — the Nix workflow should not trigger.

Changed files

  • gateway/platforms/feishu.py (modified, +52/-37)
  • tests/gateway/test_feishu_approval_buttons.py (modified, +118/-10)
  • website/docs/user-guide/messaging/feishu.md (modified, +4/-6)
RAW_BUFFERClick to expand / collapse

Bug Description

On the clean v0.9.0 release (v2026.4.13 tag), Feishu command-approval cards do not reliably resolve after a button click even when card.action.trigger is subscribed and the gateway is running in webhook mode.

In our environment, ordinary Feishu text messages still reach Hermes, but approval-card button clicks either:

  • do nothing on the clean release, or
  • during debugging of the callback path, fall through to synthetic /card handling and Hermes replies with Unknown command /card.

This looks adjacent to the existing Feishu approval issues, but the failure mode here is specifically that approval callbacks can fall through to a generic /card path that Hermes does not actually implement as a command.

Steps to Reproduce

  1. Run Hermes v0.9.0 (v2026.4.13) with Feishu in webhook mode.
  2. In the Feishu developer console, subscribe to card.action.trigger.
  3. Trigger a dangerous command so Hermes sends the interactive approval card.
  4. Click any approval button.
  5. Observe that the pending approval is not resolved.
  6. Ordinary text messages sent afterward still reach Hermes normally.

Expected Behavior

Clicking a Feishu approval button should resolve the pending approval and patch/update the original approval card in place.

If Hermes cannot recognize the callback payload as a valid approval callback, it should reject it explicitly and log why, not fall through into an unsupported generic command path.

Actual Behavior

  • On the clean release, approval-card button clicks do not resolve the pending approval.
  • During callback-path debugging, the event can fall through to synthetic /card ... handling and Hermes replies:

Unknown command /card. Type /commands to see what's available, or resend without the leading slash to send as a regular message.

Why This Looks Like a Real Release Bug

On v0.9.0:

  • website/docs/user-guide/messaging/feishu.md documents interactive card actions as synthetic /card command events.
  • gateway/platforms/feishu.py builds synthetic_text = f"/card {action_tag}" for non-intercepted card callbacks.
  • hermes_cli/commands.py does not define a card command.

So if an approval callback is not recognized by the Feishu adapter for any reason, it can fall through into a command path that Hermes does not actually support.

Related Issues / PRs

  • #9585
  • #10073
  • #9533
  • #10256
  • #10412
  • #10301
  • #10801

This report is meant to capture the remaining release-level symptom after checking those related threads.

Environment

  • Hermes version: v0.9.0
  • Tag: v2026.4.13
  • Platform: Feishu
  • Connection mode: webhook
  • card.action.trigger: enabled

extent analysis

TL;DR

The issue can be fixed by modifying the Feishu adapter to correctly recognize and handle approval callbacks, preventing them from falling through to an unsupported generic command path.

Guidance

  • Review the gateway/platforms/feishu.py file to ensure that the synthetic_text variable is correctly constructed for approval callbacks, and that it does not trigger the unsupported /card command path.
  • Verify that the card.action.trigger subscription is correctly configured and that the callback payload is being received and processed by the Feishu adapter.
  • Check the logging to see why the approval callback is not being recognized as a valid callback, and adjust the adapter's logic accordingly.
  • Consider adding explicit error handling and logging in the Feishu adapter to reject unrecognized callbacks and provide more informative error messages.

Example

No code snippet is provided as the issue requires a deeper understanding of the Feishu adapter's implementation and the specific requirements of the approval callback handling.

Notes

The issue seems to be related to the Feishu adapter's handling of approval callbacks, and the fact that the /card command is not defined in hermes_cli/commands.py. The solution will likely involve modifying the adapter to correctly recognize and handle approval callbacks.

Recommendation

Apply a workaround by modifying the Feishu adapter to correctly handle approval callbacks, as the issue is specific to the v0.9.0 release and the related threads do not provide a clear solution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [Bug]: [Feishu][v0.9.0] approval card clicks can fall through to unsupported /card path [1 pull requests, 1 participants]