@vellumai/assistant 0.4.31 → 0.4.33
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 +1 -1
- package/docs/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/access-request-decision.test.ts +83 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +0 -1
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -23
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +6 -5
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -4
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-regressions.test.ts +6 -6
- 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 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/relay-server.test.ts +145 -2
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +0 -1
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +66 -74
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/amazon/session.ts +30 -91
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +271 -956
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/channels/config.ts +41 -2
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- 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/core-schema.ts +1 -1
- package/src/config/env.ts +0 -14
- package/src/config/feature-flag-registry.json +5 -5
- package/src/config/loader.ts +19 -0
- package/src/config/schema.ts +2 -2
- package/src/config/user-reference.ts +47 -9
- package/src/daemon/handlers/config-channels.ts +11 -10
- package/src/daemon/handlers/contacts.ts +5 -1
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1338
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +18 -55
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +9 -1
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/invite-store.ts +71 -1
- package/src/memory/job-handlers/conflict.ts +24 -0
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +127 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1385
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +52 -26
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +1 -3
- package/src/runtime/channel-invite-transport.ts +121 -34
- package/src/runtime/channel-invite-transports/email.ts +50 -0
- package/src/runtime/channel-invite-transports/slack.ts +81 -0
- package/src/runtime/channel-invite-transports/sms.ts +70 -0
- package/src/runtime/channel-invite-transports/telegram.ts +29 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/http-server.ts +83 -722
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/invite-redemption-service.ts +193 -0
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/access-request-decision.ts +52 -6
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -0
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +96 -6
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +190 -193
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +15 -0
- package/src/runtime/routes/guardian-action-routes.ts +22 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +21 -6
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +9 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +295 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -42
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +32 -0
- package/src/runtime/routes/migration-routes.ts +30 -0
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/browser/browser-manager.ts +10 -1
- package/src/tools/browser/runtime-check.ts +3 -1
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/tools/shared/shell-output.ts +7 -2
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/util/platform.ts +0 -4
- package/src/workspace/git-service.ts +10 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/contacts/startup-migration.ts +0 -21
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
parseInterfaceId,
|
|
12
12
|
} from "../../channels/types.js";
|
|
13
13
|
import { getChannelPermissionProfile } from "../../config/channel-permission-profiles.js";
|
|
14
|
+
import { touchContactInteraction } from "../../contacts/contacts-write.js";
|
|
14
15
|
import type { TrustContext } from "../../daemon/session-runtime-assembly.js";
|
|
15
16
|
import * as attachmentsStore from "../../memory/attachments-store.js";
|
|
16
17
|
import * as channelDeliveryStore from "../../memory/channel-delivery-store.js";
|
|
@@ -44,9 +45,6 @@ import { handleGuardianReplyIntercept } from "./inbound-stages/guardian-reply-in
|
|
|
44
45
|
import { runSecretIngressCheck } from "./inbound-stages/secret-ingress-check.js";
|
|
45
46
|
import { handleVerificationIntercept } from "./inbound-stages/verification-intercept.js";
|
|
46
47
|
|
|
47
|
-
import "../channel-invite-transports/telegram.js";
|
|
48
|
-
import "../channel-invite-transports/voice.js";
|
|
49
|
-
|
|
50
48
|
const log = getLogger("runtime-http");
|
|
51
49
|
|
|
52
50
|
export async function handleChannelInbound(
|
|
@@ -251,6 +249,7 @@ export async function handleChannelInbound(
|
|
|
251
249
|
canonicalAssistantId,
|
|
252
250
|
assistantId,
|
|
253
251
|
content,
|
|
252
|
+
contactId: resolvedMember?.contact.id,
|
|
254
253
|
});
|
|
255
254
|
}
|
|
256
255
|
|
|
@@ -302,6 +301,13 @@ export async function handleChannelInbound(
|
|
|
302
301
|
}
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
// Track contact interaction only for genuinely new messages (not webhook
|
|
305
|
+
// retries). This was previously in ACL enforcement which runs before dedup,
|
|
306
|
+
// causing retries to inflate interaction counts.
|
|
307
|
+
if (!result.duplicate && resolvedMember) {
|
|
308
|
+
touchContactInteraction(resolvedMember.contact.id);
|
|
309
|
+
}
|
|
310
|
+
|
|
305
311
|
// external_conversation_bindings is assistant-agnostic. Restrict writes to
|
|
306
312
|
// self so assistant-scoped legacy routes do not overwrite each other's
|
|
307
313
|
// channel binding metadata for the same chat.
|
|
@@ -6,12 +6,10 @@
|
|
|
6
6
|
* Extracted from inbound-message-handler.ts to keep the top-level handler
|
|
7
7
|
* focused on orchestration.
|
|
8
8
|
*/
|
|
9
|
+
import { isInviteCodeRedemptionEnabled } from "../../../channels/config.js";
|
|
9
10
|
import type { ChannelId } from "../../../channels/types.js";
|
|
10
11
|
import { findContactChannel } from "../../../contacts/contact-store.js";
|
|
11
|
-
import {
|
|
12
|
-
touchChannelLastSeen,
|
|
13
|
-
touchContactInteraction,
|
|
14
|
-
} from "../../../contacts/contacts-write.js";
|
|
12
|
+
import { touchChannelLastSeen } from "../../../contacts/contacts-write.js";
|
|
15
13
|
import type {
|
|
16
14
|
ChannelStatus,
|
|
17
15
|
ContactChannel,
|
|
@@ -19,7 +17,12 @@ import type {
|
|
|
19
17
|
MemberStatus,
|
|
20
18
|
} from "../../../contacts/types.js";
|
|
21
19
|
import * as channelDeliveryStore from "../../../memory/channel-delivery-store.js";
|
|
20
|
+
import {
|
|
21
|
+
findByInviteCodeHash,
|
|
22
|
+
findByInviteCodeHashAnyChannel,
|
|
23
|
+
} from "../../../memory/invite-store.js";
|
|
22
24
|
import { getLogger } from "../../../util/logger.js";
|
|
25
|
+
import { hashVoiceCode } from "../../../util/voice-code.js";
|
|
23
26
|
import { notifyGuardianOfAccessRequest } from "../../access-request-helper.js";
|
|
24
27
|
import {
|
|
25
28
|
createOutboundSession,
|
|
@@ -27,9 +30,12 @@ import {
|
|
|
27
30
|
getPendingChallenge,
|
|
28
31
|
resolveBootstrapToken,
|
|
29
32
|
} from "../../channel-guardian-service.js";
|
|
30
|
-
import {
|
|
33
|
+
import { getInviteAdapterRegistry } from "../../channel-invite-transport.js";
|
|
31
34
|
import { deliverChannelReply } from "../../gateway-client.js";
|
|
32
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
redeemInvite,
|
|
37
|
+
redeemInviteByCode,
|
|
38
|
+
} from "../../invite-redemption-service.js";
|
|
33
39
|
import { getInviteRedemptionReply } from "../../invite-redemption-templates.js";
|
|
34
40
|
|
|
35
41
|
const log = getLogger("runtime-http");
|
|
@@ -149,8 +155,8 @@ export async function enforceIngressAcl(
|
|
|
149
155
|
!Array.isArray(rawCommandIntentForAcl)
|
|
150
156
|
? (rawCommandIntentForAcl as Record<string, unknown>)
|
|
151
157
|
: undefined;
|
|
152
|
-
const
|
|
153
|
-
const inviteToken =
|
|
158
|
+
const inviteAdapter = getInviteAdapterRegistry().get(sourceChannel);
|
|
159
|
+
const inviteToken = inviteAdapter?.extractInboundToken?.({
|
|
154
160
|
commandIntent: commandIntentForAcl,
|
|
155
161
|
content: trimmedContent,
|
|
156
162
|
sourceMetadata,
|
|
@@ -256,6 +262,32 @@ export async function enforceIngressAcl(
|
|
|
256
262
|
};
|
|
257
263
|
}
|
|
258
264
|
|
|
265
|
+
// ── 6-digit invite code intercept (non-member) ──
|
|
266
|
+
// On channels with codeRedemptionEnabled, a bare 6-digit message may be
|
|
267
|
+
// an invite code. Attempt redemption; on failure (no matching code) fall
|
|
268
|
+
// through to normal processing — the number may be a regular message.
|
|
269
|
+
if (denyNonMember && /^\d{6}$/.test(trimmedContent)) {
|
|
270
|
+
const codeInterceptResult = await handleInviteCodeIntercept({
|
|
271
|
+
code: trimmedContent,
|
|
272
|
+
sourceChannel,
|
|
273
|
+
externalChatId: conversationExternalId,
|
|
274
|
+
externalMessageId,
|
|
275
|
+
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
276
|
+
senderName: actorDisplayName,
|
|
277
|
+
senderUsername: actorUsername,
|
|
278
|
+
replyCallbackUrl,
|
|
279
|
+
bearerToken: mintBearerToken(),
|
|
280
|
+
assistantId,
|
|
281
|
+
canonicalAssistantId,
|
|
282
|
+
});
|
|
283
|
+
if (codeInterceptResult)
|
|
284
|
+
return {
|
|
285
|
+
resolvedMember: null,
|
|
286
|
+
earlyResponse: codeInterceptResult,
|
|
287
|
+
guardianVerifyCode,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
259
291
|
if (denyNonMember) {
|
|
260
292
|
log.info(
|
|
261
293
|
{ sourceChannel, externalUserId: canonicalSenderId },
|
|
@@ -462,6 +494,31 @@ export async function enforceIngressAcl(
|
|
|
462
494
|
};
|
|
463
495
|
}
|
|
464
496
|
|
|
497
|
+
// ── 6-digit invite code intercept (inactive member) ──
|
|
498
|
+
// Same as the non-member branch: codes can reactivate revoked/pending
|
|
499
|
+
// members. Non-matching codes fall through to normal processing.
|
|
500
|
+
if (denyInactiveMember && /^\d{6}$/.test(trimmedContent)) {
|
|
501
|
+
const codeInterceptResult = await handleInviteCodeIntercept({
|
|
502
|
+
code: trimmedContent,
|
|
503
|
+
sourceChannel,
|
|
504
|
+
externalChatId: conversationExternalId,
|
|
505
|
+
externalMessageId,
|
|
506
|
+
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
507
|
+
senderName: actorDisplayName,
|
|
508
|
+
senderUsername: actorUsername,
|
|
509
|
+
replyCallbackUrl,
|
|
510
|
+
bearerToken: mintBearerToken(),
|
|
511
|
+
assistantId,
|
|
512
|
+
canonicalAssistantId,
|
|
513
|
+
});
|
|
514
|
+
if (codeInterceptResult)
|
|
515
|
+
return {
|
|
516
|
+
resolvedMember: null,
|
|
517
|
+
earlyResponse: codeInterceptResult,
|
|
518
|
+
guardianVerifyCode,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
465
522
|
if (denyInactiveMember) {
|
|
466
523
|
log.info(
|
|
467
524
|
{
|
|
@@ -635,9 +692,12 @@ export async function enforceIngressAcl(
|
|
|
635
692
|
};
|
|
636
693
|
}
|
|
637
694
|
|
|
638
|
-
// 'allow' or 'escalate' — update last seen
|
|
695
|
+
// 'allow' or 'escalate' — update last seen timestamp.
|
|
696
|
+
// touchContactInteraction is intentionally NOT called here because
|
|
697
|
+
// duplicate detection hasn't run yet. It's called in
|
|
698
|
+
// inbound-message-handler.ts after dedup so webhook retries don't
|
|
699
|
+
// inflate interaction counts.
|
|
639
700
|
touchChannelLastSeen(resolvedMember.channel.id);
|
|
640
|
-
touchContactInteraction(resolvedMember.contact.id);
|
|
641
701
|
}
|
|
642
702
|
}
|
|
643
703
|
|
|
@@ -796,6 +856,231 @@ async function handleInviteTokenIntercept(params: {
|
|
|
796
856
|
});
|
|
797
857
|
}
|
|
798
858
|
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
// 6-digit invite code intercept
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Handle a bare 6-digit message as a potential invite code redemption.
|
|
865
|
+
*
|
|
866
|
+
* Checks channel policy (codeRedemptionEnabled), attempts redemption via
|
|
867
|
+
* `redeemInviteByCode`, and returns a Response to short-circuit the handler
|
|
868
|
+
* on success. Returns `null` when the code does not match any active invite,
|
|
869
|
+
* allowing the message to fall through to normal processing.
|
|
870
|
+
*/
|
|
871
|
+
async function handleInviteCodeIntercept(params: {
|
|
872
|
+
code: string;
|
|
873
|
+
sourceChannel: ChannelId;
|
|
874
|
+
externalChatId: string;
|
|
875
|
+
externalMessageId: string;
|
|
876
|
+
senderExternalUserId?: string;
|
|
877
|
+
senderName?: string;
|
|
878
|
+
senderUsername?: string;
|
|
879
|
+
replyCallbackUrl?: string;
|
|
880
|
+
bearerToken?: string;
|
|
881
|
+
assistantId?: string;
|
|
882
|
+
canonicalAssistantId: string;
|
|
883
|
+
}): Promise<Response | null> {
|
|
884
|
+
const {
|
|
885
|
+
code,
|
|
886
|
+
sourceChannel,
|
|
887
|
+
externalChatId,
|
|
888
|
+
externalMessageId,
|
|
889
|
+
senderExternalUserId,
|
|
890
|
+
senderName,
|
|
891
|
+
senderUsername,
|
|
892
|
+
replyCallbackUrl,
|
|
893
|
+
bearerToken,
|
|
894
|
+
assistantId,
|
|
895
|
+
canonicalAssistantId,
|
|
896
|
+
} = params;
|
|
897
|
+
|
|
898
|
+
// Skip channels that don't support code redemption
|
|
899
|
+
if (!isInviteCodeRedemptionEnabled(sourceChannel)) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Pre-check: verify a matching invite exists before committing to handle
|
|
904
|
+
// this message. A bare 6-digit number may be a regular message, so we
|
|
905
|
+
// must not record inbound dedup until we know the code maps to an invite.
|
|
906
|
+
const codeHash = hashVoiceCode(code);
|
|
907
|
+
const candidateInvite = findByInviteCodeHash(codeHash, sourceChannel);
|
|
908
|
+
if (!candidateInvite) {
|
|
909
|
+
// The code doesn't match any invite on this channel. Before falling
|
|
910
|
+
// through to normal processing, check if it matches on a different
|
|
911
|
+
// channel — if so, inform the user instead of silently ignoring it.
|
|
912
|
+
const crossChannelInvite = findByInviteCodeHashAnyChannel(codeHash);
|
|
913
|
+
if (crossChannelInvite) {
|
|
914
|
+
// Record inbound for dedup tracking — without this, duplicate webhook
|
|
915
|
+
// deliveries would re-enter ACL and send the mismatch reply again.
|
|
916
|
+
const dedupResult = channelDeliveryStore.recordInbound(
|
|
917
|
+
sourceChannel,
|
|
918
|
+
externalChatId,
|
|
919
|
+
externalMessageId,
|
|
920
|
+
{ assistantId: canonicalAssistantId },
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
if (dedupResult.duplicate) {
|
|
924
|
+
return Response.json({
|
|
925
|
+
accepted: true,
|
|
926
|
+
duplicate: true,
|
|
927
|
+
eventId: dedupResult.eventId,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const mismatchReply = "This invite is not valid for this channel.";
|
|
932
|
+
if (replyCallbackUrl) {
|
|
933
|
+
try {
|
|
934
|
+
await deliverChannelReply(
|
|
935
|
+
replyCallbackUrl,
|
|
936
|
+
{
|
|
937
|
+
chatId: externalChatId,
|
|
938
|
+
text: mismatchReply,
|
|
939
|
+
assistantId,
|
|
940
|
+
},
|
|
941
|
+
bearerToken,
|
|
942
|
+
);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
log.error(
|
|
945
|
+
{ err, externalChatId },
|
|
946
|
+
"Failed to deliver invite code channel-mismatch reply",
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
channelDeliveryStore.markProcessed(dedupResult.eventId);
|
|
951
|
+
return Response.json({
|
|
952
|
+
accepted: true,
|
|
953
|
+
eventId: dedupResult.eventId,
|
|
954
|
+
denied: true,
|
|
955
|
+
inviteRedemption: "channel_mismatch",
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Record the inbound event for dedup tracking BEFORE performing redemption,
|
|
962
|
+
// matching the token intercept path. Without this, duplicate webhook
|
|
963
|
+
// deliveries could slip through: the first delivery redeems the invite and
|
|
964
|
+
// activates membership, then a retry finds an active member, passes ACL,
|
|
965
|
+
// and the raw 6-digit message leaks into the agent pipeline.
|
|
966
|
+
const dedupResult = channelDeliveryStore.recordInbound(
|
|
967
|
+
sourceChannel,
|
|
968
|
+
externalChatId,
|
|
969
|
+
externalMessageId,
|
|
970
|
+
{ assistantId: canonicalAssistantId },
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
if (dedupResult.duplicate) {
|
|
974
|
+
return Response.json({
|
|
975
|
+
accepted: true,
|
|
976
|
+
duplicate: true,
|
|
977
|
+
eventId: dedupResult.eventId,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
let outcome: ReturnType<typeof redeemInviteByCode>;
|
|
982
|
+
try {
|
|
983
|
+
outcome = redeemInviteByCode({
|
|
984
|
+
code,
|
|
985
|
+
sourceChannel,
|
|
986
|
+
externalUserId: senderExternalUserId,
|
|
987
|
+
externalChatId,
|
|
988
|
+
displayName: senderName,
|
|
989
|
+
username: senderUsername,
|
|
990
|
+
assistantId: canonicalAssistantId,
|
|
991
|
+
});
|
|
992
|
+
} catch (err) {
|
|
993
|
+
// Redemption threw — roll back the dedup record so webhook retries
|
|
994
|
+
// can re-attempt instead of short-circuiting as duplicates.
|
|
995
|
+
log.error(
|
|
996
|
+
{ err, sourceChannel, externalChatId },
|
|
997
|
+
"Invite code intercept: redemption threw, rolling back dedup record",
|
|
998
|
+
);
|
|
999
|
+
channelDeliveryStore.deleteInbound(dedupResult.eventId);
|
|
1000
|
+
throw err;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
log.info(
|
|
1004
|
+
{
|
|
1005
|
+
sourceChannel,
|
|
1006
|
+
externalChatId,
|
|
1007
|
+
ok: outcome.ok,
|
|
1008
|
+
type: outcome.ok ? outcome.type : undefined,
|
|
1009
|
+
reason: !outcome.ok ? outcome.reason : undefined,
|
|
1010
|
+
},
|
|
1011
|
+
"Invite code intercept: redemption result",
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
// already_member: deliver acknowledgement and short-circuit
|
|
1015
|
+
if (outcome.ok && outcome.type === "already_member") {
|
|
1016
|
+
const replyText = getInviteRedemptionReply(outcome);
|
|
1017
|
+
if (replyCallbackUrl) {
|
|
1018
|
+
try {
|
|
1019
|
+
await deliverChannelReply(
|
|
1020
|
+
replyCallbackUrl,
|
|
1021
|
+
{
|
|
1022
|
+
chatId: externalChatId,
|
|
1023
|
+
text: replyText,
|
|
1024
|
+
assistantId,
|
|
1025
|
+
},
|
|
1026
|
+
bearerToken,
|
|
1027
|
+
);
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
log.error(
|
|
1030
|
+
{ err, externalChatId },
|
|
1031
|
+
"Failed to deliver invite code already-member reply",
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
channelDeliveryStore.markProcessed(dedupResult.eventId);
|
|
1036
|
+
return Response.json({
|
|
1037
|
+
accepted: true,
|
|
1038
|
+
eventId: dedupResult.eventId,
|
|
1039
|
+
inviteRedemption: "already_member",
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const replyText = getInviteRedemptionReply(outcome);
|
|
1044
|
+
|
|
1045
|
+
if (replyCallbackUrl) {
|
|
1046
|
+
try {
|
|
1047
|
+
await deliverChannelReply(
|
|
1048
|
+
replyCallbackUrl,
|
|
1049
|
+
{
|
|
1050
|
+
chatId: externalChatId,
|
|
1051
|
+
text: replyText,
|
|
1052
|
+
assistantId,
|
|
1053
|
+
},
|
|
1054
|
+
bearerToken,
|
|
1055
|
+
);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
log.error(
|
|
1058
|
+
{ err, externalChatId },
|
|
1059
|
+
"Failed to deliver invite code redemption reply",
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (outcome.ok && outcome.type === "redeemed") {
|
|
1065
|
+
channelDeliveryStore.markProcessed(dedupResult.eventId);
|
|
1066
|
+
return Response.json({
|
|
1067
|
+
accepted: true,
|
|
1068
|
+
eventId: dedupResult.eventId,
|
|
1069
|
+
inviteRedemption: "redeemed",
|
|
1070
|
+
memberId: outcome.memberId,
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Failed redemption (expired, revoked, etc.) — inform and deny
|
|
1075
|
+
channelDeliveryStore.markProcessed(dedupResult.eventId);
|
|
1076
|
+
return Response.json({
|
|
1077
|
+
accepted: true,
|
|
1078
|
+
eventId: dedupResult.eventId,
|
|
1079
|
+
denied: true,
|
|
1080
|
+
inviteRedemption: !outcome.ok ? outcome.reason : undefined,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
799
1084
|
// ---------------------------------------------------------------------------
|
|
800
1085
|
// Slack verification challenge
|
|
801
1086
|
// ---------------------------------------------------------------------------
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* focused on orchestration.
|
|
9
9
|
*/
|
|
10
10
|
import type { ChannelId, InterfaceId } from "../../../channels/types.js";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveGuardianName } from "../../../config/user-reference.js";
|
|
12
|
+
import { findGuardianForChannel } from "../../../contacts/contact-store.js";
|
|
12
13
|
import type { TrustContext } from "../../../daemon/session-runtime-assembly.js";
|
|
13
14
|
import * as channelDeliveryStore from "../../../memory/channel-delivery-store.js";
|
|
14
15
|
import {
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
getApprovalInfoByConversation,
|
|
24
25
|
getChannelApprovalPrompt,
|
|
25
26
|
} from "../../channel-approvals.js";
|
|
26
|
-
import { getGuardianBinding } from "../../channel-guardian-service.js";
|
|
27
27
|
import { deliverChannelReply } from "../../gateway-client.js";
|
|
28
28
|
import type {
|
|
29
29
|
ApprovalCopyGenerator,
|
|
@@ -441,41 +441,6 @@ export function startPendingApprovalPromptWatcher(params: {
|
|
|
441
441
|
};
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
// Guardian display name resolver
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Resolve a human-readable guardian name from the guardian binding metadata.
|
|
450
|
-
* Returns the display name, username (prefixed with @), or undefined if
|
|
451
|
-
* no name is available.
|
|
452
|
-
*/
|
|
453
|
-
export function resolveGuardianDisplayName(
|
|
454
|
-
assistantId: string,
|
|
455
|
-
sourceChannel: ChannelId,
|
|
456
|
-
): string | undefined {
|
|
457
|
-
const binding = getGuardianBinding(assistantId, sourceChannel);
|
|
458
|
-
if (!binding?.metadataJson) return undefined;
|
|
459
|
-
try {
|
|
460
|
-
const parsed = JSON.parse(binding.metadataJson) as Record<string, unknown>;
|
|
461
|
-
if (
|
|
462
|
-
typeof parsed.displayName === "string" &&
|
|
463
|
-
parsed.displayName.trim().length > 0
|
|
464
|
-
) {
|
|
465
|
-
return parsed.displayName.trim();
|
|
466
|
-
}
|
|
467
|
-
if (
|
|
468
|
-
typeof parsed.username === "string" &&
|
|
469
|
-
parsed.username.trim().length > 0
|
|
470
|
-
) {
|
|
471
|
-
return `@${parsed.username.trim()}`;
|
|
472
|
-
}
|
|
473
|
-
} catch {
|
|
474
|
-
// ignore malformed metadata
|
|
475
|
-
}
|
|
476
|
-
return undefined;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
444
|
// ---------------------------------------------------------------------------
|
|
480
445
|
// Trusted contact approval notifier
|
|
481
446
|
// ---------------------------------------------------------------------------
|
|
@@ -542,11 +507,13 @@ export function startTrustedContactApprovalNotifier(params: {
|
|
|
542
507
|
|
|
543
508
|
if (info && !globalNotifiedApprovalRequestIds.has(info.requestId)) {
|
|
544
509
|
globalNotifiedApprovalRequestIds.set(info.requestId, conversationId);
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
510
|
+
const guardian = findGuardianForChannel(
|
|
511
|
+
sourceChannel,
|
|
512
|
+
assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
513
|
+
);
|
|
514
|
+
const guardianName = resolveGuardianName(
|
|
515
|
+
guardian?.contact.displayName,
|
|
516
|
+
);
|
|
550
517
|
const waitingText = `Waiting for ${guardianName}'s approval...`;
|
|
551
518
|
try {
|
|
552
519
|
await deliverChannelReply(
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* focused on orchestration.
|
|
12
12
|
*/
|
|
13
13
|
import type { ChannelId } from "../../../channels/types.js";
|
|
14
|
+
import { touchContactInteraction } from "../../../contacts/contacts-write.js";
|
|
14
15
|
import * as channelDeliveryStore from "../../../memory/channel-delivery-store.js";
|
|
15
16
|
import * as conversationStore from "../../../memory/conversation-store.js";
|
|
16
17
|
import { getLogger } from "../../../util/logger.js";
|
|
@@ -29,6 +30,8 @@ export interface EditInterceptParams {
|
|
|
29
30
|
canonicalAssistantId: string;
|
|
30
31
|
assistantId: string;
|
|
31
32
|
content: string | undefined;
|
|
33
|
+
/** Contact ID for interaction tracking; omitted when the sender has no resolved member. */
|
|
34
|
+
contactId?: string;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
@@ -49,6 +52,7 @@ export async function handleEditIntercept(
|
|
|
49
52
|
canonicalAssistantId,
|
|
50
53
|
assistantId,
|
|
51
54
|
content,
|
|
55
|
+
contactId,
|
|
52
56
|
} = params;
|
|
53
57
|
|
|
54
58
|
// Dedup the edit event itself (retried edited_message webhooks)
|
|
@@ -67,6 +71,12 @@ export async function handleEditIntercept(
|
|
|
67
71
|
});
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
// Track contact interaction only for genuinely new edit events (not webhook
|
|
75
|
+
// retries), matching the pattern used for the normal message path.
|
|
76
|
+
if (contactId) {
|
|
77
|
+
touchContactInteraction(contactId);
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
// Retry lookup a few times -- the original message may still be processing
|
|
71
81
|
// (linkMessage hasn't been called yet). Short backoff avoids losing edits
|
|
72
82
|
// that arrive while the original agent loop is in progress.
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
startOutbound,
|
|
49
49
|
} from "../guardian-outbound-actions.js";
|
|
50
50
|
import { httpError } from "../http-errors.js";
|
|
51
|
+
import type { RouteDefinition } from "../http-router.js";
|
|
51
52
|
import { guardianVerificationLimiter } from "../verification-rate-limiter.js";
|
|
52
53
|
|
|
53
54
|
/**
|
|
@@ -296,3 +297,85 @@ export async function handleCancelOutbound(req: Request): Promise<Response> {
|
|
|
296
297
|
const status = result.success ? 200 : 400;
|
|
297
298
|
return Response.json(result, { status });
|
|
298
299
|
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Route definitions
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
export function integrationRouteDefinitions(): RouteDefinition[] {
|
|
306
|
+
return [
|
|
307
|
+
// Telegram
|
|
308
|
+
{
|
|
309
|
+
endpoint: "integrations/telegram/config",
|
|
310
|
+
method: "GET",
|
|
311
|
+
handler: () => handleGetTelegramConfig(),
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
endpoint: "integrations/telegram/config",
|
|
315
|
+
method: "POST",
|
|
316
|
+
handler: async ({ req }) => handleSetTelegramConfig(req),
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
endpoint: "integrations/telegram/config",
|
|
320
|
+
method: "DELETE",
|
|
321
|
+
handler: async () => handleClearTelegramConfig(),
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
endpoint: "integrations/telegram/commands",
|
|
325
|
+
method: "POST",
|
|
326
|
+
handler: async ({ req }) => handleSetTelegramCommands(req),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
endpoint: "integrations/telegram/setup",
|
|
330
|
+
method: "POST",
|
|
331
|
+
handler: async ({ req }) => handleSetupTelegram(req),
|
|
332
|
+
},
|
|
333
|
+
// Slack
|
|
334
|
+
{
|
|
335
|
+
endpoint: "integrations/slack/channel/config",
|
|
336
|
+
method: "GET",
|
|
337
|
+
handler: () => handleGetSlackChannelConfig(),
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
endpoint: "integrations/slack/channel/config",
|
|
341
|
+
method: "POST",
|
|
342
|
+
handler: async ({ req }) => handleSetSlackChannelConfig(req),
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
endpoint: "integrations/slack/channel/config",
|
|
346
|
+
method: "DELETE",
|
|
347
|
+
handler: () => handleClearSlackChannelConfig(),
|
|
348
|
+
},
|
|
349
|
+
// Guardian
|
|
350
|
+
{
|
|
351
|
+
endpoint: "integrations/guardian/challenge",
|
|
352
|
+
method: "POST",
|
|
353
|
+
handler: async ({ req }) => handleCreateGuardianChallenge(req),
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
endpoint: "integrations/guardian/status",
|
|
357
|
+
method: "GET",
|
|
358
|
+
handler: ({ url }) => handleGetGuardianStatus(url),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
endpoint: "integrations/guardian/revoke",
|
|
362
|
+
method: "POST",
|
|
363
|
+
handler: async ({ req }) => handleRevokeGuardian(req),
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
endpoint: "integrations/guardian/outbound/start",
|
|
367
|
+
method: "POST",
|
|
368
|
+
handler: async ({ req }) => handleStartOutbound(req),
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
endpoint: "integrations/guardian/outbound/resend",
|
|
372
|
+
method: "POST",
|
|
373
|
+
handler: async ({ req }) => handleResendOutbound(req),
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
endpoint: "integrations/guardian/outbound/cancel",
|
|
377
|
+
method: "POST",
|
|
378
|
+
handler: async ({ req }) => handleCancelOutbound(req),
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* POST /v1/contacts/invites/redeem — redeem an invite (token or voice code)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { RouteDefinition } from "../http-router.js";
|
|
11
12
|
import {
|
|
12
13
|
createIngressInvite,
|
|
13
14
|
listIngressInvites,
|
|
@@ -51,6 +52,7 @@ export async function handleCreateInvite(req: Request): Promise<Response> {
|
|
|
51
52
|
note: body.note as string | undefined,
|
|
52
53
|
maxUses: body.maxUses as number | undefined,
|
|
53
54
|
expiresInMs: body.expiresInMs as number | undefined,
|
|
55
|
+
contactName: body.contactName as string | undefined,
|
|
54
56
|
expectedExternalUserId: body.expectedExternalUserId as string | undefined,
|
|
55
57
|
voiceCodeDigits: body.voiceCodeDigits as number | undefined,
|
|
56
58
|
friendName: body.friendName as string | undefined,
|
|
@@ -138,3 +140,33 @@ export async function handleRedeemInvite(req: Request): Promise<Response> {
|
|
|
138
140
|
}
|
|
139
141
|
return Response.json({ ok: true, invite: result.data });
|
|
140
142
|
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Route definitions
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
export function inviteRouteDefinitions(): RouteDefinition[] {
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
endpoint: "contacts/invites",
|
|
152
|
+
method: "GET",
|
|
153
|
+
handler: ({ url }) => handleListInvites(url),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
endpoint: "contacts/invites",
|
|
157
|
+
method: "POST",
|
|
158
|
+
handler: async ({ req }) => handleCreateInvite(req),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
endpoint: "contacts/invites/redeem",
|
|
162
|
+
method: "POST",
|
|
163
|
+
handler: async ({ req }) => handleRedeemInvite(req),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
endpoint: "contacts/invites/:id",
|
|
167
|
+
method: "DELETE",
|
|
168
|
+
policyKey: "contacts/invites",
|
|
169
|
+
handler: ({ params }) => handleRevokeInvite(params.id),
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|