@vellumai/assistant 0.3.27 → 0.4.0
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 +81 -4
- package/Dockerfile +2 -2
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +9 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +119 -0
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/bundled-asset.test.ts +107 -0
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +21 -19
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/mcp-cli.test.ts +77 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +212 -36
- package/src/__tests__/notification-decision-fallback.test.ts +63 -3
- package/src/__tests__/notification-decision-strategy.test.ts +78 -0
- package/src/__tests__/notification-guardian-path.test.ts +15 -15
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/onboarding-template-contract.test.ts +116 -21
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +126 -59
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +358 -24
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +22 -16
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +33 -6
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +68 -326
- package/src/daemon/session-runtime-assembly.ts +119 -25
- package/src/daemon/session-tool-setup.ts +3 -2
- package/src/daemon/session.ts +4 -3
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +586 -0
- package/src/memory/channel-guardian-store.ts +2 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +56 -0
- package/src/notifications/copy-composer.ts +31 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +173 -0
- package/src/runtime/actor-trust-resolver.ts +221 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -71
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +717 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +20 -2
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +205 -529
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +53 -10
- package/src/tools/types.ts +13 -2
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -6,40 +6,28 @@
|
|
|
6
6
|
*/
|
|
7
7
|
// Side-effect import: registers the Telegram invite transport adapter so
|
|
8
8
|
// getTransport('telegram') resolves at runtime.
|
|
9
|
-
import { answerCall } from '../../calls/call-domain.js';
|
|
10
|
-
import { isTerminalState } from '../../calls/call-state-machine.js';
|
|
11
|
-
import { getCallSession } from '../../calls/call-store.js';
|
|
12
9
|
import type { ChannelId, InterfaceId } from '../../channels/types.js';
|
|
13
10
|
import { CHANNEL_IDS, INTERFACE_IDS, isChannelId, parseInterfaceId } from '../../channels/types.js';
|
|
14
11
|
import { getGatewayInternalBaseUrl } from '../../config/env.js';
|
|
15
12
|
import { RESEND_COOLDOWN_MS } from '../../daemon/handlers/config-channels.js';
|
|
16
13
|
import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
17
|
-
import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
|
|
18
14
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
createCanonicalGuardianRequest,
|
|
16
|
+
listCanonicalGuardianRequests,
|
|
17
|
+
listPendingCanonicalGuardianRequestsByDestinationChat,
|
|
18
|
+
} from '../../memory/canonical-guardian-store.js';
|
|
19
|
+
import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
|
|
22
20
|
import { recordConversationSeenSignal } from '../../memory/conversation-attention-store.js';
|
|
23
21
|
import * as conversationStore from '../../memory/conversation-store.js';
|
|
24
22
|
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
25
|
-
import {
|
|
26
|
-
finalizeFollowup,
|
|
27
|
-
getDeliveriesByRequestId,
|
|
28
|
-
getExpiredDeliveriesByDestination,
|
|
29
|
-
getFollowupDeliveriesByDestination,
|
|
30
|
-
getGuardianActionRequest,
|
|
31
|
-
getPendingDeliveriesByDestination,
|
|
32
|
-
getPendingRequestByCallSessionId,
|
|
33
|
-
progressFollowupState,
|
|
34
|
-
resolveGuardianActionRequest,
|
|
35
|
-
startFollowupFromExpiredRequest,
|
|
36
|
-
} from '../../memory/guardian-action-store.js';
|
|
37
23
|
import { findMember, updateLastSeen, upsertMember } from '../../memory/ingress-member-store.js';
|
|
38
24
|
import { emitNotificationSignal } from '../../notifications/emit-signal.js';
|
|
39
25
|
import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
26
|
+
import { canonicalizeInboundIdentity } from '../../util/canonicalize-identity.js';
|
|
40
27
|
import { IngressBlockedError } from '../../util/errors.js';
|
|
41
28
|
import { getLogger } from '../../util/logger.js';
|
|
42
29
|
import { readHttpToken } from '../../util/platform.js';
|
|
30
|
+
import { notifyGuardianOfAccessRequest } from '../access-request-helper.js';
|
|
43
31
|
import {
|
|
44
32
|
buildApprovalUIMetadata,
|
|
45
33
|
getApprovalInfoByConversation,
|
|
@@ -58,11 +46,8 @@ import {
|
|
|
58
46
|
} from '../channel-guardian-service.js';
|
|
59
47
|
import { getTransport } from '../channel-invite-transport.js';
|
|
60
48
|
import { deliverChannelReply } from '../gateway-client.js';
|
|
61
|
-
import { processGuardianFollowUpTurn } from '../guardian-action-conversation-turn.js';
|
|
62
|
-
import { executeFollowupAction } from '../guardian-action-followup-executor.js';
|
|
63
|
-
import { tryMintGuardianActionGrant } from '../guardian-action-grant-minter.js';
|
|
64
|
-
import { composeGuardianActionMessageGenerative } from '../guardian-action-message-composer.js';
|
|
65
49
|
import { resolveGuardianContext } from '../guardian-context-resolver.js';
|
|
50
|
+
import { routeGuardianReply } from '../guardian-reply-router.js';
|
|
66
51
|
import {
|
|
67
52
|
composeChannelVerifyReply,
|
|
68
53
|
composeVerificationTelegram,
|
|
@@ -91,6 +76,7 @@ import { handleApprovalInterception } from './guardian-approval-interception.js'
|
|
|
91
76
|
import { deliverGeneratedApprovalPrompt } from './guardian-approval-prompt.js';
|
|
92
77
|
|
|
93
78
|
import '../channel-invite-transports/telegram.js';
|
|
79
|
+
import '../channel-invite-transports/voice.js';
|
|
94
80
|
|
|
95
81
|
const log = getLogger('runtime-http');
|
|
96
82
|
|
|
@@ -115,8 +101,8 @@ export async function handleChannelInbound(
|
|
|
115
101
|
gatewayOriginSecret?: string,
|
|
116
102
|
approvalCopyGenerator?: ApprovalCopyGenerator,
|
|
117
103
|
approvalConversationGenerator?: ApprovalConversationGenerator,
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
_guardianActionCopyGenerator?: GuardianActionCopyGenerator,
|
|
105
|
+
_guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator,
|
|
120
106
|
): Promise<Response> {
|
|
121
107
|
// Reject requests that lack valid gateway-origin proof. This ensures
|
|
122
108
|
// channel inbound messages can only arrive via the gateway (which
|
|
@@ -202,6 +188,28 @@ export async function handleChannelInbound(
|
|
|
202
188
|
log.debug({ raw: assistantId, canonical: canonicalAssistantId }, 'Canonicalized channel assistant ID');
|
|
203
189
|
}
|
|
204
190
|
|
|
191
|
+
// Coerce senderExternalUserId to a string at the boundary — the field
|
|
192
|
+
// comes from unvalidated JSON and may be a number, object, or other
|
|
193
|
+
// non-string type. Non-string truthy values would throw inside
|
|
194
|
+
// canonicalizeInboundIdentity when it calls .trim().
|
|
195
|
+
const rawSenderId = body.senderExternalUserId != null
|
|
196
|
+
? String(body.senderExternalUserId)
|
|
197
|
+
: undefined;
|
|
198
|
+
|
|
199
|
+
// Canonicalize the sender identity so all trust lookups, member matching,
|
|
200
|
+
// and guardian binding comparisons use a normalized form. Phone-like
|
|
201
|
+
// channels (sms, voice, whatsapp) are normalized to E.164; non-phone
|
|
202
|
+
// channels pass through the platform-stable ID unchanged.
|
|
203
|
+
const canonicalSenderId = rawSenderId
|
|
204
|
+
? canonicalizeInboundIdentity(sourceChannel, rawSenderId)
|
|
205
|
+
: null;
|
|
206
|
+
|
|
207
|
+
// Track whether the original payload included a sender identity. A
|
|
208
|
+
// whitespace-only senderExternalUserId canonicalizes to null but still
|
|
209
|
+
// represents an explicit (malformed) identity claim that must enter the
|
|
210
|
+
// ACL deny path rather than bypassing it.
|
|
211
|
+
const hasSenderIdentityClaim = rawSenderId !== undefined;
|
|
212
|
+
|
|
205
213
|
// ── Ingress ACL enforcement ──
|
|
206
214
|
// Track the resolved member so the escalate branch can reference it after
|
|
207
215
|
// recordInbound (where we have a conversationId).
|
|
@@ -222,7 +230,7 @@ export async function handleChannelInbound(
|
|
|
222
230
|
typeof (rawCommandIntentForAcl as Record<string, unknown>).payload === 'string' &&
|
|
223
231
|
((rawCommandIntentForAcl as Record<string, unknown>).payload as string).startsWith('gv_');
|
|
224
232
|
|
|
225
|
-
// Parse invite token from /start
|
|
233
|
+
// Parse invite token from /start payloads using the channel transport
|
|
226
234
|
// adapter. The token is extracted once here so both the ACL bypass and
|
|
227
235
|
// the intercept handler can reference it without re-parsing.
|
|
228
236
|
const commandIntentForAcl = rawCommandIntentForAcl && typeof rawCommandIntentForAcl === 'object' && !Array.isArray(rawCommandIntentForAcl)
|
|
@@ -235,13 +243,18 @@ export async function handleChannelInbound(
|
|
|
235
243
|
sourceMetadata: body.sourceMetadata,
|
|
236
244
|
});
|
|
237
245
|
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
if (canonicalSenderId || hasSenderIdentityClaim) {
|
|
247
|
+
// Only perform member lookup when we have a usable canonical ID.
|
|
248
|
+
// Whitespace-only senders (hasSenderIdentityClaim=true but
|
|
249
|
+
// canonicalSenderId=null) skip the lookup and fall into the deny path.
|
|
250
|
+
if (canonicalSenderId) {
|
|
251
|
+
resolvedMember = findMember({
|
|
252
|
+
assistantId: canonicalAssistantId,
|
|
253
|
+
sourceChannel,
|
|
254
|
+
externalUserId: canonicalSenderId,
|
|
255
|
+
externalChatId,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
245
258
|
|
|
246
259
|
if (!resolvedMember) {
|
|
247
260
|
// Determine whether a verification-code bypass is warranted: only allow
|
|
@@ -279,7 +292,7 @@ export async function handleChannelInbound(
|
|
|
279
292
|
}
|
|
280
293
|
|
|
281
294
|
// ── Invite token intercept (non-member) ──
|
|
282
|
-
// /start
|
|
295
|
+
// /start invite deep links grant access without guardian approval.
|
|
283
296
|
// Intercept here — before the deny gate — so valid invites short-circuit
|
|
284
297
|
// the ACL rejection and never reach the agent pipeline.
|
|
285
298
|
if (inviteToken && denyNonMember) {
|
|
@@ -288,7 +301,7 @@ export async function handleChannelInbound(
|
|
|
288
301
|
sourceChannel,
|
|
289
302
|
externalChatId,
|
|
290
303
|
externalMessageId,
|
|
291
|
-
senderExternalUserId:
|
|
304
|
+
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
292
305
|
senderName: body.senderName,
|
|
293
306
|
senderUsername: body.senderUsername,
|
|
294
307
|
replyCallbackUrl: body.replyCallbackUrl,
|
|
@@ -300,21 +313,22 @@ export async function handleChannelInbound(
|
|
|
300
313
|
}
|
|
301
314
|
|
|
302
315
|
if (denyNonMember) {
|
|
303
|
-
log.info({ sourceChannel, externalUserId:
|
|
316
|
+
log.info({ sourceChannel, externalUserId: canonicalSenderId }, 'Ingress ACL: no member record, denying');
|
|
304
317
|
|
|
305
318
|
// Notify the guardian about the access request so they can approve/deny.
|
|
306
|
-
//
|
|
307
|
-
// request
|
|
319
|
+
// Uses the shared helper which handles guardian binding lookup,
|
|
320
|
+
// deduplication, canonical request creation, and notification emission.
|
|
308
321
|
let guardianNotified = false;
|
|
309
322
|
try {
|
|
310
|
-
|
|
323
|
+
const accessResult = notifyGuardianOfAccessRequest({
|
|
311
324
|
canonicalAssistantId,
|
|
312
325
|
sourceChannel,
|
|
313
326
|
externalChatId,
|
|
314
|
-
senderExternalUserId:
|
|
327
|
+
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
315
328
|
senderName: body.senderName,
|
|
316
329
|
senderUsername: body.senderUsername,
|
|
317
330
|
});
|
|
331
|
+
guardianNotified = accessResult.notified;
|
|
318
332
|
} catch (err) {
|
|
319
333
|
log.error({ err, sourceChannel, externalChatId }, 'Failed to notify guardian of access request');
|
|
320
334
|
}
|
|
@@ -373,7 +387,7 @@ export async function handleChannelInbound(
|
|
|
373
387
|
sourceChannel,
|
|
374
388
|
externalChatId,
|
|
375
389
|
externalMessageId,
|
|
376
|
-
senderExternalUserId:
|
|
390
|
+
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
377
391
|
senderName: body.senderName,
|
|
378
392
|
senderUsername: body.senderUsername,
|
|
379
393
|
replyCallbackUrl: body.replyCallbackUrl,
|
|
@@ -393,14 +407,15 @@ export async function handleChannelInbound(
|
|
|
393
407
|
let guardianNotified = false;
|
|
394
408
|
if (resolvedMember.status !== 'blocked') {
|
|
395
409
|
try {
|
|
396
|
-
|
|
410
|
+
const accessResult = notifyGuardianOfAccessRequest({
|
|
397
411
|
canonicalAssistantId,
|
|
398
412
|
sourceChannel,
|
|
399
413
|
externalChatId,
|
|
400
|
-
senderExternalUserId:
|
|
414
|
+
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
401
415
|
senderName: body.senderName,
|
|
402
416
|
senderUsername: body.senderUsername,
|
|
403
417
|
});
|
|
418
|
+
guardianNotified = accessResult.notified;
|
|
404
419
|
} catch (err) {
|
|
405
420
|
log.error({ err, sourceChannel, externalChatId }, 'Failed to notify guardian of access request');
|
|
406
421
|
}
|
|
@@ -570,7 +585,7 @@ export async function handleChannelInbound(
|
|
|
570
585
|
conversationId: result.conversationId,
|
|
571
586
|
sourceChannel,
|
|
572
587
|
externalChatId,
|
|
573
|
-
externalUserId:
|
|
588
|
+
externalUserId: canonicalSenderId ?? rawSenderId ?? null,
|
|
574
589
|
displayName: body.senderName ?? null,
|
|
575
590
|
username: body.senderUsername ?? null,
|
|
576
591
|
});
|
|
@@ -600,19 +615,16 @@ export async function handleChannelInbound(
|
|
|
600
615
|
assistantId: canonicalAssistantId,
|
|
601
616
|
});
|
|
602
617
|
|
|
603
|
-
|
|
604
|
-
|
|
618
|
+
createCanonicalGuardianRequest({
|
|
619
|
+
kind: 'tool_approval',
|
|
620
|
+
sourceType: 'channel',
|
|
621
|
+
sourceChannel,
|
|
605
622
|
conversationId: result.conversationId,
|
|
606
|
-
|
|
607
|
-
channel: sourceChannel,
|
|
608
|
-
requesterExternalUserId: body.senderExternalUserId ?? '',
|
|
609
|
-
requesterChatId: externalChatId,
|
|
623
|
+
requesterExternalUserId: canonicalSenderId ?? rawSenderId ?? undefined,
|
|
610
624
|
guardianExternalUserId: binding.guardianExternalUserId,
|
|
611
|
-
guardianChatId: binding.guardianDeliveryChatId,
|
|
612
625
|
toolName: 'ingress_message',
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
|
|
626
|
+
questionText: 'Ingress policy requires guardian approval',
|
|
627
|
+
expiresAt: new Date(Date.now() + GUARDIAN_APPROVAL_TTL_MS).toISOString(),
|
|
616
628
|
});
|
|
617
629
|
|
|
618
630
|
// Emit notification signal through the unified pipeline (fire-and-forget).
|
|
@@ -633,7 +645,7 @@ export async function handleChannelInbound(
|
|
|
633
645
|
conversationId: result.conversationId,
|
|
634
646
|
sourceChannel,
|
|
635
647
|
externalChatId,
|
|
636
|
-
senderIdentifier: body.senderName || body.senderUsername ||
|
|
648
|
+
senderIdentifier: body.senderName || body.senderUsername || rawSenderId || 'Unknown sender',
|
|
637
649
|
eventId: result.eventId,
|
|
638
650
|
},
|
|
639
651
|
dedupeKey: `escalation:${result.eventId}`,
|
|
@@ -679,14 +691,14 @@ export async function handleChannelInbound(
|
|
|
679
691
|
commandIntent?.type === 'start' &&
|
|
680
692
|
typeof commandIntent.payload === 'string' &&
|
|
681
693
|
(commandIntent.payload as string).startsWith('gv_') &&
|
|
682
|
-
|
|
694
|
+
rawSenderId
|
|
683
695
|
) {
|
|
684
696
|
const bootstrapToken = (commandIntent.payload as string).slice(3);
|
|
685
697
|
const bootstrapSession = resolveBootstrapToken(canonicalAssistantId, sourceChannel, bootstrapToken);
|
|
686
698
|
|
|
687
699
|
if (bootstrapSession && bootstrapSession.status === 'pending_bootstrap') {
|
|
688
700
|
// Bind the pending_bootstrap session to the sender's identity
|
|
689
|
-
bindSessionIdentity(bootstrapSession.id,
|
|
701
|
+
bindSessionIdentity(bootstrapSession.id, rawSenderId!, externalChatId);
|
|
690
702
|
|
|
691
703
|
// Transition bootstrap session to awaiting_response
|
|
692
704
|
updateSessionStatus(bootstrapSession.id, 'awaiting_response');
|
|
@@ -696,7 +708,7 @@ export async function handleChannelInbound(
|
|
|
696
708
|
const newSession = createOutboundSession({
|
|
697
709
|
assistantId: canonicalAssistantId,
|
|
698
710
|
channel: sourceChannel,
|
|
699
|
-
expectedExternalUserId:
|
|
711
|
+
expectedExternalUserId: rawSenderId!,
|
|
700
712
|
expectedChatId: externalChatId,
|
|
701
713
|
identityBindingStatus: 'bound',
|
|
702
714
|
destinationAddress: externalChatId,
|
|
@@ -748,13 +760,13 @@ export async function handleChannelInbound(
|
|
|
748
760
|
!result.duplicate &&
|
|
749
761
|
shouldInterceptVerification &&
|
|
750
762
|
guardianVerifyCode !== undefined &&
|
|
751
|
-
|
|
763
|
+
rawSenderId
|
|
752
764
|
) {
|
|
753
765
|
const verifyResult = validateAndConsumeChallenge(
|
|
754
766
|
canonicalAssistantId,
|
|
755
767
|
sourceChannel,
|
|
756
768
|
guardianVerifyCode,
|
|
757
|
-
|
|
769
|
+
canonicalSenderId ?? rawSenderId!,
|
|
758
770
|
externalChatId,
|
|
759
771
|
body.senderUsername,
|
|
760
772
|
body.senderName,
|
|
@@ -763,21 +775,37 @@ export async function handleChannelInbound(
|
|
|
763
775
|
const guardianVerifyOutcome: 'verified' | 'failed' = verifyResult.success ? 'verified' : 'failed';
|
|
764
776
|
|
|
765
777
|
if (verifyResult.success) {
|
|
778
|
+
const existingMember = (canonicalSenderId ?? rawSenderId)
|
|
779
|
+
? findMember({
|
|
780
|
+
assistantId: canonicalAssistantId,
|
|
781
|
+
sourceChannel,
|
|
782
|
+
externalUserId: canonicalSenderId ?? rawSenderId!,
|
|
783
|
+
externalChatId,
|
|
784
|
+
})
|
|
785
|
+
: null;
|
|
786
|
+
const memberMatchesSender = existingMember?.externalUserId
|
|
787
|
+
? canonicalizeInboundIdentity(sourceChannel, existingMember.externalUserId) === (canonicalSenderId ?? rawSenderId)
|
|
788
|
+
: false;
|
|
789
|
+
const preservedDisplayName = memberMatchesSender && existingMember?.displayName?.trim().length
|
|
790
|
+
? existingMember.displayName
|
|
791
|
+
: body.senderName;
|
|
792
|
+
|
|
766
793
|
upsertMember({
|
|
767
794
|
assistantId: canonicalAssistantId,
|
|
768
795
|
sourceChannel,
|
|
769
|
-
externalUserId:
|
|
796
|
+
externalUserId: canonicalSenderId ?? rawSenderId!,
|
|
770
797
|
externalChatId,
|
|
771
798
|
status: 'active',
|
|
772
799
|
policy: 'allow',
|
|
773
|
-
|
|
800
|
+
// Keep guardian-curated member name stable across re-verification.
|
|
801
|
+
displayName: preservedDisplayName,
|
|
774
802
|
username: body.senderUsername,
|
|
775
803
|
});
|
|
776
804
|
|
|
777
805
|
const verifyLogLabel = verifyResult.verificationType === 'trusted_contact'
|
|
778
806
|
? 'Trusted contact verified'
|
|
779
807
|
: 'Guardian verified';
|
|
780
|
-
log.info({ sourceChannel, externalUserId:
|
|
808
|
+
log.info({ sourceChannel, externalUserId: canonicalSenderId, verificationType: verifyResult.verificationType }, `${verifyLogLabel}: auto-upserted ingress member`);
|
|
781
809
|
|
|
782
810
|
// Emit activated signal when a trusted contact completes verification.
|
|
783
811
|
// Member record is persisted above before this event fires, satisfying
|
|
@@ -796,12 +824,12 @@ export async function handleChannelInbound(
|
|
|
796
824
|
},
|
|
797
825
|
contextPayload: {
|
|
798
826
|
sourceChannel,
|
|
799
|
-
externalUserId:
|
|
827
|
+
externalUserId: canonicalSenderId ?? rawSenderId!,
|
|
800
828
|
externalChatId,
|
|
801
829
|
senderName: body.senderName ?? null,
|
|
802
830
|
senderUsername: body.senderUsername ?? null,
|
|
803
831
|
},
|
|
804
|
-
dedupeKey: `trusted-contact:activated:${canonicalAssistantId}:${sourceChannel}:${
|
|
832
|
+
dedupeKey: `trusted-contact:activated:${canonicalAssistantId}:${sourceChannel}:${canonicalSenderId ?? rawSenderId!}`,
|
|
805
833
|
});
|
|
806
834
|
}
|
|
807
835
|
}
|
|
@@ -873,382 +901,126 @@ export async function handleChannelInbound(
|
|
|
873
901
|
});
|
|
874
902
|
}
|
|
875
903
|
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
// auto-match without requiring a code prefix. Callback payloads (inline
|
|
881
|
-
// button presses) are excluded — they should not be misclassified as
|
|
882
|
-
// guardian answers.
|
|
883
|
-
if (
|
|
884
|
-
!result.duplicate &&
|
|
885
|
-
!hasCallbackData &&
|
|
886
|
-
trimmedContent.length > 0 &&
|
|
887
|
-
body.senderExternalUserId &&
|
|
888
|
-
replyCallbackUrl
|
|
889
|
-
) {
|
|
890
|
-
// Gather deliveries across all states for this destination, filtered by sender identity
|
|
891
|
-
const allPending = getPendingDeliveriesByDestination(canonicalAssistantId, sourceChannel, externalChatId)
|
|
892
|
-
.filter((d) => d.destinationExternalUserId === body.senderExternalUserId);
|
|
893
|
-
const allFollowup = getFollowupDeliveriesByDestination(canonicalAssistantId, sourceChannel, externalChatId)
|
|
894
|
-
.filter((d) => d.destinationExternalUserId === body.senderExternalUserId);
|
|
895
|
-
const allExpired = getExpiredDeliveriesByDestination(canonicalAssistantId, sourceChannel, externalChatId)
|
|
896
|
-
.filter((d) => d.destinationExternalUserId === body.senderExternalUserId);
|
|
897
|
-
const totalActionable = allPending.length + allFollowup.length + allExpired.length;
|
|
898
|
-
|
|
899
|
-
if (totalActionable > 0) {
|
|
900
|
-
// ── Try to parse an explicit request code from the message ──
|
|
901
|
-
// Check all deliveries across states for a code prefix match, in priority order
|
|
902
|
-
type CodeMatch = { delivery: typeof allPending[0]; request: NonNullable<ReturnType<typeof getGuardianActionRequest>>; state: 'pending' | 'followup' | 'expired'; answerText: string };
|
|
903
|
-
let codeMatch: CodeMatch | null = null;
|
|
904
|
-
const upperContent = trimmedContent.toUpperCase();
|
|
905
|
-
const orderedSets: Array<{ deliveries: typeof allPending; state: 'pending' | 'followup' | 'expired' }> = [
|
|
906
|
-
{ deliveries: allPending, state: 'pending' },
|
|
907
|
-
{ deliveries: allFollowup, state: 'followup' },
|
|
908
|
-
{ deliveries: allExpired, state: 'expired' },
|
|
909
|
-
];
|
|
910
|
-
for (const { deliveries, state } of orderedSets) {
|
|
911
|
-
for (const d of deliveries) {
|
|
912
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
913
|
-
if (req && upperContent.startsWith(req.requestCode)) {
|
|
914
|
-
codeMatch = { delivery: d, request: req, state, answerText: trimmedContent.slice(req.requestCode.length).trim() };
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
if (codeMatch) break;
|
|
919
|
-
}
|
|
904
|
+
// Legacy voice guardian action interception removed — all guardian reply
|
|
905
|
+
// routing now flows through the canonical router below (routeGuardianReply),
|
|
906
|
+
// which handles request code matching, callback parsing, and NL classification
|
|
907
|
+
// against canonical_guardian_requests.
|
|
920
908
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
{},
|
|
933
|
-
guardianActionCopyGenerator,
|
|
934
|
-
);
|
|
935
|
-
try {
|
|
936
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: staleText, assistantId }, bearerToken);
|
|
937
|
-
} catch (err) {
|
|
938
|
-
log.error({ err, externalChatId }, 'Failed to deliver superseded terminal notice');
|
|
939
|
-
}
|
|
940
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'stale_superseded' });
|
|
941
|
-
}
|
|
942
|
-
}
|
|
909
|
+
// ── Actor role resolution ──
|
|
910
|
+
// Uses shared channel-agnostic resolution so all ingress paths classify
|
|
911
|
+
// guardian vs non-guardian actors the same way.
|
|
912
|
+
const guardianCtx: GuardianContext = resolveGuardianContext({
|
|
913
|
+
assistantId: canonicalAssistantId,
|
|
914
|
+
sourceChannel,
|
|
915
|
+
externalChatId,
|
|
916
|
+
senderExternalUserId: rawSenderId,
|
|
917
|
+
senderUsername: body.senderUsername,
|
|
918
|
+
senderDisplayName: body.senderName,
|
|
919
|
+
});
|
|
943
920
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
921
|
+
// Hoisted flag: set by the canonical guardian reply router when the invite
|
|
922
|
+
// handoff bypass fires. Prevents legacy approval interception from swallowing
|
|
923
|
+
// the message when other approvals are pending in the same chat.
|
|
924
|
+
let skipApprovalInterception = false;
|
|
948
925
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
926
|
+
// ── Canonical guardian reply router ──
|
|
927
|
+
// Attempts to route inbound messages through the canonical decision pipeline
|
|
928
|
+
// before falling through to the legacy approval interception. Handles
|
|
929
|
+
// deterministic callbacks (button presses), request code prefixes, and
|
|
930
|
+
// NL classification via the conversational approval engine.
|
|
931
|
+
if (
|
|
932
|
+
!result.duplicate &&
|
|
933
|
+
replyCallbackUrl &&
|
|
934
|
+
(trimmedContent.length > 0 || hasCallbackData) &&
|
|
935
|
+
rawSenderId &&
|
|
936
|
+
guardianCtx.trustClass === 'guardian'
|
|
937
|
+
) {
|
|
938
|
+
// Compute destination-scoped pending request hints so the router can
|
|
939
|
+
// discover canonical requests delivered to this chat even when the
|
|
940
|
+
// request lacks a guardianExternalUserId (e.g. voice-originated
|
|
941
|
+
// pending_question requests).
|
|
942
|
+
//
|
|
943
|
+
// When delivery-scoped matches exist, union them with any identity-
|
|
944
|
+
// based pending requests so that requests without delivery rows (e.g.
|
|
945
|
+
// tool_approval requests created inline) are not silently excluded.
|
|
946
|
+
// Pass undefined (not []) when there are zero combined results so the
|
|
947
|
+
// router's own identity-based fallback stays active.
|
|
948
|
+
const deliveryScopedPendingRequests = listPendingCanonicalGuardianRequestsByDestinationChat(
|
|
949
|
+
sourceChannel,
|
|
950
|
+
externalChatId,
|
|
951
|
+
);
|
|
952
|
+
let pendingRequestIds: string[] | undefined;
|
|
953
|
+
if (deliveryScopedPendingRequests.length > 0) {
|
|
954
|
+
const deliveryIds = new Set(deliveryScopedPendingRequests.map(r => r.id));
|
|
955
|
+
// Also include identity-based pending requests so we don't hide them
|
|
956
|
+
const identityId = canonicalSenderId ?? rawSenderId!;
|
|
957
|
+
const identityPending = listCanonicalGuardianRequests({
|
|
958
|
+
status: 'pending',
|
|
959
|
+
guardianExternalUserId: identityId,
|
|
960
|
+
});
|
|
961
|
+
for (const r of identityPending) {
|
|
962
|
+
deliveryIds.add(r.id);
|
|
963
963
|
}
|
|
964
|
+
pendingRequestIds = [...deliveryIds];
|
|
965
|
+
}
|
|
964
966
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
try {
|
|
985
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: unknownText, assistantId }, bearerToken);
|
|
986
|
-
} catch (err) {
|
|
987
|
-
log.error({ err, externalChatId }, 'Failed to deliver unknown code notice');
|
|
988
|
-
}
|
|
989
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'unknown_code' });
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
967
|
+
const routerResult = await routeGuardianReply({
|
|
968
|
+
messageText: trimmedContent,
|
|
969
|
+
channel: sourceChannel,
|
|
970
|
+
actor: {
|
|
971
|
+
externalUserId: canonicalSenderId ?? rawSenderId!,
|
|
972
|
+
channel: sourceChannel,
|
|
973
|
+
isTrusted: false,
|
|
974
|
+
},
|
|
975
|
+
conversationId: result.conversationId,
|
|
976
|
+
callbackData: body.callbackData,
|
|
977
|
+
pendingRequestIds,
|
|
978
|
+
approvalConversationGenerator,
|
|
979
|
+
channelDeliveryContext: {
|
|
980
|
+
replyCallbackUrl,
|
|
981
|
+
guardianChatId: externalChatId,
|
|
982
|
+
assistantId: canonicalAssistantId,
|
|
983
|
+
bearerToken,
|
|
984
|
+
},
|
|
985
|
+
});
|
|
993
986
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const codes = allDeliveries
|
|
998
|
-
.map((d) => { const req = getGuardianActionRequest(d.requestId); return req ? req.requestCode : null; })
|
|
999
|
-
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
1000
|
-
|
|
1001
|
-
// Choose the appropriate disambiguation scenario based on which states are present
|
|
1002
|
-
const disambiguationScenario = allPending.length > 0
|
|
1003
|
-
? 'guardian_pending_disambiguation' as const
|
|
1004
|
-
: allFollowup.length > 0
|
|
1005
|
-
? 'guardian_followup_disambiguation' as const
|
|
1006
|
-
: 'guardian_expired_disambiguation' as const;
|
|
1007
|
-
|
|
1008
|
-
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
1009
|
-
{ scenario: disambiguationScenario, requestCodes: codes, channel: sourceChannel },
|
|
1010
|
-
{ requiredKeywords: codes },
|
|
1011
|
-
guardianActionCopyGenerator,
|
|
1012
|
-
);
|
|
987
|
+
if (routerResult.consumed) {
|
|
988
|
+
// Deliver reply text if the router produced one
|
|
989
|
+
if (routerResult.replyText) {
|
|
1013
990
|
try {
|
|
1014
|
-
await deliverChannelReply(replyCallbackUrl, {
|
|
991
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
992
|
+
chatId: externalChatId,
|
|
993
|
+
text: routerResult.replyText,
|
|
994
|
+
assistantId: canonicalAssistantId,
|
|
995
|
+
}, bearerToken);
|
|
1015
996
|
} catch (err) {
|
|
1016
|
-
log.error({ err, externalChatId }, 'Failed to deliver
|
|
997
|
+
log.error({ err, externalChatId }, 'Failed to deliver canonical router reply');
|
|
1017
998
|
}
|
|
1018
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'disambiguation_sent' });
|
|
1019
999
|
}
|
|
1020
1000
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (!('ok' in answerResult) || !answerResult.ok) {
|
|
1030
|
-
const errorMsg = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
1031
|
-
log.warn({ callSessionId: request.callSessionId, error: errorMsg }, 'answerCall failed for guardian answer');
|
|
1032
|
-
try {
|
|
1033
|
-
const failureText = await composeGuardianActionMessageGenerative(
|
|
1034
|
-
{ scenario: 'guardian_answer_delivery_failed' },
|
|
1035
|
-
{},
|
|
1036
|
-
guardianActionCopyGenerator,
|
|
1037
|
-
);
|
|
1038
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: failureText, assistantId }, bearerToken);
|
|
1039
|
-
} catch (deliverErr) {
|
|
1040
|
-
log.error({ err: deliverErr, externalChatId }, 'Failed to deliver guardian answer failure notice');
|
|
1041
|
-
}
|
|
1042
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'answer_failed' });
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
const resolved = resolveGuardianActionRequest(request.id, answerText, sourceChannel, body.senderExternalUserId);
|
|
1046
|
-
|
|
1047
|
-
if (resolved) {
|
|
1048
|
-
await tryMintGuardianActionGrant({
|
|
1049
|
-
request,
|
|
1050
|
-
answerText,
|
|
1051
|
-
decisionChannel: sourceChannel,
|
|
1052
|
-
guardianExternalUserId: body.senderExternalUserId,
|
|
1053
|
-
approvalConversationGenerator,
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'resolved' });
|
|
1057
|
-
} else {
|
|
1058
|
-
const freshRequest = getGuardianActionRequest(request.id);
|
|
1059
|
-
const relayedText = await composeGuardianActionMessageGenerative(
|
|
1060
|
-
{ scenario: 'guardian_stale_answered' as const },
|
|
1061
|
-
{},
|
|
1062
|
-
guardianActionCopyGenerator,
|
|
1063
|
-
);
|
|
1064
|
-
try {
|
|
1065
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: relayedText, assistantId }, bearerToken);
|
|
1066
|
-
} catch (err) {
|
|
1067
|
-
log.error({ err, externalChatId }, 'Failed to deliver guardian action stale notice');
|
|
1068
|
-
}
|
|
1069
|
-
log.info(
|
|
1070
|
-
{ requestId: request.id, freshStatus: freshRequest?.status },
|
|
1071
|
-
'answerCall succeeded but resolveGuardianActionRequest returned null — informed guardian answer was relayed',
|
|
1072
|
-
);
|
|
1073
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'stale' });
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// ── FOLLOW-UP state handler ──
|
|
1078
|
-
if (state === 'followup' && request.followupState === 'awaiting_guardian_choice') {
|
|
1079
|
-
const turnResult = await processGuardianFollowUpTurn(
|
|
1080
|
-
{
|
|
1081
|
-
questionText: request.questionText,
|
|
1082
|
-
lateAnswerText: request.lateAnswerText ?? '',
|
|
1083
|
-
guardianReply: answerText,
|
|
1084
|
-
},
|
|
1085
|
-
guardianFollowUpConversationGenerator,
|
|
1086
|
-
);
|
|
1087
|
-
|
|
1088
|
-
let stateApplied = true;
|
|
1089
|
-
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
1090
|
-
stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !== null;
|
|
1091
|
-
} else if (turnResult.disposition === 'decline') {
|
|
1092
|
-
stateApplied = finalizeFollowup(request.id, 'declined') !== null;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
if (!stateApplied) {
|
|
1096
|
-
log.warn({ requestId: request.id, disposition: turnResult.disposition }, 'Follow-up state transition failed (already resolved)');
|
|
1097
|
-
const staleText = await composeGuardianActionMessageGenerative(
|
|
1098
|
-
{ scenario: 'guardian_stale_followup' as const },
|
|
1099
|
-
{},
|
|
1100
|
-
guardianActionCopyGenerator,
|
|
1101
|
-
);
|
|
1102
|
-
try {
|
|
1103
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: staleText, assistantId }, bearerToken);
|
|
1104
|
-
} catch (err) {
|
|
1105
|
-
log.error({ err, externalChatId }, 'Failed to deliver stale follow-up notice');
|
|
1106
|
-
}
|
|
1107
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianFollowUp: 'stale_ignored' });
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
try {
|
|
1111
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: turnResult.replyText, assistantId }, bearerToken);
|
|
1112
|
-
} catch (err) {
|
|
1113
|
-
log.error({ err, externalChatId }, 'Failed to deliver guardian follow-up conversation reply');
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
1117
|
-
void (async () => {
|
|
1118
|
-
try {
|
|
1119
|
-
const execResult = await executeFollowupAction(
|
|
1120
|
-
request.id,
|
|
1121
|
-
turnResult.disposition as 'call_back' | 'message_back',
|
|
1122
|
-
guardianActionCopyGenerator,
|
|
1123
|
-
);
|
|
1124
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: execResult.guardianReplyText, assistantId }, bearerToken);
|
|
1125
|
-
} catch (execErr) {
|
|
1126
|
-
log.error({ err: execErr, requestId: request.id }, 'Follow-up action execution or completion reply failed');
|
|
1127
|
-
}
|
|
1128
|
-
})();
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianFollowUp: turnResult.disposition });
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// ── EXPIRED state handler ──
|
|
1135
|
-
if (state === 'expired' && request.status === 'expired' && request.followupState === 'none') {
|
|
1136
|
-
// Superseded remap: if the request was superseded (not timed out
|
|
1137
|
-
// or disconnected), check whether the call is still active with a
|
|
1138
|
-
// current pending request. If so, remap the late approval to the
|
|
1139
|
-
// current request instead of entering the callback/message follow-up.
|
|
1140
|
-
if (request.expiredReason === 'superseded') {
|
|
1141
|
-
const callSession = getCallSession(request.callSessionId);
|
|
1142
|
-
const callStillActive = callSession && !isTerminalState(callSession.status);
|
|
1143
|
-
const currentPending = callStillActive
|
|
1144
|
-
? getPendingRequestByCallSessionId(request.callSessionId)
|
|
1145
|
-
: null;
|
|
1146
|
-
|
|
1147
|
-
if (callStillActive && currentPending) {
|
|
1148
|
-
const currentDeliveries = getDeliveriesByRequestId(currentPending.id);
|
|
1149
|
-
// When senderExternalUserId is present, verify the sender has a
|
|
1150
|
-
// matching delivery on the current pending request. When it's absent
|
|
1151
|
-
// (trusted session), allow the remap without delivery check.
|
|
1152
|
-
const senderHasDelivery = body.senderExternalUserId
|
|
1153
|
-
? currentDeliveries.some((d) => d.destinationExternalUserId === body.senderExternalUserId)
|
|
1154
|
-
: true;
|
|
1155
|
-
if (!senderHasDelivery) {
|
|
1156
|
-
log.info(
|
|
1157
|
-
{ supersededRequestId: request.id, currentRequestId: currentPending.id, senderExternalUserId: body.senderExternalUserId },
|
|
1158
|
-
'Superseded remap skipped: sender has no delivery on current pending request',
|
|
1159
|
-
);
|
|
1160
|
-
} else {
|
|
1161
|
-
const remapResult = await answerCall({
|
|
1162
|
-
callSessionId: currentPending.callSessionId,
|
|
1163
|
-
answer: answerText,
|
|
1164
|
-
pendingQuestionId: currentPending.pendingQuestionId,
|
|
1165
|
-
});
|
|
1166
|
-
|
|
1167
|
-
if ('ok' in remapResult && remapResult.ok) {
|
|
1168
|
-
const resolved = resolveGuardianActionRequest(currentPending.id, answerText, sourceChannel, body.senderExternalUserId);
|
|
1169
|
-
|
|
1170
|
-
if (resolved) {
|
|
1171
|
-
await tryMintGuardianActionGrant({
|
|
1172
|
-
request: currentPending,
|
|
1173
|
-
answerText,
|
|
1174
|
-
decisionChannel: sourceChannel,
|
|
1175
|
-
guardianExternalUserId: body.senderExternalUserId,
|
|
1176
|
-
approvalConversationGenerator,
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
const remapText = await composeGuardianActionMessageGenerative(
|
|
1181
|
-
{ scenario: 'guardian_superseded_remap', questionText: currentPending.questionText },
|
|
1182
|
-
{},
|
|
1183
|
-
guardianActionCopyGenerator,
|
|
1184
|
-
);
|
|
1185
|
-
try {
|
|
1186
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: remapText, assistantId }, bearerToken);
|
|
1187
|
-
} catch (err) {
|
|
1188
|
-
log.error({ err, externalChatId }, 'Failed to deliver superseded remap confirmation');
|
|
1189
|
-
}
|
|
1190
|
-
log.info(
|
|
1191
|
-
{ supersededRequestId: request.id, remappedToRequestId: currentPending.id },
|
|
1192
|
-
'Late approval for superseded request remapped to current pending request',
|
|
1193
|
-
);
|
|
1194
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'superseded_remapped' });
|
|
1195
|
-
}
|
|
1196
|
-
log.warn(
|
|
1197
|
-
{ callSessionId: currentPending.callSessionId, error: 'error' in remapResult ? remapResult.error : 'unknown' },
|
|
1198
|
-
'Superseded remap answerCall failed, falling through to follow-up',
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
// Call not active or no pending request — fall through to follow-up
|
|
1203
|
-
}
|
|
1001
|
+
return Response.json({
|
|
1002
|
+
accepted: true,
|
|
1003
|
+
duplicate: false,
|
|
1004
|
+
eventId: result.eventId,
|
|
1005
|
+
canonicalRouter: routerResult.type,
|
|
1006
|
+
requestId: routerResult.requestId,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1204
1009
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const followupText = await composeGuardianActionMessageGenerative(
|
|
1208
|
-
{ scenario: 'guardian_late_answer_followup', questionText: request.questionText, lateAnswerText: answerText },
|
|
1209
|
-
{},
|
|
1210
|
-
guardianActionCopyGenerator,
|
|
1211
|
-
);
|
|
1212
|
-
try {
|
|
1213
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: followupText, assistantId }, bearerToken);
|
|
1214
|
-
} catch (err) {
|
|
1215
|
-
log.error({ err, externalChatId }, 'Failed to deliver guardian action late answer follow-up');
|
|
1216
|
-
}
|
|
1217
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'followup_initiated' });
|
|
1218
|
-
} else {
|
|
1219
|
-
const staleText = await composeGuardianActionMessageGenerative(
|
|
1220
|
-
{ scenario: 'guardian_stale_expired' as const },
|
|
1221
|
-
{},
|
|
1222
|
-
guardianActionCopyGenerator,
|
|
1223
|
-
);
|
|
1224
|
-
try {
|
|
1225
|
-
await deliverChannelReply(replyCallbackUrl, { chatId: externalChatId, text: staleText, assistantId }, bearerToken);
|
|
1226
|
-
} catch (err) {
|
|
1227
|
-
log.error({ err, externalChatId }, 'Failed to deliver guardian action stale notice for expired follow-up race');
|
|
1228
|
-
}
|
|
1229
|
-
return Response.json({ accepted: true, duplicate: false, eventId: result.eventId, guardianAnswer: 'stale' });
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1010
|
+
if (routerResult.skipApprovalInterception) {
|
|
1011
|
+
skipApprovalInterception = true;
|
|
1233
1012
|
}
|
|
1234
1013
|
}
|
|
1235
1014
|
|
|
1236
|
-
// ── Actor role resolution ──
|
|
1237
|
-
// Uses shared channel-agnostic resolution so all ingress paths classify
|
|
1238
|
-
// guardian vs non-guardian actors the same way.
|
|
1239
|
-
const guardianCtx: GuardianContext = resolveGuardianContext({
|
|
1240
|
-
assistantId: canonicalAssistantId,
|
|
1241
|
-
sourceChannel,
|
|
1242
|
-
externalChatId,
|
|
1243
|
-
senderExternalUserId: body.senderExternalUserId,
|
|
1244
|
-
senderUsername: body.senderUsername,
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
1015
|
// ── Approval interception ──
|
|
1248
1016
|
// Keep this active whenever callback context is available.
|
|
1017
|
+
// Skipped when the canonical router flagged skipApprovalInterception (e.g.
|
|
1018
|
+
// invite handoff bypass) to prevent the legacy interceptor from swallowing
|
|
1019
|
+
// messages that should reach the assistant.
|
|
1249
1020
|
if (
|
|
1250
1021
|
replyCallbackUrl &&
|
|
1251
|
-
!result.duplicate
|
|
1022
|
+
!result.duplicate &&
|
|
1023
|
+
!skipApprovalInterception
|
|
1252
1024
|
) {
|
|
1253
1025
|
const approvalResult = await handleApprovalInterception({
|
|
1254
1026
|
conversationId: result.conversationId,
|
|
@@ -1256,7 +1028,7 @@ export async function handleChannelInbound(
|
|
|
1256
1028
|
content: trimmedContent,
|
|
1257
1029
|
externalChatId,
|
|
1258
1030
|
sourceChannel,
|
|
1259
|
-
senderExternalUserId:
|
|
1031
|
+
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
1260
1032
|
replyCallbackUrl,
|
|
1261
1033
|
bearerToken,
|
|
1262
1034
|
guardianCtx,
|
|
@@ -1547,111 +1319,6 @@ async function handleInviteTokenIntercept(params: {
|
|
|
1547
1319
|
return Response.json({ accepted: true, eventId: dedupResult.eventId, denied: true, inviteRedemption: outcome.reason });
|
|
1548
1320
|
}
|
|
1549
1321
|
|
|
1550
|
-
// ---------------------------------------------------------------------------
|
|
1551
|
-
// Non-member access request notification
|
|
1552
|
-
// ---------------------------------------------------------------------------
|
|
1553
|
-
|
|
1554
|
-
/**
|
|
1555
|
-
* Fire-and-forget: look up the guardian binding and, if present, create an
|
|
1556
|
-
* approval request + emit a notification signal so the guardian can
|
|
1557
|
-
* approve/deny the unknown user. Deduplicates by checking for an existing
|
|
1558
|
-
* pending approval for the same (requester, assistant, channel).
|
|
1559
|
-
*/
|
|
1560
|
-
function notifyGuardianOfAccessRequest(params: {
|
|
1561
|
-
canonicalAssistantId: string;
|
|
1562
|
-
sourceChannel: ChannelId;
|
|
1563
|
-
externalChatId: string;
|
|
1564
|
-
senderExternalUserId?: string;
|
|
1565
|
-
senderName?: string;
|
|
1566
|
-
senderUsername?: string;
|
|
1567
|
-
}): boolean {
|
|
1568
|
-
const {
|
|
1569
|
-
canonicalAssistantId,
|
|
1570
|
-
sourceChannel,
|
|
1571
|
-
externalChatId,
|
|
1572
|
-
senderExternalUserId,
|
|
1573
|
-
senderName,
|
|
1574
|
-
senderUsername,
|
|
1575
|
-
} = params;
|
|
1576
|
-
|
|
1577
|
-
if (!senderExternalUserId) return false;
|
|
1578
|
-
|
|
1579
|
-
const binding = getGuardianBinding(canonicalAssistantId, sourceChannel);
|
|
1580
|
-
if (!binding) {
|
|
1581
|
-
log.debug({ sourceChannel, canonicalAssistantId }, 'No guardian binding for access request notification');
|
|
1582
|
-
return false;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
// Deduplicate: skip if there is already a pending approval request for
|
|
1586
|
-
// the same requester on this channel. Still return true — the guardian
|
|
1587
|
-
// was already notified for this request.
|
|
1588
|
-
const existing = findPendingAccessRequestForRequester(
|
|
1589
|
-
canonicalAssistantId,
|
|
1590
|
-
sourceChannel,
|
|
1591
|
-
senderExternalUserId,
|
|
1592
|
-
'ingress_access_request',
|
|
1593
|
-
);
|
|
1594
|
-
if (existing) {
|
|
1595
|
-
log.debug(
|
|
1596
|
-
{ sourceChannel, senderExternalUserId, existingId: existing.id },
|
|
1597
|
-
'Skipping duplicate access request notification',
|
|
1598
|
-
);
|
|
1599
|
-
return true;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
const senderIdentifier = senderName || senderUsername || senderExternalUserId;
|
|
1603
|
-
const requestId = `access-req-${canonicalAssistantId}-${sourceChannel}-${senderExternalUserId}-${Date.now()}`;
|
|
1604
|
-
|
|
1605
|
-
const approvalRequest = createApprovalRequest({
|
|
1606
|
-
runId: `ingress-access-request-${Date.now()}`,
|
|
1607
|
-
requestId,
|
|
1608
|
-
conversationId: `access-req-${sourceChannel}-${senderExternalUserId}`,
|
|
1609
|
-
assistantId: canonicalAssistantId,
|
|
1610
|
-
channel: sourceChannel,
|
|
1611
|
-
requesterExternalUserId: senderExternalUserId,
|
|
1612
|
-
requesterChatId: externalChatId,
|
|
1613
|
-
guardianExternalUserId: binding.guardianExternalUserId,
|
|
1614
|
-
guardianChatId: binding.guardianDeliveryChatId,
|
|
1615
|
-
toolName: 'ingress_access_request',
|
|
1616
|
-
riskLevel: 'access_request',
|
|
1617
|
-
reason: `${senderIdentifier} is requesting access to the assistant`,
|
|
1618
|
-
expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
|
|
1619
|
-
});
|
|
1620
|
-
|
|
1621
|
-
void emitNotificationSignal({
|
|
1622
|
-
sourceEventName: 'ingress.access_request',
|
|
1623
|
-
sourceChannel,
|
|
1624
|
-
sourceSessionId: `access-req-${sourceChannel}-${senderExternalUserId}`,
|
|
1625
|
-
assistantId: canonicalAssistantId,
|
|
1626
|
-
attentionHints: {
|
|
1627
|
-
requiresAction: true,
|
|
1628
|
-
urgency: 'high',
|
|
1629
|
-
isAsyncBackground: false,
|
|
1630
|
-
visibleInSourceNow: false,
|
|
1631
|
-
},
|
|
1632
|
-
contextPayload: {
|
|
1633
|
-
requestId,
|
|
1634
|
-
sourceChannel,
|
|
1635
|
-
externalChatId,
|
|
1636
|
-
senderExternalUserId,
|
|
1637
|
-
senderName: senderName ?? null,
|
|
1638
|
-
senderUsername: senderUsername ?? null,
|
|
1639
|
-
senderIdentifier,
|
|
1640
|
-
},
|
|
1641
|
-
// Scoped to the approval request ID so duplicate notifications for the
|
|
1642
|
-
// same request are suppressed, but a new request (after deny/expire)
|
|
1643
|
-
// gets its own dedupe key and the guardian is notified again.
|
|
1644
|
-
dedupeKey: `access-request:${approvalRequest.id}`,
|
|
1645
|
-
});
|
|
1646
|
-
|
|
1647
|
-
log.info(
|
|
1648
|
-
{ sourceChannel, senderExternalUserId, senderIdentifier },
|
|
1649
|
-
'Guardian notified of non-member access request',
|
|
1650
|
-
);
|
|
1651
|
-
|
|
1652
|
-
return true;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
1322
|
// ---------------------------------------------------------------------------
|
|
1656
1323
|
// Background message processing
|
|
1657
1324
|
// ---------------------------------------------------------------------------
|
|
@@ -1733,6 +1400,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1733
1400
|
conversationId: string;
|
|
1734
1401
|
sourceChannel: ChannelId;
|
|
1735
1402
|
externalChatId: string;
|
|
1403
|
+
guardianTrustClass: GuardianContext['trustClass'];
|
|
1736
1404
|
replyCallbackUrl: string;
|
|
1737
1405
|
bearerToken?: string;
|
|
1738
1406
|
assistantId?: string;
|
|
@@ -1742,12 +1410,19 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1742
1410
|
conversationId,
|
|
1743
1411
|
sourceChannel,
|
|
1744
1412
|
externalChatId,
|
|
1413
|
+
guardianTrustClass,
|
|
1745
1414
|
replyCallbackUrl,
|
|
1746
1415
|
bearerToken,
|
|
1747
1416
|
assistantId,
|
|
1748
1417
|
approvalCopyGenerator,
|
|
1749
1418
|
} = params;
|
|
1750
1419
|
|
|
1420
|
+
// Approval prompt delivery is guardian-only. Non-guardian and unverified
|
|
1421
|
+
// actors must never receive approval prompt broadcasts for the conversation.
|
|
1422
|
+
if (guardianTrustClass !== 'guardian') {
|
|
1423
|
+
return () => {};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1751
1426
|
let active = true;
|
|
1752
1427
|
const deliveredRequestIds = new Set<string>();
|
|
1753
1428
|
|
|
@@ -1826,6 +1501,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1826
1501
|
conversationId,
|
|
1827
1502
|
sourceChannel,
|
|
1828
1503
|
externalChatId,
|
|
1504
|
+
guardianTrustClass: guardianCtx.trustClass,
|
|
1829
1505
|
replyCallbackUrl,
|
|
1830
1506
|
bearerToken,
|
|
1831
1507
|
assistantId,
|
|
@@ -1849,7 +1525,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1849
1525
|
},
|
|
1850
1526
|
assistantId,
|
|
1851
1527
|
guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
|
|
1852
|
-
isInteractive: guardianCtx.
|
|
1528
|
+
isInteractive: guardianCtx.trustClass === 'guardian',
|
|
1853
1529
|
...(cmdIntent ? { commandIntent: cmdIntent } : {}),
|
|
1854
1530
|
},
|
|
1855
1531
|
sourceChannel,
|