From 133df9a099785b5eabb5ad19bcd7daa33eff9afe Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 18 Dec 2025 00:09:05 -0500 Subject: Add Telegram button confirmation for subagent spawning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security improvement: subagents now require explicit user confirmation via Telegram inline buttons, preventing the agent from bypassing approval. Changes: - Add InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery types - Add parseCallbackQuery for handling button presses - Add sendMessageWithKeyboard and answerCallbackQuery API functions - Add PendingSpawn registry for tracking unconfirmed spawn requests - Add spawnSubagentToolWithApproval that sends approval buttons - Add handleCallbackQuery to process approve/reject button clicks - Add approveAndSpawnSubagent and rejectPendingSpawn functions Flow: 1. Agent calls spawn_subagent → creates pending request 2. User receives message with ✅ Approve / ❌ Reject buttons 3. Button click (outside agent loop) spawns or cancels 4. Pending requests expire after 10 minutes --- Omni/Agent/Telegram/Types.hs | 112 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) (limited to 'Omni/Agent/Telegram') diff --git a/Omni/Agent/Telegram/Types.hs b/Omni/Agent/Telegram/Types.hs index 7a91df3..e6d8957 100644 --- a/Omni/Agent/Telegram/Types.hs +++ b/Omni/Agent/Telegram/Types.hs @@ -22,9 +22,15 @@ module Omni.Agent.Telegram.Types BotAddedToGroup (..), ChatType (..), + -- * Inline Keyboard + InlineKeyboardMarkup (..), + InlineKeyboardButton (..), + CallbackQuery (..), + -- * Parsing parseUpdate, parseBotAddedToGroup, + parseCallbackQuery, parseDocument, parseLargestPhoto, parsePhotoSize, @@ -652,3 +658,109 @@ shouldRespondInGroup botUsername msg mention = "@" <> Text.toLower botUsername isMentioned = mention `Text.isInfixOf` msgText isReplyToBot = isJust (tmReplyTo msg) + +-- | Inline keyboard button +data InlineKeyboardButton = InlineKeyboardButton + { ikbText :: Text, + ikbCallbackData :: Maybe Text, + ikbUrl :: Maybe Text + } + deriving (Show, Eq, Generic) + +instance Aeson.ToJSON InlineKeyboardButton where + toJSON b = + Aeson.object + <| catMaybes + [ Just ("text" .= ikbText b), + ("callback_data" .=) + (InlineKeyboardButton (v .:? "callback_data") + <*> (v .:? "url") + +-- | Inline keyboard markup (grid of buttons) +newtype InlineKeyboardMarkup = InlineKeyboardMarkup + { ikmInlineKeyboard :: [[InlineKeyboardButton]] + } + deriving (Show, Eq, Generic) + +instance Aeson.ToJSON InlineKeyboardMarkup where + toJSON m = Aeson.object ["inline_keyboard" .= ikmInlineKeyboard m] + +instance Aeson.FromJSON InlineKeyboardMarkup where + parseJSON = + Aeson.withObject "InlineKeyboardMarkup" <| \v -> + InlineKeyboardMarkup + (CallbackQuery (v .: "from_id") + <*> (v .: "from_first_name") + <*> (v .: "chat_id") + <*> (v .: "message_id") + <*> (v .: "data") + +-- | Parse a callback query from a raw Telegram update +parseCallbackQuery :: Aeson.Value -> Maybe CallbackQuery +parseCallbackQuery val = do + Aeson.Object obj <- pure val + Aeson.Object cqObj <- KeyMap.lookup "callback_query" obj + cqId <- case KeyMap.lookup "id" cqObj of + Just (Aeson.String s) -> Just s + _ -> Nothing + Aeson.Object fromObj <- KeyMap.lookup "from" cqObj + fromId <- case KeyMap.lookup "id" fromObj of + Just (Aeson.Number n) -> Just (round n) + _ -> Nothing + fromFirstName <- case KeyMap.lookup "first_name" fromObj of + Just (Aeson.String s) -> Just s + _ -> Nothing + Aeson.Object msgObj <- KeyMap.lookup "message" cqObj + Aeson.Object chatObj <- KeyMap.lookup "chat" msgObj + chatId <- case KeyMap.lookup "id" chatObj of + Just (Aeson.Number n) -> Just (round n) + _ -> Nothing + messageId <- case KeyMap.lookup "message_id" msgObj of + Just (Aeson.Number n) -> Just (round n) + _ -> Nothing + callbackData <- case KeyMap.lookup "data" cqObj of + Just (Aeson.String s) -> Just s + _ -> Nothing + pure + CallbackQuery + { cqId = cqId, + cqFromId = fromId, + cqFromFirstName = fromFirstName, + cqChatId = chatId, + cqMessageId = messageId, + cqData = callbackData + } -- cgit v1.2.3