From e8e2af705dbcbbe57dde707128d7efac9ff70991 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:50:07 -0700 Subject: [PATCH] feat(cli): improve Telegram connector with --allowed-user-id flag (#11256) * feat(cli): add Telegram allowed user id flag * fix(cli): tighten connector authorization hooks --- apps/cli/src/connectors/adapters/telegram.md | 10 ++- .../src/connectors/adapters/telegram.test.ts | 66 +++++++++++++++++++ apps/cli/src/connectors/adapters/telegram.ts | 37 ++++++++++- apps/cli/src/wizards/connect/index.ts | 4 +- .../cli/src/wizards/connect/platforms.test.ts | 24 +++++++ apps/cli/src/wizards/connect/platforms.ts | 11 ++-- apps/cline-hub/src/server/connectors.ts | 5 +- docs/cli/connectors.mdx | 20 ++++-- docs/cli/samples/supply-chain-alerts.mdx | 6 +- 9 files changed, 159 insertions(+), 24 deletions(-) diff --git a/apps/cli/src/connectors/adapters/telegram.md b/apps/cli/src/connectors/adapters/telegram.md index 21571a3e73..fcaa2850bb 100644 --- a/apps/cli/src/connectors/adapters/telegram.md +++ b/apps/cli/src/connectors/adapters/telegram.md @@ -76,7 +76,15 @@ cline connect telegram -k "$TELEGRAM_BOT_TOKEN" --no-tools When the connector starts with `--no-tools`, chat commands such as `/tools on` and `/yolo on` cannot re-enable tools for that connector run. -For participant restrictions, run the interactive connector wizard with `cline connect` or pass a `--hook-command` that returns `{"action":"deny"}` for unauthorized `session.authorize` events. If no hook is configured, messages are allowed. +For participant restrictions, run the interactive connector wizard with `cline connect`. The Telegram wizard asks whether to restrict access, points you to `@userinfobot`, and configures your numeric Telegram user ID. + +You can also pass the user ID directly: + +```bash +cline connect telegram -k "$TELEGRAM_BOT_TOKEN" --allowed-user-id 12345 +``` + +You can also pass a manual `--hook-command` that returns `{"action":"deny"}` for unauthorized `session.authorize` events. If neither access option is configured, messages are allowed. ## Message Delivery diff --git a/apps/cli/src/connectors/adapters/telegram.test.ts b/apps/cli/src/connectors/adapters/telegram.test.ts index a418fac2e4..5f49101366 100644 --- a/apps/cli/src/connectors/adapters/telegram.test.ts +++ b/apps/cli/src/connectors/adapters/telegram.test.ts @@ -62,6 +62,72 @@ describe("telegramConnector", () => { expect(options.enableTools).toBe(true); }); + it("builds an authorization hook from --allowed-user-id", () => { + const options = parseTelegramArgs([ + "--bot-token", + "123:test", + "--cwd", + "/tmp/work", + "--allowed-user-id", + "1201547643", + ]); + + expect(options.hookCommand).toBe( + `jq -r ".payload.actor.participantKey" | grep -qx "telegram:id:1201547643" && echo '{"action":"allow"}' || echo '{"action":"deny","message":"unauthorized","reason":"not_on_allowlist"}'`, + ); + }); + + it("rejects unsafe --allowed-user-id values", () => { + expect(() => + parseTelegramArgs([ + "--bot-token", + "123:test", + "--cwd", + "/tmp/work", + "--allowed-user-id", + "123; rm -rf /", + ]), + ).toThrow("digits only"); + }); + + it("rejects mixing --allowed-user-id with --hook-command", () => { + expect(() => + parseTelegramArgs([ + "--bot-token", + "123:test", + "--cwd", + "/tmp/work", + "--allowed-user-id", + "1201547643", + "--hook-command", + "echo noop", + ]), + ).toThrow("either --allowed-user-id or --hook-command"); + }); + + it("rejects mixing --allowed-user-id with the hook command env var", () => { + const originalHookCommand = process.env.CLINE_CONNECT_HOOK_COMMAND; + process.env.CLINE_CONNECT_HOOK_COMMAND = "echo noop"; + try { + expect(() => + parseTelegramArgs([ + "--bot-token", + "123:test", + "--cwd", + "/tmp/work", + "--allowed-user-id", + "1201547643", + ]), + ).toThrow("either --allowed-user-id or --hook-command"); + } finally { + if (originalHookCommand === undefined) { + delete process.env.CLINE_CONNECT_HOOK_COMMAND; + } else { + process.env.CLINE_CONNECT_HOOK_COMMAND = originalHookCommand; + } + } + }); + it("does not require the bot username", () => { const options = parseTelegramArgs([ "--bot-token", diff --git a/apps/cli/src/connectors/adapters/telegram.ts b/apps/cli/src/connectors/adapters/telegram.ts index 5bce0e7b96..0ea0203278 100644 --- a/apps/cli/src/connectors/adapters/telegram.ts +++ b/apps/cli/src/connectors/adapters/telegram.ts @@ -89,6 +89,20 @@ function readTelegramBotId(botToken: string): string | undefined { return /^\d+$/.test(botId) ? botId : undefined; } +function normalizeAllowedTelegramUserId(value: string): string { + const userId = value.trim(); + if (!/^\d+$/.test(userId)) { + throw new Error( + "connect telegram --allowed-user-id must contain digits only", + ); + } + return userId; +} + +function buildTelegramAllowedUserHookCommand(userId: string): string { + return `jq -r ".payload.actor.participantKey" | grep -qx "telegram:id:${userId}" && echo '{"action":"allow"}' || echo '{"action":"deny","message":"unauthorized","reason":"not_on_allowlist"}'`; +} + function describeTelegramGetMeFailure( response: Response, body: string, @@ -418,6 +432,10 @@ class TelegramConnector extends ConnectorBase< .option("--mode ", "Agent mode", "act") .option("-i, --interactive", "Keep connector in foreground") .option("--no-tools", "Disable tools for Telegram sessions") + .option( + "--allowed-user-id ", + "Only allow this Telegram user ID to use the bot", + ) .option( "--hook-command ", "Run a shell command for connector events", @@ -434,6 +452,7 @@ class TelegramConnector extends ConnectorBase< "Notes:", " - Without -i, the connector is launched in the background.", " - Tools are enabled by default for Telegram sessions.", + " - Use --allowed-user-id or `cline connect` to restrict Telegram access.", " - Bot username is discovered from the Telegram bot token when omitted.", " - Provider/model default to the CLI's last-used provider settings.", ].join("\n"), @@ -454,6 +473,7 @@ class TelegramConnector extends ConnectorBase< tools?: boolean; rpcAddress?: string; hookCommand?: string; + allowedUserId?: string; }>(); const botUsername = normalizeTelegramBotUsername(opts.botUsername ?? "") || @@ -465,6 +485,15 @@ class TelegramConnector extends ConnectorBase< if (!botToken) { throw new Error("connect telegram requires -k/--bot-token "); } + const hookCommand = + opts.hookCommand?.trim() || + process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(); + const allowedUserId = opts.allowedUserId?.trim(); + if (hookCommand && allowedUserId) { + throw new Error( + "connect telegram accepts either --allowed-user-id or --hook-command, not both", + ); + } return { botToken, ...(botUsername ? { botUsername } : {}), @@ -480,9 +509,11 @@ class TelegramConnector extends ConnectorBase< opts.rpcAddress?.trim() || process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(), - hookCommand: - opts.hookCommand?.trim() || - process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(), + hookCommand: allowedUserId + ? buildTelegramAllowedUserHookCommand( + normalizeAllowedTelegramUserId(allowedUserId), + ) + : hookCommand, }; } diff --git a/apps/cli/src/wizards/connect/index.ts b/apps/cli/src/wizards/connect/index.ts index b40a466862..bdce32665c 100644 --- a/apps/cli/src/wizards/connect/index.ts +++ b/apps/cli/src/wizards/connect/index.ts @@ -121,9 +121,9 @@ async function collectSecurity( values[field.key] = (value as string).trim(); } - const hookCmd = security.buildHookCommand(values); + const args = security.buildArgs(values); p.log.success("Access restriction enabled"); - return ["--hook-command", hookCmd]; + return args; } export async function runConnectWizard(): Promise { diff --git a/apps/cli/src/wizards/connect/platforms.test.ts b/apps/cli/src/wizards/connect/platforms.test.ts index d148fa6913..a674a2279f 100644 --- a/apps/cli/src/wizards/connect/platforms.test.ts +++ b/apps/cli/src/wizards/connect/platforms.test.ts @@ -31,6 +31,30 @@ describe("connect wizard platform security fields", () => { expect(slackUser?.validate?.("U01$(bad)")).toContain("Slack member"); }); + it("uses the Telegram allowed user ID flag for wizard security", () => { + const telegram = PLATFORMS.find((platform) => platform.id === "telegram"); + + const args = telegram?.security?.buildArgs({ + userId: "123456", + }); + + expect(args).toEqual(["--allowed-user-id", "123456"]); + }); + + it("builds an exact-match Slack authorization hook", () => { + const slack = PLATFORMS.find((platform) => platform.id === "slack"); + + const args = slack?.security?.buildArgs({ + teamId: "T01ABC123", + userId: "U01ABC123", + }); + + expect(args).toEqual([ + "--hook-command", + `jq -r ".payload.actor.participantKey" | grep -qx "slack:team:T01ABC123:user:U01ABC123" && echo '{"action":"allow"}' || echo '{"action":"deny"}'`, + ]); + }); + it("asks Slack users for mode-specific setup fields", () => { const slack = PLATFORMS.find((platform) => platform.id === "slack"); const fields = slack?.fields ?? []; diff --git a/apps/cli/src/wizards/connect/platforms.ts b/apps/cli/src/wizards/connect/platforms.ts index 2556a21f88..3f4ddb17d3 100644 --- a/apps/cli/src/wizards/connect/platforms.ts +++ b/apps/cli/src/wizards/connect/platforms.ts @@ -36,7 +36,7 @@ export interface SecurityFieldDef { export interface SecurityDef { prompt: string; fields: SecurityFieldDef[]; - buildHookCommand: (values: Record) => string; + buildArgs: (values: Record) => string[]; } export function shouldIncludeField( @@ -111,8 +111,7 @@ export const PLATFORMS: PlatformDef[] = [ validate: validateTelegramUserId, }, ], - buildHookCommand: ({ userId }) => - `jq -r ".payload.actor.participantKey" | grep -q "telegram:id:${userId}" && echo '{"action":"allow"}' || echo '{"action":"deny"}'`, + buildArgs: ({ userId }) => ["--allowed-user-id", userId ?? ""], }, }, { @@ -186,8 +185,10 @@ export const PLATFORMS: PlatformDef[] = [ validate: validateSlackUserId, }, ], - buildHookCommand: ({ teamId, userId }) => - `jq -r ".payload.actor.participantKey" | grep -q "slack:team:${teamId}:user:${userId}" && echo '{"action":"allow"}' || echo '{"action":"deny"}'`, + buildArgs: ({ teamId, userId }) => [ + "--hook-command", + `jq -r ".payload.actor.participantKey" | grep -qx "slack:team:${teamId}:user:${userId}" && echo '{"action":"allow"}' || echo '{"action":"deny"}'`, + ], }, }, { diff --git a/apps/cline-hub/src/server/connectors.ts b/apps/cline-hub/src/server/connectors.ts index cdbe34ad82..2bda290e9b 100644 --- a/apps/cline-hub/src/server/connectors.ts +++ b/apps/cline-hub/src/server/connectors.ts @@ -142,10 +142,7 @@ function buildConnectorStartArgs(args?: Record): string[] { if (validationError) throw new Error(validationError); hookValues[field.key] = value; } - cliArgs.push( - "--hook-command", - platform.security.buildHookCommand(hookValues), - ); + cliArgs.push(...platform.security.buildArgs(hookValues)); } return cliArgs; } diff --git a/docs/cli/connectors.mdx b/docs/cli/connectors.mdx index eb6c746765..317781d89d 100644 --- a/docs/cli/connectors.mdx +++ b/docs/cli/connectors.mdx @@ -54,24 +54,32 @@ cline connect ### Security -By default, anyone who finds your bot can message it and it will execute tasks on your machine. Lock it down with the `--hook-command` flag. +By default, anyone who finds your bot can message it and it will execute tasks on your machine. The `cline connect` wizard asks whether to restrict Telegram access and can configure this for you. - Message [@userinfobot](https://t.me/userinfobot) on Telegram. It replies with your user ID immediately. + Message [@userinfobot](https://t.me/userinfobot) on Telegram. It replies with your numeric user ID immediately. - - Replace `12345` with your actual Telegram user ID: + + ```bash + cline connect + ``` + + Choose Telegram, enter the bot token, answer yes to access restriction, then enter your user ID. + + + + Replace `12345` with your Telegram user ID: ```bash cline connect telegram -k \ - --hook-command 'jq -r ".payload.actor.participantKey" | grep -q "telegram:id:12345" && echo "{\"action\":\"allow\"}" || echo "{\"action\":\"deny\",\"message\":\"unauthorized\"}"' + --allowed-user-id 12345 ``` -The `--hook-command` receives each incoming message with sender info via stdin. Your script returns `{"action": "allow"}` or `{"action": "deny", "message": "reason"}`. Without `--hook-command`, everything is auto-approved. +Use `--hook-command` only when you need custom access logic. The hook receives each incoming message with sender info via stdin. Your script returns `{"action": "allow"}` or `{"action": "deny", "message": "reason"}`. Without `--allowed-user-id` or `--hook-command`, everything is auto-approved, so restrict Telegram bots that can reach a running Cline instance. ## Slack diff --git a/docs/cli/samples/supply-chain-alerts.mdx b/docs/cli/samples/supply-chain-alerts.mdx index f564e77a48..e5cfc2fd3b 100644 --- a/docs/cli/samples/supply-chain-alerts.mdx +++ b/docs/cli/samples/supply-chain-alerts.mdx @@ -129,14 +129,14 @@ Scan profiles control where Bumblebee looks. `baseline` checks standard global t -By default, anyone who finds your bot can message it and it will run tasks on your machine. Lock it down. Message [@userinfobot](https://t.me/userinfobot) to get your Telegram user ID, then restart the connector with an access-control hook: +By default, anyone who finds your bot can message it and it will run tasks on your machine. Lock it down before leaving the connector running. The `cline connect` wizard can guide you through Telegram user ID setup, or you can message [@userinfobot](https://t.me/userinfobot) and restart the connector with your allowed user ID: ```bash cline connect telegram -k "" --cwd ~/tools/bumblebee \ - --hook-command 'jq -r ".payload.actor.participantKey" | grep -q "telegram:id:12345" && echo "{\"action\":\"allow\"}" || echo "{\"action\":\"deny\",\"message\":\"unauthorized\"}"' + --allowed-user-id 12345 ``` -Replace `12345` with your user ID. +Replace `12345` with your Telegram user ID. ## 5. Schedule the scan