@openclaw/voice-call 2026.8.2 → 2026.9.1
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/dist/api.js +2 -2
- package/dist/cli-DNq2qUU0.js +630 -0
- package/dist/config-DQwvsxeV.js +697 -0
- package/dist/{guarded-json-api-BL8mhcJj.js → guarded-json-api-9je4i7qf.js} +1 -1
- package/dist/index.js +17 -580
- package/dist/{plivo-CgYWuWVr.js → plivo-BfKoYCMW.js} +2 -2
- package/dist/{realtime-handler-oehCkuE2.js → realtime-handler-BYWqpZ6N.js} +23 -16
- package/dist/{response-generator-BPKgHnuI.js → response-generator-C0577tL-.js} +2 -1
- package/dist/runtime-api.js +2 -2
- package/dist/{runtime-entry-CJkbbEgg.js → runtime-entry-gE2Yd4V8.js} +33 -715
- package/dist/runtime-entry.js +1 -1
- package/dist/{telnyx-BWmhDBtd.js → telnyx-rcjslHOP.js} +1 -1
- package/dist/{twilio-78mzH8Lk.js → twilio-B100ALoj.js} +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { TtsConfigSchema } from "./runtime-api.js";
|
|
2
|
+
import "./api.js";
|
|
3
|
+
import { t as TWILIO_REGIONS } from "./twilio-region-4fkgz3UG.js";
|
|
4
|
+
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
5
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES, resolveRealtimeVoiceAgentConsultTools } from "openclaw/plugin-sdk/realtime-voice";
|
|
6
|
+
import { mergeDeep } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
7
|
+
import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
8
|
+
import { canonicalizeMainSessionAlias } from "openclaw/plugin-sdk/session-store-runtime";
|
|
9
|
+
import { resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core";
|
|
10
|
+
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { MAX_TCP_PORT } from "openclaw/plugin-sdk/number-runtime";
|
|
13
|
+
//#region extensions/voice-call/src/realtime-call-control.ts
|
|
14
|
+
/** Stable provider-facing tool name for ending the current phone call. */
|
|
15
|
+
const REALTIME_VOICE_END_CALL_TOOL_NAME = "openclaw_end_call";
|
|
16
|
+
/** Closure-bound end-call control exposed on every realtime phone call. */
|
|
17
|
+
const REALTIME_VOICE_END_CALL_TOOL = {
|
|
18
|
+
type: "function",
|
|
19
|
+
name: REALTIME_VOICE_END_CALL_TOOL_NAME,
|
|
20
|
+
description: "End the current phone call immediately. Speak any final words to the caller before invoking this tool because the call ends as soon as it is invoked and no further reply will be spoken.",
|
|
21
|
+
parameters: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Merge built-in call controls with consult and configured realtime tools. */
|
|
27
|
+
function resolveVoiceCallRealtimeTools(policy, customTools = []) {
|
|
28
|
+
const tools = resolveRealtimeVoiceAgentConsultTools(policy, customTools).filter((tool) => tool.name !== REALTIME_VOICE_END_CALL_TOOL_NAME);
|
|
29
|
+
return [REALTIME_VOICE_END_CALL_TOOL, ...tools];
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region extensions/voice-call/src/realtime-defaults.ts
|
|
33
|
+
/** Baseline instructions that keep realtime calls brief and route deep work to agent consult. */
|
|
34
|
+
const DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS = `You are OpenClaw's phone-call realtime voice interface. Keep spoken replies brief and natural. When a question needs deeper reasoning, current information, or tools, call ${REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME} before answering. When the caller asks to end the call, speak any final words first, then call ${REALTIME_VOICE_END_CALL_TOOL_NAME}; it ends the call immediately and nothing after it will be spoken.`;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region extensions/voice-call/src/tailscale-config.ts
|
|
37
|
+
const VoiceCallTailscaleConfigSchema = z.object({
|
|
38
|
+
mode: z.enum([
|
|
39
|
+
"off",
|
|
40
|
+
"serve",
|
|
41
|
+
"funnel"
|
|
42
|
+
]).default("off"),
|
|
43
|
+
port: z.number().int().positive().max(MAX_TCP_PORT).default(443),
|
|
44
|
+
path: z.string().min(1).default("/voice/webhook")
|
|
45
|
+
}).strict().default({
|
|
46
|
+
mode: "off",
|
|
47
|
+
port: 443,
|
|
48
|
+
path: "/voice/webhook"
|
|
49
|
+
});
|
|
50
|
+
const TAILSCALE_FUNNEL_HTTPS_PORTS = /* @__PURE__ */ new Set([
|
|
51
|
+
443,
|
|
52
|
+
8443,
|
|
53
|
+
1e4
|
|
54
|
+
]);
|
|
55
|
+
function isTailscalePortAllowed(config) {
|
|
56
|
+
return !(config.tailscale.mode === "funnel" || config.tunnel.provider === "tailscale-funnel") || TAILSCALE_FUNNEL_HTTPS_PORTS.has(config.tailscale.port);
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region extensions/voice-call/src/config.ts
|
|
60
|
+
/**
|
|
61
|
+
* E.164 phone number format: +[country code][number]
|
|
62
|
+
* Examples use 555 prefix (reserved for fictional numbers)
|
|
63
|
+
*/
|
|
64
|
+
const E164Schema = z.string().regex(/^\+[1-9]\d{1,14}$/, "Expected E.164 format, e.g. +15550001234");
|
|
65
|
+
/**
|
|
66
|
+
* Controls how inbound calls are handled:
|
|
67
|
+
* - "disabled": Block all inbound calls (outbound only)
|
|
68
|
+
* - "allowlist": Only accept calls from numbers in allowFrom
|
|
69
|
+
* - "pairing": Unknown callers can request pairing (future)
|
|
70
|
+
* - "open": Accept all inbound calls (dangerous!)
|
|
71
|
+
*/
|
|
72
|
+
const InboundPolicySchema = z.enum([
|
|
73
|
+
"disabled",
|
|
74
|
+
"allowlist",
|
|
75
|
+
"pairing",
|
|
76
|
+
"open"
|
|
77
|
+
]);
|
|
78
|
+
const SecretInputSchema = buildSecretInputSchema();
|
|
79
|
+
const TelnyxConfigSchema = z.object({
|
|
80
|
+
/** Telnyx API v2 key */
|
|
81
|
+
apiKey: z.string().min(1).optional(),
|
|
82
|
+
/** Telnyx connection ID (from Call Control app) */
|
|
83
|
+
connectionId: z.string().min(1).optional(),
|
|
84
|
+
/** Public key for webhook signature verification */
|
|
85
|
+
publicKey: z.string().min(1).optional()
|
|
86
|
+
}).strict();
|
|
87
|
+
const TwilioConfigSchema = z.object({
|
|
88
|
+
/** Twilio Account SID */
|
|
89
|
+
accountSid: z.string().min(1).optional(),
|
|
90
|
+
/** Twilio Auth Token */
|
|
91
|
+
authToken: SecretInputSchema.optional(),
|
|
92
|
+
/** Twilio processing Region (for example, ie1) */
|
|
93
|
+
region: z.enum(TWILIO_REGIONS).optional()
|
|
94
|
+
}).strict();
|
|
95
|
+
const PlivoConfigSchema = z.object({
|
|
96
|
+
/** Plivo Auth ID (starts with MA/SA) */
|
|
97
|
+
authId: z.string().min(1).optional(),
|
|
98
|
+
/** Plivo Auth Token */
|
|
99
|
+
authToken: z.string().min(1).optional()
|
|
100
|
+
}).strict();
|
|
101
|
+
const VoiceCallNumberRouteConfigSchema = z.object({
|
|
102
|
+
/** Greeting message for inbound calls to this number. */
|
|
103
|
+
inboundGreeting: z.string().optional(),
|
|
104
|
+
/** TTS override for inbound calls to this number. Deep-merges with global voice-call TTS. */
|
|
105
|
+
tts: TtsConfigSchema,
|
|
106
|
+
/** Agent ID to use for voice response generation for this number. */
|
|
107
|
+
agentId: z.string().min(1).optional(),
|
|
108
|
+
/** Optional model override for voice responses for this number. */
|
|
109
|
+
responseModel: z.string().optional(),
|
|
110
|
+
/** System prompt for voice responses for this number. */
|
|
111
|
+
responseSystemPrompt: z.string().optional(),
|
|
112
|
+
/** Timeout for response generation in ms for this number. */
|
|
113
|
+
responseTimeoutMs: z.number().int().positive().optional()
|
|
114
|
+
}).strict();
|
|
115
|
+
const VoiceCallServeConfigSchema = z.object({
|
|
116
|
+
/** Port to listen on */
|
|
117
|
+
port: z.number().int().positive().default(3334),
|
|
118
|
+
/** Bind address */
|
|
119
|
+
bind: z.string().default("127.0.0.1"),
|
|
120
|
+
/** Webhook path */
|
|
121
|
+
path: z.string().min(1).default("/voice/webhook")
|
|
122
|
+
}).strict().default({
|
|
123
|
+
port: 3334,
|
|
124
|
+
bind: "127.0.0.1",
|
|
125
|
+
path: "/voice/webhook"
|
|
126
|
+
});
|
|
127
|
+
const VoiceCallTunnelConfigSchema = z.object({
|
|
128
|
+
/**
|
|
129
|
+
* Tunnel provider:
|
|
130
|
+
* - "none": No tunnel (use publicUrl if set, or manual setup)
|
|
131
|
+
* - "ngrok": Use ngrok for public HTTPS tunnel
|
|
132
|
+
* - "tailscale-serve": Tailscale serve (private to tailnet)
|
|
133
|
+
* - "tailscale-funnel": Tailscale funnel (public HTTPS)
|
|
134
|
+
*/
|
|
135
|
+
provider: z.enum([
|
|
136
|
+
"none",
|
|
137
|
+
"ngrok",
|
|
138
|
+
"tailscale-serve",
|
|
139
|
+
"tailscale-funnel"
|
|
140
|
+
]).default("none"),
|
|
141
|
+
/** ngrok auth token (optional, enables longer sessions and more features) */
|
|
142
|
+
ngrokAuthToken: z.string().min(1).optional(),
|
|
143
|
+
/** ngrok custom domain (paid feature, e.g., "myapp.ngrok.io") */
|
|
144
|
+
ngrokDomain: z.string().min(1).optional(),
|
|
145
|
+
/**
|
|
146
|
+
* Allow ngrok free tier compatibility mode.
|
|
147
|
+
* When true, forwarded headers may be trusted for loopback requests
|
|
148
|
+
* to reconstruct the public ngrok URL used for signing.
|
|
149
|
+
*
|
|
150
|
+
* IMPORTANT: This does NOT bypass signature verification.
|
|
151
|
+
*/
|
|
152
|
+
allowNgrokFreeTierLoopbackBypass: z.boolean().default(false)
|
|
153
|
+
}).strict().default({
|
|
154
|
+
provider: "none",
|
|
155
|
+
allowNgrokFreeTierLoopbackBypass: false
|
|
156
|
+
});
|
|
157
|
+
const VoiceCallWebhookSecurityConfigSchema = z.object({
|
|
158
|
+
/**
|
|
159
|
+
* Allowed hostnames for webhook URL reconstruction.
|
|
160
|
+
* Only these hosts are accepted from forwarding headers.
|
|
161
|
+
*/
|
|
162
|
+
allowedHosts: z.array(z.string().min(1)).default([]),
|
|
163
|
+
/**
|
|
164
|
+
* Trust X-Forwarded-* headers without a hostname allowlist.
|
|
165
|
+
* WARNING: Only enable if you trust your proxy configuration.
|
|
166
|
+
*/
|
|
167
|
+
trustForwardingHeaders: z.boolean().default(false),
|
|
168
|
+
/**
|
|
169
|
+
* Trusted proxy IP addresses. Forwarded headers are only trusted when
|
|
170
|
+
* the remote IP matches one of these addresses.
|
|
171
|
+
*/
|
|
172
|
+
trustedProxyIPs: z.array(z.string().min(1)).default([])
|
|
173
|
+
}).strict().default({
|
|
174
|
+
allowedHosts: [],
|
|
175
|
+
trustForwardingHeaders: false,
|
|
176
|
+
trustedProxyIPs: []
|
|
177
|
+
});
|
|
178
|
+
/**
|
|
179
|
+
* Call mode determines how outbound calls behave:
|
|
180
|
+
* - "notify": Deliver message and auto-hangup after delay (one-way notification)
|
|
181
|
+
* - "conversation": Stay open for back-and-forth until explicit end or timeout
|
|
182
|
+
*/
|
|
183
|
+
const CallModeSchema = z.enum(["notify", "conversation"]);
|
|
184
|
+
const VoiceCallSessionScopeSchema = z.enum([
|
|
185
|
+
"per-phone",
|
|
186
|
+
"per-call",
|
|
187
|
+
"main"
|
|
188
|
+
]);
|
|
189
|
+
const OutboundConfigSchema = z.object({
|
|
190
|
+
/** Default call mode for outbound calls */
|
|
191
|
+
defaultMode: CallModeSchema.default("notify"),
|
|
192
|
+
/** Seconds to wait after TTS before auto-hangup in notify mode */
|
|
193
|
+
notifyHangupDelaySec: z.number().int().nonnegative().default(3)
|
|
194
|
+
}).strict().default({
|
|
195
|
+
defaultMode: "notify",
|
|
196
|
+
notifyHangupDelaySec: 3
|
|
197
|
+
});
|
|
198
|
+
const RealtimeToolSchema = z.object({
|
|
199
|
+
type: z.literal("function"),
|
|
200
|
+
name: z.string().min(1),
|
|
201
|
+
description: z.string(),
|
|
202
|
+
parameters: z.object({
|
|
203
|
+
type: z.literal("object"),
|
|
204
|
+
properties: z.record(z.string(), z.unknown()),
|
|
205
|
+
required: z.array(z.string()).optional()
|
|
206
|
+
})
|
|
207
|
+
}).strict();
|
|
208
|
+
const VoiceCallRealtimeProvidersConfigSchema = z.record(z.string(), z.record(z.string(), z.unknown())).default({});
|
|
209
|
+
const VoiceCallRealtimeToolPolicySchema = z.enum(REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES);
|
|
210
|
+
const VoiceCallRealtimeConsultPolicySchema = z.enum([
|
|
211
|
+
"auto",
|
|
212
|
+
"substantive",
|
|
213
|
+
"always"
|
|
214
|
+
]);
|
|
215
|
+
const VoiceCallRealtimeFastContextSourceSchema = z.enum(["memory", "sessions"]);
|
|
216
|
+
const VoiceCallRealtimeFastContextConfigSchema = z.object({
|
|
217
|
+
/** Enable bounded memory/session lookup before the full consult agent. */
|
|
218
|
+
enabled: z.boolean().default(false),
|
|
219
|
+
/** Hard deadline for the fast context lookup. */
|
|
220
|
+
timeoutMs: z.number().int().positive().default(800),
|
|
221
|
+
/** Maximum memory/session hits to inject into the realtime tool result. */
|
|
222
|
+
maxResults: z.number().int().positive().default(3),
|
|
223
|
+
/** Indexed sources used by the fast context lookup. */
|
|
224
|
+
sources: z.array(VoiceCallRealtimeFastContextSourceSchema).min(1).default(["memory", "sessions"]),
|
|
225
|
+
/** Fall back to the full agent consult when fast context has no answer. */
|
|
226
|
+
fallbackToConsult: z.boolean().default(false)
|
|
227
|
+
}).strict().default({
|
|
228
|
+
enabled: false,
|
|
229
|
+
timeoutMs: 800,
|
|
230
|
+
maxResults: 3,
|
|
231
|
+
sources: ["memory", "sessions"],
|
|
232
|
+
fallbackToConsult: false
|
|
233
|
+
});
|
|
234
|
+
const VoiceCallRealtimeAgentContextConfigSchema = z.object({
|
|
235
|
+
/** Inject a compact agent persona/context capsule into realtime voice instructions. */
|
|
236
|
+
enabled: z.boolean().default(false),
|
|
237
|
+
/** Maximum number of characters from the generated capsule to append. */
|
|
238
|
+
maxChars: z.number().int().positive().default(6e3),
|
|
239
|
+
/** Include configured agent identity fields. */
|
|
240
|
+
includeIdentity: z.boolean().default(true),
|
|
241
|
+
/** Include selected workspace files such as SOUL.md and IDENTITY.md. */
|
|
242
|
+
includeWorkspaceFiles: z.boolean().default(true),
|
|
243
|
+
/** Workspace-relative files to include, bounded by maxChars. */
|
|
244
|
+
files: z.array(z.string().min(1)).default([
|
|
245
|
+
"SOUL.md",
|
|
246
|
+
"IDENTITY.md",
|
|
247
|
+
"USER.md"
|
|
248
|
+
])
|
|
249
|
+
}).strict().default({
|
|
250
|
+
enabled: false,
|
|
251
|
+
maxChars: 6e3,
|
|
252
|
+
includeIdentity: true,
|
|
253
|
+
includeWorkspaceFiles: true,
|
|
254
|
+
files: [
|
|
255
|
+
"SOUL.md",
|
|
256
|
+
"IDENTITY.md",
|
|
257
|
+
"USER.md"
|
|
258
|
+
]
|
|
259
|
+
});
|
|
260
|
+
const VoiceCallRealtimeConsultThinkingLevelSchema = z.enum([
|
|
261
|
+
"off",
|
|
262
|
+
"minimal",
|
|
263
|
+
"low",
|
|
264
|
+
"medium",
|
|
265
|
+
"high",
|
|
266
|
+
"xhigh",
|
|
267
|
+
"adaptive",
|
|
268
|
+
"max",
|
|
269
|
+
"ultra"
|
|
270
|
+
]);
|
|
271
|
+
const VoiceCallStreamingProvidersConfigSchema = z.record(z.string(), z.record(z.string(), z.unknown())).default({});
|
|
272
|
+
const VoiceCallRealtimeConfigSchema = z.object({
|
|
273
|
+
/** Enable realtime voice-to-voice mode. */
|
|
274
|
+
enabled: z.boolean().default(false),
|
|
275
|
+
/** Provider id from registered realtime voice providers. */
|
|
276
|
+
provider: z.string().min(1).optional(),
|
|
277
|
+
/** Optional override for the local WebSocket route path. */
|
|
278
|
+
streamPath: z.string().min(1).optional(),
|
|
279
|
+
/** System instructions passed to the realtime provider. */
|
|
280
|
+
instructions: z.string().default(DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS),
|
|
281
|
+
/** Tool policy for the shared OpenClaw agent consult tool. */
|
|
282
|
+
toolPolicy: VoiceCallRealtimeToolPolicySchema.default("safe-read-only"),
|
|
283
|
+
/** Guidance for when the realtime model should call the OpenClaw agent consult tool. */
|
|
284
|
+
consultPolicy: VoiceCallRealtimeConsultPolicySchema.default("auto"),
|
|
285
|
+
/** Optional thinking level override for the regular agent behind realtime consults. */
|
|
286
|
+
consultThinkingLevel: VoiceCallRealtimeConsultThinkingLevelSchema.optional(),
|
|
287
|
+
/** Optional fast mode override for the regular agent behind realtime consults. */
|
|
288
|
+
consultFastMode: z.boolean().optional(),
|
|
289
|
+
/** Tool definitions exposed to the realtime provider. */
|
|
290
|
+
tools: z.array(RealtimeToolSchema).default([]),
|
|
291
|
+
/** Low-latency memory/session context for the consult tool. */
|
|
292
|
+
fastContext: VoiceCallRealtimeFastContextConfigSchema,
|
|
293
|
+
/** Bounded agent persona/context injection for the fast realtime voice path. */
|
|
294
|
+
agentContext: VoiceCallRealtimeAgentContextConfigSchema,
|
|
295
|
+
/** Provider-owned raw config blobs keyed by provider id. */
|
|
296
|
+
providers: VoiceCallRealtimeProvidersConfigSchema
|
|
297
|
+
}).strict().default({
|
|
298
|
+
enabled: false,
|
|
299
|
+
instructions: DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS,
|
|
300
|
+
toolPolicy: "safe-read-only",
|
|
301
|
+
consultPolicy: "auto",
|
|
302
|
+
tools: [],
|
|
303
|
+
fastContext: {
|
|
304
|
+
enabled: false,
|
|
305
|
+
timeoutMs: 800,
|
|
306
|
+
maxResults: 3,
|
|
307
|
+
sources: ["memory", "sessions"],
|
|
308
|
+
fallbackToConsult: false
|
|
309
|
+
},
|
|
310
|
+
agentContext: {
|
|
311
|
+
enabled: false,
|
|
312
|
+
maxChars: 6e3,
|
|
313
|
+
includeIdentity: true,
|
|
314
|
+
includeWorkspaceFiles: true,
|
|
315
|
+
files: [
|
|
316
|
+
"SOUL.md",
|
|
317
|
+
"IDENTITY.md",
|
|
318
|
+
"USER.md"
|
|
319
|
+
]
|
|
320
|
+
},
|
|
321
|
+
providers: {}
|
|
322
|
+
});
|
|
323
|
+
const VoiceCallStreamingConfigSchema = z.object({
|
|
324
|
+
/** Enable Twilio Media Streams for real-time transcription. */
|
|
325
|
+
enabled: z.boolean().default(false),
|
|
326
|
+
/** Provider id from registered realtime transcription providers. */
|
|
327
|
+
provider: z.string().min(1).optional(),
|
|
328
|
+
/** WebSocket path for media stream connections */
|
|
329
|
+
streamPath: z.string().min(1).default("/voice/stream"),
|
|
330
|
+
/** Provider-owned raw config blobs keyed by provider id. */
|
|
331
|
+
providers: VoiceCallStreamingProvidersConfigSchema,
|
|
332
|
+
/**
|
|
333
|
+
* Close unauthenticated media stream sockets if no valid `start` frame arrives in time.
|
|
334
|
+
* Protects against pre-auth idle connection hold attacks.
|
|
335
|
+
*/
|
|
336
|
+
preStartTimeoutMs: z.number().int().positive().default(5e3),
|
|
337
|
+
/** Maximum number of concurrently pending (pre-start) media stream sockets. */
|
|
338
|
+
maxPendingConnections: z.number().int().positive().default(32),
|
|
339
|
+
/** Maximum pending media stream sockets per source IP. */
|
|
340
|
+
maxPendingConnectionsPerIp: z.number().int().positive().default(4),
|
|
341
|
+
/** Hard cap for all open media stream sockets (pending + active). */
|
|
342
|
+
maxConnections: z.number().int().positive().default(128)
|
|
343
|
+
}).strict().default({
|
|
344
|
+
enabled: false,
|
|
345
|
+
streamPath: "/voice/stream",
|
|
346
|
+
providers: {},
|
|
347
|
+
preStartTimeoutMs: 5e3,
|
|
348
|
+
maxPendingConnections: 32,
|
|
349
|
+
maxPendingConnectionsPerIp: 4,
|
|
350
|
+
maxConnections: 128
|
|
351
|
+
});
|
|
352
|
+
const VoiceCallConfigSchema = z.object({
|
|
353
|
+
/** Enable voice call functionality */
|
|
354
|
+
enabled: z.boolean().default(false),
|
|
355
|
+
/** Active provider (telnyx, twilio, plivo, or mock) */
|
|
356
|
+
provider: z.enum([
|
|
357
|
+
"telnyx",
|
|
358
|
+
"twilio",
|
|
359
|
+
"plivo",
|
|
360
|
+
"mock"
|
|
361
|
+
]).optional(),
|
|
362
|
+
/** Telnyx-specific configuration */
|
|
363
|
+
telnyx: TelnyxConfigSchema.optional(),
|
|
364
|
+
/** Twilio-specific configuration */
|
|
365
|
+
twilio: TwilioConfigSchema.optional(),
|
|
366
|
+
/** Plivo-specific configuration */
|
|
367
|
+
plivo: PlivoConfigSchema.optional(),
|
|
368
|
+
/** Phone number to call from (E.164) */
|
|
369
|
+
fromNumber: E164Schema.optional(),
|
|
370
|
+
/** Default phone number to call (E.164) */
|
|
371
|
+
toNumber: E164Schema.optional(),
|
|
372
|
+
/** Inbound call policy */
|
|
373
|
+
inboundPolicy: InboundPolicySchema.default("disabled"),
|
|
374
|
+
/** Allowlist of phone numbers for inbound calls (E.164) */
|
|
375
|
+
allowFrom: z.array(E164Schema).default([]),
|
|
376
|
+
/** Greeting message for inbound calls */
|
|
377
|
+
inboundGreeting: z.string().optional(),
|
|
378
|
+
/** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */
|
|
379
|
+
numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}),
|
|
380
|
+
/** Outbound call configuration */
|
|
381
|
+
outbound: OutboundConfigSchema,
|
|
382
|
+
/** Maximum call duration in seconds */
|
|
383
|
+
maxDurationSeconds: z.number().int().positive().default(300),
|
|
384
|
+
/**
|
|
385
|
+
* Maximum age of a call in seconds before it is automatically reaped.
|
|
386
|
+
* Catches calls stuck before answer (for example, local mock calls that
|
|
387
|
+
* never receive provider webhooks). Set to 0 to disable.
|
|
388
|
+
*/
|
|
389
|
+
staleCallReaperSeconds: z.number().int().nonnegative().default(120),
|
|
390
|
+
/** Silence timeout for end-of-speech detection (ms) */
|
|
391
|
+
silenceTimeoutMs: z.number().int().positive().default(800),
|
|
392
|
+
/** Timeout for user transcript (ms) */
|
|
393
|
+
transcriptTimeoutMs: z.number().int().positive().default(18e4),
|
|
394
|
+
/** Ring timeout for outbound calls (ms) */
|
|
395
|
+
ringTimeoutMs: z.number().int().positive().default(3e4),
|
|
396
|
+
/** Maximum concurrent calls */
|
|
397
|
+
maxConcurrentCalls: z.number().int().positive().default(1),
|
|
398
|
+
/** Webhook server configuration */
|
|
399
|
+
serve: VoiceCallServeConfigSchema,
|
|
400
|
+
/** @deprecated Prefer tunnel config. */
|
|
401
|
+
tailscale: VoiceCallTailscaleConfigSchema,
|
|
402
|
+
/** Tunnel configuration (unified ngrok/tailscale) */
|
|
403
|
+
tunnel: VoiceCallTunnelConfigSchema,
|
|
404
|
+
/** Webhook signature reconstruction and proxy trust configuration */
|
|
405
|
+
webhookSecurity: VoiceCallWebhookSecurityConfigSchema,
|
|
406
|
+
/** Real-time audio streaming configuration */
|
|
407
|
+
streaming: VoiceCallStreamingConfigSchema,
|
|
408
|
+
/** Realtime voice-to-voice configuration */
|
|
409
|
+
realtime: VoiceCallRealtimeConfigSchema,
|
|
410
|
+
/** Session memory scope for voice conversations. */
|
|
411
|
+
sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
|
|
412
|
+
/** Public webhook URL override (if set, bypasses tunnel auto-detection) */
|
|
413
|
+
publicUrl: z.string().url().optional(),
|
|
414
|
+
/** Skip webhook signature verification (development only, NOT for production) */
|
|
415
|
+
skipSignatureVerification: z.boolean().default(false),
|
|
416
|
+
/** TTS override (deep-merges with core tts) */
|
|
417
|
+
tts: TtsConfigSchema,
|
|
418
|
+
/** Store path for call logs */
|
|
419
|
+
store: z.string().optional(),
|
|
420
|
+
/** Response/session owner. Required when multiple agents have no legacy owner. */
|
|
421
|
+
agentId: z.string().min(1).optional(),
|
|
422
|
+
/** Optional model override for generating voice responses. */
|
|
423
|
+
responseModel: z.string().optional(),
|
|
424
|
+
/** System prompt for voice responses */
|
|
425
|
+
responseSystemPrompt: z.string().optional(),
|
|
426
|
+
/** Timeout for response generation in ms (default 30s) */
|
|
427
|
+
responseTimeoutMs: z.number().int().positive().default(3e4)
|
|
428
|
+
}).strict().refine(isTailscalePortAllowed, {
|
|
429
|
+
path: ["tailscale", "port"],
|
|
430
|
+
message: "Tailscale Funnel HTTPS port must be one of 443, 8443, 10000"
|
|
431
|
+
});
|
|
432
|
+
const TWILIO_AUTH_TOKEN_PATH = "plugins.entries.voice-call.config.twilio.authToken";
|
|
433
|
+
const DEFAULT_VOICE_CALL_CONFIG = VoiceCallConfigSchema.parse({});
|
|
434
|
+
function cloneDefaultVoiceCallConfig() {
|
|
435
|
+
return structuredClone(DEFAULT_VOICE_CALL_CONFIG);
|
|
436
|
+
}
|
|
437
|
+
function defaultRealtimeStreamPathForServePath(servePath) {
|
|
438
|
+
const normalized = normalizeWebhookPath(servePath);
|
|
439
|
+
if (normalized.endsWith("/webhook")) return `${normalized.slice(0, -8)}/stream/realtime`;
|
|
440
|
+
if (normalized === "/") return "/voice/stream/realtime";
|
|
441
|
+
return `${normalized}/stream/realtime`;
|
|
442
|
+
}
|
|
443
|
+
function resolveVoiceCallPublicPathPrefix(publicWebhookPath, localWebhookPath) {
|
|
444
|
+
const publicPath = normalizeWebhookPath(publicWebhookPath);
|
|
445
|
+
const localPathIndex = publicPath.indexOf(normalizeWebhookPath(localWebhookPath));
|
|
446
|
+
return localPathIndex > 0 ? publicPath.slice(0, localPathIndex) : "";
|
|
447
|
+
}
|
|
448
|
+
function resolveVoiceCallStreamExposurePaths(config, webhookPaths = {}) {
|
|
449
|
+
const exposurePaths = [];
|
|
450
|
+
const localWebhookPath = webhookPaths.localWebhookPath ?? config.serve.path;
|
|
451
|
+
const publicPathPrefix = resolveVoiceCallPublicPathPrefix(webhookPaths.publicWebhookPath ?? config.tailscale.path, localWebhookPath);
|
|
452
|
+
if (config.realtime.enabled) {
|
|
453
|
+
const localPath = normalizeWebhookPath(config.realtime.streamPath ?? defaultRealtimeStreamPathForServePath(config.serve.path));
|
|
454
|
+
exposurePaths.push({
|
|
455
|
+
localPath,
|
|
456
|
+
publicPath: `${publicPathPrefix}${localPath}`
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (config.streaming.enabled) {
|
|
460
|
+
const localPath = normalizeWebhookPath(config.streaming.streamPath);
|
|
461
|
+
if (!exposurePaths.some((path) => path.localPath === localPath && path.publicPath === localPath)) exposurePaths.push({
|
|
462
|
+
localPath,
|
|
463
|
+
publicPath: localPath
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return exposurePaths;
|
|
467
|
+
}
|
|
468
|
+
function normalizeVoiceCallTtsConfig(defaults, overrides) {
|
|
469
|
+
if (!defaults && !overrides) return;
|
|
470
|
+
return TtsConfigSchema.parse(mergeDeep(defaults ?? {}, overrides ?? {}));
|
|
471
|
+
}
|
|
472
|
+
function normalizePhoneRouteKey(phone) {
|
|
473
|
+
return phone?.replace(/\D/g, "") ?? "";
|
|
474
|
+
}
|
|
475
|
+
function resolveVoiceCallNumberRouteKey(config, phone) {
|
|
476
|
+
const routes = config.numbers;
|
|
477
|
+
if (!routes) return;
|
|
478
|
+
if (phone && Object.hasOwn(routes, phone)) return phone;
|
|
479
|
+
const normalizedPhone = normalizePhoneRouteKey(phone);
|
|
480
|
+
if (!normalizedPhone) return;
|
|
481
|
+
return Object.keys(routes).find((routeKey) => normalizePhoneRouteKey(routeKey) === normalizedPhone);
|
|
482
|
+
}
|
|
483
|
+
/** Resolve inbound-only number routing from a persisted call record. */
|
|
484
|
+
function resolveVoiceCallNumberRouteKeyForCall(call) {
|
|
485
|
+
if (call.direction !== "inbound") return;
|
|
486
|
+
const storedRouteKey = call.metadata?.numberRouteKey;
|
|
487
|
+
if (typeof storedRouteKey === "string") return storedRouteKey;
|
|
488
|
+
return call.to;
|
|
489
|
+
}
|
|
490
|
+
function resolveVoiceCallEffectiveConfig(config, phoneOrRouteKey) {
|
|
491
|
+
const numberRouteKey = resolveVoiceCallNumberRouteKey(config, phoneOrRouteKey);
|
|
492
|
+
if (!numberRouteKey) return { config };
|
|
493
|
+
const route = config.numbers[numberRouteKey];
|
|
494
|
+
if (!route) return { config };
|
|
495
|
+
return {
|
|
496
|
+
numberRouteKey,
|
|
497
|
+
config: {
|
|
498
|
+
...config,
|
|
499
|
+
...route,
|
|
500
|
+
tts: normalizeVoiceCallTtsConfig(config.tts, route.tts),
|
|
501
|
+
numbers: config.numbers
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function sanitizeVoiceCallProviderConfigs(value) {
|
|
506
|
+
if (!value) return {};
|
|
507
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
|
|
508
|
+
}
|
|
509
|
+
function sanitizeVoiceCallNumberRoutes(value) {
|
|
510
|
+
if (!value) return {};
|
|
511
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0).map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]));
|
|
512
|
+
}
|
|
513
|
+
function resolveTwilioAuthToken(config) {
|
|
514
|
+
return normalizeResolvedSecretInputString({
|
|
515
|
+
value: config.twilio?.authToken,
|
|
516
|
+
path: TWILIO_AUTH_TOKEN_PATH
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function normalizeVoiceCallConfig(config) {
|
|
520
|
+
const defaults = cloneDefaultVoiceCallConfig();
|
|
521
|
+
const serve = {
|
|
522
|
+
...defaults.serve,
|
|
523
|
+
...config.serve
|
|
524
|
+
};
|
|
525
|
+
const streamingProvider = config.streaming?.provider;
|
|
526
|
+
const streamingProviders = sanitizeVoiceCallProviderConfigs(config.streaming?.providers ?? defaults.streaming.providers);
|
|
527
|
+
const realtimeProvider = config.realtime?.provider ?? defaults.realtime.provider;
|
|
528
|
+
const realtimeProviders = sanitizeVoiceCallProviderConfigs(config.realtime?.providers ?? defaults.realtime.providers);
|
|
529
|
+
const realtimeFastContext = {
|
|
530
|
+
...defaults.realtime.fastContext,
|
|
531
|
+
...config.realtime?.fastContext,
|
|
532
|
+
sources: config.realtime?.fastContext?.sources ?? defaults.realtime.fastContext.sources
|
|
533
|
+
};
|
|
534
|
+
const realtimeAgentContext = {
|
|
535
|
+
...defaults.realtime.agentContext,
|
|
536
|
+
...config.realtime?.agentContext,
|
|
537
|
+
files: config.realtime?.agentContext?.files ?? defaults.realtime.agentContext.files
|
|
538
|
+
};
|
|
539
|
+
return {
|
|
540
|
+
...defaults,
|
|
541
|
+
...config,
|
|
542
|
+
allowFrom: config.allowFrom ?? defaults.allowFrom,
|
|
543
|
+
numbers: sanitizeVoiceCallNumberRoutes(config.numbers ?? defaults.numbers),
|
|
544
|
+
outbound: {
|
|
545
|
+
...defaults.outbound,
|
|
546
|
+
...config.outbound
|
|
547
|
+
},
|
|
548
|
+
serve,
|
|
549
|
+
tailscale: {
|
|
550
|
+
...defaults.tailscale,
|
|
551
|
+
...config.tailscale
|
|
552
|
+
},
|
|
553
|
+
tunnel: {
|
|
554
|
+
...defaults.tunnel,
|
|
555
|
+
...config.tunnel
|
|
556
|
+
},
|
|
557
|
+
webhookSecurity: {
|
|
558
|
+
...defaults.webhookSecurity,
|
|
559
|
+
...config.webhookSecurity,
|
|
560
|
+
allowedHosts: config.webhookSecurity?.allowedHosts ?? defaults.webhookSecurity.allowedHosts,
|
|
561
|
+
trustedProxyIPs: config.webhookSecurity?.trustedProxyIPs ?? defaults.webhookSecurity.trustedProxyIPs
|
|
562
|
+
},
|
|
563
|
+
streaming: {
|
|
564
|
+
...defaults.streaming,
|
|
565
|
+
...config.streaming,
|
|
566
|
+
provider: streamingProvider,
|
|
567
|
+
providers: streamingProviders
|
|
568
|
+
},
|
|
569
|
+
realtime: {
|
|
570
|
+
...defaults.realtime,
|
|
571
|
+
...config.realtime,
|
|
572
|
+
provider: realtimeProvider,
|
|
573
|
+
streamPath: config.realtime?.streamPath ?? defaultRealtimeStreamPathForServePath(serve.path ?? defaults.serve.path),
|
|
574
|
+
tools: config.realtime?.tools ?? defaults.realtime.tools,
|
|
575
|
+
consultThinkingLevel: VoiceCallRealtimeConsultThinkingLevelSchema.optional().parse(config.realtime?.consultThinkingLevel ?? defaults.realtime.consultThinkingLevel),
|
|
576
|
+
consultFastMode: config.realtime?.consultFastMode ?? defaults.realtime.consultFastMode,
|
|
577
|
+
fastContext: realtimeFastContext,
|
|
578
|
+
agentContext: realtimeAgentContext,
|
|
579
|
+
providers: realtimeProviders
|
|
580
|
+
},
|
|
581
|
+
tts: normalizeVoiceCallTtsConfig(defaults.tts, config.tts)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function resolveVoiceCallSessionKey(params) {
|
|
585
|
+
const explicit = params.explicitSessionKey?.trim();
|
|
586
|
+
if (explicit) return resolveVoiceCallAgentSessionKey({
|
|
587
|
+
config: params.config,
|
|
588
|
+
sessionKey: explicit,
|
|
589
|
+
coreSession: params.coreSession
|
|
590
|
+
});
|
|
591
|
+
if (params.config.sessionScope === "main") return resolveVoiceCallAgentSessionKey({
|
|
592
|
+
config: params.config,
|
|
593
|
+
sessionKey: "main",
|
|
594
|
+
coreSession: params.coreSession
|
|
595
|
+
});
|
|
596
|
+
const prefix = `agent:${normalizeAgentId(params.config.agentId)}:voice`;
|
|
597
|
+
if (params.config.sessionScope === "per-call") return `${prefix}:call:${params.callId}`.toLowerCase();
|
|
598
|
+
const normalizedPhone = params.phone?.replace(/\D/g, "");
|
|
599
|
+
return (normalizedPhone ? `${prefix}:${normalizedPhone}` : `${prefix}:${params.callId}`).toLowerCase();
|
|
600
|
+
}
|
|
601
|
+
/** Resolve persisted or integration-provided keys into the configured agent namespace. */
|
|
602
|
+
function resolveVoiceCallAgentSessionKey(params) {
|
|
603
|
+
const sessionKey = params.sessionKey.trim();
|
|
604
|
+
if (!sessionKey) throw new Error("Voice Call session key cannot be empty");
|
|
605
|
+
const lower = sessionKey.toLowerCase();
|
|
606
|
+
const agentId = normalizeAgentId(params.config.agentId);
|
|
607
|
+
if (lower === "global" || lower === "unknown") return lower;
|
|
608
|
+
const parsedInput = parseAgentSessionKey(sessionKey);
|
|
609
|
+
let normalizedScopedKey;
|
|
610
|
+
if (parsedInput && normalizeAgentId(parsedInput.agentId) === parsedInput.agentId && parsedInput.agentId === agentId) normalizedScopedKey = `agent:${parsedInput.agentId}:${parsedInput.rest}`;
|
|
611
|
+
else {
|
|
612
|
+
const wrappedInput = parseAgentSessionKey(`agent:${agentId}:${sessionKey}`);
|
|
613
|
+
if (!wrappedInput) throw new Error("Voice Call session key could not be normalized");
|
|
614
|
+
normalizedScopedKey = `agent:${agentId}:${wrappedInput.rest}`;
|
|
615
|
+
}
|
|
616
|
+
const canonicalMain = canonicalizeMainSessionAlias({
|
|
617
|
+
cfg: { session: params.coreSession },
|
|
618
|
+
agentId,
|
|
619
|
+
sessionKey: normalizedScopedKey
|
|
620
|
+
});
|
|
621
|
+
return canonicalMain === normalizedScopedKey ? normalizedScopedKey : canonicalMain;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Resolves the configuration by merging environment variables into missing fields.
|
|
625
|
+
* Returns a new configuration object with environment variables applied.
|
|
626
|
+
*/
|
|
627
|
+
function resolveVoiceCallConfig(config) {
|
|
628
|
+
const resolved = normalizeVoiceCallConfig(config);
|
|
629
|
+
if (resolved.provider === "telnyx") {
|
|
630
|
+
resolved.telnyx = resolved.telnyx ?? {};
|
|
631
|
+
resolved.telnyx.apiKey = resolved.telnyx.apiKey ?? resolveSpeechProviderApiKey(process.env.TELNYX_API_KEY);
|
|
632
|
+
resolved.telnyx.connectionId = resolved.telnyx.connectionId ?? resolveSpeechProviderApiKey(process.env.TELNYX_CONNECTION_ID);
|
|
633
|
+
resolved.telnyx.publicKey = resolved.telnyx.publicKey ?? resolveSpeechProviderApiKey(process.env.TELNYX_PUBLIC_KEY);
|
|
634
|
+
}
|
|
635
|
+
if (resolved.provider === "twilio") {
|
|
636
|
+
resolved.fromNumber = resolved.fromNumber ?? resolveSpeechProviderApiKey(process.env.TWILIO_FROM_NUMBER);
|
|
637
|
+
resolved.twilio = resolved.twilio ?? {};
|
|
638
|
+
resolved.twilio.accountSid = resolved.twilio.accountSid ?? resolveSpeechProviderApiKey(process.env.TWILIO_ACCOUNT_SID);
|
|
639
|
+
resolved.twilio.authToken = resolved.twilio.authToken ?? resolveSpeechProviderApiKey(process.env.TWILIO_AUTH_TOKEN);
|
|
640
|
+
}
|
|
641
|
+
if (resolved.provider === "plivo") {
|
|
642
|
+
resolved.plivo = resolved.plivo ?? {};
|
|
643
|
+
resolved.plivo.authId = resolved.plivo.authId ?? resolveSpeechProviderApiKey(process.env.PLIVO_AUTH_ID);
|
|
644
|
+
resolved.plivo.authToken = resolved.plivo.authToken ?? resolveSpeechProviderApiKey(process.env.PLIVO_AUTH_TOKEN);
|
|
645
|
+
}
|
|
646
|
+
resolved.tunnel = resolved.tunnel ?? {
|
|
647
|
+
provider: "none",
|
|
648
|
+
allowNgrokFreeTierLoopbackBypass: false
|
|
649
|
+
};
|
|
650
|
+
resolved.tunnel.allowNgrokFreeTierLoopbackBypass = resolved.tunnel.allowNgrokFreeTierLoopbackBypass ?? false;
|
|
651
|
+
resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? resolveSpeechProviderApiKey(process.env.NGROK_AUTHTOKEN);
|
|
652
|
+
resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? resolveSpeechProviderApiKey(process.env.NGROK_DOMAIN);
|
|
653
|
+
resolved.webhookSecurity = resolved.webhookSecurity ?? {
|
|
654
|
+
allowedHosts: [],
|
|
655
|
+
trustForwardingHeaders: false,
|
|
656
|
+
trustedProxyIPs: []
|
|
657
|
+
};
|
|
658
|
+
resolved.webhookSecurity.allowedHosts = resolved.webhookSecurity.allowedHosts ?? [];
|
|
659
|
+
resolved.webhookSecurity.trustForwardingHeaders = resolved.webhookSecurity.trustForwardingHeaders ?? false;
|
|
660
|
+
resolved.webhookSecurity.trustedProxyIPs = resolved.webhookSecurity.trustedProxyIPs ?? [];
|
|
661
|
+
return normalizeVoiceCallConfig(resolved);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Validate that the configuration has all required fields for the selected provider.
|
|
665
|
+
*/
|
|
666
|
+
function validateProviderConfig(config) {
|
|
667
|
+
const errors = [];
|
|
668
|
+
if (!config.enabled) return {
|
|
669
|
+
valid: true,
|
|
670
|
+
errors: []
|
|
671
|
+
};
|
|
672
|
+
if (!config.provider) errors.push("plugins.entries.voice-call.config.provider is required");
|
|
673
|
+
if (!config.fromNumber && config.provider !== "mock") errors.push(config.provider === "twilio" ? "plugins.entries.voice-call.config.fromNumber is required (or set TWILIO_FROM_NUMBER env)" : "plugins.entries.voice-call.config.fromNumber is required");
|
|
674
|
+
if (config.provider === "telnyx") {
|
|
675
|
+
if (!config.telnyx?.apiKey) errors.push("plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)");
|
|
676
|
+
if (!config.telnyx?.connectionId) errors.push("plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)");
|
|
677
|
+
if (!config.skipSignatureVerification && !config.telnyx?.publicKey) errors.push("plugins.entries.voice-call.config.telnyx.publicKey is required (or set TELNYX_PUBLIC_KEY env)");
|
|
678
|
+
}
|
|
679
|
+
if (config.provider === "twilio") {
|
|
680
|
+
if (!config.twilio?.accountSid) errors.push("plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)");
|
|
681
|
+
if (!hasConfiguredSecretInput(config.twilio?.authToken)) errors.push("plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)");
|
|
682
|
+
}
|
|
683
|
+
if (config.provider === "plivo") {
|
|
684
|
+
if (!config.plivo?.authId) errors.push("plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)");
|
|
685
|
+
if (!config.plivo?.authToken) errors.push("plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)");
|
|
686
|
+
}
|
|
687
|
+
if (config.realtime.enabled && config.inboundPolicy === "disabled") errors.push("plugins.entries.voice-call.config.inboundPolicy must not be \"disabled\" when realtime.enabled is true");
|
|
688
|
+
if (config.realtime.enabled && config.streaming.enabled) errors.push("plugins.entries.voice-call.config.realtime.enabled and plugins.entries.voice-call.config.streaming.enabled cannot both be true");
|
|
689
|
+
if (config.streaming.enabled && config.provider && config.provider !== "twilio") errors.push("plugins.entries.voice-call.config.provider must be \"twilio\" when streaming.enabled is true");
|
|
690
|
+
if (config.realtime.enabled && config.provider && config.provider !== "twilio" && config.provider !== "telnyx" && config.provider !== "mock") errors.push("plugins.entries.voice-call.config.provider must be \"twilio\", \"telnyx\", or \"mock\" when realtime.enabled is true");
|
|
691
|
+
return {
|
|
692
|
+
valid: errors.length === 0,
|
|
693
|
+
errors
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
//#endregion
|
|
697
|
+
export { resolveVoiceCallEffectiveConfig as a, resolveVoiceCallSessionKey as c, REALTIME_VOICE_END_CALL_TOOL_NAME as d, resolveVoiceCallRealtimeTools as f, resolveVoiceCallConfig as i, resolveVoiceCallStreamExposurePaths as l, normalizeVoiceCallConfig as n, resolveVoiceCallNumberRouteKeyForCall as o, resolveTwilioAuthToken as r, resolveVoiceCallPublicPathPrefix as s, VoiceCallConfigSchema as t, validateProviderConfig as u };
|