Navigation

Pairing & OTP System

The Pairing & OTP system controls how new Telegram users gain access to CamelAGI bots. When an unauthorized user messages a bot, a pairing request is created. An admin must approve the request, which generates a one-time password (OTP). The user then enters the OTP in the bot chat to complete verification. This two-step flow ensures that only admin-authorized users can interact with the bot.

There is also a separate Bot Approval flow that governs whether new agent bots themselves are allowed to start polling Telegram.


Table of Contents

  1. Complete Pairing Flow
  2. OTP Generation and Verification
  3. Security Details
  4. Three Approval Methods
  5. Admin Bot Middleware
  6. Agent Bot Middleware
  7. Bot Approval Flow
  8. Notification System
  9. Storage Format
  10. REST API Endpoints
  11. CLI Usage

Complete Pairing Flow

Step-by-Step

  1. Unauthorized user messages a bot. The middleware in either admin-bot.ts or agent-bot.ts checks isUserAllowed(). The user is not found.
  2. Pairing request created. createPairingRequest() generates a 6-character alphanumeric code and persists the request to ~/.camelagi/pairing.json with status "pending".
  3. User sees confirmation. The bot replies with the pairing code (e.g., Code: H7KM3R) and tells the user to wait for admin approval.
  4. Admin is notified. For agent bots, notifyAdminOfPairing() sends an inline-keyboard message to all admin bot users with Approve/Deny buttons. For admin bots, the user simply sees their code and waits.
  5. Admin approves. Via one of three methods (REST API, CLI, admin bot inline button), approveRequest() is called. This generates a 5-digit OTP, sets status to "otp_pending", and records otpCreatedAt.
  6. User is notified to enter OTP. notifyUserOtpRequired() sends a message to the user’s chat telling them to enter the verification code.
  7. Admin receives the OTP. The OTP is displayed to the admin (in the app, CLI output, or inline button edit) so they can communicate it to the user out-of-band.
  8. User enters the OTP. On the next message from the user, the middleware detects status === "otp_pending" and checks the input against the stored OTP via verifyOtp().
  9. Verification succeeds. The user’s Telegram ID is added to allowedUsers in config.yaml, the pairing request is removed, and the user’s ID is cached in the in-memory otpVerifiedUsers Set. The user can now use the bot.

Flow Diagram

User                        Bot Middleware              Admin                     Storage
  |                              |                        |                         |
  |-- sends message ----------->|                        |                         |
  |                              |-- isUserAllowed? NO   |                         |
  |                              |-- createPairingRequest ----------------------->| pairing.json
  |<-- "Code: H7KM3R" ---------|                        |                         |  status: pending
  |                              |-- notifyAdminOfPairing ------->|               |
  |                              |                        |                         |
  |                              |           approveRequest <-----|               |
  |                              |                        |  (generates OTP) ----->| status: otp_pending
  |                              |                        |<-- OTP: 48271          |
  |<-- "Enter 5-digit code" ----|<- notifyUserOtpRequired |                        |
  |                              |                        |                         |
  |-- "48271" ----------------->|                        |                         |
  |                              |-- verifyOtp(48271) --->|                         |
  |                              |   OTP matches          |                         |
  |                              |-- addUserToAllowedList ----------------------->| config.yaml
  |                              |-- remove request ------------------------------>| pairing.json
  |<-- "Verification complete" -|                        |                         |
  |                              |                        |                         |
  |-- (future messages) ------->|-- isUserAllowed? YES   |                         |
  |<-- (normal response) -------|                        |                         |

OTP Generation and Verification

Generation

  • Generated by generateOtp() using crypto.randomInt(10000, 99999).
  • Produces a 5-digit numeric string (range 10000-99999).
  • Created at approval time (not at request time).

Verification (verifyOtp)

The function performs checks in this order:

  1. Lookup: Finds a request matching userId, agentId, and status === "otp_pending". If not found, returns { ok: false, reason: "not_found" }.
  2. Expiry check: If Date.now() - otpCreatedAt >= OTP_TTL_MS (5 minutes), the request is deleted and returns { ok: false, reason: "expired" }.
  3. Brute-force check: If otpAttempts >= MAX_OTP_ATTEMPTS (5), the request is deleted and returns { ok: false, reason: "locked" }.
  4. OTP comparison: If the trimmed input does not match the stored OTP, increments otpAttempts and returns { ok: false, reason: "wrong" }.
  5. Success: Calls addUserToAllowedList() first (atomic: user gets access even if cleanup fails), then marks the request as "completed" and removes it.

OtpResult Type

type OtpResult =
  | { ok: true; request: PairingRequest }
  | { ok: false; reason: "not_found" | "expired" | "locked" | "wrong" };

Security Details

Pairing Code

Property Value
Alphabet ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (32 chars)
Excluded O, 0, I, 1 (ambiguity avoidance)
Length 6 characters
Entropy ~30 bits (32^6 = 1,073,741,824 combinations)
Generation crypto.randomBytes, modulo-mapped to alphabet
Uniqueness Checked against all existing codes; retries up to 100 times
TTL 1 hour (60 * 60 * 1000 ms)
Case Case-insensitive lookup (.toUpperCase())

OTP

Property Value
Format 5-digit numeric (10000-99999)
Generation crypto.randomInt
TTL 5 minutes (5 * 60 * 1000 ms)
Max attempts 5 (MAX_OTP_ATTEMPTS)
Brute-force After 5 wrong attempts, request is deleted and user must start over
Input matching Exact string match after .trim()
Regex gate Middleware only calls verifyOtp if input matches /^\d{5}$/

General

  • Max pending requests: 10 (MAX_PENDING). If the cap is reached, the oldest requests are evicted.
  • Pruning: Expired requests are pruned on every read. Pending requests expire after 1 hour; OTP-pending requests expire after 5 minutes from OTP creation.
  • Atomic writes: addUserToAllowedList runs before request cleanup, so a crash between the two operations still leaves the user authorized.
  • Write verification: After adding a user to allowedUsers, the config file is re-read to verify the write succeeded.

Three Approval Methods

1. Camel App (REST API)

The macOS companion app calls the gateway REST endpoints:

POST /pairing/:code/approve   -> returns { ok, otp, userId, agentId }
POST /pairing/:code/deny      -> returns { ok }

On approval, the endpoint also calls notifyUserOtpRequired() to message the user in Telegram. The OTP is returned in the response so the app can display it to the admin.

2. CLI Command (camelagi pairing)

Interactive terminal command that lists all pending requests and prompts the admin to approve or deny each one:

$ camelagi pairing

  Request: H7KM3R
  User:    @johndoe (123456789)
  Agent:   assistant
  Status:  pending

  Approve? (y/n): y

  Approved. OTP: 48271
  Tell the user to enter this code in the bot chat.

For requests already in otp_pending status, it displays the existing OTP and notes that it is waiting for the user.

3. Admin Bot /pairing Command

The admin bot’s /pairing command lists all pending requests with inline Approve/Deny buttons. When the admin taps a button:

  • Approve: Calls approveRequest(code), edits the message to show the OTP, and sends notifyUserOtpRequired() to the user.
  • Deny: Calls denyRequest(code), edits the message to show “Denied”, and sends notifyUserOfDenial() to the user.

Additionally, when an agent bot creates a pairing request, notifyAdminOfPairing() proactively sends the request to all admin bot users with inline buttons, so admins do not need to run /pairing manually.


Admin Bot Middleware

File: src/telegram/admin-bot.ts (lines 88-182)

The admin bot always requires authorization (there is no “open access” mode). The isUserAllowed() function uses a 3-tier check:

isUserAllowed 3-Tier Check

  1. In-memory Set (otpVerifiedUsers): A Set<number> populated when a user completes OTP verification during the current process lifetime. Fastest check.
  2. In-memory config: Reads getConfig().agents[agentId].telegram.allowedUsers. This reflects the config as loaded at startup or last reload.
  3. File fallback: Calls loadConfig() to read config.yaml from disk. This catches cases where another process (e.g., the REST API or CLI) approved a user after the bot started. If the user is found here, their ID is added to otpVerifiedUsers for future fast lookups.

Middleware Flow

Message received
    |
    v
isUserAllowed(userId)?
    |-- YES --> next() (process message normally)
    |-- NO
        |
        v
    Is group chat? --> YES --> silent reject (return)
        |-- NO
        v
    hasPendingRequest(userId, agentId)?
        |
        |-- status: "otp_pending"
        |       |-- input is 5-digit number? --> verifyOtp()
        |       |       |-- ok: true --> add to otpVerifiedUsers, reply "Verification complete", next()
        |       |       |-- wrong/expired/locked --> reply error message
        |       |-- not 5-digit --> reply "Enter the 5-digit verification code"
        |
        |-- status: "pending"
        |       --> reply "Your access request is pending. Code: XXXXXX"
        |
        |-- no pending request
                --> createPairingRequest()
                --> reply "Access requested. Code: XXXXXX"

Agent Bot Middleware

File: src/telegram/agent-bot.ts (lines 66-148)

The agent bot middleware is structurally similar to the admin bot but has one key difference:

Open Access Mode

if (agent.allowedUsers.length === 0) { await next(); return; }

If allowedUsers is an empty array (or not set), the agent bot allows all users without any pairing or OTP check. This is the “open access” mode for public bots.

isUserAllowed 3-Tier Check

Same pattern as the admin bot:

  1. In-memory Set (otpVerifiedUsers)
  2. In-memory config (getAgent(0).allowedUsers)
  3. File fallback (loadConfig(), checking both telegram.allowedUsers and agents[agentId].telegram.allowedUsers)

Additional Behavior

When an agent bot creates a pairing request for an unauthorized user, it also calls notifyAdminOfPairing() to push the request to all admin bots with inline Approve/Deny buttons. The admin bot does not do this (since it is the admin interface itself).


Bot Approval Flow

File: src/telegram/bot-approval.ts

This is a separate system from user pairing. Bot approval controls whether a new agent’s Telegram bot is allowed to start polling.

Purpose

When a new agent with a botToken is configured, it does not automatically start. Instead, it enters a pending approval queue. An admin must approve the bot before it begins polling Telegram.

Data Model

interface BotApproval {
  agentId: string;
  agentName: string;
  botToken: string;
  botUsername?: string;
  model?: string;
  requestedAt: number;
}

Storage

Stored in ~/.camelagi/bot-approvals.json as a JSON array.

Operations

Function Description
requestBotApproval() Add a bot to the pending queue (replaces if already pending)
listPendingBotApprovals() List all pending bot approvals
approveBotApproval(agentId) Remove from pending and return the approval object
denyBotApproval(agentId) Remove from pending
hasPendingBotApproval(agentId) Check if an agent has a pending approval

REST Endpoints

GET    /bot-approvals                    -> list pending
POST   /bot-approvals/:agentId/approve   -> approve and start bot
POST   /bot-approvals/:agentId/deny      -> deny and discard

On approval via REST, the gateway dynamically imports startBot() from telegram.js and launches the bot immediately.


Notification System

File: src/telegram/pairing-notify.ts

Three notification functions handle communication between admin bots and user-facing agent bots:

notifyAdminOfPairing(request, config, activeBots)

  • Triggered when an agent bot creates a new pairing request.
  • Iterates over all agents with admin: true in the config.
  • For each admin agent, sends a message to every user in that agent’s allowedUsers list.
  • Message includes: user label (@username or first name or user ID), agent ID, and pairing code.
  • Includes an InlineKeyboard with Approve and Deny buttons (pairing:approve:CODE / pairing:deny:CODE).
  • Errors are silently caught (admin may not have started the bot yet).

notifyUserOtpRequired(request, activeBots)

  • Called after a request is approved (from REST endpoint or admin bot callback).
  • Sends a message to the user’s chatId via the agent bot they originally messaged.
  • Message: “Your request has been approved. Please enter the 5-digit verification code to complete access.”

notifyUserOfDenial(request, activeBots)

  • Called after a request is denied.
  • Sends “Access denied.” to the user’s chat via the agent bot.

Storage Format

~/.camelagi/pairing.json

JSON array of PairingRequest objects:

[
  {
    "code": "H7KM3R",
    "userId": 123456789,
    "username": "johndoe",
    "firstName": "John",
    "agentId": "assistant",
    "chatId": 123456789,
    "requestedAt": 1710300000000,
    "status": "pending"
  },
  {
    "code": "NW4P8T",
    "userId": 987654321,
    "username": "jane",
    "firstName": "Jane",
    "agentId": "assistant",
    "chatId": 987654321,
    "requestedAt": 1710300060000,
    "status": "otp_pending",
    "otp": "48271",
    "otpCreatedAt": 1710300120000,
    "otpAttempts": 1
  }
]

PairingRequest Interface

Field Type Description
code string 6-char alphanumeric pairing code
userId number Telegram user ID
username string? Telegram @username
firstName string? Telegram first name
agentId string Which agent the user is requesting access to
chatId number Telegram chat ID (for sending notifications)
requestedAt number Unix timestamp (ms) of request creation
status PairingStatus "pending", "otp_pending", or "completed"
otp string? 5-digit OTP (set on approval)
otpCreatedAt number? Unix timestamp (ms) of OTP generation
otpAttempts number? Count of failed OTP verification attempts

~/.camelagi/bot-approvals.json

JSON array of BotApproval objects (see Bot Approval Flow).

~/.camelagi/config.yaml

Pairing completion writes to the allowedUsers array within the relevant config section:

  • For agentId === "telegram": telegram.allowedUsers
  • For named agents: agents.<agentId>.telegram.allowedUsers

REST API Endpoints

All endpoints require authentication via the Authorization header (checked by checkAuth).

User Pairing

Method Path Description Response
GET /pairing List pending requests PairingRequest[]
POST /pairing/:code/approve Approve a pending request { ok, otp, userId, agentId } or 404
POST /pairing/:code/deny Deny a pending request { ok } or 404

Approve side effects: Generates OTP, sets status to otp_pending, sends Telegram notification to the user via notifyUserOtpRequired().

Deny side effects: Removes the request, sends “Access denied.” to the user via notifyUserOfDenial().

Bot Approvals

Method Path Description Response
GET /bot-approvals List pending bot approvals BotApproval[]
POST /bot-approvals/:agentId/approve Approve and start bot { ok, agentId, botUsername } or 404
POST /bot-approvals/:agentId/deny Deny bot approval { ok } or 404

CLI Usage

camelagi pairing

Lists all pending pairing requests interactively. For each request with status "pending", prompts:

  Request: H7KM3R
  User:    @johndoe (123456789)
  Agent:   assistant
  Status:  pending

  Approve? (y/n):
  • Entering y calls approveRequest() and displays the generated OTP.
  • Entering anything else calls denyRequest().

For requests with status "otp_pending", it displays the existing OTP and notes the system is waiting for the user:

  Request: NW4P8T
  User:    @jane (987654321)
  Agent:   assistant
  Status:  otp_pending [OTP: 48271]

  (Waiting for user to enter OTP in bot chat)

If there are no pending requests, it prints “No pending pairing requests.” and exits.


Source Files

File Purpose
src/telegram/pairing.ts Core pairing logic, OTP generation/verification, storage
src/telegram/pairing-notify.ts Telegram notification helpers
src/telegram/bot-approval.ts Bot-level approval queue
src/telegram/admin-bot.ts Admin bot middleware + /pairing command + inline callbacks
src/telegram/agent-bot.ts Agent bot middleware (with open-access mode)
src/gateway/routes.ts REST API endpoints for pairing and bot approvals
src/cli/cmd-pairing.ts CLI command for terminal-based approval