@openclaw/voice-call 2026.9.7 → 2026.10.1-beta.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/.setup/{cli-D6uwMXyS.mjs → cli-Cn1leykM.mjs} +57 -37
- package/dist/.setup/{config-Ds7xoZbI.mjs → config-CX9J-053.mjs} +18 -119
- package/dist/.setup/{guarded-json-api-Bqc6QS8A.mjs → guarded-json-api-x55yqHvW.mjs} +39 -94
- package/dist/.setup/{mock-ClWW6Ufu.mjs → mock-eLv_Dc4o.mjs} +3 -6
- package/dist/.setup/{plivo-69ObrQPk.mjs → plivo-CpPIENu4.mjs} +48 -101
- package/dist/.setup/{realtime-handler-BgDPcPZt.mjs → realtime-handler-Do7Sz7Gh.mjs} +123 -235
- package/dist/.setup/{response-generator-BzKrJMEb.mjs → response-generator-DAhbXEta.mjs} +6 -25
- package/dist/.setup/{runtime-entry-BNOQRXii.mjs → runtime-entry-CbSKxSKk.mjs} +554 -425
- package/dist/.setup/state-retention-api-ONgqE-_V.mjs +201 -0
- package/dist/.setup/store-path-Cfwff6j7.mjs +33 -0
- package/dist/.setup/{telnyx-DjFGCfy1.mjs → telnyx-DPilutCH.mjs} +10 -29
- package/dist/.setup/{twilio-Bp3gfmUB.mjs → twilio-BWFNGyVx.mjs} +78 -201
- package/dist/.setup/{twilio-region-4fkgz3UG.mjs → twilio-region-wBOx-8sz.mjs} +1 -4
- package/dist/.setup/voice-mapping-B_iAUkER.mjs +31 -0
- package/dist/.setup/{webhook-replay-CCQSoBNF.mjs → webhook-replay-B7RJNWNG.mjs} +2 -5
- package/dist/doctor-contract-api.js +7 -330
- package/dist/index.js +18 -16
- package/dist/runtime-entry.js +1 -1
- package/dist/state-retention-api.js +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -6
- package/dist/.setup/utils-B5pnM_Ij.mjs +0 -437
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -22
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -530
- package/node_modules/ws/lib/receiver.js +0 -743
- package/node_modules/ws/lib/sender.js +0 -607
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -562
- package/node_modules/ws/lib/websocket.js +0 -1407
- package/node_modules/ws/package.json +0 -74
- package/node_modules/ws/wrapper.mjs +0 -21
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { sleep } from "../api.js";
|
|
2
|
-
import { l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { l as resolveVoiceCallStreamExposurePaths, u as validateProviderConfig } from "./config-CX9J-053.mjs";
|
|
3
|
+
import { _ as MAX_CALL_RECORD_EVENTS, b as loadActiveCallsFromStore, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoutes, h as resolveVoiceCallAgentId, l as cleanupTailscaleExposureRoute, u as getTailscaleSelfInfo, v as findCallInStore, x as setVoiceCallStateRuntime, y as getCallHistoryFromStore } from "./runtime-entry-CbSKxSKk.mjs";
|
|
4
|
+
import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Cfwff6j7.mjs";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { callGatewayFromCli, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
7
|
import { isRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
8
|
import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { createDeferred } from "openclaw/plugin-sdk/concurrency-runtime";
|
|
11
12
|
import { once } from "node:events";
|
|
12
13
|
import { format } from "node:util";
|
|
13
14
|
//#region extensions/voice-call/src/cli-command-io.ts
|
|
@@ -94,11 +95,6 @@ async function* readLogLines(file, last, pollMs) {
|
|
|
94
95
|
await sleep(pollMs);
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
function percentile(values, p) {
|
|
98
|
-
if (values.length === 0) return 0;
|
|
99
|
-
const sorted = [...values].toSorted((a, b) => a - b);
|
|
100
|
-
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1))] ?? 0;
|
|
101
|
-
}
|
|
102
98
|
function summarizeSeries(values) {
|
|
103
99
|
if (values.length === 0) return {
|
|
104
100
|
count: 0,
|
|
@@ -108,16 +104,16 @@ function summarizeSeries(values) {
|
|
|
108
104
|
p50Ms: 0,
|
|
109
105
|
p95Ms: 0
|
|
110
106
|
};
|
|
111
|
-
const
|
|
112
|
-
const
|
|
107
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
108
|
+
const percentile = (fraction) => sorted[Math.ceil(fraction * sorted.length) - 1] ?? 0;
|
|
113
109
|
const avgMs = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
114
110
|
return {
|
|
115
111
|
count: values.length,
|
|
116
|
-
minMs,
|
|
117
|
-
maxMs,
|
|
112
|
+
minMs: sorted[0] ?? 0,
|
|
113
|
+
maxMs: sorted[sorted.length - 1] ?? 0,
|
|
118
114
|
avgMs,
|
|
119
|
-
p50Ms: percentile(
|
|
120
|
-
p95Ms: percentile(
|
|
115
|
+
p50Ms: percentile(.5),
|
|
116
|
+
p95Ms: percentile(.95)
|
|
121
117
|
};
|
|
122
118
|
}
|
|
123
119
|
async function writeVoiceCallLatencySummary(calls) {
|
|
@@ -388,8 +384,40 @@ function writeSetupStatus(status) {
|
|
|
388
384
|
writeCliLine("Voice Call setup: %s", status.ok ? "OK" : "needs attention");
|
|
389
385
|
for (const check of status.checks) writeCliLine("%s %s: %s", check.ok ? "OK" : "FAIL", check.id, check.message);
|
|
390
386
|
}
|
|
387
|
+
async function runWithStandaloneRuntime(createRuntime, run) {
|
|
388
|
+
let runtime;
|
|
389
|
+
const interrupted = createDeferred();
|
|
390
|
+
let exitSignal;
|
|
391
|
+
const interrupt = (signal) => {
|
|
392
|
+
exitSignal ??= signal;
|
|
393
|
+
process.exitCode = exitSignal === "SIGINT" ? 130 : 143;
|
|
394
|
+
interrupted.resolve();
|
|
395
|
+
};
|
|
396
|
+
const onSigint = () => interrupt("SIGINT");
|
|
397
|
+
const onSigterm = () => interrupt("SIGTERM");
|
|
398
|
+
try {
|
|
399
|
+
await run(async () => {
|
|
400
|
+
if (!runtime) {
|
|
401
|
+
process.on("SIGINT", onSigint);
|
|
402
|
+
process.on("SIGTERM", onSigterm);
|
|
403
|
+
runtime = createRuntime();
|
|
404
|
+
}
|
|
405
|
+
const active = await runtime;
|
|
406
|
+
if (exitSignal) throw new Error(`Voice call command interrupted by ${exitSignal}`);
|
|
407
|
+
return active;
|
|
408
|
+
});
|
|
409
|
+
if (runtime) await interrupted.promise;
|
|
410
|
+
} finally {
|
|
411
|
+
try {
|
|
412
|
+
await (await runtime?.catch(() => void 0))?.stop();
|
|
413
|
+
} finally {
|
|
414
|
+
process.off("SIGINT", onSigint);
|
|
415
|
+
process.off("SIGTERM", onSigterm);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
391
419
|
function registerVoiceCallCli(params) {
|
|
392
|
-
const { program, config, coreConfig, ensureRuntime, stateRuntime } = params;
|
|
420
|
+
const { program, config, coreConfig, ensureRuntime: createRuntime, stateRuntime } = params;
|
|
393
421
|
const ensureHistoryStateRuntime = () => {
|
|
394
422
|
if (stateRuntime) setVoiceCallStateRuntime({ state: stateRuntime });
|
|
395
423
|
};
|
|
@@ -402,7 +430,7 @@ function registerVoiceCallCli(params) {
|
|
|
402
430
|
}
|
|
403
431
|
writeSetupStatus(status);
|
|
404
432
|
});
|
|
405
|
-
root.command("smoke").description("Check Voice Call readiness and optionally place a short outbound test call").option("-t, --to <phone>", "Phone number to call for a live smoke").option("--message <text>", "Message to speak during the smoke call", "OpenClaw voice call smoke test.").option("--mode <mode>", "Call mode: notify or conversation", "notify").option("--yes", "Actually place the live outbound call").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
433
|
+
root.command("smoke").description("Check Voice Call readiness and optionally place a short outbound test call").option("-t, --to <phone>", "Phone number to call for a live smoke").option("--message <text>", "Message to speak during the smoke call", "OpenClaw voice call smoke test.").option("--mode <mode>", "Call mode: notify or conversation", "notify").option("--yes", "Actually place the live outbound call").option("--json", "Print machine-readable JSON").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
|
|
406
434
|
const setup = buildSetupStatus(config, coreConfig);
|
|
407
435
|
if (!setup.ok) {
|
|
408
436
|
if (options.json) writeCliJson({
|
|
@@ -459,28 +487,20 @@ function registerVoiceCallCli(params) {
|
|
|
459
487
|
}
|
|
460
488
|
writeSetupStatus(setup);
|
|
461
489
|
writeCliLine("live-call: started %s", callId);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
writeCliJson({ callId: await initiateVoiceCall({
|
|
465
|
-
ensureRuntime,
|
|
466
|
-
config,
|
|
467
|
-
method: "voicecall.initiate",
|
|
468
|
-
to: options.to,
|
|
469
|
-
message: options.message,
|
|
470
|
-
mode: options.mode
|
|
471
|
-
}) });
|
|
472
|
-
});
|
|
473
|
-
root.command("start").description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(async (options) => {
|
|
490
|
+
}));
|
|
491
|
+
const callAction = (method) => async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
|
|
474
492
|
writeCliJson({ callId: await initiateVoiceCall({
|
|
475
493
|
ensureRuntime,
|
|
476
494
|
config,
|
|
477
|
-
method
|
|
495
|
+
method,
|
|
478
496
|
to: options.to,
|
|
479
497
|
message: options.message,
|
|
480
498
|
mode: options.mode
|
|
481
499
|
}) });
|
|
482
500
|
});
|
|
483
|
-
root.command("
|
|
501
|
+
root.command("call").description("Initiate an outbound voice call").requiredOption("-m, --message <text>", "Message to speak when call connects").option("-t, --to <phone>", "Phone number to call (E.164 format, uses config toNumber if not set)").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(callAction("voicecall.initiate"));
|
|
502
|
+
root.command("start").description("Alias for voicecall call").requiredOption("--to <phone>", "Phone number to call").option("--message <text>", "Message to speak when call connects").option("--mode <mode>", "Call mode: notify (hangup after message) or conversation (stay open)", "conversation").action(callAction("voicecall.start"));
|
|
503
|
+
root.command("continue").description("Speak a message and wait for a response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
|
|
484
504
|
const gatewayParams = {
|
|
485
505
|
callId: options.callId,
|
|
486
506
|
message: options.message
|
|
@@ -501,8 +521,8 @@ function registerVoiceCallCli(params) {
|
|
|
501
521
|
managerFallback: (manager) => manager.continueCall(options.callId, options.message),
|
|
502
522
|
failureLabel: "continue"
|
|
503
523
|
});
|
|
504
|
-
});
|
|
505
|
-
root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => {
|
|
524
|
+
}));
|
|
525
|
+
root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
|
|
506
526
|
await runGatewayManagerCommand({
|
|
507
527
|
config,
|
|
508
528
|
ensureRuntime,
|
|
@@ -513,8 +533,8 @@ function registerVoiceCallCli(params) {
|
|
|
513
533
|
managerFallback: (manager) => manager.speak(options.callId, options.message),
|
|
514
534
|
failureLabel: "speak"
|
|
515
535
|
});
|
|
516
|
-
});
|
|
517
|
-
root.command("dtmf").description("Send DTMF digits to an active call").requiredOption("--call-id <id>", "Call ID").requiredOption("--digits <digits>", "DTMF digits").action(async (options) => {
|
|
536
|
+
}));
|
|
537
|
+
root.command("dtmf").description("Send DTMF digits to an active call").requiredOption("--call-id <id>", "Call ID").requiredOption("--digits <digits>", "DTMF digits").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
|
|
518
538
|
await runGatewayManagerCommand({
|
|
519
539
|
config,
|
|
520
540
|
ensureRuntime,
|
|
@@ -525,8 +545,8 @@ function registerVoiceCallCli(params) {
|
|
|
525
545
|
managerFallback: (manager) => manager.sendDtmf(options.callId, options.digits),
|
|
526
546
|
failureLabel: "dtmf"
|
|
527
547
|
});
|
|
528
|
-
});
|
|
529
|
-
root.command("end").description("Hang up an active call").requiredOption("--call-id <id>", "Call ID").action(async (options) => {
|
|
548
|
+
}));
|
|
549
|
+
root.command("end").description("Hang up an active call").requiredOption("--call-id <id>", "Call ID").action(async (options) => runWithStandaloneRuntime(createRuntime, async (ensureRuntime) => {
|
|
530
550
|
await runGatewayManagerCommand({
|
|
531
551
|
config,
|
|
532
552
|
ensureRuntime,
|
|
@@ -534,7 +554,7 @@ function registerVoiceCallCli(params) {
|
|
|
534
554
|
managerFallback: (manager) => manager.endCall(options.callId),
|
|
535
555
|
failureLabel: "end"
|
|
536
556
|
});
|
|
537
|
-
});
|
|
557
|
+
}));
|
|
538
558
|
root.command("status").description("Show call status").option("--call-id <id>", "Call ID").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
539
559
|
const gateway = await callVoiceCallGateway("voicecall.status", options.callId ? { callId: options.callId } : void 0);
|
|
540
560
|
if (gateway.ok) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TtsConfigSchema } from "../api.js";
|
|
2
|
-
import { t as TWILIO_REGIONS } from "./twilio-region-
|
|
2
|
+
import { t as TWILIO_REGIONS } from "./twilio-region-wBOx-8sz.mjs";
|
|
3
3
|
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
4
4
|
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES, resolveRealtimeVoiceAgentConsultTools } from "openclaw/plugin-sdk/realtime-voice";
|
|
5
5
|
import { mergeDeep } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
@@ -10,18 +10,13 @@ import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { MAX_TCP_PORT } from "openclaw/plugin-sdk/number-runtime";
|
|
12
12
|
//#region extensions/voice-call/src/allowlist.ts
|
|
13
|
-
/** Normalize a phone number to digits only. */
|
|
14
13
|
function normalizePhoneNumber(input) {
|
|
15
|
-
|
|
16
|
-
return input.replace(/\D/g, "");
|
|
14
|
+
return input?.replace(/\D/g, "") ?? "";
|
|
17
15
|
}
|
|
18
16
|
/** Return true when the normalized caller exactly matches an allowlist entry. */
|
|
19
17
|
function isAllowlistedCaller(normalizedFrom, allowFrom) {
|
|
20
18
|
if (!normalizedFrom) return false;
|
|
21
|
-
return (allowFrom ?? []).some((num) =>
|
|
22
|
-
const normalizedAllow = normalizePhoneNumber(num);
|
|
23
|
-
return normalizedAllow !== "" && normalizedAllow === normalizedFrom;
|
|
24
|
-
});
|
|
19
|
+
return (allowFrom ?? []).some((num) => normalizePhoneNumber(num) === normalizedFrom);
|
|
25
20
|
}
|
|
26
21
|
//#endregion
|
|
27
22
|
//#region extensions/voice-call/src/realtime-call-control.ts
|
|
@@ -71,18 +66,7 @@ function isTailscalePortAllowed(config) {
|
|
|
71
66
|
}
|
|
72
67
|
//#endregion
|
|
73
68
|
//#region extensions/voice-call/src/config.ts
|
|
74
|
-
/**
|
|
75
|
-
* E.164 phone number format: +[country code][number]
|
|
76
|
-
* Examples use 555 prefix (reserved for fictional numbers)
|
|
77
|
-
*/
|
|
78
69
|
const E164Schema = z.string().regex(/^\+[1-9]\d{1,14}$/, "Expected E.164 format, e.g. +15550001234");
|
|
79
|
-
/**
|
|
80
|
-
* Controls how inbound calls are handled:
|
|
81
|
-
* - "disabled": Block all inbound calls (outbound only)
|
|
82
|
-
* - "allowlist": Only accept calls from numbers in allowFrom
|
|
83
|
-
* - "pairing": Unknown callers can request pairing (future)
|
|
84
|
-
* - "open": Accept all inbound calls (dangerous!)
|
|
85
|
-
*/
|
|
86
70
|
const InboundPolicySchema = z.enum([
|
|
87
71
|
"disabled",
|
|
88
72
|
"allowlist",
|
|
@@ -91,47 +75,33 @@ const InboundPolicySchema = z.enum([
|
|
|
91
75
|
]);
|
|
92
76
|
const SecretInputSchema = buildSecretInputSchema();
|
|
93
77
|
const TelnyxConfigSchema = z.object({
|
|
94
|
-
/** Telnyx API v2 key */
|
|
95
78
|
apiKey: z.string().min(1).optional(),
|
|
96
|
-
/** Telnyx connection ID (from Call Control app) */
|
|
97
79
|
connectionId: z.string().min(1).optional(),
|
|
98
80
|
/** Public key for webhook signature verification */
|
|
99
81
|
publicKey: z.string().min(1).optional()
|
|
100
82
|
}).strict();
|
|
101
83
|
const TwilioConfigSchema = z.object({
|
|
102
|
-
/** Twilio Account SID */
|
|
103
84
|
accountSid: z.string().min(1).optional(),
|
|
104
|
-
/** Twilio Auth Token */
|
|
105
85
|
authToken: SecretInputSchema.optional(),
|
|
106
86
|
/** Twilio processing Region (for example, ie1) */
|
|
107
87
|
region: z.enum(TWILIO_REGIONS).optional()
|
|
108
88
|
}).strict();
|
|
109
89
|
const PlivoConfigSchema = z.object({
|
|
110
|
-
/** Plivo Auth ID (starts with MA/SA) */
|
|
111
90
|
authId: z.string().min(1).optional(),
|
|
112
|
-
/** Plivo Auth Token */
|
|
113
91
|
authToken: z.string().min(1).optional()
|
|
114
92
|
}).strict();
|
|
115
93
|
const VoiceCallNumberRouteConfigSchema = z.object({
|
|
116
|
-
/** Greeting message for inbound calls to this number. */
|
|
117
94
|
inboundGreeting: z.string().optional(),
|
|
118
95
|
/** TTS override for inbound calls to this number. Deep-merges with global voice-call TTS. */
|
|
119
96
|
tts: TtsConfigSchema,
|
|
120
|
-
/** Agent ID to use for voice response generation for this number. */
|
|
121
97
|
agentId: z.string().min(1).optional(),
|
|
122
|
-
/** Optional model override for voice responses for this number. */
|
|
123
98
|
responseModel: z.string().optional(),
|
|
124
|
-
/** System prompt for voice responses for this number. */
|
|
125
99
|
responseSystemPrompt: z.string().optional(),
|
|
126
|
-
/** Timeout for response generation in ms for this number. */
|
|
127
100
|
responseTimeoutMs: z.number().int().positive().optional()
|
|
128
101
|
}).strict();
|
|
129
102
|
const VoiceCallServeConfigSchema = z.object({
|
|
130
|
-
/** Port to listen on */
|
|
131
103
|
port: z.number().int().positive().default(3334),
|
|
132
|
-
/** Bind address */
|
|
133
104
|
bind: z.string().default("127.0.0.1"),
|
|
134
|
-
/** Webhook path */
|
|
135
105
|
path: z.string().min(1).default("/voice/webhook")
|
|
136
106
|
}).strict().default({
|
|
137
107
|
port: 3334,
|
|
@@ -139,13 +109,6 @@ const VoiceCallServeConfigSchema = z.object({
|
|
|
139
109
|
path: "/voice/webhook"
|
|
140
110
|
});
|
|
141
111
|
const VoiceCallTunnelConfigSchema = z.object({
|
|
142
|
-
/**
|
|
143
|
-
* Tunnel provider:
|
|
144
|
-
* - "none": No tunnel (use publicUrl if set, or manual setup)
|
|
145
|
-
* - "ngrok": Use ngrok for public HTTPS tunnel
|
|
146
|
-
* - "tailscale-serve": Tailscale serve (private to tailnet)
|
|
147
|
-
* - "tailscale-funnel": Tailscale funnel (public HTTPS)
|
|
148
|
-
*/
|
|
149
112
|
provider: z.enum([
|
|
150
113
|
"none",
|
|
151
114
|
"ngrok",
|
|
@@ -156,23 +119,14 @@ const VoiceCallTunnelConfigSchema = z.object({
|
|
|
156
119
|
ngrokAuthToken: z.string().min(1).optional(),
|
|
157
120
|
/** ngrok custom domain (paid feature, e.g., "myapp.ngrok.io") */
|
|
158
121
|
ngrokDomain: z.string().min(1).optional(),
|
|
159
|
-
/**
|
|
160
|
-
* Allow ngrok free tier compatibility mode.
|
|
161
|
-
* When true, forwarded headers may be trusted for loopback requests
|
|
162
|
-
* to reconstruct the public ngrok URL used for signing.
|
|
163
|
-
*
|
|
164
|
-
* IMPORTANT: This does NOT bypass signature verification.
|
|
165
|
-
*/
|
|
122
|
+
/** Trust loopback forwarding for ngrok URL reconstruction; signatures remain mandatory. */
|
|
166
123
|
allowNgrokFreeTierLoopbackBypass: z.boolean().default(false)
|
|
167
124
|
}).strict().default({
|
|
168
125
|
provider: "none",
|
|
169
126
|
allowNgrokFreeTierLoopbackBypass: false
|
|
170
127
|
});
|
|
171
128
|
const VoiceCallWebhookSecurityConfigSchema = z.object({
|
|
172
|
-
/**
|
|
173
|
-
* Allowed hostnames for webhook URL reconstruction.
|
|
174
|
-
* Only these hosts are accepted from forwarding headers.
|
|
175
|
-
*/
|
|
129
|
+
/** Only these hosts are accepted from forwarding headers. */
|
|
176
130
|
allowedHosts: z.array(z.string().min(1)).default([]),
|
|
177
131
|
/**
|
|
178
132
|
* Trust X-Forwarded-* headers without a hostname allowlist.
|
|
@@ -189,11 +143,6 @@ const VoiceCallWebhookSecurityConfigSchema = z.object({
|
|
|
189
143
|
trustForwardingHeaders: false,
|
|
190
144
|
trustedProxyIPs: []
|
|
191
145
|
});
|
|
192
|
-
/**
|
|
193
|
-
* Call mode determines how outbound calls behave:
|
|
194
|
-
* - "notify": Deliver message and auto-hangup after delay (one-way notification)
|
|
195
|
-
* - "conversation": Stay open for back-and-forth until explicit end or timeout
|
|
196
|
-
*/
|
|
197
146
|
const CallModeSchema = z.enum(["notify", "conversation"]);
|
|
198
147
|
const VoiceCallSessionScopeSchema = z.enum([
|
|
199
148
|
"per-phone",
|
|
@@ -201,7 +150,6 @@ const VoiceCallSessionScopeSchema = z.enum([
|
|
|
201
150
|
"main"
|
|
202
151
|
]);
|
|
203
152
|
const OutboundConfigSchema = z.object({
|
|
204
|
-
/** Default call mode for outbound calls */
|
|
205
153
|
defaultMode: CallModeSchema.default("notify"),
|
|
206
154
|
/** Seconds to wait after TTS before auto-hangup in notify mode */
|
|
207
155
|
notifyHangupDelaySec: z.number().int().nonnegative().default(3)
|
|
@@ -250,7 +198,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z.object({
|
|
|
250
198
|
enabled: z.boolean().default(false),
|
|
251
199
|
/** Maximum number of characters in the generated profile-file block. */
|
|
252
200
|
maxChars: z.number().int().positive().default(6e3),
|
|
253
|
-
/** Include configured agent identity fields. */
|
|
254
201
|
includeIdentity: z.boolean().default(true),
|
|
255
202
|
/** Include selected workspace files such as SOUL.md and IDENTITY.md. */
|
|
256
203
|
includeWorkspaceFiles: z.boolean().default(true),
|
|
@@ -283,7 +230,6 @@ const VoiceCallRealtimeConsultThinkingLevelSchema = z.enum([
|
|
|
283
230
|
"ultra"
|
|
284
231
|
]);
|
|
285
232
|
const VoiceCallRealtimeConfigSchema = z.object({
|
|
286
|
-
/** Enable realtime voice-to-voice mode. */
|
|
287
233
|
enabled: z.boolean().default(false),
|
|
288
234
|
/** Provider id from registered realtime voice providers. */
|
|
289
235
|
provider: z.string().min(1).optional(),
|
|
@@ -299,7 +245,6 @@ const VoiceCallRealtimeConfigSchema = z.object({
|
|
|
299
245
|
consultThinkingLevel: VoiceCallRealtimeConsultThinkingLevelSchema.optional(),
|
|
300
246
|
/** Optional fast mode override for the regular agent behind realtime consults. */
|
|
301
247
|
consultFastMode: z.boolean().optional(),
|
|
302
|
-
/** Tool definitions exposed to the realtime provider. */
|
|
303
248
|
tools: z.array(RealtimeToolSchema).default([]),
|
|
304
249
|
/** Low-latency memory/session context for the consult tool. */
|
|
305
250
|
fastContext: VoiceCallRealtimeFastContextConfigSchema,
|
|
@@ -313,24 +258,8 @@ const VoiceCallRealtimeConfigSchema = z.object({
|
|
|
313
258
|
toolPolicy: "safe-read-only",
|
|
314
259
|
consultPolicy: "auto",
|
|
315
260
|
tools: [],
|
|
316
|
-
fastContext: {
|
|
317
|
-
|
|
318
|
-
timeoutMs: 800,
|
|
319
|
-
maxResults: 3,
|
|
320
|
-
sources: ["memory", "sessions"],
|
|
321
|
-
fallbackToConsult: false
|
|
322
|
-
},
|
|
323
|
-
agentContext: {
|
|
324
|
-
enabled: false,
|
|
325
|
-
maxChars: 6e3,
|
|
326
|
-
includeIdentity: true,
|
|
327
|
-
includeWorkspaceFiles: true,
|
|
328
|
-
files: [
|
|
329
|
-
"SOUL.md",
|
|
330
|
-
"IDENTITY.md",
|
|
331
|
-
"USER.md"
|
|
332
|
-
]
|
|
333
|
-
},
|
|
261
|
+
fastContext: VoiceCallRealtimeFastContextConfigSchema.parse({}),
|
|
262
|
+
agentContext: VoiceCallRealtimeAgentContextConfigSchema.parse({}),
|
|
334
263
|
providers: {}
|
|
335
264
|
});
|
|
336
265
|
const VoiceCallStreamingConfigSchema = z.object({
|
|
@@ -338,7 +267,6 @@ const VoiceCallStreamingConfigSchema = z.object({
|
|
|
338
267
|
enabled: z.boolean().default(false),
|
|
339
268
|
/** Provider id from registered realtime transcription providers. */
|
|
340
269
|
provider: z.string().min(1).optional(),
|
|
341
|
-
/** WebSocket path for media stream connections */
|
|
342
270
|
streamPath: z.string().min(1).default("/voice/stream"),
|
|
343
271
|
/** Provider-owned raw config blobs keyed by provider id. */
|
|
344
272
|
providers: VoiceCallProvidersConfigSchema,
|
|
@@ -363,36 +291,25 @@ const VoiceCallStreamingConfigSchema = z.object({
|
|
|
363
291
|
maxConnections: 128
|
|
364
292
|
});
|
|
365
293
|
const VoiceCallConfigSchema = z.object({
|
|
366
|
-
/** Enable voice call functionality */
|
|
367
294
|
enabled: z.boolean().default(false),
|
|
368
|
-
/** Active provider (telnyx, twilio, plivo, or mock) */
|
|
369
295
|
provider: z.enum([
|
|
370
296
|
"telnyx",
|
|
371
297
|
"twilio",
|
|
372
298
|
"plivo",
|
|
373
299
|
"mock"
|
|
374
300
|
]).optional(),
|
|
375
|
-
/** Telnyx-specific configuration */
|
|
376
301
|
telnyx: TelnyxConfigSchema.optional(),
|
|
377
|
-
/** Twilio-specific configuration */
|
|
378
302
|
twilio: TwilioConfigSchema.optional(),
|
|
379
|
-
/** Plivo-specific configuration */
|
|
380
303
|
plivo: PlivoConfigSchema.optional(),
|
|
381
|
-
/** Phone number to call from (E.164) */
|
|
382
304
|
fromNumber: E164Schema.optional(),
|
|
383
|
-
/** Default
|
|
305
|
+
/** Default outbound target. */
|
|
384
306
|
toNumber: E164Schema.optional(),
|
|
385
|
-
/** Inbound call policy */
|
|
386
307
|
inboundPolicy: InboundPolicySchema.default("disabled"),
|
|
387
|
-
/** Allowlist of phone numbers for inbound calls (E.164) */
|
|
388
308
|
allowFrom: z.array(E164Schema).default([]),
|
|
389
|
-
/** Greeting message for inbound calls */
|
|
390
309
|
inboundGreeting: z.string().optional(),
|
|
391
310
|
/** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */
|
|
392
311
|
numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}),
|
|
393
|
-
/** Outbound call configuration */
|
|
394
312
|
outbound: OutboundConfigSchema,
|
|
395
|
-
/** Maximum call duration in seconds */
|
|
396
313
|
maxDurationSeconds: z.number().int().positive().default(300),
|
|
397
314
|
/**
|
|
398
315
|
* Maximum age of a call in seconds before it is automatically reaped.
|
|
@@ -402,23 +319,15 @@ const VoiceCallConfigSchema = z.object({
|
|
|
402
319
|
staleCallReaperSeconds: z.number().int().nonnegative().default(120),
|
|
403
320
|
/** Silence timeout for end-of-speech detection (ms) */
|
|
404
321
|
silenceTimeoutMs: z.number().int().positive().default(800),
|
|
405
|
-
/** Timeout for user transcript (ms) */
|
|
406
322
|
transcriptTimeoutMs: z.number().int().positive().default(18e4),
|
|
407
|
-
/** Ring timeout for outbound calls (ms) */
|
|
408
323
|
ringTimeoutMs: z.number().int().positive().default(3e4),
|
|
409
|
-
/** Maximum concurrent calls */
|
|
410
324
|
maxConcurrentCalls: z.number().int().positive().default(1),
|
|
411
|
-
/** Webhook server configuration */
|
|
412
325
|
serve: VoiceCallServeConfigSchema,
|
|
413
326
|
/** @deprecated Prefer tunnel config. */
|
|
414
327
|
tailscale: VoiceCallTailscaleConfigSchema,
|
|
415
|
-
/** Tunnel configuration (unified ngrok/tailscale) */
|
|
416
328
|
tunnel: VoiceCallTunnelConfigSchema,
|
|
417
|
-
/** Webhook signature reconstruction and proxy trust configuration */
|
|
418
329
|
webhookSecurity: VoiceCallWebhookSecurityConfigSchema,
|
|
419
|
-
/** Real-time audio streaming configuration */
|
|
420
330
|
streaming: VoiceCallStreamingConfigSchema,
|
|
421
|
-
/** Realtime voice-to-voice configuration */
|
|
422
331
|
realtime: VoiceCallRealtimeConfigSchema,
|
|
423
332
|
/** Session memory scope for voice conversations. */
|
|
424
333
|
sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
|
|
@@ -428,15 +337,11 @@ const VoiceCallConfigSchema = z.object({
|
|
|
428
337
|
skipSignatureVerification: z.boolean().default(false),
|
|
429
338
|
/** TTS override (deep-merges with core tts) */
|
|
430
339
|
tts: TtsConfigSchema,
|
|
431
|
-
/** Store path for call logs */
|
|
432
340
|
store: z.string().optional(),
|
|
433
341
|
/** Response/session owner. Required when multiple agents have no legacy owner. */
|
|
434
342
|
agentId: z.string().min(1).optional(),
|
|
435
|
-
/** Optional model override for generating voice responses. */
|
|
436
343
|
responseModel: z.string().optional(),
|
|
437
|
-
/** System prompt for voice responses */
|
|
438
344
|
responseSystemPrompt: z.string().optional(),
|
|
439
|
-
/** Timeout for response generation in ms (default 30s) */
|
|
440
345
|
responseTimeoutMs: z.number().int().positive().default(3e4)
|
|
441
346
|
}).strict().refine(isTailscalePortAllowed, {
|
|
442
347
|
path: ["tailscale", "port"],
|
|
@@ -626,10 +531,6 @@ function resolveVoiceCallAgentSessionKey(params) {
|
|
|
626
531
|
sessionKey: normalizedScopedKey
|
|
627
532
|
});
|
|
628
533
|
}
|
|
629
|
-
/**
|
|
630
|
-
* Resolves the configuration by merging environment variables into missing fields.
|
|
631
|
-
* Returns a new configuration object with environment variables applied.
|
|
632
|
-
*/
|
|
633
534
|
function resolveVoiceCallConfig(config) {
|
|
634
535
|
const resolved = normalizeVoiceCallConfig(config);
|
|
635
536
|
if (resolved.provider === "telnyx") {
|
|
@@ -652,14 +553,9 @@ function resolveVoiceCallConfig(config) {
|
|
|
652
553
|
resolved.tunnel.allowNgrokFreeTierLoopbackBypass = resolved.tunnel.allowNgrokFreeTierLoopbackBypass ?? false;
|
|
653
554
|
resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? resolveSpeechProviderApiKey(process.env.NGROK_AUTHTOKEN);
|
|
654
555
|
resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? resolveSpeechProviderApiKey(process.env.NGROK_DOMAIN);
|
|
655
|
-
resolved.webhookSecurity.allowedHosts = resolved.webhookSecurity.allowedHosts ?? [];
|
|
656
556
|
resolved.webhookSecurity.trustForwardingHeaders = resolved.webhookSecurity.trustForwardingHeaders ?? false;
|
|
657
|
-
resolved.webhookSecurity.trustedProxyIPs = resolved.webhookSecurity.trustedProxyIPs ?? [];
|
|
658
557
|
return normalizeVoiceCallConfig(resolved);
|
|
659
558
|
}
|
|
660
|
-
/**
|
|
661
|
-
* Validate that the configuration has all required fields for the selected provider.
|
|
662
|
-
*/
|
|
663
559
|
function validateProviderConfig(config) {
|
|
664
560
|
const errors = [];
|
|
665
561
|
if (!config.enabled) return {
|
|
@@ -668,18 +564,21 @@ function validateProviderConfig(config) {
|
|
|
668
564
|
};
|
|
669
565
|
if (!config.provider) errors.push("plugins.entries.voice-call.config.provider is required");
|
|
670
566
|
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");
|
|
567
|
+
const requireCredential = (field, value, envName) => {
|
|
568
|
+
if (!value) errors.push(`plugins.entries.voice-call.config.${config.provider}.${field} is required (or set ${envName} env)`);
|
|
569
|
+
};
|
|
671
570
|
if (config.provider === "telnyx") {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
if (!config.skipSignatureVerification
|
|
571
|
+
requireCredential("apiKey", config.telnyx?.apiKey, "TELNYX_API_KEY");
|
|
572
|
+
requireCredential("connectionId", config.telnyx?.connectionId, "TELNYX_CONNECTION_ID");
|
|
573
|
+
if (!config.skipSignatureVerification) requireCredential("publicKey", config.telnyx?.publicKey, "TELNYX_PUBLIC_KEY");
|
|
675
574
|
}
|
|
676
575
|
if (config.provider === "twilio") {
|
|
677
|
-
|
|
678
|
-
|
|
576
|
+
requireCredential("accountSid", config.twilio?.accountSid, "TWILIO_ACCOUNT_SID");
|
|
577
|
+
requireCredential("authToken", hasConfiguredSecretInput(config.twilio?.authToken), "TWILIO_AUTH_TOKEN");
|
|
679
578
|
}
|
|
680
579
|
if (config.provider === "plivo") {
|
|
681
|
-
|
|
682
|
-
|
|
580
|
+
requireCredential("authId", config.plivo?.authId, "PLIVO_AUTH_ID");
|
|
581
|
+
requireCredential("authToken", config.plivo?.authToken, "PLIVO_AUTH_TOKEN");
|
|
683
582
|
}
|
|
684
583
|
if (config.realtime.enabled && config.inboundPolicy === "disabled") errors.push("plugins.entries.voice-call.config.inboundPolicy must not be \"disabled\" when realtime.enabled is true");
|
|
685
584
|
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");
|