@vellumai/assistant 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +24 -0
- package/Dockerfile +1 -1
- package/README.md +16 -9
- package/package.json +1 -1
- package/src/__tests__/account-registry.test.ts +1 -0
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -0
- package/src/__tests__/asset-search-tool.test.ts +7 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +29 -0
- package/src/__tests__/channel-guardian.test.ts +2143 -1546
- package/src/__tests__/channel-retry-sweep.test.ts +169 -14
- package/src/__tests__/claude-code-tool-profiles.test.ts +1 -0
- package/src/__tests__/computer-use-tools.test.ts +1 -0
- package/src/__tests__/contacts-tools.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +1 -0
- package/src/__tests__/credential-policy-validate.test.ts +97 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +1 -0
- package/src/__tests__/credential-vault.test.ts +1 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +1 -0
- package/src/__tests__/file-edit-tool.test.ts +1 -0
- package/src/__tests__/file-read-tool.test.ts +1 -0
- package/src/__tests__/file-write-tool.test.ts +1 -0
- package/src/__tests__/followup-tools.test.ts +1 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +5 -4
- package/src/__tests__/guardian-grant-minting.test.ts +3 -0
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +4 -3
- package/src/__tests__/guardian-routing-state.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +1 -0
- package/src/__tests__/headless-browser-navigate.test.ts +1 -0
- package/src/__tests__/headless-browser-read-tools.test.ts +1 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +1 -0
- package/src/__tests__/host-file-edit-tool.test.ts +1 -0
- package/src/__tests__/host-file-read-tool.test.ts +1 -0
- package/src/__tests__/host-file-write-tool.test.ts +1 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -0
- package/src/__tests__/lifecycle-docs-guard.test.ts +207 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +1 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +8 -0
- package/src/__tests__/messaging-send-tool.test.ts +1 -0
- package/src/__tests__/playbook-execution.test.ts +1 -0
- package/src/__tests__/playbook-tools.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +4 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -0
- package/src/__tests__/schedule-tools.test.ts +1 -0
- package/src/__tests__/secret-onetime-send.test.ts +4 -0
- package/src/__tests__/secret-scanner-executor.test.ts +2 -0
- package/src/__tests__/send-notification-tool.test.ts +2 -0
- package/src/__tests__/shell-credential-ref.test.ts +1 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skill-load-tool.test.ts +1 -0
- package/src/__tests__/skill-script-runner-host.test.ts +1 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -0
- package/src/__tests__/skill-script-runner.test.ts +1 -0
- package/src/__tests__/skill-tool-factory.test.ts +1 -0
- package/src/__tests__/subagent-tools.test.ts +1 -1
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-session-integration.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/task-management-tools.test.ts +1 -0
- package/src/__tests__/task-tools.test.ts +1 -0
- package/src/__tests__/terminal-tools.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +2 -2
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +1 -0
- package/src/__tests__/tool-executor.test.ts +1 -0
- package/src/__tests__/trust-context-guards.test.ts +218 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +6 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +1 -0
- package/src/__tests__/trusted-contact-verification.test.ts +1 -0
- package/src/__tests__/view-image-tool.test.ts +1 -0
- package/src/calls/guardian-dispatch.ts +4 -4
- package/src/cli/mcp.ts +183 -3
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -4
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +17 -119
- package/src/config/system-prompt.ts +4 -2
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/computer-use-session.ts +1 -0
- package/src/daemon/session-agent-loop.ts +1 -1
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-runtime-assembly.ts +2 -2
- package/src/daemon/session-tool-setup.ts +1 -1
- package/src/mcp/client.ts +55 -6
- package/src/mcp/manager.ts +9 -0
- package/src/mcp/mcp-oauth-provider.ts +347 -0
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/delivery-status.ts +43 -0
- package/src/memory/guardian-bindings.ts +3 -3
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +108 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/schema.ts +1 -1
- package/src/runtime/actor-trust-resolver.ts +13 -4
- package/src/runtime/channel-retry-sweep.ts +31 -14
- package/src/runtime/guardian-context-resolver.ts +25 -64
- package/src/runtime/guardian-outbound-actions.ts +399 -108
- package/src/runtime/guardian-vellum-migration.ts +1 -23
- package/src/runtime/guardian-verification-templates.ts +66 -30
- package/src/runtime/local-actor-identity.ts +4 -6
- package/src/runtime/middleware/actor-token.ts +2 -8
- package/src/runtime/routes/channel-route-shared.ts +0 -1
- package/src/runtime/routes/inbound-message-handler.ts +3 -4
- package/src/runtime/tool-grant-request-helper.ts +1 -1
- package/src/tools/credentials/policy-validate.ts +22 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/types.ts +1 -1
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
* IPC handler (config-channels.ts) and the HTTP route layer (integration-routes.ts).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createHash,randomBytes } from
|
|
11
|
-
|
|
12
|
-
import { startGuardianVerificationCall } from
|
|
13
|
-
import type { ChannelId } from
|
|
14
|
-
import { getGatewayInternalBaseUrl } from
|
|
15
|
-
import { sendMessage as sendSms } from
|
|
16
|
-
import { getCredentialMetadata } from
|
|
17
|
-
import { getLogger } from
|
|
18
|
-
import { normalizePhoneNumber } from
|
|
19
|
-
import { readHttpToken } from
|
|
20
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
import { startGuardianVerificationCall } from "../calls/call-domain.js";
|
|
13
|
+
import type { ChannelId } from "../channels/types.js";
|
|
14
|
+
import { getGatewayInternalBaseUrl } from "../config/env.js";
|
|
15
|
+
import { sendMessage as sendSms } from "../messaging/providers/sms/client.js";
|
|
16
|
+
import { getCredentialMetadata } from "../tools/credentials/metadata-store.js";
|
|
17
|
+
import { getLogger } from "../util/logger.js";
|
|
18
|
+
import { normalizePhoneNumber } from "../util/phone.js";
|
|
19
|
+
import { readHttpToken } from "../util/platform.js";
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
|
|
21
21
|
import {
|
|
22
22
|
countRecentSendsToDestination,
|
|
23
23
|
createOutboundSession,
|
|
@@ -25,14 +25,15 @@ import {
|
|
|
25
25
|
getGuardianBinding,
|
|
26
26
|
updateSessionDelivery,
|
|
27
27
|
updateSessionStatus,
|
|
28
|
-
} from
|
|
28
|
+
} from "./channel-guardian-service.js";
|
|
29
29
|
import {
|
|
30
|
+
composeVerificationSlack,
|
|
30
31
|
composeVerificationSms,
|
|
31
32
|
composeVerificationTelegram,
|
|
32
33
|
GUARDIAN_VERIFY_TEMPLATE_KEYS,
|
|
33
|
-
} from
|
|
34
|
+
} from "./guardian-verification-templates.js";
|
|
34
35
|
|
|
35
|
-
const log = getLogger(
|
|
36
|
+
const log = getLogger("guardian-outbound-actions");
|
|
36
37
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
38
39
|
// Rate limit constants for outbound verification
|
|
@@ -73,7 +74,7 @@ function isTelegramChatId(destination: string): boolean {
|
|
|
73
74
|
*/
|
|
74
75
|
export function normalizeTelegramDestination(destination: string): string {
|
|
75
76
|
if (isTelegramChatId(destination)) return destination;
|
|
76
|
-
return destination.replace(/^@/,
|
|
77
|
+
return destination.replace(/^@/, "").toLowerCase();
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|
|
@@ -81,8 +82,12 @@ export function normalizeTelegramDestination(destination: string): string {
|
|
|
81
82
|
* Falls back to process.env.TELEGRAM_BOT_USERNAME.
|
|
82
83
|
*/
|
|
83
84
|
function getTelegramBotUsername(): string | undefined {
|
|
84
|
-
const meta = getCredentialMetadata(
|
|
85
|
-
if (
|
|
85
|
+
const meta = getCredentialMetadata("telegram", "bot_token");
|
|
86
|
+
if (
|
|
87
|
+
meta?.accountInfo &&
|
|
88
|
+
typeof meta.accountInfo === "string" &&
|
|
89
|
+
meta.accountInfo.trim().length > 0
|
|
90
|
+
) {
|
|
86
91
|
return meta.accountInfo.trim();
|
|
87
92
|
}
|
|
88
93
|
return process.env.TELEGRAM_BOT_USERNAME || undefined;
|
|
@@ -150,13 +155,15 @@ function deliverVerificationSms(
|
|
|
150
155
|
const gatewayUrl = getGatewayInternalBaseUrl();
|
|
151
156
|
const bearerToken = readHttpToken();
|
|
152
157
|
if (!bearerToken) {
|
|
153
|
-
log.error(
|
|
158
|
+
log.error(
|
|
159
|
+
"Cannot deliver verification SMS: no runtime HTTP token available",
|
|
160
|
+
);
|
|
154
161
|
return;
|
|
155
162
|
}
|
|
156
163
|
await sendSms(gatewayUrl, bearerToken, to, text, assistantId);
|
|
157
|
-
log.info({ to, assistantId },
|
|
164
|
+
log.info({ to, assistantId }, "Verification SMS delivered");
|
|
158
165
|
} catch (err) {
|
|
159
|
-
log.error({ err, to, assistantId },
|
|
166
|
+
log.error({ err, to, assistantId }, "Failed to deliver verification SMS");
|
|
160
167
|
}
|
|
161
168
|
})();
|
|
162
169
|
}
|
|
@@ -179,26 +186,37 @@ function deliverVerificationTelegram(
|
|
|
179
186
|
const gatewayUrl = getGatewayInternalBaseUrl();
|
|
180
187
|
const bearerToken = readHttpToken();
|
|
181
188
|
if (!bearerToken) {
|
|
182
|
-
log.error(
|
|
189
|
+
log.error(
|
|
190
|
+
"Cannot deliver verification Telegram message: no runtime HTTP token available",
|
|
191
|
+
);
|
|
183
192
|
return;
|
|
184
193
|
}
|
|
185
194
|
const url = `${gatewayUrl}/deliver/telegram`;
|
|
186
195
|
const resp = await fetch(url, {
|
|
187
|
-
method:
|
|
196
|
+
method: "POST",
|
|
188
197
|
headers: {
|
|
189
|
-
|
|
198
|
+
"Content-Type": "application/json",
|
|
190
199
|
Authorization: `Bearer ${bearerToken}`,
|
|
191
200
|
},
|
|
192
201
|
body: JSON.stringify({ chatId, text, assistantId }),
|
|
193
202
|
});
|
|
194
203
|
if (!resp.ok) {
|
|
195
|
-
const body = await resp.text().catch(() =>
|
|
196
|
-
log.error(
|
|
204
|
+
const body = await resp.text().catch(() => "<unreadable>");
|
|
205
|
+
log.error(
|
|
206
|
+
{ chatId, assistantId, status: resp.status, body },
|
|
207
|
+
"Gateway /deliver/telegram failed for verification",
|
|
208
|
+
);
|
|
197
209
|
} else {
|
|
198
|
-
log.info(
|
|
210
|
+
log.info(
|
|
211
|
+
{ chatId, assistantId },
|
|
212
|
+
"Verification Telegram message delivered",
|
|
213
|
+
);
|
|
199
214
|
}
|
|
200
215
|
} catch (err) {
|
|
201
|
-
log.error(
|
|
216
|
+
log.error(
|
|
217
|
+
{ err, chatId, assistantId },
|
|
218
|
+
"Failed to deliver verification Telegram message",
|
|
219
|
+
);
|
|
202
220
|
}
|
|
203
221
|
})();
|
|
204
222
|
}
|
|
@@ -226,12 +244,25 @@ function initiateGuardianVoiceCall(
|
|
|
226
244
|
originConversationId,
|
|
227
245
|
});
|
|
228
246
|
if (result.ok) {
|
|
229
|
-
log.info(
|
|
247
|
+
log.info(
|
|
248
|
+
{
|
|
249
|
+
phoneNumber,
|
|
250
|
+
guardianVerificationSessionId,
|
|
251
|
+
callSid: result.callSid,
|
|
252
|
+
},
|
|
253
|
+
"Guardian verification call initiated",
|
|
254
|
+
);
|
|
230
255
|
} else {
|
|
231
|
-
log.error(
|
|
256
|
+
log.error(
|
|
257
|
+
{ phoneNumber, guardianVerificationSessionId, error: result.error },
|
|
258
|
+
"Failed to initiate guardian verification call",
|
|
259
|
+
);
|
|
232
260
|
}
|
|
233
261
|
} catch (err) {
|
|
234
|
-
log.error(
|
|
262
|
+
log.error(
|
|
263
|
+
{ err, phoneNumber, guardianVerificationSessionId },
|
|
264
|
+
"Failed to initiate guardian verification call",
|
|
265
|
+
);
|
|
235
266
|
}
|
|
236
267
|
})();
|
|
237
268
|
}
|
|
@@ -240,23 +271,51 @@ function initiateGuardianVoiceCall(
|
|
|
240
271
|
// Start outbound
|
|
241
272
|
// ---------------------------------------------------------------------------
|
|
242
273
|
|
|
243
|
-
export function startOutbound(
|
|
274
|
+
export function startOutbound(
|
|
275
|
+
params: StartOutboundParams,
|
|
276
|
+
): OutboundActionResult {
|
|
244
277
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
245
278
|
const channel = params.channel;
|
|
246
279
|
const originConversationId = params.originConversationId;
|
|
247
280
|
|
|
248
|
-
if (channel ===
|
|
249
|
-
return startOutboundSms(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
if (channel === "sms") {
|
|
282
|
+
return startOutboundSms(
|
|
283
|
+
params.destination,
|
|
284
|
+
assistantId,
|
|
285
|
+
channel,
|
|
286
|
+
params.rebind,
|
|
287
|
+
originConversationId,
|
|
288
|
+
);
|
|
289
|
+
} else if (channel === "telegram") {
|
|
290
|
+
return startOutboundTelegram(
|
|
291
|
+
params.destination,
|
|
292
|
+
assistantId,
|
|
293
|
+
channel,
|
|
294
|
+
params.rebind,
|
|
295
|
+
originConversationId,
|
|
296
|
+
);
|
|
297
|
+
} else if (channel === "voice") {
|
|
298
|
+
return startOutboundVoice(
|
|
299
|
+
params.destination,
|
|
300
|
+
assistantId,
|
|
301
|
+
channel,
|
|
302
|
+
params.rebind,
|
|
303
|
+
originConversationId,
|
|
304
|
+
);
|
|
305
|
+
} else if (channel === "slack") {
|
|
306
|
+
return startOutboundSlack(
|
|
307
|
+
params.destination,
|
|
308
|
+
assistantId,
|
|
309
|
+
channel,
|
|
310
|
+
params.rebind,
|
|
311
|
+
originConversationId,
|
|
312
|
+
);
|
|
254
313
|
}
|
|
255
314
|
|
|
256
315
|
return {
|
|
257
316
|
success: false,
|
|
258
|
-
error:
|
|
259
|
-
message: `Outbound verification is only supported for SMS, Telegram, and
|
|
317
|
+
error: "unsupported_channel",
|
|
318
|
+
message: `Outbound verification is only supported for SMS, Telegram, voice, and Slack. Got: ${channel}`,
|
|
260
319
|
channel,
|
|
261
320
|
};
|
|
262
321
|
}
|
|
@@ -271,8 +330,9 @@ function startOutboundSms(
|
|
|
271
330
|
if (!rawDestination) {
|
|
272
331
|
return {
|
|
273
332
|
success: false,
|
|
274
|
-
error:
|
|
275
|
-
message:
|
|
333
|
+
error: "missing_destination",
|
|
334
|
+
message:
|
|
335
|
+
"A destination phone number is required for outbound SMS verification.",
|
|
276
336
|
channel,
|
|
277
337
|
};
|
|
278
338
|
}
|
|
@@ -281,8 +341,9 @@ function startOutboundSms(
|
|
|
281
341
|
if (!destination) {
|
|
282
342
|
return {
|
|
283
343
|
success: false,
|
|
284
|
-
error:
|
|
285
|
-
message:
|
|
344
|
+
error: "invalid_destination",
|
|
345
|
+
message:
|
|
346
|
+
"Could not parse phone number. Please enter a valid number (e.g. +15551234567, (555) 123-4567, or 555-123-4567).",
|
|
286
347
|
channel,
|
|
287
348
|
};
|
|
288
349
|
}
|
|
@@ -291,18 +352,24 @@ function startOutboundSms(
|
|
|
291
352
|
if (existingBinding && !rebind) {
|
|
292
353
|
return {
|
|
293
354
|
success: false,
|
|
294
|
-
error:
|
|
295
|
-
message:
|
|
355
|
+
error: "already_bound",
|
|
356
|
+
message:
|
|
357
|
+
"A guardian is already bound for this channel. Set rebind: true to replace.",
|
|
296
358
|
channel,
|
|
297
359
|
};
|
|
298
360
|
}
|
|
299
361
|
|
|
300
|
-
const recentSendCount = countRecentSendsToDestination(
|
|
362
|
+
const recentSendCount = countRecentSendsToDestination(
|
|
363
|
+
channel,
|
|
364
|
+
destination,
|
|
365
|
+
DESTINATION_RATE_WINDOW_MS,
|
|
366
|
+
);
|
|
301
367
|
if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
|
|
302
368
|
return {
|
|
303
369
|
success: false,
|
|
304
|
-
error:
|
|
305
|
-
message:
|
|
370
|
+
error: "rate_limited",
|
|
371
|
+
message:
|
|
372
|
+
"Too many verification attempts to this phone number. Please try again later.",
|
|
306
373
|
channel,
|
|
307
374
|
};
|
|
308
375
|
}
|
|
@@ -352,8 +419,9 @@ function startOutboundTelegram(
|
|
|
352
419
|
if (!destination) {
|
|
353
420
|
return {
|
|
354
421
|
success: false,
|
|
355
|
-
error:
|
|
356
|
-
message:
|
|
422
|
+
error: "missing_destination",
|
|
423
|
+
message:
|
|
424
|
+
"A destination (Telegram handle or chat ID) is required for outbound Telegram verification.",
|
|
357
425
|
channel,
|
|
358
426
|
};
|
|
359
427
|
}
|
|
@@ -362,20 +430,26 @@ function startOutboundTelegram(
|
|
|
362
430
|
if (existingBinding && !rebind) {
|
|
363
431
|
return {
|
|
364
432
|
success: false,
|
|
365
|
-
error:
|
|
366
|
-
message:
|
|
433
|
+
error: "already_bound",
|
|
434
|
+
message:
|
|
435
|
+
"A guardian is already bound for this channel. Set rebind: true to replace.",
|
|
367
436
|
channel,
|
|
368
437
|
};
|
|
369
438
|
}
|
|
370
439
|
|
|
371
440
|
const normalizedDestination = normalizeTelegramDestination(destination);
|
|
372
441
|
|
|
373
|
-
const recentSendCount = countRecentSendsToDestination(
|
|
442
|
+
const recentSendCount = countRecentSendsToDestination(
|
|
443
|
+
channel,
|
|
444
|
+
normalizedDestination,
|
|
445
|
+
DESTINATION_RATE_WINDOW_MS,
|
|
446
|
+
);
|
|
374
447
|
if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
|
|
375
448
|
return {
|
|
376
449
|
success: false,
|
|
377
|
-
error:
|
|
378
|
-
message:
|
|
450
|
+
error: "rate_limited",
|
|
451
|
+
message:
|
|
452
|
+
"Too many verification attempts to this destination. Please try again later.",
|
|
379
453
|
channel,
|
|
380
454
|
};
|
|
381
455
|
}
|
|
@@ -385,8 +459,9 @@ function startOutboundTelegram(
|
|
|
385
459
|
if (isNaN(chatIdNum) || chatIdNum < 0) {
|
|
386
460
|
return {
|
|
387
461
|
success: false,
|
|
388
|
-
error:
|
|
389
|
-
message:
|
|
462
|
+
error: "invalid_destination",
|
|
463
|
+
message:
|
|
464
|
+
"Telegram group chats are not supported for verification. Use a private chat ID or @handle.",
|
|
390
465
|
channel,
|
|
391
466
|
};
|
|
392
467
|
}
|
|
@@ -395,7 +470,7 @@ function startOutboundTelegram(
|
|
|
395
470
|
assistantId,
|
|
396
471
|
channel,
|
|
397
472
|
expectedChatId: destination,
|
|
398
|
-
identityBindingStatus:
|
|
473
|
+
identityBindingStatus: "bound",
|
|
399
474
|
destinationAddress: normalizedDestination,
|
|
400
475
|
});
|
|
401
476
|
|
|
@@ -411,7 +486,12 @@ function startOutboundTelegram(
|
|
|
411
486
|
const nextResendAt = now + RESEND_COOLDOWN_MS;
|
|
412
487
|
const sendCount = 1;
|
|
413
488
|
|
|
414
|
-
updateSessionDelivery(
|
|
489
|
+
updateSessionDelivery(
|
|
490
|
+
sessionResult.sessionId,
|
|
491
|
+
now,
|
|
492
|
+
sendCount,
|
|
493
|
+
nextResendAt,
|
|
494
|
+
);
|
|
415
495
|
deliverVerificationTelegram(destination, telegramBody, assistantId);
|
|
416
496
|
|
|
417
497
|
return {
|
|
@@ -431,19 +511,22 @@ function startOutboundTelegram(
|
|
|
431
511
|
if (!botUsername) {
|
|
432
512
|
return {
|
|
433
513
|
success: false,
|
|
434
|
-
error:
|
|
435
|
-
message:
|
|
514
|
+
error: "no_bot_username",
|
|
515
|
+
message:
|
|
516
|
+
"Telegram bot username is not configured. Set up the Telegram integration first.",
|
|
436
517
|
channel,
|
|
437
518
|
};
|
|
438
519
|
}
|
|
439
520
|
|
|
440
|
-
const bootstrapToken = randomBytes(16).toString(
|
|
441
|
-
const bootstrapTokenHash = createHash(
|
|
521
|
+
const bootstrapToken = randomBytes(16).toString("hex");
|
|
522
|
+
const bootstrapTokenHash = createHash("sha256")
|
|
523
|
+
.update(bootstrapToken)
|
|
524
|
+
.digest("hex");
|
|
442
525
|
|
|
443
526
|
const sessionResult = createOutboundSession({
|
|
444
527
|
assistantId,
|
|
445
528
|
channel,
|
|
446
|
-
identityBindingStatus:
|
|
529
|
+
identityBindingStatus: "pending_bootstrap",
|
|
447
530
|
destinationAddress: normalizedDestination,
|
|
448
531
|
bootstrapTokenHash,
|
|
449
532
|
});
|
|
@@ -470,8 +553,9 @@ function startOutboundVoice(
|
|
|
470
553
|
if (!rawDestination) {
|
|
471
554
|
return {
|
|
472
555
|
success: false,
|
|
473
|
-
error:
|
|
474
|
-
message:
|
|
556
|
+
error: "missing_destination",
|
|
557
|
+
message:
|
|
558
|
+
"A destination phone number is required for outbound voice verification.",
|
|
475
559
|
channel,
|
|
476
560
|
};
|
|
477
561
|
}
|
|
@@ -480,8 +564,9 @@ function startOutboundVoice(
|
|
|
480
564
|
if (!destination) {
|
|
481
565
|
return {
|
|
482
566
|
success: false,
|
|
483
|
-
error:
|
|
484
|
-
message:
|
|
567
|
+
error: "invalid_destination",
|
|
568
|
+
message:
|
|
569
|
+
"Could not parse phone number. Please enter a valid number (e.g. +15551234567, (555) 123-4567, or 555-123-4567).",
|
|
485
570
|
channel,
|
|
486
571
|
};
|
|
487
572
|
}
|
|
@@ -490,18 +575,24 @@ function startOutboundVoice(
|
|
|
490
575
|
if (existingBinding && !rebind) {
|
|
491
576
|
return {
|
|
492
577
|
success: false,
|
|
493
|
-
error:
|
|
494
|
-
message:
|
|
578
|
+
error: "already_bound",
|
|
579
|
+
message:
|
|
580
|
+
"A guardian is already bound for this channel. Set rebind: true to replace.",
|
|
495
581
|
channel,
|
|
496
582
|
};
|
|
497
583
|
}
|
|
498
584
|
|
|
499
|
-
const recentSendCount = countRecentSendsToDestination(
|
|
585
|
+
const recentSendCount = countRecentSendsToDestination(
|
|
586
|
+
channel,
|
|
587
|
+
destination,
|
|
588
|
+
DESTINATION_RATE_WINDOW_MS,
|
|
589
|
+
);
|
|
500
590
|
if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
|
|
501
591
|
return {
|
|
502
592
|
success: false,
|
|
503
|
-
error:
|
|
504
|
-
message:
|
|
593
|
+
error: "rate_limited",
|
|
594
|
+
message:
|
|
595
|
+
"Too many verification attempts to this phone number. Please try again later.",
|
|
505
596
|
channel,
|
|
506
597
|
};
|
|
507
598
|
}
|
|
@@ -520,7 +611,140 @@ function startOutboundVoice(
|
|
|
520
611
|
const sendCount = 1;
|
|
521
612
|
|
|
522
613
|
updateSessionDelivery(sessionResult.sessionId, now, sendCount, nextResendAt);
|
|
523
|
-
initiateGuardianVoiceCall(
|
|
614
|
+
initiateGuardianVoiceCall(
|
|
615
|
+
destination,
|
|
616
|
+
sessionResult.sessionId,
|
|
617
|
+
assistantId,
|
|
618
|
+
originConversationId,
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
success: true,
|
|
623
|
+
verificationSessionId: sessionResult.sessionId,
|
|
624
|
+
secret: sessionResult.secret,
|
|
625
|
+
expiresAt: sessionResult.expiresAt,
|
|
626
|
+
nextResendAt,
|
|
627
|
+
sendCount,
|
|
628
|
+
channel,
|
|
629
|
+
originConversationId,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// Slack delivery helper
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Deliver a verification Slack DM via the gateway's /deliver/slack endpoint.
|
|
639
|
+
* Fire-and-forget with error logging.
|
|
640
|
+
*/
|
|
641
|
+
function deliverVerificationSlack(
|
|
642
|
+
userId: string,
|
|
643
|
+
text: string,
|
|
644
|
+
assistantId: string,
|
|
645
|
+
): void {
|
|
646
|
+
(async () => {
|
|
647
|
+
try {
|
|
648
|
+
const gatewayUrl = getGatewayInternalBaseUrl();
|
|
649
|
+
const bearerToken = readHttpToken();
|
|
650
|
+
if (!bearerToken) {
|
|
651
|
+
log.error(
|
|
652
|
+
"Cannot deliver verification Slack DM: no runtime HTTP token available",
|
|
653
|
+
);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const url = `${gatewayUrl}/deliver/slack`;
|
|
657
|
+
const resp = await fetch(url, {
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers: {
|
|
660
|
+
"Content-Type": "application/json",
|
|
661
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
662
|
+
},
|
|
663
|
+
body: JSON.stringify({ chatId: userId, text, assistantId }),
|
|
664
|
+
});
|
|
665
|
+
if (!resp.ok) {
|
|
666
|
+
const body = await resp.text().catch(() => "<unreadable>");
|
|
667
|
+
log.error(
|
|
668
|
+
{ userId, assistantId, status: resp.status, body },
|
|
669
|
+
"Gateway /deliver/slack failed for verification",
|
|
670
|
+
);
|
|
671
|
+
} else {
|
|
672
|
+
log.info({ userId, assistantId }, "Verification Slack DM delivered");
|
|
673
|
+
}
|
|
674
|
+
} catch (err) {
|
|
675
|
+
log.error(
|
|
676
|
+
{ err, userId, assistantId },
|
|
677
|
+
"Failed to deliver verification Slack DM",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
})();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function startOutboundSlack(
|
|
684
|
+
destination: string | undefined,
|
|
685
|
+
assistantId: string,
|
|
686
|
+
channel: ChannelId,
|
|
687
|
+
rebind?: boolean,
|
|
688
|
+
originConversationId?: string,
|
|
689
|
+
): OutboundActionResult {
|
|
690
|
+
if (!destination) {
|
|
691
|
+
return {
|
|
692
|
+
success: false,
|
|
693
|
+
error: "missing_destination",
|
|
694
|
+
message: "A Slack user ID is required for outbound Slack verification.",
|
|
695
|
+
channel,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const existingBinding = getGuardianBinding(assistantId, channel);
|
|
700
|
+
if (existingBinding && !rebind) {
|
|
701
|
+
return {
|
|
702
|
+
success: false,
|
|
703
|
+
error: "already_bound",
|
|
704
|
+
message:
|
|
705
|
+
"A guardian is already bound for this channel. Set rebind: true to replace.",
|
|
706
|
+
channel,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const recentSendCount = countRecentSendsToDestination(
|
|
711
|
+
channel,
|
|
712
|
+
destination,
|
|
713
|
+
DESTINATION_RATE_WINDOW_MS,
|
|
714
|
+
);
|
|
715
|
+
if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
|
|
716
|
+
return {
|
|
717
|
+
success: false,
|
|
718
|
+
error: "rate_limited",
|
|
719
|
+
message:
|
|
720
|
+
"Too many verification attempts to this Slack user. Please try again later.",
|
|
721
|
+
channel,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const sessionResult = createOutboundSession({
|
|
726
|
+
assistantId,
|
|
727
|
+
channel,
|
|
728
|
+
expectedExternalUserId: destination,
|
|
729
|
+
expectedChatId: destination,
|
|
730
|
+
identityBindingStatus: "bound",
|
|
731
|
+
destinationAddress: destination,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
const slackBody = composeVerificationSlack(
|
|
735
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS.SLACK_CHALLENGE_REQUEST,
|
|
736
|
+
{
|
|
737
|
+
code: sessionResult.secret,
|
|
738
|
+
expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
|
|
739
|
+
},
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
const nextResendAt = now + RESEND_COOLDOWN_MS;
|
|
744
|
+
const sendCount = 1;
|
|
745
|
+
|
|
746
|
+
updateSessionDelivery(sessionResult.sessionId, now, sendCount, nextResendAt);
|
|
747
|
+
deliverVerificationSlack(destination, slackBody, assistantId);
|
|
524
748
|
|
|
525
749
|
return {
|
|
526
750
|
success: true,
|
|
@@ -538,7 +762,9 @@ function startOutboundVoice(
|
|
|
538
762
|
// Resend outbound
|
|
539
763
|
// ---------------------------------------------------------------------------
|
|
540
764
|
|
|
541
|
-
export function resendOutbound(
|
|
765
|
+
export function resendOutbound(
|
|
766
|
+
params: ResendOutboundParams,
|
|
767
|
+
): OutboundActionResult {
|
|
542
768
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
543
769
|
const channel = params.channel;
|
|
544
770
|
const originConversationId = params.originConversationId;
|
|
@@ -547,17 +773,18 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
547
773
|
if (!session) {
|
|
548
774
|
return {
|
|
549
775
|
success: false,
|
|
550
|
-
error:
|
|
551
|
-
message:
|
|
776
|
+
error: "no_active_session",
|
|
777
|
+
message: "No active outbound verification session found.",
|
|
552
778
|
channel,
|
|
553
779
|
};
|
|
554
780
|
}
|
|
555
781
|
|
|
556
|
-
if (session.identityBindingStatus ===
|
|
782
|
+
if (session.identityBindingStatus === "pending_bootstrap") {
|
|
557
783
|
return {
|
|
558
784
|
success: false,
|
|
559
|
-
error:
|
|
560
|
-
message:
|
|
785
|
+
error: "pending_bootstrap",
|
|
786
|
+
message:
|
|
787
|
+
"Cannot resend: waiting for bootstrap deep-link activation. The user must click the link first.",
|
|
561
788
|
channel,
|
|
562
789
|
};
|
|
563
790
|
}
|
|
@@ -565,8 +792,8 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
565
792
|
if (session.nextResendAt != null && Date.now() < session.nextResendAt) {
|
|
566
793
|
return {
|
|
567
794
|
success: false,
|
|
568
|
-
error:
|
|
569
|
-
message:
|
|
795
|
+
error: "rate_limited",
|
|
796
|
+
message: "Please wait before requesting another verification code.",
|
|
570
797
|
channel,
|
|
571
798
|
};
|
|
572
799
|
}
|
|
@@ -575,41 +802,52 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
575
802
|
if (currentSendCount >= MAX_SENDS_PER_SESSION) {
|
|
576
803
|
return {
|
|
577
804
|
success: false,
|
|
578
|
-
error:
|
|
579
|
-
message:
|
|
805
|
+
error: "max_sends_exceeded",
|
|
806
|
+
message: "Maximum number of verification sends reached for this session.",
|
|
580
807
|
channel,
|
|
581
808
|
};
|
|
582
809
|
}
|
|
583
810
|
|
|
584
|
-
const resendDestination =
|
|
811
|
+
const resendDestination =
|
|
812
|
+
session.destinationAddress ??
|
|
813
|
+
session.expectedPhoneE164 ??
|
|
814
|
+
session.expectedChatId;
|
|
585
815
|
if (resendDestination) {
|
|
586
|
-
const recentDestSends = countRecentSendsToDestination(
|
|
816
|
+
const recentDestSends = countRecentSendsToDestination(
|
|
817
|
+
channel,
|
|
818
|
+
resendDestination,
|
|
819
|
+
DESTINATION_RATE_WINDOW_MS,
|
|
820
|
+
);
|
|
587
821
|
if (recentDestSends >= MAX_SENDS_PER_DESTINATION_WINDOW) {
|
|
588
822
|
return {
|
|
589
823
|
success: false,
|
|
590
|
-
error:
|
|
591
|
-
message:
|
|
824
|
+
error: "rate_limited",
|
|
825
|
+
message:
|
|
826
|
+
"Too many verification attempts to this destination. Please try again later.",
|
|
592
827
|
channel,
|
|
593
828
|
};
|
|
594
829
|
}
|
|
595
830
|
}
|
|
596
831
|
|
|
597
|
-
const destination =
|
|
832
|
+
const destination =
|
|
833
|
+
session.destinationAddress ??
|
|
834
|
+
session.expectedPhoneE164 ??
|
|
835
|
+
session.expectedChatId;
|
|
598
836
|
if (!destination) {
|
|
599
837
|
return {
|
|
600
838
|
success: false,
|
|
601
|
-
error:
|
|
602
|
-
message:
|
|
839
|
+
error: "no_destination",
|
|
840
|
+
message: "Cannot resend: no destination address on the session.",
|
|
603
841
|
channel,
|
|
604
842
|
};
|
|
605
843
|
}
|
|
606
844
|
|
|
607
|
-
if (channel ===
|
|
845
|
+
if (channel === "telegram") {
|
|
608
846
|
const newSession = createOutboundSession({
|
|
609
847
|
assistantId,
|
|
610
848
|
channel,
|
|
611
849
|
expectedChatId: destination,
|
|
612
|
-
identityBindingStatus:
|
|
850
|
+
identityBindingStatus: "bound",
|
|
613
851
|
destinationAddress: destination,
|
|
614
852
|
});
|
|
615
853
|
|
|
@@ -625,7 +863,12 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
625
863
|
const newSendCount = currentSendCount + 1;
|
|
626
864
|
const nextResendAt = now + RESEND_COOLDOWN_MS;
|
|
627
865
|
|
|
628
|
-
updateSessionDelivery(
|
|
866
|
+
updateSessionDelivery(
|
|
867
|
+
newSession.sessionId,
|
|
868
|
+
now,
|
|
869
|
+
newSendCount,
|
|
870
|
+
nextResendAt,
|
|
871
|
+
);
|
|
629
872
|
deliverVerificationTelegram(destination, telegramBody, assistantId);
|
|
630
873
|
|
|
631
874
|
return {
|
|
@@ -637,7 +880,7 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
637
880
|
channel,
|
|
638
881
|
originConversationId,
|
|
639
882
|
};
|
|
640
|
-
} else if (channel ===
|
|
883
|
+
} else if (channel === "voice") {
|
|
641
884
|
const newSession = createOutboundSession({
|
|
642
885
|
assistantId,
|
|
643
886
|
channel,
|
|
@@ -651,8 +894,57 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
651
894
|
const newSendCount = currentSendCount + 1;
|
|
652
895
|
const nextResendAt = now + RESEND_COOLDOWN_MS;
|
|
653
896
|
|
|
654
|
-
updateSessionDelivery(
|
|
655
|
-
|
|
897
|
+
updateSessionDelivery(
|
|
898
|
+
newSession.sessionId,
|
|
899
|
+
now,
|
|
900
|
+
newSendCount,
|
|
901
|
+
nextResendAt,
|
|
902
|
+
);
|
|
903
|
+
initiateGuardianVoiceCall(
|
|
904
|
+
destination,
|
|
905
|
+
newSession.sessionId,
|
|
906
|
+
assistantId,
|
|
907
|
+
originConversationId,
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
success: true,
|
|
912
|
+
verificationSessionId: newSession.sessionId,
|
|
913
|
+
secret: newSession.secret,
|
|
914
|
+
nextResendAt,
|
|
915
|
+
sendCount: newSendCount,
|
|
916
|
+
channel,
|
|
917
|
+
originConversationId,
|
|
918
|
+
};
|
|
919
|
+
} else if (channel === "slack") {
|
|
920
|
+
const newSession = createOutboundSession({
|
|
921
|
+
assistantId,
|
|
922
|
+
channel,
|
|
923
|
+
expectedExternalUserId: destination,
|
|
924
|
+
expectedChatId: destination,
|
|
925
|
+
identityBindingStatus: "bound",
|
|
926
|
+
destinationAddress: destination,
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const slackBody = composeVerificationSlack(
|
|
930
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS.SLACK_RESEND,
|
|
931
|
+
{
|
|
932
|
+
code: newSession.secret,
|
|
933
|
+
expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
|
|
934
|
+
},
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
const now = Date.now();
|
|
938
|
+
const newSendCount = currentSendCount + 1;
|
|
939
|
+
const nextResendAt = now + RESEND_COOLDOWN_MS;
|
|
940
|
+
|
|
941
|
+
updateSessionDelivery(
|
|
942
|
+
newSession.sessionId,
|
|
943
|
+
now,
|
|
944
|
+
newSendCount,
|
|
945
|
+
nextResendAt,
|
|
946
|
+
);
|
|
947
|
+
deliverVerificationSlack(destination, slackBody, assistantId);
|
|
656
948
|
|
|
657
949
|
return {
|
|
658
950
|
success: true,
|
|
@@ -674,13 +966,10 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
674
966
|
destinationAddress: destination,
|
|
675
967
|
});
|
|
676
968
|
|
|
677
|
-
const smsBody = composeVerificationSms(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
|
|
682
|
-
},
|
|
683
|
-
);
|
|
969
|
+
const smsBody = composeVerificationSms(GUARDIAN_VERIFY_TEMPLATE_KEYS.RESEND, {
|
|
970
|
+
code: newSession.secret,
|
|
971
|
+
expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
|
|
972
|
+
});
|
|
684
973
|
|
|
685
974
|
const now = Date.now();
|
|
686
975
|
const newSendCount = currentSendCount + 1;
|
|
@@ -704,7 +993,9 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
|
|
|
704
993
|
// Cancel outbound
|
|
705
994
|
// ---------------------------------------------------------------------------
|
|
706
995
|
|
|
707
|
-
export function cancelOutbound(
|
|
996
|
+
export function cancelOutbound(
|
|
997
|
+
params: CancelOutboundParams,
|
|
998
|
+
): OutboundActionResult {
|
|
708
999
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
709
1000
|
const channel = params.channel;
|
|
710
1001
|
|
|
@@ -712,13 +1003,13 @@ export function cancelOutbound(params: CancelOutboundParams): OutboundActionResu
|
|
|
712
1003
|
if (!session) {
|
|
713
1004
|
return {
|
|
714
1005
|
success: false,
|
|
715
|
-
error:
|
|
716
|
-
message:
|
|
1006
|
+
error: "no_active_session",
|
|
1007
|
+
message: "No active outbound verification session found.",
|
|
717
1008
|
channel,
|
|
718
1009
|
};
|
|
719
1010
|
}
|
|
720
1011
|
|
|
721
|
-
updateSessionStatus(session.id,
|
|
1012
|
+
updateSessionStatus(session.id, "revoked");
|
|
722
1013
|
|
|
723
1014
|
return {
|
|
724
1015
|
success: true,
|