summaryrefslogtreecommitdiff
path: root/Omni/Agent/Telegram
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-18 00:09:05 -0500
committerBen Sima <ben@bensima.com>2025-12-18 00:09:05 -0500
commit133df9a099785b5eabb5ad19bcd7daa33eff9afe (patch)
tree600ea108ceca9a3aad2579a4b0227c77a68dc632 /Omni/Agent/Telegram
parentb5337a6c08b500cd3e603a48f8dfdb4772420929 (diff)
Add Telegram button confirmation for subagent spawning
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
Diffstat (limited to 'Omni/Agent/Telegram')
-rw-r--r--Omni/Agent/Telegram/Types.hs112
1 files changed, 112 insertions, 0 deletions
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" .=) </ ikbCallbackData b,
+ ("url" .=) </ ikbUrl b
+ ]
+
+instance Aeson.FromJSON InlineKeyboardButton where
+ parseJSON =
+ Aeson.withObject "InlineKeyboardButton" <| \v ->
+ (InlineKeyboardButton </ (v .: "text"))
+ <*> (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 </ (v .: "inline_keyboard")
+
+-- | Callback query from inline keyboard button press
+data CallbackQuery = CallbackQuery
+ { cqId :: Text,
+ cqFromId :: Int,
+ cqFromFirstName :: Text,
+ cqChatId :: Int,
+ cqMessageId :: Int,
+ cqData :: Text
+ }
+ deriving (Show, Eq, Generic)
+
+instance Aeson.ToJSON CallbackQuery where
+ toJSON cq =
+ Aeson.object
+ [ "id" .= cqId cq,
+ "from_id" .= cqFromId cq,
+ "from_first_name" .= cqFromFirstName cq,
+ "chat_id" .= cqChatId cq,
+ "message_id" .= cqMessageId cq,
+ "data" .= cqData cq
+ ]
+
+instance Aeson.FromJSON CallbackQuery where
+ parseJSON =
+ Aeson.withObject "CallbackQuery" <| \v ->
+ (CallbackQuery </ (v .: "id"))
+ <*> (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
+ }