@vellumai/assistant 0.4.32 → 0.4.34
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/docs/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +85 -4
- package/src/__tests__/actor-token-service.test.ts +4 -12
- package/src/__tests__/approval-primitive.test.ts +0 -45
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/callback-handoff-copy.test.ts +0 -1
- package/src/__tests__/channel-approval-routes.test.ts +5 -45
- package/src/__tests__/channel-guardian.test.ts +122 -346
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
- package/src/__tests__/contacts-tools.test.ts +4 -5
- package/src/__tests__/conversation-attention-store.test.ts +2 -65
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -3
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
- package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-grant-minting.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -3
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -7
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -8
- package/src/__tests__/notification-broadcaster.test.ts +1 -2
- package/src/__tests__/notification-decision-fallback.test.ts +0 -2
- package/src/__tests__/notification-decision-strategy.test.ts +0 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- package/src/__tests__/relay-server.test.ts +151 -80
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/scoped-approval-grants.test.ts +9 -40
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/send-notification-tool.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -5
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/thread-seed-composer.test.ts +0 -1
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +65 -77
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -18
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -14
- package/src/__tests__/trusted-contact-verification.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -38
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/approvals/approval-primitive.ts +0 -15
- package/src/approvals/guardian-decision-primitive.ts +0 -3
- package/src/approvals/guardian-request-resolvers.ts +0 -5
- package/src/calls/call-domain.ts +0 -3
- package/src/calls/call-store.ts +0 -3
- package/src/calls/guardian-action-sweep.ts +2 -1
- package/src/calls/guardian-dispatch.ts +1 -2
- package/src/calls/relay-access-wait.ts +0 -4
- package/src/calls/relay-server.ts +8 -66
- package/src/calls/relay-setup-router.ts +1 -2
- package/src/calls/relay-verification.ts +0 -1
- package/src/calls/twilio-routes.ts +0 -3
- package/src/calls/types.ts +0 -1
- package/src/calls/voice-session-bridge.ts +0 -1
- package/src/channels/config.ts +41 -2
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
- package/src/config/env.ts +0 -4
- package/src/config/feature-flag-registry.json +4 -4
- package/src/config/user-reference.ts +47 -9
- package/src/contacts/contact-store.ts +13 -88
- package/src/contacts/contacts-write.ts +3 -11
- package/src/contacts/types.ts +0 -1
- package/src/daemon/handlers/config-channels.ts +19 -44
- package/src/daemon/handlers/config-inbox.ts +6 -6
- package/src/daemon/handlers/contacts.ts +8 -12
- package/src/daemon/handlers/index.ts +0 -2
- package/src/daemon/lifecycle.ts +18 -26
- package/src/daemon/session-process.ts +0 -4
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/conversation-attention-store.ts +4 -19
- package/src/memory/conversation-crud.ts +0 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/guardian-action-store.ts +0 -12
- package/src/memory/guardian-approvals.ts +35 -80
- package/src/memory/guardian-rate-limits.ts +1 -14
- package/src/memory/guardian-verification.ts +6 -34
- package/src/memory/invite-store.ts +76 -15
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +14 -1
- package/src/memory/schema/calls.ts +0 -7
- package/src/memory/schema/contacts.ts +2 -8
- package/src/memory/schema/guardian.ts +0 -5
- package/src/memory/schema/infrastructure.ts +0 -2
- package/src/memory/schema/notifications.ts +3 -17
- package/src/memory/scoped-approval-grants.ts +2 -24
- package/src/notifications/adapters/sms.ts +2 -1
- package/src/notifications/broadcaster.ts +1 -6
- package/src/notifications/decision-engine.ts +3 -4
- package/src/notifications/deliveries-store.ts +0 -4
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/deterministic-checks.ts +1 -6
- package/src/notifications/emit-signal.ts +4 -11
- package/src/notifications/events-store.ts +7 -17
- package/src/notifications/preference-summary.ts +2 -2
- package/src/notifications/preferences-store.ts +2 -9
- package/src/notifications/signal.ts +0 -1
- package/src/notifications/thread-candidates.ts +1 -11
- package/src/notifications/types.ts +0 -3
- package/src/runtime/access-request-helper.ts +3 -10
- package/src/runtime/actor-refresh-token-store.ts +0 -6
- package/src/runtime/actor-token-store.ts +3 -16
- package/src/runtime/actor-trust-resolver.ts +1 -4
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
- package/src/runtime/auth/credential-service.ts +1 -15
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +16 -49
- package/src/runtime/channel-invite-transport.ts +129 -34
- package/src/runtime/channel-invite-transports/email.ts +54 -0
- package/src/runtime/channel-invite-transports/slack.ts +87 -0
- package/src/runtime/channel-invite-transports/sms.ts +74 -0
- package/src/runtime/channel-invite-transports/telegram.ts +35 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
- package/src/runtime/guardian-action-followup-executor.ts +3 -2
- package/src/runtime/guardian-action-grant-minter.ts +0 -1
- package/src/runtime/guardian-outbound-actions.ts +2 -12
- package/src/runtime/guardian-vellum-migration.ts +2 -3
- package/src/runtime/http-server.ts +0 -1
- package/src/runtime/invite-redemption-service.ts +191 -11
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/local-actor-identity.ts +2 -5
- package/src/runtime/routes/access-request-decision.ts +52 -7
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -9
- package/src/runtime/routes/channel-readiness-routes.ts +29 -18
- package/src/runtime/routes/contact-routes.ts +48 -46
- package/src/runtime/routes/conversation-attention-routes.ts +0 -2
- package/src/runtime/routes/global-search-routes.ts +0 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -12
- package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +1 -6
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +296 -47
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +6 -42
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
- package/src/runtime/routes/invite-routes.ts +1 -0
- package/src/runtime/routes/pairing-routes.ts +4 -4
- package/src/runtime/tool-grant-request-helper.ts +0 -1
- package/src/tools/browser/browser-manager.ts +22 -12
- package/src/tools/browser/runtime-check.ts +110 -3
- package/src/tools/calls/call-start.ts +1 -3
- package/src/tools/followups/followup_create.ts +1 -2
- package/src/tools/shared/shell-output.ts +7 -2
- package/src/tools/tool-approval-handler.ts +0 -2
- package/src/util/platform.ts +0 -4
- package/src/workspace/git-service.ts +10 -4
|
@@ -13,6 +13,7 @@ import { upsertMember } from "../contacts/contacts-write.js";
|
|
|
13
13
|
import { getSqlite } from "../memory/db.js";
|
|
14
14
|
import {
|
|
15
15
|
findActiveVoiceInvites,
|
|
16
|
+
findByInviteCodeHash,
|
|
16
17
|
findByTokenHash,
|
|
17
18
|
hashToken,
|
|
18
19
|
markInviteExpired,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
} from "../memory/invite-store.js";
|
|
21
22
|
import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
|
|
22
23
|
import { hashVoiceCode } from "../util/voice-code.js";
|
|
23
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
// Outcome type
|
|
@@ -83,7 +83,6 @@ export function redeemInvite(params: {
|
|
|
83
83
|
externalChatId,
|
|
84
84
|
displayName,
|
|
85
85
|
username,
|
|
86
|
-
assistantId,
|
|
87
86
|
} = params;
|
|
88
87
|
|
|
89
88
|
if (!externalUserId && !externalChatId) {
|
|
@@ -179,7 +178,6 @@ export function redeemInvite(params: {
|
|
|
179
178
|
getSqlite()
|
|
180
179
|
.transaction(() => {
|
|
181
180
|
reactivated = upsertMember({
|
|
182
|
-
assistantId: assistantId ?? invite.assistantId,
|
|
183
181
|
sourceChannel,
|
|
184
182
|
externalUserId,
|
|
185
183
|
externalChatId,
|
|
@@ -226,7 +224,6 @@ export function redeemInvite(params: {
|
|
|
226
224
|
getSqlite()
|
|
227
225
|
.transaction(() => {
|
|
228
226
|
freshResult = upsertMember({
|
|
229
|
-
assistantId: assistantId ?? invite.assistantId,
|
|
230
227
|
sourceChannel,
|
|
231
228
|
externalUserId,
|
|
232
229
|
externalChatId,
|
|
@@ -287,11 +284,7 @@ export function redeemVoiceInviteCode(params: {
|
|
|
287
284
|
sourceChannel: "voice";
|
|
288
285
|
code: string;
|
|
289
286
|
}): VoiceRedemptionOutcome {
|
|
290
|
-
const {
|
|
291
|
-
assistantId = DAEMON_INTERNAL_ASSISTANT_ID,
|
|
292
|
-
callerExternalUserId,
|
|
293
|
-
code,
|
|
294
|
-
} = params;
|
|
287
|
+
const { callerExternalUserId, code } = params;
|
|
295
288
|
|
|
296
289
|
if (!callerExternalUserId) {
|
|
297
290
|
return { ok: false, reason: "invalid_or_expired" };
|
|
@@ -299,7 +292,6 @@ export function redeemVoiceInviteCode(params: {
|
|
|
299
292
|
|
|
300
293
|
// Find all active voice invites bound to the caller's phone number
|
|
301
294
|
const candidates = findActiveVoiceInvites({
|
|
302
|
-
assistantId,
|
|
303
295
|
expectedExternalUserId: callerExternalUserId,
|
|
304
296
|
});
|
|
305
297
|
|
|
@@ -371,7 +363,6 @@ export function redeemVoiceInviteCode(params: {
|
|
|
371
363
|
getSqlite()
|
|
372
364
|
.transaction(() => {
|
|
373
365
|
const writeResult = upsertMember({
|
|
374
|
-
assistantId: invite.assistantId,
|
|
375
366
|
sourceChannel: "voice",
|
|
376
367
|
externalUserId: callerExternalUserId,
|
|
377
368
|
externalChatId: callerExternalUserId,
|
|
@@ -404,3 +395,192 @@ export function redeemVoiceInviteCode(params: {
|
|
|
404
395
|
inviteId: invite.id,
|
|
405
396
|
};
|
|
406
397
|
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// redeemInviteByCode
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Redeem an invite using a 6-digit invite code (channel-agnostic).
|
|
405
|
+
*
|
|
406
|
+
* Unlike token-based redemption which uses deep links, code redemption works
|
|
407
|
+
* by intercepting bare 6-digit messages on channels with codeRedemptionEnabled.
|
|
408
|
+
* The code is hashed and looked up via `findByInviteCodeHash`.
|
|
409
|
+
*
|
|
410
|
+
* Validation: active status, not expired, uses remaining, channel match.
|
|
411
|
+
* On success: upserts/reactivates a member with status 'active', policy 'allow'.
|
|
412
|
+
*/
|
|
413
|
+
export function redeemInviteByCode(params: {
|
|
414
|
+
code: string;
|
|
415
|
+
sourceChannel: string;
|
|
416
|
+
externalUserId?: string;
|
|
417
|
+
externalChatId?: string;
|
|
418
|
+
displayName?: string;
|
|
419
|
+
username?: string;
|
|
420
|
+
assistantId?: string;
|
|
421
|
+
}): InviteRedemptionOutcome {
|
|
422
|
+
const {
|
|
423
|
+
code,
|
|
424
|
+
sourceChannel,
|
|
425
|
+
externalUserId,
|
|
426
|
+
externalChatId,
|
|
427
|
+
displayName,
|
|
428
|
+
username,
|
|
429
|
+
} = params;
|
|
430
|
+
|
|
431
|
+
if (!externalUserId && !externalChatId) {
|
|
432
|
+
return { ok: false, reason: "missing_identity" };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const codeHash = hashVoiceCode(code);
|
|
436
|
+
const invite = findByInviteCodeHash(codeHash, sourceChannel);
|
|
437
|
+
|
|
438
|
+
if (!invite) {
|
|
439
|
+
return { ok: false, reason: "invalid_token" };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (invite.status !== "active") {
|
|
443
|
+
const mapped = STORE_ERROR_TO_REASON[`invite_${invite.status}`];
|
|
444
|
+
if (mapped) return mapped;
|
|
445
|
+
return { ok: false, reason: "invalid_token" };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (invite.expiresAt <= Date.now()) {
|
|
449
|
+
markInviteExpired(invite.id);
|
|
450
|
+
return { ok: false, reason: "expired" };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (invite.useCount >= invite.maxUses) {
|
|
454
|
+
return { ok: false, reason: "max_uses_reached" };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Code is valid — now safe to check existing membership without leaking
|
|
458
|
+
// membership status to callers with bogus codes.
|
|
459
|
+
const canonicalUserId = externalUserId
|
|
460
|
+
? (canonicalizeInboundIdentity(
|
|
461
|
+
sourceChannel as ChannelId,
|
|
462
|
+
externalUserId,
|
|
463
|
+
) ?? externalUserId)
|
|
464
|
+
: undefined;
|
|
465
|
+
const contactResult = findContactChannel({
|
|
466
|
+
channelType: sourceChannel,
|
|
467
|
+
externalUserId: canonicalUserId,
|
|
468
|
+
externalChatId: externalChatId,
|
|
469
|
+
});
|
|
470
|
+
const existingChannel = contactResult?.channel ?? null;
|
|
471
|
+
const existingContact = contactResult?.contact ?? null;
|
|
472
|
+
|
|
473
|
+
if (existingChannel && existingChannel.status === "active") {
|
|
474
|
+
return { ok: true, type: "already_member", memberId: existingChannel.id };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Blocked members cannot bypass the guardian's explicit block via invite
|
|
478
|
+
// codes. Return the same generic failure as an invalid token to avoid
|
|
479
|
+
// leaking membership status to the caller.
|
|
480
|
+
if (existingChannel && existingChannel.status === "blocked") {
|
|
481
|
+
return { ok: false, reason: "invalid_token" };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Inactive member reactivation: reactivate via upsertMember and consume
|
|
485
|
+
// an invite use atomically.
|
|
486
|
+
if (existingChannel) {
|
|
487
|
+
const STALE_INVITE_REACTIVATE = Symbol("stale_invite_reactivate");
|
|
488
|
+
const canonicalMemberId = existingChannel.externalUserId
|
|
489
|
+
? canonicalizeInboundIdentity(
|
|
490
|
+
sourceChannel as ChannelId,
|
|
491
|
+
existingChannel.externalUserId,
|
|
492
|
+
)
|
|
493
|
+
: null;
|
|
494
|
+
const canonicalCallerId = externalUserId
|
|
495
|
+
? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId)
|
|
496
|
+
: null;
|
|
497
|
+
const memberMatchesSender = !!(
|
|
498
|
+
canonicalMemberId &&
|
|
499
|
+
canonicalCallerId &&
|
|
500
|
+
canonicalMemberId === canonicalCallerId
|
|
501
|
+
);
|
|
502
|
+
const preservedDisplayName =
|
|
503
|
+
memberMatchesSender && existingContact?.displayName?.trim().length
|
|
504
|
+
? existingContact.displayName
|
|
505
|
+
: displayName;
|
|
506
|
+
|
|
507
|
+
let reactivated: ReturnType<typeof upsertMember> | undefined;
|
|
508
|
+
try {
|
|
509
|
+
getSqlite()
|
|
510
|
+
.transaction(() => {
|
|
511
|
+
reactivated = upsertMember({
|
|
512
|
+
sourceChannel,
|
|
513
|
+
externalUserId,
|
|
514
|
+
externalChatId,
|
|
515
|
+
displayName: preservedDisplayName,
|
|
516
|
+
username,
|
|
517
|
+
status: "active",
|
|
518
|
+
policy: "allow",
|
|
519
|
+
inviteId: invite.id,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const recorded = recordInviteUse({
|
|
523
|
+
inviteId: invite.id,
|
|
524
|
+
externalUserId,
|
|
525
|
+
externalChatId,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (!recorded) throw STALE_INVITE_REACTIVATE;
|
|
529
|
+
})
|
|
530
|
+
.immediate();
|
|
531
|
+
} catch (err) {
|
|
532
|
+
if (err === STALE_INVITE_REACTIVATE) {
|
|
533
|
+
return { ok: false, reason: "invalid_token" };
|
|
534
|
+
}
|
|
535
|
+
throw err;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
ok: true,
|
|
540
|
+
type: "redeemed",
|
|
541
|
+
memberId: reactivated!.channel.id,
|
|
542
|
+
inviteId: invite.id,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Fresh member creation: upsert into contacts tables and consume an invite
|
|
547
|
+
// use atomically.
|
|
548
|
+
const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
|
|
549
|
+
let freshResult: ReturnType<typeof upsertMember> | undefined;
|
|
550
|
+
try {
|
|
551
|
+
getSqlite()
|
|
552
|
+
.transaction(() => {
|
|
553
|
+
freshResult = upsertMember({
|
|
554
|
+
sourceChannel,
|
|
555
|
+
externalUserId,
|
|
556
|
+
externalChatId,
|
|
557
|
+
displayName,
|
|
558
|
+
username,
|
|
559
|
+
status: "active",
|
|
560
|
+
policy: "allow",
|
|
561
|
+
inviteId: invite.id,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const recorded = recordInviteUse({
|
|
565
|
+
inviteId: invite.id,
|
|
566
|
+
externalUserId,
|
|
567
|
+
externalChatId,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
if (!recorded) throw STALE_INVITE_FRESH;
|
|
571
|
+
})
|
|
572
|
+
.immediate();
|
|
573
|
+
} catch (err) {
|
|
574
|
+
if (err === STALE_INVITE_FRESH) {
|
|
575
|
+
return { ok: false, reason: "invalid_token" };
|
|
576
|
+
}
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
ok: true,
|
|
582
|
+
type: "redeemed",
|
|
583
|
+
memberId: freshResult!.channel.id,
|
|
584
|
+
inviteId: invite.id,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
@@ -13,13 +13,13 @@ import type { InviteRedemptionOutcome } from "./invite-redemption-service.js";
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
15
|
export const INVITE_REPLY_TEMPLATES = {
|
|
16
|
-
redeemed: "Welcome! You've been granted access
|
|
16
|
+
redeemed: "Welcome! You've been granted access.",
|
|
17
17
|
already_member: "You already have access.",
|
|
18
|
-
invalid_token: "This invite
|
|
19
|
-
expired: "This invite
|
|
20
|
-
revoked: "This invite
|
|
21
|
-
max_uses_reached: "This invite
|
|
22
|
-
channel_mismatch: "This invite
|
|
18
|
+
invalid_token: "This invite is no longer valid.",
|
|
19
|
+
expired: "This invite is no longer valid.",
|
|
20
|
+
revoked: "This invite is no longer valid.",
|
|
21
|
+
max_uses_reached: "This invite is no longer valid.",
|
|
22
|
+
channel_mismatch: "This invite is not valid for this channel.",
|
|
23
23
|
missing_identity:
|
|
24
24
|
"Unable to process this invite. Please contact the person who shared it.",
|
|
25
25
|
generic_failure:
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { isChannelId } from "../channels/types.js";
|
|
12
|
+
import {
|
|
13
|
+
DECLINED_BY_USER_SENTINEL,
|
|
14
|
+
DEFAULT_USER_REFERENCE,
|
|
15
|
+
resolveGuardianName,
|
|
16
|
+
} from "../config/user-reference.js";
|
|
12
17
|
import {
|
|
13
18
|
createInvite,
|
|
14
19
|
findByTokenHash,
|
|
@@ -20,7 +25,7 @@ import {
|
|
|
20
25
|
} from "../memory/invite-store.js";
|
|
21
26
|
import { isValidE164 } from "../util/phone.js";
|
|
22
27
|
import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
|
|
23
|
-
import {
|
|
28
|
+
import { getInviteAdapterRegistry } from "./channel-invite-transport.js";
|
|
24
29
|
import {
|
|
25
30
|
type InviteRedemptionOutcome,
|
|
26
31
|
redeemInvite as redeemInviteTyped,
|
|
@@ -28,8 +33,6 @@ import {
|
|
|
28
33
|
type VoiceRedemptionOutcome,
|
|
29
34
|
} from "./invite-redemption-service.js";
|
|
30
35
|
|
|
31
|
-
import "./channel-invite-transports/telegram.js";
|
|
32
|
-
|
|
33
36
|
// ---------------------------------------------------------------------------
|
|
34
37
|
// Response shapes — used by both HTTP routes and IPC handlers
|
|
35
38
|
// ---------------------------------------------------------------------------
|
|
@@ -54,6 +57,10 @@ export interface InviteResponseData {
|
|
|
54
57
|
voiceCodeDigits?: number;
|
|
55
58
|
friendName?: string;
|
|
56
59
|
guardianName?: string;
|
|
60
|
+
// Non-voice invite fields (present only for non-voice invites)
|
|
61
|
+
inviteCode?: string;
|
|
62
|
+
guardianInstruction?: string;
|
|
63
|
+
channelHandle?: string;
|
|
57
64
|
createdAt: number;
|
|
58
65
|
}
|
|
59
66
|
|
|
@@ -66,11 +73,11 @@ function buildSharePayload(
|
|
|
66
73
|
rawToken?: string,
|
|
67
74
|
): InviteResponseData["share"] | undefined {
|
|
68
75
|
if (!rawToken || !isChannelId(sourceChannel)) return undefined;
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
76
|
+
const adapter = getInviteAdapterRegistry().get(sourceChannel);
|
|
77
|
+
if (!adapter?.buildShareLink) return undefined;
|
|
71
78
|
|
|
72
79
|
try {
|
|
73
|
-
return
|
|
80
|
+
return adapter.buildShareLink({
|
|
74
81
|
rawToken,
|
|
75
82
|
sourceChannel,
|
|
76
83
|
});
|
|
@@ -83,7 +90,13 @@ function buildSharePayload(
|
|
|
83
90
|
|
|
84
91
|
function inviteToResponse(
|
|
85
92
|
inv: IngressInvite,
|
|
86
|
-
opts?: {
|
|
93
|
+
opts?: {
|
|
94
|
+
rawToken?: string;
|
|
95
|
+
voiceCode?: string;
|
|
96
|
+
inviteCode?: string;
|
|
97
|
+
guardianInstruction?: string;
|
|
98
|
+
channelHandle?: string;
|
|
99
|
+
},
|
|
87
100
|
): InviteResponseData {
|
|
88
101
|
const share = buildSharePayload(inv.sourceChannel, opts?.rawToken);
|
|
89
102
|
return {
|
|
@@ -106,6 +119,11 @@ function inviteToResponse(
|
|
|
106
119
|
: {}),
|
|
107
120
|
...(inv.friendName ? { friendName: inv.friendName } : {}),
|
|
108
121
|
...(inv.guardianName ? { guardianName: inv.guardianName } : {}),
|
|
122
|
+
...(opts?.inviteCode ? { inviteCode: opts.inviteCode } : {}),
|
|
123
|
+
...(opts?.guardianInstruction
|
|
124
|
+
? { guardianInstruction: opts.guardianInstruction }
|
|
125
|
+
: {}),
|
|
126
|
+
...(opts?.channelHandle ? { channelHandle: opts.channelHandle } : {}),
|
|
109
127
|
createdAt: inv.createdAt,
|
|
110
128
|
};
|
|
111
129
|
}
|
|
@@ -127,6 +145,8 @@ export function createIngressInvite(params: {
|
|
|
127
145
|
note?: string;
|
|
128
146
|
maxUses?: number;
|
|
129
147
|
expiresInMs?: number;
|
|
148
|
+
// Contact display name for personalizing guardian instructions
|
|
149
|
+
contactName?: string;
|
|
130
150
|
// Voice invite parameters
|
|
131
151
|
expectedExternalUserId?: string;
|
|
132
152
|
voiceCodeDigits?: number;
|
|
@@ -142,8 +162,15 @@ export function createIngressInvite(params: {
|
|
|
142
162
|
// exactly once and never stored.
|
|
143
163
|
let voiceCode: string | undefined;
|
|
144
164
|
let voiceCodeHash: string | undefined;
|
|
165
|
+
let effectiveGuardianName: string | undefined;
|
|
145
166
|
const isVoice = params.sourceChannel === "voice";
|
|
146
167
|
|
|
168
|
+
// For non-voice invites: generate a 6-digit invite code for guardian-mediated
|
|
169
|
+
// redemption. The plaintext code is returned once in the response; only the
|
|
170
|
+
// hash is persisted for later redemption lookup.
|
|
171
|
+
let inviteCode: string | undefined;
|
|
172
|
+
let inviteCodeHash: string | undefined;
|
|
173
|
+
|
|
147
174
|
if (isVoice) {
|
|
148
175
|
if (!params.expectedExternalUserId) {
|
|
149
176
|
return {
|
|
@@ -161,14 +188,20 @@ export function createIngressInvite(params: {
|
|
|
161
188
|
if (typeof params.friendName !== "string" || !params.friendName.trim()) {
|
|
162
189
|
return { ok: false, error: "friendName is required for voice invites" };
|
|
163
190
|
}
|
|
191
|
+
effectiveGuardianName =
|
|
192
|
+
params.guardianName?.trim() || resolveGuardianName();
|
|
164
193
|
if (
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
!effectiveGuardianName ||
|
|
195
|
+
effectiveGuardianName === DEFAULT_USER_REFERENCE ||
|
|
196
|
+
effectiveGuardianName === DECLINED_BY_USER_SENTINEL
|
|
167
197
|
) {
|
|
168
198
|
return { ok: false, error: "guardianName is required for voice invites" };
|
|
169
199
|
}
|
|
170
200
|
voiceCode = generateVoiceCode(6);
|
|
171
201
|
voiceCodeHash = hashVoiceCode(voiceCode);
|
|
202
|
+
} else {
|
|
203
|
+
inviteCode = generateVoiceCode(6);
|
|
204
|
+
inviteCodeHash = hashVoiceCode(inviteCode);
|
|
172
205
|
}
|
|
173
206
|
|
|
174
207
|
const { invite, rawToken } = createInvite({
|
|
@@ -182,10 +215,44 @@ export function createIngressInvite(params: {
|
|
|
182
215
|
voiceCodeHash,
|
|
183
216
|
voiceCodeDigits: 6,
|
|
184
217
|
friendName: params.friendName,
|
|
185
|
-
guardianName:
|
|
218
|
+
guardianName: effectiveGuardianName,
|
|
186
219
|
}
|
|
187
|
-
: {}),
|
|
220
|
+
: { inviteCodeHash }),
|
|
188
221
|
});
|
|
222
|
+
|
|
223
|
+
// Build guardian instruction for non-voice invites
|
|
224
|
+
let guardianInstruction: string | undefined;
|
|
225
|
+
let channelHandle: string | undefined;
|
|
226
|
+
if (!isVoice && inviteCode) {
|
|
227
|
+
const channelId = isChannelId(params.sourceChannel)
|
|
228
|
+
? params.sourceChannel
|
|
229
|
+
: undefined;
|
|
230
|
+
const adapter = channelId
|
|
231
|
+
? getInviteAdapterRegistry().get(channelId)
|
|
232
|
+
: undefined;
|
|
233
|
+
|
|
234
|
+
if (adapter?.buildGuardianInstruction) {
|
|
235
|
+
try {
|
|
236
|
+
const adapterResult = adapter.buildGuardianInstruction({
|
|
237
|
+
inviteCode,
|
|
238
|
+
contactName: params.contactName,
|
|
239
|
+
});
|
|
240
|
+
if (adapterResult) {
|
|
241
|
+
guardianInstruction = adapterResult.instruction;
|
|
242
|
+
channelHandle = adapterResult.channelHandle;
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Fall through to generic instruction if adapter fails
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!guardianInstruction) {
|
|
250
|
+
const contactLabel = params.contactName || "the contact";
|
|
251
|
+
const channelLabel = params.sourceChannel;
|
|
252
|
+
guardianInstruction = `Tell ${contactLabel} to contact the assistant on ${channelLabel} and provide the code ${inviteCode}.`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
189
256
|
// Voice invites must not expose the token — callers must redeem via the
|
|
190
257
|
// identity-bound voice code flow, not the generic token redemption path.
|
|
191
258
|
return {
|
|
@@ -193,6 +260,9 @@ export function createIngressInvite(params: {
|
|
|
193
260
|
data: inviteToResponse(invite, {
|
|
194
261
|
rawToken: isVoice ? undefined : rawToken,
|
|
195
262
|
voiceCode,
|
|
263
|
+
inviteCode,
|
|
264
|
+
guardianInstruction,
|
|
265
|
+
channelHandle,
|
|
196
266
|
}),
|
|
197
267
|
};
|
|
198
268
|
}
|
|
@@ -41,7 +41,7 @@ export function resolveLocalIpcTrustContext(
|
|
|
41
41
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
42
42
|
|
|
43
43
|
// Try contacts-first for the vellum guardian channel
|
|
44
|
-
const guardianResult = findGuardianForChannel("vellum"
|
|
44
|
+
const guardianResult = findGuardianForChannel("vellum");
|
|
45
45
|
if (guardianResult && guardianResult.contact.principalId) {
|
|
46
46
|
const guardianPrincipalId = guardianResult.contact.principalId;
|
|
47
47
|
const trustCtx = resolveTrustContext({
|
|
@@ -93,10 +93,7 @@ export function resolveLocalIpcAuthContext(sessionId: string): AuthContext {
|
|
|
93
93
|
const authContext = buildIpcAuthContext(sessionId);
|
|
94
94
|
|
|
95
95
|
// Enrich with the guardian principal ID from contacts-first path
|
|
96
|
-
const guardianResult = findGuardianForChannel(
|
|
97
|
-
"vellum",
|
|
98
|
-
authContext.assistantId,
|
|
99
|
-
);
|
|
96
|
+
const guardianResult = findGuardianForChannel("vellum");
|
|
100
97
|
if (guardianResult && guardianResult.contact.principalId) {
|
|
101
98
|
return {
|
|
102
99
|
...authContext,
|
|
@@ -86,7 +86,6 @@ export function handleAccessRequestDecision(
|
|
|
86
86
|
// so only the original requester can consume the code. Mark as
|
|
87
87
|
// trusted_contact so the consume path skips guardian binding creation.
|
|
88
88
|
const session = createOutboundSession({
|
|
89
|
-
assistantId: approval.assistantId,
|
|
90
89
|
channel: approval.channel,
|
|
91
90
|
expectedExternalUserId: approval.requesterExternalUserId,
|
|
92
91
|
expectedChatId: approval.requesterChatId,
|
|
@@ -141,6 +140,40 @@ export async function deliverVerificationCodeToGuardian(params: {
|
|
|
141
140
|
}
|
|
142
141
|
}
|
|
143
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the delivery target for requester notifications. On Slack,
|
|
145
|
+
* posting to a user ID (rather than the originating channel) delivers
|
|
146
|
+
* the message as a DM, which is less disruptive than replying in a
|
|
147
|
+
* shared channel. When routing to a DM, the `threadTs` query param is
|
|
148
|
+
* stripped from the callback URL because it belongs to the guardian's
|
|
149
|
+
* channel thread and would cause `thread_not_found` errors in the DM.
|
|
150
|
+
*/
|
|
151
|
+
function resolveRequesterTarget(params: {
|
|
152
|
+
channel?: string;
|
|
153
|
+
replyCallbackUrl: string;
|
|
154
|
+
requesterChatId: string;
|
|
155
|
+
requesterExternalUserId?: string;
|
|
156
|
+
}): { chatId: string; callbackUrl: string } {
|
|
157
|
+
if (params.channel === "slack" && params.requesterExternalUserId) {
|
|
158
|
+
let callbackUrl = params.replyCallbackUrl;
|
|
159
|
+
try {
|
|
160
|
+
const url = new URL(params.replyCallbackUrl);
|
|
161
|
+
url.searchParams.delete("threadTs");
|
|
162
|
+
callbackUrl = url.toString();
|
|
163
|
+
} catch {
|
|
164
|
+
// Malformed URL — use as-is; the downstream fetch will handle the error.
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
chatId: params.requesterExternalUserId,
|
|
168
|
+
callbackUrl,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
chatId: params.requesterChatId,
|
|
173
|
+
callbackUrl: params.replyCallbackUrl,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
144
177
|
/**
|
|
145
178
|
* Notify the requester that the guardian has approved their access request
|
|
146
179
|
* and they should enter the verification code they receive from the guardian.
|
|
@@ -150,16 +183,20 @@ export async function notifyRequesterOfApproval(params: {
|
|
|
150
183
|
requesterChatId: string;
|
|
151
184
|
assistantId: string;
|
|
152
185
|
bearerToken?: string;
|
|
186
|
+
channel?: string;
|
|
187
|
+
requesterExternalUserId?: string;
|
|
153
188
|
}): Promise<void> {
|
|
154
189
|
const text =
|
|
155
190
|
"Your access request has been approved! " +
|
|
156
191
|
"Please enter the 6-digit verification code you receive from the guardian.";
|
|
157
192
|
|
|
193
|
+
const target = resolveRequesterTarget(params);
|
|
194
|
+
|
|
158
195
|
try {
|
|
159
196
|
await deliverChannelReply(
|
|
160
|
-
|
|
197
|
+
target.callbackUrl,
|
|
161
198
|
{
|
|
162
|
-
chatId:
|
|
199
|
+
chatId: target.chatId,
|
|
163
200
|
text,
|
|
164
201
|
assistantId: params.assistantId,
|
|
165
202
|
},
|
|
@@ -183,16 +220,20 @@ export async function notifyRequesterOfDeliveryFailure(params: {
|
|
|
183
220
|
requesterChatId: string;
|
|
184
221
|
assistantId: string;
|
|
185
222
|
bearerToken?: string;
|
|
223
|
+
channel?: string;
|
|
224
|
+
requesterExternalUserId?: string;
|
|
186
225
|
}): Promise<void> {
|
|
187
226
|
const text =
|
|
188
227
|
"Your access request was approved, but we were unable to " +
|
|
189
228
|
"deliver the verification code. Please try again later.";
|
|
190
229
|
|
|
230
|
+
const target = resolveRequesterTarget(params);
|
|
231
|
+
|
|
191
232
|
try {
|
|
192
233
|
await deliverChannelReply(
|
|
193
|
-
|
|
234
|
+
target.callbackUrl,
|
|
194
235
|
{
|
|
195
|
-
chatId:
|
|
236
|
+
chatId: target.chatId,
|
|
196
237
|
text,
|
|
197
238
|
assistantId: params.assistantId,
|
|
198
239
|
},
|
|
@@ -214,14 +255,18 @@ export async function notifyRequesterOfDenial(params: {
|
|
|
214
255
|
requesterChatId: string;
|
|
215
256
|
assistantId: string;
|
|
216
257
|
bearerToken?: string;
|
|
258
|
+
channel?: string;
|
|
259
|
+
requesterExternalUserId?: string;
|
|
217
260
|
}): Promise<void> {
|
|
218
261
|
const text = "Your access request has been denied by the guardian.";
|
|
219
262
|
|
|
263
|
+
const target = resolveRequesterTarget(params);
|
|
264
|
+
|
|
220
265
|
try {
|
|
221
266
|
await deliverChannelReply(
|
|
222
|
-
|
|
267
|
+
target.callbackUrl,
|
|
223
268
|
{
|
|
224
|
-
chatId:
|
|
269
|
+
chatId: target.chatId,
|
|
225
270
|
text,
|
|
226
271
|
assistantId: params.assistantId,
|
|
227
272
|
},
|
|
@@ -99,7 +99,6 @@ export async function handleGuardianCallbackDecision(
|
|
|
99
99
|
callbackDecision.requestId,
|
|
100
100
|
sourceChannel,
|
|
101
101
|
conversationExternalId,
|
|
102
|
-
assistantId,
|
|
103
102
|
)
|
|
104
103
|
: null;
|
|
105
104
|
|
|
@@ -110,7 +109,6 @@ export async function handleGuardianCallbackDecision(
|
|
|
110
109
|
const allPending = getAllPendingApprovalsByGuardianChat(
|
|
111
110
|
sourceChannel,
|
|
112
111
|
conversationExternalId,
|
|
113
|
-
assistantId,
|
|
114
112
|
);
|
|
115
113
|
if (allPending.length === 1) {
|
|
116
114
|
guardianApproval = allPending[0];
|
|
@@ -141,7 +139,6 @@ export async function handleGuardianCallbackDecision(
|
|
|
141
139
|
const allPending = getAllPendingApprovalsByGuardianChat(
|
|
142
140
|
sourceChannel,
|
|
143
141
|
conversationExternalId,
|
|
144
|
-
assistantId,
|
|
145
142
|
);
|
|
146
143
|
if (allPending.length === 1) {
|
|
147
144
|
guardianApproval = allPending[0];
|
|
@@ -204,7 +201,6 @@ export async function handleGuardianCallbackDecision(
|
|
|
204
201
|
const allGuardianPending = getAllPendingApprovalsByGuardianChat(
|
|
205
202
|
sourceChannel,
|
|
206
203
|
conversationExternalId,
|
|
207
|
-
assistantId,
|
|
208
204
|
);
|
|
209
205
|
// Only present approvals that belong to this sender so the engine
|
|
210
206
|
// does not offer disambiguation for requests assigned to a rotated
|
|
@@ -609,7 +605,6 @@ async function handleLegacyDecision(params: {
|
|
|
609
605
|
legacyGuardianDecision.requestId,
|
|
610
606
|
sourceChannel,
|
|
611
607
|
conversationExternalId,
|
|
612
|
-
assistantId,
|
|
613
608
|
);
|
|
614
609
|
if (!resolvedByRequest) {
|
|
615
610
|
// The referenced request doesn't match any pending guardian
|
|
@@ -789,6 +784,8 @@ async function handleAccessRequestApproval(
|
|
|
789
784
|
requesterChatId: approval.requesterChatId,
|
|
790
785
|
assistantId,
|
|
791
786
|
bearerToken,
|
|
787
|
+
channel: approval.channel,
|
|
788
|
+
requesterExternalUserId: approval.requesterExternalUserId,
|
|
792
789
|
});
|
|
793
790
|
|
|
794
791
|
// Emit both guardian_decision and denied signals so all lifecycle
|
|
@@ -805,7 +802,6 @@ async function handleAccessRequestApproval(
|
|
|
805
802
|
sourceEventName: "ingress.trusted_contact.guardian_decision",
|
|
806
803
|
sourceChannel: approval.channel,
|
|
807
804
|
sourceSessionId: approval.conversationId,
|
|
808
|
-
assistantId,
|
|
809
805
|
attentionHints: {
|
|
810
806
|
requiresAction: false,
|
|
811
807
|
urgency: "medium",
|
|
@@ -820,7 +816,6 @@ async function handleAccessRequestApproval(
|
|
|
820
816
|
sourceEventName: "ingress.trusted_contact.denied",
|
|
821
817
|
sourceChannel: approval.channel,
|
|
822
818
|
sourceSessionId: approval.conversationId,
|
|
823
|
-
assistantId,
|
|
824
819
|
attentionHints: {
|
|
825
820
|
requiresAction: false,
|
|
826
821
|
urgency: "low",
|
|
@@ -863,6 +858,8 @@ async function handleAccessRequestApproval(
|
|
|
863
858
|
requesterChatId: approval.requesterChatId,
|
|
864
859
|
assistantId,
|
|
865
860
|
bearerToken,
|
|
861
|
+
channel: approval.channel,
|
|
862
|
+
requesterExternalUserId: approval.requesterExternalUserId,
|
|
866
863
|
});
|
|
867
864
|
} else {
|
|
868
865
|
// Let the requester know something went wrong without revealing details
|
|
@@ -871,6 +868,8 @@ async function handleAccessRequestApproval(
|
|
|
871
868
|
requesterChatId: approval.requesterChatId,
|
|
872
869
|
assistantId,
|
|
873
870
|
bearerToken,
|
|
871
|
+
channel: approval.channel,
|
|
872
|
+
requesterExternalUserId: approval.requesterExternalUserId,
|
|
874
873
|
});
|
|
875
874
|
}
|
|
876
875
|
|
|
@@ -885,7 +884,6 @@ async function handleAccessRequestApproval(
|
|
|
885
884
|
sourceEventName: "ingress.trusted_contact.guardian_decision",
|
|
886
885
|
sourceChannel: approval.channel,
|
|
887
886
|
sourceSessionId: approval.conversationId,
|
|
888
|
-
assistantId,
|
|
889
887
|
attentionHints: {
|
|
890
888
|
requiresAction: false,
|
|
891
889
|
urgency: "medium",
|
|
@@ -912,7 +910,6 @@ async function handleAccessRequestApproval(
|
|
|
912
910
|
sourceEventName: "ingress.trusted_contact.verification_sent",
|
|
913
911
|
sourceChannel: approval.channel,
|
|
914
912
|
sourceSessionId: approval.conversationId,
|
|
915
|
-
assistantId,
|
|
916
913
|
attentionHints: {
|
|
917
914
|
requiresAction: false,
|
|
918
915
|
urgency: "low",
|