When I added confirmation prompts to APX, the obvious implementation was almost enough: show Yes and No, wait for a callback, then continue the tool call.
That works in a direct chat. It fails in a group.
A confirmation message can be visible to several people, but visibility is not authority. If one person asks an agent to send a quote, create a file, or perform another gated action, a nearby button must not let a bystander authorize it. The hard part was not making a button. It was preserving the identity behind the request.
My thesis is simple: a confirmation should belong to the person who initiated the action, not to the chat where the button happens to appear.
The tempting shortcut
A Telegram confirmation is asynchronous. An agent is midway through a tool call, decides that an action needs approval, sends an inline keyboard, and waits. Later, Telegram delivers a callback query when somebody taps a button.
The shortcut is to store only a random confirmation ID and resolve it when any matching callback arrives. It is concise, and it looks correct in a one-person conversation. In a group, it silently changes the question from “did the initiator approve?” to “did anyone with access to the chat press Yes?”
Those are not equivalent questions.
The second question is especially dangerous because the interface gives it an air of legitimacy. Everyone sees the same card. Everyone can reach the same button. Without an explicit actor check, a colleague trying to help, a curious participant, or the wrong person tapping by mistake can turn someone else’s request into an authorized action.
Bind the pending request to an actor
APX keeps pending confirmations in memory. Each entry has a correlation ID, a promise waiting for a boolean answer, an expiry timer, and optionally a guardActorId. Telegram passes the ID of the person who started the turn as that guard.
When a callback arrives, APX checks the presser before it resolves anything:
confirmation belongs to initiator 111
bystander 999 taps Yes
→ reject callback
→ keep confirmation pending
initiator 111 taps Yes
→ resolve confirmation
→ tool may execute
The detail I care about most is the middle line: rejection does not consume the request. The bystander gets a “Not your confirmation” response, but the original person can still decide. A failed authorization attempt is not the same thing as a decline.
That distinction keeps the state machine small and truthful. There are three outcomes that matter: approved by the right actor, declined by the right actor, or still waiting. “Someone else pressed a button” belongs outside that set.
Make silence safe
A confirmation cannot wait forever. An agent loop suspended forever is not safer; it is merely harder to reason about. APX gives Telegram confirmations a 60-second lifetime. If nobody answers, the promise resolves to false. The tool does not run.
This is deliberate. A missing answer should never become permission through a retry, a timeout handler, or a later reconnect. Defaulting to cancellation makes the system’s behavior legible: no explicit approval, no action.
The same rule matters after a restart. Pending confirmations live only in the daemon process because the agent turn that created them lives there too. If APX restarts, there is no valid suspended turn to resume. A later tap on the old button is shown as expired rather than treated as a fresh approval.
I prefer that small inconvenience to reviving an action after its surrounding context has disappeared. A quote request might have been edited, a file operation might no longer be relevant, and the person who asked might have changed their mind. An expired button says exactly what happened: this decision window is gone.
Confirmation still is not execution
There is another boundary worth keeping explicit. A successful confirmation means the gate is open; it does not prove that the tool succeeded. The handler still has to run, and it can still fail. Conversely, a declined or expired confirmation means the handler never ran.
APX tests that separation. Its security-risk tests assert that a declined confirmation leaves the tool’s execution count at zero. The actor-guard test asserts that a different user cannot resolve a guarded confirmation, while the original initiator still can. These are boring tests, but that is exactly the point: a safety promise should be precise enough to test without interpretation.
What I learned building it
I used to think of confirmation as a UI feature. It is really a small authorization protocol crossing time and a chat transport. It needs an identity, a one-time correlation key, an expiration rule, and a clear answer for stale state.
The practical checklist I now use is short:
- Bind approval to an actor when the transport is shared.
- Reject unauthorized responses without destroying the real request.
- Treat no answer as cancellation.
- Treat post-restart buttons as expired.
- Never report a request as pending when the current surface cannot actually ask.
That last point matters outside Telegram too. Some APX surfaces have a confirmation adapter; some execution contexts do not. Where APX cannot present a real confirmation, it refuses the action and says that no request was sent. Pretending otherwise would turn a missing capability into a false promise.
Agents become useful when they can act. They stay trustworthy when every boundary around that action remains explicit. A group chat is a shared place, not shared authority—and a confirmation button should make that true in code, not just in the copy around it.
Top comments (0)