Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions assistant/src/config/vellum-skills/telegram-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,45 @@ If the webhook secret changes (e.g., secret rotation), the gateway's credential

### Step 4: Register Bot Commands

Send the `telegram_config` IPC message with `action: "set_commands"` to register the `/new` command. The daemon handles token retrieval from secure storage internally — you do not need to retrieve it yourself.
Send the `telegram_config` IPC message with `action: "set_commands"` to register the `/new` and `/guardian-verify` commands:

### Step 5: Validate Routing Configuration
```json
{
"type": "telegram_config",
"action": "set_commands",
"commands": [
{ "command": "new", "description": "Start a new conversation" },
{ "command": "guardian_verify", "description": "Verify your guardian identity" }
]
}
```

The daemon handles token retrieval from secure storage internally — you do not need to retrieve it yourself.

### Step 5: Verify Guardian Identity

Now link the user's Telegram account as the trusted guardian for this bot. Tell the user: "Now let's verify your guardian identity. This links your Telegram account as the trusted guardian for this bot."

1. Send the `guardian_verification` IPC message with `action: "create_challenge"` to generate a verification challenge:

```json
{
"type": "guardian_verification",
"action": "create_challenge"
}
```

2. The daemon returns a `guardian_verification_response` with `success: true`, `secret`, and `instruction`. Display the instruction to the user. It will look like: "Send `/guardian-verify <secret>` to your bot from your Telegram account within 10 minutes."

3. Wait for the user to confirm they have sent the command. The verification happens automatically when the bot receives the `/guardian-verify` message — the channel inbound handler validates the token and creates the guardian binding.

4. If the user confirms success: "Guardian verified! Your Telegram account is now the trusted guardian for this bot."

5. If the user reports failure or the challenge times out (10 minutes): "The verification code may have expired. Let's generate a new one." Then repeat from substep 1.

**Note:** Guardian verification is optional but recommended. If the user declines or wants to skip, proceed to Step 6 without blocking.

### Step 6: Validate Routing Configuration

Verify that the gateway routing is configured to deliver inbound messages to the assistant:

Expand All @@ -57,12 +93,13 @@ Verify that the gateway routing is configured to deliver inbound messages to the

If routing is misconfigured, inbound Telegram messages will be rejected and the gateway will send a visible notice to the chat explaining the issue (rate-limited to once per 5 minutes per chat).

### Step 6: Report Success
### Step 7: Report Success

Summarize what was done:
- Bot verified and credentials stored securely via daemon
- Webhook registration: handled automatically by the gateway
- Bot commands registered: /new
- Bot commands registered: /new, /guardian_verify
- Guardian identity verified (if completed)
- Routing configuration validated

The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. In single-assistant mode, routing is automatically configured — no manual environment variable configuration or webhook registration is needed. If the webhook secret changes later, the gateway's credential watcher will automatically re-register the webhook. If the ingress URL changes (e.g., tunnel restart), the assistant daemon triggers an immediate internal reconcile so the webhook re-registers automatically without a gateway restart.
Expand Down Expand Up @@ -94,4 +131,5 @@ The following steps still require **manual** action:
|------|---------|
| Bot token from @BotFather | User must create a bot and provide the token via secure prompt |
| Bot command registration | Registered via the setup skill (Step 4 above) |
| Guardian verification | User sends `/guardian-verify <secret>` to the bot (Step 5 above) |
| Multi-assistant routing | Requires manual `GATEWAY_ASSISTANT_ROUTING_JSON` configuration |
41 changes: 41 additions & 0 deletions assistant/src/daemon/handlers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import type {
VercelApiConfigRequest,
TwitterIntegrationConfigRequest,
TelegramConfigRequest,
GuardianVerificationRequest,
ToolPermissionSimulateRequest,
} from '../ipc-protocol.js';
import { createVerificationChallenge } from '../../runtime/channel-guardian-service.js';
import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
import { MODEL_TO_PROVIDER } from '../session-slash.js';

Expand Down Expand Up @@ -1089,6 +1091,44 @@ export async function handleTelegramConfig(
}
}

export function handleGuardianVerification(
msg: GuardianVerificationRequest,
socket: net.Socket,
ctx: HandlerContext,
): void {
try {
if (msg.action !== 'create_challenge') {
ctx.send(socket, {
type: 'guardian_verification_response',
success: false,
error: `Unknown action: ${String(msg.action)}`,
});
return;
}

// In single-assistant mode, 'self' is the canonical assistant ID used
// by channel routes when validating challenges on the inbound path.
const assistantId = 'self';
const channel = msg.channel ?? 'telegram';
const result = createVerificationChallenge(assistantId, channel, msg.sessionId);

ctx.send(socket, {
type: 'guardian_verification_response',
success: true,
secret: result.secret,
instruction: result.instruction,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
log.error({ err }, 'Failed to handle guardian verification');
ctx.send(socket, {
type: 'guardian_verification_response',
success: false,
error: message,
});
}
}

export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): void {
const vars: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
Expand Down Expand Up @@ -1220,6 +1260,7 @@ export const configHandlers = defineHandlers({
vercel_api_config: handleVercelApiConfig,
twitter_integration_config: handleTwitterIntegrationConfig,
telegram_config: handleTelegramConfig,
guardian_verification: handleGuardianVerification,
env_vars_request: (_msg, socket, ctx) => handleEnvVarsRequest(socket, ctx),
tool_permission_simulate: handleToolPermissionSimulate,
tool_names_list: (_msg, socket, ctx) => handleToolNamesList(socket, ctx),
Expand Down
17 changes: 17 additions & 0 deletions assistant/src/daemon/ipc-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,21 @@ export interface TelegramConfigResponse {
error?: string;
}

export interface GuardianVerificationRequest {
type: 'guardian_verification';
action: 'create_challenge';
channel?: string; // Defaults to 'telegram'
sessionId?: string;
}

export interface GuardianVerificationResponse {
type: 'guardian_verification_response';
success: boolean;
secret?: string;
instruction?: string;
error?: string;
}

export interface TwitterIntegrationConfigResponse {
type: 'twitter_integration_config_response';
success: boolean;
Expand Down Expand Up @@ -1032,6 +1047,7 @@ export type ClientMessage =
| VercelApiConfigRequest
| TwitterIntegrationConfigRequest
| TelegramConfigRequest
| GuardianVerificationRequest
Comment thread
noanflaherty marked this conversation as resolved.
| TwitterAuthStartRequest
| TwitterAuthStatusRequest
| SessionsClearRequest
Expand Down Expand Up @@ -2396,6 +2412,7 @@ export type ServerMessage =
| VercelApiConfigResponse
| TwitterIntegrationConfigResponse
| TelegramConfigResponse
| GuardianVerificationResponse
| TwitterAuthResult
| TwitterAuthStatusResponse
| OpenUrl
Expand Down
41 changes: 41 additions & 0 deletions assistant/src/runtime/routes/channel-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IngressBlockedError } from '../../util/errors.js';
import { getLogger } from '../../util/logger.js';
import { deliverChannelReply, deliverApprovalPrompt } from '../gateway-client.js';
import { parseApprovalDecision } from '../channel-approval-parser.js';
import { validateAndConsumeChallenge } from '../channel-guardian-service.js';
import {
getChannelApprovalPrompt,
buildApprovalUIMetadata,
Expand Down Expand Up @@ -243,6 +244,46 @@ export async function handleChannelInbound(

const replyCallbackUrl = body.replyCallbackUrl;

// ── Guardian verification command intercept ──
// Handled before normal message processing so it never enters the agent loop.
if (
!result.duplicate &&
trimmedContent.startsWith('/guardian-verify ') &&
Comment thread
noanflaherty marked this conversation as resolved.
replyCallbackUrl &&
body.senderExternalUserId
) {
const token = trimmedContent.slice('/guardian-verify '.length).trim();
Comment thread
noanflaherty marked this conversation as resolved.
if (token.length > 0) {
const verifyResult = validateAndConsumeChallenge(
'self',
sourceChannel,
token,
body.senderExternalUserId,
externalChatId,
);

const replyText = verifyResult.success
? 'Guardian verified successfully. Your identity is now linked to this bot.'
: 'Verification failed. The code may be invalid or expired.';

try {
await deliverChannelReply(replyCallbackUrl, {
chatId: externalChatId,
text: replyText,
}, bearerToken);
} catch (err) {
log.error({ err, externalChatId }, 'Failed to deliver guardian verification reply');
}

return Response.json({
accepted: true,
duplicate: false,
eventId: result.eventId,
guardianVerification: verifyResult.success ? 'verified' : 'failed',
});
}
}

// ── Approval interception (gated behind feature flag) ──
if (
isChannelApprovalsEnabled() &&
Expand Down
Loading