@vellumai/assistant 0.4.30 → 0.4.31
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/Dockerfile +14 -8
- package/README.md +2 -2
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +1 -4
- package/package.json +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +0 -4
- package/src/__tests__/config-schema.test.ts +0 -9
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
- package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/relay-server.ts +5 -0
- package/src/config/bundled-skills/contacts/SKILL.md +7 -18
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
- package/src/config/bundled-tool-registry.ts +0 -5
- package/src/config/memory-schema.ts +0 -10
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +36 -62
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +9 -4
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/sessions.ts +2 -1
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/session-agent-loop.ts +1 -45
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/db-init.ts +4 -0
- package/src/memory/job-handlers/conflict.ts +0 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +1 -18
- package/src/messaging/index.ts +0 -1
- package/src/messaging/types.ts +0 -38
- package/src/runtime/guardian-action-service.ts +3 -2
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/http-server.ts +12 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/contact-routes.ts +308 -29
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/global-search-routes.ts +2 -2
- package/src/runtime/routes/guardian-action-routes.ts +1 -1
- package/src/runtime/routes/guardian-approval-interception.ts +2 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/migration-routes.ts +17 -17
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -36
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
|
@@ -140,6 +140,7 @@ import {
|
|
|
140
140
|
handleMergeContacts,
|
|
141
141
|
handleUpdateContactChannel,
|
|
142
142
|
handleUpsertContact,
|
|
143
|
+
handleVerifyContactChannel,
|
|
143
144
|
} from "./routes/contact-routes.js";
|
|
144
145
|
import { handleListConversationAttention } from "./routes/conversation-attention-routes.js";
|
|
145
146
|
// Route handlers — grouped by domain
|
|
@@ -1128,6 +1129,17 @@ export class RuntimeHttpServer {
|
|
|
1128
1129
|
handler: async ({ req, params, authContext }) =>
|
|
1129
1130
|
handleUpdateContactChannel(req, params.id, authContext.assistantId),
|
|
1130
1131
|
},
|
|
1132
|
+
{
|
|
1133
|
+
endpoint: "contacts/:contactId/channels/:channelId/verify",
|
|
1134
|
+
method: "POST",
|
|
1135
|
+
policyKey: "contacts/channels",
|
|
1136
|
+
handler: async ({ params, authContext }) =>
|
|
1137
|
+
handleVerifyContactChannel(
|
|
1138
|
+
params.contactId,
|
|
1139
|
+
params.channelId,
|
|
1140
|
+
authContext.assistantId,
|
|
1141
|
+
),
|
|
1142
|
+
},
|
|
1131
1143
|
|
|
1132
1144
|
// ------------------------------------------------------------------
|
|
1133
1145
|
// Contacts invites — must precede contacts/:id to avoid shadowing
|
|
@@ -297,7 +297,8 @@ async function handleCallbackDecision(params: {
|
|
|
297
297
|
const result = applyGuardianDecision({
|
|
298
298
|
approval: guardianApproval,
|
|
299
299
|
decision: callbackDecision,
|
|
300
|
-
|
|
300
|
+
actorPrincipalId: undefined, // Callback path — principal not available at this layer
|
|
301
|
+
actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
|
|
301
302
|
actorChannel: sourceChannel,
|
|
302
303
|
});
|
|
303
304
|
|
|
@@ -491,7 +492,8 @@ async function handleConversationalDecision(params: {
|
|
|
491
492
|
const result = applyGuardianDecision({
|
|
492
493
|
approval: targetApproval,
|
|
493
494
|
decision: engineDecision,
|
|
494
|
-
|
|
495
|
+
actorPrincipalId: undefined, // Callback path — principal not available at this layer
|
|
496
|
+
actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
|
|
495
497
|
actorChannel: sourceChannel,
|
|
496
498
|
});
|
|
497
499
|
|
|
@@ -675,7 +677,8 @@ async function handleLegacyDecision(params: {
|
|
|
675
677
|
const result = applyGuardianDecision({
|
|
676
678
|
approval: targetLegacyApproval,
|
|
677
679
|
decision: legacyGuardianDecision,
|
|
678
|
-
|
|
680
|
+
actorPrincipalId: undefined, // Callback path — principal not available at this layer
|
|
681
|
+
actorExternalUserId: actorExternalId, // Channel-native ID (Telegram user ID, phone, etc.)
|
|
679
682
|
actorChannel: sourceChannel,
|
|
680
683
|
});
|
|
681
684
|
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* GET /v1/contacts/:id — get a contact by ID
|
|
7
7
|
* POST /v1/contacts/merge — merge two contacts
|
|
8
8
|
* PATCH /v1/contacts/channels/:id — update a contact channel's status/policy
|
|
9
|
+
* POST /v1/contacts/:contactId/channels/:channelId/verify — initiate trusted contact verification
|
|
9
10
|
*/
|
|
10
11
|
|
|
12
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
import type { ChannelId } from "../../channels/types.js";
|
|
11
15
|
import {
|
|
12
16
|
getAssistantContactMetadata,
|
|
13
17
|
getChannelById,
|
|
@@ -20,6 +24,7 @@ import {
|
|
|
20
24
|
upsertContact,
|
|
21
25
|
validateSpeciesMetadata,
|
|
22
26
|
} from "../../contacts/contact-store.js";
|
|
27
|
+
import type { ContactChannel } from "../../contacts/types.js";
|
|
23
28
|
import type {
|
|
24
29
|
AssistantSpecies,
|
|
25
30
|
ChannelPolicy,
|
|
@@ -27,6 +32,27 @@ import type {
|
|
|
27
32
|
ContactRole,
|
|
28
33
|
ContactType,
|
|
29
34
|
} from "../../contacts/types.js";
|
|
35
|
+
import { getCredentialMetadata } from "../../tools/credentials/metadata-store.js";
|
|
36
|
+
import { normalizePhoneNumber } from "../../util/phone.js";
|
|
37
|
+
import {
|
|
38
|
+
countRecentSendsToDestination,
|
|
39
|
+
createOutboundSession,
|
|
40
|
+
updateSessionDelivery,
|
|
41
|
+
} from "../channel-guardian-service.js";
|
|
42
|
+
import {
|
|
43
|
+
deliverVerificationSlack,
|
|
44
|
+
deliverVerificationSms,
|
|
45
|
+
deliverVerificationTelegram,
|
|
46
|
+
DESTINATION_RATE_WINDOW_MS,
|
|
47
|
+
MAX_SENDS_PER_DESTINATION_WINDOW,
|
|
48
|
+
normalizeTelegramDestination,
|
|
49
|
+
} from "../guardian-outbound-actions.js";
|
|
50
|
+
import {
|
|
51
|
+
composeVerificationSlack,
|
|
52
|
+
composeVerificationSms,
|
|
53
|
+
composeVerificationTelegram,
|
|
54
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS,
|
|
55
|
+
} from "../guardian-verification-templates.js";
|
|
30
56
|
import { httpError } from "../http-errors.js";
|
|
31
57
|
|
|
32
58
|
const VALID_CONTACT_TYPES: readonly ContactType[] = ["human", "assistant"];
|
|
@@ -38,7 +64,7 @@ const VALID_ASSISTANT_SPECIES: readonly AssistantSpecies[] = [
|
|
|
38
64
|
/**
|
|
39
65
|
* GET /v1/contacts?limit=50&role=guardian&contactType=human
|
|
40
66
|
*
|
|
41
|
-
* Also supports search query params: query, channelAddress, channelType
|
|
67
|
+
* Also supports search query params: query, channelAddress, channelType.
|
|
42
68
|
* When any search param is provided, delegates to searchContacts() instead of listContacts().
|
|
43
69
|
*/
|
|
44
70
|
export function handleListContacts(url: URL, assistantId: string): Response {
|
|
@@ -48,8 +74,6 @@ export function handleListContacts(url: URL, assistantId: string): Response {
|
|
|
48
74
|
const query = url.searchParams.get("query");
|
|
49
75
|
const channelAddress = url.searchParams.get("channelAddress");
|
|
50
76
|
const channelType = url.searchParams.get("channelType");
|
|
51
|
-
const relationship = url.searchParams.get("relationship");
|
|
52
|
-
|
|
53
77
|
if (contactTypeParam && !isContactType(contactTypeParam)) {
|
|
54
78
|
return httpError(
|
|
55
79
|
"BAD_REQUEST",
|
|
@@ -58,8 +82,7 @@ export function handleListContacts(url: URL, assistantId: string): Response {
|
|
|
58
82
|
);
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
const hasSearchParams =
|
|
62
|
-
query || channelAddress || channelType || relationship;
|
|
85
|
+
const hasSearchParams = query || channelAddress || channelType;
|
|
63
86
|
|
|
64
87
|
const contactType = contactTypeParam
|
|
65
88
|
? (contactTypeParam as ContactType)
|
|
@@ -71,7 +94,6 @@ export function handleListContacts(url: URL, assistantId: string): Response {
|
|
|
71
94
|
query: query ?? undefined,
|
|
72
95
|
channelAddress: channelAddress ?? undefined,
|
|
73
96
|
channelType: channelType ?? undefined,
|
|
74
|
-
relationship: relationship ?? undefined,
|
|
75
97
|
role: role ?? undefined,
|
|
76
98
|
contactType,
|
|
77
99
|
limit,
|
|
@@ -162,7 +184,7 @@ function isChannelPolicy(value: string): value is ChannelPolicy {
|
|
|
162
184
|
}
|
|
163
185
|
|
|
164
186
|
/**
|
|
165
|
-
* POST /v1/contacts { displayName, id?,
|
|
187
|
+
* POST /v1/contacts { displayName, id?, notes?, contactType?, assistantMetadata?, ... }
|
|
166
188
|
*/
|
|
167
189
|
export async function handleUpsertContact(
|
|
168
190
|
req: Request,
|
|
@@ -171,10 +193,7 @@ export async function handleUpsertContact(
|
|
|
171
193
|
const body = (await req.json()) as {
|
|
172
194
|
id?: string;
|
|
173
195
|
displayName?: string;
|
|
174
|
-
|
|
175
|
-
importance?: number;
|
|
176
|
-
responseExpectation?: string;
|
|
177
|
-
preferredTone?: string;
|
|
196
|
+
notes?: string;
|
|
178
197
|
role?: string;
|
|
179
198
|
contactType?: string;
|
|
180
199
|
assistantMetadata?: {
|
|
@@ -204,20 +223,6 @@ export async function handleUpsertContact(
|
|
|
204
223
|
);
|
|
205
224
|
}
|
|
206
225
|
|
|
207
|
-
if (
|
|
208
|
-
body.importance !== undefined &&
|
|
209
|
-
(typeof body.importance !== "number" ||
|
|
210
|
-
Number.isNaN(body.importance) ||
|
|
211
|
-
body.importance < 0 ||
|
|
212
|
-
body.importance > 1)
|
|
213
|
-
) {
|
|
214
|
-
return httpError(
|
|
215
|
-
"BAD_REQUEST",
|
|
216
|
-
"importance must be a number between 0 and 1",
|
|
217
|
-
400,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
226
|
if (body.contactType !== undefined && !isContactType(body.contactType)) {
|
|
222
227
|
return httpError(
|
|
223
228
|
"BAD_REQUEST",
|
|
@@ -297,10 +302,7 @@ export async function handleUpsertContact(
|
|
|
297
302
|
const contact = upsertContact({
|
|
298
303
|
id: body.id,
|
|
299
304
|
displayName: body.displayName.trim(),
|
|
300
|
-
|
|
301
|
-
importance: body.importance,
|
|
302
|
-
responseExpectation: body.responseExpectation,
|
|
303
|
-
preferredTone: body.preferredTone,
|
|
305
|
+
notes: body.notes,
|
|
304
306
|
role: body.role as ContactRole | undefined,
|
|
305
307
|
contactType: body.contactType as ContactType | undefined,
|
|
306
308
|
assistantId,
|
|
@@ -404,3 +406,280 @@ export async function handleUpdateContactChannel(
|
|
|
404
406
|
const parentContact = getContact(updated.contactId, assistantId);
|
|
405
407
|
return Response.json({ ok: true, contact: parentContact ?? undefined });
|
|
406
408
|
}
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Channel verification
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
/** Session TTL in seconds (matches challenge TTL of 10 minutes). */
|
|
415
|
+
const SESSION_TTL_SECONDS = 600;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Map a contact channel type to the verification ChannelId used by the
|
|
419
|
+
* guardian service. Returns null for unsupported channel types.
|
|
420
|
+
*/
|
|
421
|
+
function toVerificationChannel(channelType: string): ChannelId | null {
|
|
422
|
+
switch (channelType) {
|
|
423
|
+
case "phone":
|
|
424
|
+
return "sms";
|
|
425
|
+
case "telegram":
|
|
426
|
+
return "telegram";
|
|
427
|
+
case "slack":
|
|
428
|
+
return "slack";
|
|
429
|
+
default:
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get the Telegram bot username from credential metadata.
|
|
436
|
+
* Falls back to process.env.TELEGRAM_BOT_USERNAME.
|
|
437
|
+
*/
|
|
438
|
+
function getTelegramBotUsername(): string | undefined {
|
|
439
|
+
const meta = getCredentialMetadata("telegram", "bot_token");
|
|
440
|
+
if (
|
|
441
|
+
meta?.accountInfo &&
|
|
442
|
+
typeof meta.accountInfo === "string" &&
|
|
443
|
+
meta.accountInfo.trim().length > 0
|
|
444
|
+
) {
|
|
445
|
+
return meta.accountInfo.trim();
|
|
446
|
+
}
|
|
447
|
+
return process.env.TELEGRAM_BOT_USERNAME || undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* POST /v1/contacts/:contactId/channels/:channelId/verify
|
|
452
|
+
*
|
|
453
|
+
* Initiate trusted contact verification for a specific channel. Sends a
|
|
454
|
+
* verification code via SMS, Telegram, Slack, or voice and returns session
|
|
455
|
+
* info so the client can track the verification flow.
|
|
456
|
+
*/
|
|
457
|
+
export async function handleVerifyContactChannel(
|
|
458
|
+
contactId: string,
|
|
459
|
+
channelId: string,
|
|
460
|
+
assistantId: string,
|
|
461
|
+
): Promise<Response> {
|
|
462
|
+
const contact = getContact(contactId, assistantId);
|
|
463
|
+
if (!contact) {
|
|
464
|
+
return httpError("NOT_FOUND", `Contact "${contactId}" not found`, 404);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const channel: ContactChannel | undefined = contact.channels.find(
|
|
468
|
+
(ch) => ch.id === channelId,
|
|
469
|
+
);
|
|
470
|
+
if (!channel) {
|
|
471
|
+
return httpError("NOT_FOUND", `Channel "${channelId}" not found`, 404);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Already verified — no need to re-verify
|
|
475
|
+
if (channel.status === "active" && channel.verifiedAt != null) {
|
|
476
|
+
return httpError("CONFLICT", "Channel is already verified", 409);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const verificationChannel = toVerificationChannel(channel.type);
|
|
480
|
+
if (!verificationChannel) {
|
|
481
|
+
return httpError(
|
|
482
|
+
"BAD_REQUEST",
|
|
483
|
+
`Verification is not supported for channel type "${channel.type}"`,
|
|
484
|
+
400,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const destination = channel.address;
|
|
489
|
+
if (!destination) {
|
|
490
|
+
return httpError(
|
|
491
|
+
"BAD_REQUEST",
|
|
492
|
+
"Channel has no address to send verification to",
|
|
493
|
+
400,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Normalize Telegram destinations so rate-limit lookups are consistent with
|
|
498
|
+
// guardian-outbound-actions (strips leading '@', lowercases handles).
|
|
499
|
+
const effectiveDestination =
|
|
500
|
+
verificationChannel === "telegram"
|
|
501
|
+
? normalizeTelegramDestination(destination)
|
|
502
|
+
: destination;
|
|
503
|
+
|
|
504
|
+
// Rate limit check
|
|
505
|
+
const recentSendCount = countRecentSendsToDestination(
|
|
506
|
+
verificationChannel,
|
|
507
|
+
effectiveDestination,
|
|
508
|
+
DESTINATION_RATE_WINDOW_MS,
|
|
509
|
+
);
|
|
510
|
+
if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
|
|
511
|
+
return httpError(
|
|
512
|
+
"RATE_LIMITED",
|
|
513
|
+
"Too many verification attempts to this destination. Please try again later.",
|
|
514
|
+
429,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// --- SMS verification ---
|
|
519
|
+
if (verificationChannel === "sms") {
|
|
520
|
+
const phoneE164 = normalizePhoneNumber(destination);
|
|
521
|
+
if (!phoneE164) {
|
|
522
|
+
return httpError(
|
|
523
|
+
"BAD_REQUEST",
|
|
524
|
+
"Channel address is not a valid phone number",
|
|
525
|
+
400,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const sessionResult = createOutboundSession({
|
|
530
|
+
assistantId,
|
|
531
|
+
channel: verificationChannel,
|
|
532
|
+
expectedPhoneE164: phoneE164,
|
|
533
|
+
expectedExternalUserId: channel.externalUserId ?? undefined,
|
|
534
|
+
destinationAddress: phoneE164,
|
|
535
|
+
verificationPurpose: "trusted_contact",
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const smsBody = composeVerificationSms(
|
|
539
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS.CHALLENGE_REQUEST,
|
|
540
|
+
{
|
|
541
|
+
code: sessionResult.secret,
|
|
542
|
+
expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
|
|
543
|
+
},
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
const sendCount = 1;
|
|
548
|
+
updateSessionDelivery(sessionResult.sessionId, now, sendCount, null);
|
|
549
|
+
deliverVerificationSms(phoneE164, smsBody, assistantId);
|
|
550
|
+
|
|
551
|
+
return Response.json({
|
|
552
|
+
ok: true,
|
|
553
|
+
verificationSessionId: sessionResult.sessionId,
|
|
554
|
+
expiresAt: sessionResult.expiresAt,
|
|
555
|
+
sendCount,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// --- Telegram verification ---
|
|
560
|
+
if (verificationChannel === "telegram") {
|
|
561
|
+
// Telegram with known chat ID: identity is already bound
|
|
562
|
+
if (channel.externalChatId) {
|
|
563
|
+
const sessionResult = createOutboundSession({
|
|
564
|
+
assistantId,
|
|
565
|
+
channel: verificationChannel,
|
|
566
|
+
expectedChatId: channel.externalChatId,
|
|
567
|
+
expectedExternalUserId: channel.externalUserId ?? undefined,
|
|
568
|
+
identityBindingStatus: "bound",
|
|
569
|
+
destinationAddress: effectiveDestination,
|
|
570
|
+
verificationPurpose: "trusted_contact",
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const telegramBody = composeVerificationTelegram(
|
|
574
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS.TELEGRAM_CHALLENGE_REQUEST,
|
|
575
|
+
{
|
|
576
|
+
code: sessionResult.secret,
|
|
577
|
+
expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
|
|
578
|
+
},
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const now = Date.now();
|
|
582
|
+
const sendCount = 1;
|
|
583
|
+
updateSessionDelivery(sessionResult.sessionId, now, sendCount, null);
|
|
584
|
+
deliverVerificationTelegram(
|
|
585
|
+
channel.externalChatId,
|
|
586
|
+
telegramBody,
|
|
587
|
+
assistantId,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
return Response.json({
|
|
591
|
+
ok: true,
|
|
592
|
+
verificationSessionId: sessionResult.sessionId,
|
|
593
|
+
expiresAt: sessionResult.expiresAt,
|
|
594
|
+
sendCount,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Telegram handle only (no chat ID): bootstrap flow
|
|
599
|
+
const botUsername = getTelegramBotUsername();
|
|
600
|
+
if (!botUsername) {
|
|
601
|
+
return httpError(
|
|
602
|
+
"BAD_REQUEST",
|
|
603
|
+
"Telegram bot username is not configured. Set up the Telegram integration first.",
|
|
604
|
+
400,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const bootstrapToken = randomBytes(16).toString("hex");
|
|
609
|
+
const bootstrapTokenHash = createHash("sha256")
|
|
610
|
+
.update(bootstrapToken)
|
|
611
|
+
.digest("hex");
|
|
612
|
+
|
|
613
|
+
const sessionResult = createOutboundSession({
|
|
614
|
+
assistantId,
|
|
615
|
+
channel: verificationChannel,
|
|
616
|
+
identityBindingStatus: "pending_bootstrap",
|
|
617
|
+
destinationAddress: effectiveDestination,
|
|
618
|
+
bootstrapTokenHash,
|
|
619
|
+
verificationPurpose: "trusted_contact",
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const telegramBootstrapUrl = `https://t.me/${botUsername}?start=gv_${bootstrapToken}`;
|
|
623
|
+
|
|
624
|
+
return Response.json({
|
|
625
|
+
ok: true,
|
|
626
|
+
verificationSessionId: sessionResult.sessionId,
|
|
627
|
+
expiresAt: sessionResult.expiresAt,
|
|
628
|
+
sendCount: 0,
|
|
629
|
+
telegramBootstrapUrl,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// --- Slack verification ---
|
|
634
|
+
if (verificationChannel === "slack") {
|
|
635
|
+
const slackUserId = channel.externalUserId ?? destination;
|
|
636
|
+
|
|
637
|
+
// Only claim identity is bound when we have at least one platform identifier
|
|
638
|
+
const hasIdentityBinding = Boolean(
|
|
639
|
+
channel.externalUserId || channel.externalChatId,
|
|
640
|
+
);
|
|
641
|
+
if (!hasIdentityBinding) {
|
|
642
|
+
return httpError(
|
|
643
|
+
"BAD_REQUEST",
|
|
644
|
+
"Slack verification requires an externalUserId or externalChatId for identity binding",
|
|
645
|
+
400,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const sessionResult = createOutboundSession({
|
|
650
|
+
assistantId,
|
|
651
|
+
channel: verificationChannel,
|
|
652
|
+
expectedExternalUserId: channel.externalUserId ?? undefined,
|
|
653
|
+
expectedChatId: channel.externalChatId ?? undefined,
|
|
654
|
+
identityBindingStatus: "bound",
|
|
655
|
+
destinationAddress: slackUserId,
|
|
656
|
+
verificationPurpose: "trusted_contact",
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const slackBody = composeVerificationSlack(
|
|
660
|
+
GUARDIAN_VERIFY_TEMPLATE_KEYS.SLACK_CHALLENGE_REQUEST,
|
|
661
|
+
{
|
|
662
|
+
code: sessionResult.secret,
|
|
663
|
+
expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const now = Date.now();
|
|
668
|
+
const sendCount = 1;
|
|
669
|
+
updateSessionDelivery(sessionResult.sessionId, now, sendCount, null);
|
|
670
|
+
deliverVerificationSlack(slackUserId, slackBody, assistantId);
|
|
671
|
+
|
|
672
|
+
return Response.json({
|
|
673
|
+
ok: true,
|
|
674
|
+
verificationSessionId: sessionResult.sessionId,
|
|
675
|
+
expiresAt: sessionResult.expiresAt,
|
|
676
|
+
sendCount,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return httpError(
|
|
681
|
+
"BAD_REQUEST",
|
|
682
|
+
`Verification is not supported for channel type "${channel.type}"`,
|
|
683
|
+
400,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
@@ -124,7 +124,8 @@ async function tryConsumeCanonicalGuardianReply(params: {
|
|
|
124
124
|
messageText: trimmedContent,
|
|
125
125
|
channel: sourceChannel,
|
|
126
126
|
actor: {
|
|
127
|
-
|
|
127
|
+
actorPrincipalId: verifiedActorPrincipalId,
|
|
128
|
+
actorExternalUserId: verifiedActorExternalUserId,
|
|
128
129
|
channel: sourceChannel,
|
|
129
130
|
guardianPrincipalId: verifiedActorPrincipalId,
|
|
130
131
|
},
|
|
@@ -58,7 +58,7 @@ interface GlobalSearchSchedule {
|
|
|
58
58
|
interface GlobalSearchContact {
|
|
59
59
|
id: string;
|
|
60
60
|
displayName: string;
|
|
61
|
-
|
|
61
|
+
notes: string | null;
|
|
62
62
|
lastInteraction: number | null;
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -256,7 +256,7 @@ export async function handleGlobalSearch(url: URL): Promise<Response> {
|
|
|
256
256
|
results.contacts = contactResults.map((c) => ({
|
|
257
257
|
id: c.id,
|
|
258
258
|
displayName: c.displayName,
|
|
259
|
-
|
|
259
|
+
notes: c.notes,
|
|
260
260
|
lastInteraction: c.lastInteraction,
|
|
261
261
|
}));
|
|
262
262
|
}
|
|
@@ -96,7 +96,7 @@ export async function handleGuardianActionDecision(
|
|
|
96
96
|
conversationId,
|
|
97
97
|
channel: "vellum",
|
|
98
98
|
actorContext: {
|
|
99
|
-
|
|
99
|
+
actorPrincipalId: authContext.actorPrincipalId ?? undefined,
|
|
100
100
|
guardianPrincipalId: authContext.actorPrincipalId ?? undefined,
|
|
101
101
|
},
|
|
102
102
|
});
|
|
@@ -232,7 +232,8 @@ export async function handleApprovalInterception(
|
|
|
232
232
|
const cancelApplyResult = applyGuardianDecision({
|
|
233
233
|
approval: guardianApprovalForRequest,
|
|
234
234
|
decision: rejectDecision,
|
|
235
|
-
|
|
235
|
+
actorPrincipalId: undefined, // Interception path — principal not available
|
|
236
|
+
actorExternalUserId: actorExternalId, // Channel-native ID
|
|
236
237
|
actorChannel: sourceChannel,
|
|
237
238
|
});
|
|
238
239
|
if (cancelApplyResult.applied) {
|
|
@@ -13,6 +13,7 @@ import { createHash } from "node:crypto";
|
|
|
13
13
|
|
|
14
14
|
import { v4 as uuid } from "uuid";
|
|
15
15
|
|
|
16
|
+
import { resolveUserReference } from "../../config/user-reference.js";
|
|
16
17
|
import { findGuardianForChannel } from "../../contacts/contact-store.js";
|
|
17
18
|
import { createGuardianBinding } from "../../contacts/contacts-write.js";
|
|
18
19
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -54,6 +55,7 @@ function ensureGuardianPrincipal(assistantId: string): {
|
|
|
54
55
|
|
|
55
56
|
// Mint a new principal ID for the vellum channel
|
|
56
57
|
const guardianPrincipalId = `vellum-principal-${uuid()}`;
|
|
58
|
+
const guardianDisplayName = resolveUserReference();
|
|
57
59
|
|
|
58
60
|
createGuardianBinding({
|
|
59
61
|
assistantId,
|
|
@@ -62,7 +64,10 @@ function ensureGuardianPrincipal(assistantId: string): {
|
|
|
62
64
|
guardianDeliveryChatId: "local",
|
|
63
65
|
guardianPrincipalId,
|
|
64
66
|
verifiedVia: "bootstrap",
|
|
65
|
-
metadataJson: JSON.stringify({
|
|
67
|
+
metadataJson: JSON.stringify({
|
|
68
|
+
bootstrappedAt: Date.now(),
|
|
69
|
+
displayName: guardianDisplayName,
|
|
70
|
+
}),
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
log.info(
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ChannelId } from "../../../channels/types.js";
|
|
10
10
|
import { findContactChannel } from "../../../contacts/contact-store.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
touchChannelLastSeen,
|
|
13
|
+
touchContactInteraction,
|
|
14
|
+
} from "../../../contacts/contacts-write.js";
|
|
12
15
|
import type {
|
|
13
16
|
ChannelStatus,
|
|
14
17
|
ContactChannel,
|
|
@@ -634,6 +637,7 @@ export async function enforceIngressAcl(
|
|
|
634
637
|
|
|
635
638
|
// 'allow' or 'escalate' — update last seen and continue
|
|
636
639
|
touchChannelLastSeen(resolvedMember.channel.id);
|
|
640
|
+
touchContactInteraction(resolvedMember.contact.id);
|
|
637
641
|
}
|
|
638
642
|
}
|
|
639
643
|
|
|
@@ -129,7 +129,8 @@ export async function handleGuardianReplyIntercept(
|
|
|
129
129
|
messageText: trimmedContent,
|
|
130
130
|
channel: sourceChannel,
|
|
131
131
|
actor: {
|
|
132
|
-
|
|
132
|
+
actorPrincipalId: guardianPrincipalId ?? undefined,
|
|
133
|
+
actorExternalUserId: canonicalSenderId ?? rawSenderId!,
|
|
133
134
|
channel: sourceChannel,
|
|
134
135
|
guardianPrincipalId: guardianPrincipalId ?? undefined,
|
|
135
136
|
},
|