feat(line): inline-button approval prompts (Allow Once / Session / Always / Deny) - #23581
Closed
wuwushi4 wants to merge 5 commits into
Closed
feat(line): inline-button approval prompts (Allow Once / Session / Always / Deny)#23581wuwushi4 wants to merge 5 commits into
wuwushi4 wants to merge 5 commits into
Conversation
…ways / Deny)
Adds `LineAdapter.send_exec_approval` so dangerous-command approval
prompts render as a Template Buttons bubble with four postback actions
— matching the Telegram / Slack / Discord / Feishu / QQBot inline-button
UX instead of forcing the user to type `/approve`, `/approve session`,
`/approve always`, or `/deny` into the chat.
## How it works
Sends two LINE messages in a single Push call:
1. A plain text bubble carrying the command preview + reason. Lives
separately because the Buttons template's `text` field caps at 160
chars — fine for the prompt, far too short for arbitrary shell
commands.
2. A Buttons template with four postback actions:
``✅ Allow Once`` / ``✅ Session`` / ``✅ Always`` / ``❌ Deny``.
Each action carries postback data
``{"action": "approve", "choice": <...>, "approval_id": <int>}``.
`_handle_postback_event` dispatches on `action`: the existing slow-LLM
`show_response` path is untouched; the new `approve` branch routes the
choice into ``tools.approval.resolve_gateway_approval(session_key,
choice)`` — the same unblock primitive every other platform's button
flow uses.
The `_approval_state` map (approval_id → session_key) mirrors
Telegram's exactly, with a `pop`-based discipline so a second tap on
the same prompt is a safe no-op (and gets an "already resolved" reply
back via the fresh postback token).
## Why Push and not Reply
Approval prompts fire mid-agent-turn, long after the inbound message's
reply token has expired (~60s window) — so we always Push. Each
prompt costs one Push call against the LINE quota (200 free/month on
the developer tier). In exchange the user gets the tappable UX, and
the per-tap *acknowledgement* messages reuse the fresh postback reply
token, so confirmations stay free.
## Why no message edit
LINE has no message-edit API, so unlike Telegram we can't strike out
the prompt and replace it with "✅ Approved by …". Instead we send a
follow-up bubble with the chosen label (`✅ Approved once`,
`✅ Approved permanently`, `❌ Denied`, etc.) using the postback reply
token.
## Tests
`tests/gateway/test_line_approval_buttons.py` — 23 tests, all passing.
Coverage:
* `build_exec_approval_button_message`: four actions, correct choice
ordering, label-length / altText / template-text caps respected,
postback data well-formed.
* `send_exec_approval`: pushes preview + buttons in one call, command
and reason show in preview bubble, approval_id increments, session
is only recorded after a successful send (failed Push leaves no
dangling state), long commands are truncated, returns
``SendResult(success=False)`` when disconnected so the gateway can
fall back to the text prompt.
* `_handle_approval_postback`: each of the four choices (`once`,
`session`, `always`, `deny`) routes to `resolve_gateway_approval`
with the right kwargs; double-tap is idempotent and surfaces a
user-visible "already resolved" notice; unknown choice or non-int
approval_id are silently ignored; confirmation reply uses the
postback reply token; the existing `show_response` postback path
still works after the dispatch-on-action refactor.
* Class-level visibility check: `getattr(type(adapter),
"send_exec_approval", None)` resolves — necessary because
`gateway/run.py:_approval_notify_sync` uses exactly that
duck-typing probe to decide whether to use buttons vs. text.
Also verified live against a real LINE Official Account on the
trycloudflare-down → ngrok tunnel path: prompt appeared, tapping
``Always`` produced
``LINE button resolved 1 approval(s) for session
agent:main:line:dm:Uxxxx (choice=always)`` and the agent
proceeded to run the gated command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two AttributeError crashes prevented inbound LINE messages from reaching the agent: - `self.create_source` does not exist on `BasePlatformAdapter`; the factory is `self.build_source` (used by IRC, Teams, etc.). - `MessageType.IMAGE` is not a member of the enum — `PHOTO` is. The ternary fallback also mis-classified audio, video, file, sticker, and location messages as images. Mirrors PR NousResearch#23867. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ch hard rules Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o-patch Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wuwushi4
force-pushed
the
feat/line-approval-buttons
branch
from
May 20, 2026 03:03
62ee339 to
a139ade
Compare
Author
|
Superseded by #29053. Rebased onto v2026.5.16 with a clean scope — the new PR contains only the LINE inline-button approval feature ( Same author, same approach, same tests. Please review #29053 instead. Thanks! |
3 tasks
19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LINE's dangerous-command approval today is text-only — the user has to type
/approve,/approve session,/approve always, or/denyinto the chat. Every other platform with a button UI (Telegram, Slack, Discord, Feishu, QQBot) renders four tappable buttons instead. This PR brings LINE up to parity using Template Buttons + postback dispatch.Surfaced by a user in Taiwan running a private LINE OA who pointed out the friction:
"在 LINE 上面如果遇到需要使用者允許的指令的話,會有點麻煩 … 之前 Telegram 聊天室窗中這會變成一個可以點按的按鈕,方便許多"(rough English: "approval prompts in LINE are clunky — Telegram makes them tappable buttons, much easier").How it works
LineAdapter.send_exec_approvalsends two LINE messages in one Push call:textfield caps at 160 chars, which is too tight for arbitrary shell commands.✅ Allow Once/✅ Session/✅ Always/❌ Deny. Each carries postback data{\"action\": \"approve\", \"choice\": <...>, \"approval_id\": <int>}._handle_postback_eventis refactored to dispatch on theactionfield — the existing slow-LLMshow_responsepath is untouched (regression test included); a newapprovebranch routes the choice intotools.approval.resolve_gateway_approval(session_key, choice)— the same unblock primitive every other platform's button flow uses.The
_approval_statemap (approval_id → session_key) mirrors Telegram's exactly, withpop-based discipline: a second tap on the same prompt is a safe no-op and gets an "already resolved" reply via the fresh postback reply token.Trade-offs documented in the docstring
✅ Approved once,✅ Approved permanently,❌ Denied, etc.).Duck-typing contract
gateway/run.py:_approval_notify_syncdoes:So adding the method on the class is enough — no base-class change, no platform-registry change, no env or config knob. Adapters that fail to push will still degrade cleanly to the existing text path.
Tests
tests/gateway/test_line_approval_buttons.py— 23 tests, all passing.once → session → always → deny), action-label / altText / template-text caps respected, postback data well-formed JSON.send_exec_approval: pushes preview + buttons in one call, command and reason show in preview bubble, approval_id increments, session is recorded only after a successful send (failed Push leaves no dangling state), long commands are truncated, returnsSendResult(success=False)when disconnected so the gateway can fall back to text._handle_approval_postback: each of the four choices routes toresolve_gateway_approvalwith the right kwargs; double-tap is idempotent and surfaces a user-visible "already resolved" notice; unknown choice or non-int approval_id are silently ignored; confirmation reply uses the postback reply token; existingshow_responsepostback path still works after the dispatch-on-action refactor.getattr(type(adapter), \"send_exec_approval\", None)is not None — guards the duck-typing probe ingateway/run.py.The existing
tests/gateway/test_line_plugin.py(73 tests) still passes unchanged.Test plan
AlwaysproducedLINE button resolved 1 approval(s) for session agent:main:line:dm:Uxxxx (choice=always)and the agent then ran the gated command end-to-end.Note on overlap with #23569
This branch is based on a clean
origin/mainand does not include thecreate_source→build_sourcetypo fix from #23569. Without that fix, inbound LINE messages don't dispatch at all, so this feature can't be triggered end-to-end on plainmain. Suggested merge order: land #23569 first (1-line fix), then this PR rebases cleanly on top.🤖 Generated with Claude Code