mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix: resolve voice-call SecretRef inputs (#73632)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -109,6 +109,10 @@ function setup(config: Record<string, unknown>): Registered {
|
||||
return { methods, tools, service };
|
||||
}
|
||||
|
||||
function envRef(id: string) {
|
||||
return { source: "env" as const, provider: "default", id };
|
||||
}
|
||||
|
||||
async function registerVoiceCallCli(
|
||||
program: Command,
|
||||
pluginConfig: Record<string, unknown> = { provider: "mock" },
|
||||
@@ -275,6 +279,26 @@ describe("voice-call plugin", () => {
|
||||
expect(noopLogger.warn).toHaveBeenCalledWith(expect.stringContaining("TWILIO_ACCOUNT_SID"));
|
||||
});
|
||||
|
||||
it("registers Twilio configs with SecretRef auth tokens", async () => {
|
||||
const authToken = envRef("TWILIO_AUTH_TOKEN");
|
||||
const { service } = setup({
|
||||
enabled: true,
|
||||
provider: "twilio",
|
||||
fromNumber: "+15550001234",
|
||||
twilio: {
|
||||
accountSid: "AC123",
|
||||
authToken,
|
||||
},
|
||||
});
|
||||
|
||||
await service?.start(createServiceContext());
|
||||
|
||||
expect(createVoiceCallRuntime).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(createVoiceCallRuntime).mock.calls[0]?.[0]?.config.twilio?.authToken).toEqual(
|
||||
authToken,
|
||||
);
|
||||
});
|
||||
|
||||
it("still reports missing provider setup when a command needs the runtime", async () => {
|
||||
vi.stubEnv("TWILIO_ACCOUNT_SID", "");
|
||||
vi.stubEnv("TWILIO_AUTH_TOKEN", "");
|
||||
|
||||
@@ -727,6 +727,8 @@
|
||||
"secretInputs": {
|
||||
"paths": [
|
||||
{ "path": "twilio.authToken", "expected": "string" },
|
||||
{ "path": "realtime.providers.*.apiKey", "expected": "string" },
|
||||
{ "path": "streaming.providers.*.apiKey", "expected": "string" },
|
||||
{ "path": "tts.providers.*.apiKey", "expected": "string" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
VoiceCallConfigSchema,
|
||||
resolveTwilioAuthToken,
|
||||
validateProviderConfig,
|
||||
normalizeVoiceCallConfig,
|
||||
resolveVoiceCallConfig,
|
||||
@@ -11,6 +13,10 @@ function createBaseConfig(provider: "telnyx" | "twilio" | "plivo" | "mock"): Voi
|
||||
return createVoiceCallBaseConfig({ provider });
|
||||
}
|
||||
|
||||
function envRef(id: string) {
|
||||
return { source: "env" as const, provider: "default", id };
|
||||
}
|
||||
|
||||
function requireElevenLabsTtsConfig(config: Pick<VoiceCallConfig, "tts">) {
|
||||
const tts = config.tts;
|
||||
const elevenlabs = tts?.providers?.elevenlabs;
|
||||
@@ -80,6 +86,24 @@ describe("validateProviderConfig", () => {
|
||||
});
|
||||
|
||||
describe("twilio provider", () => {
|
||||
it("accepts SecretRef-backed auth tokens before runtime resolution", () => {
|
||||
const config = VoiceCallConfigSchema.parse({
|
||||
enabled: true,
|
||||
provider: "twilio",
|
||||
fromNumber: "+15550001234",
|
||||
twilio: {
|
||||
accountSid: "AC123",
|
||||
authToken: envRef("TWILIO_AUTH_TOKEN"),
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.twilio?.authToken).toEqual(envRef("TWILIO_AUTH_TOKEN"));
|
||||
expect(validateProviderConfig(config)).toMatchObject({ valid: true, errors: [] });
|
||||
expect(() => resolveTwilioAuthToken(config)).toThrow(
|
||||
'plugins.entries.voice-call.config.twilio.authToken: unresolved SecretRef "env:default:TWILIO_AUTH_TOKEN"',
|
||||
);
|
||||
});
|
||||
|
||||
it("passes validation with mixed config and env vars", () => {
|
||||
process.env.TWILIO_AUTH_TOKEN = "secret";
|
||||
let config = createBaseConfig("twilio");
|
||||
|
||||
@@ -2,6 +2,12 @@ import {
|
||||
REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES,
|
||||
type RealtimeVoiceAgentConsultToolPolicy,
|
||||
} from "openclaw/plugin-sdk/realtime-voice";
|
||||
import {
|
||||
buildSecretInputSchema,
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
type SecretInput,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
import { z } from "openclaw/plugin-sdk/zod";
|
||||
import { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema } from "../api.js";
|
||||
import { deepMergeDefined } from "./deep-merge.js";
|
||||
@@ -39,6 +45,8 @@ export type InboundPolicy = z.infer<typeof InboundPolicySchema>;
|
||||
// Provider-Specific Configuration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const SecretInputSchema = buildSecretInputSchema();
|
||||
|
||||
export const TelnyxConfigSchema = z
|
||||
.object({
|
||||
/** Telnyx API v2 key */
|
||||
@@ -56,10 +64,12 @@ export const TwilioConfigSchema = z
|
||||
/** Twilio Account SID */
|
||||
accountSid: z.string().min(1).optional(),
|
||||
/** Twilio Auth Token */
|
||||
authToken: z.string().min(1).optional(),
|
||||
authToken: SecretInputSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
export type TwilioConfig = z.infer<typeof TwilioConfigSchema>;
|
||||
export type TwilioConfig = Omit<z.infer<typeof TwilioConfigSchema>, "authToken"> & {
|
||||
authToken?: SecretInput;
|
||||
};
|
||||
|
||||
export const PlivoConfigSchema = z
|
||||
.object({
|
||||
@@ -393,13 +403,15 @@ export const VoiceCallConfigSchema = z
|
||||
.strict();
|
||||
|
||||
export type VoiceCallConfig = z.infer<typeof VoiceCallConfigSchema>;
|
||||
type DeepPartial<T> =
|
||||
T extends Array<infer U>
|
||||
type DeepPartial<T> = T extends SecretInput
|
||||
? T
|
||||
: T extends Array<infer U>
|
||||
? DeepPartial<U>[]
|
||||
: T extends object
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: T;
|
||||
export type VoiceCallConfigInput = DeepPartial<VoiceCallConfig>;
|
||||
const TWILIO_AUTH_TOKEN_PATH = "plugins.entries.voice-call.config.twilio.authToken";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Configuration Helpers
|
||||
@@ -458,6 +470,15 @@ function sanitizeVoiceCallProviderConfigs(
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTwilioAuthToken(
|
||||
config: Pick<VoiceCallConfig, "twilio">,
|
||||
): string | undefined {
|
||||
return normalizeResolvedSecretInputString({
|
||||
value: config.twilio?.authToken,
|
||||
path: TWILIO_AUTH_TOKEN_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeVoiceCallConfig(config: VoiceCallConfigInput): VoiceCallConfig {
|
||||
const defaults = cloneDefaultVoiceCallConfig();
|
||||
const serve = { ...defaults.serve, ...config.serve };
|
||||
@@ -608,7 +629,7 @@ export function validateProviderConfig(config: VoiceCallConfig): {
|
||||
"plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
|
||||
);
|
||||
}
|
||||
if (!config.twilio?.authToken) {
|
||||
if (!hasConfiguredSecretInput(config.twilio?.authToken)) {
|
||||
errors.push(
|
||||
"plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@ import crypto from "node:crypto";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { TwilioConfig } from "../config.js";
|
||||
import { getHeader } from "../http-headers.js";
|
||||
import type { MediaStreamHandler } from "../media-stream.js";
|
||||
import { chunkAudio } from "../telephony-audio.js";
|
||||
@@ -69,6 +68,11 @@ type StreamSendResult = {
|
||||
sent: boolean;
|
||||
};
|
||||
|
||||
type TwilioProviderConfig = {
|
||||
accountSid?: string;
|
||||
authToken?: string;
|
||||
};
|
||||
|
||||
export class TwilioProvider implements VoiceCallProvider {
|
||||
readonly name = "twilio" as const;
|
||||
|
||||
@@ -129,7 +133,7 @@ export class TwilioProvider implements VoiceCallProvider {
|
||||
this.streamAuthTokens.delete(providerCallId);
|
||||
}
|
||||
|
||||
constructor(config: TwilioConfig, options: TwilioProviderOptions = {}) {
|
||||
constructor(config: TwilioProviderConfig, options: TwilioProviderOptions = {}) {
|
||||
if (!config.accountSid) {
|
||||
throw new Error("Twilio Account SID is required");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createVoiceCallBaseConfig } from "./test-fixtures.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
resolveVoiceCallConfig: vi.fn(),
|
||||
resolveTwilioAuthToken: vi.fn(),
|
||||
validateProviderConfig: vi.fn(),
|
||||
managerInitialize: vi.fn(),
|
||||
managerGetCall: vi.fn(),
|
||||
@@ -26,6 +27,7 @@ const mocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("./config.js", () => ({
|
||||
resolveVoiceCallConfig: mocks.resolveVoiceCallConfig,
|
||||
resolveTwilioAuthToken: mocks.resolveTwilioAuthToken,
|
||||
validateProviderConfig: mocks.validateProviderConfig,
|
||||
}));
|
||||
|
||||
@@ -109,6 +111,9 @@ describe("createVoiceCallRuntime lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveVoiceCallConfig.mockImplementation((cfg: VoiceCallConfig) => cfg);
|
||||
mocks.resolveTwilioAuthToken.mockImplementation(
|
||||
(cfg: VoiceCallConfig) => cfg.twilio?.authToken,
|
||||
);
|
||||
mocks.validateProviderConfig.mockReturnValue({ valid: true, errors: [] });
|
||||
mocks.managerInitialize.mockResolvedValue(undefined);
|
||||
mocks.managerGetCall.mockReset();
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
type ResolvedRealtimeVoiceProvider,
|
||||
} from "openclaw/plugin-sdk/realtime-voice";
|
||||
import type { VoiceCallConfig } from "./config.js";
|
||||
import { resolveVoiceCallConfig, validateProviderConfig } from "./config.js";
|
||||
import {
|
||||
resolveTwilioAuthToken,
|
||||
resolveVoiceCallConfig,
|
||||
validateProviderConfig,
|
||||
} from "./config.js";
|
||||
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
|
||||
import { CallManager } from "./manager.js";
|
||||
import type { VoiceCallProvider } from "./providers/base.js";
|
||||
@@ -195,7 +199,7 @@ async function resolveProvider(config: VoiceCallConfig): Promise<VoiceCallProvid
|
||||
return new TwilioProvider(
|
||||
{
|
||||
accountSid: config.twilio?.accountSid,
|
||||
authToken: config.twilio?.authToken,
|
||||
authToken: resolveTwilioAuthToken(config),
|
||||
},
|
||||
{
|
||||
allowNgrokFreeTierLoopbackBypass,
|
||||
|
||||
Reference in New Issue
Block a user