Skip to content

feat(approval): native cross-platform approval delegation - #47863

Open
alenzhong wants to merge 1 commit into
NousResearch:mainfrom
alenzhong:feat/approval-delegation-native
Open

alenzhong wants to merge 1 commit into
NousResearch:mainfrom
alenzhong:feat/approval-delegation-native

Conversation

@alenzhong

@alenzhong alenzhong commented Jun 17, 2026

Copy link
Copy Markdown

Summary

Native, config-driven approval delegation — routes non-admin users' dangerous command approvals to designated admins across platforms (e.g. WeChat/WeCom → Feishu). Replaces PR #37771's monkey-patch approach with direct hook integration in core modules.

Rebase note (2026-09-13): rebased onto latest main (979576d938) and grafted the delegation layer onto upstream's exec-approval template method refactor (ad305be: send_exec_approval now builds an ExecApprovalPrompt in BasePlatformAdapter; adapters only render it via _send_exec_approval_prompt), and onto the plain-language message pass (23036e2) — delegated approval labels/card headers track upstream's shared EA_HEADER_TEXT wording in English and localize exactly once via an opt-in locale flag. Where upstream ships its own approval buttons (qqbot, WhatsApp, relay) they render the local user's approval exactly as before — this PR does not touch those adapters at all, and delegated button cards are never routed to them (the marker gate falls delegation back to typed /approve there; see finding #8).

Why a new PR (not iteration of #37771)

PR #37771 used monkey-patching to intercept gateway approval flows — 3 runtime patches on run.py internals that break on refactors and are hard to review. This implementation achieves the same feature via native hooks:

Aspect #37771 (monkey-patch) This PR (native)
Integration Runtime patches on _approval_notify_sync, _handle_approve_command, _handle_deny_command Direct config-driven branches in the same functions
Config key approvals.delegate_to (list) approvals.delegation.enabled + approvals.delegation.admins (list)
Button i18n Hardcoded English t() for all card elements (header, reason, buttons) across all platforms
Code reuse Duplicated approve/deny logic (~120 lines) Extracted _try_handle_delegated_approval() shared method
State management delegation.py + __init__.py monkey-patches Single __init__.py — pure state, no side effects
Files touched 6 core files modified 4 core files + 1 new module + 1 config

Enterprise Security Context

In enterprise environments, the default approval mechanism allows self-approval — the same user who triggers a dangerous command can approve it. This violates separation of duties and makes Hermes unsuitable for regulated deployments.

Before:  User → dangerous cmd → own approval → self-approve → executes  ← Security flaw
After:   User → dangerous cmd → admin on Feishu/WeCom → approve/deny → relay result

Key Changes

File Change
gateway/approval_delegation/__init__.py New: pure state management (config, admin check, delegation register/resolve/clear)
gateway/run_turn_runner.py Delegation redirect in _approval_notify_sync: button-based (send_exec_approval) + text fallback. register_delegation() only called after successful send. Button routing additionally requires the adapter to declare the admin identity gate (_enforces_delegation_admin_identity marker; see finding #8) — adapters without it get the text /approve path instead.
gateway/platforms/base.py Delegation rides the shared exec-approval template: ExecApprovalPrompt.admin_user_id field + send_exec_approval forwarding; explicit enforcement marker _enforces_delegation_admin_identity / supports_delegation_admin_gate(); locale-aware approval labels (_EA_I18N_ACTION_LABELS) resolved once in the base — English rendering is byte-identical to upstream
gateway/slash_commands.py Admin /approve /deny handling with cross-platform result notification. Delegation check runs BEFORE the local pending-approval gate (admins resolve other users' sessions).
plugins/platforms/feishu/adapter.py i18n for approval card + admin identity enforcement (operator.user_id not open_id)
plugins/platforms/wecom/adapter.py send_exec_approval() with template_card buttons, re-adapted to the streaming architecture (group chats reply passively via cached req_id; DMs use APP_CMD_SEND) + dual security
plugins/platforms/slack/adapter.py Block Kit approval buttons (4 buttons) + i18n + dual-dict design (_approval_resolved for double-click + _approval_admin for admin identity). Multi-workspace compatible.
plugins/platforms/telegram/adapter.py Inline keyboard approval (4 buttons) + _approval_state dict with admin_user_id/chat_id enforcement
plugins/platforms/discord/adapter.py Button components approval (4 buttons) + _component_check_auth admin enforcement
plugins/platforms/matrix/adapter.py Reaction-based approval with admin_user_id enforcement
plugins/platforms/teams/adapter.py Adaptive Card approval with admin_user_id enforcement
tests/gateway/test_telegram_approval_buttons.py adapted 2 upstream tests to the dict-shaped _approval_state (assertions unchanged)
hermes_cli/config_defaults.py Default config for approvals.delegation
locales/*.yaml (16 files) gateway.approval_delegation.* translation keys (22 keys)
tests/gateway/approval_delegation/ 21 unit tests (config, admin detection, state, concurrency, TTL, admin-identity-gate detection)
tests/gateway/platforms/test_wecom_approval.py 16 WeCom template_card button security + delivery-path tests (adapted to streaming architecture)

Reviewer Feedback (addressed)

Three security findings from @teknium1's review have been resolved across all 7 button-capable platforms:

# Finding Resolution
1 Typed /approve//deny didn't verify user_id — any chat member could resolve Added is_admin_user() check in _try_handle_delegated_approval()
2 admin_user_id swallowed by **kwargs — button callbacks used general gateway auth, not delegation admin identity Made admin_user_id a formal parameter across all adapters; stored in callback state; enforced on every button click
3 async handler blocked event loop with future.result(timeout=15) Replaced with await asyncio.wrap_future()

Adversarial Review Findings (addressed)

Independent adversarial review (multiple rounds, incl. after the WeCom button restoration and the latest rebase) discovered five additional hardening issues:

# Finding Resolution
4 Fail-open admin_user_id: all platforms silently skipped admin identity enforcement when admin_user_id was empty string Added logger.warning() on all button-capable platforms when admin_user_id is empty. Discord already fail-closed (empty admin_user_ids set = deny).
5 register_delegation() called before send_exec_approval() — failed sends left orphan delegations blocking sessions for 600s TTL Moved register_delegation() inside the if _sent: block. Delegation only created after confirmed delivery to admin. Failed sends cleanly skip to next admin or fallback.
6 Telegram pop-before-validate DoS: approval callback popped _approval_state BEFORE admin identity validation — any chat member (attacker) clicking the button first consumed the state, so the real admin's later click got "already been resolved", permanently blocking the approval .get() for validation, .pop() only after all checks pass. Unauthorized clicks return early with state preserved, matching the Feishu/WeCom/Teams pattern (verified: attacker click → state retained → admin can still approve).
7 WeCom forwarded-card DoS (restored buttons): the event_key whitelist check popped the task BEFORE chat/user identity validation — a group member forwarding the admin's card and clicking an invalid key deleted the real task, blocking the admin's approval Unknown event_key no longer pops (TTL expiry handles cleanup). Also hardened stored=None to fail-closed (unknown/guessed task_id → no action), matching the Feishu/Telegram/Teams pattern. Validated with adversarial tests.
8 Rebase-introduced attack-surface expansion: upstream made send_exec_approval a base template method (ad305be) and added native button rendering on qqbot / WhatsApp / relay after this PR's original security review. The template accepts admin_user_id for every adapter, so a signature probe can no longer tell the enforcing renderers from the ones that silently ignore the field (WhatsApp: any allowlist member could approve another user's session; qqbot: session-owner check makes admin clicks dead UX) Enforcement is now an explicit adapter opt-in marker (_enforces_delegation_admin_identity = True + supports_delegation_admin_gate()). The delegation router only sends buttons to marked adapters; everything else — including upstream-native qqbot/WhatsApp/relay and any future template renderer — falls back to typed /approve, which always runs is_admin_user. Fail-closed by default (base marker = False). 7 regression tests pin the invariant across all adapters.

Per-platform admin_user_id enforcement

Platform Storage Validation
Telegram _approval_state: Dict[int, dict] caller_id vs stored admin_user_id + chat_id match
Discord _approval_state admin_user_id checked in _component_check_auth
Feishu _approval_state operator.user_id vs admin user_id (not open_id) + chat_id match
Slack _approval_resolved: Dict[Any, bool] + _approval_admin: Dict[Any, tuple] channel_id + admin_user_id in separate dict. Multi-key lookup for workspace-scoped markers. WARNING on missing/empty admin info.
Teams Callback state dict admin_user_id check on button click
Matrix Callback state dict admin_user_id check on reaction
WeCom _approval_tasks: Dict[str, tuple] _handle_template_card_event: chat_id + user_id match, fail-closed (preserves task on reject)

Config

approvals:
  delegation:
    enabled: false  # disabled by default — zero impact on personal users
    admins:
      - platform: feishu
        user_id: "79b3f..."
        chat_id: "oc_96ce9a..."

Features

  • Disabled by default — no behavior change unless explicitly enabled
  • Cross-platform: WeChat/WeCom user → Feishu/Telegram/Discord/WeCom admin
  • 4-button UI on all capable platforms (Allow Once / Session / Always / Deny)
  • Text fallback /approve /deny for platforms without button support
  • Multi-admin: iterates configured admins, uses first reachable
  • Fail-safe: any delegation failure → fallback to user's own approval
  • i18n: all user-facing strings use locale keys with English fallback (16 languages)
  • Audit trail: all delegation actions logged with admin identity. Empty admin_user_id produces WARNING log for visibility.
  • Dual security on all button platforms: user identity + chat origin validation

Platform Support

Platform Button Card 4 Buttons Text Fallback User ID Check Chat ID Check
Feishu ✅ Interactive card _is_interactive_operator_authorized expected_chat_id
Telegram ✅ Inline keyboard _is_callback_user_authorized chat_id/chat_type match
Discord ✅ Button components _component_check_auth + roles channel_id match
Slack ✅ Block Kit actions _is_interactive_user_authorized expected_chat_id
WeCom ✅ template_card (re-adapted to streaming) sender_id in _handle_template_card_event expected_chat_id
WeChat

Button Layout (all platforms)

All button-capable platforms render the same 4 options:

┌──────────────────────────────────────────────────┐
│ 🔐 Approval Delegation · Dangerous Command       │
│ ──────────────────────────────────────────────── │
│ Command preview:                                  │
│   rm -rf /data/...                                │
│                                                   │
│ [✅ Allow Once] [✅ Session] [✅ Always] [❌ Deny] │
└──────────────────────────────────────────────────┘

Button clicks flow through platform-specific callbacks, then synthesise /approve [session|always] or /deny text commands through the standard slash-command pipeline.

Security: Forwarded Card Prevention

All button-capable platforms implement dual validation to prevent forwarded-card abuse:

  1. User identity: the clicking user must match the configured admin (user_id / open_id)
  2. Chat origin: the click must come from the expected chat (chat_id / channel_id)

If an admin forwards an approval card to another chat, any button click from that chat is rejected with audit logging — the original admin retains the ability to approve.

Platform User Validation Chat Validation Unauthorized Behavior
Feishu operator.user_id check open_chat_id match Log warning, ignore click
Telegram from_user.id check chat_id/chat_type match "⛔ Not authorized" answer
Slack user.id check channel.id match Log warning, ignore click
Discord _component_check_auth (user + roles + admin_user_id) channel_id match "Not authorized" reply
WeCom sender.userid check chatid match Log warning, ignore click

Testing

Unit Tests

pytest tests/gateway/approval_delegation/ -v      # 21 passed
pytest tests/gateway/platforms/test_wecom_approval.py -v  # 16 passed

Full regression after the latest rebase (onto 979576d, template-method graft): 434 passed, 4
skipped
in a single tests/gateway -k "approv or delegat or exec_approval or wecom" sweep —
including the two suites above, upstream's own test_exec_approval_template.py, the
approval-boundary / approve-deny / send-timeout-ambiguity / prompt-redaction / decline-fallback
suites, and the feishu/slack/telegram/discord/teams/matrix/qqbot/whatsapp approval-button +
callback-auth suites. No upstream regressions.

E2E Verified

Scenario Result
WeChat user → Feishu admin button approval → command executes
Re-verified on the final graft head (live gateway, 617c7cc, restarted 2026-09-15 22:20): WeChat user triggers dangerous cmd → delegation module logs Registered: admin=feishu:... → session=agent:main:weixin + Redirected approval to admin → Feishu admin button click resolves → command executes; same-session follow-up skips re-approval (session tier) ✅ (2026-09-15)
WeChat user → Feishu admin button deny → command blocked
Feishu admin self-approval (no delegation loop)
WeCom 4-button approval card (allow once/session/always/deny)
Telegram admin inline keyboard approval (allow once/session/always/deny) ✅ (2026-07-08)
Cross-platform notification (approve/deny)
i18n: Chinese UI on Feishu/WeCom/Slack/Telegram cards (locale.language: zh)
Forwarded card rejection (chat_id mismatch)
Telegram forwarded card chat_id / user_id mismatch rejection ✅ (2026-07-08)

Breaking Changes

None. Existing approval behavior is unchanged when delegation.enabled is false (default).

Related Work

Three other open PRs touch adjacent parts of the approval pipeline. No overlap in implementation, but listed here for reviewer context:

PR Scope Relationship to this PR
#59181 approvals.escalate_to: platform:chat_id — statically route approval prompts to a single operator target; text-only, fail-closed on unavailable target Complementary: single-target static routing vs. this PR's multi-admin dynamic delegation with button cards. Different config keys, no file conflicts.
#60495 Scope _gateway_queues by (session_key, user_id) to prevent cross-user /approve hijack in shared gateway threads Complementary security layer: fixes core queue isolation; this PR adds is_admin_user() + chat-origin validation at the delegation boundary. Both can merge independently.
#76194 load_config_readonly() on approval guard path (perf) Unrelated — performance only.

Closes

Closes #37771

@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery area/auth Authentication, OAuth, credential pools platform/feishu Feishu / Lark adapter labels Jun 17, 2026
@alenzhong

Copy link
Copy Markdown
Author

@teknium1 Would you mind reviewing this PR? It replaces the monkey-patched approval delegation (#37771) with a native implementation. E2E tested on live Feishu + WeChat setup. Thanks! 🙏

@tonydwb tonydwb 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.

Code Review Summary

Verdict: Comment (deferred full review — large diff)

This is a substantial new feature (1,264 lines) introducing cross-platform approval delegation. The design is well-documented in the module docstring. A few observations for the author:

Looks Good

  • Thread-safe state management using threading.Lock — appropriate for a gateway service
  • TTL-based delegation entries (600s) prevent indefinite memory growth
  • reload_delegation_config() supports hot-reload after config edits
  • Clear admin_chat_key format for routing

Suggestions

  • The global _delegation_map is in-process only. In a multi-worker gateway setup, approvals delegated to one worker would not be visible to another. Consider noting this limitation in the module docstring if horizontal scaling is expected.
  • resolve_delegation uses max(sessions.values(), key=...) on the most recent entry — this could raise ValueError if sessions becomes empty after pruning. The pruning happens inside the lock, so it should be safe, but an explicit guard would be defensive.

Reviewed by Hermes Agent

@alenzhong

Copy link
Copy Markdown
Author

@tonydwb

Thanks for the review! 1. Multi-worker: Good catch — added a .. note:: to the module docstring documenting the in-process limitation
and the Redis path for horizontal scaling.
2. max() on empty: The guard is already there at line 184 (if not sessions: return None) — max() is only
reached when the dict is non-empty. Added a clarifying comment to make this explicit.

@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from 1416ee3 to 8d36d69 Compare June 17, 2026 15:35
@alenzhong
alenzhong requested a review from tonydwb June 18, 2026 03:41
@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch 5 times, most recently from c4b684e to 3843bb8 Compare June 22, 2026 08:12
@alenzhong

Copy link
Copy Markdown
Author

@teknium1 @tonydwb Ready for re-review 🙏
Updated since last review:

  • Rebased onto latest main (532b7ed) — clean merge, no conflicts
  • Fixed PR description: corrected file paths (plugins/platforms/*/adapter.py), accurate test count (14 passed)
    No code changes — same implementation as before, just rebased forward.
    Quick recap of what this PR does:
  • Native config-driven approval delegation (approvals.delegation.enabled)
  • Routes non-admin users' dangerous command approvals to designated admins across platforms
  • 4-button UI on Feishu/Telegram/Discord/Slack/WeCom with dual security validation
  • Replaces PR Feat(approval): add approval delegation mechanism v2 #37771's monkey-patch approach with direct hook integration
    Tests: pytest tests/gateway/approval_delegation/ -v → 14 passed ✅
    Let me know if anything needs attention!

@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch 4 times, most recently from 7f58570 to c2a66bc Compare July 6, 2026 08:48
@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from c2a66bc to e4a01c6 Compare July 8, 2026 08:16
@alenzhong

Copy link
Copy Markdown
Author

Hi @teknium1 @tonydwb
this PR has been updated: - Rebased to latest main — zero conflicts, all 6 overlapping files auto-merged - Telegram E2E tested and verified (inline keyboard: allow once/session/always/deny + forwarded card rejection) - PR description updated with Telegram test results This is ready for re-review. Thanks!

@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch 2 times, most recently from 5cdc0a8 to e789122 Compare July 14, 2026 01:54

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for replacing the earlier monkey-patch approach with an opt-in native flow. The delegation premise remains valid on current main: gateway/run.py:18632-18653 still sends approvals to the originating chat.

Problems

  • gateway/slash_commands.py:4355 resolves by platform/chat only. It does not verify event.source.user_id against the configured admin, so an otherwise gateway-authorized member of a configured admin group can resolve a delegated request with typed /approve or /deny.
  • gateway/run.py:18857 passes admin_user_id, but most changed adapters only accept ignored **kwargs. Telegram's existing pending state stores only session_key (plugins/platforms/telegram/adapter.py:4631-4633) and authorizes callbacks through the general gateway policy (:5361-5373), not the delegation admin identity.
  • gateway/slash_commands.py:4430-4435 schedules work on asyncio.get_running_loop() and blocks on .result(). safe_schedule_threadsafe() calls asyncio.run_coroutine_threadsafe() (agent/async_utils.py:62-63), so this blocks the loop needed to send the notification.

Suggested changes

  • Carry and enforce configured admin user ID plus chat ID for typed and button approval paths on every supported platform.
  • Await the originating-user notification directly, and add runner/slash integration tests for identity rejection and notification delivery.

Automated hermes-sweeper review.

Comment thread gateway/slash_commands.py
source = event.source
_src_plat = source.platform.value if hasattr(source.platform, "value") else str(source.platform)
_src_chat_id = str(source.chat_id or "")
_delegation = resolve_delegation(_src_plat, _src_chat_id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This lookup is keyed only by platform and chat. Before resolving, verify event.source.user_id against the configured admin for this delegation; otherwise any gateway-authorized participant in a configured admin group can type /approve and resolve the original user's command.

Comment thread gateway/run.py Outdated
command=cmd,
session_key=_approval_session_key,
description=f"[Delegation] {_user_name} ({_src_plat}): {desc} | {cmd[:50]}",
admin_user_id=_admin.get("user_id"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

admin_user_id is ignored by the newly changed Telegram/Discord/Feishu/Matrix/Teams/QQ/WhatsApp sender signatures (**kwargs). Store and enforce it in each callback state, rather than relying on the broader platform authorization policy, or the configured separation-of-duties identity is not enforced.

Comment thread gateway/slash_commands.py Outdated
log_message="Delegation user notify error",
)
if _notify_fut is not None:
_notify_fut.result(timeout=15)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This async handler schedules onto its own running loop and then blocks that loop with .result(timeout=15). The scheduled send cannot run until the wait returns. await _user_adapter.send(...) here, or use non-blocking task scheduling with failure logging.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@alenzhong

Copy link
Copy Markdown
Author

Hi @teknium1, could you please approve the workflow runs for this PR when you have a moment? CI is currently blocked on first-time-contributor workflow approval (action_required on all runs). All 24 unit tests pass locally (tests/gateway/approval_delegation/ + tests/gateway/platforms/test_wecom_approval.py), and the branch is mergeable with zero conflicts against current main.

Thanks!

@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from 8cb5f8b to e98d07a Compare August 4, 2026 15:02
@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from bd125e8 to 61d64b0 Compare August 16, 2026 10:10
@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from 5fa7bd0 to a3a4801 Compare August 22, 2026 01:41
@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from 9cb9f4c to b5ba30b Compare August 29, 2026 05:02
alenzhong added a commit to alenzhong/hermes-agent that referenced this pull request Aug 29, 2026
… (+7 platforms)

Addresses three review findings from @teknium1 on PR NousResearch#47863:

1. /approve verifies event.source.user_id via is_admin_user() before
   resolving delegated approvals — any gateway-authorized user in the
   admin's chat could previously resolve them.

2. admin_user_id was passed by gateway/run.py but silently swallowed
   by **kwargs in 6 platform adapters (Telegram, Discord, Feishu,
   Slack, Teams, Matrix). Now each adapter declares it as a formal
   parameter, stores it in the callback state, and enforces it on
   every button click / reaction — the separation-of-duties model
   requires the exact configured admin, not just any chat member.
   (WeCom already had this; unchanged.)

3. async handler _try_handle_delegated_approval blocked its own event
   loop with .result(timeout=15). Replaced with await
   asyncio.wrap_future() so the scheduled send can run.

Changes:
  +126 / -12 across 7 files
  14/14 tests pass
@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from 0c5c1ad to 3fbaa1b Compare September 13, 2026 08:37
@alenzhong

alenzhong commented Sep 13, 2026

Copy link
Copy Markdown
Author

Rebased onto latest main (979576d938) — delegation grafted onto the upstream exec-approval template method architecture

Done: branch rebased onto 979576d938 (squashed to a single commit 617c7cc674 for a clean replay over ~8k upstream commits — reviewer comment anchors on the old head will not resolve, sorry for the noise; full history preserved on my fork before the squash if needed).

Meanwhile upstream ad305bead5 refactored send_exec_approval into a BasePlatformAdapter template method (ExecApprovalPrompt dataclass + _send_exec_approval_prompt hook; the 9 adapters only render buttons), so this rebase re-grafts the delegation layer onto that new shape instead of the per-adapter signatures this PR originally added:

  • ExecApprovalPrompt gains an admin_user_id field and the base template threads it through; the per-adapter send_exec_approval signature changes are gone — delegation is now carried entirely by the prompt object.
  • Telegram / Discord / Slack / Teams / Matrix / Feishu / WeCom: each adapter just stores (chat_id, admin_user_id) alongside its existing pending-entry state in _send_exec_approval_prompt and validates the clicker in its own callback. WeCom template_card buttons preserved on the streaming architecture (group chats via cached-req_id passive reply, DMs proactive), and its override now accepts the tier flags (allow_permanent / allow_session / smart_denied) the runner passes, so delegation cards can't TypeError-degrade to plain-stream sends.
  • Button label i18n (opt-in via _EA_I18N_ACTION_LABELS, default English) moved up to the shared action builder, so zh/ja/… locales translate exactly once for all button platforms; English output is byte-identical to upstream's previous literal labels.
  • Slack / qqbot / WhatsApp: no longer carry delegation changes at all — qqbot/WhatsApp keep upstream's rendering untouched and delegation simply never posts a button card there (see the detection pivot below).
  • run_turn_runner / slash_commands: delegation check stays before the _blocking_approval_or_stale gate (admins resolve other users' sessions — the local gate would answer "nothing pending"); command redaction still happens before delegation display.

Attack-surface pivot forced by the template refactor (replaces the earlier signature-probe note): with send_exec_approval unified on the base template, every adapter's signature now accepts admin_user_id — so probing for the parameter would wrongly trust render-only adapters (qqbot/WhatsApp/relay) to enforce the admin-identity gate. Enforcement detection is now an explicit opt-in class attribute, _enforces_delegation_admin_identity, default False in the base (fail-closed), set True only by the seven adapters that actually check the clicker against the stored admin; the runner gates the button-UX branch on it and falls back to typed /approve (which always runs is_admin_user). A new platform overriding only _send_exec_approval_prompt gets no delegation by default until it declares the gate. 7 regression tests pin the invariant (including the exact scenario where a subclass inherits the button renderer but no validation); see finding #8 in the description.

Verification: 434 passed, 4 skipped locally — the full tests/gateway approval / delegation / exec-approval / WeCom surface (delegation core 21, WeCom approval 16, upstream exec-approval template suite, approval-boundary / approve-deny / send-timeout-ambiguity / prompt-redaction / decline-fallback suites, and the matrix/teams/qqbot/whatsapp/discord/telegram platform approval callbacks). 0 conflict markers, all changed files compile, deletion audit against main verified line-by-line (−28 lines are all the old signature-probe / per-adapter code this graft intentionally replaces).

Net diff vs main: 32 files, +2132/−28. Live E2E re-run on this exact head after the gateway restart: WeChat user's dangerous command → Registered: admin=feishu:… → session=agent:main:weixin + Redirected approval to admin → Feishu admin button resolves → command executes. (The session-tier skip was verified on the immediately-prior head; the only deltas since are the feishu card-header wording, one dead import, and the telegram callback restructure.) CI is queued action_required@teknium1 could you approve the workflows when convenient?

Config-driven delegation of dangerous-command approvals from non-admin
users to designated admins across platforms (e.g. WeChat user -> Feishu
admin), with 4-button cards on all capable platforms, typed /approve
text fallback, and per-platform admin-identity + chat-origin validation
on every button click.

Grafted onto upstream's exec-approval template method (ad305be) and
the plain-language message pass (23036e2):
- ExecApprovalPrompt carries admin_user_id through the base template.
- Delegation buttons route only to adapters that declare
  _enforces_delegation_admin_identity (fail-closed; the template method
  made the old signature probe unable to tell enforcing adapters from
  render-only ones).
- Approval button/card labels resolve once in the base under an opt-in
  locale flag; the English default tracks upstream's shared
  EA_HEADER_TEXT wording so card header and body never diverge.
- Telegram keeps validate-before-consume (auth via the shared
  _UNAUTHORIZED notice, admin-gate pop-after-validate) instead of
  _claim_callback_state's pop-on-claim, preserving finding NousResearch#6.

Addresses review findings NousResearch#1-NousResearch#8 (see PR description).
@alenzhong
alenzhong force-pushed the feat/approval-delegation-native branch from 4ada018 to 617c7cc Compare September 15, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/feishu Feishu / Lark adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants