@vellumai/assistant 0.3.14 → 0.3.16
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 +142 -0
- package/Dockerfile +2 -2
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +8 -8
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-executor.ts +1 -1
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +6 -7
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +4 -7
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +5 -4
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +82 -20
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +2 -2
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -12,14 +12,20 @@ import * as attachmentsStore from '../../memory/attachments-store.js';
|
|
|
12
12
|
import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
|
|
13
13
|
import {
|
|
14
14
|
createApprovalRequest,
|
|
15
|
+
findPendingAccessRequestForRequester,
|
|
15
16
|
} from '../../memory/channel-guardian-store.js';
|
|
16
17
|
import { recordConversationSeenSignal } from '../../memory/conversation-attention-store.js';
|
|
17
18
|
import * as conversationStore from '../../memory/conversation-store.js';
|
|
18
19
|
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
19
20
|
import {
|
|
21
|
+
finalizeFollowup,
|
|
22
|
+
getExpiredDeliveriesByDestination,
|
|
23
|
+
getFollowupDeliveriesByDestination,
|
|
20
24
|
getGuardianActionRequest,
|
|
21
25
|
getPendingDeliveriesByDestination,
|
|
26
|
+
progressFollowupState,
|
|
22
27
|
resolveGuardianActionRequest,
|
|
28
|
+
startFollowupFromExpiredRequest,
|
|
23
29
|
} from '../../memory/guardian-action-store.js';
|
|
24
30
|
import { findMember, updateLastSeen, upsertMember } from '../../memory/ingress-member-store.js';
|
|
25
31
|
import { emitNotificationSignal } from '../../notifications/emit-signal.js';
|
|
@@ -53,8 +59,14 @@ import {
|
|
|
53
59
|
import type {
|
|
54
60
|
ApprovalConversationGenerator,
|
|
55
61
|
ApprovalCopyGenerator,
|
|
62
|
+
GuardianActionCopyGenerator,
|
|
63
|
+
GuardianFollowUpConversationGenerator,
|
|
56
64
|
MessageProcessor,
|
|
57
65
|
} from '../http-types.js';
|
|
66
|
+
import { processGuardianFollowUpTurn } from '../guardian-action-conversation-turn.js';
|
|
67
|
+
import { composeGuardianActionMessageGenerative } from '../guardian-action-message-composer.js';
|
|
68
|
+
import { executeFollowupAction } from '../guardian-action-followup-executor.js';
|
|
69
|
+
import { httpError } from '../http-errors.js';
|
|
58
70
|
import { deliverReplyViaCallback } from './channel-delivery-routes.js';
|
|
59
71
|
import {
|
|
60
72
|
canonicalChannelAssistantId,
|
|
@@ -71,26 +83,13 @@ const log = getLogger('runtime-http');
|
|
|
71
83
|
|
|
72
84
|
/**
|
|
73
85
|
* Parse a guardian verification code from message content.
|
|
74
|
-
* Accepts
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* 3. A bare code as the entire message: 6-digit numeric OR 64-char hex
|
|
78
|
-
* (hex is retained for backward compatibility with in-flight inbound
|
|
79
|
-
* challenges that still use high-entropy secrets)
|
|
80
|
-
* Returns `{ code, isExplicitCommand }` if recognized, or undefined otherwise.
|
|
81
|
-
* `isExplicitCommand` is true for legacy /guardian_verify commands (explicit
|
|
82
|
-
* intent) and false for bare codes (which need additional gating to avoid
|
|
83
|
-
* intercepting normal 6-digit messages like zip codes or PINs).
|
|
86
|
+
* Accepts a bare code as the entire message: 6-digit numeric OR 64-char hex
|
|
87
|
+
* (hex is retained for compatibility with unbound inbound/bootstrap sessions
|
|
88
|
+
* that intentionally use high-entropy secrets).
|
|
84
89
|
*/
|
|
85
|
-
function
|
|
86
|
-
// Legacy /guardian_verify command format
|
|
87
|
-
const commandMatch = content.match(/^\/guardian_verify(?:@\S+)?\s+(\S+)/);
|
|
88
|
-
if (commandMatch) return { code: commandMatch[1], isExplicitCommand: true };
|
|
89
|
-
|
|
90
|
-
// Bare code: 6-digit numeric (identity-bound outbound sessions) or
|
|
91
|
-
// 64-char hex (unbound inbound challenges)
|
|
90
|
+
function parseGuardianVerifyCode(content: string): string | undefined {
|
|
92
91
|
const bareMatch = content.match(/^([0-9a-fA-F]{64}|\d{6})$/);
|
|
93
|
-
if (bareMatch) return
|
|
92
|
+
if (bareMatch) return bareMatch[1];
|
|
94
93
|
|
|
95
94
|
return undefined;
|
|
96
95
|
}
|
|
@@ -103,6 +102,8 @@ export async function handleChannelInbound(
|
|
|
103
102
|
gatewayOriginSecret?: string,
|
|
104
103
|
approvalCopyGenerator?: ApprovalCopyGenerator,
|
|
105
104
|
approvalConversationGenerator?: ApprovalConversationGenerator,
|
|
105
|
+
guardianActionCopyGenerator?: GuardianActionCopyGenerator,
|
|
106
|
+
guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator,
|
|
106
107
|
): Promise<Response> {
|
|
107
108
|
// Reject requests that lack valid gateway-origin proof. This ensures
|
|
108
109
|
// channel inbound messages can only arrive via the gateway (which
|
|
@@ -142,40 +143,34 @@ export async function handleChannelInbound(
|
|
|
142
143
|
} = body;
|
|
143
144
|
|
|
144
145
|
if (!body.sourceChannel || typeof body.sourceChannel !== 'string') {
|
|
145
|
-
return
|
|
146
|
+
return httpError('BAD_REQUEST', 'sourceChannel is required', 400);
|
|
146
147
|
}
|
|
147
148
|
// Validate and narrow to canonical ChannelId at the boundary — the gateway
|
|
148
149
|
// only sends well-known channel strings, so an unknown value is rejected.
|
|
149
150
|
if (!isChannelId(body.sourceChannel)) {
|
|
150
|
-
return
|
|
151
|
-
{ error: `Invalid sourceChannel: ${body.sourceChannel}. Valid values: ${CHANNEL_IDS.join(', ')}` },
|
|
152
|
-
{ status: 400 },
|
|
153
|
-
);
|
|
151
|
+
return httpError('BAD_REQUEST', `Invalid sourceChannel: ${body.sourceChannel}. Valid values: ${CHANNEL_IDS.join(', ')}`, 400);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
const sourceChannel = body.sourceChannel;
|
|
157
155
|
|
|
158
156
|
if (!body.interface || typeof body.interface !== 'string') {
|
|
159
|
-
return
|
|
157
|
+
return httpError('BAD_REQUEST', 'interface is required', 400);
|
|
160
158
|
}
|
|
161
159
|
const sourceInterface = parseInterfaceId(body.interface);
|
|
162
160
|
if (!sourceInterface) {
|
|
163
|
-
return
|
|
164
|
-
{ error: `Invalid interface: ${body.interface}. Valid values: ${INTERFACE_IDS.join(', ')}` },
|
|
165
|
-
{ status: 400 },
|
|
166
|
-
);
|
|
161
|
+
return httpError('BAD_REQUEST', `Invalid interface: ${body.interface}. Valid values: ${INTERFACE_IDS.join(', ')}`, 400);
|
|
167
162
|
}
|
|
168
163
|
|
|
169
164
|
if (!externalChatId || typeof externalChatId !== 'string') {
|
|
170
|
-
return
|
|
165
|
+
return httpError('BAD_REQUEST', 'externalChatId is required', 400);
|
|
171
166
|
}
|
|
172
167
|
if (!externalMessageId || typeof externalMessageId !== 'string') {
|
|
173
|
-
return
|
|
168
|
+
return httpError('BAD_REQUEST', 'externalMessageId is required', 400);
|
|
174
169
|
}
|
|
175
170
|
|
|
176
171
|
// Reject non-string content regardless of whether attachments are present.
|
|
177
172
|
if (content != null && typeof content !== 'string') {
|
|
178
|
-
return
|
|
173
|
+
return httpError('BAD_REQUEST', 'content must be a string', 400);
|
|
179
174
|
}
|
|
180
175
|
|
|
181
176
|
const trimmedContent = typeof content === 'string' ? content.trim() : '';
|
|
@@ -184,7 +179,7 @@ export async function handleChannelInbound(
|
|
|
184
179
|
const hasCallbackData = typeof body.callbackData === 'string' && body.callbackData.length > 0;
|
|
185
180
|
|
|
186
181
|
if (trimmedContent.length === 0 && !hasAttachments && !isEdit && !hasCallbackData) {
|
|
187
|
-
return
|
|
182
|
+
return httpError('BAD_REQUEST', 'content or attachmentIds is required', 400);
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
// Canonicalize the assistant ID so all DB-facing operations use the
|
|
@@ -199,10 +194,10 @@ export async function handleChannelInbound(
|
|
|
199
194
|
// recordInbound (where we have a conversationId).
|
|
200
195
|
let resolvedMember: ReturnType<typeof findMember> = null;
|
|
201
196
|
|
|
202
|
-
//
|
|
197
|
+
// Verification codes must bypass the ACL membership check — users without a
|
|
203
198
|
// member record need to verify before they can be recognized as members.
|
|
204
|
-
const
|
|
205
|
-
const
|
|
199
|
+
const guardianVerifyCode = parseGuardianVerifyCode(trimmedContent);
|
|
200
|
+
const isGuardianVerifyCode = guardianVerifyCode !== undefined;
|
|
206
201
|
|
|
207
202
|
// /start gv_<token> bootstrap commands must also bypass ACL — the user
|
|
208
203
|
// hasn't been verified yet and needs to complete the bootstrap handshake.
|
|
@@ -223,11 +218,11 @@ export async function handleChannelInbound(
|
|
|
223
218
|
});
|
|
224
219
|
|
|
225
220
|
if (!resolvedMember) {
|
|
226
|
-
// Determine whether a
|
|
221
|
+
// Determine whether a verification-code bypass is warranted: only allow
|
|
227
222
|
// when there is a pending (unconsumed, unexpired) challenge AND no
|
|
228
223
|
// active guardian binding for this (assistantId, channel).
|
|
229
224
|
let denyNonMember = true;
|
|
230
|
-
if (
|
|
225
|
+
if (isGuardianVerifyCode) {
|
|
231
226
|
// Allow bypass when there is any consumable challenge or active
|
|
232
227
|
// outbound session. The !hasActiveBinding guard is intentionally
|
|
233
228
|
// omitted: rebind sessions create a consumable challenge while a
|
|
@@ -238,7 +233,7 @@ export async function handleChannelInbound(
|
|
|
238
233
|
if (hasPendingChallenge || hasActiveOutboundSession) {
|
|
239
234
|
denyNonMember = false;
|
|
240
235
|
} else {
|
|
241
|
-
log.info({ sourceChannel, hasPendingChallenge, hasActiveOutboundSession }, 'Ingress ACL:
|
|
236
|
+
log.info({ sourceChannel, hasPendingChallenge, hasActiveOutboundSession }, 'Ingress ACL: guardian verification bypass denied');
|
|
242
237
|
}
|
|
243
238
|
}
|
|
244
239
|
|
|
@@ -270,6 +265,23 @@ export async function handleChannelInbound(
|
|
|
270
265
|
log.error({ err, externalChatId }, 'Failed to deliver ACL rejection reply');
|
|
271
266
|
}
|
|
272
267
|
}
|
|
268
|
+
|
|
269
|
+
// Notify the guardian about the access request so they can approve/deny.
|
|
270
|
+
// Only fires when a guardian binding exists and no duplicate pending
|
|
271
|
+
// request already exists for this requester.
|
|
272
|
+
try {
|
|
273
|
+
notifyGuardianOfAccessRequest({
|
|
274
|
+
canonicalAssistantId,
|
|
275
|
+
sourceChannel,
|
|
276
|
+
externalChatId,
|
|
277
|
+
senderExternalUserId: body.senderExternalUserId,
|
|
278
|
+
senderName: body.senderName,
|
|
279
|
+
senderUsername: body.senderUsername,
|
|
280
|
+
});
|
|
281
|
+
} catch (err) {
|
|
282
|
+
log.error({ err, sourceChannel, externalChatId }, 'Failed to notify guardian of access request');
|
|
283
|
+
}
|
|
284
|
+
|
|
273
285
|
return Response.json({ accepted: true, denied: true, reason: 'not_a_member' });
|
|
274
286
|
}
|
|
275
287
|
}
|
|
@@ -329,7 +341,7 @@ export async function handleChannelInbound(
|
|
|
329
341
|
: undefined;
|
|
330
342
|
|
|
331
343
|
if (isEdit && !sourceMessageId) {
|
|
332
|
-
return
|
|
344
|
+
return httpError('BAD_REQUEST', 'sourceMetadata.messageId is required for edits', 400);
|
|
333
345
|
}
|
|
334
346
|
|
|
335
347
|
// ── Edit path: update existing message content, no new agent loop ──
|
|
@@ -536,7 +548,7 @@ export async function handleChannelInbound(
|
|
|
536
548
|
: undefined;
|
|
537
549
|
|
|
538
550
|
// ── Telegram bootstrap deep-link handling ──
|
|
539
|
-
// Intercept /start gv_<token> commands BEFORE the
|
|
551
|
+
// Intercept /start gv_<token> commands BEFORE the verification-code intercept.
|
|
540
552
|
// When a user clicks the deep link, Telegram sends /start gv_<token> which
|
|
541
553
|
// the gateway forwards with commandIntent: { type: 'start', payload: 'gv_<token>' }.
|
|
542
554
|
// We resolve the bootstrap token, bind the session identity, create a new
|
|
@@ -569,7 +581,7 @@ export async function handleChannelInbound(
|
|
|
569
581
|
destinationAddress: externalChatId,
|
|
570
582
|
});
|
|
571
583
|
|
|
572
|
-
// Compose and send the verification
|
|
584
|
+
// Compose and send the verification prompt via Telegram
|
|
573
585
|
const telegramBody = composeVerificationTelegram(
|
|
574
586
|
GUARDIAN_VERIFY_TEMPLATE_KEYS.TELEGRAM_CHALLENGE_REQUEST,
|
|
575
587
|
{
|
|
@@ -595,34 +607,32 @@ export async function handleChannelInbound(
|
|
|
595
607
|
// If not found or expired, fall through to normal /start handling
|
|
596
608
|
}
|
|
597
609
|
|
|
598
|
-
// ── Guardian verification
|
|
610
|
+
// ── Guardian verification code intercept (deterministic) ──
|
|
599
611
|
// Validate/consume the challenge synchronously so side effects (member
|
|
600
612
|
// upsert, binding creation) complete before any reply. The reply is
|
|
601
|
-
// delivered via template-driven deterministic messages and the
|
|
613
|
+
// delivered via template-driven deterministic messages and the code
|
|
602
614
|
// is short-circuited — it NEVER enters the agent pipeline. This
|
|
603
|
-
// prevents verification
|
|
615
|
+
// prevents verification code messages from producing agent-generated copy.
|
|
604
616
|
//
|
|
605
617
|
// Bare 6-digit codes are only intercepted when there is actually a
|
|
606
618
|
// pending challenge or active outbound session for this channel.
|
|
607
619
|
// Without this guard, normal 6-digit messages (zip codes, PINs, etc.)
|
|
608
620
|
// would be swallowed by the verification handler and never reach the
|
|
609
|
-
// agent pipeline.
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
(guardianVerifyParsed.isExplicitCommand ||
|
|
613
|
-
!!getPendingChallenge(canonicalAssistantId, sourceChannel) ||
|
|
621
|
+
// agent pipeline.
|
|
622
|
+
const shouldInterceptVerification = guardianVerifyCode !== undefined &&
|
|
623
|
+
(!!getPendingChallenge(canonicalAssistantId, sourceChannel) ||
|
|
614
624
|
!!findActiveSession(canonicalAssistantId, sourceChannel));
|
|
615
625
|
|
|
616
626
|
if (
|
|
617
627
|
!result.duplicate &&
|
|
618
628
|
shouldInterceptVerification &&
|
|
619
|
-
|
|
629
|
+
guardianVerifyCode !== undefined &&
|
|
620
630
|
body.senderExternalUserId
|
|
621
631
|
) {
|
|
622
632
|
const verifyResult = validateAndConsumeChallenge(
|
|
623
633
|
canonicalAssistantId,
|
|
624
634
|
sourceChannel,
|
|
625
|
-
|
|
635
|
+
guardianVerifyCode,
|
|
626
636
|
body.senderExternalUserId,
|
|
627
637
|
externalChatId,
|
|
628
638
|
body.senderUsername,
|
|
@@ -642,17 +652,52 @@ export async function handleChannelInbound(
|
|
|
642
652
|
displayName: body.senderName,
|
|
643
653
|
username: body.senderUsername,
|
|
644
654
|
});
|
|
645
|
-
|
|
655
|
+
|
|
656
|
+
const verifyLogLabel = verifyResult.verificationType === 'trusted_contact'
|
|
657
|
+
? 'Trusted contact verified'
|
|
658
|
+
: 'Guardian verified';
|
|
659
|
+
log.info({ sourceChannel, externalUserId: body.senderExternalUserId, verificationType: verifyResult.verificationType }, `${verifyLogLabel}: auto-upserted ingress member`);
|
|
660
|
+
|
|
661
|
+
// Emit activated signal when a trusted contact completes verification.
|
|
662
|
+
// Member record is persisted above before this event fires, satisfying
|
|
663
|
+
// the persistence-before-event ordering invariant.
|
|
664
|
+
if (verifyResult.verificationType === 'trusted_contact') {
|
|
665
|
+
void emitNotificationSignal({
|
|
666
|
+
sourceEventName: 'ingress.trusted_contact.activated',
|
|
667
|
+
sourceChannel,
|
|
668
|
+
sourceSessionId: result.conversationId,
|
|
669
|
+
assistantId: canonicalAssistantId,
|
|
670
|
+
attentionHints: {
|
|
671
|
+
requiresAction: false,
|
|
672
|
+
urgency: 'low',
|
|
673
|
+
isAsyncBackground: false,
|
|
674
|
+
visibleInSourceNow: false,
|
|
675
|
+
},
|
|
676
|
+
contextPayload: {
|
|
677
|
+
sourceChannel,
|
|
678
|
+
externalUserId: body.senderExternalUserId,
|
|
679
|
+
externalChatId,
|
|
680
|
+
senderName: body.senderName ?? null,
|
|
681
|
+
senderUsername: body.senderUsername ?? null,
|
|
682
|
+
},
|
|
683
|
+
dedupeKey: `trusted-contact:activated:${canonicalAssistantId}:${sourceChannel}:${body.senderExternalUserId}`,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
646
686
|
}
|
|
647
687
|
|
|
648
688
|
// Deliver a deterministic template-driven reply and short-circuit.
|
|
649
|
-
// Verification
|
|
689
|
+
// Verification code messages must never produce agent-generated copy.
|
|
650
690
|
if (replyCallbackUrl) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
691
|
+
let replyText: string;
|
|
692
|
+
if (!verifyResult.success) {
|
|
693
|
+
replyText = composeChannelVerifyReply(GUARDIAN_VERIFY_TEMPLATE_KEYS.CHANNEL_VERIFY_FAILED, {
|
|
694
|
+
failureReason: stripVerificationFailurePrefix(verifyResult.reason),
|
|
695
|
+
});
|
|
696
|
+
} else if (verifyResult.verificationType === 'trusted_contact') {
|
|
697
|
+
replyText = composeChannelVerifyReply(GUARDIAN_VERIFY_TEMPLATE_KEYS.CHANNEL_TRUSTED_CONTACT_VERIFY_SUCCESS);
|
|
698
|
+
} else {
|
|
699
|
+
replyText = composeChannelVerifyReply(GUARDIAN_VERIFY_TEMPLATE_KEYS.CHANNEL_VERIFY_SUCCESS);
|
|
700
|
+
}
|
|
656
701
|
try {
|
|
657
702
|
await deliverChannelReply(replyCallbackUrl, {
|
|
658
703
|
chatId: externalChatId,
|
|
@@ -711,8 +756,11 @@ export async function handleChannelInbound(
|
|
|
711
756
|
// Check if this inbound message is a reply to a cross-channel guardian
|
|
712
757
|
// action request (from a voice call). Must run before approval interception
|
|
713
758
|
// so guardian answers are not mistakenly routed into the approval flow.
|
|
759
|
+
// Callback payloads (inline button presses) are excluded — they should
|
|
760
|
+
// not be misclassified as guardian answers.
|
|
714
761
|
if (
|
|
715
762
|
!result.duplicate &&
|
|
763
|
+
!hasCallbackData &&
|
|
716
764
|
trimmedContent.length > 0 &&
|
|
717
765
|
body.senderExternalUserId &&
|
|
718
766
|
replyCallbackUrl
|
|
@@ -778,9 +826,14 @@ export async function handleChannelInbound(
|
|
|
778
826
|
const errorMsg = 'error' in answerResult ? answerResult.error : 'Unknown error';
|
|
779
827
|
log.warn({ callSessionId: request.callSessionId, error: errorMsg }, 'answerCall failed for guardian answer');
|
|
780
828
|
try {
|
|
829
|
+
const failureText = await composeGuardianActionMessageGenerative(
|
|
830
|
+
{ scenario: 'guardian_answer_delivery_failed' },
|
|
831
|
+
{},
|
|
832
|
+
guardianActionCopyGenerator,
|
|
833
|
+
);
|
|
781
834
|
await deliverChannelReply(replyCallbackUrl, {
|
|
782
835
|
chatId: externalChatId,
|
|
783
|
-
text:
|
|
836
|
+
text: failureText,
|
|
784
837
|
assistantId,
|
|
785
838
|
}, bearerToken);
|
|
786
839
|
} catch (deliverErr) {
|
|
@@ -809,16 +862,34 @@ export async function handleChannelInbound(
|
|
|
809
862
|
guardianAnswer: 'resolved',
|
|
810
863
|
});
|
|
811
864
|
} else {
|
|
812
|
-
//
|
|
865
|
+
// resolveGuardianActionRequest returned null — request was no
|
|
866
|
+
// longer pending. answerCall already succeeded above, so the
|
|
867
|
+
// answer WAS delivered to the call. Don't initiate a follow-up
|
|
868
|
+
// negotiation; instead tell the guardian the answer was relayed.
|
|
869
|
+
const freshRequest = getGuardianActionRequest(request.id);
|
|
870
|
+
|
|
871
|
+
// answerCall succeeded, so the answer was delivered regardless
|
|
872
|
+
// of the resolve race. Inform the guardian accordingly.
|
|
873
|
+
const relayedText = await composeGuardianActionMessageGenerative(
|
|
874
|
+
{
|
|
875
|
+
scenario: 'guardian_stale_answered' as const,
|
|
876
|
+
},
|
|
877
|
+
{},
|
|
878
|
+
guardianActionCopyGenerator,
|
|
879
|
+
);
|
|
813
880
|
try {
|
|
814
881
|
await deliverChannelReply(replyCallbackUrl, {
|
|
815
882
|
chatId: externalChatId,
|
|
816
|
-
text:
|
|
883
|
+
text: relayedText,
|
|
817
884
|
assistantId,
|
|
818
885
|
}, bearerToken);
|
|
819
886
|
} catch (err) {
|
|
820
887
|
log.error({ err, externalChatId }, 'Failed to deliver guardian action stale notice');
|
|
821
888
|
}
|
|
889
|
+
log.info(
|
|
890
|
+
{ requestId: request.id, freshStatus: freshRequest?.status },
|
|
891
|
+
'answerCall succeeded but resolveGuardianActionRequest returned null — informed guardian answer was relayed',
|
|
892
|
+
);
|
|
822
893
|
return Response.json({
|
|
823
894
|
accepted: true,
|
|
824
895
|
duplicate: false,
|
|
@@ -832,6 +903,298 @@ export async function handleChannelInbound(
|
|
|
832
903
|
}
|
|
833
904
|
}
|
|
834
905
|
|
|
906
|
+
// ── Expired guardian action late answer interception ──
|
|
907
|
+
// When no pending delivery was found above, check for expired requests
|
|
908
|
+
// eligible for follow-up (status='expired', followup_state='none').
|
|
909
|
+
// Exclude callback payloads — inline button presses should not be
|
|
910
|
+
// misclassified as late guardian answers.
|
|
911
|
+
if (
|
|
912
|
+
!result.duplicate &&
|
|
913
|
+
!hasCallbackData &&
|
|
914
|
+
trimmedContent.length > 0 &&
|
|
915
|
+
body.senderExternalUserId &&
|
|
916
|
+
replyCallbackUrl
|
|
917
|
+
) {
|
|
918
|
+
const expiredDeliveries = getExpiredDeliveriesByDestination(canonicalAssistantId, sourceChannel, externalChatId);
|
|
919
|
+
if (expiredDeliveries.length > 0) {
|
|
920
|
+
const validExpired = expiredDeliveries.filter(
|
|
921
|
+
(d) => d.destinationExternalUserId === body.senderExternalUserId,
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
if (validExpired.length > 0) {
|
|
925
|
+
let matchedExpired = validExpired.length === 1 ? validExpired[0] : null;
|
|
926
|
+
let expiredAnswerText = trimmedContent;
|
|
927
|
+
|
|
928
|
+
// Multiple expired deliveries: require request code prefix for disambiguation
|
|
929
|
+
if (validExpired.length > 1) {
|
|
930
|
+
for (const d of validExpired) {
|
|
931
|
+
const req = getGuardianActionRequest(d.requestId);
|
|
932
|
+
if (req && trimmedContent.toUpperCase().startsWith(req.requestCode)) {
|
|
933
|
+
matchedExpired = d;
|
|
934
|
+
expiredAnswerText = trimmedContent.slice(req.requestCode.length).trim();
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (!matchedExpired) {
|
|
940
|
+
// Send disambiguation message listing the request codes
|
|
941
|
+
const codes = validExpired
|
|
942
|
+
.map((d) => {
|
|
943
|
+
const req = getGuardianActionRequest(d.requestId);
|
|
944
|
+
return req ? req.requestCode : null;
|
|
945
|
+
})
|
|
946
|
+
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
947
|
+
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
948
|
+
{
|
|
949
|
+
scenario: 'guardian_expired_disambiguation',
|
|
950
|
+
requestCodes: codes,
|
|
951
|
+
channel: sourceChannel,
|
|
952
|
+
},
|
|
953
|
+
{ requiredKeywords: codes },
|
|
954
|
+
guardianActionCopyGenerator,
|
|
955
|
+
);
|
|
956
|
+
try {
|
|
957
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
958
|
+
chatId: externalChatId,
|
|
959
|
+
text: disambiguationText,
|
|
960
|
+
assistantId,
|
|
961
|
+
}, bearerToken);
|
|
962
|
+
} catch (err) {
|
|
963
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian action expired disambiguation message');
|
|
964
|
+
}
|
|
965
|
+
return Response.json({
|
|
966
|
+
accepted: true,
|
|
967
|
+
duplicate: false,
|
|
968
|
+
eventId: result.eventId,
|
|
969
|
+
guardianAnswer: 'disambiguation_sent',
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (matchedExpired) {
|
|
975
|
+
const expiredRequest = getGuardianActionRequest(matchedExpired.requestId);
|
|
976
|
+
|
|
977
|
+
if (expiredRequest && expiredRequest.status === 'expired' && expiredRequest.followupState === 'none') {
|
|
978
|
+
const followupResult = startFollowupFromExpiredRequest(expiredRequest.id, expiredAnswerText);
|
|
979
|
+
if (followupResult) {
|
|
980
|
+
const followupText = await composeGuardianActionMessageGenerative(
|
|
981
|
+
{
|
|
982
|
+
scenario: 'guardian_late_answer_followup',
|
|
983
|
+
questionText: expiredRequest.questionText,
|
|
984
|
+
lateAnswerText: expiredAnswerText,
|
|
985
|
+
},
|
|
986
|
+
{},
|
|
987
|
+
guardianActionCopyGenerator,
|
|
988
|
+
);
|
|
989
|
+
try {
|
|
990
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
991
|
+
chatId: externalChatId,
|
|
992
|
+
text: followupText,
|
|
993
|
+
assistantId,
|
|
994
|
+
}, bearerToken);
|
|
995
|
+
} catch (err) {
|
|
996
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian action late answer follow-up');
|
|
997
|
+
}
|
|
998
|
+
return Response.json({
|
|
999
|
+
accepted: true,
|
|
1000
|
+
duplicate: false,
|
|
1001
|
+
eventId: result.eventId,
|
|
1002
|
+
guardianAnswer: 'followup_initiated',
|
|
1003
|
+
});
|
|
1004
|
+
} else {
|
|
1005
|
+
// startFollowupFromExpiredRequest returned null (race condition:
|
|
1006
|
+
// another reply already transitioned the request). Send a stale
|
|
1007
|
+
// notice instead of falling through to the normal agent pipeline.
|
|
1008
|
+
const staleText = await composeGuardianActionMessageGenerative(
|
|
1009
|
+
{ scenario: 'guardian_stale_expired' as const },
|
|
1010
|
+
{},
|
|
1011
|
+
guardianActionCopyGenerator,
|
|
1012
|
+
);
|
|
1013
|
+
try {
|
|
1014
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1015
|
+
chatId: externalChatId,
|
|
1016
|
+
text: staleText,
|
|
1017
|
+
assistantId,
|
|
1018
|
+
}, bearerToken);
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian action stale notice for expired follow-up race');
|
|
1021
|
+
}
|
|
1022
|
+
return Response.json({
|
|
1023
|
+
accepted: true,
|
|
1024
|
+
duplicate: false,
|
|
1025
|
+
eventId: result.eventId,
|
|
1026
|
+
guardianAnswer: 'stale',
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ── Guardian follow-up conversation interception ──
|
|
1036
|
+
// When a request is in `awaiting_guardian_choice` state, the guardian has
|
|
1037
|
+
// already been asked "call back or send a message?". Their next message
|
|
1038
|
+
// is the reply to that prompt — route it through the conversation engine
|
|
1039
|
+
// to classify their intent.
|
|
1040
|
+
if (
|
|
1041
|
+
!result.duplicate &&
|
|
1042
|
+
!hasCallbackData &&
|
|
1043
|
+
trimmedContent.length > 0 &&
|
|
1044
|
+
body.senderExternalUserId &&
|
|
1045
|
+
replyCallbackUrl
|
|
1046
|
+
) {
|
|
1047
|
+
const followupDeliveries = getFollowupDeliveriesByDestination(canonicalAssistantId, sourceChannel, externalChatId);
|
|
1048
|
+
if (followupDeliveries.length > 0) {
|
|
1049
|
+
const validFollowup = followupDeliveries.filter(
|
|
1050
|
+
(d) => d.destinationExternalUserId === body.senderExternalUserId,
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
if (validFollowup.length > 0) {
|
|
1054
|
+
let matchedFollowup = validFollowup.length === 1 ? validFollowup[0] : null;
|
|
1055
|
+
let followupReplyText = trimmedContent;
|
|
1056
|
+
|
|
1057
|
+
// Multiple follow-up deliveries: require request code prefix for disambiguation
|
|
1058
|
+
if (validFollowup.length > 1) {
|
|
1059
|
+
for (const d of validFollowup) {
|
|
1060
|
+
const req = getGuardianActionRequest(d.requestId);
|
|
1061
|
+
if (req && trimmedContent.toUpperCase().startsWith(req.requestCode)) {
|
|
1062
|
+
matchedFollowup = d;
|
|
1063
|
+
followupReplyText = trimmedContent.slice(req.requestCode.length).trim();
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (!matchedFollowup) {
|
|
1069
|
+
// Send disambiguation message listing the request codes
|
|
1070
|
+
const codes = validFollowup
|
|
1071
|
+
.map((d) => {
|
|
1072
|
+
const req = getGuardianActionRequest(d.requestId);
|
|
1073
|
+
return req ? req.requestCode : null;
|
|
1074
|
+
})
|
|
1075
|
+
.filter((code): code is string => typeof code === 'string' && code.length > 0);
|
|
1076
|
+
const disambiguationText = await composeGuardianActionMessageGenerative(
|
|
1077
|
+
{
|
|
1078
|
+
scenario: 'guardian_followup_disambiguation',
|
|
1079
|
+
requestCodes: codes,
|
|
1080
|
+
channel: sourceChannel,
|
|
1081
|
+
},
|
|
1082
|
+
{ requiredKeywords: codes },
|
|
1083
|
+
guardianActionCopyGenerator,
|
|
1084
|
+
);
|
|
1085
|
+
try {
|
|
1086
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1087
|
+
chatId: externalChatId,
|
|
1088
|
+
text: disambiguationText,
|
|
1089
|
+
assistantId,
|
|
1090
|
+
}, bearerToken);
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian follow-up disambiguation message');
|
|
1093
|
+
}
|
|
1094
|
+
return Response.json({
|
|
1095
|
+
accepted: true,
|
|
1096
|
+
duplicate: false,
|
|
1097
|
+
eventId: result.eventId,
|
|
1098
|
+
guardianFollowUp: 'disambiguation_sent',
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (matchedFollowup) {
|
|
1104
|
+
const followupRequest = getGuardianActionRequest(matchedFollowup.requestId);
|
|
1105
|
+
|
|
1106
|
+
if (followupRequest && followupRequest.followupState === 'awaiting_guardian_choice') {
|
|
1107
|
+
const turnResult = await processGuardianFollowUpTurn(
|
|
1108
|
+
{
|
|
1109
|
+
questionText: followupRequest.questionText,
|
|
1110
|
+
lateAnswerText: followupRequest.lateAnswerText ?? '',
|
|
1111
|
+
guardianReply: followupReplyText,
|
|
1112
|
+
},
|
|
1113
|
+
guardianFollowUpConversationGenerator,
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
// Apply the disposition to the follow-up state machine.
|
|
1117
|
+
// Both progressFollowupState and finalizeFollowup are compare-and-set:
|
|
1118
|
+
// they return null when the transition was not applied (e.g. a concurrent
|
|
1119
|
+
// reply already advanced the state). In that case we notify the guardian
|
|
1120
|
+
// that the request was already resolved and skip action execution.
|
|
1121
|
+
let stateApplied = true;
|
|
1122
|
+
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
1123
|
+
stateApplied = progressFollowupState(followupRequest.id, 'dispatching', turnResult.disposition) !== null;
|
|
1124
|
+
} else if (turnResult.disposition === 'decline') {
|
|
1125
|
+
stateApplied = finalizeFollowup(followupRequest.id, 'declined') !== null;
|
|
1126
|
+
}
|
|
1127
|
+
// keep_pending: no state change — guardian can reply again
|
|
1128
|
+
|
|
1129
|
+
if (!stateApplied) {
|
|
1130
|
+
log.warn({ requestId: followupRequest.id, disposition: turnResult.disposition }, 'Follow-up state transition failed (already resolved)');
|
|
1131
|
+
const staleText = await composeGuardianActionMessageGenerative(
|
|
1132
|
+
{ scenario: 'guardian_stale_followup' as const },
|
|
1133
|
+
{},
|
|
1134
|
+
guardianActionCopyGenerator,
|
|
1135
|
+
);
|
|
1136
|
+
try {
|
|
1137
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1138
|
+
chatId: externalChatId,
|
|
1139
|
+
text: staleText,
|
|
1140
|
+
assistantId,
|
|
1141
|
+
}, bearerToken);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
log.error({ err, externalChatId }, 'Failed to deliver stale follow-up notice');
|
|
1144
|
+
}
|
|
1145
|
+
return Response.json({
|
|
1146
|
+
accepted: true,
|
|
1147
|
+
duplicate: false,
|
|
1148
|
+
eventId: result.eventId,
|
|
1149
|
+
guardianFollowUp: 'stale_ignored',
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Deliver the generated reply to the guardian
|
|
1154
|
+
try {
|
|
1155
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1156
|
+
chatId: externalChatId,
|
|
1157
|
+
text: turnResult.replyText,
|
|
1158
|
+
assistantId,
|
|
1159
|
+
}, bearerToken);
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian follow-up conversation reply');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Execute the action and send a completion/failure reply (fire-and-forget).
|
|
1165
|
+
// The initial reply above acknowledges the guardian's choice; the executor
|
|
1166
|
+
// carries out the actual call_back or message_back and posts a second message.
|
|
1167
|
+
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
1168
|
+
void (async () => {
|
|
1169
|
+
try {
|
|
1170
|
+
const execResult = await executeFollowupAction(
|
|
1171
|
+
followupRequest.id,
|
|
1172
|
+
turnResult.disposition as 'call_back' | 'message_back',
|
|
1173
|
+
guardianActionCopyGenerator,
|
|
1174
|
+
);
|
|
1175
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1176
|
+
chatId: externalChatId,
|
|
1177
|
+
text: execResult.guardianReplyText,
|
|
1178
|
+
assistantId,
|
|
1179
|
+
}, bearerToken);
|
|
1180
|
+
} catch (execErr) {
|
|
1181
|
+
log.error({ err: execErr, requestId: followupRequest.id }, 'Follow-up action execution or completion reply failed');
|
|
1182
|
+
}
|
|
1183
|
+
})();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
return Response.json({
|
|
1187
|
+
accepted: true,
|
|
1188
|
+
duplicate: false,
|
|
1189
|
+
eventId: result.eventId,
|
|
1190
|
+
guardianFollowUp: turnResult.disposition,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
835
1198
|
// ── Actor role resolution ──
|
|
836
1199
|
// Uses shared channel-agnostic resolution so all ingress paths classify
|
|
837
1200
|
// guardian vs non-guardian actors the same way.
|
|
@@ -1028,6 +1391,108 @@ export async function handleChannelInbound(
|
|
|
1028
1391
|
});
|
|
1029
1392
|
}
|
|
1030
1393
|
|
|
1394
|
+
// ---------------------------------------------------------------------------
|
|
1395
|
+
// Non-member access request notification
|
|
1396
|
+
// ---------------------------------------------------------------------------
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Fire-and-forget: look up the guardian binding and, if present, create an
|
|
1400
|
+
* approval request + emit a notification signal so the guardian can
|
|
1401
|
+
* approve/deny the unknown user. Deduplicates by checking for an existing
|
|
1402
|
+
* pending approval for the same (requester, assistant, channel).
|
|
1403
|
+
*/
|
|
1404
|
+
function notifyGuardianOfAccessRequest(params: {
|
|
1405
|
+
canonicalAssistantId: string;
|
|
1406
|
+
sourceChannel: ChannelId;
|
|
1407
|
+
externalChatId: string;
|
|
1408
|
+
senderExternalUserId?: string;
|
|
1409
|
+
senderName?: string;
|
|
1410
|
+
senderUsername?: string;
|
|
1411
|
+
}): void {
|
|
1412
|
+
const {
|
|
1413
|
+
canonicalAssistantId,
|
|
1414
|
+
sourceChannel,
|
|
1415
|
+
externalChatId,
|
|
1416
|
+
senderExternalUserId,
|
|
1417
|
+
senderName,
|
|
1418
|
+
senderUsername,
|
|
1419
|
+
} = params;
|
|
1420
|
+
|
|
1421
|
+
if (!senderExternalUserId) return;
|
|
1422
|
+
|
|
1423
|
+
const binding = getGuardianBinding(canonicalAssistantId, sourceChannel);
|
|
1424
|
+
if (!binding) {
|
|
1425
|
+
log.debug({ sourceChannel, canonicalAssistantId }, 'No guardian binding for access request notification');
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Deduplicate: skip if there is already a pending approval request for
|
|
1430
|
+
// the same requester on this channel.
|
|
1431
|
+
const existing = findPendingAccessRequestForRequester(
|
|
1432
|
+
canonicalAssistantId,
|
|
1433
|
+
sourceChannel,
|
|
1434
|
+
senderExternalUserId,
|
|
1435
|
+
'ingress_access_request',
|
|
1436
|
+
);
|
|
1437
|
+
if (existing) {
|
|
1438
|
+
log.debug(
|
|
1439
|
+
{ sourceChannel, senderExternalUserId, existingId: existing.id },
|
|
1440
|
+
'Skipping duplicate access request notification',
|
|
1441
|
+
);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const senderIdentifier = senderName || senderUsername || senderExternalUserId;
|
|
1446
|
+
const requestId = `access-req-${canonicalAssistantId}-${sourceChannel}-${senderExternalUserId}-${Date.now()}`;
|
|
1447
|
+
|
|
1448
|
+
const approvalRequest = createApprovalRequest({
|
|
1449
|
+
runId: `ingress-access-request-${Date.now()}`,
|
|
1450
|
+
requestId,
|
|
1451
|
+
conversationId: `access-req-${sourceChannel}-${senderExternalUserId}`,
|
|
1452
|
+
assistantId: canonicalAssistantId,
|
|
1453
|
+
channel: sourceChannel,
|
|
1454
|
+
requesterExternalUserId: senderExternalUserId,
|
|
1455
|
+
requesterChatId: externalChatId,
|
|
1456
|
+
guardianExternalUserId: binding.guardianExternalUserId,
|
|
1457
|
+
guardianChatId: binding.guardianDeliveryChatId,
|
|
1458
|
+
toolName: 'ingress_access_request',
|
|
1459
|
+
riskLevel: 'access_request',
|
|
1460
|
+
reason: `${senderIdentifier} is requesting access to the assistant`,
|
|
1461
|
+
expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
void emitNotificationSignal({
|
|
1465
|
+
sourceEventName: 'ingress.access_request',
|
|
1466
|
+
sourceChannel,
|
|
1467
|
+
sourceSessionId: `access-req-${sourceChannel}-${senderExternalUserId}`,
|
|
1468
|
+
assistantId: canonicalAssistantId,
|
|
1469
|
+
attentionHints: {
|
|
1470
|
+
requiresAction: true,
|
|
1471
|
+
urgency: 'high',
|
|
1472
|
+
isAsyncBackground: false,
|
|
1473
|
+
visibleInSourceNow: false,
|
|
1474
|
+
},
|
|
1475
|
+
contextPayload: {
|
|
1476
|
+
requestId,
|
|
1477
|
+
sourceChannel,
|
|
1478
|
+
externalChatId,
|
|
1479
|
+
senderExternalUserId,
|
|
1480
|
+
senderName: senderName ?? null,
|
|
1481
|
+
senderUsername: senderUsername ?? null,
|
|
1482
|
+
senderIdentifier,
|
|
1483
|
+
},
|
|
1484
|
+
// Scoped to the approval request ID so duplicate notifications for the
|
|
1485
|
+
// same request are suppressed, but a new request (after deny/expire)
|
|
1486
|
+
// gets its own dedupe key and the guardian is notified again.
|
|
1487
|
+
dedupeKey: `access-request:${approvalRequest.id}`,
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
log.info(
|
|
1491
|
+
{ sourceChannel, senderExternalUserId, senderIdentifier },
|
|
1492
|
+
'Guardian notified of non-member access request',
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1031
1496
|
// ---------------------------------------------------------------------------
|
|
1032
1497
|
// Background message processing
|
|
1033
1498
|
// ---------------------------------------------------------------------------
|
|
@@ -1225,7 +1690,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1225
1690
|
},
|
|
1226
1691
|
assistantId,
|
|
1227
1692
|
guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
|
|
1228
|
-
isInteractive:
|
|
1693
|
+
isInteractive: guardianCtx.actorRole === 'guardian',
|
|
1229
1694
|
...(cmdIntent ? { commandIntent: cmdIntent } : {}),
|
|
1230
1695
|
},
|
|
1231
1696
|
sourceChannel,
|