@vellumai/assistant 0.3.28 → 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 +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- 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__/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__/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__/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-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- 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 +138 -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__/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 +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- 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__/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__/send-endpoint-busy.test.ts +4 -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 +50 -12
- 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 +7 -7
- 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/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- 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/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- 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/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- 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 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- 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 +2 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- 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
|
@@ -85,6 +85,12 @@ export interface GuardianReplyResult {
|
|
|
85
85
|
requestId?: string;
|
|
86
86
|
/** Detailed result from the canonical decision primitive (when a decision was attempted). */
|
|
87
87
|
canonicalResult?: CanonicalDecisionResult;
|
|
88
|
+
/**
|
|
89
|
+
* When true, the caller should skip legacy approval interception for this
|
|
90
|
+
* message. Set by the invite handoff bypass so that "open invite flow"
|
|
91
|
+
* reaches the assistant even when other legacy guardian approvals are pending.
|
|
92
|
+
*/
|
|
93
|
+
skipApprovalInterception?: boolean;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
// ---------------------------------------------------------------------------
|
|
@@ -319,7 +325,30 @@ export async function routeGuardianReply(
|
|
|
319
325
|
}
|
|
320
326
|
}
|
|
321
327
|
|
|
322
|
-
// ── 2.5.
|
|
328
|
+
// ── 2.5. Invite handoff bypass for access requests ──
|
|
329
|
+
// When the guardian sends "open invite flow" and there is at least one
|
|
330
|
+
// pending access_request, return not_consumed so the message falls through
|
|
331
|
+
// to the normal assistant turn and can invoke the Trusted Contacts skill.
|
|
332
|
+
if (messageText.length > 0 && pendingRequests.length > 0) {
|
|
333
|
+
const normalized = messageText.trim().toLowerCase().replace(/[.!?]+$/g, '');
|
|
334
|
+
if (normalized === 'open invite flow') {
|
|
335
|
+
const hasAccessRequest = pendingRequests.some(r => r.kind === 'access_request');
|
|
336
|
+
if (hasAccessRequest) {
|
|
337
|
+
log.info(
|
|
338
|
+
{ event: 'router_invite_handoff', pendingCount: pendingRequests.length },
|
|
339
|
+
'Guardian sent "open invite flow" with pending access_request — passing through to assistant',
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
consumed: false,
|
|
343
|
+
decisionApplied: false,
|
|
344
|
+
type: 'not_consumed' as const,
|
|
345
|
+
skipApprovalInterception: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── 2.6. Deterministic plain-text decisions for known pending targets ──
|
|
323
352
|
// Desktop sessions intentionally do not enable NL classification; when the
|
|
324
353
|
// caller has exactly one known pending request and sends an explicit
|
|
325
354
|
// approve/reject phrase ("approve", "yes", "reject", "no"), apply the
|
|
@@ -551,6 +580,7 @@ const EXPLICIT_APPROVE_PHRASES: ReadonlySet<string> = new Set([
|
|
|
551
580
|
'yes',
|
|
552
581
|
'y',
|
|
553
582
|
'allow',
|
|
583
|
+
'go for it',
|
|
554
584
|
'go ahead',
|
|
555
585
|
'proceed',
|
|
556
586
|
'do it',
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* both the HTTP routes and the IPC handlers call the same logic.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isChannelId } from '../channels/types.js';
|
|
8
9
|
import {
|
|
9
10
|
createInvite,
|
|
10
11
|
type IngressInvite,
|
|
@@ -22,11 +23,18 @@ import {
|
|
|
22
23
|
revokeMember,
|
|
23
24
|
upsertMember,
|
|
24
25
|
} from '../memory/ingress-member-store.js';
|
|
26
|
+
import { isValidE164 } from '../util/phone.js';
|
|
27
|
+
import { generateVoiceCode, hashVoiceCode } from '../util/voice-code.js';
|
|
28
|
+
import { getTransport } from './channel-invite-transport.js';
|
|
25
29
|
import {
|
|
26
30
|
type InviteRedemptionOutcome,
|
|
27
31
|
redeemInvite as redeemInviteTyped,
|
|
32
|
+
redeemVoiceInviteCode as redeemVoiceInviteCodeTyped,
|
|
33
|
+
type VoiceRedemptionOutcome,
|
|
28
34
|
} from './invite-redemption-service.js';
|
|
29
35
|
|
|
36
|
+
import './channel-invite-transports/telegram.js';
|
|
37
|
+
|
|
30
38
|
// ---------------------------------------------------------------------------
|
|
31
39
|
// Response shapes — used by both HTTP routes and IPC handlers
|
|
32
40
|
// ---------------------------------------------------------------------------
|
|
@@ -35,12 +43,20 @@ export interface InviteResponseData {
|
|
|
35
43
|
id: string;
|
|
36
44
|
sourceChannel: string;
|
|
37
45
|
token?: string;
|
|
46
|
+
share?: {
|
|
47
|
+
url: string;
|
|
48
|
+
displayText: string;
|
|
49
|
+
};
|
|
38
50
|
tokenHash: string;
|
|
39
51
|
maxUses: number;
|
|
40
52
|
useCount: number;
|
|
41
53
|
expiresAt: number | null;
|
|
42
54
|
status: string;
|
|
43
55
|
note?: string;
|
|
56
|
+
// Voice invite fields (present only for voice invites)
|
|
57
|
+
expectedExternalUserId?: string;
|
|
58
|
+
voiceCode?: string;
|
|
59
|
+
voiceCodeDigits?: number;
|
|
44
60
|
createdAt: number;
|
|
45
61
|
}
|
|
46
62
|
|
|
@@ -61,17 +77,39 @@ export interface MemberResponseData {
|
|
|
61
77
|
// Mappers
|
|
62
78
|
// ---------------------------------------------------------------------------
|
|
63
79
|
|
|
64
|
-
function
|
|
80
|
+
function buildSharePayload(sourceChannel: string, rawToken?: string): InviteResponseData['share'] | undefined {
|
|
81
|
+
if (!rawToken || !isChannelId(sourceChannel)) return undefined;
|
|
82
|
+
const transport = getTransport(sourceChannel);
|
|
83
|
+
if (!transport?.buildShareableInvite) return undefined;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
return transport.buildShareableInvite({
|
|
87
|
+
rawToken,
|
|
88
|
+
sourceChannel,
|
|
89
|
+
});
|
|
90
|
+
} catch {
|
|
91
|
+
// Missing channel-specific config (e.g. Telegram bot username) should
|
|
92
|
+
// not fail invite creation — callers can still use the raw token.
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function inviteToResponse(inv: IngressInvite, opts?: { rawToken?: string; voiceCode?: string }): InviteResponseData {
|
|
98
|
+
const share = buildSharePayload(inv.sourceChannel, opts?.rawToken);
|
|
65
99
|
return {
|
|
66
100
|
id: inv.id,
|
|
67
101
|
sourceChannel: inv.sourceChannel,
|
|
68
|
-
...(rawToken ? { token: rawToken } : {}),
|
|
102
|
+
...(opts?.rawToken ? { token: opts.rawToken } : {}),
|
|
103
|
+
...(share ? { share } : {}),
|
|
69
104
|
tokenHash: inv.tokenHash,
|
|
70
105
|
maxUses: inv.maxUses,
|
|
71
106
|
useCount: inv.useCount,
|
|
72
107
|
expiresAt: inv.expiresAt,
|
|
73
108
|
status: inv.status,
|
|
74
109
|
note: inv.note ?? undefined,
|
|
110
|
+
...(inv.expectedExternalUserId ? { expectedExternalUserId: inv.expectedExternalUserId } : {}),
|
|
111
|
+
...(opts?.voiceCode ? { voiceCode: opts.voiceCode } : {}),
|
|
112
|
+
...(inv.voiceCodeDigits != null ? { voiceCodeDigits: inv.voiceCodeDigits } : {}),
|
|
75
113
|
createdAt: inv.createdAt,
|
|
76
114
|
};
|
|
77
115
|
}
|
|
@@ -108,17 +146,46 @@ export function createIngressInvite(params: {
|
|
|
108
146
|
note?: string;
|
|
109
147
|
maxUses?: number;
|
|
110
148
|
expiresInMs?: number;
|
|
149
|
+
// Voice invite parameters
|
|
150
|
+
expectedExternalUserId?: string;
|
|
151
|
+
voiceCodeDigits?: number;
|
|
111
152
|
}): IngressResult<InviteResponseData> {
|
|
112
153
|
if (!params.sourceChannel) {
|
|
113
154
|
return { ok: false, error: 'sourceChannel is required for create' };
|
|
114
155
|
}
|
|
156
|
+
|
|
157
|
+
// For voice invites: generate a one-time numeric code, hash it, and pass
|
|
158
|
+
// the hash to the store. The plaintext code is included in the response
|
|
159
|
+
// exactly once and never stored.
|
|
160
|
+
let voiceCode: string | undefined;
|
|
161
|
+
let voiceCodeHash: string | undefined;
|
|
162
|
+
const isVoice = params.sourceChannel === 'voice';
|
|
163
|
+
|
|
164
|
+
if (isVoice) {
|
|
165
|
+
if (!params.expectedExternalUserId) {
|
|
166
|
+
return { ok: false, error: 'expectedExternalUserId is required for voice invites' };
|
|
167
|
+
}
|
|
168
|
+
if (!isValidE164(params.expectedExternalUserId)) {
|
|
169
|
+
return { ok: false, error: 'expectedExternalUserId must be in E.164 format (e.g., +15551234567)' };
|
|
170
|
+
}
|
|
171
|
+
voiceCode = generateVoiceCode(6);
|
|
172
|
+
voiceCodeHash = hashVoiceCode(voiceCode);
|
|
173
|
+
}
|
|
174
|
+
|
|
115
175
|
const { invite, rawToken } = createInvite({
|
|
116
176
|
sourceChannel: params.sourceChannel,
|
|
117
177
|
note: params.note,
|
|
118
178
|
maxUses: params.maxUses,
|
|
119
179
|
expiresInMs: params.expiresInMs,
|
|
180
|
+
...(isVoice ? {
|
|
181
|
+
expectedExternalUserId: params.expectedExternalUserId,
|
|
182
|
+
voiceCodeHash,
|
|
183
|
+
voiceCodeDigits: 6,
|
|
184
|
+
} : {}),
|
|
120
185
|
});
|
|
121
|
-
|
|
186
|
+
// Voice invites must not expose the token — callers must redeem via the
|
|
187
|
+
// identity-bound voice code flow, not the generic token redemption path.
|
|
188
|
+
return { ok: true, data: inviteToResponse(invite, { rawToken: isVoice ? undefined : rawToken, voiceCode }) };
|
|
122
189
|
}
|
|
123
190
|
|
|
124
191
|
export function listIngressInvites(params: {
|
|
@@ -172,6 +239,7 @@ export function redeemIngressInvite(params: {
|
|
|
172
239
|
// ---------------------------------------------------------------------------
|
|
173
240
|
|
|
174
241
|
export { type InviteRedemptionOutcome } from './invite-redemption-service.js';
|
|
242
|
+
export { type VoiceRedemptionOutcome } from './invite-redemption-service.js';
|
|
175
243
|
|
|
176
244
|
export function redeemIngressInviteTyped(params: {
|
|
177
245
|
rawToken: string;
|
|
@@ -185,6 +253,15 @@ export function redeemIngressInviteTyped(params: {
|
|
|
185
253
|
return redeemInviteTyped(params);
|
|
186
254
|
}
|
|
187
255
|
|
|
256
|
+
export function redeemVoiceInviteCode(params: {
|
|
257
|
+
assistantId?: string;
|
|
258
|
+
callerExternalUserId: string;
|
|
259
|
+
sourceChannel: 'voice';
|
|
260
|
+
code: string;
|
|
261
|
+
}): VoiceRedemptionOutcome {
|
|
262
|
+
return redeemVoiceInviteCodeTyped(params);
|
|
263
|
+
}
|
|
264
|
+
|
|
188
265
|
// ---------------------------------------------------------------------------
|
|
189
266
|
// Member operations
|
|
190
267
|
// ---------------------------------------------------------------------------
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* persisted, or returned in the outcome.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { ChannelId } from '../channels/types.js';
|
|
10
11
|
import { getSqlite } from '../memory/db.js';
|
|
11
|
-
import { findByTokenHash, hashToken, markInviteExpired, recordInviteUse,redeemInvite as storeRedeemInvite } from '../memory/ingress-invite-store.js';
|
|
12
|
+
import { findActiveVoiceInvites,findByTokenHash, hashToken, markInviteExpired, recordInviteUse, redeemInvite as storeRedeemInvite } from '../memory/ingress-invite-store.js';
|
|
12
13
|
import { findMember, upsertMember } from '../memory/ingress-member-store.js';
|
|
14
|
+
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
15
|
+
import { hashVoiceCode } from '../util/voice-code.js';
|
|
13
16
|
|
|
14
17
|
// ---------------------------------------------------------------------------
|
|
15
18
|
// Outcome type
|
|
@@ -20,6 +23,13 @@ export type InviteRedemptionOutcome =
|
|
|
20
23
|
| { ok: true; type: 'already_member'; memberId: string }
|
|
21
24
|
| { ok: false; reason: 'invalid_token' | 'expired' | 'revoked' | 'max_uses_reached' | 'channel_mismatch' | 'missing_identity' };
|
|
22
25
|
|
|
26
|
+
// Generic failure reasons for voice redemption — intentionally vague to avoid
|
|
27
|
+
// leaking information about which invites exist or which identity is bound.
|
|
28
|
+
export type VoiceRedemptionOutcome =
|
|
29
|
+
| { ok: true; type: 'redeemed'; memberId: string; inviteId: string }
|
|
30
|
+
| { ok: true; type: 'already_member'; memberId: string }
|
|
31
|
+
| { ok: false; reason: 'invalid_or_expired' };
|
|
32
|
+
|
|
23
33
|
// ---------------------------------------------------------------------------
|
|
24
34
|
// Error-string to typed-reason mapping
|
|
25
35
|
// ---------------------------------------------------------------------------
|
|
@@ -111,6 +121,12 @@ export function redeemInvite(params: {
|
|
|
111
121
|
// Sentinel error used to trigger a transaction rollback when the invite
|
|
112
122
|
// was concurrently revoked/expired between pre-validation and write time.
|
|
113
123
|
const STALE_INVITE = Symbol('stale_invite');
|
|
124
|
+
const canonicalMemberId = existingMember.externalUserId ? canonicalizeInboundIdentity(sourceChannel as ChannelId, existingMember.externalUserId) : null;
|
|
125
|
+
const canonicalCallerId = externalUserId ? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId) : null;
|
|
126
|
+
const memberMatchesSender = !!(canonicalMemberId && canonicalCallerId && canonicalMemberId === canonicalCallerId);
|
|
127
|
+
const preservedDisplayName = memberMatchesSender && existingMember.displayName?.trim().length
|
|
128
|
+
? existingMember.displayName
|
|
129
|
+
: displayName;
|
|
114
130
|
|
|
115
131
|
let reactivated: ReturnType<typeof upsertMember> | undefined;
|
|
116
132
|
try {
|
|
@@ -120,7 +136,8 @@ export function redeemInvite(params: {
|
|
|
120
136
|
sourceChannel,
|
|
121
137
|
externalUserId,
|
|
122
138
|
externalChatId,
|
|
123
|
-
|
|
139
|
+
// Reactivation should not overwrite a guardian-managed nickname.
|
|
140
|
+
displayName: preservedDisplayName,
|
|
124
141
|
username,
|
|
125
142
|
status: 'active',
|
|
126
143
|
policy: 'allow',
|
|
@@ -179,3 +196,125 @@ export function redeemInvite(params: {
|
|
|
179
196
|
inviteId: result.invite.id,
|
|
180
197
|
};
|
|
181
198
|
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// redeemVoiceInviteCode
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Redeem a voice invite code for a caller identified by their E.164 phone number.
|
|
206
|
+
*
|
|
207
|
+
* Unlike token-based redemption, voice redemption:
|
|
208
|
+
* 1. Filters only active voice invites bound to the caller's identity
|
|
209
|
+
* (expectedExternalUserId must match callerExternalUserId).
|
|
210
|
+
* 2. Validates the short numeric code by hashing it and comparing to the
|
|
211
|
+
* stored voiceCodeHash.
|
|
212
|
+
* 3. Enforces expiry and use limits.
|
|
213
|
+
* 4. On success: upserts/reactivates a member with status 'active', policy 'allow'.
|
|
214
|
+
* 5. Consumes one invite use atomically (increment useCount).
|
|
215
|
+
*
|
|
216
|
+
* Failure responses are intentionally generic ("invalid_or_expired") to prevent
|
|
217
|
+
* oracle attacks that could reveal which invites exist or which phone numbers
|
|
218
|
+
* are bound.
|
|
219
|
+
*/
|
|
220
|
+
export function redeemVoiceInviteCode(params: {
|
|
221
|
+
assistantId?: string;
|
|
222
|
+
callerExternalUserId: string;
|
|
223
|
+
sourceChannel: 'voice';
|
|
224
|
+
code: string;
|
|
225
|
+
}): VoiceRedemptionOutcome {
|
|
226
|
+
const { assistantId = 'self', callerExternalUserId, code } = params;
|
|
227
|
+
|
|
228
|
+
if (!callerExternalUserId) {
|
|
229
|
+
return { ok: false, reason: 'invalid_or_expired' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Find all active voice invites bound to the caller's phone number
|
|
233
|
+
const candidates = findActiveVoiceInvites({
|
|
234
|
+
assistantId,
|
|
235
|
+
expectedExternalUserId: callerExternalUserId,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (candidates.length === 0) {
|
|
239
|
+
return { ok: false, reason: 'invalid_or_expired' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const codeHash = hashVoiceCode(code);
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
|
|
245
|
+
// Search for a matching invite: code hash match, not expired, uses remaining
|
|
246
|
+
const invite = candidates.find((inv) => {
|
|
247
|
+
if (inv.voiceCodeHash !== codeHash) return false;
|
|
248
|
+
if (inv.expiresAt <= now) return false;
|
|
249
|
+
if (inv.useCount >= inv.maxUses) return false;
|
|
250
|
+
return true;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (!invite) {
|
|
254
|
+
// Mark any expired candidates while we're here
|
|
255
|
+
for (const inv of candidates) {
|
|
256
|
+
if (inv.expiresAt <= now && inv.status === 'active') {
|
|
257
|
+
markInviteExpired(inv.id);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { ok: false, reason: 'invalid_or_expired' };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Channel enforcement: voice invites can only be redeemed on the voice channel
|
|
264
|
+
if (invite.sourceChannel !== 'voice') {
|
|
265
|
+
return { ok: false, reason: 'invalid_or_expired' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check for existing membership
|
|
269
|
+
const existingMember = findMember({
|
|
270
|
+
assistantId: invite.assistantId,
|
|
271
|
+
sourceChannel: 'voice',
|
|
272
|
+
externalUserId: callerExternalUserId,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (existingMember && existingMember.status === 'active') {
|
|
276
|
+
return { ok: true, type: 'already_member', memberId: existingMember.id };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Blocked members cannot bypass the guardian's explicit block
|
|
280
|
+
if (existingMember && existingMember.status === 'blocked') {
|
|
281
|
+
return { ok: false, reason: 'invalid_or_expired' };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Atomic redemption: upsert member + consume invite use in a transaction
|
|
285
|
+
const STALE_INVITE = Symbol('stale_invite');
|
|
286
|
+
let memberId: string | undefined;
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
getSqlite().transaction(() => {
|
|
290
|
+
const member = upsertMember({
|
|
291
|
+
assistantId: invite.assistantId,
|
|
292
|
+
sourceChannel: 'voice',
|
|
293
|
+
externalUserId: callerExternalUserId,
|
|
294
|
+
status: 'active',
|
|
295
|
+
policy: 'allow',
|
|
296
|
+
inviteId: invite.id,
|
|
297
|
+
});
|
|
298
|
+
memberId = member.id;
|
|
299
|
+
|
|
300
|
+
const recorded = recordInviteUse({
|
|
301
|
+
inviteId: invite.id,
|
|
302
|
+
externalUserId: callerExternalUserId,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (!recorded) throw STALE_INVITE;
|
|
306
|
+
}).immediate();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (err === STALE_INVITE) {
|
|
309
|
+
return { ok: false, reason: 'invalid_or_expired' };
|
|
310
|
+
}
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
type: 'redeemed',
|
|
317
|
+
memberId: memberId!,
|
|
318
|
+
inviteId: invite.id,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
ApprovalUIMetadata,
|
|
12
12
|
} from '../channel-approval-types.js';
|
|
13
13
|
import type { DenialReason } from '../guardian-context-resolver.js';
|
|
14
|
-
export type {
|
|
14
|
+
export type { ActorTrustClass, DenialReason, GuardianContext } from '../guardian-context-resolver.js';
|
|
15
15
|
export { toGuardianRuntimeContext } from '../guardian-context-resolver.js';
|
|
16
16
|
|
|
17
17
|
/** Canonicalize assistantId for channel ingress paths. */
|
|
@@ -283,7 +283,7 @@ export async function handleSendMessage(
|
|
|
283
283
|
const session = await smDeps.getOrCreateSession(mapping.conversationId);
|
|
284
284
|
// HTTP API is a trusted local ingress (same as IPC) — set guardian context
|
|
285
285
|
// so that memory extraction is not silently disabled by unverified provenance.
|
|
286
|
-
session.setGuardianContext({
|
|
286
|
+
session.setGuardianContext({ trustClass: 'guardian', sourceChannel: sourceChannel ?? 'http' });
|
|
287
287
|
const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
|
|
288
288
|
|
|
289
289
|
const attachments = hasAttachments
|
|
@@ -353,7 +353,7 @@ export async function handleSendMessage(
|
|
|
353
353
|
mapping.conversationId,
|
|
354
354
|
content ?? '',
|
|
355
355
|
hasAttachments ? attachmentIds : undefined,
|
|
356
|
-
{ guardianContext: {
|
|
356
|
+
{ guardianContext: { trustClass: 'guardian', sourceChannel } },
|
|
357
357
|
sourceChannel,
|
|
358
358
|
sourceInterface,
|
|
359
359
|
);
|
|
@@ -100,7 +100,7 @@ export async function handleApprovalInterception(
|
|
|
100
100
|
// request targeting this chat, the message might be a decision on behalf
|
|
101
101
|
// of a non-guardian requester.
|
|
102
102
|
if (
|
|
103
|
-
guardianCtx.
|
|
103
|
+
guardianCtx.trustClass === 'guardian' &&
|
|
104
104
|
senderExternalUserId
|
|
105
105
|
) {
|
|
106
106
|
// Callback/button path: deterministic and takes priority.
|
|
@@ -161,7 +161,7 @@ export async function handleApprovalInterception(
|
|
|
161
161
|
if (guardianApproval) {
|
|
162
162
|
// Validate that the sender is the specific guardian who was assigned
|
|
163
163
|
// this approval request. This is a defense-in-depth check — the
|
|
164
|
-
//
|
|
164
|
+
// trustClass check above already verifies the sender is a guardian,
|
|
165
165
|
// but this catches edge cases like binding rotation between request
|
|
166
166
|
// creation and decision.
|
|
167
167
|
if (senderExternalUserId !== guardianApproval.guardianExternalUserId) {
|
|
@@ -548,9 +548,15 @@ export async function handleApprovalInterception(
|
|
|
548
548
|
const pendingPrompt = getChannelApprovalPrompt(conversationId);
|
|
549
549
|
if (!pendingPrompt) return { handled: false };
|
|
550
550
|
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
|
|
551
|
+
// Legacy unverified-channel equivalent:
|
|
552
|
+
// unknown trust + explicit denial reason (`no_identity` / `no_binding`).
|
|
553
|
+
// Unknown without a denial reason means identity-known, non-member sender
|
|
554
|
+
// in a shared channel; that case must not force-reject someone else's request.
|
|
555
|
+
const isLegacyUnverifiedSender = guardianCtx.trustClass === 'unknown' && !!guardianCtx.denialReason;
|
|
556
|
+
|
|
557
|
+
// When the sender is from a legacy-unverified channel actor, auto-deny any
|
|
558
|
+
// pending confirmation and block self-approval.
|
|
559
|
+
if (isLegacyUnverifiedSender) {
|
|
554
560
|
const pending = getApprovalInfoByConversation(conversationId);
|
|
555
561
|
if (pending.length > 0) {
|
|
556
562
|
handleChannelDecision(
|
|
@@ -569,7 +575,12 @@ export async function handleApprovalInterception(
|
|
|
569
575
|
// When the sender is a non-guardian and there's a pending guardian approval
|
|
570
576
|
// for this conversation's request, block self-approval. The non-guardian must
|
|
571
577
|
// wait for the guardian to decide.
|
|
572
|
-
|
|
578
|
+
//
|
|
579
|
+
// Include identity-known, non-member senders (`unknown` without denialReason)
|
|
580
|
+
// so shared-channel participants can't approve/deny someone else's pending request.
|
|
581
|
+
const isIdentityKnownNonGuardian = guardianCtx.trustClass === 'trusted_contact'
|
|
582
|
+
|| (guardianCtx.trustClass === 'unknown' && !guardianCtx.denialReason);
|
|
583
|
+
if (isIdentityKnownNonGuardian) {
|
|
573
584
|
const pending = getApprovalInfoByConversation(conversationId);
|
|
574
585
|
if (pending.length > 0) {
|
|
575
586
|
const guardianApprovalForRequest = getPendingApprovalForRequest(pending[0].requestId);
|
|
@@ -76,6 +76,7 @@ import { handleApprovalInterception } from './guardian-approval-interception.js'
|
|
|
76
76
|
import { deliverGeneratedApprovalPrompt } from './guardian-approval-prompt.js';
|
|
77
77
|
|
|
78
78
|
import '../channel-invite-transports/telegram.js';
|
|
79
|
+
import '../channel-invite-transports/voice.js';
|
|
79
80
|
|
|
80
81
|
const log = getLogger('runtime-http');
|
|
81
82
|
|
|
@@ -229,7 +230,7 @@ export async function handleChannelInbound(
|
|
|
229
230
|
typeof (rawCommandIntentForAcl as Record<string, unknown>).payload === 'string' &&
|
|
230
231
|
((rawCommandIntentForAcl as Record<string, unknown>).payload as string).startsWith('gv_');
|
|
231
232
|
|
|
232
|
-
// Parse invite token from /start
|
|
233
|
+
// Parse invite token from /start payloads using the channel transport
|
|
233
234
|
// adapter. The token is extracted once here so both the ACL bypass and
|
|
234
235
|
// the intercept handler can reference it without re-parsing.
|
|
235
236
|
const commandIntentForAcl = rawCommandIntentForAcl && typeof rawCommandIntentForAcl === 'object' && !Array.isArray(rawCommandIntentForAcl)
|
|
@@ -291,7 +292,7 @@ export async function handleChannelInbound(
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
// ── Invite token intercept (non-member) ──
|
|
294
|
-
// /start
|
|
295
|
+
// /start invite deep links grant access without guardian approval.
|
|
295
296
|
// Intercept here — before the deny gate — so valid invites short-circuit
|
|
296
297
|
// the ACL rejection and never reach the agent pipeline.
|
|
297
298
|
if (inviteToken && denyNonMember) {
|
|
@@ -774,6 +775,21 @@ export async function handleChannelInbound(
|
|
|
774
775
|
const guardianVerifyOutcome: 'verified' | 'failed' = verifyResult.success ? 'verified' : 'failed';
|
|
775
776
|
|
|
776
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
|
+
|
|
777
793
|
upsertMember({
|
|
778
794
|
assistantId: canonicalAssistantId,
|
|
779
795
|
sourceChannel,
|
|
@@ -781,7 +797,8 @@ export async function handleChannelInbound(
|
|
|
781
797
|
externalChatId,
|
|
782
798
|
status: 'active',
|
|
783
799
|
policy: 'allow',
|
|
784
|
-
|
|
800
|
+
// Keep guardian-curated member name stable across re-verification.
|
|
801
|
+
displayName: preservedDisplayName,
|
|
785
802
|
username: body.senderUsername,
|
|
786
803
|
});
|
|
787
804
|
|
|
@@ -898,8 +915,14 @@ export async function handleChannelInbound(
|
|
|
898
915
|
externalChatId,
|
|
899
916
|
senderExternalUserId: rawSenderId,
|
|
900
917
|
senderUsername: body.senderUsername,
|
|
918
|
+
senderDisplayName: body.senderName,
|
|
901
919
|
});
|
|
902
920
|
|
|
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;
|
|
925
|
+
|
|
903
926
|
// ── Canonical guardian reply router ──
|
|
904
927
|
// Attempts to route inbound messages through the canonical decision pipeline
|
|
905
928
|
// before falling through to the legacy approval interception. Handles
|
|
@@ -910,7 +933,7 @@ export async function handleChannelInbound(
|
|
|
910
933
|
replyCallbackUrl &&
|
|
911
934
|
(trimmedContent.length > 0 || hasCallbackData) &&
|
|
912
935
|
rawSenderId &&
|
|
913
|
-
guardianCtx.
|
|
936
|
+
guardianCtx.trustClass === 'guardian'
|
|
914
937
|
) {
|
|
915
938
|
// Compute destination-scoped pending request hints so the router can
|
|
916
939
|
// discover canonical requests delivered to this chat even when the
|
|
@@ -983,13 +1006,21 @@ export async function handleChannelInbound(
|
|
|
983
1006
|
requestId: routerResult.requestId,
|
|
984
1007
|
});
|
|
985
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
if (routerResult.skipApprovalInterception) {
|
|
1011
|
+
skipApprovalInterception = true;
|
|
1012
|
+
}
|
|
986
1013
|
}
|
|
987
1014
|
|
|
988
1015
|
// ── Approval interception ──
|
|
989
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.
|
|
990
1020
|
if (
|
|
991
1021
|
replyCallbackUrl &&
|
|
992
|
-
!result.duplicate
|
|
1022
|
+
!result.duplicate &&
|
|
1023
|
+
!skipApprovalInterception
|
|
993
1024
|
) {
|
|
994
1025
|
const approvalResult = await handleApprovalInterception({
|
|
995
1026
|
conversationId: result.conversationId,
|
|
@@ -1369,7 +1400,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1369
1400
|
conversationId: string;
|
|
1370
1401
|
sourceChannel: ChannelId;
|
|
1371
1402
|
externalChatId: string;
|
|
1372
|
-
|
|
1403
|
+
guardianTrustClass: GuardianContext['trustClass'];
|
|
1373
1404
|
replyCallbackUrl: string;
|
|
1374
1405
|
bearerToken?: string;
|
|
1375
1406
|
assistantId?: string;
|
|
@@ -1379,7 +1410,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1379
1410
|
conversationId,
|
|
1380
1411
|
sourceChannel,
|
|
1381
1412
|
externalChatId,
|
|
1382
|
-
|
|
1413
|
+
guardianTrustClass,
|
|
1383
1414
|
replyCallbackUrl,
|
|
1384
1415
|
bearerToken,
|
|
1385
1416
|
assistantId,
|
|
@@ -1388,7 +1419,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1388
1419
|
|
|
1389
1420
|
// Approval prompt delivery is guardian-only. Non-guardian and unverified
|
|
1390
1421
|
// actors must never receive approval prompt broadcasts for the conversation.
|
|
1391
|
-
if (
|
|
1422
|
+
if (guardianTrustClass !== 'guardian') {
|
|
1392
1423
|
return () => {};
|
|
1393
1424
|
}
|
|
1394
1425
|
|
|
@@ -1470,7 +1501,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1470
1501
|
conversationId,
|
|
1471
1502
|
sourceChannel,
|
|
1472
1503
|
externalChatId,
|
|
1473
|
-
|
|
1504
|
+
guardianTrustClass: guardianCtx.trustClass,
|
|
1474
1505
|
replyCallbackUrl,
|
|
1475
1506
|
bearerToken,
|
|
1476
1507
|
assistantId,
|
|
@@ -1494,7 +1525,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1494
1525
|
},
|
|
1495
1526
|
assistantId,
|
|
1496
1527
|
guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
|
|
1497
|
-
isInteractive: guardianCtx.
|
|
1528
|
+
isInteractive: guardianCtx.trustClass === 'guardian',
|
|
1498
1529
|
...(cmdIntent ? { commandIntent: cmdIntent } : {}),
|
|
1499
1530
|
},
|
|
1500
1531
|
sourceChannel,
|